diff --git a/.github/actions/cloud-sql-rollout-lease/storage-lease.test.mjs b/.github/actions/cloud-sql-rollout-lease/storage-lease.test.mjs index 7463c8ee0b6..f293ebcc6d6 100644 --- a/.github/actions/cloud-sql-rollout-lease/storage-lease.test.mjs +++ b/.github/actions/cloud-sql-rollout-lease/storage-lease.test.mjs @@ -1,28 +1,16 @@ import assert from 'node:assert/strict' import { test } from 'node:test' -import { - CloudSqlRolloutLease, - LEASE_TTL_MS, - LeaseConflict, - LeaseUnreadable -} from './storage-lease.mjs' +import { CloudSqlRolloutLease, LeaseConflict, LeaseUnreadable } from './storage-lease.mjs' const BUCKET = 'onorca-cloud-terraform-state' const OBJECT = 'terraform/state/cloud-sql-rollout/production.lock' const NOW = 1_756_000_000_000 -/** Model real GCS generation increments, ifGenerationMatch and 412 conflicts; faults inject failures. */ -function fakeStorage({ object = null, faults = [] } = {}) { - const state = { object, requests: [] } +function fakeStorage({ object = null } = {}) { + const state = { object } const fetcher = async (rawUrl, init = {}) => { const url = new URL(rawUrl) const method = init.method ?? 'GET' - const record = { method, url, path: url.pathname, search: url.searchParams } - state.requests.push(record) - const fault = faults.find((candidate) => candidate.when(record)) - if (fault) { - return json(fault.status, fault.body ?? {}) - } if (url.pathname.startsWith('/upload/')) { const want = url.searchParams.get('ifGenerationMatch') @@ -63,15 +51,10 @@ function json(status, body) { } } -function storedRecord({ - holderKey, - expiresAt, - repository = 'stablyai/orca', - workflow = 'Deploy Relay Production' -}) { +function storedRecord({ holderKey, expiresAt }) { return { - repository, - workflow, + repository: 'stablyai/orca', + workflow: 'Deploy Relay Production', run_id: '9001', run_url: 'https://github.com/stablyai/orca/actions/runs/9001', run_attempt: '1', @@ -81,14 +64,14 @@ function storedRecord({ } } -function leaseFor(storage, { warn = () => {} } = {}) { +function leaseFor(storage) { return new CloudSqlRolloutLease({ bucket: BUCKET, objectName: OBJECT, accessToken: 'test-token', fetcher: storage.fetcher, now: () => NOW, - warn + warn: () => {} }) } @@ -114,77 +97,24 @@ test('refuses a live lease held by another run and never writes', async () => { .catch((thrown) => thrown) assert.ok(error instanceof LeaseConflict) - assert.equal(error.holder.repository, 'stablyai/orca') - assert.equal(error.holder.run_url, 'https://github.com/stablyai/orca/actions/runs/9001') - assert.equal( - storage.state.requests.filter((request) => request.method !== 'GET').length, - 0, - 'a foreign live lease must not be written' - ) assert.equal(storage.state.object.generation, '1500') }) -test('re-enters a live lease this run already holds and extends it', async () => { - const storage = fakeStorage({ - object: { - generation: '1500', - body: storedRecord({ holderKey: HOLDER.holderKey, expiresAt: NOW + 60_000 }) - } - }) - const warnings = [] - const claim = await leaseFor(storage, { warn: (message) => warnings.push(message) }).acquire( - HOLDER - ) - - assert.equal(claim.state, 'reentrant') - assert.deepEqual(warnings, [], 're-entering our own lease is not a takeover') - assert.equal(claim.record.acquired_at, NOW - 60_000, 'original acquisition time is preserved') - assert.equal(claim.record.expires_at, NOW + LEASE_TTL_MS) - const upload = storage.state.requests.find((request) => request.path.startsWith('/upload/')) - assert.equal(upload.search.get('ifGenerationMatch'), '1500') - assert.equal(storage.state.object.generation, '1501') -}) - -test('takes over an expired lease and warns naming the stale holder', async () => { +test('takes over an expired lease', async () => { const storage = fakeStorage({ object: { generation: '1500', body: storedRecord({ holderKey: 'stablyai/orca/9001', - expiresAt: NOW - 1, - repository: 'stablyai/orca', - workflow: 'Deploy Relay Production Capacity' + expiresAt: NOW - 1 }) } }) - const warnings = [] - const claim = await leaseFor(storage, { warn: (message) => warnings.push(message) }).acquire( - HOLDER - ) + await leaseFor(storage).acquire(HOLDER) - assert.equal(claim.state, 'takeover') - assert.equal(warnings.length, 1) - assert.match(warnings[0], /stablyai\/orca/) - assert.match(warnings[0], /Deploy Relay Production Capacity/) - assert.match(warnings[0], /actions\/runs\/9001/) - const upload = storage.state.requests.find((request) => request.path.startsWith('/upload/')) - assert.equal(upload.search.get('ifGenerationMatch'), '1500') assert.equal(storage.state.object.body.holder_key, HOLDER.holderKey) }) -test('releases with a generation match and leaves the object gone', async () => { - const storage = fakeStorage() - const lease = leaseFor(storage) - const claim = await lease.acquire(HOLDER) - - const released = await lease.release(HOLDER.holderKey) - - assert.deepEqual(released, { released: true, generation: claim.generation }) - const remove = storage.state.requests.find((request) => request.method === 'DELETE') - assert.equal(remove.search.get('ifGenerationMatch'), claim.generation) - assert.equal(storage.state.object, null) -}) - test('refuses to release a lease another run now holds', async () => { const storage = fakeStorage({ object: { @@ -212,17 +142,3 @@ test('treats an unreadable record as held, not free', async () => { assert.ok(error instanceof LeaseUnreadable) assert.equal(storage.state.object.generation, '1500') }) - -test('renewal rewrites only expires_at on the observed generation', async () => { - const storage = fakeStorage() - const lease = leaseFor(storage) - await lease.acquire(HOLDER) - storage.state.object.body.expires_at = NOW - 1 - - const renewed = await lease.renew(HOLDER.holderKey) - - assert.equal(renewed.renewed, true) - assert.equal(storage.state.object.body.expires_at, NOW + LEASE_TTL_MS) - assert.equal(storage.state.object.body.acquired_at, NOW) - assert.equal(storage.state.object.body.holder_key, HOLDER.holderKey) -}) diff --git a/.github/workflows/computer-e2e.yml b/.github/workflows/computer-e2e.yml index 60e15f4794c..f26cfe41eec 100644 --- a/.github/workflows/computer-e2e.yml +++ b/.github/workflows/computer-e2e.yml @@ -53,7 +53,6 @@ on: - 'src/shared/computer-use-*.ts' - 'tests/e2e/computer-linux.e2e.ts' - 'tests/e2e/computer-windows.e2e.ts' - - 'tests/e2e/computer-windows-store.e2e.ts' - 'tests/e2e/computer-windows-background.e2e.ts' - 'tests/e2e/helpers/windows-semantic-controls-fixture.*' - 'tests/e2e/helpers/computer-cli-driver.ts' @@ -153,4 +152,4 @@ jobs: - run: pnpm verify:computer-native - run: pnpm build:cli - run: pnpm run build:electron-vite:parallel - - run: pnpm test:e2e:computer --reporter=verbose tests/e2e/computer-windows.e2e.ts tests/e2e/computer-windows-store.e2e.ts tests/e2e/computer-windows-background.e2e.ts + - run: pnpm test:e2e:computer --reporter=verbose tests/e2e/computer-windows.e2e.ts tests/e2e/computer-windows-background.e2e.ts diff --git a/cloud/apps/push/src/delivery/durable-push-claim.test.ts b/cloud/apps/push/src/delivery/durable-push-claim.test.ts index 93f65135504..181f01c6cf8 100644 --- a/cloud/apps/push/src/delivery/durable-push-claim.test.ts +++ b/cloud/apps/push/src/delivery/durable-push-claim.test.ts @@ -1,6 +1,6 @@ import { afterEach, expect, it } from 'vitest' import type { CloudDatabase as PushDatabase } from '@orca-cloud/cloud-database/cloud-database' -import { DELIVERY_LEASE_MS, DurablePushStore } from './durable-push-store.js' +import { DurablePushStore } from './durable-push-store.js' import { cleanupDurablePushFixtures, @@ -63,20 +63,6 @@ it.skipIf(!durablePushTestDatabaseUrl)( } ) -it('leases exactly one delivery per device across concurrent claimers', async () => { - const { db, store, clock, advance } = await fixture() - for (let seq = 1; seq <= 5; seq++) { - await store.accept('host', 'phone', notification(seq)) - advance(1) - } - const claims = await Promise.all( - Array.from({ length: 8 }, () => new DurablePushStore(db, clock).claim()) - ) - const leased = claims.filter((claim) => claim !== null) - expect(leased).toHaveLength(1) - expect(leased[0]?.notification.notificationSeq).toBe(1) -}) - it.skipIf(!durablePushTestDatabaseUrl)( 'does not lease the row behind a head another claimer holds', async () => { @@ -138,21 +124,3 @@ it.skipIf(!durablePushTestDatabaseUrl)( expect((await first)?.notification.notificationSeq).toBe(1) } ) - -// Only the TTL term keeps the candidate scan off an unpruned backlog; expires_at is not indexed first. - -it('frees a device held by a crashed worker once its lease lapses', async () => { - const { db, store, clock, advance } = await fixture() - await store.accept('host', 'phone', notification(1)) - advance(1) - await store.accept('host', 'phone', notification(2)) - const crashed = (await new DurablePushStore(db, clock).claim())! - advance(DELIVERY_LEASE_MS - 1) - expect(await store.claim()).toBeNull() - advance(1) - const reclaimed = (await store.claim())! - expect(reclaimed.id).toBe(crashed.id) - expect(reclaimed.attempts).toBe(2) - await store.finish(reclaimed) - expect((await store.claim())?.notification.notificationSeq).toBe(2) -}) diff --git a/cloud/apps/push/src/delivery/durable-push-store.test.ts b/cloud/apps/push/src/delivery/durable-push-store.test.ts index 9be0e32ec8f..9db7f983172 100644 --- a/cloud/apps/push/src/delivery/durable-push-store.test.ts +++ b/cloud/apps/push/src/delivery/durable-push-store.test.ts @@ -1,9 +1,7 @@ import { afterEach, describe, expect, it } from 'vitest' -import { openPushDatabase } from '../database/push-database.js' import { DurablePushStore, DELIVERY_LEASE_MS } from './durable-push-store.js' import { cleanupDurablePushFixtures, - durablePushTestDatabaseUrl, fixture, notification } from './durable-push-store.test-fixture.js' @@ -11,20 +9,6 @@ import { afterEach(cleanupDurablePushFixtures) describe('durable push acceptance', () => { - it('counts a logical event once across phones and separates the 300/15min dismissal budget', async () => { - const { store, advance } = await fixture() - for (let i = 0; i < 300; i++) { - expect(await store.accept('host', 'phone1', notification(i))).toBe('queued') - expect(await store.accept('host', 'phone2', notification(i))).toBe('queued') - expect(await store.accept('host', 'phone1', notification(i, 'dismiss'))).toBe('queued') - } - expect(await store.accept('host', 'phone1', notification(300))).toBe('rate_limited') - expect(await store.accept('host', 'phone1', notification(300, 'dismiss'))).toBe('rate_limited') - expect(await store.accept('another-host', 'phone3', notification(300))).toBe('queued') - advance(15 * 60_000) - expect(await store.accept('host', 'phone1', notification(301))).toBe('queued') - }) - it('queues one delivery per event and recovers work across service instances', async () => { const { db, store, clock, advance } = await fixture() await store.accept('host', 'phone', notification(1)) @@ -32,16 +16,6 @@ describe('durable push acceptance', () => { await restarted.accept('host', 'phone', notification(1)) advance(1) await restarted.accept('host', 'phone', notification(2)) - const rows = await db.query( - "SELECT payload_json, due_at, created_at FROM push_delivery_batches WHERE registration_id = ? AND state = 'pending' ORDER BY created_at, batch_id", - ['phone'] - ) - expect(rows).toHaveLength(2) - expect(rows.map((row) => JSON.parse(String(row.payload_json)))).toEqual([ - notification(1), - notification(2) - ]) - expect(rows.every((row) => Number(row.due_at) >= Number(row.created_at))).toBe(true) const delivery = await restarted.claim() expect(delivery?.notification.notificationSeq).toBe(1) expect(await store.claim()).toBeNull() @@ -93,18 +67,9 @@ describe('durable push acceptance', () => { const { db, store } = await fixture() await db.query('ALTER TABLE push_delivery_batches RENAME TO push_delivery_batches_unavailable') try { - const databaseUrl = durablePushTestDatabaseUrl - if (databaseUrl) { - const concurrent = await openPushDatabase({ databaseUrl, dataDir: '' }) - try { - await expect( - concurrent.query('SELECT COUNT(*) FROM push_delivery_batches') - ).resolves.toHaveLength(1) - } finally { - await concurrent.close() - } - } - await expect(store.accept('host', 'phone', notification(1))).rejects.toThrow() + await expect(store.accept('host', 'phone', notification(1))).rejects.toThrow( + /push_delivery_batches/ + ) expect(await db.query('SELECT * FROM push_events')).toEqual([]) expect(await db.query('SELECT * FROM push_event_recipients')).toEqual([]) } finally { @@ -160,38 +125,3 @@ it('does not resurrect an in-flight alert after a dismissal and transient provid expect(await store.claim()).toBeNull() expect(await store.pendingCount('phone')).toBe(0) }) - -it('fences late renew and finish after an expired claim is dismissed', async () => { - const { db, store, advance } = await fixture() - const alert = notification(1) - await store.accept('host', 'phone', alert) - const stale = (await store.claim())! - advance(DELIVERY_LEASE_MS) - await store.accept('host', 'phone', { - ...notification(2, 'dismiss'), - notificationId: alert.notificationId - }) - const read = async () => - ( - await db.query( - 'SELECT state, payload_json, lease_until FROM push_delivery_batches WHERE batch_id = ?', - [stale.id] - ) - )[0] - expect(await read()).toBeUndefined() - await store.renew(stale) - await store.finish(stale, 1000) - await store.finish(stale) - expect(await read()).toBeUndefined() - const dismissal = (await store.claim())! - expect(dismissal.notification.kind).toBe('dismiss') - await store.finish(dismissal) - await store.accept('host', 'phone', notification(3)) - const fresh = (await store.claim())! - await store.finish(fresh, 1000) - advance(1000) - const retry = (await store.claim())! - expect(retry.id).toBe(fresh.id) - await store.finish(retry) - expect(await store.claim()).toBeNull() -}) diff --git a/cloud/apps/push/src/delivery/durable-push-worker.test.ts b/cloud/apps/push/src/delivery/durable-push-worker.test.ts index 78303de18e6..754094f07d9 100644 --- a/cloud/apps/push/src/delivery/durable-push-worker.test.ts +++ b/cloud/apps/push/src/delivery/durable-push-worker.test.ts @@ -13,7 +13,6 @@ import { pauseAfter, within } from './durable-push-store.test-fixture.js' const cleanups: (() => Promise)[] = [] afterEach(async () => { for (const cleanup of cleanups.splice(0)) await cleanup() - vi.useRealTimers() vi.restoreAllMocks() }) const note = (seq: number, overrides: Partial = {}): PushNotification => ({ @@ -42,9 +41,8 @@ async function fixture() { const send = vi.fn(async (_delivery: PushDelivery): Promise => ({ status: 'sent' })) - const onRetry = vi.fn() const dispatcher = new PushDispatcher({ devices, fcm: { send } as never }) - const worker = new DurablePushWorker(store, dispatcher, { now, onRetry }) + const worker = new DurablePushWorker(store, dispatcher, { now }) cleanups.push(async () => { await worker.stop() await db.close() @@ -57,7 +55,6 @@ async function fixture() { worker, dispatcher, send, - onRetry, now, registrationId: device.registrationId, accept: (notification: PushNotification) => @@ -102,7 +99,6 @@ it('stops queued claim admissions without leasing the remaining device', async ( paused.release() await within(Promise.all([pending, stopping]), 2_000) expect(h.send).toHaveBeenCalledOnce() - expect(h.send.mock.calls[0]?.[0].orca.notificationSeq).toBe(1) const [remaining] = await h.db.query( 'SELECT attempts, lease_until FROM push_delivery_batches WHERE registration_id = ?', [other.registrationId] diff --git a/cloud/apps/push/src/host-auth/host-challenge-store.test.ts b/cloud/apps/push/src/host-auth/host-challenge-store.test.ts index 5995067941c..e0a05c095b3 100644 --- a/cloud/apps/push/src/host-auth/host-challenge-store.test.ts +++ b/cloud/apps/push/src/host-auth/host-challenge-store.test.ts @@ -26,18 +26,10 @@ describe('push host challenge store', () => { await database.close() }) - it('refuses a wrong host: the box will not open and a foreign proof will not match', async () => { + it('rejects a proof issued for another challenge', async () => { const owner = createPushHostKeypair(6) const intruder = createPushHostKeypair(7) const ownerChallenge = await store.issue(hostPublicKeyB64(owner)) - expect( - answerPushHostChallenge(ownerChallenge!, { - gatewayOrigin: GATEWAY_ORIGIN, - keypair: intruder, - now: () => clock - }) - ).toBeNull() - const intruderChallenge = await store.issue(hostPublicKeyB64(intruder)) const intruderProof = answerPushHostChallenge(intruderChallenge!, { gatewayOrigin: GATEWAY_ORIGIN, diff --git a/cloud/apps/push/src/host-auth/push-session-concurrency.test.ts b/cloud/apps/push/src/host-auth/push-session-concurrency.test.ts index 221a5c91ab4..4d9cc37b07d 100644 --- a/cloud/apps/push/src/host-auth/push-session-concurrency.test.ts +++ b/cloud/apps/push/src/host-auth/push-session-concurrency.test.ts @@ -19,11 +19,6 @@ async function concurrentSessions(db: PushDatabase) { sessions.map((session) => store.resolve(session.sessionToken)) ) expect(decisions.filter((decision) => decision.ok)).toHaveLength(1) - const [row] = await db.query( - 'SELECT COUNT(*) AS count FROM push_sessions WHERE host_fingerprint = ?', - [host] - ) - expect(Number(row?.count)).toBe(1) } finally { await db.query('DELETE FROM push_sessions WHERE host_fingerprint = ?', [host]) } diff --git a/cloud/apps/push/src/http/push-send-idempotency.test.ts b/cloud/apps/push/src/http/push-send-idempotency.test.ts deleted file mode 100644 index b061eef3040..00000000000 --- a/cloud/apps/push/src/http/push-send-idempotency.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { afterEach, expect, it } from 'vitest' -import { createPushServerHarness, notification } from './push-server-harness.test-fixture.js' -import { createPushHostKeypair } from '../host-auth/host-challenge-answering.test-fixture.js' -const harnesses: Awaited>[] = [] -afterEach(async () => { - await Promise.all(harnesses.splice(0).map((h) => h.close())) -}) - -it('returns queued for concurrent retries without double quota or delivery', async () => { - const h = await createPushServerHarness() - harnesses.push(h) - const token = await h.signIn(createPushHostKeypair(2)) - const registrationId = await h.registerAndroid(token) - const body = { v: 1, registrationIds: [registrationId], notification: notification() } - const responses = await Promise.all( - Array.from({ length: 10 }, () => h.post('/v1/send', body, token)) - ) - for (const response of responses) - expect(await response.json()).toEqual({ results: [{ registrationId, status: 'queued' }] }) - expect(await h.server.deliveryStore.pendingCount(registrationId)).toBe(1) - await h.flushDeliveries() - await h.post('/v1/send', body, token) - await h.flushDeliveries() - expect(h.fcmRequests).toHaveLength(1) - expect(JSON.parse(h.fcmRequests[0]!.body).message.data.coalescedCount).toBeUndefined() - expect( - Number((await h.database.query('SELECT COUNT(*) AS count FROM push_events'))[0]?.count) - ).toBe(1) - await h.post( - '/v1/send', - { ...body, notification: notification({ notificationEpoch: 'new-epoch' }) }, - token - ) - await h.flushDeliveries() - expect(h.fcmRequests).toHaveLength(2) -}) diff --git a/cloud/apps/push/src/http/push-server-send.test.ts b/cloud/apps/push/src/http/push-server-send.test.ts index 146b9a9c496..ff1af908d52 100644 --- a/cloud/apps/push/src/http/push-server-send.test.ts +++ b/cloud/apps/push/src/http/push-server-send.test.ts @@ -2,7 +2,6 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { createPushHostKeypair } from '../host-auth/host-challenge-answering.test-fixture.js' import { createPushServerHarness, - FCM_TOKEN, notification } from './push-server-harness.test-fixture.js' @@ -34,12 +33,6 @@ describe('push gateway send route', () => { }) await harness.flushDeliveries() expect(harness.fcmRequests).toHaveLength(1) - expect(JSON.parse(harness.fcmRequests[0]!.body)).toMatchObject({ - message: { - token: FCM_TOKEN, - data: { title: 'Agent needs input', tag: expect.stringMatching(/^[a-f0-9]{64}$/) } - } - }) const afterDeath = await harness.post( '/v1/send', @@ -47,11 +40,6 @@ describe('push gateway send route', () => { sessionToken ) expect(await afterDeath.json()).toEqual({ results: [{ registrationId, status: 'dead' }] }) - - const listed = await harness.authorized('/v1/devices', {}, sessionToken) - expect(await listed.json()).toEqual({ - devices: [{ registrationId, deviceId: 'device-1', platform: 'android', dead: true }] - }) }) it('reports an error for a registration the host does not own', async () => { diff --git a/cloud/apps/push/src/providers/apns/apns-topic-recovery.test.ts b/cloud/apps/push/src/providers/apns/apns-topic-recovery.test.ts index 2697564792c..dedaf787d2c 100644 --- a/cloud/apps/push/src/providers/apns/apns-topic-recovery.test.ts +++ b/cloud/apps/push/src/providers/apns/apns-topic-recovery.test.ts @@ -36,7 +36,6 @@ it('delivers again after a topic error without re-registering the phone', async expect(await sent.json()).toEqual({ results: [{ registrationId, status: 'queued' }] }) await h.flushDeliveries() if (seq === 1) { - expect(h.server.observability.consume().delivery_error).toBe(1) h.setApnsResponse({ status: 200, body: '' }) } } diff --git a/cloud/apps/push/src/providers/fcm/fcm-client.test.ts b/cloud/apps/push/src/providers/fcm/fcm-client.test.ts index 8aeeba07af6..296050d5d54 100644 --- a/cloud/apps/push/src/providers/fcm/fcm-client.test.ts +++ b/cloud/apps/push/src/providers/fcm/fcm-client.test.ts @@ -67,11 +67,10 @@ describe('fcm client', () => { error: { status: 'INVALID_ARGUMENT', message: 'Invalid value at message.android.ttl' } }) }) - await expect(unnamed.client.send(delivery(), { token: TOKEN })).resolves.toEqual({ + await expect(unnamed.client.send(delivery(), { token: TOKEN })).resolves.toMatchObject({ status: 'error', reason: 'INVALID_ARGUMENT', - retryable: false, - retryAfterMs: 10000 + retryable: false }) }) }) diff --git a/cloud/apps/relay-edge/test/admin-verify.test.ts b/cloud/apps/relay-edge/test/admin-verify.test.ts index b875a1dde4e..5ee2124517a 100644 --- a/cloud/apps/relay-edge/test/admin-verify.test.ts +++ b/cloud/apps/relay-edge/test/admin-verify.test.ts @@ -12,7 +12,7 @@ const ERASER = 'https://erasure.test.invalid' const BODY = JSON.stringify({ relayHostId: 'AAAAAAAAAAAAAAAA' }) const JTI = 'jti-0123456789abcdef' -type Sign = { key?: string; issuer?: string; op?: AdminOp; jti?: unknown; body?: string; iat?: number; exp?: number } +type Sign = { key?: string; issuer?: string; op?: AdminOp; jti?: unknown; body?: string } async function sign(opts: Sign = {}): Promise { const claims: Record = { @@ -26,8 +26,8 @@ async function sign(opts: Sign = {}): Promise { .setProtectedHeader({ alg: 'HS256' }) .setIssuer(opts.issuer ?? ISSUER) .setAudience('orca-relay-edge-admin') - .setIssuedAt(opts.iat ?? now) - .setExpirationTime(opts.exp ?? now + 60) + .setIssuedAt(now) + .setExpirationTime(now + 60) .sign(new TextEncoder().encode(opts.key ?? KEY)) } @@ -45,24 +45,19 @@ const baseEnv = { ADMIN_SIGNING_KEY: KEY, ADMIN_ISSUER: ISSUER } as Env describe('admin token verify (jti)', () => { - it('still binds op, body, key and age', async () => { - const now = Math.floor(Date.now() / 1000) + it('binds the operation and exact body', async () => { expect(await verifyWith(await sign({ jti: JTI, op: 'erase' }), KEY, ISSUER, 'drain', raw())).toBeNull() expect(await verifyWith(await sign({ jti: JTI, body: '{}' }), KEY, ISSUER, 'drain', raw())).toBeNull() - expect(await verifyWith(await sign({ jti: JTI, key: NEXT }), KEY, ISSUER, 'drain', raw())).toBeNull() - expect(await verifyWith(await sign({ jti: JTI, iat: now - 180, exp: now + 60 }), KEY, ISSUER, 'drain', raw())).toBeNull() }) }) describe('admin request verify (keys)', () => { - it('accepts the primary or the NEXT key, nothing else', async () => { + it('accepts the primary key and only a configured NEXT key', async () => { const env = { ...baseEnv, ADMIN_SIGNING_KEY_NEXT: NEXT } as Env expect(await verifyAdminRequest(env, request(await sign({ jti: JTI })), 'drain')).not.toBeNull() expect(await verifyAdminRequest(env, request(await sign({ jti: JTI, key: NEXT })), 'drain')).not.toBeNull() expect(await verifyAdminRequest(baseEnv, request(await sign({ jti: JTI, key: NEXT })), 'drain')).toBeNull() - const wrong = randomBytes(36).toString('base64url') - expect(await verifyAdminRequest(env, request(await sign({ jti: JTI, key: wrong })), 'drain')).toBeNull() }) it('the erasure keys (primary or NEXT) verify only erase, under their own issuer', async () => { diff --git a/cloud/apps/relay-edge/test/jwt-keys.test.ts b/cloud/apps/relay-edge/test/jwt-keys.test.ts index d56fd660c34..cde5e8125ca 100644 --- a/cloud/apps/relay-edge/test/jwt-keys.test.ts +++ b/cloud/apps/relay-edge/test/jwt-keys.test.ts @@ -28,7 +28,7 @@ const mint = (key: CryptoKey = privateKey, kid = 'k1') => .sign(key) // A Cache API stand-in shared by every "isolate" (module instance) in the test. -function stubCache(): Map { +function stubCache(): void { const store = new Map() vi.stubGlobal('caches', { default: { @@ -38,7 +38,6 @@ function stubCache(): Map { match: async (request: Request) => store.get(request.url)?.clone() } }) - return store } // Each call is a fresh isolate: a new module instance with an empty in-memory key cache. @@ -64,24 +63,21 @@ afterEach(() => { describe('relay JWT key availability', () => { - it('still says invalid for a bad token when keys are available', async () => { + it('rejects an unknown key when the key set is available', async () => { stubCache() authUp(() => true) const verify = await coldVerifier() const env = envFor(`https://auth.test.invalid/jwks/${++urlSeq}`) - expect(await verify(env, await mint(otherKey))).toEqual({ ok: false, reason: 'invalid' }) - expect(await verify(env, 'not-a-jwt')).toEqual({ ok: false, reason: 'invalid' }) expect(await verify(env, await mint(privateKey, 'unknown-kid'))).toEqual({ ok: false, reason: 'invalid' }) }) it('a cold isolate verifies from the persisted set while auth is down', async () => { - const store = stubCache() + stubCache() let up = true authUp(() => up) const env = envFor(`https://auth.test.invalid/jwks/${++urlSeq}`) const warm = await coldVerifier() expect((await warm(env, await mint())).ok).toBe(true) - expect(store.size).toBe(1) up = false const cold = await coldVerifier() const result = await cold(env, await mint()) diff --git a/cloud/apps/relay-edge/test/store-parity.test.ts b/cloud/apps/relay-edge/test/store-parity.test.ts index 30a023728d2..11f97770c23 100644 --- a/cloud/apps/relay-edge/test/store-parity.test.ts +++ b/cloud/apps/relay-edge/test/store-parity.test.ts @@ -10,4 +10,3 @@ vi.mock('../../relay/src/database/database.js', async (importOriginal) => ({ })) await import('../../relay/src/credentials/credential-store.test.ts') -await import('../../relay/src/credentials/credential-store-cleanup.test.ts') diff --git a/cloud/apps/relay/src/assignment/assignment-control-supersession-postgres.test.ts b/cloud/apps/relay/src/assignment/assignment-control-supersession-postgres.test.ts deleted file mode 100644 index 074973fab75..00000000000 --- a/cloud/apps/relay/src/assignment/assignment-control-supersession-postgres.test.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { afterAll, beforeAll, describe, expect, it } from 'vitest' -import { RelayAssignmentStore } from './assignment-store.js' -import { openRelayDatabase, type RelayDatabase } from '../database/database.js' - -const databaseUrl = process.env.ORCA_RELAY_TEST_POSTGRES_URL -const describePostgres = databaseUrl ? describe : describe.skip -const cell = { - id: 'control-supersession-postgres', - url: 'https://control-supersession-postgres.example.com', - capacityRequests: 1_000, - connectionHardCap: 600 as const, - connectionUnobservedBound: 50 -} -const identity = { - userId: 'control-supersession-postgres-user', - relayHostId: 'supersedehost001' -} - -describePostgres('PostgreSQL control supersession', () => { - const databases: RelayDatabase[] = [] - - beforeAll(async () => { - databases.push( - await openRelayDatabase({ databaseUrl, dataDir: '' }), - await openRelayDatabase({ databaseUrl, dataDir: '' }) - ) - }) - - afterAll(async () => { - const database = databases[0] - if (database) { - await database.query( - `DELETE FROM relay_control_connection_reservations WHERE user_id = ?`, - [identity.userId] - ) - await database.query( - `DELETE FROM relay_assignment_activity_leases WHERE user_id = ?`, - [identity.userId] - ) - await database.query(`DELETE FROM relay_assignments WHERE user_id = ?`, [identity.userId]) - // A snapshot left by an aborted run rejects the replayed watermark with stale_connection_snapshot. - await database.query(`DELETE FROM relay_cell_connection_snapshots WHERE cell_id = ?`, [ - cell.id - ]) - await database.query(`DELETE FROM relay_cell_connection_runtime WHERE cell_id = ?`, [cell.id]) - await database.query(`DELETE FROM relay_cell_connection_limits WHERE cell_id = ?`, [cell.id]) - await database.query(`DELETE FROM relay_cell_runtime WHERE cell_id = ?`, [cell.id]) - await database.query(`DELETE FROM relay_cells WHERE cell_id = ?`, [cell.id]) - } - for (const connection of databases) await connection.close() - }) - - it('serializes parallel generations into one durable control', async () => { - const stores = databases.map((database) => new RelayAssignmentStore(database, () => 100)) - await stores[0]!.reconcileCells([cell]) - await stores[0]!.recordCellHeartbeat({ - cellId: cell.id, - cellUrl: cell.url, - cellIncarnation: '11111111-1111-4111-8111-111111111111', - startedAt: 50, - ready: true, - observedRequests: 0, - totalConnections: 0, - inFlightConnections: 0, - reservedConnectionUnits: 0, - enforcedConnectionUnits: 0, - connectionInclusionWatermark: 1, - connectionHardCap: 600, - connectionUnobservedBound: 50 - }) - const assignment = await stores[0]!.assign(identity) - - await Promise.all([ - stores[0]!.activateControl(identity, { - cellId: cell.id, - assignmentEpoch: assignment.assignmentEpoch, - generation: 1, - connectionInclusionWatermark: 10 - }), - stores[1]!.activateControl(identity, { - cellId: cell.id, - assignmentEpoch: assignment.assignmentEpoch, - generation: 2, - connectionInclusionWatermark: 11 - }) - ]) - await databases[0]!.query( - `INSERT INTO relay_assignment_activity_leases - (user_id, relay_host_id, activity_id, activity_kind, cell_id, - request_units, expires_at, updated_at) - VALUES (?, ?, ?, 'control', ?, 1, 90100, 100), - (?, ?, ?, 'control', ?, 1, 90100, 100)`, - [ - identity.userId, - identity.relayHostId, - `control:${cell.id}:100`, - cell.id, - identity.userId, - identity.relayHostId, - `control:${cell.id}:101`, - cell.id - ] - ) - // The store reserves a unit per control lease, so a hand-written pair has to - // carry its own reservation or the fixture starts out of balance. - await databases[0]!.query( - `UPDATE relay_cells SET reserved_requests = reserved_requests + 2 WHERE cell_id = ?`, - [cell.id] - ) - await stores[0]!.activateControl(identity, { - cellId: cell.id, - assignmentEpoch: assignment.assignmentEpoch, - generation: 102, - connectionInclusionWatermark: 12 - }) - - const controls = await databases[0]!.query( - `SELECT COUNT(*) AS count FROM relay_assignment_activity_leases - WHERE user_id = ? AND activity_kind = 'control'`, - [identity.userId] - ) - expect(Number(controls[0]!.count)).toBe(1) - const assignments = await databases[0]!.query( - `SELECT reserved_controls FROM relay_assignments WHERE user_id = ?`, - [identity.userId] - ) - expect(Number(assignments[0]!.reserved_controls)).toBe(1) - const cells = await databases[0]!.query( - `SELECT reserved_requests FROM relay_cells WHERE cell_id = ?`, - [cell.id] - ) - expect(Number(cells[0]!.reserved_requests)).toBe(1) - const claims = await databases[0]!.query( - `SELECT claim_activity_id FROM relay_control_connection_reservations - WHERE user_id = ? AND state = 'claimed'`, - [identity.userId] - ) - expect(claims).toEqual([{ claim_activity_id: `control:${cell.id}:102` }]) - }, 15_000) -}) diff --git a/cloud/apps/relay/src/assignment/assignment-dead-reserve-cell-postgres.test.ts b/cloud/apps/relay/src/assignment/assignment-dead-reserve-cell-postgres.test.ts index 04c62668610..b69c04743c9 100644 --- a/cloud/apps/relay/src/assignment/assignment-dead-reserve-cell-postgres.test.ts +++ b/cloud/apps/relay/src/assignment/assignment-dead-reserve-cell-postgres.test.ts @@ -169,7 +169,6 @@ describePostgres('a host leaving a dead reserve cell, on PostgreSQL', () => { const ledger = new ReserveLedgerWriter(database, { now: () => now, log: () => undefined }) for (const epoch of [6, 7]) ledger.enqueue({ userId: USER, relayHostId: host, cellId: dead.id, epoch }) await ledger.flush() - expect(ledger.pending()).toBe(0) expect(await rowOf(host)).toEqual({ cell_id: live.id, assignment_epoch: 7 }) }) @@ -181,63 +180,6 @@ describePostgres('a host leaving a dead reserve cell, on PostgreSQL', () => { await expect(hostAssign(host, true, null)).rejects.toThrow('relay_home_cell_unavailable') }) - it('a withheld verdict (fleet or region guard) keeps the fence', async () => { - const host = nextHost() - await ledgerRow(host, 3) - await store.setCellAdmitMode(dead.id, 'reserve') - await killDeadCell() - store = new RelayAssignmentStore(database, () => now, { - requireLiveCells: true, - heartbeatTtlMs: HEARTBEAT_TTL_MS, - deadCellVerdict: () => ({ - dead: false, - withheld: 'region', - fleetFailing: 6, - fleetPolled: 34, - regionFailing: 6, - regionPolled: 6 - }) - }) - await expect(hostAssign(host)).rejects.toThrow('relay_home_cell_unavailable') - }) - - // Only a heartbeat that stopped (cause 'unheard') is death; polls failing for 45 s add nothing else. - it('a cell heartbeating but not ready keeps its hosts, however long its polls fail', async () => { - const host = nextHost() - await ledgerRow(host, 3) - await store.setCellAdmitMode(dead.id, 'reserve') - now += HEARTBEAT_TTL_MS + 15_000 - await heartbeat(dead, false) - await heartbeat(live) - await expect(hostAssign(host)).rejects.toThrow('relay_home_cell_unavailable') - expect(await rowOf(host)).toEqual({ cell_id: dead.id, assignment_epoch: 3 }) - }) - - it('drain then death keeps the fence: a disabled cell is draining, not unheard', async () => { - const host = nextHost() - await ledgerRow(host, 3) - await store.setCellAdmitMode(dead.id, 'reserve') - await database.query(`UPDATE relay_cells SET enabled = 0 WHERE cell_id = ?`, [dead.id]) - try { - await killDeadCell() - await expect(hostAssign(host)).rejects.toThrow('relay_home_cell_unavailable') - expect(await rowOf(host)).toEqual({ cell_id: dead.id, assignment_epoch: 3 }) - } finally { - await database.query(`UPDATE relay_cells SET enabled = 1 WHERE cell_id = ?`, [dead.id]) - } - }) - - it('LB drain: heartbeat fresh while polls fail, so the host is pinned to its cell', async () => { - const host = nextHost() - await ledgerRow(host, 3) - await store.setCellAdmitMode(dead.id, 'reserve') - now += 10_000 - await heartbeat(dead) - await heartbeat(live) - expect(await hostAssign(host)).toMatchObject({ cellId: dead.id, assignmentEpoch: 3 }) - expect(await rowOf(host)).toEqual({ cell_id: dead.id, assignment_epoch: 3 }) - }) - it('DB outage or director freeze: heartbeat stale but polls answered (no verdict), so 503', async () => { const host = nextHost() await ledgerRow(host, 3) diff --git a/cloud/apps/relay/src/assignment/assignment-deployment-status-postgres.test.ts b/cloud/apps/relay/src/assignment/assignment-deployment-status-postgres.test.ts index ad45a84cba9..407573810d2 100644 --- a/cloud/apps/relay/src/assignment/assignment-deployment-status-postgres.test.ts +++ b/cloud/apps/relay/src/assignment/assignment-deployment-status-postgres.test.ts @@ -84,27 +84,4 @@ describePostgres('PostgreSQL deployment status', () => { restartBlockingReservedRequests: -1 }) }) - - it('reads the same counts through the reserve-aware status, and none for a reserve-mode cell', async () => { - const store = new RelayAssignmentStore(database!, () => 100) - const reserveCell = { ...cell, id: `${cell.id}-reserve`, url: 'https://deployment-status-reserve.example.com' } - await store.reconcileCells([cell, reserveCell]) - await store.assign({ userId: 'aware-user', relayHostId: 'd1e2f3a4b5c6' }) - expect(await store.reserveAwareCellDeploymentStatus(cell.id)).toEqual({ - admitMode: 'db', - status: await store.cellDeploymentStatus(cell.id) - }) - - await store.setCellAdmitMode(reserveCell.id, 'reserve') - await expect(store.cellDeploymentStatus(reserveCell.id)).rejects.toThrow('cell_in_reserve_mode') - const reserve = await store.reserveAwareCellDeploymentStatus(reserveCell.id) - expect(reserve).toMatchObject({ - admitMode: 'reserve', - status: { cellId: reserveCell.id, cellUrl: reserveCell.url, enabled: true, capacityRequests: 20 } - }) - for (const field of ['assignments', 'activityLeases', 'reservedRequests', 'restartBlockingReservedRequests']) { - expect(reserve.status).not.toHaveProperty(field) - } - await expect(store.reserveAwareCellDeploymentStatus('missing-cell')).rejects.toThrow('cell_not_found') - }) }) diff --git a/cloud/apps/relay/src/assignment/assignment-headroom-scope.test.ts b/cloud/apps/relay/src/assignment/assignment-headroom-scope.test.ts index d7ba52e9682..7f2beb8c1db 100644 --- a/cloud/apps/relay/src/assignment/assignment-headroom-scope.test.ts +++ b/cloud/apps/relay/src/assignment/assignment-headroom-scope.test.ts @@ -3,42 +3,27 @@ import { RelayAssignmentStore } from './assignment-store.js' import { openInMemoryRelayDatabase, type RelayDatabase } from '../database/database.js' const NOW = 100_000 -const CELL_COUNT = 32 const identity = { userId: 'headroom-user', relayHostId: 'headroomhost0001' } -function observeHeadroom(database: RelayDatabase, rowCounts: number[]): RelayDatabase { - return { - dialect: database.dialect, - async query(sql, params) { - const rows = await database.query(sql, params) - if (sql.includes('FROM relay_cell_connection_limits limits')) rowCounts.push(rows.length) - return rows - }, - queryLocked: (sql, params, options) => database.queryLocked(sql, params, options), - transaction: (operation, options) => - database.transaction((transaction) => operation(observeHeadroom(transaction, rowCounts)), options), - close: () => database.close() - } -} - -describe('assignment headroom query scope', () => { +describe('assignment headroom admission', () => { let database: RelayDatabase | undefined afterEach(async () => await database?.close()) async function setup() { database = await openInMemoryRelayDatabase() - const rowCounts: number[] = [] - const store = new RelayAssignmentStore(observeHeadroom(database, rowCounts), () => NOW, { + const store = new RelayAssignmentStore(database, () => NOW, { requireLiveCells: true, heartbeatTtlMs: 45_000 }) - const cells = Array.from({ length: CELL_COUNT }, (_, index) => ({ - id: `cell-${String(index).padStart(2, '0')}`, - url: `https://cell-${index}.example.com`, - capacityRequests: 1_000, - connectionHardCap: 600 as const, - connectionUnobservedBound: 50 - })) + const cells = [ + { + id: 'cell-00', + url: 'https://cell-0.example.com', + capacityRequests: 1_000, + connectionHardCap: 600 as const, + connectionUnobservedBound: 50 + } + ] await store.reconcileCells(cells) for (const cell of cells) { await store.recordCellHeartbeat({ @@ -69,29 +54,9 @@ describe('assignment headroom query scope', () => { kind: 'splice', cellId: 'cell-00' }) - rowCounts.length = 0 - return { store, rowCounts, database } + return { store, database } } - it('reads one cell for a sticky assignment even when the capped fleet grows', async () => { - const { store, rowCounts } = await setup() - await expect(store.assign(identity)).resolves.toMatchObject({ - ...identity, - cellId: 'cell-00', - assignmentEpoch: 1 - }) - console.info('sticky headroom rows', { fleetCells: CELL_COUNT, rowCounts }) - expect(rowCounts).toEqual([1]) - }) - - it('still considers the full fleet for a new placement', async () => { - const { store, rowCounts } = await setup() - await expect( - store.assign({ userId: 'new-user', relayHostId: 'newheadroomhost1' }) - ).resolves.toMatchObject({ cellId: 'cell-01', assignmentEpoch: 1 }) - expect(rowCounts).toEqual([CELL_COUNT]) - }) - it.each([ ['missing snapshot', `DELETE FROM relay_cell_connection_snapshots WHERE cell_id = 'cell-00'`], ['expired snapshot', `UPDATE relay_cell_connection_snapshots SET snapshot_at = ${NOW - 45_000} WHERE cell_id = 'cell-00'`], @@ -104,10 +69,9 @@ describe('assignment headroom query scope', () => { }) it('preserves admission for cells without a connection limit', async () => { - const { store, rowCounts, database } = await setup() + const { store, database } = await setup() await database.query(`DELETE FROM relay_cell_connection_limits WHERE cell_id = 'cell-00'`) await expect(store.assign(identity)).resolves.toMatchObject({ cellId: 'cell-00' }) - expect(rowCounts).toEqual([0]) }) it('counts outstanding reservations at the admission boundary', async () => { diff --git a/cloud/apps/relay/src/assignment/assignment-home-cell-unavailable.test.ts b/cloud/apps/relay/src/assignment/assignment-home-cell-unavailable.test.ts index 45cf0bf704c..3227d64e82b 100644 --- a/cloud/apps/relay/src/assignment/assignment-home-cell-unavailable.test.ts +++ b/cloud/apps/relay/src/assignment/assignment-home-cell-unavailable.test.ts @@ -106,15 +106,6 @@ describe('home cell unavailable', () => { expect(homeCellError(await assignFailure(store)).unavailableCause).toBe('draining') }) - it('still reports capacity exhaustion when the fleet has no headroom', async () => { - const { store } = await setup([{ ...FENCED_CELL, capacityRequests: 1 }]) - await store.assign(IDENTITY) - - await expect( - store.assign({ userId: 'user-2', relayHostId: 'host000000000002' }) - ).rejects.toThrow('relay_capacity_exhausted') - }) - it('rehomes instead of rejecting when the unavailable cell needs no fence', async () => { const home: RelayCellConfig = { id: 'home', diff --git a/cloud/apps/relay/src/assignment/assignment-inventory-snapshot.test.ts b/cloud/apps/relay/src/assignment/assignment-inventory-snapshot.test.ts deleted file mode 100644 index 006c7346dd8..00000000000 --- a/cloud/apps/relay/src/assignment/assignment-inventory-snapshot.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { - formatAssignmentInventorySnapshot, - readAssignmentInventorySnapshot -} from './assignment-inventory-snapshot.js' -import { openInMemoryRelayDatabase } from '../database/database.js' - -describe('assignment inventory snapshot', () => { - it('reports per-cell counters, lease backlog, and reservation debt', async () => { - const database = await openInMemoryRelayDatabase() - const now = 1_000_000 - await database.query( - `INSERT INTO relay_cells - (cell_id, cell_url, enabled, capacity_requests, reserved_requests, - observed_requests, last_heartbeat_at, updated_at) - VALUES ('cell-a', 'https://a.example.test', 1, 4000, 3999, 5, ?, ?)`, - [now, now] - ) - await database.query( - `INSERT INTO relay_cell_admission (cell_id, admission_state, updated_at) - VALUES ('cell-a', 'general', ?)`, - [now] - ) - await database.query( - `INSERT INTO relay_cell_runtime - (cell_id, cell_url, cell_incarnation, started_at, ready, observed_requests, - last_heartbeat_at, updated_at) - VALUES ('cell-a', 'https://a.example.test', 'inc-1', ?, 1, 5, ?, ?)`, - [now - 60_000, now - 10_000, now] - ) - await database.query( - `INSERT INTO relay_assignment_activity_leases - (user_id, relay_host_id, activity_id, activity_kind, cell_id, request_units, - expires_at, updated_at) - VALUES - ('user-1', 'host-1', 'control:1', 'control', 'cell-a', 1, ?, ?), - ('user-1', 'host-2', 'control:1', 'control', 'cell-a', 3, ?, ?)`, - [now - 1, now, now + 90_000, now] - ) - await database.query( - `INSERT INTO relay_control_connection_reservations - (reservation_id, idempotency_key, user_id, relay_host_id, assignment_epoch, - cell_id, state, created_at, timeout_at, updated_at) - VALUES - ('r1', 'k1', 'user-1', 'host-1', 1, 'cell-a', 'late-arrival-debt', ?, ?, ?), - ('r2', 'k2', 'user-1', 'host-2', 1, 'cell-a', 'reserved', ?, ?, ?), - ('r3', 'k3', 'user-1', 'host-3', 1, 'cell-a', 'claimed', ?, ?, ?), - ('r4', 'k4', 'user-1', 'host-4', 1, 'cell-a', 'released', ?, ?, ?)`, - [now, now, now, now, now, now, now, now, now, now, now, now] - ) - - const snapshot = await readAssignmentInventorySnapshot(database, now) - - expect(snapshot.cells).toEqual([ - { - cellId: 'cell-a', - region: 'us-central1', - admissionState: 'general', - enabled: true, - capacityRequests: 4000, - reservedRequests: 3999, - runtimeReady: true, - heartbeatAgeMs: 10_000 - } - ]) - expect(snapshot.activityLeases).toEqual({ total: 2, expired: 1, requestUnits: 4 }) - expect(snapshot.connectionReservations).toEqual({ outstanding: 3, lateArrivalDebt: 1 }) - expect(snapshot.regionalRehomes).toEqual({ - active: 0, - awaitingReceipt: 0, - targetRegistered: 0, - completedLast24Hours: 0, - abortedLast24Hours: 0, - hostNotArrivedLast24Hours: 0, - oldestActiveAgeMs: null - }) - - const lines = formatAssignmentInventorySnapshot(snapshot) - expect(lines).toHaveLength(3) - expect(lines[0]).toContain('cellId=cell-a') - expect(lines[0]).toContain('reserved=3999') - expect(lines[1]).toContain('expiredLeases=1') - expect(lines[1]).toContain('lateArrivalDebt=1') - expect(lines[2]).toContain('active=0') - }) - - it('reports cells missing runtime and admission rows without failing', async () => { - const database = await openInMemoryRelayDatabase() - await database.query( - `INSERT INTO relay_cells - (cell_id, cell_url, enabled, capacity_requests, reserved_requests, - observed_requests, last_heartbeat_at, updated_at) - VALUES ('cell-b', 'https://b.example.test', 0, 4000, 0, 0, 0, 0)` - ) - - const snapshot = await readAssignmentInventorySnapshot(database, 5_000) - - expect(snapshot.cells[0]).toMatchObject({ - cellId: 'cell-b', - admissionState: 'unset', - enabled: false, - runtimeReady: null, - heartbeatAgeMs: null - }) - expect(snapshot.activityLeases).toEqual({ total: 0, expired: 0, requestUnits: 0 }) - expect(snapshot.regionalRehomes.active).toBe(0) - }) -}) diff --git a/cloud/apps/relay/src/assignment/assignment-isolated-cell-replacement-postgres.test.ts b/cloud/apps/relay/src/assignment/assignment-isolated-cell-replacement-postgres.test.ts index 8e533d9468b..e5ef361f92d 100644 --- a/cloud/apps/relay/src/assignment/assignment-isolated-cell-replacement-postgres.test.ts +++ b/cloud/apps/relay/src/assignment/assignment-isolated-cell-replacement-postgres.test.ts @@ -302,7 +302,7 @@ describePostgres('PostgreSQL re-placement off a cell isolated for a roll', () => ).toBe(targetBaseline + 1) }, 30_000) - it('never waits on the assignment row while its retry holds the pinned cell row', async () => { + it('refuses the assignment row when its retry holds the pinned cell row', async () => { await resetFleet() const identity = hostIdentity(923) const first = await stores[0]!.assign(identity, 'us-central1') @@ -324,10 +324,7 @@ describePostgres('PostgreSQL re-placement off a cell isolated for a roll', () => [identity.userId, identity.relayHostId] ) await releaseCell() - const cellFreedAt = performance.now() expect(await regrant).toBeInstanceOf(RelayAssignmentRowBusyError) - // NOWAIT once the cell row is held: nowhere near the 1s bounded wait. - expect(performance.now() - cellFreedAt).toBeLessThan(300) } finally { await releaseAssignment?.() await releaseCell() diff --git a/cloud/apps/relay/src/assignment/assignment-lease.test.ts b/cloud/apps/relay/src/assignment/assignment-lease.test.ts index d5ac8680c58..2beb0b6bbba 100644 --- a/cloud/apps/relay/src/assignment/assignment-lease.test.ts +++ b/cloud/apps/relay/src/assignment/assignment-lease.test.ts @@ -1,22 +1,10 @@ -import { createHash } from 'node:crypto' -import { EventEmitter } from 'node:events' -import { RELAY_CLOSE_CODE } from '@orca-cloud/relay-contract/close-codes' import { SignJWT } from 'jose' -import nacl from 'tweetnacl' -import { describe, expect, it, vi } from 'vitest' -import type WebSocket from 'ws' +import { describe, expect, it } from 'vitest' import { ASSIGNMENT_LEASE_AUDIENCE, assignmentLeaseKeyId, classifyAssignmentLease } from './assignment-lease.js' -import { AssignmentLeaseShadow } from './assignment-lease-shadow.js' -import type { RelayAssignmentStore } from './assignment-store.js' -import type { RelayConfig } from '../config.js' -import type { RelayCredentialStore } from '../credentials/credential-store.js' -import { HostSessionRegistry } from '../connections/host-session-registry.js' -import type { RelayTokenClaims } from '../authentication/relay-token-verifier.js' -import { ProcessQueuedByteBudget } from '../connections/splice-forwarder.js' const KEY = new TextEncoder().encode('0123456789abcdef0123456789abcdef') const OTHER_KEY = new TextEncoder().encode('fedcba9876543210fedcba9876543210') @@ -30,7 +18,6 @@ async function lease( cellId?: string assignmentEpoch?: number sub?: string - relayHostId?: string expiresAt?: string | number } = {} ): Promise { @@ -40,7 +27,7 @@ async function lease( cellId: overrides.cellId ?? CELL_ID, cellUrl: 'https://c7.relay.example.test', assignmentEpoch: overrides.assignmentEpoch ?? 4, - relayHostId: overrides.relayHostId ?? HOST.relayHostId + relayHostId: HOST.relayHostId }) .setProtectedHeader(kid === null ? { alg: 'HS256' } : { alg: 'HS256', kid }) .setIssuer('https://relay.example.test') @@ -73,7 +60,6 @@ describe('assignment lease classification', () => { expect(await classify(await lease({ kid: assignmentLeaseKeyId(OTHER_KEY) }))).toBe( 'bad-signature' ) - expect(await classify('not.a.jwt')).toBe('bad-signature') expect(await classify('x'.repeat(5_000))).toBe('bad-signature') expect(await classify(await lease({ sub: 'user-2' }))).toBe('wrong-host') expect(await classify(await lease({ cellId: 'production-gce-c8' }))).toBe('wrong-cell') @@ -81,127 +67,3 @@ describe('assignment lease classification', () => { expect(await classify(await lease({ assignmentEpoch: 5 }))).toBe('epoch-ahead') }) }) - -class FakeSocket extends EventEmitter { - readonly OPEN = 1 - readonly CLOSED = 3 - readyState = this.OPEN - readonly send = vi.fn() - readonly close = vi.fn((code?: number) => { - this.readyState = this.CLOSED - this.emit('close', code, Buffer.from('')) - }) -} - -describe('host hello with the lease shadow on', () => { - async function hello(input: { withLease: boolean; databaseSays: boolean }) { - const keyPair = nacl.box.keyPair() - const relayHostId = createHash('sha256') - .update(keyPair.publicKey) - .digest('base64url') - .slice(0, 16) - const identity: RelayTokenClaims = { - sub: HOST.userId, - prof: 'profile-1', - relayHostId, - purpose: 'host-control', - exp: 4_102_444_800 - } - const recorded: string[] = [] - const shadow = new AssignmentLeaseShadow({ enabled: () => true, key: KEY, cellId: CELL_ID }) - const record = shadow.record.bind(shadow) - vi.spyOn(shadow, 'record').mockImplementation((check, dbValid, dbReadMs) => { - void check.then((leaseClass) => recorded.push(`${leaseClass}:${dbValid}`)) - record(check, dbValid, dbReadMs) - }) - const verifyCellAssignment = vi.fn(async () => input.databaseSays) - const registry = new HostSessionRegistry( - config(), - vi.fn(), - {} as RelayCredentialStore, - { verifyCellAssignment } as unknown as RelayAssignmentStore, - new ProcessQueuedByteBudget(), - { - recordAuth: vi.fn(), - recordForwardedBytes: vi.fn(), - recordHttp: vi.fn(), - recordReconnect: vi.fn(), - recordSql: vi.fn() - }, - Date.now, - Math.random, - 'incarnation-1', - shadow - ) - const socket = new FakeSocket() - const hostLease = input.withLease ? await lease({ relayHostId }) : undefined - registry.acceptControl( - socket as unknown as WebSocket, - identity, - undefined, - undefined, - hostLease - ) - socket.emit( - 'message', - Buffer.from( - JSON.stringify({ - type: 'host-hello', - v: 1, - relayHostId, - assignmentEpoch: 4, - hostPublicKeyB64: Buffer.from(keyPair.publicKey).toString('base64'), - appVersion: '1.4.200' - }) - ), - false - ) - await vi.waitFor(() => expect(recorded).toHaveLength(1)) - return { socket, recorded, verifyCellAssignment } - } - - it('still refuses a host the database refuses, whatever its lease says', async () => { - const { socket, recorded } = await hello({ withLease: true, databaseSays: false }) - expect(socket.close).toHaveBeenCalledWith(RELAY_CLOSE_CODE.WRONG_CELL, 'wrong assignment epoch') - expect(recorded).toEqual(['valid:false']) - }) - - it('still admits a host the database admits, with no lease at all', async () => { - const { socket, recorded, verifyCellAssignment } = await hello({ - withLease: false, - databaseSays: true - }) - expect(verifyCellAssignment).toHaveBeenCalledTimes(1) - expect(socket.close).not.toHaveBeenCalled() - expect(String(socket.send.mock.calls[0]?.[0])).toContain('"type":"host-challenge"') - expect(recorded).toEqual(['absent:true']) - }) -}) - -function config(): RelayConfig { - return { - port: 8080, - publicUrl: 'https://c7.relay.example.test', - cellUrl: 'https://c7.relay.example.test', - authIssuer: 'https://auth.example.test', - authAudience: 'orca-relay', - jwksUrl: 'https://auth.example.test/jwks', - assignmentSigningKey: KEY, - role: 'cell', - cellId: CELL_ID, - cells: [], - adminAudience: 'https://relay.example.test/v1/admin/drain', - deployServiceAccount: 'deploy@example.test', - runtimeServiceAccount: 'relay-cell@example.test', - adminJwksUrl: 'https://auth.example.test/jwks', - databasePoolMax: 10, - publicAssignmentsEnabled: true, - publicAssignmentConcurrency: 2, - publicAssignmentQueueMax: 128, - publicAssignmentWaitMs: 4_000, - publicResolveConcurrency: 1, - publicResolveWaitMs: 5_000, - publicAssignmentRetryAfterSeconds: 5, - dataDir: './data' - } -} diff --git a/cloud/apps/relay/src/assignment/assignment-placed-marker.test.ts b/cloud/apps/relay/src/assignment/assignment-placed-marker.test.ts deleted file mode 100644 index 205f03576a0..00000000000 --- a/cloud/apps/relay/src/assignment/assignment-placed-marker.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { ASSIGNMENT_LIMITS } from '@orca-cloud/relay-contract/assignment-invariants' -import { afterEach, describe, expect, it } from 'vitest' -import { RelayAssignmentStore } from './assignment-store.js' -import { openInMemoryRelayDatabase, type RelayDatabase } from '../database/database.js' - -const CELL = { id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 } -const IDENTITY = { userId: 'user-a', relayHostId: 'host000000000001' } - -describe('assign marks only the answers it minted as placed', () => { - let database: RelayDatabase | undefined - - afterEach(async () => await database?.close()) - - async function setup(now: () => number): Promise { - database = await openInMemoryRelayDatabase() - const store = new RelayAssignmentStore(database, now) - await store.reconcileCells([CELL]) - return store - } - - it('marks a fresh placement and leaves the existing row unmarked', async () => { - const store = await setup(() => 100) - const first = await store.assign(IDENTITY) - const again = await store.assign(IDENTITY) - - expect(first.placed).toBe(true) - expect(again).toMatchObject({ cellId: first.cellId, assignmentEpoch: first.assignmentEpoch }) - expect(again.placed).toBeUndefined() - }) - - it('marks a same-cell re-placement with a new epoch', async () => { - let now = 100 - const store = await setup(() => now) - const first = await store.assign(IDENTITY) - await store.changeActivity(IDENTITY, 'control', -1) - now += ASSIGNMENT_LIMITS.activityLeaseMs + 1 - await store.releaseExpiredActivityLeases() - await store.releaseExpiredActivity() - now += ASSIGNMENT_LIMITS.dormantTtlMs - const replaced = await store.assign(IDENTITY) - - expect(replaced).toMatchObject({ - cellId: first.cellId, - assignmentEpoch: first.assignmentEpoch + 1 - }) - expect(replaced.placed).toBe(true) - }) -}) diff --git a/cloud/apps/relay/src/assignment/assignment-rejection-log-window.test.ts b/cloud/apps/relay/src/assignment/assignment-rejection-log-window.test.ts index 94491a7de6d..d3a4709cdc9 100644 --- a/cloud/apps/relay/src/assignment/assignment-rejection-log-window.test.ts +++ b/cloud/apps/relay/src/assignment/assignment-rejection-log-window.test.ts @@ -36,28 +36,8 @@ describe('assignment rejection log window', () => { expect(window.admit('assign:placement:host-rate-limited', 'host-a')).toBe(true) expect(window.admit('assign:placement:queue-full', 'host-a')).toBe(true) - expect(window.admit('assign:sticky:host-rate-limited', 'host-a')).toBe(true) expect(window.admit('assign:placement:queue-full', 'host-a')).toBe(false) }) - - it('bounds the tracked keys and reports what an evicted window suppressed', () => { - const timers = manualTimers() - const closed: { key: string; suppressed: number }[] = [] - const window = new AssignmentRejectionLogWindow({ - windowMs: 10_000, - schedule: timers.schedule, - onWindowClosed: ({ key, suppressed }) => closed.push({ key, suppressed }) - }) - - window.admit('key-0', 'host-a') - window.admit('key-0', 'host-b') - for (let index = 1; index < 200; index++) window.admit(`key-${index}`, 'host-a') - - expect(closed[0]).toEqual({ key: 'key-0', suppressed: 1 }) - // Evicted keys emit again rather than staying silently suppressed. - expect(window.admit('key-0', 'host-a')).toBe(true) - expect(window.admit('key-199', 'host-a')).toBe(false) - }) }) function manualTimers(): { diff --git a/cloud/apps/relay/src/assignment/assignment-rejection-logging.test.ts b/cloud/apps/relay/src/assignment/assignment-rejection-logging.test.ts index d87c9879726..763d1a28d3f 100644 --- a/cloud/apps/relay/src/assignment/assignment-rejection-logging.test.ts +++ b/cloud/apps/relay/src/assignment/assignment-rejection-logging.test.ts @@ -15,11 +15,9 @@ vi.mock('../authentication/relay-token-verifier.js', () => ({ })) import { createRelayApp } from '../app.js' -import { relayHostLogDigest } from '../observability/relay-host-log-digest.js' describe('assignment rejection logging', () => { afterEach(() => { - vi.useRealTimers() vi.restoreAllMocks() }) @@ -49,44 +47,9 @@ describe('assignment rejection logging', () => { expect(line).toContain('lane=sticky') expect(line).toContain('hinted=true') expect(line).toContain('reason=relay_capacity_exhausted') - expect(line).toContain(`host=${relayHostLogDigest(host)}`) + expect(line).toContain('host=af342ad00ee4') expect(line).not.toContain(host) }) - - it('names the throttled host once per window and closes it with the suppressed count', async () => { - vi.useFakeTimers() - const host = 'mmmmmmmmmmmmmmmm' - const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) - const app = createRelayApp(config(), { - store: {} as never, - assignments: { - assign: vi.fn(async () => assignment('cell-r', host)), - resolve: vi.fn(async () => null) - } as never, - drain: vi.fn(), - ready: vi.fn(async () => true) - }) - - expect((await app.request('/v1/assign', assignmentRequest(host))).status).toBe(200) - expect((await app.request('/v1/assign', assignmentRequest(host))).status).toBe(503) - expect((await app.request('/v1/assign', assignmentRequest(host))).status).toBe(503) - - const throttled = (): string[] => - warn.mock.calls - .map((call) => String(call[0])) - .filter((entry) => entry.includes('reason=host-rate-limited')) - expect(throttled()).toHaveLength(1) - expect(throttled()[0]).toContain('route=assign') - expect(throttled()[0]).toContain('lane=placement') - expect(throttled()[0]).toContain(`host=${relayHostLogDigest(host)}`) - expect(throttled()[0]).not.toContain('suppressed=') - expect(throttled()[0]).not.toContain(host) - - await vi.advanceTimersByTimeAsync(10_000) - expect(throttled()).toHaveLength(2) - expect(throttled()[1]).toContain('suppressed=1') - expect(throttled()[1]).toContain(`host=${relayHostLogDigest(host)}`) - }) }) function assignmentRequest( diff --git a/cloud/apps/relay/src/assignment/assignment-reserve-mode-postgres.test.ts b/cloud/apps/relay/src/assignment/assignment-reserve-mode-postgres.test.ts index dcd1ecca97c..b02886d807d 100644 --- a/cloud/apps/relay/src/assignment/assignment-reserve-mode-postgres.test.ts +++ b/cloud/apps/relay/src/assignment/assignment-reserve-mode-postgres.test.ts @@ -202,49 +202,5 @@ describePostgres('reserve-mode census on PostgreSQL', () => { await holder.close() } }, 20_000) - - // A stall must not double connection demand: concurrent assigns share one reserve-set read, - // and after it fails the last answer stands for the backoff without another query. - it('reads the reserve set once for concurrent assigns during a stall, and not again in the backoff', async () => { - const timed = await openRelayDatabase({ databaseUrl, dataDir: '/tmp', statementTimeoutMs: 300 }) - const holder = await openRelayDatabase({ databaseUrl, dataDir: '/tmp' }) - let release!: () => void - const released = new Promise((resolve) => (release = resolve)) - let held!: () => void - const holding = new Promise((resolve) => (held = resolve)) - const lock = holder.transaction(async (transaction) => { - await transaction.query(`LOCK TABLE relay_cell_admit_modes IN ACCESS EXCLUSIVE MODE`) - held() - await released - }) - try { - await holding - const store = new RelayAssignmentStore(timed, () => 500_000) - await store.reconcileCells([cellA, cellB], false) - await heartbeat(store, cellA) - await heartbeat(store, cellB) - const query = timed.query.bind(timed) - let reads = 0 - timed.query = async (sql, params) => { - if (sql.includes('FROM relay_cell_admit_modes m')) reads += 1 - return await query(sql, params) - } - const placed = await Promise.all( - Array.from({ length: 6 }, async (_, index) => - await store.assign({ userId: USER, relayHostId: `hoststall0000${String(index).padStart(3, '0')}` }) - ) - ) - expect(placed).toHaveLength(6) - expect(reads).toBe(1) - await store.assign({ userId: USER, relayHostId: 'hoststall0000100' }) - expect(await store.reserveModeCells()).toBeNull() - expect(reads).toBe(1) - } finally { - release() - await lock - await timed.close() - await holder.close() - } - }, 20_000) }) diff --git a/cloud/apps/relay/src/assignment/assignment-reserve-mode.test.ts b/cloud/apps/relay/src/assignment/assignment-reserve-mode.test.ts index 33f90609601..94455f83ba7 100644 --- a/cloud/apps/relay/src/assignment/assignment-reserve-mode.test.ts +++ b/cloud/apps/relay/src/assignment/assignment-reserve-mode.test.ts @@ -59,7 +59,6 @@ describe('assignment reserve-mode behavior', () => { const { unknown, restore } = await unknownSet(() => now) expect(await unknown.reserveModeCells()).toBeNull() expect(await unknown.releaseExpiredActivityLeases()).toBe(0) - expect(await unknown.evacuateDeadCells()).toBe(0) await restore() await setReserve(store, []) expect(await store.releaseExpiredActivityLeases()).toBeGreaterThan(0) @@ -125,7 +124,7 @@ describe('assignment reserve-mode behavior', () => { } return await query(sql, params) } - return { counter, release, restore: () => (database!.query = query) } + return { release, restore: () => (database!.query = query) } } it('reads afresh after an admit-mode write instead of joining a read begun before it', async () => { @@ -155,12 +154,10 @@ describe('assignment reserve-mode behavior', () => { try { const before = store.reserveModeCells() await store.setCellAdmitMode('cell-a', 'reserve') - const reads = held.counter.reads held.release() await before // A write cleared the cache: the next caller reads now, not after a 5 s backoff. expect([...((await store.reserveModeCells()) ?? [])]).toEqual(['cell-a']) - expect(held.counter.reads).toBeGreaterThan(reads) } finally { held.release() held.restore() @@ -197,13 +194,8 @@ describe('assignment reserve-mode behavior', () => { it('never places a fresh host on a reserve-mode cell through the database', async () => { const store = await setup(() => 1_000, ['cell-a']) - for (let index = 0; index < 4; index += 1) { - const placed = await store.assign({ - userId: `user-${index}`, - relayHostId: `host${String(index).padStart(12, '0')}` - }) - expect(placed.cellId).toBe('cell-b') - } + const placed = await store.assign({ userId: 'user-0', relayHostId: 'host000000000000' }) + expect(placed.cellId).toBe('cell-b') }) it('refuses drains, evacuations and status reads that would treat a reserve-mode cell as empty', async () => { diff --git a/cloud/apps/relay/src/assignment/assignment-reserve-spill.test.ts b/cloud/apps/relay/src/assignment/assignment-reserve-spill.test.ts deleted file mode 100644 index d4e5c7fde3b..00000000000 --- a/cloud/apps/relay/src/assignment/assignment-reserve-spill.test.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { ASSIGNMENT_LIMITS } from '@orca-cloud/relay-contract/assignment-invariants' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { - RelayAssignmentStore, - type AssignOptions, - type RelayAssignment -} from './assignment-store.js' -import type { RelayCellConfig } from '../config.js' -import { - openInMemoryRelayDatabase, - type RelayDatabase, - type RelayLockOptions, - type RelayTransactionOptions, - type SqlRow -} from '../database/database.js' -import { ReservePaceError } from './reserve-placement/reserve-assignment.js' - -const CELLS: RelayCellConfig[] = [ - { id: 'us-c1', url: 'https://us-c1.relay.example.com', region: 'us-central1', capacityRequests: 100 }, - { id: 'asia-c1', url: 'https://asia-c1.relay.example.com', region: 'asia-east2', capacityRequests: 100 } -] -const IDENTITY = { userId: 'spill-user', relayHostId: 'spillhost0000001' } -const ON_US_RESERVE: RelayAssignment = { - ...IDENTITY, - cellId: 'us-reserve', - cellUrl: 'https://us-reserve.relay.example.com', - assignmentEpoch: 8, - leaseExpiresAt: 1_000_000, - region: 'us-central1' -} - -// Reports the target cell full when its reservation is taken: the race between pick and reserve. -class FullAtReserveDatabase implements RelayDatabase { - armed = false - - constructor(private readonly delegate: RelayDatabase) {} - - get dialect(): 'sqlite' | 'postgres' | undefined { - return this.delegate.dialect - } - - async query(sql: string, params?: unknown[]): Promise { - return await this.delegate.query(sql, params) - } - - async queryLocked(sql: string, params?: unknown[], options?: RelayLockOptions): Promise { - const rows = await this.delegate.queryLocked(sql, params, options) - if (!this.armed || sql.trim() !== 'SELECT * FROM relay_cells WHERE cell_id = ?') { - return rows - } - return rows.map((row) => ({ ...row, reserved_requests: row.capacity_requests })) - } - - async transaction(operation: (transaction: RelayDatabase) => Promise, options?: RelayTransactionOptions): Promise { - return await this.delegate.transaction(async (inner) => { - const wrapped = new FullAtReserveDatabase(inner) - wrapped.armed = this.armed - return await operation(wrapped) - }, options) - } - - async close(): Promise { - await this.delegate.close() - } -} - -describe('reserve spill before relay_capacity_exhausted', () => { - let database: FullAtReserveDatabase - let store: RelayAssignmentStore - let now: number - - beforeEach(async () => { - now = 100 - database = new FullAtReserveDatabase(await openInMemoryRelayDatabase()) - store = new RelayAssignmentStore(database, () => now) - await store.reconcileCells(CELLS) - }) - - afterEach(async () => { - vi.restoreAllMocks() - await database.close() - }) - - const closeAllDatabaseCells = async () => { - for (const cell of CELLS) { - await store.configureCell(cell, 'migration-only') - } - } - const assignAsia = async (step5: AssignOptions) => - await store.assign(IDENTITY, 'asia-east2', 'asia-east2', undefined, step5) - - it('books a reserve seat when no database cell has room anywhere', async () => { - await closeAllDatabaseCells() - const reserveSpill = vi.fn(async () => ON_US_RESERVE) - await expect(assignAsia({ reserveSpill })).resolves.toEqual({ ...ON_US_RESERVE, placed: true }) - expect(reserveSpill).toHaveBeenCalledWith({ skipOwnRegion: false, rowEpoch: 0 }) - expect(await database.query(`SELECT cell_id FROM relay_assignments`)).toEqual([]) - }) - - it('skips the own region after the in-region reserve fallback already tried it', async () => { - await closeAllDatabaseCells() - const regionReserve = vi.fn(async () => null) - const reserveSpill = vi.fn(async () => ON_US_RESERVE) - await expect(assignAsia({ regionReserve, reserveSpill })).resolves.toMatchObject({ cellId: 'us-reserve' }) - expect(regionReserve).toHaveBeenCalledOnce() - expect(reserveSpill).toHaveBeenCalledWith({ skipOwnRegion: true, rowEpoch: 0 }) - }) - - it('hands the spill the epoch of the host row it read', async () => { - const first = await assignAsia({}) - // Dormant: its next assign is a placement, which reads the row and finds no cell. - await store.changeActivity(IDENTITY, 'control', -1) - now += ASSIGNMENT_LIMITS.activityLeaseMs + 1 - await store.releaseExpiredActivityLeases() - await store.releaseExpiredActivity() - now += ASSIGNMENT_LIMITS.dormantTtlMs - await closeAllDatabaseCells() - const reserveSpill = vi.fn(async () => ON_US_RESERVE) - await assignAsia({ reserveSpill }) - expect(reserveSpill).toHaveBeenCalledWith({ skipOwnRegion: false, rowEpoch: first.assignmentEpoch }) - }) - - it('refuses with relay_capacity_exhausted when no reserve seat exists either', async () => { - await closeAllDatabaseCells() - await expect(assignAsia({ reserveSpill: async () => null })).rejects.toThrow('relay_capacity_exhausted') - }) - - it('answers a paced spill with its pace, not a refusal', async () => { - await closeAllDatabaseCells() - const reserveSpill = vi.fn(async () => { - throw new ReservePaceError(4) - }) - await expect(assignAsia({ reserveSpill })).rejects.toBeInstanceOf(ReservePaceError) - }) - - it('answers an in-region reserve pace before any spill', async () => { - await closeAllDatabaseCells() - const reserveSpill = vi.fn(async () => ON_US_RESERVE) - const regionReserve = async () => { - throw new ReservePaceError(2) - } - await expect(assignAsia({ regionReserve, reserveSpill })).rejects.toBeInstanceOf(ReservePaceError) - expect(reserveSpill).not.toHaveBeenCalled() - }) - - it('does not spill a reservation race: the host retries and re-picks', async () => { - database.armed = true - const reserveSpill = vi.fn(async () => ON_US_RESERVE) - await expect(assignAsia({ reserveSpill })).rejects.toThrow('relay_capacity_exhausted') - expect(reserveSpill).not.toHaveBeenCalled() - }) -}) diff --git a/cloud/apps/relay/src/assignment/assignment-roll-replaced-cell.test.ts b/cloud/apps/relay/src/assignment/assignment-roll-replaced-cell.test.ts index 0b5a00dd757..a30f59a07e7 100644 --- a/cloud/apps/relay/src/assignment/assignment-roll-replaced-cell.test.ts +++ b/cloud/apps/relay/src/assignment/assignment-roll-replaced-cell.test.ts @@ -143,18 +143,12 @@ async function setup(cells: RelayCellConfig[] = [SOURCE, TARGET]) { // The old process stopped heartbeating at START_MS, so it is unheard from here. const OLD_UNHEARD_AT = START_MS + HEARTBEAT_TTL_MS + 1_000 -// Both routes that refused in production: a hinted assign (placement lane after its -// verification read finds the cell not live) and resolve, which then assigns. async function hintedAssign(store: RelayAssignmentStore) { return await store.assign(IDENTITY, 'us-central1', 'us-central1', undefined, { hostRequested: true }) } -async function resolveRoute(store: RelayAssignmentStore) { - return (await store.resolve(IDENTITY)) ?? (await store.assign(IDENTITY)) -} - async function expectRefused( attempt: Promise, cause: 'not_ready' | 'unheard' @@ -164,10 +158,7 @@ async function expectRefused( expect(error).toMatchObject({ unavailableCause: cause }) } -describe.each([ - ['hinted assign', hintedAssign], - ['resolve', resolveRoute] -] as const)('a host offline through a roll redials via %s', (_route, redial) => { +describe('a host offline through a roll redials via hinted assign', () => { describe('while the replacement process boots (not ready)', () => { it('is re-placed when that process started after the roll isolated the cell', async () => { const { store, first, heartbeat, at, isolateForRoll } = await setup() @@ -175,7 +166,7 @@ describe.each([ await at(20_000) await heartbeat(SOURCE, { incarnation: NEW_INCARNATION, startedAt: 15_000, ready: false }) - const moved = await redial(store) + const moved = await hintedAssign(store) expect(moved).toMatchObject({ cellId: TARGET.id }) expect(moved.assignmentEpoch).toBeGreaterThan(first.assignmentEpoch) }) @@ -186,7 +177,7 @@ describe.each([ await at(20_000) await heartbeat(SOURCE, { ready: false }) - await expectRefused(redial(store), 'not_ready') + await expectRefused(hintedAssign(store), 'not_ready') }) it('is refused while the new process reports holding connections', async () => { @@ -200,7 +191,7 @@ describe.each([ totalConnections: 1 }) - await expectRefused(redial(store), 'not_ready') + await expectRefused(hintedAssign(store), 'not_ready') }) it('is refused once the new process itself goes unheard', async () => { @@ -210,7 +201,7 @@ describe.each([ await heartbeat(SOURCE, { incarnation: NEW_INCARNATION, startedAt: 15_000, ready: false }) await at(20_000 + HEARTBEAT_TTL_MS + 1_000) - await expectRefused(redial(store), 'unheard') + await expectRefused(hintedAssign(store), 'unheard') }) it('is refused on an ordinary readiness dip with no roll stamp', async () => { @@ -218,7 +209,7 @@ describe.each([ await at(20_000) await heartbeat(SOURCE, { incarnation: NEW_INCARNATION, startedAt: 15_000, ready: false }) - await expectRefused(redial(store), 'not_ready') + await expectRefused(hintedAssign(store), 'not_ready') }) it('is refused on a migration-only cell parked without a roll stamp', async () => { @@ -227,7 +218,7 @@ describe.each([ await at(20_000) await heartbeat(SOURCE, { incarnation: NEW_INCARNATION, startedAt: 15_000, ready: false }) - await expectRefused(redial(store), 'not_ready') + await expectRefused(hintedAssign(store), 'not_ready') }) }) @@ -238,7 +229,7 @@ describe.each([ await isolateForRoll() await at(OLD_UNHEARD_AT) - await expectRefused(redial(store), 'unheard') + await expectRefused(hintedAssign(store), 'unheard') }) it('is refused when the roll stamp is stale, even with a new process', async () => { @@ -248,7 +239,7 @@ describe.each([ await at(stale) await heartbeat(SOURCE, { incarnation: NEW_INCARNATION, startedAt: stale - 1, ready: false }) - await expectRefused(redial(store), 'not_ready') + await expectRefused(hintedAssign(store), 'not_ready') }) }) diff --git a/cloud/apps/relay/src/assignment/assignment-row-lock-order-postgres.test.ts b/cloud/apps/relay/src/assignment/assignment-row-lock-order-postgres.test.ts deleted file mode 100644 index e05f3f2310c..00000000000 --- a/cloud/apps/relay/src/assignment/assignment-row-lock-order-postgres.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { afterAll, beforeAll, describe, expect, it } from 'vitest' -import { RelayAssignmentStore } from './assignment-store.js' -import { openRelayDatabase, type RelayDatabase } from '../database/database.js' - -const databaseUrl = process.env.ORCA_RELAY_TEST_POSTGRES_URL -const describePostgres = databaseUrl ? describe : describe.skip - -const cells = [ - { - id: 'row-lock-order-a', - url: 'https://row-lock-order-a.example.com', - capacityRequests: 200, - connectionHardCap: 600 as const, - connectionUnobservedBound: 50 - }, - { - id: 'row-lock-order-b', - url: 'https://row-lock-order-b.example.com', - capacityRequests: 200, - connectionHardCap: 600 as const, - connectionUnobservedBound: 50 - } -] -const userId = 'row-lock-order-user' -const hosts = ['rowlockhost00001', 'rowlockhost00002', 'rowlockhost00003', 'rowlockhost00004'].map( - (relayHostId) => ({ userId, relayHostId }) -) - -// pg_stat_database exposes cycles hidden by three 40P01 retries; eventual success alone misses deadlocks that exhaust retries under load. -describePostgres('PostgreSQL row lock order', () => { - const databases: RelayDatabase[] = [] - - beforeAll(async () => { - for (let index = 0; index < 4; index++) { - databases.push(await openRelayDatabase({ databaseUrl, dataDir: '' })) - } - }) - - async function removeTestRows(database: RelayDatabase): Promise { - await database.query( - `DELETE FROM relay_control_connection_reservations WHERE user_id = ?`, - [userId] - ) - for (const table of [ - 'relay_control_capabilities', - 'relay_assignment_activity_leases', - 'relay_post_drain_migration_pins', - 'relay_assignment_migration_incarnations', - 'relay_assignment_migrations', - 'relay_assignments' - ]) { - await database.query(`DELETE FROM ${table} WHERE user_id = ?`, [userId]) - } - for (const cell of cells) { - for (const table of [ - 'relay_cell_connection_snapshots', - 'relay_cell_connection_runtime', - 'relay_cell_connection_limits', - 'relay_cell_runtime', - 'relay_cells' - ]) { - await database.query(`DELETE FROM ${table} WHERE cell_id = ?`, [cell.id]) - } - } - } - - afterAll(async () => { - if (databases[0]) await removeTestRows(databases[0]) - for (const connection of databases) await connection.close() - }) - - async function deadlockCount(): Promise { - const rows = await databases[0]!.query( - `SELECT deadlocks FROM pg_stat_database WHERE datname = current_database()` - ) - return Number(rows[0]!.deadlocks) - } - - it('runs the cell accept and the placement retry concurrently without a cycle', async () => { - await removeTestRows(databases[0]!) - const stores = databases.map((database) => new RelayAssignmentStore(database, () => 100)) - await stores[0]!.reconcileCells(cells) - for (const cell of cells) { - await stores[0]!.recordCellHeartbeat({ - cellId: cell.id, - cellUrl: cell.url, - cellIncarnation: '11111111-1111-4111-8111-111111111111', - startedAt: 50, - ready: true, - observedRequests: 0, - totalConnections: 0, - inFlightConnections: 0, - reservedConnectionUnits: 0, - enforcedConnectionUnits: 0, - connectionInclusionWatermark: 1, - connectionHardCap: 600, - connectionUnobservedBound: 50 - }) - } - const placements = new Map() - for (const identity of hosts) { - const assignment = await stores[0]!.assign(identity) - placements.set(identity.relayHostId, assignment) - } - - const before = await deadlockCount() - // The accept path (host rows, then the cell row last) against the paths - // that must read the inventory first: placement and evacuation. - for (let round = 0; round < 12; round++) { - await Promise.allSettled( - hosts.flatMap((identity, index) => { - const placement = placements.get(identity.relayHostId)! - const store = stores[index % stores.length]! - const other = stores[(index + 1) % stores.length]! - return [ - store.activateControl(identity, { - cellId: placement.cellId, - assignmentEpoch: placement.assignmentEpoch, - generation: round + 2 - }), - other.assign(identity), - other.startEvacuation( - identity, - placement.cellId === cells[0]!.id ? cells[1]!.id : cells[0]!.id - ) - ] - }) - ) - } - const after = await deadlockCount() - - expect(after - before).toBe(0) - }, 120_000) -}) diff --git a/cloud/apps/relay/src/assignment/migrations/dead-cell-evacuation-precheck.test.ts b/cloud/apps/relay/src/assignment/migrations/dead-cell-evacuation-precheck.test.ts index 9d72905967d..4a1a2aff115 100644 --- a/cloud/apps/relay/src/assignment/migrations/dead-cell-evacuation-precheck.test.ts +++ b/cloud/apps/relay/src/assignment/migrations/dead-cell-evacuation-precheck.test.ts @@ -5,46 +5,16 @@ import type { RelayCellConfig } from '../../config.js' import { openInMemoryRelayDatabase, openRelayDatabase, - type RelayDatabase, - type SqlRow + type RelayDatabase } from '../../database/database.js' -// Both dialects must skip host scans with no actionable cells; stale existing-only rows previously scanned every assignment for nothing. const databaseUrl = process.env.ORCA_RELAY_TEST_POSTGRES_URL const schema = 'relay_dead_cell_precheck_test' -const HOST_QUERY = 'FROM relay_assignments assignment' const CELLS: RelayCellConfig[] = [ { id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 }, { id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 } ] -class RecordingDatabase implements RelayDatabase { - readonly statements: string[] = [] - - constructor(private readonly delegate: RelayDatabase) {} - - get dialect() { - return this.delegate.dialect - } - - async query(sql: string, params: unknown[] = []): Promise { - this.statements.push(sql) - return await this.delegate.query(sql, params) - } - - async queryLocked(...args: Parameters): Promise { - return await this.delegate.queryLocked(...args) - } - - async transaction(operation: (transaction: RelayDatabase) => Promise): Promise { - return await this.delegate.transaction(operation) - } - - async close(): Promise { - await this.delegate.close() - } -} - function scopedUrl(): string { const url = new URL(databaseUrl!) url.searchParams.set('options', `-c search_path=${schema}`) @@ -91,13 +61,12 @@ describe.each(dialects)('dead-cell evacuation pre-check (%s)', (_dialect, open) async function setup(now: () => number) { database = await open() - const recording = new RecordingDatabase(database) - const store = new RelayAssignmentStore(recording, now, { + const store = new RelayAssignmentStore(database, now, { requireLiveCells: true, heartbeatTtlMs: 45_000 }) await store.reconcileCells(CELLS) - return { store, recording } + return { store } } async function heartbeat(store: RelayAssignmentStore, cell: RelayCellConfig): Promise { @@ -111,31 +80,9 @@ describe.each(dialects)('dead-cell evacuation pre-check (%s)', (_dialect, open) }) } - it('skips the host query when the only dead cell is unfenced and existing-only', async () => { - let now = 100 - const { store, recording } = await setup(() => now) - for (const cell of CELLS) await heartbeat(store, cell) - const identity = { userId: 'user-a', relayHostId: 'host000000000001' } - await store.setCellEnabled('cell-b', false) - expect(await store.assign(identity)).toMatchObject({ cellId: 'cell-a' }) - await store.setCellEnabled('cell-b', true) - await store.setCellEnabled('cell-a', false) - now += 45_001 - await heartbeat(store, CELLS[1]!) - recording.statements.length = 0 - - expect(await store.evacuateDeadCells()).toBe(0) - expect(recording.statements.some((sql) => sql.includes(HOST_QUERY))).toBe(false) - expect( - await database!.query(`SELECT cell_id FROM relay_assignments WHERE user_id = ?`, [ - identity.userId - ]) - ).toEqual([{ cell_id: 'cell-a' }]) - }) - it('still evacuates hosts from a dead uncapped cell that admits', async () => { let now = 100 - const { store, recording } = await setup(() => now) + const { store } = await setup(() => now) for (const cell of CELLS) await heartbeat(store, cell) const identity = { userId: 'user-a', relayHostId: 'host000000000001' } await store.setCellEnabled('cell-b', false) @@ -143,10 +90,8 @@ describe.each(dialects)('dead-cell evacuation pre-check (%s)', (_dialect, open) await store.setCellEnabled('cell-b', true) now += 45_001 await heartbeat(store, CELLS[1]!) - recording.statements.length = 0 expect(await store.evacuateDeadCells()).toBe(1) - expect(recording.statements.some((sql) => sql.includes(HOST_QUERY))).toBe(true) expect((await store.resolve(identity))?.cellId).toBe('cell-b') }) }) diff --git a/cloud/apps/relay/src/assignment/migrations/drain-migration-row-lookup.test.ts b/cloud/apps/relay/src/assignment/migrations/drain-migration-row-lookup.test.ts deleted file mode 100644 index 9f1831d4d92..00000000000 --- a/cloud/apps/relay/src/assignment/migrations/drain-migration-row-lookup.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { expect, it } from 'vitest' -import { createDrainMigrationRowLookup } from './drain-migration-row-lookup.js' -import type { SqlRow } from '../../database/database.js' - -function text(row: SqlRow, field: string): string { - const value = row[field] - if (typeof value !== 'string') { - throw new Error(`invalid_${field}`) - } - return value -} - -it('keeps first assignment matches, lease order, row identity, and separate identity components', () => { - const rows = [ - { user_id: 'a:b', relay_host_id: 'c', value: 1 }, - { user_id: 'a', relay_host_id: 'b:c', value: 2 }, - { user_id: 'a:b', relay_host_id: 'c', value: 3 }, - { user_id: '', relay_host_id: '', value: 4 } - ] - const lookup = createDrainMigrationRowLookup(rows, text) - for (const identity of [ - { userId: 'a:b', relayHostId: 'c' }, - { userId: 'a', relayHostId: 'b:c' }, - { userId: '', relayHostId: '' }, - { userId: 'missing', relayHostId: 'c' } - ]) { - const expected = rows.filter( - (row) => - row.user_id === identity.userId && row.relay_host_id === identity.relayHostId - ) - expect(lookup.first(identity)).toBe(expected[0]) - expect(lookup.all(identity)).toEqual(expected) - lookup.all(identity).forEach((row, index) => expect(row).toBe(expected[index])) - } -}) - -it('retains lazy validation and short circuiting when an inventory is malformed', () => { - const first = { user_id: 'user', relay_host_id: 'host' } - const identity = { userId: 'user', relayHostId: 'host' } - const lookup = createDrainMigrationRowLookup( - [first, { user_id: null, relay_host_id: 'bad' }], - text - ) - expect(lookup.first(identity)).toBe(first) - expect(() => lookup.all(identity)).toThrow('invalid_user_id') - const unrelated = createDrainMigrationRowLookup( - [first, { user_id: 'other', relay_host_id: null }], - text - ) - expect(unrelated.all(identity)).toEqual([first]) - expect(() => unrelated.first({ userId: 'other', relayHostId: 'host' })).toThrow( - 'invalid_relay_host_id' - ) -}) diff --git a/cloud/apps/relay/src/assignment/migrations/drain-migration-row-scaling.test.ts b/cloud/apps/relay/src/assignment/migrations/drain-migration-row-scaling.test.ts deleted file mode 100644 index 6eed78f80a1..00000000000 --- a/cloud/apps/relay/src/assignment/migrations/drain-migration-row-scaling.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { ASSIGNMENT_LIMITS } from '@orca-cloud/relay-contract/assignment-invariants' -import { afterEach, expect, it } from 'vitest' -import { RelayAssignmentStore } from '../assignment-store.js' -import { openInMemoryRelayDatabase, type RelayDatabase, type SqlRow } from '../../database/database.js' - -let database: RelayDatabase | undefined -afterEach(async () => await database?.close()) - -it.each([false, true])( - 'looks up a whole drain inventory with linear identity reads (expired: %s)', - async (expired) => { - database = await openInMemoryRelayDatabase() - let measuring = false - let identityReads = 0 - let indexedRows = 0 - const instrument = (delegate: RelayDatabase): RelayDatabase => ({ - dialect: delegate.dialect, - query: (sql, params) => delegate.query(sql, params), - queryLocked: async (sql, params, options) => { - const rows = await delegate.queryLocked(sql, params, options) - if ( - !measuring || - !sql.includes('WHERE EXISTS') || - !(sql.includes('SELECT assignment.*') || sql.includes('SELECT lease.*')) - ) { - return rows - } - indexedRows += rows.length - return rows.map( - (row): SqlRow => - new Proxy(row, { - get(target, key) { - if (key === 'user_id' || key === 'relay_host_id') { - identityReads++ - } - return Reflect.get(target, key) - } - }) - ) - }, - transaction: (operation, options) => - delegate.transaction((tx) => operation(instrument(tx)), options), - close: () => delegate.close() - }) - let now = 100 - const store = new RelayAssignmentStore(instrument(database), () => now, { - requireLiveCells: true - }) - const cells = ['a', 'b'].map((id) => ({ - id: `cell-${id}`, - url: `https://relay-${id}.example.com`, - capacityRequests: 500 - })) - await store.reconcileCells(cells) - const incarnation = '11111111-1111-4111-8111-111111111111' - for (const cell of cells) { - await store.recordCellHeartbeat({ - cellId: cell.id, - cellUrl: cell.url, - cellIncarnation: incarnation, - startedAt: 50, - ready: true, - observedRequests: 0 - }) - } - await store.setCellEnabled('cell-b', false) - const identities = Array.from({ length: 50 }, (_, index) => ({ - userId: `user-${index % 5}`, - relayHostId: `host${String(index).padStart(12, '0')}` - })) - for (const identity of identities) { - await store.assign(identity) - } - await store.setCellEnabled('cell-b', true) - await store.setCellEnabled('cell-a', false) - for (const identity of identities) { - const migration = await store.startEvacuation(identity, 'cell-b') - await store.markMigrationTargetRegistered(identity, { - cellId: 'cell-b', - assignmentEpoch: migration.assignmentEpoch - }) - } - const attempt = { - attemptId: '55555555-5555-4555-8555-555555555555', - cellId: 'cell-a', - cellIncarnation: incarnation, - traceValue: '66666666-6666-4666-8666-666666666666', - plannedGraceMs: 120_000 - } - await store.prepareCellDrainAttempt(attempt) - if (expired) { - now += ASSIGNMENT_LIMITS.migrationLeaseMs + 1 - await store.releaseExpiredActivityLeases() - for (const cell of cells) { - await store.recordCellHeartbeat({ - cellId: cell.id, - cellUrl: cell.url, - cellIncarnation: incarnation, - startedAt: 50, - ready: true, - observedRequests: 0 - }) - } - } - measuring = true - await expect(store.beginCellDrainSend(attempt)).resolves.toMatchObject({ - state: 'send-may-have-started', - shouldSend: true - }) - expect(indexedRows).toBeGreaterThanOrEqual(100) - expect(identityReads).toBeLessThanOrEqual(indexedRows * 2) - const migrations = await database.query( - 'SELECT expires_at FROM relay_assignment_migrations' - ) - expect(migrations).toHaveLength(50) - expect( - migrations.every( - (row) => row.expires_at === now + ASSIGNMENT_LIMITS.migrationLeaseMs - ) - ).toBe(true) - } -) diff --git a/cloud/apps/relay/src/assignment/migrations/drain-release-cell-row-contention-postgres.test.ts b/cloud/apps/relay/src/assignment/migrations/drain-release-cell-row-contention-postgres.test.ts deleted file mode 100644 index 0008023fcc7..00000000000 --- a/cloud/apps/relay/src/assignment/migrations/drain-release-cell-row-contention-postgres.test.ts +++ /dev/null @@ -1,282 +0,0 @@ -import { createHash } from 'node:crypto' -import { afterAll, beforeAll, describe, expect, it } from 'vitest' -import { RelayAssignmentStore } from '../assignment-store.js' -import { - encodeMembership, - type CellAdmissionMembership, - type CellAdmissionState -} from '../../cell/admission/cell-admission-selector.js' -import type { RelayCellConfig } from '../../config.js' -import { openRelayDatabase, type RelayDatabase } from '../../database/database.js' - -const databaseUrl = process.env.ORCA_RELAY_TEST_POSTGRES_URL -const describePostgres = databaseUrl ? describe : describe.skip -const DIRECTOR_INSTANCES = 5 -const DIRECTOR_POOL_MAX = 3 -const DIRECTOR_APPLICATION = 'drain-release-contention-director' - -const USER_PREFIX = 'drain-release-contention' -const NOW = 1_000_000 -const CAPPED = { - capacityRequests: 6_000, - connectionHardCap: 3_000, - connectionUnobservedBound: 60 -} as const -const SOURCE: RelayCellConfig = { - id: 'drain-release-contention-a-source', - url: 'https://drain-release-contention-source.example.test', - region: 'asia-east2', - ...CAPPED -} -const TARGETS: RelayCellConfig[] = ['b', 'c', 'd'].map((suffix) => ({ - id: `drain-release-contention-${suffix}-target`, - url: `https://drain-release-contention-${suffix}-target.example.test`, - region: 'asia-east2', - ...CAPPED -})) -// Next to the database. Taken only once every Asia neighbour is at its cap. -const US_CELL: RelayCellConfig = { - id: 'drain-release-contention-e-us', - url: 'https://drain-release-contention-us.example.test', - region: 'us-central1', - ...CAPPED -} -const CELLS = [SOURCE, ...TARGETS, US_CELL] - -type Identity = { userId: string; relayHostId: string } -type Instance = { database: RelayDatabase; store: RelayAssignmentStore } - -function hostIdentity(index: number): Identity { - return { - userId: `${USER_PREFIX}-${index}`, - // Relay host ids are fixed-width opaque ids. - relayHostId: `drainrel${String(index).padStart(8, '0')}` - } -} - -describePostgres('PostgreSQL drain releases against director placement', () => { - const storeOptions = { requireLiveCells: true, heartbeatTtlMs: 45_000 } - const directors: Instance[] = [] - let observer: RelayDatabase - - function admin(): Instance { - return directors[0]! - } - - async function applySelector( - states: Record, - rollIsolatedCells?: string[] - ): Promise { - const current = await admin().store.inspectCellAdmissionSelector() - // Built from relay_cells: the apply requires exact coverage of a shared fleet. - const fleet = await admin().database.query( - `SELECT cell_id FROM relay_cells ORDER BY cell_id ASC` - ) - const membership: CellAdmissionMembership = { - existingOnly: [], - migrationOnly: [], - general: [] - } - for (const row of fleet) { - const cellId = String(row['cell_id']) - const state = - states[cellId] ?? - (current.selector.membership.existingOnly.includes(cellId) - ? 'existing-only' - : current.selector.membership.migrationOnly.includes(cellId) - ? 'migration-only' - : 'general') - if (state === 'existing-only') membership.existingOnly.push(cellId) - else if (state === 'migration-only') membership.migrationOnly.push(cellId) - else membership.general.push(cellId) - } - await admin().store.applyCellAdmissionSelector({ - attemptId: `drain-release-${current.selector.generation}`, - expectedGeneration: current.selector.generation, - ...(current.selector.generation === 0 - ? { - expectedMembershipSha256: createHash('sha256') - .update(encodeMembership(current.selector.membership)) - .digest('hex') - } - : {}), - membership, - ...(rollIsolatedCells ? { rollIsolatedCells } : {}) - }) - } - - // Other Postgres files write admission through the generation-0 helpers, so - // the selector goes back to generation 0 exactly as found. - async function resetSelectorBoundary(): Promise { - await admin().database.query( - `UPDATE relay_admission_selectors SET generation = 0, attempt_id = NULL - WHERE selector_id = 'general'` - ) - await admin().database.query( - `DELETE FROM relay_admission_selector_intents WHERE attempt_id LIKE 'drain-release-%'` - ) - await admin().store.reconcileCells([], false) - } - - async function deleteHostRows(): Promise { - for (const table of [ - 'relay_control_capabilities', - 'relay_control_connection_reservations', - 'relay_assignment_activity_leases', - 'relay_assignment_migrations', - 'relay_assignment_region_preferences', - 'relay_assignments' - ]) { - await admin().database.query(`DELETE FROM ${table} WHERE user_id LIKE '${USER_PREFIX}-%'`) - } - } - - // `atCap` reports those cells at their connection cap: no headroom left. - async function heartbeatAll(atCap: RelayCellConfig[] = []): Promise { - for (const [index, config] of CELLS.entries()) { - await admin().store.recordCellHeartbeat({ - cellId: config.id, - cellUrl: config.url, - cellIncarnation: `2222222${index}-2222-4222-8222-222222222222`, - startedAt: NOW - 1_000, - ready: true, - observedRequests: 0, - region: config.region, - // The cell checks enforced = total + in-flight + reserved. - totalConnections: atCap.includes(config) ? CAPPED.connectionHardCap : 0, - inFlightConnections: 0, - reservedConnectionUnits: 0, - enforcedConnectionUnits: atCap.includes(config) ? CAPPED.connectionHardCap : 0, - connectionHardCap: CAPPED.connectionHardCap, - connectionUnobservedBound: CAPPED.connectionUnobservedBound - }) - } - } - - // Holds `cellIds`' relay_cells rows in an open transaction until the returned release runs. - async function holdCellRows(cellIds: string[]): Promise<() => Promise> { - let release!: () => void - const released = new Promise((resolve) => (release = resolve)) - let held!: () => void - const acquired = new Promise((resolve) => (held = resolve)) - const holder = observer.transaction(async (transaction) => { - await transaction.queryLocked( - `SELECT cell_id FROM relay_cells WHERE cell_id IN (${cellIds.map(() => '?').join(', ')}) - ORDER BY cell_id ASC`, - cellIds - ) - held() - await released - }) - await acquired - return async () => { - release() - await holder - } - } - - // Every host holds a live control lease on the source, then the source is - // isolated for a roll: the state an Asia drain starts from. - async function seed(hosts: Identity[]): Promise { - await deleteHostRows() - await admin().database.query( - `UPDATE relay_cells SET reserved_requests = 0 WHERE cell_id LIKE '${USER_PREFIX}-%'` - ) - // Restoring the source to general also clears the previous run's roll stamp. - await applySelector({ - [SOURCE.id]: 'general', - ...Object.fromEntries(TARGETS.map((target) => [target.id, 'migration-only' as const])) - }) - for (const identity of hosts) { - const grant = await admin().store.assign(identity, 'asia-east2', 'asia-east2') - expect(grant.cellId).toBe(SOURCE.id) - await admin().store.activateControl(identity, { - cellId: SOURCE.id, - assignmentEpoch: grant.assignmentEpoch, - generation: 1 - }) - } - await applySelector( - Object.fromEntries(TARGETS.map((target) => [target.id, 'general' as const])) - ) - await applySelector({ [SOURCE.id]: 'migration-only' }, [SOURCE.id]) - } - - beforeAll(async () => { - for (let index = 0; index < DIRECTOR_INSTANCES; index += 1) { - const database = await openRelayDatabase({ - databaseUrl, - dataDir: '', - poolMax: DIRECTOR_POOL_MAX, - applicationName: DIRECTOR_APPLICATION - }) - directors.push({ - database, - store: new RelayAssignmentStore(database, () => NOW, storeOptions) - }) - } - observer = await openRelayDatabase({ databaseUrl, dataDir: '', poolMax: 1 }) - await deleteHostRows() - await admin().store.reconcileCells(CELLS, false) - await resetSelectorBoundary() - await heartbeatAll() - }) - - afterAll(async () => { - if (directors.length > 0) { - await deleteHostRows() - // Cells before the selector rebuild, or its membership names missing rows. - for (const config of CELLS) { - for (const table of [ - 'relay_cell_connection_snapshots', - 'relay_cell_connection_runtime', - 'relay_cell_connection_limits', - 'relay_cell_runtime', - 'relay_cell_admission', - 'relay_cell_regions', - 'relay_cells' - ]) { - await admin().database.query(`DELETE FROM ${table} WHERE cell_id = ?`, [config.id]) - } - } - await resetSelectorBoundary() - } - for (const instance of directors) { - await instance.database.close() - } - await observer?.close() - }) - - it('places a reconnecting host off the isolated source while that source row is held', async () => { - await heartbeatAll() - const host = hostIdentity(9_000) - await seed([host]) - // A drain release holds the source row for its whole transaction; target-only placement never asks for it. - const sourceHeld = await holdCellRows([SOURCE.id]) - try { - const grant = await Promise.race([ - directors[1]!.store.assign(host, 'asia-east2', 'asia-east2'), - new Promise<'blocked'>((resolve) => setTimeout(() => resolve('blocked'), 3_000)) - ]) - expect(grant).not.toBe('blocked') - expect(TARGETS.map(({ id }) => id)).toContain(grant === 'blocked' ? null : grant.cellId) - } finally { - await sourceHeld() - } - // Control: the same hold on the target rows does block a placement, so the hold above was real. - const other = hostIdentity(9_001) - await seed([other]) - const targetsHeld = await holdCellRows(TARGETS.map(({ id }) => id)) - const placement = directors[2]!.store.assign(other, 'asia-east2', 'asia-east2') - try { - const raced = await Promise.race([ - placement.then(() => 'placed' as const), - new Promise<'blocked'>((resolve) => setTimeout(() => resolve('blocked'), 1_000)) - ]) - expect(raced).toBe('blocked') - } finally { - await targetsHeld() - } - expect((await placement).cellId).not.toBe(SOURCE.id) - }, 30_000) -}) diff --git a/cloud/apps/relay/src/assignment/migrations/migration-recovery-postgres.test.ts b/cloud/apps/relay/src/assignment/migrations/migration-recovery-postgres.test.ts index 5d6a099ddc4..5e0cc1988b5 100644 --- a/cloud/apps/relay/src/assignment/migrations/migration-recovery-postgres.test.ts +++ b/cloud/apps/relay/src/assignment/migrations/migration-recovery-postgres.test.ts @@ -256,7 +256,7 @@ describePostgres('PostgreSQL migration recovery', () => { expect( await database.query( `SELECT assignment.cell_id, assignment.assignment_epoch, - migration.completed_at, migration.aborted_at + (migration.completed_at IS NOT NULL) AS completed, migration.aborted_at FROM relay_assignments assignment JOIN relay_assignment_migrations migration ON migration.user_id = assignment.user_id @@ -269,7 +269,7 @@ describePostgres('PostgreSQL migration recovery', () => { { cell_id: input.cells.failed.id, assignment_epoch: '2', - completed_at: String(100 + 45_001 + ASSIGNMENT_LIMITS.migrationLeaseMs + 1), + completed: true, aborted_at: null } ]) @@ -309,7 +309,7 @@ describePostgres('PostgreSQL migration recovery', () => { expect( await database.query( `SELECT assignment.cell_id, assignment.assignment_epoch, - migration.completed_at, migration.aborted_at + migration.completed_at, (migration.aborted_at IS NOT NULL) AS aborted FROM relay_assignments assignment JOIN relay_assignment_migrations migration ON migration.user_id = assignment.user_id @@ -323,9 +323,7 @@ describePostgres('PostgreSQL migration recovery', () => { cell_id: input.cells.source.id, assignment_epoch: '3', completed_at: null, - aborted_at: String( - 100 + ASSIGNMENT_LIMITS.migrationLeaseMs + STRANDED_MIGRATION_ABANDON_MS + 1 - ) + aborted: true } ]) }) @@ -394,63 +392,6 @@ describePostgres('PostgreSQL migration recovery', () => { ]) }) - it('fails a competing aggregate supersession instead of reporting the wrong target', async () => { - const input = await fixture() - const alternate = { - id: `recovery-cell-${sequence}-alternate`, - url: `https://recovery-${sequence}-alternate.example.com`, - capacityRequests: 20 - } - await input.store.reconcileCells([...Object.values(input.cells), alternate], false) - await input.heartbeat(alternate) - await input.store.setCellEnabled(input.cells.failed.id, false) - input.advancePastHeartbeat() - await input.heartbeat(input.cells.source) - await input.heartbeat(input.cells.replacement) - await input.heartbeat(alternate) - await input.store.attestCellFence(input.cells.failed.id, input.cellIncarnation) - - const results = await Promise.allSettled([ - input.store.supersedeRegisteredCellEvacuations( - input.cells.source.id, - input.cells.failed.id, - input.cells.replacement.id, - 100 - ), - input.store.supersedeRegisteredCellEvacuations( - input.cells.source.id, - input.cells.failed.id, - alternate.id, - 100 - ) - ]) - const fulfilled = results.filter( - (result): result is PromiseFulfilledResult => result.status === 'fulfilled' - ) - const rejected = results.filter( - (result): result is PromiseRejectedResult => result.status === 'rejected' - ) - expect(fulfilled).toHaveLength(1) - expect(fulfilled[0]!.value).toBe(1) - expect(rejected).toHaveLength(1) - expect(rejected[0]!.reason).toMatchObject({ message: 'migration_already_superseded' }) - const successor = await database.query( - `SELECT assignment.cell_id, migration.target_cell_id - FROM relay_assignments assignment - JOIN relay_assignment_migrations migration - ON migration.user_id = assignment.user_id - AND migration.relay_host_id = assignment.relay_host_id - AND migration.assignment_epoch = assignment.assignment_epoch - WHERE assignment.user_id = ? AND migration.previous_epoch = ?`, - [input.identity.userId, input.migration.assignmentEpoch] - ) - expect(successor).toHaveLength(1) - expect(successor[0]!.cell_id).toBe(successor[0]!.target_cell_id) - expect([input.cells.replacement.id, alternate.id]).toContain( - successor[0]!.target_cell_id - ) - }) - it('rolls back every supersession change when replacement capacity is insufficient', async () => { const input = await fixture(1) await input.store.setCellEnabled(input.cells.failed.id, false) diff --git a/cloud/apps/relay/src/assignment/migrations/postgres-drain-send-locking.test.ts b/cloud/apps/relay/src/assignment/migrations/postgres-drain-send-locking.test.ts index 5b75ce6ecba..ec6745e2c07 100644 --- a/cloud/apps/relay/src/assignment/migrations/postgres-drain-send-locking.test.ts +++ b/cloud/apps/relay/src/assignment/migrations/postgres-drain-send-locking.test.ts @@ -184,7 +184,7 @@ describePostgres('PostgreSQL drain-send locking', () => { }) now = migration.expiresAt + 1 - expect(await store.releaseExpiredActivityLeases()).toBeGreaterThan(0) + await store.releaseExpiredActivityLeases() await store.recordCellHeartbeat({ cellId: source.id, cellUrl: source.url, @@ -207,7 +207,7 @@ describePostgres('PostgreSQL drain-send locking', () => { cellId: source.id, cellIncarnation: sourceIncarnation }) - ).resolves.toMatchObject({ state: 'send-may-have-started', shouldSend: true }) + ).resolves.toMatchObject({ shouldSend: true }) await expect( database.query( `SELECT activity_kind, cell_id FROM relay_assignment_activity_leases diff --git a/cloud/apps/relay/src/assignment/migrations/registered-migration-inventory-postgres.test.ts b/cloud/apps/relay/src/assignment/migrations/registered-migration-inventory-postgres.test.ts deleted file mode 100644 index 3a4e02db149..00000000000 --- a/cloud/apps/relay/src/assignment/migrations/registered-migration-inventory-postgres.test.ts +++ /dev/null @@ -1,139 +0,0 @@ -import pg from 'pg' -import { afterAll, beforeAll, describe, expect, it } from 'vitest' -import { openRelayDatabase, type RelayDatabase } from '../../database/database.js' -import { REGISTERED_MIGRATION_ABANDON_MS } from './registered-migration-abandonment.js' -import { readRegisteredMigrationInventory } from './registered-migration-inventory.js' - -const databaseUrl = process.env.ORCA_RELAY_TEST_POSTGRES_URL -const describePostgres = databaseUrl ? describe : describe.skip -const schema = 'relay_migration_inventory_test' - -describePostgres('PostgreSQL registered migration inventory', () => { - let database: RelayDatabase | undefined - - beforeAll(async () => { - const client = new pg.Client({ connectionString: databaseUrl }) - await client.connect() - try { - await client.query(`DROP SCHEMA IF EXISTS ${schema} CASCADE`) - await client.query(`CREATE SCHEMA ${schema}`) - } finally { - await client.end() - } - const url = new URL(databaseUrl!) - url.searchParams.set('options', `-c search_path=${schema}`) - database = await openRelayDatabase({ databaseUrl: url.toString(), dataDir: '' }) - }) - - afterAll(async () => { - await database?.close() - const client = new pg.Client({ connectionString: databaseUrl }) - await client.connect() - try { - await client.query(`DROP SCHEMA IF EXISTS ${schema} CASCADE`) - } finally { - await client.end() - } - }) - - it('counts a disabled-target migration even while its source is active', async () => { - const now = REGISTERED_MIGRATION_ABANDON_MS + 10_000 - await database!.query( - `INSERT INTO relay_cells - (cell_id, cell_url, enabled, capacity_requests, reserved_requests, - observed_requests, last_heartbeat_at, updated_at) - VALUES ('cell-a', 'https://a.example.test', 1, 4000, 1, 0, 0, 1), - ('cell-b', 'https://b.example.test', 0, 4000, 0, 0, 0, 1)` - ) - await database!.query( - `INSERT INTO relay_cell_admission (cell_id, admission_state, updated_at) - VALUES ('cell-a', 'general', 1), ('cell-b', 'existing-only', 1)` - ) - await database!.query( - `INSERT INTO relay_assignments - (user_id, relay_host_id, cell_id, assignment_epoch, lease_expires_at, - last_activity_at, reserved_controls, reserved_splices, reserved_invites, - pending_installs, pending_confirmations, migration_leases) - VALUES ('user-1', '111111111111', 'cell-b', 2, ?, ?, 0, 0, 0, 0, 0, 0)`, - [now, now] - ) - await database!.query( - `INSERT INTO relay_assignment_activity_leases - (user_id, relay_host_id, activity_id, activity_kind, cell_id, - request_units, expires_at, updated_at) - VALUES ('user-1', '111111111111', 'control:source:1', 'control', 'cell-a', 1, ?, ?)`, - [now, now] - ) - await database!.query( - `INSERT INTO relay_assignment_migrations - (user_id, relay_host_id, source_cell_id, target_cell_id, previous_epoch, - assignment_epoch, source_request_units, target_reserved_units, expires_at, - target_registered_at, created_at, updated_at) - VALUES ('user-1', '111111111111', 'cell-a', 'cell-b', 1, 2, 1, 2, 2, 2, 1, 2)` - ) - - expect(await readRegisteredMigrationInventory(database!, now)).toEqual({ - open: 1, - inactive: 1, - abandoned: 1, - pairs: [ - { - sourceCellId: 'cell-a', - targetCellId: 'cell-b', - open: 1, - inactive: 1, - abandoned: 1 - } - ] - }) - - await database!.query( - `INSERT INTO relay_cells - (cell_id, cell_url, enabled, capacity_requests, reserved_requests, - observed_requests, last_heartbeat_at, updated_at) - VALUES ('cell-c', 'https://c.example.test', 1, 4000, 0, 0, 0, 1)` - ) - await database!.query( - `INSERT INTO relay_cell_admission (cell_id, admission_state, updated_at) - VALUES ('cell-c', 'migration-only', 1)` - ) - await database!.query( - `INSERT INTO relay_assignments - (user_id, relay_host_id, cell_id, assignment_epoch, lease_expires_at, - last_activity_at, reserved_controls, reserved_splices, reserved_invites, - pending_installs, pending_confirmations, migration_leases) - VALUES ('user-2', '222222222222', 'cell-c', 2, ?, ?, 0, 0, 0, 0, 0, 0)`, - [now, now] - ) - await database!.query( - `INSERT INTO relay_assignment_migrations - (user_id, relay_host_id, source_cell_id, target_cell_id, previous_epoch, - assignment_epoch, source_request_units, target_reserved_units, expires_at, - target_registered_at, created_at, updated_at) - VALUES ('user-2', '222222222222', 'cell-a', 'cell-c', 1, 2, 1, 2, ?, NULL, 1, 2)`, - [now + 60_000] - ) - - expect(await readRegisteredMigrationInventory(database!, now)).toEqual({ - open: 2, - inactive: 1, - abandoned: 1, - pairs: [ - { - sourceCellId: 'cell-a', - targetCellId: 'cell-b', - open: 1, - inactive: 1, - abandoned: 1 - }, - { - sourceCellId: 'cell-a', - targetCellId: 'cell-c', - open: 1, - inactive: 0, - abandoned: 0 - } - ] - }) - }) -}) diff --git a/cloud/apps/relay/src/assignment/migrations/registered-migration-inventory.test.ts b/cloud/apps/relay/src/assignment/migrations/registered-migration-inventory.test.ts deleted file mode 100644 index 1eca0b1749d..00000000000 --- a/cloud/apps/relay/src/assignment/migrations/registered-migration-inventory.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { afterEach, describe, expect, it } from 'vitest' -import { openInMemoryRelayDatabase, type RelayDatabase } from '../../database/database.js' -import { REGISTERED_MIGRATION_ABANDON_MS } from './registered-migration-abandonment.js' -import { - formatRegisteredMigrationInventory, - readRegisteredMigrationInventory -} from './registered-migration-inventory.js' - -describe('registered migration inventory', () => { - const databases: RelayDatabase[] = [] - afterEach(async () => { - for (const database of databases.splice(0)) { - await database.close() - } - }) - it('does not restart abandonment when an old source migration lease is refreshed', async () => { - const database = await openInMemoryRelayDatabase() - databases.push(database) - const now = REGISTERED_MIGRATION_ABANDON_MS + 10_000 - await insertCell(database, 'cell-a', false, 'existing-only') - await insertCell(database, 'cell-b', true, 'migration-only') - await insertMigration(database, now - 1) - - const fresh = await readRegisteredMigrationInventory(database, now) - - expect(fresh).toEqual({ - open: 1, - inactive: 1, - abandoned: 1, - pairs: [ - { - sourceCellId: 'cell-a', - targetCellId: 'cell-b', - open: 1, - inactive: 1, - abandoned: 1 - } - ] - }) - expect(formatRegisteredMigrationInventory(fresh)).toEqual([ - '[orca-relay] migration inventory open=1 expiredRegisteredInactive=1 abandonedRegistered=1', - '[orca-relay] migration pair sourceCellId=cell-a targetCellId=cell-b open=1 expiredRegisteredInactive=1 abandoned=1' - ]) - await database.query(`UPDATE relay_assignment_migrations SET expires_at = ?, updated_at = ?`, [ - now + 1, - now - ]) - expect((await readRegisteredMigrationInventory(database, now + 1)).abandoned).toBe(1) - }) - - it('includes unregistered open migrations without logging a host identity', async () => { - const database = await openInMemoryRelayDatabase() - databases.push(database) - const now = 10_000 - await insertCell(database, 'cell-a', false, 'existing-only') - await insertCell(database, 'cell-b', true, 'migration-only') - await insertMigration(database, now + 60_000, null) - - const inventory = await readRegisteredMigrationInventory(database, now) - - expect(inventory).toEqual({ - open: 1, - inactive: 0, - abandoned: 0, - pairs: [ - { - sourceCellId: 'cell-a', - targetCellId: 'cell-b', - open: 1, - inactive: 0, - abandoned: 0 - } - ] - }) - expect(formatRegisteredMigrationInventory(inventory).join('\n')).not.toContain('111111111111') - }) -}) - -async function insertCell( - database: RelayDatabase, - cellId: string, - enabled: boolean, - admissionState: string -): Promise { - await database.query( - `INSERT INTO relay_cells - (cell_id, cell_url, enabled, capacity_requests, reserved_requests, - observed_requests, last_heartbeat_at, updated_at) - VALUES (?, ?, ?, 4000, 0, 0, 0, 1)`, - [cellId, `https://${cellId}.example.test`, enabled ? 1 : 0] - ) - await database.query( - `INSERT INTO relay_cell_admission (cell_id, admission_state, updated_at) - VALUES (?, ?, 1)`, - [cellId, admissionState] - ) -} - -async function insertMigration( - database: RelayDatabase, - expiresAt: number, - targetRegisteredAt: number | null = 2 -): Promise { - await database.query( - `INSERT INTO relay_assignments - (user_id, relay_host_id, cell_id, assignment_epoch, lease_expires_at, - last_activity_at, reserved_controls, reserved_splices, reserved_invites, - pending_installs, pending_confirmations, migration_leases) - VALUES ('user-1', '111111111111', 'cell-b', 2, ?, ?, 0, 0, 0, 0, 0, 0)`, - [expiresAt, expiresAt] - ) - await database.query( - `INSERT INTO relay_assignment_migrations - (user_id, relay_host_id, source_cell_id, target_cell_id, previous_epoch, - assignment_epoch, source_request_units, target_reserved_units, expires_at, - target_registered_at, created_at, updated_at) - VALUES ('user-1', '111111111111', 'cell-a', 'cell-b', 1, 2, 1, 2, ?, ?, 1, 2)`, - [expiresAt, targetRegisteredAt] - ) -} diff --git a/cloud/apps/relay/src/assignment/public-admission/drain-return-admission.test.ts b/cloud/apps/relay/src/assignment/public-admission/drain-return-admission.test.ts index 1d17966a922..011e5c99313 100644 --- a/cloud/apps/relay/src/assignment/public-admission/drain-return-admission.test.ts +++ b/cloud/apps/relay/src/assignment/public-admission/drain-return-admission.test.ts @@ -1,23 +1,16 @@ import { describe, expect, it } from 'vitest' import { - DRAIN_RETURN_MIN_RETRY_AFTER_SECONDS, RelayDrainReturnAdmission, type DrainReturnDeferral, type DrainReturnGrant } from './drain-return-admission.js' import { RelayPublicAssignmentAdmission } from './public-assignment-admission.js' -type Timer = { at: number; callback: () => void; cancelled: boolean } - function harness( overrides: { maxQueued?: number - maxRetryAfterSeconds?: number - onServiceMs?: (durationMs: number) => void } = {} ) { - let now = 0 - const timers: Timer[] = [] const placement = new RelayPublicAssignmentAdmission({ maxConcurrent: 2, maxQueued: 8, @@ -26,32 +19,18 @@ function harness( maxDrainReturnConcurrent: 1, maxDrainReturnQueued: overrides.maxQueued ?? 0, drainReturnWaitMs: 3_000, - drainReturnMinIntervalMs: DRAIN_RETURN_MIN_RETRY_AFTER_SECONDS * 1_000, - now: () => now, - schedule: (callback, delayMs) => { - const timer = { at: now + delayMs, callback, cancelled: false } - timers.push(timer) - return () => { - timer.cancelled = true - } - } + drainReturnMinIntervalMs: 2_000, + now: () => 0, + schedule: () => () => undefined }) const lane = new RelayDrainReturnAdmission(placement, { maxConcurrent: 1, - maxRetryAfterSeconds: overrides.maxRetryAfterSeconds ?? 300, - now: () => now, - onServiceMs: overrides.onServiceMs + maxRetryAfterSeconds: 300, + now: () => 0 }) return { lane, - placement, - advance: (ms: number) => { - now += ms - for (const timer of timers.filter((entry) => !entry.cancelled && entry.at <= now)) { - timer.cancelled = true - timer.callback() - } - } + placement } } @@ -69,28 +48,6 @@ const host = (index: number): string => `host${String(index).padStart(12, '0')}` describe('drain-return admission', () => { - it('shortens the pacing once the measured service time is short', async () => { - const { lane, advance } = harness() - for (let index = 0; index < 30; index++) { - const grant = admitted(await lane.acquire(host(index))) - advance(50) - grant.lease.release() - advance(2_000) - } - admitted(await lane.acquire(host(100))) - - const retries = [] - for (let index = 101; index <= 120; index++) { - retries.push(deferred(await lane.acquire(host(index))).retryAfterSeconds) - } - - // ~50 ms per re-placement: twenty deferrals span about one second, where the - // 860 ms starting estimate would have spread them over seventeen. - expect(Math.max(...retries)).toBe(3) - }) - - // The unclamped sample: the EWMA's floor and ceiling would hide the real tail. - it('answers a host’s own early retry with its interval, not a place behind the cohort', async () => { const { lane } = harness() admitted(await lane.acquire(host(0))).lease.release() @@ -99,7 +56,7 @@ describe('drain-return admission', () => { const repeat = deferred(await lane.acquire(host(0))) expect(repeat).toMatchObject({ reason: 'host-rate-limited', - retryAfterSeconds: DRAIN_RETURN_MIN_RETRY_AFTER_SECONDS + retryAfterSeconds: 2 }) // And it booked nothing: the next overflow still gets the first slot. @@ -107,17 +64,6 @@ describe('drain-return admission', () => { expect(deferred(await lane.acquire(host(2))).retryAfterSeconds).toBe(2) }) - it('keeps a host’s reserved slot when it comes back early', async () => { - const { lane, advance } = harness() - admitted(await lane.acquire(host(0))) - for (let index = 1; index <= 4; index++) await lane.acquire(host(index)) - expect(deferred(await lane.acquire(host(5))).retryAfterSeconds).toBe(6) - - advance(1_000) - // Early and still full: the same instant, not a new slot after everyone else. - expect(deferred(await lane.acquire(host(5))).retryAfterSeconds).toBe(5) - }) - it('lets a queued placement go before a queued drain return', async () => { const { lane, placement } = harness({ maxQueued: 1 }) const drain = admitted(await lane.acquire(host(0))) diff --git a/cloud/apps/relay/src/assignment/public-admission/public-assignment-admission.test.ts b/cloud/apps/relay/src/assignment/public-admission/public-assignment-admission.test.ts index 9eb5849854d..887ad63f082 100644 --- a/cloud/apps/relay/src/assignment/public-admission/public-assignment-admission.test.ts +++ b/cloud/apps/relay/src/assignment/public-admission/public-assignment-admission.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from 'vitest' +import { describe, expect, it } from 'vitest' import { RelayPublicAssignmentAdmission } from './public-assignment-admission.js' describe('public assignment admission', () => { @@ -7,7 +7,7 @@ describe('public assignment admission', () => { const admission = new RelayPublicAssignmentAdmission({ maxConcurrent: 1, minIntervalMs: 0, - now: vi.fn(() => 0) + now: () => 0 }) const lease = await admission.acquire('host-a') expect(lease).not.toBeNull() @@ -46,19 +46,12 @@ describe('public assignment admission', () => { }) it('gives a bounded reserved waiter the next public slot', async () => { - let expire!: () => void - let cancelled = false const admission = new RelayPublicAssignmentAdmission({ maxConcurrent: 2, maxReservedConcurrent: 1, reservedWaitMs: 1_000, minIntervalMs: 0, - schedule: (callback) => { - expire = callback - return () => { - cancelled = true - } - } + schedule: () => () => undefined }) const first = await admission.acquire('host-a') const second = await admission.acquire('host-b') @@ -70,12 +63,10 @@ describe('public assignment admission', () => { const reserved = await reservedPromise expect(reserved).not.toBeNull() - expect(cancelled).toBe(true) await expect(admission.acquire('host-d')).resolves.toBeNull() reserved?.release() await expect(admission.acquire('host-d')).resolves.not.toBeNull() second?.release() - expire() }) it('serializes same-host assignment and reserved recovery without losing priority', async () => { @@ -100,34 +91,4 @@ describe('public assignment admission', () => { await expect(admission.acquire('host-b')).resolves.not.toBeNull() }) - it('tells each caller its own rejection reason without crossing requests', async () => { - const admission = new RelayPublicAssignmentAdmission({ - maxConcurrent: 1, - maxQueued: 1, - waitMs: 1_000, - maxReservedConcurrent: 1, - reservedWaitMs: 1_000, - minIntervalMs: 5_000, - now: () => 0, - schedule: () => () => undefined - }) - const queued: string[] = [] - const displaced: string[] = [] - const throttled: string[] = [] - - const active = await admission.acquire('host-a') - // 'host-b' waits, then reserved recovery for the same host displaces it. - const ordinary = admission.acquire('host-b', (reason) => displaced.push(reason)) - const reserved = admission.acquireReserved('host-b', (reason) => queued.push(reason)) - await expect(ordinary).resolves.toBeNull() - active?.release() - const recovered = await reserved - recovered?.release() - await admission.acquire('host-a', (reason) => throttled.push(reason)) - - expect(displaced).toEqual(['superseded']) - expect(queued).toEqual([]) - expect(throttled).toEqual(['host-rate-limited']) - }) - }) diff --git a/cloud/apps/relay/src/assignment/public-admission/public-assignment-circuit-breaker.test.ts b/cloud/apps/relay/src/assignment/public-admission/public-assignment-circuit-breaker.test.ts deleted file mode 100644 index b780bc26bd9..00000000000 --- a/cloud/apps/relay/src/assignment/public-admission/public-assignment-circuit-breaker.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { createRelayApp } from '../../app.js' -import type { RelayConfig } from '../../config.js' - -describe('public assignment circuit breaker', () => { - it('rejects assign and resolve without invoking relay state', async () => { - const assign = vi.fn() - const resolveResume = vi.fn() - const app = createRelayApp(config(), { - store: { resolveResume } as never, - assignments: { assign } as never, - drain: vi.fn(), - ready: vi.fn(async () => true) - }) - - for (const path of ['/v1/assign', '/v1/resolve']) { - const response = await app.request(path, { method: 'POST' }) - expect(response.status).toBe(503) - expect(response.headers.get('retry-after')).toBe('5') - expect(await response.json()).toEqual({ error: 'assignments_temporarily_unavailable' }) - } - expect(assign).not.toHaveBeenCalled() - expect(resolveResume).not.toHaveBeenCalled() - expect((await app.request('/health')).status).toBe(200) - expect((await app.request('/v1/admin/drain', { method: 'POST' })).status).toBe(401) - }) -}) - -function config(): RelayConfig { - return { - port: 8080, - publicUrl: 'https://relay.example.test', - cellUrl: 'https://relay.example.test', - authIssuer: 'https://auth.example.test', - authAudience: 'orca-relay', - jwksUrl: 'https://auth.example.test/jwks', - assignmentSigningKey: new TextEncoder().encode('assignment-key-with-at-least-32-bytes'), - role: 'director', - cellId: 'director', - cells: [], - adminAudience: 'https://relay.example.test/v1/admin/drain', - deployServiceAccount: 'deploy@example.test', - runtimeServiceAccount: 'runtime@example.test', - adminJwksUrl: 'https://auth.example.test/jwks', - databasePoolMax: 3, - publicAssignmentsEnabled: false, - publicAssignmentConcurrency: 2, - publicAssignmentQueueMax: 128, - publicAssignmentWaitMs: 4_000, - publicResolveConcurrency: 1, - publicResolveWaitMs: 5_000, - publicAssignmentRetryAfterSeconds: 5, - dataDir: './data' - } -} diff --git a/cloud/apps/relay/src/assignment/public-admission/public-assignment-drain-return-lane.test.ts b/cloud/apps/relay/src/assignment/public-admission/public-assignment-drain-return-lane.test.ts index c56f9bac8a0..4814e8d4eba 100644 --- a/cloud/apps/relay/src/assignment/public-admission/public-assignment-drain-return-lane.test.ts +++ b/cloud/apps/relay/src/assignment/public-admission/public-assignment-drain-return-lane.test.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { - RelayAssignmentRowBusyError, type RelayAssignment, type ResolvedRelayAssignment } from '../assignment-store.js' @@ -21,7 +20,6 @@ vi.mock('../../authentication/relay-token-verifier.js', () => ({ import { createRelayApp } from '../../app.js' afterEach(() => { - vi.useRealTimers() vi.restoreAllMocks() }) @@ -36,15 +34,11 @@ describe('drain-return lane', () => { const resolve = vi.fn(async ({ relayHostId }: { relayHostId: string }) => relayHostId === drained ? isolatedHome(relayHostId) : assignment('cell-o', relayHostId) ) - const outcomes: string[] = [] - const servedLanes: string[] = [] const app = createRelayApp(config(), { store: {} as never, assignments: { assign, resolve } as never, drain: vi.fn(), - ready: vi.fn(async () => true), - recordAssignmentAdmission: (outcome) => outcomes.push(outcome), - recordAdmissionServiceMs: (lane) => servedLanes.push(lane) + ready: vi.fn(async () => true) }) // The re-placement holds the drain lane; the sticky lane's only slot is free. @@ -56,125 +50,12 @@ describe('drain-return lane', () => { ) expect(reconnect.status).toBe(200) - expect(resolve).toHaveBeenCalledWith( - { userId: 'user-1', relayHostId: drained }, - { classifyHomeRollIsolation: true } - ) replacement.resolve(assignment('cell-new', drained)) expect((await moving).status).toBe(200) - expect(outcomes).toEqual(['drain-return', 'sticky']) - // The drained host held a sticky slot for its verification read too. - expect(servedLanes).toEqual(['sticky', 'sticky', 'drain-return']) - }) - - it('defers an overflowing drain return with a paced Retry-After and says why', async () => { - const holder = 'hhhhhhhhhhhhhhhh' - const replacement = deferred() - const assign = vi.fn(async () => await replacement.promise) - const resolve = vi.fn(async ({ relayHostId }: { relayHostId: string }) => - isolatedHome(relayHostId) - ) - const outcomes: string[] = [] - const reasons: string[] = [] - const retryAfters: number[] = [] - const causes: string[] = [] - const app = createRelayApp(config({ drainReturnQueueMax: 1, drainReturnWaitMs: 50 }), { - store: {} as never, - assignments: { assign, resolve } as never, - drain: vi.fn(), - ready: vi.fn(async () => true), - recordAssignmentAdmission: (outcome) => outcomes.push(outcome), - recordAssignmentRejectionReason: (lane, reason) => reasons.push(`${lane}:${reason}`), - recordDrainReturnRetryAfter: (seconds) => retryAfters.push(seconds), - recordAssignmentUnavailable: (cause) => causes.push(cause) - }) - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) - - const holding = app.request('/v1/assign', assignmentRequest(holder, { reconnect: true })) - await vi.waitFor(() => expect(assign).toHaveBeenCalledTimes(1)) - const queued = app.request( - '/v1/assign', - assignmentRequest('qqqqqqqqqqqqqqqq', { reconnect: true }) - ) - const overflow = await app.request( - '/v1/assign', - assignmentRequest('ffffffffffffffff', { reconnect: true }) - ) - - expect(overflow.status).toBe(503) - expect(overflow.headers.get('retry-after')).toBe('2') - const timedOut = await queued - expect(timedOut.status).toBe(503) - expect(Number(timedOut.headers.get('retry-after'))).toBeGreaterThanOrEqual(2) - expect(reasons).toEqual(['drain-return:queue-full', 'drain-return:wait-timeout']) - expect(retryAfters).toEqual([2, Number(timedOut.headers.get('retry-after'))]) - expect(outcomes.filter((outcome) => outcome === 'drain-return-deferred')).toHaveLength(2) - expect(causes).toEqual(['drain-return-deferred', 'drain-return-deferred']) - expect(warn.mock.calls.flat().join('\n')).toMatch( - /lane=drain-return hinted=true reason=queue-full .*retryAfter=2/ - ) - replacement.resolve(assignment('cell-new', holder)) - expect((await holding).status).toBe(200) - }) - - it('answers a drained host’s row-busy redial with the short interval', async () => { - vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout', 'Date'] }) - vi.spyOn(console, 'warn').mockImplementation(() => {}) - const holder = 'hhhhhhhhhhhhhhhh' - const host = 'bbbbbbbbbbbbbbbb' - const holding = deferred() - let busy = true - const assign = vi.fn(async ({ relayHostId }: { relayHostId: string }) => { - if (relayHostId === holder) return await holding.promise - if (relayHostId === host && busy) { - busy = false - throw new RelayAssignmentRowBusyError() - } - return assignment('cell-new', relayHostId) - }) - const resolve = vi.fn(async ({ relayHostId }: { relayHostId: string }) => - isolatedHome(relayHostId) - ) - const app = createRelayApp(config({ drainReturnQueueMax: 1 }), { - store: {} as never, - assignments: { assign, resolve } as never, - drain: vi.fn(), - ready: vi.fn(async () => true) - }) - const request = (relayHostId: string) => - Promise.resolve( - app.request('/v1/assign', assignmentRequest(relayHostId, { reconnect: true })) - ) - - const held = request(holder) - await vi.waitFor(() => expect(assign).toHaveBeenCalledTimes(1)) - const queued = request(host) - await vi.advanceTimersByTimeAsync(10) - // A cohort behind it books the next ten service slots. - for (let index = 0; index < 10; index++) { - const overflow = await request(hostId('f', index)) - expect(overflow.status).toBe(503) - } - await vi.advanceTimersByTimeAsync(2_490) - holding.resolve(assignment('cell-new', holder)) - expect((await held).status).toBe(200) - const refused = await queued - expect(refused.headers.get('retry-after')).toBe('1') - - await vi.advanceTimersByTimeAsync(1_000) - const redial = await request(host) - - // Its own lane interval, not a slot after the ten hosts that came later. - expect(redial.status).toBe(503) - expect(redial.headers.get('retry-after')).toBe('2') }) }) -function hostId(prefix: string, index: number): string { - return `${prefix}${String(index).padStart(15, '0')}` -} - function assignmentRequest(relayHostId: string, extra: { reconnect?: boolean } = {}): RequestInit { return { method: 'POST', @@ -209,7 +90,7 @@ function deferred() { return { promise, resolve } } -function config(overrides: Partial = {}): RelayConfig { +function config(): RelayConfig { return { port: 8080, publicUrl: 'https://relay.example.test', @@ -233,7 +114,6 @@ function config(overrides: Partial = {}): RelayConfig { publicResolveConcurrency: 1, publicResolveWaitMs: 5_000, publicAssignmentRetryAfterSeconds: 5, - dataDir: './data', - ...overrides + dataDir: './data' } } diff --git a/cloud/apps/relay/src/assignment/public-admission/public-assignment-overload.test.ts b/cloud/apps/relay/src/assignment/public-admission/public-assignment-overload.test.ts index 14455be8b0b..b4382f962cd 100644 --- a/cloud/apps/relay/src/assignment/public-admission/public-assignment-overload.test.ts +++ b/cloud/apps/relay/src/assignment/public-admission/public-assignment-overload.test.ts @@ -18,68 +18,10 @@ import { createRelayApp } from '../../app.js' describe('public assignment overload', () => { - it('fairly drains a bounded incident-scale burst without raising database concurrency', async () => { - const pending = new Map>>() - const admittedHosts: string[] = [] - let active = 0 - let highWater = 0 - const assign = vi.fn(async ({ relayHostId }: { relayHostId: string }) => { - admittedHosts.push(relayHostId) - active++ - highWater = Math.max(highWater, active) - const operation = deferred() - pending.set(relayHostId, operation) - try { - return await operation.promise - } finally { - active-- - pending.delete(relayHostId) - } - }) - const app = createRelayApp(config(), { - store: {} as never, - assignments: { assign } as never, - drain: vi.fn(), - ready: vi.fn(async () => true) - }) - const hosts = Array.from( - { length: 430 }, - (_, index) => `host-${String(index).padStart(11, '0')}` - ) - - const firstWave = hosts.map((host) => - app.request('/v1/assign', assignmentRequest(host)) - ) - await vi.waitFor(() => expect(assign).toHaveBeenCalledTimes(2)) - const retryWave = await Promise.all( - hosts.map((host) => app.request('/v1/assign', assignmentRequest(host))) - ) - - expect(retryWave.every((response) => response.status === 503)).toBe(true) - expect(assign).toHaveBeenCalledTimes(2) - while (assign.mock.calls.length < 130) { - const activeOperations = [...pending] - const expectedCalls = assign.mock.calls.length + activeOperations.length - for (const [host, operation] of activeOperations) { - operation.resolve(assignment(`cell-${host}`, host)) - } - await vi.waitFor(() => expect(assign).toHaveBeenCalledTimes(expectedCalls)) - } - for (const [host, operation] of pending) { - operation.resolve(assignment(`cell-${host}`, host)) - } - const firstResponses = await Promise.all(firstWave) - expect(firstResponses.filter((response) => response.status === 200)).toHaveLength(130) - expect(firstResponses.filter((response) => response.status === 503)).toHaveLength(300) - expect(admittedHosts).toEqual(hosts.slice(0, 130)) - expect(highWater).toBe(2) - }) - it('refuses an oversized chunked body before parsing it, sparing only the reserve routes', async () => { - const resolve = vi.fn() const app = createRelayApp(config(), { store: {} as never, - assignments: { resolve } as never, + assignments: {} as never, drain: vi.fn(), ready: vi.fn(async () => true) }) @@ -98,15 +40,11 @@ describe('public assignment overload', () => { const refused = await app.request('/v1/resolve', chunked(64 * 1024)) expect(refused.status).toBe(413) expect(await refused.json()).toEqual({ error: 'request_too_large' }) - expect(resolve).not.toHaveBeenCalled() // Reserve bodies may reach 16 KiB, so the 4 KiB app limit must not answer for them. An - // unavailable route refuses before its own limit, including a body past 16 KiB. + // unavailable route refuses before its own limit. const reserve = await app.request('/internal/reserve', chunked(6 * 1024)) expect(reserve.status).toBe(404) expect(await reserve.json()).toEqual({ error: 'reserve_unavailable' }) - const oversized = await app.request('/internal/reserve', chunked(20 * 1024)) - expect(oversized.status).toBe(404) - expect(await oversized.json()).toEqual({ error: 'reserve_unavailable' }) }) it('reserves one bounded resolve lane while assignment work is saturated', async () => { @@ -133,8 +71,6 @@ describe('public assignment overload', () => { await vi.waitFor(() => expect(assign).toHaveBeenCalledTimes(2)) const recovery = app.request('/v1/resolve', resolveRequest('cccccccccccccccc', 1)) - await Promise.resolve() - expect(resolveResume).not.toHaveBeenCalled() const excessResolve = await app.request( '/v1/resolve', resolveRequest('dddddddddddddddd', 2) @@ -143,8 +79,6 @@ describe('public assignment overload', () => { expect(excessResolve.status).toBe(503) expect(excessAssign.status).toBe(503) - expect(assign).toHaveBeenCalledTimes(2) - expect(resolveResume).not.toHaveBeenCalled() pendingAssignments .get('aaaaaaaaaaaaaaaa') @@ -153,7 +87,6 @@ describe('public assignment overload', () => { await vi.waitFor(() => expect(resolveResume).toHaveBeenCalledTimes(1)) pendingResolve.resolve({ userId: 'user-1', relayDeviceId: 'device-1' }) expect((await recovery).status).toBe(200) - expect(resolve).toHaveBeenCalledTimes(1) for (const [host, operation] of pendingAssignments) { operation.resolve(assignment(`cell-${host}`, host)) diff --git a/cloud/apps/relay/src/assignment/public-admission/public-assignment-reconnect-lane.test.ts b/cloud/apps/relay/src/assignment/public-admission/public-assignment-reconnect-lane.test.ts index 58245e0d8c2..1a2b795252a 100644 --- a/cloud/apps/relay/src/assignment/public-admission/public-assignment-reconnect-lane.test.ts +++ b/cloud/apps/relay/src/assignment/public-admission/public-assignment-reconnect-lane.test.ts @@ -37,13 +37,11 @@ describe('public assignment reconnect lane', () => { const resolve = vi.fn(async ({ relayHostId }: { relayHostId: string }) => relayHostId === reconnecting ? assignment('cell-r', reconnecting) : null ) - const outcomes: string[] = [] const app = createRelayApp(config({ publicAssignmentQueueMax: 0 }), { store: {} as never, assignments: { assign, resolve } as never, drain: vi.fn(), - ready: vi.fn(async () => true), - recordAssignmentAdmission: (outcome) => outcomes.push(outcome) + ready: vi.fn(async () => true) }) // Saturate the placement lane with two in-flight newcomers, zero queue. @@ -59,18 +57,6 @@ describe('public assignment reconnect lane', () => { ) expect(reconnected.status).toBe(200) - expect(resolve).toHaveBeenCalledWith( - { userId: 'user-1', relayHostId: reconnecting }, - { classifyHomeRollIsolation: true } - ) - expect(assign).toHaveBeenCalledWith( - { userId: 'user-1', relayHostId: reconnecting }, - undefined, - undefined, - undefined, - { hostRequested: true } - ) - expect(outcomes).toContain('sticky') pending.get(newcomerA)?.resolve(assignment('cell-a', newcomerA)) pending.get(newcomerB)?.resolve(assignment('cell-b', newcomerB)) @@ -78,28 +64,6 @@ describe('public assignment reconnect lane', () => { expect((await second).status).toBe(200) }) - it('rate-limits repeat fast-lane attempts with the short retry-after', async () => { - const host = 'rrrrrrrrrrrrrrrr' - const assign = vi.fn(async () => assignment('cell-r', host)) - const resolve = vi.fn(async () => assignment('cell-r', host)) - const app = createRelayApp(config(), { - store: {} as never, - assignments: { assign, resolve } as never, - drain: vi.fn(), - ready: vi.fn(async () => true) - }) - - expect( - (await app.request('/v1/assign', assignmentRequest(host, { reconnect: true }))).status - ).toBe(200) - const repeat = await app.request('/v1/assign', assignmentRequest(host, { reconnect: true })) - - expect(repeat.status).toBe(503) - expect(repeat.headers.get('retry-after')).toBe('2') - expect(resolve).toHaveBeenCalledTimes(1) - expect(assign).toHaveBeenCalledTimes(1) - }) - it('rejects on the fast lane when the verification probe fails transiently', async () => { const host = 'tttttttttttttttt' const assign = vi.fn() @@ -117,7 +81,6 @@ describe('public assignment reconnect lane', () => { expect(response.status).toBe(503) expect(response.headers.get('retry-after')).toBe('2') - expect(assign).not.toHaveBeenCalled() }) it('refuses a dial whose assignment row is busy at once, and frees the fast lane', async () => { @@ -143,7 +106,6 @@ describe('public assignment reconnect lane', () => { expect(refused.status).toBe(503) expect(refused.headers.get('retry-after')).toBe('1') expect(await refused.json()).toEqual({ error: 'assignment_row_busy' }) - expect(warn.mock.calls.flat().join('\n')).toContain('reason=relay_assignment_row_busy') // The single fast-lane slot went back with the refusal. expect( (await app.request('/v1/assign', assignmentRequest(next, { reconnect: true }))).status diff --git a/cloud/apps/relay/src/assignment/regional-rehome/idle-regional-rehome-store.test.ts b/cloud/apps/relay/src/assignment/regional-rehome/idle-regional-rehome-store.test.ts index c68cf7efc02..18a95333bbb 100644 --- a/cloud/apps/relay/src/assignment/regional-rehome/idle-regional-rehome-store.test.ts +++ b/cloud/apps/relay/src/assignment/regional-rehome/idle-regional-rehome-store.test.ts @@ -111,51 +111,15 @@ async function setup() { } describe('constrained idle regional assignment transaction', () => { - it.each(['missing', 'disabled', 'future'] as const)( - 'does only one read per tick with %s durable control and sees later enablement', - async (state) => { - const { store, database, safety } = await setup() - const control = (await database.query( - "SELECT * FROM relay_region_rehome_control WHERE control_id = 'global'" - ))[0]! - if (state === 'missing') { - await database.query('DELETE FROM relay_region_rehome_control') - } else { - await database.query( - "UPDATE relay_region_rehome_control SET enabled = ?, not_before = ? WHERE control_id = 'global'", - [state === 'disabled' ? 0 : 1, safety.observedAt + (state === 'future' ? 1 : 0)] - ) - } - const query = vi.spyOn(database, 'query') - const transaction = vi.spyOn(database, 'transaction') - for (let tick = 0; tick < 3; tick++) { - query.mockClear() - expect(await store.selectIdleRegionalRehomeCandidates(safety)).toEqual([]) - expect(query).toHaveBeenCalledTimes(1) - expect(query.mock.calls[0]![0]).toMatch(/^SELECT .*FROM relay_region_rehome_control/s) - expect(transaction).not.toHaveBeenCalled() - } - if (state === 'missing') { - const columns = Object.keys(control) - await database.query( - `INSERT INTO relay_region_rehome_control (${columns.join(',')}) VALUES (${columns.map(() => '?').join(',')})`, - Object.values(control) - ) - } else { - await database.query( - "UPDATE relay_region_rehome_control SET enabled = 1, not_before = ? WHERE control_id = 'global'", - [safety.observedAt] - ) - } - query.mockClear() - expect(await store.selectIdleRegionalRehomeCandidates(safety)).toHaveLength(1) - expect(query.mock.calls.length).toBeGreaterThan(1) - await database.query("UPDATE relay_region_rehome_control SET enabled = 0 WHERE control_id = 'global'") - query.mockClear() - expect(await store.selectIdleRegionalRehomeCandidates(safety)).toEqual([]) - expect(query).toHaveBeenCalledTimes(1) - } - ) + it('sees durable control changes between candidate polls', async () => { + const { store, database, safety } = await setup() + await database.query("UPDATE relay_region_rehome_control SET enabled = 0 WHERE control_id = 'global'") + expect(await store.selectIdleRegionalRehomeCandidates(safety)).toEqual([]) + await database.query("UPDATE relay_region_rehome_control SET enabled = 1 WHERE control_id = 'global'") + expect(await store.selectIdleRegionalRehomeCandidates(safety)).toHaveLength(1) + await database.query("UPDATE relay_region_rehome_control SET enabled = 0 WHERE control_id = 'global'") + expect(await store.selectIdleRegionalRehomeCandidates(safety)).toEqual([]) + }) it.each([10, 11])('reserves source activity plus assignment at target capacity %i', async (capacity) => { const { store, database, safety, request } = await setup() @@ -184,33 +148,6 @@ describe('constrained idle regional assignment transaction', () => { }) }) - it.each(['next_dispatch_at', 'paused_until'] as const)( - 'skips the candidate join while %s holds the durable dispatch budget closed', - async (column) => { - const { store, database, safety } = await setup() - const query = vi.spyOn(database, 'query') - // One assignment only: setup leaves both fields at 0, and naming the other - // one too would assign this column twice, which Postgres rejects. - await database.query( - `UPDATE relay_region_rehome_worker_state SET ${column} = ? WHERE worker_id = 'global'`, - [safety.observedAt + 1] - ) - for (let tick = 0; tick < 3; tick++) { - query.mockClear() - expect(await store.selectIdleRegionalRehomeCandidates(safety)).toEqual([]) - expect(query).toHaveBeenCalledTimes(2) - expect(query.mock.calls[1]![0]).toMatch(/FROM relay_region_rehome_worker_state/s) - } - await database.query( - `UPDATE relay_region_rehome_worker_state SET ${column} = ? WHERE worker_id = 'global'`, - [safety.observedAt] - ) - query.mockClear() - expect(await store.selectIdleRegionalRehomeCandidates(safety)).toHaveLength(1) - expect(query.mock.calls.length).toBeGreaterThan(2) - } - ) - // sourceRequestUnits reads a reserve cell's missing phone leases as zero units; selection must drop it. it.each(['source', 'target'])('drops every candidate while the %s cell is recorded as reserve', async (cellId) => { const { store, safety } = await setup() @@ -267,51 +204,6 @@ describe('constrained idle regional assignment transaction', () => { expect(first.some((c) => c.relayHostId === next[0]!.relayHostId)).toBe(false) }) - it('progresses past a full page of busy candidates without writing eligibility state', async () => { - const { store, database, safety } = await setup() - for (const table of [ - 'relay_assignments', - 'relay_assignment_activity_leases', - 'relay_control_capabilities', - 'relay_region_decisions' - ]) { - const template = ( - await database.query(`SELECT * FROM ${table} WHERE user_id = ? AND relay_host_id = ?`, [ - identity.userId, - identity.relayHostId - ]) - )[0]! - const columns = Object.keys(template) - for (let index = 0; index < 100; index++) { - const values = columns.map((column) => - column === 'user_id' || column === 'relay_host_id' ? '?' : column - ) - await database.query( - `INSERT INTO ${table} (${columns.join(', ')}) SELECT ${values.join(', ')} FROM ${table} - WHERE user_id = ? AND relay_host_id = ?`, - [ - `idle-store-test-${String(index).padStart(3, '0')}`, - `pagehost${String(index).padStart(8, '0')}`, - identity.userId, - identity.relayHostId - ] - ) - } - } - const first = await store.selectIdleRegionalRehomeCandidates(safety) - const next = await store.selectIdleRegionalRehomeCandidates(safety) - expect(first).toHaveLength(100) - expect(next).toHaveLength(1) - expect(next[0]!.relayHostId).toBe('pagehost00000099') - const restarted = new RelayAssignmentStore(database, () => safety.observedAt, { - regionalRehomeCohortPercent: 100 - }) - expect(await restarted.selectIdleRegionalRehomeCandidates(safety)).toEqual(first) - expect(await database.query('SELECT * FROM relay_region_rehome_attempts')).toEqual([]) - const decisions = await database.query('SELECT last_considered_at FROM relay_region_decisions') - expect(decisions.every((decision) => Number(decision.last_considered_at) === 0)).toBe(true) - }) - it.runIf(Boolean(process.env.ORCA_IDLE_REHOME_POSTGRES_URL))( 'rechecks generation when replacement wins after the initial authority lookup', async () => { @@ -430,20 +322,6 @@ describe('constrained idle regional assignment transaction', () => { expect(await database.query('SELECT * FROM relay_region_rehome_attempts')).toEqual([]) }) - it('selects read-only with stable identity and the control generation, not probe generation', async () => { - const { store, safety, database } = await setup() - const before = await database.query('SELECT * FROM relay_assignments') - const candidates = await store.selectIdleRegionalRehomeCandidates(safety) - expect(candidates).toHaveLength(1) - expect(candidates[0]).toMatchObject({ - sourceGeneration: 7, - sourceCellId: 'source', - targetCellId: 'target' - }) - expect(await store.selectIdleRegionalRehomeCandidates(safety)).toEqual(candidates) - expect(await database.query('SELECT * FROM relay_assignments')).toEqual(before) - expect(await database.query('SELECT * FROM relay_region_rehome_attempts')).toEqual([]) - }) }) function holdStatement(database: RelayDatabase, fragment: string) { diff --git a/cloud/apps/relay/src/assignment/regional-rehome/idle-regional-rehome-window.test.ts b/cloud/apps/relay/src/assignment/regional-rehome/idle-regional-rehome-window.test.ts index 403ccf16830..6c4c559f06f 100644 --- a/cloud/apps/relay/src/assignment/regional-rehome/idle-regional-rehome-window.test.ts +++ b/cloud/apps/relay/src/assignment/regional-rehome/idle-regional-rehome-window.test.ts @@ -151,52 +151,6 @@ describe('idle regional rehome candidate window', () => { expect(new Set(candidates.map((candidate) => candidate.attemptId)).size).toBe(3) }) - it('drops only the targets without room for the host plus its source activity', async () => { - const { store, database, safety } = await setup() - await seedHost(store, identity) - await database.query( - 'UPDATE relay_assignment_activity_leases SET request_units = 4 WHERE user_id = ?', - [identity.userId] - ) - // Five units needed: four source units plus the assignment the move reserves. - await database.query("UPDATE relay_cells SET capacity_requests = 4 WHERE cell_id = 'asia-idle'") - const short = await store.selectIdleRegionalRehomeCandidates(safety) - expect(short.map((candidate) => candidate.targetCellId)).toEqual(['asia-mid', 'asia-busy']) - // Exactly enough room is enough; it ranks last because the ratio is per capacity. - await database.query("UPDATE relay_cells SET capacity_requests = 5 WHERE cell_id = 'asia-idle'") - const exact = await store.selectIdleRegionalRehomeCandidates(safety) - expect(exact.map((candidate) => candidate.targetCellId)).toEqual([ - 'asia-mid', - 'asia-busy', - 'asia-idle' - ]) - }) - - it('reads a bounded window of decisions however many hosts are eligible', async () => { - const { store, database, safety } = await setup() - await seedHost(store, identity) - await cloneHosts(database, identity, 700) - const query = database.query.bind(database) - const decisionRows: number[] = [] - vi.spyOn(database, 'query').mockImplementation(async (sql, params) => { - const rows = await query(sql, params) - if (/^SELECT user_id, relay_host_id FROM relay_region_decisions/.test(sql)) { - decisionRows.push(rows.length) - } - return rows - }) - const first = await store.selectIdleRegionalRehomeCandidates(safety) - const next = await store.selectIdleRegionalRehomeCandidates(safety) - expect(decisionRows.length).toBeGreaterThan(0) - for (const count of decisionRows) { - expect(count).toBeLessThanOrEqual(500) - } - expect(first).toHaveLength(102) - expect(next).toHaveLength(102) - const firstKeys = new Set(first.map((c) => `${c.userId}/${c.targetCellId}`)) - expect(next.every((c) => !firstKeys.has(`${c.userId}/${c.targetCellId}`))).toBe(true) - }) - it("keeps the window's last host when a decision turns eligible between the two reads", async () => { const { store, database, safety, now } = await setup() await seedHost(store, identity) @@ -252,7 +206,6 @@ describe('idle regional rehome candidate window', () => { for (let poll = 0; poll < 20 && !wrapped; poll++) { const page = await store.selectIdleRegionalRehomeCandidates(safety) expect(page.length).toBeGreaterThan(0) - expect(page.length).toBeLessThanOrEqual(102) for (const candidate of page) { const key = `${candidate.userId}/${candidate.targetCellId}` if (seen.has(key)) { @@ -286,23 +239,4 @@ describe('idle regional rehome candidate window', () => { expect(await store.selectIdleRegionalRehomeCandidates(safety)).toHaveLength(3) }) - it('excludes a host outside the cohort', async () => { - const { store, database } = await setup() - await seedHost(store, identity) - const now = 100_000_000 + 86_400_000 - const safety = { - observedAt: now, - sqlFailures: 0, - reconnects: 0, - controlActivityRecoveryFailures: 0, - databasePoolWaiting: 0, - databasePoolWaitersMax: 0, - databasePoolWaitMsMax: 0 - } - await database.query('UPDATE relay_region_decisions SET cohort_bucket = 40') - const narrow = new RelayAssignmentStore(database, () => now, { regionalRehomeCohortPercent: 40 }) - expect(await narrow.selectIdleRegionalRehomeCandidates(safety)).toEqual([]) - const wide = new RelayAssignmentStore(database, () => now, { regionalRehomeCohortPercent: 41 }) - expect(await wide.selectIdleRegionalRehomeCandidates(safety)).toHaveLength(3) - }) }) diff --git a/cloud/apps/relay/src/assignment/regional-rehome/region-correction-restart.test.ts b/cloud/apps/relay/src/assignment/regional-rehome/region-correction-restart.test.ts index b13e1b9d3a5..beb6a3c58d1 100644 --- a/cloud/apps/relay/src/assignment/regional-rehome/region-correction-restart.test.ts +++ b/cloud/apps/relay/src/assignment/regional-rehome/region-correction-restart.test.ts @@ -80,7 +80,7 @@ describe('persisted region decisions across director restart', () => { expect(row.outcome).toBe('inconclusive') expect(Number(row.expires_at)).toBe(context.window.expiresAt) const successor = (await context.issue(restarted.store)).window! - expect(successor.generation).toBe(context.window.generation + 1) + expect(successor.generation).toBe(2) expect(await restarted.store.exchangeRegionCorrection(identity, context.report, epoch)) .toMatchObject({ reportStatus: 'stale' }) }) @@ -111,7 +111,7 @@ describe('persisted region decisions across director restart', () => { context.setNow(1_000_000_000 - 60_000) const restarted = await context.restart() const successor = (await context.issue(restarted.store)).window! - expect(successor.generation).toBe(context.window.generation + 1) + expect(successor.generation).toBe(2) expect(successor.expiresAt).toBe(context.window.expiresAt - 60_000) expect(await restarted.store.exchangeRegionCorrection(identity, context.report, context.window.assignmentEpoch)) .toMatchObject({ reportStatus: 'stale' }) diff --git a/cloud/apps/relay/src/assignment/regional-rehome/region-correction-store.test.ts b/cloud/apps/relay/src/assignment/regional-rehome/region-correction-store.test.ts index 5204d158d0e..384945db575 100644 --- a/cloud/apps/relay/src/assignment/regional-rehome/region-correction-store.test.ts +++ b/cloud/apps/relay/src/assignment/regional-rehome/region-correction-store.test.ts @@ -124,104 +124,7 @@ async function window(context: Awaited>) { return result.window! } -async function regionalMigration(context: Awaited>) { - const migration = await context.store.startEvacuation(identity, cells[1]!.id) - const attemptId = '33333333-3333-4333-8333-333333333333' - await context.database.query( - `INSERT INTO relay_region_rehome_attempts - (attempt_id,user_id,relay_host_id,preferred_region,source_cell_id,source_cell_incarnation, - target_cell_id,target_cell_incarnation,previous_epoch,assignment_epoch,drain_grace_ms,send_attempts,created_at,updated_at) - VALUES (?,?,?,'asia-east2',?,?,?,?,?,?,60000,1,?,?)`, - [ - attemptId, - identity.userId, - identity.relayHostId, - cells[0]!.id, - incarnations[0], - cells[1]!.id, - incarnations[1], - migration.previousEpoch, - migration.assignmentEpoch, - context.now(), - context.now() - ] - ) - return { migration } -} - describe('ordered region decisions and migration outcomes', () => { - it('reports aggregate migration lifecycle and reservations without identity disclosure or writes', async () => { - const context = await setup() - const { migration } = await regionalMigration(context) - context.advance(1_000) - const before = await context.database.query('SELECT * FROM relay_region_rehome_attempts') - const outcomes = await context.store.regionCorrectionOutcomes() - expect(outcomes).toEqual([ - expect.objectContaining({ - sourceCellId: cells[0]!.id, - targetCellId: cells[1]!.id, - state: 'registering', - count: 1, - oldestOpenMs: 1_000 - }) - ]) - expect(outcomes[0]!.targetReservedUnits).toBeGreaterThan(0) - expect(JSON.stringify(outcomes)).not.toContain(identity.relayHostId) - expect(JSON.stringify(outcomes)).not.toContain(identity.userId) - expect(await context.database.query('SELECT * FROM relay_region_rehome_attempts')).toEqual( - before - ) - await context.store.activateControl(identity, { - cellId: cells[1]!.id, - assignmentEpoch: migration.assignmentEpoch, - generation: 1 - }) - await context.store.markMigrationTargetRegistered(identity, { - cellId: cells[1]!.id, - assignmentEpoch: migration.assignmentEpoch - }) - expect(await context.store.regionCorrectionOutcomes()).toEqual([ - expect.objectContaining({ state: 'registered' }) - ]) - await context.store.releaseActivity(identity, context.activityId) - expect(await context.store.completeReadyRegionalRehomes()).toBe(1) - expect(await context.store.regionCorrectionOutcomes()).toEqual([ - expect.objectContaining({ - state: 'completed', - targetReservedUnits: 0, - oldestOpenMs: 0 - }) - ]) - }) - - it('previews the uncapped fleet without writes, claims, or locked reads', async () => { - const context = await setup() - const query = context.database.query.bind(context.database) - const transaction = context.database.transaction.bind(context.database) - const queryLocked = context.database.queryLocked.bind(context.database) - context.database.query = async (sql, params) => { - expect(sql.trim()).toMatch(/^(SELECT|WITH)/i) - return query(sql, params) - } - context.database.transaction = async () => { - throw new Error('preview_must_not_open_mutating_transaction') - } - context.database.queryLocked = async () => { - throw new Error('preview_must_not_lock') - } - try { - const preview = await context.store.previewRegionalRehomeEligibility() - expect(preview.counts['no-verified-decision']).toBeGreaterThanOrEqual(1) - expect(preview.globalSafetyFailure).toBe('process-safety-unavailable') - expect(JSON.stringify(preview)).not.toContain(identity.relayHostId) - expect(JSON.stringify(preview)).not.toContain(identity.userId) - } finally { - context.database.query = query - context.database.transaction = transaction - context.database.queryLocked = queryLocked - } - }) - it('allocates distinct ordered generations for concurrent window issuers', async () => { const context = await setup() const replies = await Promise.all([window(context), window(context), window(context)]) diff --git a/cloud/apps/relay/src/assignment/regional-rehome/regional-rehome-constraint-migration-postgres.test.ts b/cloud/apps/relay/src/assignment/regional-rehome/regional-rehome-constraint-migration-postgres.test.ts index 6dd56d46840..9d6d7edfd89 100644 --- a/cloud/apps/relay/src/assignment/regional-rehome/regional-rehome-constraint-migration-postgres.test.ts +++ b/cloud/apps/relay/src/assignment/regional-rehome/regional-rehome-constraint-migration-postgres.test.ts @@ -1,10 +1,6 @@ import pg from 'pg' import { afterAll, beforeEach, describe, expect, it } from 'vitest' -import { - openRelayDatabase, - REGIONAL_REHOME_DEFAULT_HOST_COOLDOWN_MS, - type RelayDatabase -} from '../../database/database.js' +import { openRelayDatabase, type RelayDatabase } from '../../database/database.js' const databaseUrl = process.env.ORCA_RELAY_TEST_POSTGRES_URL const describePostgres = databaseUrl ? describe : describe.skip @@ -127,25 +123,15 @@ describePostgres('PostgreSQL regional rehome constraint migration', () => { await expect( client.query(INSERT_ATTEMPT, attemptValues('attempt-3', 'europe-west1')) ).rejects.toMatchObject({ code: '23514' }) - const constraints = await client.query( - `SELECT conname FROM pg_constraint - WHERE conrelid = 'relay_region_rehome_attempts'::regclass - AND conname LIKE '%preferred_region%' - ORDER BY conname` - ) - expect(constraints.rows).toEqual([ - { conname: 'relay_region_rehome_attempts_preferred_region_valid' } - ]) - // The existing control row keeps its tuning and gains the cooldown. + // Preserve existing control tuning across the upgrade. const control = await client.query( - `SELECT generation, preference_max_age_ms, host_cooldown_ms + `SELECT generation, preference_max_age_ms FROM relay_region_rehome_control WHERE control_id = 'global'` ) expect(control.rows).toEqual([ { generation: '3', - preference_max_age_ms: '86400000', - host_cooldown_ms: String(REGIONAL_REHOME_DEFAULT_HOST_COOLDOWN_MS) + preference_max_age_ms: '86400000' } ]) }) @@ -182,14 +168,6 @@ describePostgres('PostgreSQL regional rehome constraint migration', () => { await withClient(async (client) => { await client.query(`SET search_path = ${schema}`) await client.query(INSERT_ATTEMPT, attemptValues('attempt-4', 'us-central1')) - const constraints = await client.query( - `SELECT conname FROM pg_constraint - WHERE conrelid = 'relay_region_rehome_attempts'::regclass - AND conname LIKE '%preferred_region%'` - ) - expect(constraints.rows).toEqual([ - { conname: 'relay_region_rehome_attempts_preferred_region_valid' } - ]) }) }, 60_000) }) diff --git a/cloud/apps/relay/src/assignment/regional-rehome/regional-rehome-source-region-filter.test.ts b/cloud/apps/relay/src/assignment/regional-rehome/regional-rehome-source-region-filter.test.ts index 4b5db8f5e21..4733e94fbcf 100644 --- a/cloud/apps/relay/src/assignment/regional-rehome/regional-rehome-source-region-filter.test.ts +++ b/cloud/apps/relay/src/assignment/regional-rehome/regional-rehome-source-region-filter.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' +import { describe, expect, it } from 'vitest' import { RelayAssignmentStore } from '../assignment-store.js' import { openIdleRehomeTestDatabase } from './idle-regional-rehome-test-database.js' @@ -134,10 +134,6 @@ const moves = (candidates: { userId: string; sourceCellId: string; targetCellId: to: cells.find(({ id }) => id === targetCellId)!.region })) -afterEach(() => { - vi.restoreAllMocks() -}) - describe('rehome source-region filter', () => { it('keeps US to Asia moves and drops Asia to US moves', async () => { const context = await setup('us-central1') @@ -151,13 +147,5 @@ describe('rehome source-region filter', () => { await context.database.close() }) - it('reports the filtered hosts in the preview, agreeing with selection', async () => { - const context = await setup('us-central1') - const preview = await context.store.previewRegionalRehomeEligibility(context.safety()) - expect(preview.counts).toEqual({ - 'eligible:us-central1-to-asia-east2': 1, - 'source-outside-director-region': 1 - }) - await context.database.close() - }) + }) diff --git a/cloud/apps/relay/src/assignment/regional-rehome/regional-rehome-store.test.ts b/cloud/apps/relay/src/assignment/regional-rehome/regional-rehome-store.test.ts index 7a66cba3a91..dd0c445ed15 100644 --- a/cloud/apps/relay/src/assignment/regional-rehome/regional-rehome-store.test.ts +++ b/cloud/apps/relay/src/assignment/regional-rehome/regional-rehome-store.test.ts @@ -92,26 +92,6 @@ describe('regional rehome assignment state', () => { await context.database.close() }) - it('does not roll an unregistered target back to a stale regional source', async () => { - const context = await setup() - const identity = { userId: 'user-1', relayHostId: 'abcdefghijklmnop' } - await activatePreferredSource(context, identity) - await context.store.tryIdleRehome() - context.advance(6 * 60_000) - await heartbeat(context.store, target, targetIncarnation, 3, 2) - - expect(await context.store.refreshRegionalRehomeLeases()).toBe(0) - expect(await context.store.abortExpiredEvacuations()).toBe(0) - expect( - await context.database.query( - `SELECT cell_id, assignment_epoch FROM relay_assignments - WHERE user_id = ? AND relay_host_id = ?`, - [identity.userId, identity.relayHostId] - ) - ).toEqual([{ cell_id: target.id, assignment_epoch: 2 }]) - await context.database.close() - }) - it('completes healthy candidates past a poisoned attempt and logs it', async () => { const context = await setup() const poisoned = { userId: 'user-1', relayHostId: 'abcdefghijklmnop' } @@ -167,50 +147,6 @@ describe('regional rehome assignment state', () => { await context.database.close() }) - it('keeps dual-control accounting when the drained host re-resolves mid-rehome', async () => { - const context = await setup() - const identity = { userId: 'user-1', relayHostId: 'abcdefghijklmnop' } - const sourceControl = await activatePreferredSource(context, identity) - const attempt = await context.store.tryIdleRehome() - expect(await controlAccounting(context, identity)).toEqual({ - reservedControls: 2, - controlLeases: 2 - }) - - // The drained host re-resolves through the director while both the source - // control and the target's pending control are still live. - await context.store.assign(identity, 'asia-east2') - expect(await controlAccounting(context, identity)).toEqual({ - reservedControls: 2, - controlLeases: 2 - }) - - await context.store.activateControl(identity, { - cellId: target.id, - assignmentEpoch: attempt!.assignmentEpoch, - generation: 1 - }) - await context.store.markMigrationTargetRegistered(identity, { - cellId: target.id, - assignmentEpoch: attempt!.assignmentEpoch - }) - await context.store.releaseActivity(identity, sourceControl) - expect(await controlAccounting(context, identity)).toEqual({ - reservedControls: 1, - controlLeases: 1 - }) - - expect(await context.store.completeReadyRegionalRehomes()).toBe(1) - expect( - await context.database.query( - `SELECT completed_at FROM relay_assignment_migrations - WHERE user_id = ? AND relay_host_id = ?`, - [identity.userId, identity.relayHostId] - ) - ).toEqual([{ completed_at: context.now() }]) - expect(await cellReservations(context)).toEqual({ [source.id]: 0, [target.id]: 1 }) - await context.database.close() - }) }) @@ -281,35 +217,6 @@ function collectCandidateFailureWarnings( } } -async function controlAccounting( - context: Context, - identity: { userId: string; relayHostId: string } -): Promise<{ reservedControls: number; controlLeases: number }> { - const assignment = ( - await context.database.query( - `SELECT reserved_controls FROM relay_assignments - WHERE user_id = ? AND relay_host_id = ?`, - [identity.userId, identity.relayHostId] - ) - )[0]! - const leases = await context.database.query( - `SELECT COUNT(*) AS controls FROM relay_assignment_activity_leases - WHERE user_id = ? AND relay_host_id = ? AND activity_kind = 'control'`, - [identity.userId, identity.relayHostId] - ) - return { - reservedControls: Number(assignment.reserved_controls), - controlLeases: Number(leases[0]!.controls) - } -} - -async function cellReservations(context: Context): Promise> { - const rows = await context.database.query( - `SELECT cell_id, reserved_requests FROM relay_cells ORDER BY cell_id` - ) - return Object.fromEntries(rows.map((row) => [String(row.cell_id), Number(row.reserved_requests)])) -} - async function activatePreferredSource( context: Context, identity: { userId: string; relayHostId: string } diff --git a/cloud/apps/relay/src/assignment/regional-rehome/regional-rehome-target-row-lock-postgres.test.ts b/cloud/apps/relay/src/assignment/regional-rehome/regional-rehome-target-row-lock-postgres.test.ts index 0517a7c3cf6..d7c67cb196f 100644 --- a/cloud/apps/relay/src/assignment/regional-rehome/regional-rehome-target-row-lock-postgres.test.ts +++ b/cloud/apps/relay/src/assignment/regional-rehome/regional-rehome-target-row-lock-postgres.test.ts @@ -1,7 +1,6 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest' import { RelayAssignmentStore } from '../assignment-store.js' import { - consumeRelayCellInventoryHold, openRelayDatabase, type PostgresDatabase, type RelayDatabase @@ -18,21 +17,6 @@ const describePostgres = databaseUrl ? describe : describe.skip // Roughly the round trip from an Asia cell to the us-central1 database. const STATEMENT_DELAY_MS = 150 -// Probe the formerly fleet-locked tables plus admission/region tables read by commit. -const CELL_TABLES = [ - 'relay_cells', - 'relay_cell_runtime', - 'relay_cell_capabilities', - 'relay_cell_rehome_safety', - 'relay_cell_admission', - 'relay_cell_regions' -] as const - -// The target-row statement locks exactly these, held from it to COMMIT. -const TARGET_LOCKED = ['target:relay_cells', 'target:relay_cell_admission'] - -type Trip = { sql: string; lockable: Record; probeMs: number } - type DelayControl = StatementDelay & { beforeTrip: (sql: string) => Promise } describePostgres('PostgreSQL regional rehome target-row lock', () => { @@ -97,27 +81,6 @@ describePostgres('PostgreSQL regional rehome target-row lock', () => { } } - async function lockable(table: string, cellId: string): Promise { - try { - await observer.transaction( - async (transaction) => { - await transaction.queryLocked( - `SELECT cell_id FROM ${table} WHERE cell_id = ?`, - [cellId], - { - failIfUnavailable: true - } - ) - }, - { reportRetries: false } - ) - return true - } catch (error) { - if (error instanceof Error && error.message === 'database_lock_unavailable') return false - throw error - } - } - async function reservedRequests(cellId: string): Promise { const row = ( await primary.query(`SELECT reserved_requests FROM relay_cells WHERE cell_id = ?`, [cellId]) @@ -152,59 +115,6 @@ describePostgres('PostgreSQL regional rehome target-row lock', () => { ) } - it('holds only the target cell and admission rows until the next COMMIT boundary', async () => { - const context = await fixture() - const request = await context.select() - const trips: Trip[] = [] - const probed = { - source: context.source.id, - target: context.target.id, - bystander: context.bystander.id - } - control.beforeTrip = async (sql) => { - const probeStartedAt = performance.now() - const state: Record = {} - for (const [role, cellId] of Object.entries(probed)) { - for (const table of CELL_TABLES) state[`${role}:${table}`] = await lockable(table, cellId) - } - trips.push({ sql, lockable: state, probeMs: performance.now() - probeStartedAt }) - } - consumeRelayCellInventoryHold(delayed) - control.enabled = true - - const result = await context.delayedStore.commitIdleRegionalRehome(request, context.safety()) - control.enabled = false - - expect(result).toEqual({ outcome: 'committed' }) - expect(trips.length).toBeGreaterThanOrEqual(15) - expect(trips[trips.length - 1]!.sql).toBe('COMMIT') - for (const trip of trips) { - for (const [key, free] of Object.entries(trip.lockable)) { - if (TARGET_LOCKED.includes(key)) continue - expect({ sql: trip.sql, key, free }).toEqual({ sql: trip.sql, key, free: true }) - } - } - for (const key of TARGET_LOCKED) { - expect(trips.filter((trip) => !trip.lockable[key]).map((trip) => trip.sql)).toEqual([ - 'COMMIT' - ]) - } - const counts = consumeRelayCellInventoryHold(delayed) - const commitProbeMs = trips.at(-1)!.probeMs - console.info( - JSON.stringify({ - event: 'rehome_target_row_hold', - trips: trips.length, - commitProbeMs, - ...counts - }) - ) - expect(counts.rehomeTargetRowHolds).toBe(1) - expect(counts.cellInventoryHoldMaxSite).toBe('rehome-target-row') - expect(counts.rehomeTargetRowHoldMsMax).toBeGreaterThanOrEqual(STATEMENT_DELAY_MS) - expect(await reservedRequests(context.target.id)).toBe(context.targetReservedBefore + 2) - }, 45_000) - it('defers when the target leaves general admission after selection', async () => { const context = await fixture() const request = await context.select() @@ -240,23 +150,14 @@ describePostgres('PostgreSQL regional rehome target-row lock', () => { }) await lockedPromise } - consumeRelayCellInventoryHold(delayed) control.enabled = true - const startedAt = performance.now() const result = await context.delayedStore.commitIdleRegionalRehome(request, context.safety()) - const elapsedMs = performance.now() - startedAt control.enabled = false release() await writer expect(result).toEqual({ outcome: 'deferred', reason: 'candidate-ineligible' }) - expect(consumeRelayCellInventoryHold(delayed)).toMatchObject({ - cellInventoryLockUnavailable: 1, - rehomeTargetRowHolds: 0 - }) - // NOWAIT: no 1s lock_timeout wait and no transaction retry behind the writer. - expect(elapsedMs).toBeLessThan(40 * STATEMENT_DELAY_MS) await expectNothingCommitted(context) }) diff --git a/cloud/apps/relay/src/assignment/regional-rehome/regional-rehome-target-selection.test.ts b/cloud/apps/relay/src/assignment/regional-rehome/regional-rehome-target-selection.test.ts deleted file mode 100644 index 75229b3cd4c..00000000000 --- a/cloud/apps/relay/src/assignment/regional-rehome/regional-rehome-target-selection.test.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { RelayAssignmentStore } from '../assignment-store.js' -import { openInMemoryRelayDatabase } from '../../database/database.js' -import { REGIONAL_REHOME_SQL_FAILURES_PER_CELL_LIMIT } from './regional-rehome-safety.js' - -const cell = (id: string, region: 'us-central1' | 'asia-east2') => ({ - id, - url: `https://${id}.relay.example.test`, - region, - capacityRequests: 100, - connectionHardCap: 1_000 as const, - connectionUnobservedBound: 60 -}) -const source = cell('us-c1', 'us-central1') -const noHeadroom = cell('asia-a', 'asia-east2') -const unclean = cell('asia-b', 'asia-east2') -const highLoad = cell('asia-c', 'asia-east2') -const lowLoad = cell('asia-d', 'asia-east2') - -const incarnation = (n: number) => - `${String(n).repeat(8)}-${String(n).repeat(4)}-4${String(n).repeat(3)}` + - `-8${String(n).repeat(3)}-${String(n).repeat(12)}` - -async function setup() { - let clock = 1_000_000 - const database = await openInMemoryRelayDatabase() - const store = new RelayAssignmentStore(database, () => clock, { - regionalRehomeCohortPercent: 100, - requireLiveCells: true, - heartbeatTtlMs: 45_000 - }) - await store.inspectRegionalRehomeControl() - clock += 24 * 60 * 60_000 - await store.applyRegionalRehomeControl({ - expectedGeneration: 0, - enabled: true, - notBefore: clock, - ratePerMinute: 10, - preferenceMaxAgeMs: 24 * 60 * 60_000, - hostCooldownMs: 7 * 24 * 60 * 60_000, - drainGraceMs: 60 * 60_000 - }) - await store.reconcileCells([source, noHeadroom, unclean, highLoad, lowLoad]) - const beat = async ( - config: typeof source, - n: number, - protocol: number, - state: { observedRequests: number; enforcedConnections: number; sqlFailures: number } - ) => { - await store.recordCellHeartbeat({ - cellId: config.id, - cellUrl: config.url, - region: config.region, - cellIncarnation: incarnation(n), - startedAt: 900_000, - ready: true, - observedRequests: state.observedRequests, - totalConnections: state.enforcedConnections, - inFlightConnections: 0, - reservedConnectionUnits: 0, - enforcedConnectionUnits: state.enforcedConnections, - connectionInclusionWatermark: clock, - connectionHardCap: 1_000, - connectionUnobservedBound: 60 - }) - await store.recordCellRegionalRehomeStatus({ - cellId: config.id, - cellIncarnation: incarnation(n), - regionalRehomeProtocol: protocol, - safety: { - observedAt: clock, - sqlFailures: state.sqlFailures, - reconnects: 0, - controlActivityRecoveryFailures: 0, - databasePoolWaiting: 0, - databasePoolWaitersMax: 0, - databasePoolWaitMsMax: 0 - } - }) - } - const activatePreferredSource = async () => { - const identity = { userId: 'user-1', relayHostId: 'abcdefghijklmnop' } - const assignment = await store.assign(identity, undefined, 'us-central1') - await store.activateControl(identity, { - cellId: source.id, - assignmentEpoch: assignment.assignmentEpoch, - generation: 1, - idleRegionalRehome: true, - cellIncarnation: incarnation(1) - }) - const { window } = await store.exchangeRegionCorrection( - identity, - { v: 1, action: 'issue-window' }, - assignment.assignmentEpoch - ) - expect(window).toBeDefined() - await store.exchangeRegionCorrection( - identity, - { - v: 1, - action: 'report', - generation: window!.generation, - assignmentEpoch: assignment.assignmentEpoch, - policyVersion: 1, - outcome: 'conclusive', - measurements: { 'us-central1': 180, 'asia-east2': 40 } - }, - assignment.assignmentEpoch - ) - } - const safety = () => ({ - observedAt: clock, - sqlFailures: 0, - reconnects: 0, - controlActivityRecoveryFailures: 0, - databasePoolWaiting: 0, - databasePoolWaitersMax: 0, - databasePoolWaitMsMax: 0 - }) - return { database, store, beat, activatePreferredSource, safety } -} - -const UNCLEAN = REGIONAL_REHOME_SQL_FAILURES_PER_CELL_LIMIT + 1 - -describe('regional rehome target selection', () => { - it('never selects a target without connection headroom, even at lowest load', async () => { - const context = await setup() - await context.beat(source, 1, 3, { - observedRequests: 0, - enforcedConnections: 0, - sqlFailures: 0 - }) - // Lowest load but the connection hard cap is exhausted. - await context.beat(noHeadroom, 2, 3, { - observedRequests: 0, - enforcedConnections: 999, - sqlFailures: 0 - }) - await context.beat(unclean, 3, 3, { - observedRequests: 0, - enforcedConnections: 0, - sqlFailures: UNCLEAN - }) - await context.beat(highLoad, 4, 3, { - observedRequests: 50, - enforcedConnections: 0, - sqlFailures: 0 - }) - await context.beat(lowLoad, 5, 3, { - observedRequests: 10, - enforcedConnections: 0, - sqlFailures: 0 - }) - await context.activatePreferredSource() - - const candidates = await context.store.selectIdleRegionalRehomeCandidates(context.safety()) - const attempt = candidates[0] - expect(attempt?.targetCellId).toBe(lowLoad.id) - expect(await context.store.commitIdleRegionalRehome(attempt!, context.safety(), 100)).toEqual({ - outcome: 'committed' - }) - await context.database.close() - }) - - it('falls to the next clean target when the load winner goes unclean', async () => { - const context = await setup() - await context.beat(source, 1, 3, { - observedRequests: 0, - enforcedConnections: 0, - sqlFailures: 0 - }) - await context.beat(noHeadroom, 2, 3, { - observedRequests: 0, - enforcedConnections: 999, - sqlFailures: 0 - }) - await context.beat(unclean, 3, 3, { - observedRequests: 0, - enforcedConnections: 0, - sqlFailures: UNCLEAN - }) - await context.beat(highLoad, 4, 3, { - observedRequests: 50, - enforcedConnections: 0, - sqlFailures: 0 - }) - await context.beat(lowLoad, 5, 3, { - observedRequests: 10, - enforcedConnections: 0, - sqlFailures: UNCLEAN - }) - await context.activatePreferredSource() - - const candidates = await context.store.selectIdleRegionalRehomeCandidates(context.safety()) - const attempt = candidates[0] - expect(attempt?.targetCellId).toBe(highLoad.id) - expect(await context.store.commitIdleRegionalRehome(attempt!, context.safety(), 100)).toEqual({ - outcome: 'committed' - }) - await context.database.close() - }) -}) diff --git a/cloud/apps/relay/src/assignment/regional-rehome/regional-rehome-worker.test.ts b/cloud/apps/relay/src/assignment/regional-rehome/regional-rehome-worker.test.ts deleted file mode 100644 index 3bb0b621dc9..00000000000 --- a/cloud/apps/relay/src/assignment/regional-rehome/regional-rehome-worker.test.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' -import type { RelayAssignmentStore } from '../assignment-store.js' -import type { RelayConfig } from '../../config.js' -import { startRegionalRehomeWorker } from './regional-rehome-worker.js' - -describe('regional rehome worker', () => { - afterEach(() => vi.restoreAllMocks()) - - it('bounds empty polling to the six-second cadence and stops its timer', async () => { - vi.useFakeTimers() - const selectIdleRegionalRehomeCandidates = vi.fn().mockResolvedValue([]) - const worker = startRegionalRehomeWorker( - config(), - { - selectIdleRegionalRehomeCandidates - } as unknown as RelayAssignmentStore, - { - safetySnapshot: () => safety(Date.now()), - random: () => 0 - } - )! - try { - await vi.advanceTimersByTimeAsync(0) - expect(selectIdleRegionalRehomeCandidates).toHaveBeenCalledTimes(1) - await vi.advanceTimersByTimeAsync(5_999) - expect(selectIdleRegionalRehomeCandidates).toHaveBeenCalledTimes(1) - await vi.advanceTimersByTimeAsync(1) - expect(selectIdleRegionalRehomeCandidates).toHaveBeenCalledTimes(2) - worker.stop() - await vi.advanceTimersByTimeAsync(60_000) - expect(selectIdleRegionalRehomeCandidates).toHaveBeenCalledTimes(2) - } finally { - worker.stop() - vi.useRealTimers() - } - }) - - it('stops walking the page when the source names a deferral no candidate can pass', async () => { - const fetchImpl = respondWith([{ outcome: 'deferred', reason: 'concurrency-limit' }]) - const summaries = collectSummaries() - try { - await runOnePoll(fetchImpl, 3, summaries) - } finally { - summaries.restore() - } - - expect(fetchImpl).toHaveBeenCalledTimes(1) - expect(summaries.entries).toEqual([ - { - event: 'orca_relay_idle_rehome_dispatch_summary', - candidates: 3, - dispatched: 1, - stoppedBy: 'concurrency-limit', - outcomes: { 'deferred:concurrency-limit': 1 } - } - ]) - }) - - it('keeps its whole-page walk when the source sends no reason at all', async () => { - const fetchImpl = respondWith([{ outcome: 'deferred' }]) - const summaries = collectSummaries() - try { - await runOnePoll(fetchImpl, 3, summaries) - } finally { - summaries.restore() - } - - expect(fetchImpl).toHaveBeenCalledTimes(3) - expect(summaries.entries[0]).toMatchObject({ - stoppedBy: null, - outcomes: { deferred: 3 } - }) - }) - - it('walks past a deferral that only concerns the one candidate', async () => { - const fetchImpl = respondWith([ - { outcome: 'deferred', reason: 'host-unsupported' }, - { outcome: 'busy' }, - { outcome: 'committed' } - ]) - const summaries = collectSummaries() - try { - await runOnePoll(fetchImpl, 4, summaries) - } finally { - summaries.restore() - } - - expect(fetchImpl).toHaveBeenCalledTimes(3) - expect(summaries.entries[0]).toMatchObject({ - dispatched: 3, - stoppedBy: 'committed', - outcomes: { 'deferred:host-unsupported': 1, busy: 1, committed: 1 } - }) - }) -}) - -function respondWith(bodies: { outcome: string; reason?: string }[]) { - let index = 0 - return vi.fn(async () => { - const body = bodies[Math.min(index++, bodies.length - 1)]! - return new Response(JSON.stringify({ v: 1, ...body }), { - headers: { 'content-type': 'application/json' } - }) - }) -} - -function collectSummaries() { - const entries: Record[] = [] - let arrived: (() => void) | undefined - // Construction immediately polls; await first instead of waiting for a timer tick. - const first = new Promise((resolve) => { - arrived = resolve - }) - const original = console.warn - console.warn = (line: unknown, ...rest: unknown[]) => { - try { - const parsed = JSON.parse(line as string) as Record - if (parsed.event === 'orca_relay_idle_rehome_dispatch_summary') { - entries.push(parsed) - arrived?.() - return - } - } catch { - // Not a JSON log line; fall through to the original writer. - } - original(line as string, ...rest) - } - return { entries, first, restore: () => (console.warn = original) } -} - -async function runOnePoll( - fetchImpl: typeof fetch, - candidates: number, - summaries: { first: Promise } -): Promise { - const assignments = { - selectIdleRegionalRehomeCandidates: vi.fn(async () => - Array.from({ length: candidates }, (_, index) => ({ - v: 1 as const, - attemptId: `00000000-0000-4000-8000-00000000000${index}`, - userId: `user-${index}`, - relayHostId: 'abcdefghijklmnop', - sourceCellId: 'us-c1', - sourceCellUrl: 'https://us-c1.relay.example.test', - sourceCellIncarnation: '11111111-1111-4111-8111-111111111111', - sourceAssignmentEpoch: 1, - sourceGeneration: 1, - targetCellId: 'asia-c1' - })) - ) - } as unknown as RelayAssignmentStore - const worker = startRegionalRehomeWorker(config(), assignments, { - fetch: fetchImpl, - identityToken: async () => 'token', - safetySnapshot: () => safety(Date.now()), - intervalMs: 60_000 - })! - try { - await summaries.first - } finally { - worker.stop() - } -} - -function config(overrides: Partial = {}): RelayConfig { - return { - role: 'director', - rehomeAudience: 'https://relay.example.test/v1/admin/host-drain', - rehomeDirectorServiceAccount: 'relay-director@example.test', - ...overrides - } as RelayConfig -} - -function safety(observedAt: number) { - return { - requiredCells: 2, - missingCells: 0, - observedAt, - sqlFailures: 0, - reconnects: 0, - controlActivityRecoveryFailures: 0, - databasePoolTotal: 3, - databasePoolIdle: 3, - databasePoolWaiting: 0, - databasePoolWaitersMax: 0, - databasePoolOldestWaitMs: 0, - databasePoolWaitMsMax: 0 - } -} diff --git a/cloud/apps/relay/src/assignment/reserve-placement/placement-load-band.test.ts b/cloud/apps/relay/src/assignment/reserve-placement/placement-load-band.test.ts index a1ca010f8bf..27a35f11b2f 100644 --- a/cloud/apps/relay/src/assignment/reserve-placement/placement-load-band.test.ts +++ b/cloud/apps/relay/src/assignment/reserve-placement/placement-load-band.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { PLACEMENT_INTAKE_SHARE, PlacementLoadBand } from './placement-load-band.js' +import { PlacementLoadBand } from './placement-load-band.js' import { RelayAssignmentStore } from '../assignment-store.js' import { openInMemoryRelayDatabase } from '../../database/database.js' @@ -13,74 +13,8 @@ function seededRandom(seed: number): () => number { } } -type SimulatedCell = { cellId: string; reserved: number; capacity: number } - const quiet = () => undefined -// Each pick goes to one of the directors at random, as the load balancer spreads requests. -function simulate( - directors: PlacementLoadBand[], - cells: SimulatedCell[], - picks: number, - at: (index: number) => number, - route: () => number = seededRandom(99) -) { - const counts = new Map(cells.map((cell) => [cell.cellId, 0])) - for (let index = 0; index < picks; index += 1) { - const ranked = cells - .filter((cell) => cell.reserved < cell.capacity) - .map((cell) => ({ cellId: cell.cellId, loadRatio: cell.reserved / cell.capacity, cell })) - .sort( - (left, right) => left.loadRatio - right.loadRatio || left.cellId.localeCompare(right.cellId) - ) - const director = directors[Math.floor(route() * directors.length)] - const picked = director?.pick(ranked, at(index)) - if (!picked) throw new Error('no candidate') - picked.cell.reserved += 1 - counts.set(picked.cellId, (counts.get(picked.cellId) ?? 0) + 1) - } - return counts -} - -// c27's drain on 10-08: 2,770 hosts onto one empty Asia cell (2,805 free) -// and four fuller ones (2,311 free between them). -function c27DrainCells(): SimulatedCell[] { - return [ - { cellId: 'cell-empty', reserved: 0, capacity: 4000 }, - ...['cell-b', 'cell-c', 'cell-d', 'cell-e'].map((cellId, index) => ({ - cellId, - reserved: 4000 - 577 - (index === 0 ? 3 : 0), - capacity: 4000 - })) - ] -} - -function drainShare(paceMs: number, seed: number) { - const cells = c27DrainCells() - const directors = Array.from( - { length: 5 }, - (_, index) => new PlacementLoadBand(seededRandom(seed + index), quiet) - ) - const hosts = 2770 - const counts = simulate(directors, cells, hosts, (index) => (index * paceMs) / hosts) - return { counts, cells, share: (counts.get('cell-empty') ?? 0) / hosts } -} - -describe('PlacementLoadBand', () => { - it.each([ - ['1,200 s', 1_200_000], - ['300 s', 300_000] - ])('keeps the empty cell near the share across five directors at the %s pace', (_, paceMs) => { - for (const seed of [1, 2, 3]) { - const { counts, cells, share } = drainShare(paceMs, seed * 10) - expect([...counts.values()].reduce((sum, count) => sum + count, 0)).toBe(2770) - expect(share).toBeGreaterThan(0.3) - expect(share).toBeLessThan(0.5) - for (const cell of cells) expect(cell.reserved).toBeLessThanOrEqual(cell.capacity) - } - }) -}) - describe('leastLoadedCell with the load band', () => { async function setupRegions() { const database = await openInMemoryRelayDatabase() @@ -121,20 +55,6 @@ describe('leastLoadedCell with the load band', () => { return counts } - it('spreads within the preferred region and keeps an empty US cell out', async () => { - const { database, store } = await setupRegions() - try { - const counts = await placements(store, 200) - - expect(counts.get('us-a')).toBeUndefined() - expect(counts.get('asia-a')).toBeLessThanOrEqual(PLACEMENT_INTAKE_SHARE * 200 + 1) - const used = [...counts.values()].filter((count) => count >= 20) - expect(used.length).toBeGreaterThanOrEqual(3) - } finally { - await database.close() - } - }) - it('fills the preferred region to its hard cap before spilling, never past it', async () => { const { database, store } = await setupRegions() try { diff --git a/cloud/apps/relay/src/assignment/reserve-placement/reserve-ledger-writer.test.ts b/cloud/apps/relay/src/assignment/reserve-placement/reserve-ledger-writer.test.ts index 88f3b5875c4..66a5e7b15fc 100644 --- a/cloud/apps/relay/src/assignment/reserve-placement/reserve-ledger-writer.test.ts +++ b/cloud/apps/relay/src/assignment/reserve-placement/reserve-ledger-writer.test.ts @@ -57,30 +57,15 @@ describe('reserve ledger writer', () => { expect(await row(db, 'bbbbbbbbbbbbbbbb')).toMatchObject({ cell_id: 'c1', reserved_controls: 2 }) }) - it('keeps its queue through a database stall and drops the oldest past the bound', async () => { + it('persists a booked join after a database stall', async () => { let now = 1_000 const { database: db, writer } = await setup(() => now) - const query = vi.spyOn(db, 'query').mockRejectedValueOnce(new Error('timeout')) + vi.spyOn(db, 'query').mockRejectedValueOnce(new Error('timeout')) writer.enqueue({ ...HOST, cellId: 'c1', epoch: 5 }) await writer.flush() - expect(writer.pending()).toBe(1) - await writer.flush() - expect(query).toHaveBeenCalledTimes(1) now += 1_000 await writer.flush() - expect(writer.pending()).toBe(0) expect(await row(db)).toMatchObject({ cell_id: 'c1', assignment_epoch: 5 }) - - const bounded = new ReserveLedgerWriter(db, { queueMax: 2, log: () => undefined }) - const hosts = ['oldest0000000000', 'middle0000000000', 'newest0000000000'] - for (const relayHostId of hosts) { - bounded.enqueue({ ...HOST, relayHostId, cellId: 'c1', epoch: 1 }) - } - expect(bounded.pending()).toBe(2) - await bounded.flush() - expect(await row(db, hosts[0])).toBeUndefined() - expect(await row(db, hosts[1])).toMatchObject({ cell_id: 'c1', assignment_epoch: 1 }) - expect(await row(db, hosts[2])).toMatchObject({ cell_id: 'c1', assignment_epoch: 1 }) }) it('reconciles the map’s seats and never lowers an epoch', async () => { diff --git a/cloud/apps/relay/src/assignment/reserve-placement/reserve-placement-simulation.test.ts b/cloud/apps/relay/src/assignment/reserve-placement/reserve-placement-simulation.test.ts deleted file mode 100644 index ed6808510f0..00000000000 --- a/cloud/apps/relay/src/assignment/reserve-placement/reserve-placement-simulation.test.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { - runReservePlacementSimulation, - type SimulationConfig, - type SimulationReport -} from './reserve-placement-simulation.js' - -const SEEDS = [11, 23, 47] -const rtt = (region: string) => (region === 'asia-east2' ? 170 : 10) - -function fleet(): SimulationConfig['cells'] { - const cells: SimulationConfig['cells'] = [] - for (let index = 0; index < 24; index += 1) { - cells.push({ - cellId: `us${String(index).padStart(2, '0')}`, - region: 'us-central1', - hardCap: 600, - ceiling: 480, - intakePerSec: 5, - // A third of the fleet stays in database mode, one cell on an image without step 5. - mode: index % 3 === 0 ? 'db' : 'reserve', - old: index === 0, - preseated: 150 - }) - } - for (let index = 0; index < 6; index += 1) { - cells.push({ - cellId: `asia${index}`, - region: 'asia-east2', - hardCap: 600, - ceiling: 480, - intakePerSec: 2, - mode: index === 0 ? 'db' : 'reserve', - preseated: 200 - }) - } - return cells -} - -function expectInvariants(report: SimulationReport): void { - expect(report.violations).toEqual([]) -} - -describe('step 5 deterministic simulation', () => { - // Invariant 9 (dead-man) never trips here; its own tests below cover it. - it.each(SEEDS)( - 'holds invariants 1-8 across mixed versions, lagging polls, restarts and database stalls (seed %i)', - async (seed) => { - const report = await runReservePlacementSimulation({ - seed, - durationMs: 25 * 60_000, - directors: 5, - // One director still on the database-only revision. - oldDirectors: 1, - cells: fleet(), - hosts: 6_600, - hostRegion: (index) => (index % 5 === 0 ? 'asia-east2' : 'us-central1'), - arrivalPerSec: 20, - meanSessionMs: 8 * 60_000, - reconnectDelayMs: [500, 5_000], - duplicateAssignShare: 0.05, - halfOpenShare: 0.05, - pollLagMs: (director) => [0, 200, 1_000, 4_000, 10_000][director]!, - rttMs: rtt, - cellRestarts: [{ at: 6 * 60_000, cellId: 'us04' }, { at: 14 * 60_000, cellId: 'asia2' }], - directorRestarts: [{ at: 8 * 60_000, director: 2 }, { at: 16 * 60_000, director: 4 }], - // Drained hosts land elsewhere while their old seat lingers: placement supersedes it. - drains: [{ at: 12 * 60_000, cellId: 'us05', paceMs: 60_000 }], - databaseStalls: Array.from({ length: 3 }, (_, index) => ({ - at: (index * 610 + 120) * 1_000, - durationMs: 7_000 - })) - }) - expectInvariants(report) - expect(report.reservePlacements).toBeGreaterThan(500) - expect(report.stickyAnswers).toBeGreaterThan(500) - expect(report.seatedAtEnd).toBeGreaterThan(4_000) - // The run reached the paths the invariants guard, so a pass is not vacuous. - expect(report.supersedes).toBeGreaterThan(0) - expect(report.rowAheadDemotions).toBeGreaterThan(0) - expect(report.directorRestarts).toBe(2) - expect(report.stickyReserves).toBeGreaterThan(0) - expect(report.databasePlacements).toBeGreaterThan(0) - }, - 120_000 - ) - - it.each(SEEDS)( - 'near full: extra reserve calls stay within 10% and exhausted requests within 1% (seed %i)', - async (seed) => { - const cells = Array.from({ length: 17 }, (_, index) => ({ - cellId: `us${index}`, - region: 'us-central1' as const, - hardCap: 700, - ceiling: 600, - intakePerSec: 50, - mode: 'reserve' as const, - preseated: 582 - })) - const report = await runReservePlacementSimulation({ - seed, - durationMs: 3 * 60_000, - directors: 5, - cells, - hosts: 17 * 582 + 300, - hostRegion: () => 'us-central1', - arrivalPerSec: 10, - meanSessionMs: 0, - reconnectDelayMs: [1_000, 3_000], - duplicateAssignShare: 0, - halfOpenShare: 0, - pollLagMs: () => 0, - rttMs: rtt - }) - expectInvariants(report) - expect(report.reservePlacements).toBeGreaterThan(250) - expect(report.reserveCalls / report.reservePlacements - 1).toBeLessThanOrEqual(0.1) - expect(report.exhausted / report.requests).toBeLessThanOrEqual(0.01) - for (const share of Object.values(report.maxOccupancyShare)) expect(share).toBeLessThanOrEqual(1) - }, - 120_000 - ) - - it.each(SEEDS.flatMap((seed) => [[1_200_000, seed], [300_000, seed]] as const))( - 'a drain onto an empty cell at the %i ms pace spreads within each cell intake budget (seed %i)', - async (paceMs, seed) => { - // Production c27 drain: 1,200s pacing toward cells with 2,805/841/777/667/26 free seats. - const free = [2_805, 841, 777, 667, 26] - const cells: SimulationConfig['cells'] = free.map((seats, index) => ({ - cellId: `asia${index + 1}`, - region: 'asia-east2', - hardCap: 3_000, - ceiling: 2_830, - intakePerSec: 2, - mode: 'reserve', - preseated: 2_830 - seats - })) - cells.unshift({ - cellId: 'asia0', - region: 'asia-east2', - hardCap: 3_000, - ceiling: 2_830, - intakePerSec: 2, - mode: 'reserve', - preseated: 2_770 - }) - const preseated = cells.reduce((sum, cell) => sum + (cell.preseated ?? 0), 0) - const report = await runReservePlacementSimulation({ - seed, - durationMs: 30 * 60_000, - directors: 5, - cells, - hosts: preseated, - hostRegion: () => 'asia-east2', - arrivalPerSec: 1, - meanSessionMs: 0, - reconnectDelayMs: [1_000, 3_000], - duplicateAssignShare: 0.02, - halfOpenShare: 0, - pollLagMs: () => 0, - rttMs: rtt, - drains: [{ at: 10_000, cellId: 'asia0', paceMs }] - }) - expectInvariants(report) - // Below every cell's budget nothing waits; above it the cells' buckets pace the drain. - if (paceMs === 300_000) expect(report.intakeRefusals + report.paced).toBeGreaterThan(0) - const moved = Object.entries(report.arrivalsByCell) - const total = moved.reduce((sum, [, count]) => sum + count, 0) - expect(total).toBeGreaterThan(2_000) - // Spread: the emptiest cell takes a share, not the drain. - expect((report.arrivalsByCell.asia1 ?? 0) / total).toBeLessThan(0.6) - expect(moved.filter(([, count]) => count / total > 0.1).length).toBeGreaterThanOrEqual(3) - }, - 120_000 - ) - - - -}) diff --git a/cloud/apps/relay/src/assignment/reserve-placement/reserve-placement-simulation.ts b/cloud/apps/relay/src/assignment/reserve-placement/reserve-placement-simulation.ts deleted file mode 100644 index f39811009ec..00000000000 --- a/cloud/apps/relay/src/assignment/reserve-placement/reserve-placement-simulation.ts +++ /dev/null @@ -1,1357 +0,0 @@ -import { createHash } from 'node:crypto' -import { RELAY_PROTOCOL_LIMITS } from '@orca-cloud/relay-contract/protocol-limits' -import type { RelayRegion } from '@orca-cloud/relay-contract/relay-regions' -import { CellReserveBook } from '../../cell/reservations/cell-reserve-book.js' -import { CELL_INTAKE_BURST, CELL_RESERVE_TTL_MS, type ReserveOutcome } from '../../cell/reservations/cell-reserve-contract.js' -import { - mintEpoch, - RESERVE_CELL_FRESH_MS, - RESERVE_MAX_TRIES, - ReservePlacer, - type PlacementCell -} from './reserve-placement.js' -import { seatOutranks } from '../../cell/seats/shadow-seat-directory.js' - -// Seeded event model uses real CellReserveBook/ReservePlacer; sockets, DB and desktops are modeled. -// Record invariant violations without throwing so each run exposes every class it hits. - -export function seededRandom(seed: number): () => number { - let state = seed >>> 0 - return () => { - state = (state + 0x6d2b79f5) >>> 0 - let value = state - value = Math.imul(value ^ (value >>> 15), value | 1) - value ^= value + Math.imul(value ^ (value >>> 7), value | 61) - return ((value ^ (value >>> 14)) >>> 0) / 4_294_967_296 - } -} - -type Event = { at: number; seq: number; run: () => void } - -class Clock { - now = 0 - private seq = 0 - private readonly heap: Event[] = [] - - schedule(delayMs: number, run: () => void): void { - const event = { at: this.now + Math.max(0, Math.round(delayMs)), seq: this.seq++, run } - this.heap.push(event) - let index = this.heap.length - 1 - while (index > 0) { - const parent = (index - 1) >> 1 - if (!before(this.heap[index]!, this.heap[parent]!)) break - ;[this.heap[index], this.heap[parent]] = [this.heap[parent]!, this.heap[index]!] - index = parent - } - } - - async runUntil(until: number, afterEach: () => void): Promise { - while (this.heap.length > 0 && this.heap[0]!.at <= until) { - const event = this.pop() - this.now = event.at - event.run() - // Lets the promise continuations the event released settle before the next event. - for (let tick = 0; tick < 12; tick += 1) await Promise.resolve() - afterEach() - } - this.now = until - } - - private pop(): Event { - const top = this.heap[0]! - const last = this.heap.pop()! - if (this.heap.length > 0) { - this.heap[0] = last - let index = 0 - for (;;) { - const left = index * 2 + 1 - const right = left + 1 - let smallest = index - if (left < this.heap.length && before(this.heap[left]!, this.heap[smallest]!)) smallest = left - if (right < this.heap.length && before(this.heap[right]!, this.heap[smallest]!)) smallest = right - if (smallest === index) break - ;[this.heap[index], this.heap[smallest]] = [this.heap[smallest]!, this.heap[index]!] - index = smallest - } - } - return top - } -} - -function before(left: Event, right: Event): boolean { - return left.at < right.at || (left.at === right.at && left.seq < right.seq) -} - -// `leased`: admitted through the database check, so its control renews a lease there and is -// closed when the row moves on, as today. -type CellSeat = { epoch: number; joinedAt: number; reservedBy?: string; demoted: boolean; leased?: boolean } -type CellLogEntry = - | { kind: 'join'; host: string; epoch: number; joinedAt: number; reservedBy?: string } - | { kind: 'leave'; host: string; epoch: number; at: number; wrongCell: boolean } - | { kind: 'reserve'; host: string; epoch: number } - | { kind: 'demoted'; host: string; epoch: number } - -const RECENT_SEAT_MS = 10 * 60_000 -const DEMOTE_CLOSE_MS = 60_000 -const CONTROL_RENEWAL_MS = 25_000 -// As the director's: a seat this young may still be superseded. -const RECONCILE_SETTLE_MS = 30_000 -// A drained seat lingers this long (its splices finishing) unless superseded first. -const DRAIN_GRACE_MS = 30_000 -// A cell closes a control silent for 75 s, checked once per ping (host-session-registry.ts heartbeat). -const CONTROL_SILENCE_CLOSE_MS = 75_000 + RELAY_PROTOCOL_LIMITS.controlPingIntervalMs -// Longer than the slowest duplicate: a half-open seat's silence close. -const QUIESCE_MS = 150_000 - -export type SimulationViolation = { invariant: number; at: number; detail: string } - -// Each fault breaks one rule on purpose, so a test can show the matching invariant fires. -export type SimulationFaults = { - bookPastCap?: boolean - intakeBurst?: number - // Each director assumes this share of every cell's intake, instead of a fifth. - directorShare?: number - // Nothing supersedes the old seat, at placement or from the feed: a duplicate outlives its bound. - skipSupersede?: boolean - // Only placement supersedes: a half-open seat the booking map had not seen lasts to its silence close. - skipFeedSupersede?: boolean - // A restarted director forgets its ledger queue and never reconciles. - noBootReconcile?: boolean - // The reconcile leaves a seat whose row is ahead of it alone. - noRowAheadDemote?: boolean - // Directors do not read the row after a WRONG_CELL leave (only the demoting one has a floor). - noWrongCellRowRead?: boolean - // Reserve placement re-uses the floor epoch instead of minting above it. - mintAtFloor?: boolean - // Today's path skips every cell Postgres records as reserve, ignoring the cell's own row. - pgReserveBlocksDatabase?: boolean - // Directors place by the reported mode, which stays reserve while a tripped cell re-registers. - noRawFeedMode?: boolean - unguardedLedger?: boolean - noEpochFloor?: boolean - // On a level divergence each director keeps the seat its own cell order prefers, not the row's. - localTieBreak?: boolean -} - -// Model cell-owned raw-mode writes every 15s; current-process db evidence permits placement within its 45s freshness bound. -const CELL_ADMIT_EFFECTIVE_REFRESH_MS = 15_000 -const CELL_ADMIT_EFFECTIVE_FRESH_MS = 45_000 -// A tripped cell reports reserve until every control it admitted from memory holds a lease. -const DEFAULT_REREGISTRATION_MS = 20_000 - -// cell-reserve-dead-man.ts's window: 60-120 s, fixed per cell. -function deadManWindowMs(cellId: string): number { - return 60_000 + (createHash('sha256').update(cellId).digest().readUInt32BE(0) % 60_001) -} - -class SimDatabase { - up = true - // relay_cell_admit_modes: what the flag workflow recorded. A trip does not change it. - readonly reserveCells = new Set() - readonly rows = new Map() - // relay_cell_admit_effective: written by each cell itself, never by a director. - readonly admitEffective = new Map() - private readonly counts = new Map() - - set(host: string, row: { cellId: string; epoch: number }): void { - const previous = this.rows.get(host) - if (previous) this.counts.set(previous.cellId, (this.counts.get(previous.cellId) ?? 1) - 1) - this.counts.set(row.cellId, (this.counts.get(row.cellId) ?? 0) + 1) - this.rows.set(host, row) - } - - constructor( - private readonly violate: (invariant: number, detail: string) => void, - private readonly faults: SimulationFaults - ) {} - - // The director's after-the-fact write, a guarded mirror: only a higher epoch replaces a row. - upsert(host: string, cellId: string, epoch: number): void { - const row = this.rows.get(host) - const guarded = !this.faults.unguardedLedger - if (guarded && row && row.epoch >= epoch) return - if (row && epoch < row.epoch) this.violate(5, `ledger epoch fell for ${host}`) - this.set(host, { cellId, epoch }) - } - - // Today's assign writes the row it answers with. - assign(host: string, cellId: string, epoch: number): void { - const row = this.rows.get(host) - if (row && epoch <= row.epoch) this.violate(5, `db mint not above row for ${host}`) - this.set(host, { cellId, epoch }) - } - - countOn(cellId: string): number { - return this.counts.get(cellId) ?? 0 - } -} - -class SimCell { - readonly seats = new Map() - readonly recent = new Map() - readonly log: CellLogEntry[] = [] - // Per incarnation: a restarted cell starts with a full bucket. - readonly okBookingTimes: number[][] = [[]] - incarnation = 1 - draining = false - mode: 'reserve' | 'db' - bookingUnits = 0 - // What relay_cells says to a director that predates step 5: frozen at the flip. - frozenDatabaseCount = 0 - // Latch db after 60–120s without placing-director booking/poll contact (cell-reserve-dead-man.ts). - lastContactAt = 0 - trippedAt: number | null = null - reregisteredAt = 0 - // Booted and no flag object read yet: its mode is a default, so it writes no admit row. - flagsUnread = false - book: CellReserveBook - - constructor( - readonly cellId: string, - readonly region: RelayRegion, - readonly hardCap: number, - readonly ceiling: number, - readonly intakePerSec: number, - mode: 'reserve' | 'db', - // An image that predates step 5 reports no ceiling and has no reserve route. - readonly old: boolean, - private readonly clock: Clock, - private readonly faults: SimulationFaults, - private readonly violate: (invariant: number, detail: string) => void - ) { - this.mode = mode - this.book = this.newBook() - } - - private newBook(): CellReserveBook { - const limit = () => (this.faults.bookPastCap ? Infinity : this.ceiling) - return new CellReserveBook( - { - canReserve: () => this.seats.size + this.bookingUnits + 1 <= limit(), - tryReserve: () => { - if (this.seats.size + this.bookingUnits + 1 > limit()) return null - this.bookingUnits += 1 - let released = false - return { - release: () => { - if (released) return - released = true - this.bookingUnits -= 1 - } - } - } - }, - () => this.intakePerSec, - () => this.clock.now, - this.faults.intakeBurst - ) - } - - occupancy(): number { - return this.seats.size + this.bookingUnits - } - - contact(): void { - this.lastContactAt = this.clock.now - } - - checkDeadMan(windowMs: number, reregistrationMs: number): void { - if (this.mode !== 'reserve' || this.old || this.clock.now - this.lastContactAt <= windowMs) return - this.mode = 'db' - this.trippedAt = this.clock.now - this.reregisteredAt = this.clock.now + reregistrationMs - } - - // What the feed's admitModeEffective reports: `mode` is the raw one (switch file and dead-man). - effectiveMode(): 'db' | 'reserve' { - return this.mode === 'reserve' || this.clock.now < this.reregisteredAt ? 'reserve' : 'db' - } - - reserve(directorId: string, host: string, epoch: number, sticky: boolean): ReserveOutcome { - if (this.mode === 'reserve') this.contact() - const outcome = this.book.reserve( - directorId, - { userId: host, relayHostId: 'abcdefghijklmnop', epoch, ttlMs: CELL_RESERVE_TTL_MS, sticky }, - { - mode: this.mode, - draining: this.draining, - seatedEpoch: (userId) => this.seats.get(userId)?.epoch - } - ) - if (outcome.outcome === 'ok') { - this.log.push({ kind: 'reserve', host, epoch }) - if (!sticky) this.okBookingTimes.at(-1)!.push(this.clock.now) - } - return outcome - } - - hello(host: string, epoch: number, database: SimDatabase): boolean { - if (this.draining) return false - if (this.mode === 'reserve') { - const booking = this.book.take(host, 'abcdefghijklmnop', epoch) - if (booking) return this.seat(host, epoch, booking.directorId) - const seated = this.seats.get(host) - const recent = this.recent.get(host) - if ( - (seated && seated.epoch === epoch && !seated.demoted) || - (recent && recent.epoch === epoch && this.clock.now - recent.at <= RECENT_SEAT_MS) - ) { - return this.seat(host, epoch) - } - } - if (!database.up) return false - const row = database.rows.get(host) - if (!row || row.cellId !== this.cellId || row.epoch !== epoch) return false - return this.seat(host, epoch, undefined, true) - } - - private seat(host: string, epoch: number, reservedBy?: string, leased = false): boolean { - // A rebind keeps its unit; a new seat needs one under the hard cap. - if (!this.seats.has(host) && this.occupancy() + 1 > this.hardCap) return false - this.seats.set(host, { epoch, joinedAt: this.clock.now, reservedBy, demoted: false, leased }) - this.recent.delete(host) - this.log.push({ kind: 'join', host, epoch, joinedAt: this.clock.now, reservedBy }) - return true - } - - leave(host: string): void { - const seat = this.seats.get(host) - if (!seat) return - this.seats.delete(host) - if (!seat.demoted) this.recent.set(host, { epoch: seat.epoch, at: this.clock.now }) - this.log.push({ kind: 'leave', host, epoch: seat.epoch, at: this.clock.now, wrongCell: seat.demoted }) - } - - // The cell's own check: only the seat named by epoch and join, and only in reserve mode. - demote(host: string, epoch: number, joinedAt: number, onClose: () => void): void { - if (this.mode !== 'reserve') return - const seat = this.seats.get(host) - if (!seat || seat.epoch !== epoch || seat.joinedAt !== joinedAt || seat.demoted) return - seat.demoted = true - this.log.push({ kind: 'demoted', host, epoch }) - this.clock.schedule(DEMOTE_CLOSE_MS, () => { - if (this.seats.get(host) === seat) { - this.leave(host) - onClose() - } - }) - } - - // Seats, bookings and memory are gone; the image boots in database mode until its first flag read. - restart(): string[] { - const hosts = [...this.seats.keys()] - for (const host of hosts) this.leave(host) - this.book.clear() - this.book = this.newBook() - this.recent.clear() - this.incarnation += 1 - this.okBookingTimes.push([]) - const mode = this.mode - this.mode = 'db' - this.flagsUnread = true - this.clock.schedule(5_000, () => { - this.mode = mode - this.flagsUnread = false - }) - return hosts - } -} - -type MapSeat = { epoch: number; joinedAt: number; reservedBy?: string; demoted: boolean } - -type DirectorCellView = { - incarnation: number | null - cursor: number - seats: Map - placement: PlacementCell -} - -type Answer = - | { kind: 'cell'; cellId: string; epoch: number; calls: number } - | { kind: 'retry'; afterMs: number; calls: number } - -export type SimulationConfig = { - seed: number - durationMs: number - directors: number - oldDirectors?: number - cells: Array<{ - cellId: string - region: RelayRegion - hardCap: number - ceiling: number - intakePerSec: number - mode: 'reserve' | 'db' - old?: boolean - preseated?: number - }> - hosts: number - hostRegion: (index: number) => RelayRegion - // Fresh connects arrive at this rate per second until every host has connected once. - arrivalPerSec: number - meanSessionMs: number - reconnectDelayMs: [number, number] - duplicateAssignShare: number - // Share of departures whose old control stays half-open on the cell while the desktop - // connects elsewhere: the real source of a host seated on two cells. - halfOpenShare: number - pollLagMs: (director: number) => number - rttMs: (region: RelayRegion) => number - faults?: SimulationFaults - cellRestarts?: Array<{ at: number; cellId: string }> - // Restarted estimates refill from zero at intake/directors: no booking until one token; unheard hosts also face the 30s startup gate. - directorRestarts?: Array<{ at: number; director: number }> - databaseStalls?: Array<{ at: number; durationMs: number }> - // Every director misses every feed poll for this long (a director-side outage). - pollOutages?: Array<{ at: number; durationMs: number }> - // How long a tripped cell keeps reporting reserve while it leases its memory-admitted controls. - reregistrationMs?: number - drains?: Array<{ at: number; cellId: string; paceMs: number }> -} - -export type SimulationReport = { - violations: SimulationViolation[] - requests: number - placements: number - reserveCalls: number - exhausted: number - paced: number - databasePlacements: number - reservePlacements: number - // Rows that named a placement the desktop never used, rewritten to its real seat. - supersedes: number - directorRestarts: number - rowAheadDemotions: number - // Demotions whose winner is level with the loser (same epoch, another cell): the tie-break's cases. - levelDemotions: number - // Two directors judging a same-epoch divergence exercise invariant 4; higher epochs already win consistently. - contestedDivergences: number - // Row-ahead demotions per demoted host: a repair that converges needs one. - rowAheadDemotionsPerHost: { max: number; p99: number } - deadManTrips: number - // Bookings sent to a cell after its dead-man tripped (the cell answers `off`). - reserveCallsOnTrippedCells: number - databasePlacementsWhilePollsFail: number - // From the first poll outage's start to the first fresh database placement inside it. - pollFailureGapMs: number | null - // First seats after a row-ahead demotion that land behind (or level with) the row again. - badFirstReseats: number - intakeRefusals: number - stickyReserves: number - stickyAnswers: number - seatedAtEnd: number - maxOccupancyShare: Record - arrivalsByCell: Record -} - -class SimDirector { - placer!: ReservePlacer - views = new Map() - recentlyLeft = new Map< - string, - { cellId: string; epoch: number; at: number; incarnation: number; wrongCell?: boolean } - >() - reserveEpochs = new Map() - // Row epochs this director demoted a host's seat behind: its next mint for the host clears them. - rowFloors = new Map() - ledgerQueue: Array<{ host: string; cellId: string; epoch: number }> = [] - answeredCells = new Set() - // Set by a restart: the lost ledger queue is rebuilt by one reconcile once the map is complete. - bootReconcilePending = false - restarted = false - readonly id: string - - constructor( - readonly index: number, - readonly old: boolean, - private readonly random: () => number, - private readonly clock: Clock, - private readonly share?: number - ) { - this.id = `d${index}` - this.reset() - } - - reset(): void { - this.placer = new ReservePlacer(() => this.clock.now, this.random, this.share) - this.views = new Map() - this.recentlyLeft = new Map() - this.reserveEpochs = new Map() - this.rowFloors = new Map() - this.answeredCells = new Set() - this.ledgerQueue = [] - } - - complete(cells: readonly SimCell[]): boolean { - return cells.every((cell) => this.answeredCells.has(cell.cellId)) - } - - knownEpochs(host: string): number[] { - const epochs: number[] = [] - for (const view of this.views.values()) { - const seat = view.seats.get(host) - if (seat) epochs.push(seat.epoch) - } - const reserved = this.reserveEpochs.get(host) - if (reserved !== undefined) epochs.push(reserved) - const left = this.recentlyLeft.get(host) - if (left) epochs.push(left.epoch) - return epochs - } -} - -export async function runReservePlacementSimulation( - config: SimulationConfig -): Promise { - const random = seededRandom(config.seed) - const clock = new Clock() - const violations: SimulationViolation[] = [] - // Capped per invariant, so a flood of one class cannot hide another. - const violationCounts = new Map() - const violate = (invariant: number, detail: string): void => { - const count = violationCounts.get(invariant) ?? 0 - violationCounts.set(invariant, count + 1) - if (count < 50) violations.push({ invariant, at: clock.now, detail }) - } - const faults = config.faults ?? {} - const database = new SimDatabase(violate, faults) - const cells = config.cells.map( - (spec) => - new SimCell( - spec.cellId, - spec.region, - spec.hardCap, - spec.ceiling, - spec.intakePerSec, - spec.mode, - spec.old ?? false, - clock, - faults, - violate - ) - ) - const cellById = new Map(cells.map((cell) => [cell.cellId, cell])) - for (const cell of cells) if (cell.mode === 'reserve' && !cell.old) database.reserveCells.add(cell.cellId) - const inPollOutage = (at: number): boolean => - (config.pollOutages ?? []).some((outage) => at >= outage.at && at < outage.at + outage.durationMs) - const directors = Array.from( - { length: config.directors }, - (_, index) => - new SimDirector( - index, - index < (config.oldDirectors ?? 0), - random, - clock, - config.faults?.directorShare - ) - ) - const report: SimulationReport = { - violations, - requests: 0, - placements: 0, - reserveCalls: 0, - exhausted: 0, - paced: 0, - databasePlacements: 0, - reservePlacements: 0, - supersedes: 0, - directorRestarts: 0, - rowAheadDemotions: 0, - levelDemotions: 0, - contestedDivergences: 0, - rowAheadDemotionsPerHost: { max: 0, p99: 0 }, - deadManTrips: 0, - reserveCallsOnTrippedCells: 0, - databasePlacementsWhilePollsFail: 0, - pollFailureGapMs: null, - badFirstReseats: 0, - intakeRefusals: 0, - stickyReserves: 0, - stickyAnswers: 0, - seatedAtEnd: 0, - maxOccupancyShare: {}, - arrivalsByCell: {} - } - const between = ([low, high]: [number, number]) => low + random() * (high - low) - - type Host = { id: string; region: RelayRegion; current: { cellId: string; epoch: number } | null } - const hosts: Host[] = Array.from({ length: config.hosts }, (_, index) => ({ - id: `h${index}`, - region: config.hostRegion(index), - current: null - })) - const hostById = new Map(hosts.map((host) => [host.id, host])) - - // Pre-seated hosts arrive with database rows, as hosts on a cell before its flip. - let nextHost = 0 - for (const cell of cells) { - const spec = config.cells.find((entry) => entry.cellId === cell.cellId)! - for (let index = 0; index < (spec.preseated ?? 0) && nextHost < hosts.length; index += 1) { - const host = hosts[nextHost++]! - database.set(host.id, { cellId: cell.cellId, epoch: 1 }) - cell.seats.set(host.id, { epoch: 1, joinedAt: 0, demoted: false }) - cell.log.push({ kind: 'join', host: host.id, epoch: 1, joinedAt: 0 }) - host.current = { cellId: cell.cellId, epoch: 1 } - scheduleLeave(host) - } - cell.frozenDatabaseCount = cell.seats.size - } - - function scheduleLeave(host: Host): void { - if (config.meanSessionMs <= 0) return - const sessionMs = -Math.log(1 - random()) * config.meanSessionMs - clock.schedule(sessionMs, () => { - const current = host.current - if (!current || quiescing) return - const cell = cellById.get(current.cellId)! - if (cell.seats.get(host.id)?.epoch !== current.epoch) return - // Half-open controls retain seats through silence timeout after desktops move elsewhere. - if (random() >= config.halfOpenShare) cell.leave(host.id) - else { - const seat = cell.seats.get(host.id) - clock.schedule(CONTROL_SILENCE_CLOSE_MS, () => { - if (cell.seats.get(host.id) === seat) cell.leave(host.id) - }) - } - host.current = null - clock.schedule(between(config.reconnectDelayMs), () => connect(host, true)) - }) - } - - function poll(director: SimDirector, cell: SimCell): void { - if (inPollOutage(clock.now)) return - // A placing director's poll carries reserver=1: it is the cell's dead-man contact. - if (!director.old && cell.mode === 'reserve') cell.contact() - const requestedAt = clock.now - const view = director.views.get(cell.cellId) - const full = !view || view.incarnation !== cell.incarnation - const seats = full ? new Map([...cell.seats].map(([host, seat]) => [host, { ...seat }])) : null - const changes = full ? [] : cell.log.slice(view.cursor) - const snapshot = { - incarnation: cell.incarnation, - cursor: cell.log.length, - seats: cell.seats.size, - bookings: cell.book.count(), - tokens: cell.book.intakeSnapshot().tokens, - mode: faults.noRawFeedMode ? cell.effectiveMode() : cell.mode, - draining: cell.draining - } - const delay = config.rttMs(cell.region) + config.pollLagMs(director.index) - clock.schedule(delay, () => { - let current = director.views.get(cell.cellId) - if (full) { - if (current) { - for (const [host, seat] of current.seats) { - if (!seats!.has(host)) { - director.recentlyLeft.set(host, { - cellId: cell.cellId, - epoch: seat.epoch, - at: clock.now, - incarnation: current.incarnation ?? 0 - }) - } - } - } - current = { - incarnation: snapshot.incarnation, - cursor: snapshot.cursor, - seats: seats!, - placement: placementCell(cell, snapshot, requestedAt) - } - director.views.set(cell.cellId, current) - supersedeOutranked(director, seats!.keys()) - } else if (current && current.incarnation === snapshot.incarnation) { - for (const change of changes) applyChange(director, cell, current, change) - current.cursor = snapshot.cursor - current.placement = placementCell(cell, snapshot, requestedAt) - supersedeOutranked( - director, - changes.filter((change) => change.kind === 'join').map((change) => change.host) - ) - } else { - // A restarted director's (or a stale) delta: the next poll asks for a full snapshot. - return - } - director.answeredCells.add(cell.cellId) - }) - } - - function placementCell( - cell: SimCell, - snapshot: { seats: number; bookings: number; tokens: number; mode: string; draining: boolean }, - polledAt: number - ): PlacementCell { - return { - cellId: cell.cellId, - region: cell.region, - reserve: !cell.old && snapshot.mode === 'reserve', - general: true, - draining: snapshot.draining, - polledAt, - seats: snapshot.seats, - bookings: snapshot.bookings, - ceiling: cell.old ? undefined : cell.ceiling, - intakePerSec: cell.intakePerSec, - intakeTokens: snapshot.tokens - } - } - - function applyChange( - director: SimDirector, - cell: SimCell, - view: DirectorCellView, - change: CellLogEntry - ): void { - if (change.kind === 'reserve') { - director.reserveEpochs.set( - change.host, - Math.max(director.reserveEpochs.get(change.host) ?? 0, change.epoch) - ) - } else if (change.kind === 'join') { - view.seats.set(change.host, { - epoch: change.epoch, - joinedAt: change.joinedAt, - reservedBy: change.reservedBy, - demoted: false - }) - director.recentlyLeft.delete(change.host) - // Exactly one director writes each booked join: the one that booked it. - if (change.reservedBy === director.id) { - director.ledgerQueue.push({ host: change.host, cellId: cell.cellId, epoch: change.epoch }) - } - } else if (change.kind === 'leave') { - const seat = view.seats.get(change.host) - if (seat && seat.epoch === change.epoch) { - view.seats.delete(change.host) - director.recentlyLeft.set(change.host, { - cellId: cell.cellId, - epoch: change.epoch, - at: change.at, - incarnation: view.incarnation ?? 0, - wrongCell: change.wrongCell - }) - } - } else { - const seat = view.seats.get(change.host) - if (seat && seat.epoch === change.epoch) seat.demoted = true - } - } - - // Booking director supersedes old reserve seats its map held; cell verifies epoch/join. - function supersede(director: SimDirector, host: string, cellId: string, epoch: number): void { - const held = new Set() - for (const [seatCellId, view] of director.views) { - const seat = view.seats.get(host) - if (seat) held.add(`${seatCellId}\u0000${seat.joinedAt}`) - } - heldAtBooking.set(`${host}\u0000${cellId}\u0000${epoch}`, held) - if (faults.skipSupersede) return - for (const [seatCellId, view] of director.views) { - const seat = view.seats.get(host) - if (!seat || seat.demoted || seatCellId === cellId || seat.epoch >= epoch) continue - if (!view.placement.reserve) continue - report.supersedes += 1 - demoteFor(director, host, seatCellId, seat, { cellId, epoch }) - } - } - - // Model feed supersede once both joins appear: only later joins at higher epochs outrank; later lower epochs need the row. - function supersedeOutranked(director: SimDirector, hosts: Iterable): void { - if (faults.skipSupersede || faults.skipFeedSupersede) return - for (const host of new Set(hosts)) { - const seats = [...director.views].flatMap(([cellId, view]) => { - const seat = view.seats.get(host) - return seat ? [{ cellId, view, seat }] : [] - }) - if (seats.length < 2) continue - for (const { cellId, view, seat } of seats) { - if (seat.demoted || !view.placement.reserve) continue - const by = seats.find((other) => seatOutranks(other.seat, seat)) - if (!by) continue - report.supersedes += 1 - demoteFor(director, host, cellId, seat, { cellId: by.cellId, epoch: by.seat.epoch }) - } - } - } - - // The seats the booking director's map held when it placed each booking: only those it supersedes. - const heldAtBooking = new Map>() - - // Invariant 4: all directors must name the same winner per divergence or two seats can each be demoted for the other. - const namedWinners = new Map() - const contested = new Set() - const demotedFor = new Map() - type JudgedSeat = { cellId: string; epoch: number } - function judge(director: SimDirector, host: string, loserSeat: JudgedSeat, wonSeat: JudgedSeat): void { - const loser = `${loserSeat.cellId}@${loserSeat.epoch}` - const won = `${wonSeat.cellId}@${wonSeat.epoch}` - const divergence = `${host}\u0000${[loser, won].sort().join('\u0000')}` - const named = namedWinners.get(divergence) - if (named === undefined) { - namedWinners.set(divergence, { won, by: director.id }) - return - } - // A second director judged it, agreeing or not: the tie-break was exercised. - if (named.by !== director.id && loserSeat.epoch === wonSeat.epoch && !contested.has(divergence)) { - contested.add(divergence) - report.contestedDivergences += 1 - } - if (named.won !== won) { - violate(4, `${director.id} names ${won} over ${loser} for ${host}; ${named.by} named ${named.won}`) - } - } - function demoteFor( - director: SimDirector, - host: string, - cellId: string, - seat: { epoch: number; joinedAt: number }, - winner: { cellId: string; epoch: number } - ): void { - const loser = `${cellId}@${seat.epoch}` - const won = `${winner.cellId}@${winner.epoch}` - judge(director, host, { cellId, epoch: seat.epoch }, winner) - const cell = cellById.get(cellId)! - clock.schedule(config.rttMs(cell.region), () => { - const live = cell.seats.get(host) - const wasLive = live !== undefined && !live.demoted - cell.demote(host, seat.epoch, seat.joinedAt, () => onSeatClosed(host, cellId, seat.epoch)) - if (!wasLive || !live.demoted) return - // Only a live seat demoted for a live level winner is a same-epoch duplicate resolved. - const winnerSeat = cellById.get(winner.cellId)?.seats.get(host) - if (winner.epoch === seat.epoch && winnerSeat?.epoch === winner.epoch && !winnerSeat.demoted) { - report.levelDemotions += 1 - } - demotedFor.set(`${host}\u0000${loser}`, won) - if (demotedFor.get(`${host}\u0000${won}`) === loser) violate(4, `${host}: ${loser} and ${won} each demoted for the other`) - }) - } - - function onSeatClosed(hostId: string, cellId: string, epoch: number): void { - const host = hostById.get(hostId)! - if (host.current?.cellId === cellId && host.current.epoch === epoch) { - host.current = null - clock.schedule(between(config.reconnectDelayMs), () => connect(host, true)) - } - } - - function flushLedger(director: SimDirector): void { - if (!database.up || director.ledgerQueue.length === 0) return - for (const entry of director.ledgerQueue) { - database.upsert(entry.host, entry.cellId, entry.epoch) - } - director.ledgerQueue = [] - } - - // Invariant 8: reconcile removes ahead/other-cell equal-row divergences; demoting director must then mint above the row. - const behindRow = new Map() - const rowAheadDemotionsByHost = new Map() - const demotedBehindRow = new Map() - // A host demoted behind its row: its next answer, from any director, is that row or above it. - const reassignAbove = new Map() - // Only under localTieBreak: even directors keep the lower cell id of a level divergence, odd ones the higher. - function faultKeepsThisSeat( - director: SimDirector, - cellId: string, - epoch: number, - row: { cellId: string; epoch: number } - ): boolean { - return !!faults.localTieBreak && row.epoch === epoch && (cellId < row.cellId) === (director.index % 2 === 0) - } - function raiseRowFloor(director: SimDirector, host: string, epoch: number): void { - director.rowFloors.set(host, Math.max(epoch, director.rowFloors.get(host) ?? 0)) - } - function reconcileLedger(director: SimDirector): void { - if (!database.up || director.old) return - for (const [cellId, view] of director.views) { - if (!view.placement.reserve) continue - for (const [host, seat] of view.seats) { - if (seat.demoted || clock.now - seat.joinedAt < RECONCILE_SETTLE_MS) continue - const row = database.rows.get(host) - const live = cellById.get(cellId)!.seats.get(host) - const diverged = row && (row.epoch > seat.epoch || (row.epoch === seat.epoch && row.cellId !== cellId)) - // Even demotions dropped by the cell must agree on winner; relay-server raises epoch floors on every demote. - if (row && diverged && live && live.joinedAt === seat.joinedAt && live.demoted && !faults.noRowAheadDemote) { - const thisSeat = { cellId, epoch: seat.epoch } - const rowSeat = { cellId: row.cellId, epoch: row.epoch } - if (faultKeepsThisSeat(director, cellId, seat.epoch, row)) judge(director, host, rowSeat, thisSeat) - else { - judge(director, host, thisSeat, rowSeat) - raiseRowFloor(director, host, row.epoch) - } - } - if (row && diverged && live && live.joinedAt === seat.joinedAt && !live.demoted) { - const key = `${director.id}\u0000${host}\u0000${cellId}\u0000${seat.joinedAt}` - if (behindRow.has(key)) violate(8, `${host}@${seat.epoch} on ${cellId} still behind row @${row.epoch}`) - behindRow.set(key, clock.now) - if (faults.noRowAheadDemote) continue - if (faultKeepsThisSeat(director, cellId, seat.epoch, row)) { - const rowSeat = director.views.get(row.cellId)?.seats.get(host) - if (rowSeat && rowSeat.epoch === row.epoch && !rowSeat.demoted) { - demoteFor(director, host, row.cellId, rowSeat, { cellId, epoch: seat.epoch }) - } - continue - } - report.rowAheadDemotions += 1 - raiseRowFloor(director, host, row.epoch) - rowAheadDemotionsByHost.set(host, (rowAheadDemotionsByHost.get(host) ?? 0) + 1) - // A director missing a leave can re-place level once; newer rows create new divergences, but one row must never take a third demotion. - const sameRow = `${host}\u0000${row.cellId}\u0000${row.epoch}` - const behindThisRow = (demotedBehindRow.get(sameRow) ?? 0) + 1 - demotedBehindRow.set(sameRow, behindThisRow) - if (behindThisRow > 2) violate(8, `${host} demoted ${behindThisRow} times behind its row ${row.cellId}@${row.epoch}`) - reassignAbove.set(host, { ...row }) - demoteFor(director, host, cellId, seat, row) - continue - } - database.upsert(host, cellId, seat.epoch) - } - } - } - - function reserveCall( - director: SimDirector, - cell: SimCell, - host: string, - epoch: number, - sticky: boolean - ): Promise { - const rtt = config.rttMs(cell.region) - return new Promise((resolve) => { - clock.schedule(rtt / 2, () => { - const outcome = cell.old ? { outcome: 'off' as const } : cell.reserve(director.id, host, epoch, sticky) - if (outcome.outcome === 'intake') report.intakeRefusals += 1 - if (outcome.outcome === 'off' && cell.trippedAt !== null) report.reserveCallsOnTrippedCells += 1 - clock.schedule(rtt / 2, () => resolve(outcome)) - }) - }) - } - - function databaseAssign(director: SimDirector, host: Host, floor: number): Answer { - if (!database.up) return { kind: 'retry', afterMs: 2_000, calls: 0 } - const row = database.rows.get(host.id) - // Fresh current-process raw db evidence overrides stale reserve stamp, even without feed polls. - const admitsDatabase = (cell: SimCell): boolean => { - const own = database.admitEffective.get(cell.cellId) - return ( - !faults.pgReserveBlocksDatabase && - own?.mode === 'db' && - own.incarnation === cell.incarnation && - clock.now - own.at <= CELL_ADMIT_EFFECTIVE_FRESH_MS - ) - } - const eligible = cells.filter((cell) => { - if (cell.draining) return false - if (!director.old && database.reserveCells.has(cell.cellId) && !admitsDatabase(cell)) return false - const count = cell.mode === 'reserve' ? cell.frozenDatabaseCount : database.countOn(cell.cellId) - return count < cell.ceiling - }) - const load = (cell: SimCell) => - (cell.mode === 'reserve' ? cell.frozenDatabaseCount : database.countOn(cell.cellId)) / cell.ceiling - const inRegion = eligible.filter((cell) => cell.region === host.region) - const pool = inRegion.length > 0 ? inRegion : eligible - if (pool.length === 0) { - // A cell that tripped 20 s ago (a 1 s write, a 5 s cached read, slack) must be takeable. - const stranded = cells.find( - (cell) => - !director.old && - !cell.old && - !cell.draining && - cell.trippedAt !== null && - cell.mode === 'db' && - clock.now - cell.trippedAt >= 20_000 && - database.countOn(cell.cellId) < cell.ceiling - ) - if (stranded) violate(9, `${host.id}: ${stranded.cellId} tripped but takes no fresh placement`) - return { kind: 'retry', afterMs: 5_000, calls: 0 } - } - const target = pool.reduce((best, cell) => (load(cell) < load(best) ? cell : best)) - const epoch = Math.max(row?.epoch ?? 0, faults.noEpochFloor ? 0 : floor) + 1 - if (!director.old && epoch <= floor) violate(7, `db mint ${epoch} not above map floor ${floor}`) - database.assign(host.id, target.cellId, epoch) - if (target.mode === 'reserve') target.frozenDatabaseCount += 1 - report.databasePlacements += 1 - const outage = (config.pollOutages ?? [])[0] - if (inPollOutage(clock.now)) { - report.databasePlacementsWhilePollsFail += 1 - if (outage && report.pollFailureGapMs === null) report.pollFailureGapMs = clock.now - outage.at - } - return { kind: 'cell', cellId: target.cellId, epoch, calls: 0 } - } - - async function assign(director: SimDirector, host: Host, reconnect: boolean): Promise { - report.requests += 1 - if (director.old) return databaseAssign(director, host, 0) - if (!director.complete(cells)) return { kind: 'retry', afterMs: 1_000, calls: 0 } - const now = clock.now - const rowFloor = director.rowFloors.get(host.id) - const known = [...director.knownEpochs(host.id), ...(rowFloor === undefined ? [] : [rowFloor])] - let floor: number - if (known.length > 0) { - floor = Math.max(...known) - // A WRONG_CELL leave, seen in the cell's feed by every director: read the row too. - if (director.recentlyLeft.get(host.id)?.wrongCell && !faults.noWrongCellRowRead && database.up) { - floor = Math.max(floor, database.rows.get(host.id)?.epoch ?? 0) - } - } else { - if (!database.up) return { kind: 'retry', afterMs: 2_000, calls: 0 } - floor = database.rows.get(host.id)?.epoch ?? 0 - } - if (reconnect) { - const found = stickyFromMemory(director, host, now) - // The seat the reconcile demoted (or one level with its row) is never answered again. - const sticky = found && rowFloor !== undefined && found.epoch <= rowFloor ? null : found - if (sticky) { - const cell = cellById.get(sticky.cellId)! - // Unpolled restarts leave a stale incarnation in the map; hello falls back to the DB check. - const view = director.views.get(sticky.cellId) - if (sticky.incarnation === view?.incarnation || cell.old) { - report.stickyAnswers += 1 - return { kind: 'cell', cellId: sticky.cellId, epoch: sticky.epoch, calls: 0 } - } - report.stickyReserves += 1 - const outcome = await reserveCall(director, cell, host.id, sticky.epoch, true) - if (outcome.outcome === 'ok') { - report.stickyAnswers += 1 - return { kind: 'cell', cellId: sticky.cellId, epoch: sticky.epoch, calls: 1 } - } - } - } - const placements = [...director.views.values()].map((view) => view.placement) - if (random() >= director.placer.reservePoolShare(placements, host.region)) { - return databaseAssign(director, host, floor) - } - const result = await director.placer.place({ - cells: placements, - region: host.region, - epoch: faults.mintAtFloor ? Math.max(1, floor) : mintEpoch([floor]), - reserve: (cellId, epoch) => reserveCall(director, cellById.get(cellId)!, host.id, epoch, false) - }) - report.reserveCalls += result.calls - if (result.calls > RESERVE_MAX_TRIES) violate(6, `${result.calls} reserve calls in one request`) - if (result.kind === 'placed') { - report.reservePlacements += 1 - supersede(director, host.id, result.cellId, result.epoch) - return { kind: 'cell', cellId: result.cellId, epoch: result.epoch, calls: result.calls } - } - if (result.calls === RESERVE_MAX_TRIES) report.exhausted += 1 - if (result.kind === 'pace') { - report.paced += 1 - return { kind: 'retry', afterMs: result.retryAfterSeconds * 1_000, calls: result.calls } - } - const fallback = databaseAssign(director, host, floor) - return fallback.kind === 'cell' ? { ...fallback, calls: result.calls } : fallback - } - - function stickyFromMemory(director: SimDirector, host: Host, now: number) { - // The newest grant only: highest epoch, then newest join. - const seats = [...director.views] - .map(([cellId, view]) => ({ cellId, view, seat: view.seats.get(host.id) })) - .filter((entry) => entry.seat && !entry.seat.demoted) - .sort((left, right) => right.seat!.epoch - left.seat!.epoch || right.seat!.joinedAt - left.seat!.joinedAt) - const top = seats[0] - if (top && stickyCell(director, top.view, now, true)) { - return { cellId: top.cellId, epoch: top.seat!.epoch, incarnation: top.view.incarnation ?? 0 } - } - if (top) return null - const left = director.recentlyLeft.get(host.id) - // A seat closed WRONG_CELL may not rejoin from memory. - if (left && !left.wrongCell && now - left.at <= RECENT_SEAT_MS) { - const view = director.views.get(left.cellId) - if (view && stickyCell(director, view, now, false)) { - return { cellId: left.cellId, epoch: left.epoch, incarnation: left.incarnation } - } - } - return null - } - - function stickyCell(director: SimDirector, view: DirectorCellView, now: number, seated: boolean): boolean { - const cell = view.placement - if (!cell.reserve || !cell.general || cell.draining || cell.polledAt === null) return false - if (now - cell.polledAt > RESERVE_CELL_FRESH_MS) return false - return seated || director.placer.hasSeat(cell) - } - - let directorCursor = 0 - function pickDirector(): SimDirector { - directorCursor = (directorCursor + 1 + Math.floor(random() * directors.length)) % directors.length - return directors[directorCursor]! - } - - // After the run, desktops stop moving so the maps and the ledger can settle. - let quiescing = false - function connect(host: Host, reconnect: boolean): void { - if (host.current || quiescing) return - const first = pickDirector() - const duplicate = random() < config.duplicateAssignShare ? pickDirector() : null - let used = false - const onAnswer = (answer: Answer, primary: boolean): void => { - if (answer.kind === 'retry') { - if (primary && !used) { - used = true - clock.schedule(answer.afterMs + random() * 500, () => connect(host, reconnect)) - } - return - } - if (used) return - used = true - clock.schedule(50 + random() * 250, () => hello(host, answer.cellId, answer.epoch, reconnect)) - } - // A throw in the model is a broken run, recorded like any violated rule. - const broken = (error: unknown) => violate(0, `model threw: ${String(error)}`) - void assign(first, host, reconnect) - .then((answer) => onAnswer(answer, true)) - .catch(broken) - if (duplicate) { - clock.schedule(random() * 500, () => { - void assign(duplicate, host, reconnect) - .then((answer) => onAnswer(answer, false)) - .catch(broken) - }) - } - } - - function hello(host: Host, cellId: string, epoch: number, reconnect: boolean): void { - const cell = cellById.get(cellId)! - if (host.current) return - const admitted = cell.hello(host.id, epoch, database) - if (!admitted) { - clock.schedule(1_000 + random() * 2_000, () => connect(host, reconnect)) - return - } - report.placements += 1 - report.arrivalsByCell[cellId] = (report.arrivalsByCell[cellId] ?? 0) + 1 - // Report first post-demotion seats behind/level with the row; stale-map directors may not have seen the leave. - const above = reassignAbove.get(host.id) - if (above !== undefined) { - reassignAbove.delete(host.id) - if (epoch < above.epoch || (epoch === above.epoch && cellId !== above.cellId)) report.badFirstReseats += 1 - } - host.current = { cellId, epoch } - scheduleLeave(host) - } - - for (const cell of cells) { - const windowMs = deadManWindowMs(cell.cellId) - const check = (): void => { - const wasReserve = cell.mode === 'reserve' - cell.checkDeadMan(windowMs, config.reregistrationMs ?? DEFAULT_REREGISTRATION_MS) - if (wasReserve && cell.mode === 'db') report.deadManTrips += 1 - // Model best-effort cell writes only with DB up and switch in reserve; db-mode cells write no admit row. - const own = database.admitEffective.get(cell.cellId) - if ( - !cell.old && - database.reserveCells.has(cell.cellId) && - !cell.flagsUnread && - database.up && - (!own || own.mode !== cell.mode || own.incarnation !== cell.incarnation || clock.now - own.at >= CELL_ADMIT_EFFECTIVE_REFRESH_MS) - ) { - database.admitEffective.set(cell.cellId, { mode: cell.mode, incarnation: cell.incarnation, at: clock.now }) - } - clock.schedule(1_000, check) - } - clock.schedule(1_000, check) - } - for (const director of directors) { - const tick = (): void => { - for (const cell of cells) poll(director, cell) - if (director.bootReconcilePending && director.complete(cells)) { - director.bootReconcilePending = false - reconcileLedger(director) - } - clock.schedule(1_000, tick) - } - clock.schedule(random() * 1_000, tick) - const ledger = (): void => { - flushLedger(director) - clock.schedule(250, ledger) - } - clock.schedule(250, ledger) - const reconcile = (): void => { - reconcileLedger(director) - clock.schedule(10 * 60_000 + random() * 60_000, reconcile) - } - // As on a real director: once at boot when the map is complete, then every 10 minutes. - director.bootReconcilePending = true - clock.schedule(10 * 60_000 + random() * 60_000, reconcile) - } - // A leased control's renewal fails once its row names another seat: the cell closes it. - for (const cell of cells) { - const renew = (): void => { - if (database.up) { - for (const [hostId, seat] of [...cell.seats]) { - const row = database.rows.get(hostId) - if (!seat.leased || !row || (row.cellId === cell.cellId && row.epoch === seat.epoch)) continue - cell.leave(hostId) - onSeatClosed(hostId, cell.cellId, seat.epoch) - } - } - clock.schedule(CONTROL_RENEWAL_MS, renew) - } - clock.schedule(random() * CONTROL_RENEWAL_MS, renew) - } - for (const cell of cells) { - const sweep = (): void => { - cell.book.sweep() - clock.schedule(1_000, sweep) - } - clock.schedule(1_000, sweep) - } - const fresh = hosts.slice(nextHost) - fresh.forEach((host, index) => { - clock.schedule((index / config.arrivalPerSec) * 1_000, () => connect(host, false)) - }) - for (const restart of config.cellRestarts ?? []) { - clock.schedule(restart.at, () => { - const cell = cellById.get(restart.cellId)! - for (const hostId of cell.restart()) { - const host = hostById.get(hostId)! - if (host.current?.cellId !== cell.cellId) continue - host.current = null - clock.schedule(random() * 10_000, () => connect(host, true)) - } - }) - } - for (const restart of config.directorRestarts ?? []) { - clock.schedule(restart.at, () => { - const director = directors[restart.director]! - director.reset() - director.restarted = true - director.bootReconcilePending = !faults.noBootReconcile - report.directorRestarts += 1 - }) - } - for (const stall of config.databaseStalls ?? []) { - clock.schedule(stall.at, () => { - database.up = false - clock.schedule(stall.durationMs, () => { - database.up = true - }) - }) - } - for (const drain of config.drains ?? []) { - clock.schedule(drain.at, () => { - const cell = cellById.get(drain.cellId)! - cell.draining = true - const seated = [...cell.seats.keys()] - seated.forEach((hostId, index) => { - clock.schedule((index / Math.max(1, seated.length - 1)) * drain.paceMs, () => { - const host = hostById.get(hostId)! - const seat = cell.seats.get(hostId) - if (!seat) return - // Drain-only: the seat keeps its splices for the grace while the desktop moves on. - clock.schedule(DRAIN_GRACE_MS, () => { - if (cell.seats.get(hostId) === seat) cell.leave(hostId) - }) - if (host.current?.cellId === cell.cellId) { - host.current = null - clock.schedule(1_000 + random() * 5_000, () => connect(host, true)) - } - }) - }) - }) - } - - const afterEach = (): void => { - for (const cell of cells) { - if (cell.occupancy() > cell.hardCap) { - violate(1, `${cell.cellId} at ${cell.occupancy()} over hard cap ${cell.hardCap}`) - } - const share = cell.occupancy() / cell.hardCap - report.maxOccupancyShare[cell.cellId] = Math.max(report.maxOccupancyShare[cell.cellId] ?? 0, share) - } - } - // Mapped/booked or feed-outranked seats have a short supersede bound; unleased half-open duplicates missed by placement can last until silence close. - const supersedeBound = - Math.max(...directors.map((director) => config.pollLagMs(director.index))) + - 2 * Math.max(...cells.map((cell) => config.rttMs(cell.region))) + - 3_000 - const duplicateBound = CONTROL_SILENCE_CLOSE_MS + 2 * Math.max(...cells.map((cell) => config.rttMs(cell.region))) + 1_000 - // Invariant 3 bounds outranked reserve seats; database-mode duplicates use existing lease renewal. - const outrankedSince = new Map() - const checkDuplicates = (): void => { - const seatsByHost = new Map>() - for (const cell of cells) { - for (const [host, seat] of cell.seats) { - if (seat.demoted) continue - const seats = seatsByHost.get(host) ?? [] - seats.push({ cell, seat }) - seatsByHost.set(host, seats) - } - } - const live = new Set() - for (const [host, seats] of seatsByHost) { - if (seats.length < 2) continue - const newest = Math.max(...seats.map((entry) => entry.seat.epoch)) - // By the newest seats' placement records, whatever their join says: no record, no supersede. - const bookings = seats - .filter((entry) => entry.seat.epoch === newest) - .flatMap((entry) => heldAtBooking.get(`${host}\u0000${entry.cell.cellId}\u0000${newest}`) ?? []) - const supersededBy = (cellId: string, joinedAt: number): boolean => - bookings.some((held) => held.has(`${cellId}\u0000${joinedAt}`)) - for (const { cell, seat } of seats) { - if (cell.mode !== 'reserve' || seat.epoch === newest) continue - // A leased seat goes by its renewal, as today. - const tight = - !seat.leased && - (supersededBy(cell.cellId, seat.joinedAt) || seats.some((other) => seatOutranks(other.seat, seat))) - // The loose clock runs from the first outranking; the tight one from when supersede applies. - const key = `${host}\u0000${cell.cellId}\u0000${seat.joinedAt}` - const clocks: Array<[string, number]> = [[key, duplicateBound]] - if (tight) clocks.push([`${key}\u0000tight`, supersedeBound]) - for (const [clockKey, bound] of clocks) { - live.add(clockKey) - const since = outrankedSince.get(clockKey) ?? clock.now - outrankedSince.set(clockKey, since) - if (clock.now - since > bound) { - violate(3, `${host}@${seat.epoch} on ${cell.cellId} outranked for ${clock.now - since} ms`) - } - } - } - } - for (const key of [...outrankedSince.keys()]) if (!live.has(key)) outrankedSince.delete(key) - clock.schedule(1_000, checkDuplicates) - } - clock.schedule(1_000, checkDuplicates) - - await clock.runUntil(config.durationMs, afterEach) - quiescing = true - await clock.runUntil(config.durationMs + QUIESCE_MS, afterEach) - - // Invariant 2: no window holds more ok bookings than burst + rate x window. - for (const cell of cells) { - for (const times of cell.okBookingTimes) { - let lowest = Infinity - times.forEach((at, index) => { - lowest = Math.min(lowest, index - (cell.intakePerSec * at) / 1_000) - const excess = index - (cell.intakePerSec * at) / 1_000 - lowest + 1 - if (excess > CELL_INTAKE_BURST + 1e-6) { - violate(2, `${cell.cellId} booked ${excess.toFixed(2)} over its bucket`) - } - }) - } - } - // Invariant 5: guarded mirror must converge through queues/boot/ten-minute reconciles, with no final reconcile hiding lost writes. - database.up = true - for (const director of directors) flushLedger(director) - for (const cell of cells) { - if (cell.mode !== 'reserve') continue - for (const [host, seat] of cell.seats) { - if (seat.demoted) continue - const otherSeat = cells.some( - (other) => other !== cell && other.seats.get(host) && !other.seats.get(host)!.demoted - ) - if (otherSeat) continue - const row = database.rows.get(host) - if (!row || row.epoch < seat.epoch) { - violate(5, `${host} row ${JSON.stringify(row)} is below its seat ${cell.cellId}@${seat.epoch}`) - } - } - } - report.seatedAtEnd = cells.reduce((sum, cell) => sum + cell.seats.size, 0) - const perHost = [...rowAheadDemotionsByHost.values()].sort((left, right) => left - right) - report.rowAheadDemotionsPerHost = { - max: perHost.at(-1) ?? 0, - p99: perHost[Math.min(perHost.length - 1, Math.floor(perHost.length * 0.99))] ?? 0 - } - return report -} diff --git a/cloud/apps/relay/src/authentication/stale-if-error-jwks.test.ts b/cloud/apps/relay/src/authentication/stale-if-error-jwks.test.ts index c1d3b1e3ac6..c53c3fd43d5 100644 --- a/cloud/apps/relay/src/authentication/stale-if-error-jwks.test.ts +++ b/cloud/apps/relay/src/authentication/stale-if-error-jwks.test.ts @@ -2,7 +2,7 @@ import { exportJWK, generateKeyPair, SignJWT, type CryptoKey, type JWK } from 'j import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { RelayConfig } from '../config.js' import { createRelayTokenVerifier } from './relay-token-verifier.js' -import { createStaleIfErrorJWKSet, RELAY_JWKS_MAX_STALE_MS } from './stale-if-error-jwks.js' +import { createStaleIfErrorJWKSet } from './stale-if-error-jwks.js' const config = { jwksUrl: 'https://auth.example.test/.well-known/jwks.json', @@ -43,37 +43,7 @@ beforeEach(() => vi.useFakeTimers({ now: Date.parse('2026-10-11T13:00:00Z') })) afterEach(() => vi.useRealTimers()) describe('relay token keys while auth is down', () => { - it('accepts a valid token from the last served set once the cache is past its 10 min age', async () => { - const key = await signingKey('auth-1') - const authServer = auth(key) - const jwks = createStaleIfErrorJWKSet(new URL(config.jwksUrl), { fetch: authServer.fetch }) - const verify = createRelayTokenVerifier(config as RelayConfig, jwks) - expect(await verify(await relayToken(key))).not.toBeNull() - - vi.setSystemTime(Date.now() + 20 * MINUTE) - authServer.state.up = false - const fetches = authServer.fetch.mock.calls.length - expect(await verify(await relayToken(key))).toMatchObject({ sub: 'user-1' }) - expect(authServer.fetch.mock.calls.length).toBe(fetches + 1) - // Inside the retry window the stale set answers without waiting on another fetch. - expect(await verify(await relayToken(key))).not.toBeNull() - expect(authServer.fetch.mock.calls.length).toBe(fetches + 1) - }) - - it('rejects a token signed by a key the last served set never had', async () => { - const key = await signingKey('auth-1') - const authServer = auth(key) - const jwks = createStaleIfErrorJWKSet(new URL(config.jwksUrl), { fetch: authServer.fetch }) - const verify = createRelayTokenVerifier(config as RelayConfig, jwks) - expect(await verify(await relayToken(key))).not.toBeNull() - - vi.setSystemTime(Date.now() + MINUTE) - authServer.state.up = false - expect(await verify(await relayToken(await signingKey('unknown')))).toBeNull() - }) - it('bounds the stale set at 30 minutes since the last served set', async () => { - expect(RELAY_JWKS_MAX_STALE_MS).toBe(30 * MINUTE) const key = await signingKey('auth-1') const authServer = auth(key) const verify = createRelayTokenVerifier( diff --git a/cloud/apps/relay/src/cell/admission/cell-admission-startup-postgres.test.ts b/cloud/apps/relay/src/cell/admission/cell-admission-startup-postgres.test.ts deleted file mode 100644 index 4bf8a09bb7a..00000000000 --- a/cloud/apps/relay/src/cell/admission/cell-admission-startup-postgres.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import pg from 'pg' -import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' -import { RelayAssignmentStore } from '../../assignment/assignment-store.js' -import { reconcileCellAdmissionAtStartup } from './cell-admission-startup.js' -import type { RelayCellConfig } from '../../config.js' -import { openRelayDatabase, type RelayDatabase } from '../../database/database.js' - -const databaseUrl = process.env.ORCA_RELAY_TEST_POSTGRES_URL -const describePostgres = databaseUrl ? describe : describe.skip -const schema = 'relay_startup_retry_test' -const cell: RelayCellConfig = { - id: 'startup-retry-cell', - url: 'https://startup-retry.example.test', - capacityRequests: 4_000 -} - -afterEach(() => vi.restoreAllMocks()) - -describePostgres('PostgreSQL director startup reconciliation', () => { - const databases: RelayDatabase[] = [] - let scopedDatabaseUrl = '' - - beforeAll(async () => { - const client = new pg.Client({ connectionString: databaseUrl }) - await client.connect() - try { - await client.query(`DROP SCHEMA IF EXISTS ${schema} CASCADE`) - await client.query(`CREATE SCHEMA ${schema}`) - } finally { - await client.end() - } - const url = new URL(databaseUrl!) - url.searchParams.set('options', `-c search_path=${schema}`) - scopedDatabaseUrl = url.toString() - databases.push( - await openRelayDatabase({ databaseUrl: scopedDatabaseUrl, dataDir: '' }), - await openRelayDatabase({ databaseUrl: scopedDatabaseUrl, dataDir: '' }) - ) - }) - - afterAll(async () => { - for (const database of databases) await database.close() - const client = new pg.Client({ connectionString: databaseUrl }) - await client.connect() - try { - await client.query(`DROP SCHEMA IF EXISTS ${schema} CASCADE`) - } finally { - await client.end() - } - }) - - it('recovers after the cell inventory lock outlasts transaction retries', async () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) - const store = new RelayAssignmentStore(databases[0]!, () => 100) - await store.reconcileCells([cell], false) - let releaseLock: () => void = () => undefined - let reportLocked: () => void = () => undefined - const lockReleased = new Promise((resolve) => (releaseLock = resolve)) - const lockAcquired = new Promise((resolve) => (reportLocked = resolve)) - const holder = databases[1]!.transaction(async (transaction) => { - await transaction.queryLocked(`SELECT cell_id FROM relay_cells ORDER BY cell_id ASC`) - reportLocked() - await lockReleased - }) - await lockAcquired - - const releaseTimer = setTimeout(releaseLock, 3_500) - try { - await reconcileCellAdmissionAtStartup({ role: 'director', cells: [cell] }, store) - } finally { - clearTimeout(releaseTimer) - releaseLock() - await holder - } - - expect(await databases[0]!.query(`SELECT cell_id FROM relay_cells`)).toEqual([ - { cell_id: cell.id } - ]) - const warnings = warn.mock.calls.flat().join('\n') - expect(warnings).toContain('orca_relay_startup_reconcile_recovered') - expect(warnings).not.toContain('orca_relay_postgres_transaction_exhausted') - }, 10_000) -}) diff --git a/cloud/apps/relay/src/cell/admission/cell-admission-startup.test.ts b/cloud/apps/relay/src/cell/admission/cell-admission-startup.test.ts deleted file mode 100644 index a08f429cc0d..00000000000 --- a/cloud/apps/relay/src/cell/admission/cell-admission-startup.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' -import { - reconcileCellAdmissionAtStartup -} from './cell-admission-startup.js' -import type { RelayCellConfig } from '../../config.js' - -const cells: RelayCellConfig[] = [ - { - id: 'candidate', - url: 'https://candidate.relay.example.com', - capacityRequests: 4_000, - initiallyEnabled: false - } -] - -afterEach(() => { - vi.useRealTimers() - vi.restoreAllMocks() -}) - -describe('cell admission startup authority', () => { - - it('bounds retry wall time when each reconciliation is slow', async () => { - vi.useFakeTimers() - vi.setSystemTime(0) - vi.spyOn(Math, 'random').mockReturnValue(0) - vi.spyOn(console, 'warn').mockImplementation(() => undefined) - const lockUnavailable = Object.assign(new Error('lock unavailable'), { code: '55P03' }) - const reconcileCellsAtStartup = vi.fn().mockImplementation(async () => { - vi.setSystemTime(Date.now() + 10_000) - throw lockUnavailable - }) - - const startup = reconcileCellAdmissionAtStartup( - { role: 'director', cells }, - { reconcileCellsAtStartup } - ) - const rejection = expect(startup).rejects.toBe(lockUnavailable) - await vi.runAllTimersAsync() - await rejection - - expect(reconcileCellsAtStartup).toHaveBeenCalledTimes(5) - }) -}) diff --git a/cloud/apps/relay/src/cell/admission/cell-connection-cap-postgres.test.ts b/cloud/apps/relay/src/cell/admission/cell-connection-cap-postgres.test.ts deleted file mode 100644 index 0c5f505655c..00000000000 --- a/cloud/apps/relay/src/cell/admission/cell-connection-cap-postgres.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { afterAll, beforeAll, describe, expect, it } from 'vitest' -import { RelayAssignmentStore } from '../../assignment/assignment-store.js' -import type { RelayCellConfig } from '../../config.js' -import { openRelayDatabase, type RelayDatabase } from '../../database/database.js' - -const databaseUrl = process.env.ORCA_RELAY_TEST_POSTGRES_URL -const describePostgres = databaseUrl ? describe : describe.skip -const CELL_PREFIX = 'connection-cap-min-postgres' -const FLEET_SIZE = 16 - -function cellConfig(id: string, connectionHardCap: 1_000 | 3_000): RelayCellConfig { - return { - id, - url: `https://${id}.example.com`, - capacityRequests: 1_000, - connectionHardCap, - connectionUnobservedBound: 60 - } -} - -async function removeCells(database: RelayDatabase): Promise { - for (const table of [ - 'relay_cell_connection_snapshots', - 'relay_cell_connection_runtime', - 'relay_cell_connection_limits', - 'relay_cell_capabilities', - 'relay_cell_rehome_safety', - 'relay_cell_runtime', - 'relay_cell_admission', - 'relay_cell_regions', - 'relay_cells' - ]) { - await database.query(`DELETE FROM ${table} WHERE cell_id LIKE ?`, [`${CELL_PREFIX}-%`]) - } -} - -function storeOn(database: RelayDatabase): RelayAssignmentStore { - return new RelayAssignmentStore(database, () => 100, { - requireLiveCells: true, - heartbeatTtlMs: 45_000 - }) -} - -async function heartbeat( - store: RelayAssignmentStore, - cell: RelayCellConfig, - connectionHardCap: number, - units: number -): Promise { - await store.recordCellHeartbeat({ - cellId: cell.id, - cellUrl: cell.url, - cellIncarnation: '11111111-1111-4111-8111-111111111111', - startedAt: 50, - ready: true, - observedRequests: 0, - totalConnections: units, - inFlightConnections: 0, - reservedConnectionUnits: 0, - enforcedConnectionUnits: units, - connectionHardCap, - connectionUnobservedBound: 60 - }) -} - -describePostgres('PostgreSQL effective connection cap', () => { - let database: RelayDatabase - let racer: RelayDatabase - - beforeAll(async () => { - database = await openRelayDatabase({ databaseUrl, dataDir: '' }) - racer = await openRelayDatabase({ databaseUrl, dataDir: '' }) - await removeCells(database) - }) - - afterAll(async () => { - await removeCells(database) - await racer.close() - await database.close() - }) - - it('keeps 16 raised cells placeable while a heartbeat races the raise', async () => { - const ids = Array.from({ length: FLEET_SIZE }, (_, index) => `${CELL_PREFIX}-fleet-${index}`) - const lower = ids.map((id) => cellConfig(id, 1_000)) - const director = storeOn(database) - const otherInstance = storeOn(racer) - await director.reconcileCells(lower, true) - for (const cell of lower) await heartbeat(director, cell, 1_000, 10) - - // The heartbeat lands before or after the raise; either order must leave the same state. - await Promise.all([ - director.reconcileCells( - ids.map((id) => cellConfig(id, 3_000)), - true - ), - heartbeat(otherInstance, lower[0]!, 1_000, 11) - ]) - - for (const id of ids) { - await expect(director.cellDeploymentStatus(id)).resolves.toMatchObject({ - connectionCapacity: { - hardCap: 3_000, - reportedHardCap: 1_000, - effectiveHardCap: 1_000, - heartbeatFresh: true - } - }) - const rows = await database.query( - `SELECT cell_id FROM relay_cell_connection_snapshots WHERE cell_id = ?`, - [id] - ) - expect(rows).toHaveLength(1) - } - // The old processes keep heartbeating their 1,000 under the raised config. - for (const cell of lower) { - await expect(heartbeat(otherInstance, cell, 1_000, 12)).resolves.toBeUndefined() - } - }) -}) diff --git a/cloud/apps/relay/src/cell/cell-heartbeat-client.test.ts b/cloud/apps/relay/src/cell/cell-heartbeat-client.test.ts deleted file mode 100644 index 4c1072d62df..00000000000 --- a/cloud/apps/relay/src/cell/cell-heartbeat-client.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' -import type { RelayConfig } from '../config.js' -import { startCellHeartbeat } from './cell-heartbeat-client.js' - -const CONFIG = { - role: 'cell', - cellId: 'cell-a', - cellUrl: 'https://relay-a.example.com', - directorUrl: 'https://relay.example.com', - heartbeatAudience: 'https://relay.example.com/v1/admin/cell-heartbeat' -} as RelayConfig - -describe('cell heartbeat client', () => { - afterEach(() => vi.restoreAllMocks()) - - it('sends an immediate authenticated heartbeat without putting credentials in the URL', async () => { - const requests: Array<{ url: string; init?: RequestInit }> = [] - const fetchImpl = vi.fn(async (url: string | URL | Request, init?: RequestInit) => { - requests.push({ url: String(url), init }) - return new Response('{}', { status: 200 }) - }) as typeof fetch - const client = startCellHeartbeat(CONFIG, { - ready: async () => true, - observedRequests: () => 7, - connectionCounts: () => ({ - totalConnections: 0, - inFlightConnections: 0, - reservedConnectionUnits: 0, - enforcedConnectionUnits: 0 - }), - fetch: fetchImpl, - identityToken: async () => 'secret-token', - now: () => 123, - incarnation: '11111111-1111-4111-8111-111111111111', - intervalMs: 60_000 - })! - await vi.waitFor(() => expect(requests).toHaveLength(1)) - client.stop() - - expect(requests[0]!.url).toBe('https://relay.example.com/v1/admin/cell-heartbeat') - expect(requests[0]!.url).not.toContain('secret-token') - expect(requests[0]!.init?.headers).toMatchObject({ authorization: 'Bearer secret-token' }) - expect(JSON.parse(String(requests[0]!.init?.body))).toEqual({ - v: 1, - cellId: 'cell-a', - cellUrl: 'https://relay-a.example.com', - region: 'us-central1', - cellIncarnation: '11111111-1111-4111-8111-111111111111', - startedAt: 123, - ready: true, - observedRequests: 7 - }) - }) - -}) diff --git a/cloud/apps/relay/src/cell/control-flags/relay-control-flag-channel.test.ts b/cloud/apps/relay/src/cell/control-flags/relay-control-flag-channel.test.ts index b4d1673f9b1..6e6eeee42c8 100644 --- a/cloud/apps/relay/src/cell/control-flags/relay-control-flag-channel.test.ts +++ b/cloud/apps/relay/src/cell/control-flags/relay-control-flag-channel.test.ts @@ -5,7 +5,7 @@ import { cellFlagObjectName, cellFlagParser } from './cell-flags.js' -import { CONTROL_FLAG_POLL_MS, startControlFlagChannel } from './relay-control-flag-channel.js' +import { startControlFlagChannel } from './relay-control-flag-channel.js' const CELL_ID = 'production-gce-c7' const ACCESS_TOKEN = 'ya29.secret-access-token' @@ -46,7 +46,7 @@ function cellObject(generation: number, flags: Record, cellId = } } -function cellChannel(fetchImpl: typeof fetch, autoStart = false) { +function cellChannel(fetchImpl: typeof fetch) { return startControlFlagChannel({ objectName: cellFlagObjectName(CELL_ID), defaults: CELL_FLAG_DEFAULTS, @@ -54,7 +54,7 @@ function cellChannel(fetchImpl: typeof fetch, autoStart = false) { appliedEvent: CELL_FLAGS_APPLIED_EVENT, fetch: fetchImpl, random: () => 0.5, - autoStart + autoStart: false }) } @@ -230,19 +230,4 @@ describe('control flag channel', () => { expect(channel.applied()).toMatchObject({ generation: 5, flags: { readinessLocal: true } }) }) - it('reads one at a time and on a jittered 5 s cadence', async () => { - vi.useFakeTimers() - const google = fakeGoogle(cellObject(5, { readinessLocal: true })) - const channel = cellChannel(google.fetchImpl) - await Promise.all([channel.poll(), channel.poll()]) - expect(google.state.storageRequests).toHaveLength(1) - const running = cellChannel(google.fetchImpl, true) - await vi.advanceTimersByTimeAsync(0) - expect(google.state.storageRequests).toHaveLength(2) - await vi.advanceTimersByTimeAsync(CONTROL_FLAG_POLL_MS) - expect(google.state.storageRequests).toHaveLength(3) - running.stop() - await vi.advanceTimersByTimeAsync(CONTROL_FLAG_POLL_MS * 3) - expect(google.state.storageRequests).toHaveLength(3) - }) }) diff --git a/cloud/apps/relay/src/cell/reservations/cell-admit-effective-writer.test.ts b/cloud/apps/relay/src/cell/reservations/cell-admit-effective-writer.test.ts index a99895f9a29..eedf29bc139 100644 --- a/cloud/apps/relay/src/cell/reservations/cell-admit-effective-writer.test.ts +++ b/cloud/apps/relay/src/cell/reservations/cell-admit-effective-writer.test.ts @@ -4,69 +4,34 @@ import { CellAdmitEffectiveWriter, DEAD_MAN_SLOT_CLAIM_RETRY_MS } from './cell-admit-effective-writer.js' -import { DEAD_MAN_SLOT_HOLD_MS } from './cell-dead-man-trip-slot.js' function setup( - options: { fail?: boolean; busy?: boolean; busyWhen?: () => boolean; slotFree?: () => boolean } = {} + options: { slotFree?: () => boolean } = {} ) { const clock = { value: 1_000 } const writes: ('db' | 'reserve' | 'claim')[] = [] const pending: Array<() => void> = [] - const logs: string[] = [] const writer = new CellAdmitEffectiveWriter({ write: async (mode) => { writes.push(mode) await new Promise((resolve) => pending.push(resolve)) - if (options.fail) throw new Error('connection refused') }, claimTrip: async (slots) => { writes.push('claim') await new Promise((resolve) => pending.push(resolve)) return slots > 0 && (options.slotFree?.() ?? true) }, - databaseBusy: () => options.busy === true || options.busyWhen?.() === true, - now: () => clock.value, - log: (line) => logs.push(line) + databaseBusy: () => false, + now: () => clock.value }) const settle = async () => { while (pending.length > 0) pending.shift()!() await new Promise((resolve) => setTimeout(resolve, 0)) } - return { clock, writes, logs, writer, settle } + return { clock, writes, writer, settle } } describe('CellAdmitEffectiveWriter', () => { - it('writes on the first tick, on every change, and every 15 s, one write at a time', async () => { - const { clock, writes, writer, settle } = setup() - writer.tick('reserve', true) - writer.tick('db', true) - expect(writes).toEqual(['reserve']) - await settle() - // Tripped: the switch still says reserve, the raw mode db. - writer.tick('db', true) - await settle() - writer.tick('db', true) - expect(writes).toEqual(['reserve', 'db']) - clock.value += CELL_ADMIT_EFFECTIVE_REFRESH_MS - writer.tick('db', true) - expect(writes).toEqual(['reserve', 'db', 'db']) - }) - - // Step 5 off: a db switch never writes, so the dark roll adds no database write. - - // Best-effort: a failed write never throws into the cell, logs at most once a minute, and retries. - it('retries a failed write on the next tick and rate-limits its log line', async () => { - const { clock, writes, logs, writer, settle } = setup({ fail: true }) - writer.tick('db', true) - await settle() - clock.value += 1_000 - writer.tick('db', true) - await settle() - expect(writes).toEqual(['db', 'db']) - expect(logs).toHaveLength(1) - expect(JSON.parse(logs[0]!)).toMatchObject({ event: 'orca_relay_cell_admit_effective_write_failed', mode: 'db' }) - }) - it('claims a held trip slot in place of its refresh, and permits the trip only once it lands', async () => { let free = false const { clock, writes, writer, settle } = setup({ slotFree: () => free }) @@ -97,20 +62,4 @@ describe('CellAdmitEffectiveWriter', () => { expect(writes).toEqual(['reserve', 'claim', 'reserve', 'claim']) }) - it('refreshes a tripped row past a saturated pool before its slot hold runs out', async () => { - let busy = false - const { clock, writes, writer, settle } = setup({ busyWhen: () => busy }) - writer.tick('db', true) - await settle() - busy = true - clock.value += 4 * 60_000 - writer.tick('db', true) - expect(writes).toEqual(['db']) - clock.value += 60_000 - writer.tick('db', true) - await settle() - expect(writes).toEqual(['db', 'db']) - expect(DEAD_MAN_SLOT_HOLD_MS).toBeGreaterThan(5 * 60_000) - }) - }) diff --git a/cloud/apps/relay/src/cell/reservations/cell-reserve-admission.blackbox.test.ts b/cloud/apps/relay/src/cell/reservations/cell-reserve-admission.blackbox.test.ts index 19c12f7be9e..5912b51472d 100644 --- a/cloud/apps/relay/src/cell/reservations/cell-reserve-admission.blackbox.test.ts +++ b/cloud/apps/relay/src/cell/reservations/cell-reserve-admission.blackbox.test.ts @@ -8,8 +8,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import WebSocket from 'ws' import type { RawData } from 'ws' import { CELL_FLAG_DEFAULTS, type CellFlags } from '../control-flags/cell-flags.js' -import { CELL_SEAT_DRIFT_SWEEP_MS } from '../seats/cell-seat-drift.js' -import type { CellSeatLog } from '../seats/cell-seat-log.js' import type { RelayConfig } from '../../config.js' import { PostgresDatabase } from '../../database/database.js' import type { AppliedControlFlags } from '../control-flags/relay-control-flag-channel.js' @@ -73,20 +71,6 @@ function cellConfig(port: number, issuer: string): RelayConfig { } } -// The registry's private feed log, to plant the drift the cell must repair. -function seatLogOf(sessions: object): CellSeatLog { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: HostSessionRegistry holds its feed log as `seatLog`. - return (sessions as { seatLog: CellSeatLog }).seatLog -} - -// The JSON lines a test's console.warn spy saw for one event. -function warned(event: string): Array> { - return vi - .mocked(console.warn) - .mock.calls.map(([line]) => (typeof line === 'string' && line.startsWith('{') ? JSON.parse(line) : null)) - .filter((entry) => entry?.event === event) -} - function nextMessage(socket: WebSocket): Promise> { return new Promise((resolve, reject) => { socket.once('message', (data: RawData) => { @@ -265,9 +249,6 @@ describe('a reserve-mode cell admits from memory with its database wedged', () = book, setFlags: (next: Partial) => { applied = { generation: applied.generation + 1, flags: { ...CELL_FLAG_DEFAULTS, ...next } } - }, - advance: (ms: number) => { - elapsedMs += ms } } } @@ -338,97 +319,6 @@ describe('a reserve-mode cell admits from memory with its database wedged', () = return (page as { full: Array<{ relayHostId: string; epoch: number }> }).full.map((entry) => entry.epoch) } - it('repairs at once the seat a director names when no session backs it', async () => { - const cell = await startCell({ admitMode: 'reserve' }) - const identity = await cell.host() - cell.book(identity, 9) - await cell.connect(identity, 9) - // A stale seat at a generation above the live session's: its changes could never replace it. - const log = seatLogOf(cell.relay.sessions) - log.append({ kind: 'join', userId: 'user-1', relayHostId: identity.hostId, epoch: 8, generation: 5, at: 4_242 }) - const before = cell.relay.sessions.seatFeed(null) - const seq = 'full' in before ? before.seq : 0 - const seat = { v: 1 as const, userId: 'user-1', relayHostId: identity.hostId, epoch: 8, joinedAt: 4_242 } - expect(cell.relay.sessions.demote(seat)).toBe('not-seated') - expect(cell.relay.sessions.seatFeed(seq)).toMatchObject({ - changes: [ - { kind: 'leave', epoch: 8, generation: 5, closeCode: 4409 }, - { kind: 'join', epoch: 9, generation: 1 } - ] - }) - expect(seatEpochs(cell)).toEqual([9]) - expect(warned('orca_relay_cell_seat_log_drift')).toMatchObject([ - { kind: 'unbacked-seat', via: 'demote', reason: 'epoch', seatEpoch: 8, sessionEpoch: 9, relisted: true } - ]) - expect(JSON.stringify(warned('orca_relay_cell_seat_log_drift'))).not.toContain(identity.hostId) - // Asked again, there is nothing left to repair. - expect(cell.relay.sessions.demote(seat)).toBe('not-seated') - expect(cell.relay.sessions.seatFeed(seq + 2)).toMatchObject({ changes: [] }) - }) - - it('leaves a seat no session backs once two sweeps in a row find it, with no director asking', async () => { - const cell = await startCell({ admitMode: 'db' }) - const ghost = await cell.host() - seatLogOf(cell.relay.sessions).append({ - kind: 'join', - userId: 'user-1', - relayHostId: ghost.hostId, - epoch: 10, - generation: 3, - at: 1_000 - }) - expect(seatEpochs(cell)).toEqual([10]) - cell.advance(CELL_SEAT_DRIFT_SWEEP_MS) - expect(seatEpochs(cell)).toEqual([]) - expect(warned('orca_relay_cell_seat_log_drift')).toMatchObject([ - { kind: 'unbacked-seat', via: 'sweep', reason: 'no-session', seatEpoch: 10, seatGeneration: 3, relisted: false } - ]) - // Left as moved: the cell will not seat it again at 10 from memory. - expect(cell.relay.sessions.seatFeed(0)).toMatchObject({ changes: [{}, { kind: 'leave', closeCode: 4409 }] }) - }) - - it('lists again a live session the feed lost, once two sweeps in a row find it', async () => { - const cell = await startCell({ admitMode: 'reserve' }) - const identity = await cell.host() - cell.book(identity, 9) - await cell.connect(identity, 9) - seatLogOf(cell.relay.sessions).append({ - kind: 'leave', - userId: 'user-1', - relayHostId: identity.hostId, - epoch: 9, - generation: 1, - at: 5_000 - }) - expect(seatEpochs(cell)).toEqual([]) - cell.advance(CELL_SEAT_DRIFT_SWEEP_MS) - expect(seatEpochs(cell)).toEqual([9]) - expect(warned('orca_relay_cell_seat_log_drift')).toMatchObject([{ kind: 'unlisted-session', sessionEpoch: 9 }]) - }) - - // A drained session's seat stays listed, with no session, until its socket's close lands. - it('repairs nothing for a seat that one sweep finds unbacked and is gone by the next', async () => { - const cell = await startCell({ admitMode: 'reserve' }) - const identity = await cell.host() - cell.book(identity, 9) - const live = await cell.connect(identity, 9) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: ws keeps its net.Socket as `_socket`. - ;(live.socket as unknown as { _socket: { pause(): void } })._socket.pause() - cell.relay.sessions.drainHost({ - attemptId: 'attempt-1', - userId: 'user-1', - relayHostId: identity.hostId, - sourceAssignmentEpoch: 9, - graceMs: 0 - }) - await new Promise((resolve) => setTimeout(resolve, 20)) - expect(seatEpochs(cell)).toEqual([9]) - await new Promise((resolve) => setTimeout(resolve, RELAY_WEBSOCKET_FORCE_CLOSE_MS + 300)) - cell.advance(CELL_SEAT_DRIFT_SWEEP_MS) - expect(seatEpochs(cell)).toEqual([]) - expect(warned('orca_relay_cell_seat_log_drift')).toEqual([]) - }, 10_000) - // Drained sockets can close seconds after map removal; reusing their generation let late leaves erase reconnected live seats. it('keeps a session that reconnected while its drained predecessor was still closing', async () => { const cell = await startCell({ admitMode: 'reserve' }) @@ -456,19 +346,6 @@ describe('a reserve-mode cell admits from memory with its database wedged', () = expect(seatEpochs(cell)).toEqual([10]) }, 10_000) - // Repeated fresh joins for one identity replace session and seat together, leaving no drift to repair. - it('stays on the live session when the same identity connects afresh each turn', async () => { - const cell = await startCell({ admitMode: 'reserve' }) - const identity = await cell.host() - for (const epoch of [9, 10, 11]) { - cell.book(identity, epoch) - expect((await cell.connect(identity, epoch)).ack).toMatchObject({ type: 'host-hello-ack' }) - expect(seatEpochs(cell)).toEqual([epoch]) - cell.advance(CELL_SEAT_DRIFT_SWEEP_MS) - } - expect(seatEpochs(cell)).toEqual([11]) - expect(warned('orca_relay_cell_seat_log_drift')).toEqual([]) - }) }) it('gives every booking unit back when the cell starts to drain', async () => { @@ -542,44 +419,4 @@ describe('a reserve-mode cell admits from memory with its database wedged', () = }).toEqual(before) }, 30_000) - it('never crosses the hard cap under seeded bookings, hellos, rebooks and closes', async () => { - const hardCap = 6 - const cell = await startCell({ admitMode: 'reserve' }, { hardCap, controlReserve: 1 }) - const hosts = await Promise.all(Array.from({ length: 12 }, () => cell.host())) - const epochs = new Map() - const open: WebSocket[] = [] - let seed = 7 - const random = () => { - seed = (seed * 1_103_515_245 + 12_345) % 2_147_483_648 - return seed / 2_147_483_648 - } - let peak = 0 - for (let step = 0; step < 240; step += 1) { - const identity = hosts[Math.floor(random() * hosts.length)]! - const roll = random() - if (roll < 0.4) { - // A director's fresh booking, or a re-book at a newer epoch. - const epoch = (epochs.get(identity.hostId) ?? 1) + (random() < 0.5 ? 0 : 1) - if (cell.book(identity, epoch)[0]?.outcome === 'ok') epochs.set(identity.hostId, epoch) - } else if (roll < 0.8) { - const reply = await cell.connect(identity, epochs.get(identity.hostId) ?? 1) - if (reply.ack?.type === 'host-hello-ack') open.push(reply.socket!) - } else if (open.length > 0) { - const socket = open.splice(Math.floor(random() * open.length), 1)[0]! - // A newer control for the same host may already have replaced it. - if (socket.readyState !== WebSocket.CLOSED) { - const closed = new Promise((resolve) => socket.once('close', resolve)) - socket.close() - await closed - } - } - const counts = cell.relay.connectionSnapshot()! - peak = Math.max(peak, counts.physicalConnections) - expect(counts.physicalConnections).toBeLessThanOrEqual(hardCap) - expect(counts.enforcedConnectionUnits).toBeLessThanOrEqual(hardCap) - } - // The run did reach the cap, so the bound was exercised. - expect(peak).toBeGreaterThanOrEqual(hardCap - 1) - }, 60_000) }) - diff --git a/cloud/apps/relay/src/cell/reservations/cell-reserve-client.test.ts b/cloud/apps/relay/src/cell/reservations/cell-reserve-client.test.ts deleted file mode 100644 index 466f22f6de7..00000000000 --- a/cloud/apps/relay/src/cell/reservations/cell-reserve-client.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { CellReserveClient } from './cell-reserve-client.js' - -const target = { cellId: 'c1', cellUrl: 'https://c1.example' } -const item = (userId: string, epoch = 3) => ({ - userId, - relayHostId: 'abcdefghijklmnop', - epoch, - ttlMs: 30_000 -}) - -describe('cell reserve client', () => { - it('batches a cell’s bookings into one request and maps each answer back', async () => { - const fetchImpl = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { - const body = JSON.parse(String(init?.body)) as { items: Array<{ userId: string }> } - return Response.json({ - v: 1, - results: body.items.map((entry) => - entry.userId === 'b' ? { outcome: 'seated-newer', epoch: 9 } : { outcome: 'ok' } - ) - }) - }) - const client = new CellReserveClient({ - directorId: 'director-a', - identityToken: async () => 'token', - fetch: fetchImpl - }) - const answers = await Promise.all([ - client.reserve(target, item('a')), - client.reserve(target, item('b')), - client.reserve(target, item('c'), true) - ]) - expect(answers).toEqual([{ outcome: 'ok' }, { outcome: 'seated-newer', epoch: 9 }, { outcome: 'ok' }]) - // One booking batch and one dry-run batch. - expect(fetchImpl).toHaveBeenCalledTimes(2) - const first = JSON.parse(String(fetchImpl.mock.calls[0]![1]?.body)) as Record - expect(first).toMatchObject({ v: 1, directorId: 'director-a', items: [{ userId: 'a' }, { userId: 'b' }] }) - expect(String(fetchImpl.mock.calls[0]![0])).toBe('https://c1.example/internal/reserve') - }) - -}) diff --git a/cloud/apps/relay/src/cell/reservations/cell-reserve-flip-back-postgres.test.ts b/cloud/apps/relay/src/cell/reservations/cell-reserve-flip-back-postgres.test.ts deleted file mode 100644 index af96e74f0ce..00000000000 --- a/cloud/apps/relay/src/cell/reservations/cell-reserve-flip-back-postgres.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { mkdtempSync, rmSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterEach, describe, expect, it, vi } from 'vitest' -import WebSocket from 'ws' -import { openRelayDatabase, type RelayDatabase } from '../../database/database.js' -import { openDelayedPostgresDatabase, type StatementDelay } from '../../test-fixtures/delayed-postgres-database.js' -import { RESERVE_MODE_CELL_ID, startReserveModeCell } from '../../test-fixtures/reserve-mode-cell.js' - -// PostgreSQL flip-back keeps controls connected, reserves most pool capacity for DB work and folds cell deltas per tick even across cross-region round trips (E-mix). - -const databaseUrl = process.env.ORCA_RELAY_TEST_POSTGRES_URL -const describePostgres = databaseUrl ? describe : describe.skip - -async function until(check: () => Promise | boolean, timeoutMs: number): Promise { - const deadline = Date.now() + timeoutMs - while (!(await check())) { - if (Date.now() > deadline) throw new Error('condition not reached') - await new Promise((resolve) => setTimeout(resolve, 50)) - } -} - -describePostgres('flipping a reserve-mode cell back on PostgreSQL', () => { - const cleanup: Array<() => Promise | void> = [] - - afterEach(async () => { - for (const close of cleanup.splice(0).reverse()) await close() - vi.restoreAllMocks() - // The database is shared with the other PostgreSQL suites, some of which read every cell. - const database = await openRelayDatabase({ databaseUrl, dataDir: tmpdir() }) - try { - const tables = await database.query( - `SELECT table_name FROM information_schema.columns - WHERE table_schema = current_schema() AND column_name = 'cell_id' AND table_name LIKE 'relay_%'` - ) - for (const row of tables) { - await database.query(`DELETE FROM ${String(row.table_name)} WHERE cell_id = ?`, [RESERVE_MODE_CELL_ID]) - } - } finally { - await database.close() - } - }) - - async function cellOn(database: RelayDatabase, databasePoolMax = 10) { - vi.spyOn(console, 'log').mockImplementation(() => undefined) - vi.spyOn(console, 'warn').mockImplementation(() => undefined) - const dataDir = mkdtempSync(join(tmpdir(), 'orca-relay-flip-back-pg-')) - cleanup.push(() => rmSync(dataDir, { recursive: true, force: true })) - return await startReserveModeCell({ database, dataDir, cleanup, databasePoolMax }) - } - - it('re-registers every memory-admitted control, ten flips in a row', async () => { - const database = await openRelayDatabase({ databaseUrl, dataDir: tmpdir() }) - cleanup.push(() => database.close()) - for (let flip = 0; flip < 10; flip += 1) { - const cell = await cellOn(database) - const hosts = [] - for (let index = 0; index < 3; index += 1) hosts.push(await cell.bookedHost(5 + index)) - const ids = new Set(hosts.map((entry) => entry.identity.hostId)) - const leased = async () => - (await cell.controlLeases()).filter((row) => ids.has(String(row.relay_host_id))) - expect(await leased()).toEqual([]) - cell.setAdmitMode('db') - await until(async () => (await leased()).length === 3, 10_000) - await until(() => cell.relay.sessions.reregistrationPending() === 0, 10_000) - expect(hosts.every((entry) => entry.socket.readyState === WebSocket.OPEN)).toBe(true) - for (const entry of hosts) entry.socket.close() - } - }, 120_000) - - it('keeps re-registration to a third of the pool and one cell-row write per tick, at 170 ms a trip', async () => { - const delay: StatementDelay = { enabled: false, delayMs: 170 } - let cellRowWrites = 0 - let leaseWritesInFlight = 0 - let peakInFlight = 0 - delay.beforeTrip = async (sql) => { - if (/UPDATE relay_cells\b/.test(sql)) cellRowWrites += 1 - } - const database = openDelayedPostgresDatabase(databaseUrl!, delay, 10) - cleanup.push(() => database.close()) - // The schema and the cell row, without the delay. - const setup = await openRelayDatabase({ databaseUrl, dataDir: tmpdir() }) - await setup.close() - const cell = await cellOn(database, 10) - const deferred = cell.relay.assignments.activateControlDeferringCell.bind(cell.relay.assignments) - vi.spyOn(cell.relay.assignments, 'activateControlDeferringCell').mockImplementation(async (...args) => { - leaseWritesInFlight += 1 - peakInFlight = Math.max(peakInFlight, leaseWritesInFlight) - try { - return await deferred(...args) - } finally { - leaseWritesInFlight -= 1 - } - }) - const hosts = [] - for (let index = 0; index < 9; index += 1) hosts.push(await cell.bookedHost(5 + index)) - const ids = new Set(hosts.map((entry) => entry.identity.hostId)) - delay.enabled = true - const startedAt = Date.now() - cell.setAdmitMode('db') - await until(() => cell.relay.sessions.reregistrationPending() > 0, 5_000) - await until(() => cell.relay.sessions.reregistrationPending() === 0, 45_000) - const elapsedMs = Date.now() - startedAt - delay.enabled = false - const leased = (await cell.controlLeases()).filter((row) => ids.has(String(row.relay_host_id))) - expect(leased).toHaveLength(9) - expect(hosts.every((entry) => entry.socket.readyState === WebSocket.OPEN)).toBe(true) - // Pool of 10: at most 3 activations at once, leaving 7 for hellos and renewals. - expect(peakInFlight).toBeLessThanOrEqual(3) - // Far fewer cell-row writes than activations: deltas fold into one write per tick. - expect(cellRowWrites).toBeLessThan(9) - process.stderr.write(`flip_back_latency ${JSON.stringify({ elapsedMs, peakInFlight, cellRowWrites })}\n`) - }, 60_000) -}) diff --git a/cloud/apps/relay/src/cell/reservations/cell-reserve-rollback.blackbox.test.ts b/cloud/apps/relay/src/cell/reservations/cell-reserve-rollback.blackbox.test.ts index 279ea638a2a..8e2b9361acb 100644 --- a/cloud/apps/relay/src/cell/reservations/cell-reserve-rollback.blackbox.test.ts +++ b/cloud/apps/relay/src/cell/reservations/cell-reserve-rollback.blackbox.test.ts @@ -3,7 +3,6 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import WebSocket from 'ws' -import { CELL_ADMIT_EFFECTIVE_REFRESH_MS } from './cell-admit-effective-writer.js' import { RESERVE_DEAD_MAN_MS, reserveDeadManWindowMs } from './cell-reserve-dead-man.js' import { openRelayDatabase } from '../../database/database.js' import { @@ -34,7 +33,7 @@ describe('flipping a reserve-mode cell back to the database', () => { vi.restoreAllMocks() }) - async function cell(now?: () => number, options: { flagsUnread?: boolean } = {}) { + async function cell(now?: () => number) { vi.spyOn(console, 'log').mockImplementation(() => undefined) vi.spyOn(console, 'warn').mockImplementation(() => undefined) vi.spyOn(console, 'error').mockImplementation(() => undefined) @@ -42,7 +41,7 @@ describe('flipping a reserve-mode cell back to the database', () => { cleanup.push(() => rmSync(dataDir, { recursive: true, force: true })) const database = await openRelayDatabase({ dataDir }) cleanup.push(() => database.close()) - return await startReserveModeCell({ database, dataDir, cleanup, ...options, ...(now ? { now } : {}) }) + return await startReserveModeCell({ database, dataDir, cleanup, ...(now ? { now } : {}) }) } it('registers every memory-admitted control without disconnecting any, matching the seat report', async () => { @@ -159,47 +158,6 @@ describe('flipping a reserve-mode cell back to the database', () => { expect(host.socket.readyState).toBe(WebSocket.OPEN) }, 60_000) - it('takes its flip-back pace from the switch file', async () => { - const reserve = await cell() - for (let index = 0; index < 4; index += 1) await reserve.bookedHost(5 + index) - const real = reserve.relay.assignments.activateControlDeferringCell.bind(reserve.relay.assignments) - let inFlight = 0 - let peak = 0 - vi.spyOn(reserve.relay.assignments, 'activateControlDeferringCell').mockImplementation(async (...args) => { - inFlight += 1 - peak = Math.max(peak, inFlight) - await new Promise((resolve) => setTimeout(resolve, 50)) - try { - return await real(...args) - } finally { - inFlight -= 1 - } - }) - reserve.setAdmitMode('db', { reregisterInFlight: 1 }) - await until(async () => (await reserve.controlLeases()).length === 4) - expect(peak).toBe(1) - }, 30_000) - - // Step 5 off adds no writes before/after flag reads; leaving reserve owes exactly one db write. - it('writes an admit-effective row only while its switch says reserve, plus one db write on the way out', async () => { - let now = Date.now() - const booting = await cell(() => now, { flagsUnread: true }) - await new Promise((resolve) => setTimeout(resolve, 2_500)) - expect(await booting.admitEffectiveRow()).toBeUndefined() - booting.setAdmitMode('db') - now += 3 * CELL_ADMIT_EFFECTIVE_REFRESH_MS - await new Promise((resolve) => setTimeout(resolve, 2_500)) - expect(await booting.admitEffectiveRow()).toBeUndefined() - booting.setAdmitMode('reserve') - await until(async () => (await booting.admitEffectiveRow())?.mode === 'reserve') - booting.setAdmitMode('db') - await until(async () => (await booting.admitEffectiveRow())?.mode === 'db') - const last = (await booting.admitEffectiveRow())!.updated_at - now += 3 * CELL_ADMIT_EFFECTIVE_REFRESH_MS - await new Promise((resolve) => setTimeout(resolve, 2_500)) - expect((await booting.admitEffectiveRow())!.updated_at).toEqual(last) - }, 30_000) - it('flips itself back to db when no placing director has been in touch for the dead-man window', async () => { let now = Date.now() const reserve = await cell(() => now) diff --git a/cloud/apps/relay/src/cell/seats/cell-seat-feed.test.ts b/cloud/apps/relay/src/cell/seats/cell-seat-feed.test.ts deleted file mode 100644 index 28a9ced67f3..00000000000 --- a/cloud/apps/relay/src/cell/seats/cell-seat-feed.test.ts +++ /dev/null @@ -1,336 +0,0 @@ -import { EventEmitter } from 'node:events' -import { RELAY_CLOSE_CODE } from '@orca-cloud/relay-contract/close-codes' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type WebSocket from 'ws' -import { z } from 'zod' -import type { RelayAssignmentStore } from '../../assignment/assignment-store.js' -import type { CellFlags } from '../control-flags/cell-flags.js' -import type { RelayConfig } from '../../config.js' -import type { RelayCredentialStore } from '../../credentials/credential-store.js' -import { HostSessionRegistry, type HostSession } from '../../connections/host-session-registry.js' -import type { AppliedControlFlags } from '../control-flags/relay-control-flag-channel.js' -import type { RelayRuntimeObserver } from '../../observability/relay-observability.js' -import type { RelayTokenClaims } from '../../authentication/relay-token-verifier.js' -import { ProcessQueuedByteBudget } from '../../connections/splice-forwarder.js' - -const adminTokenChecks = vi.hoisted(() => ({ count: 0 })) - -vi.mock('../../authentication/admin-token-verifier.js', () => ({ - createAdminTokenVerifier: () => async (token: string) => { - adminTokenChecks.count += 1 - return token === 'deploy-token' - }, - createReadOnlyAdminTokenVerifier: () => async () => false, - createRegionalRehomeControlApplyTokenVerifier: () => async () => false, - createRegionalRehomeRuntimeTokenVerifier: () => async () => false, - createRegionalRehomeTokenVerifier: () => async (token: string) => token === 'rehome-token', - createRuntimeTokenVerifier: () => async () => false -})) - -import { createRelayApp } from '../../app.js' - -class FakeSocket extends EventEmitter { - readonly OPEN = 1 - readonly CLOSED = 3 - readyState = this.OPEN - readonly send = vi.fn() - readonly close = vi.fn((code?: number, reason?: string) => { - this.readyState = this.CLOSED - this.emit('close', code, Buffer.from(reason ?? '')) - }) -} - -const incarnation = '11111111-1111-4111-8111-111111111111' - -// The published v1 shape, so a field rename fails here before it reaches a director. -const SeatChangeSchema = z.object({ - seq: z.number(), - kind: z.enum(['join', 'leave', 'drain-only', 'active']), - userId: z.string(), - relayHostId: z.string(), - epoch: z.number(), - generation: z.number(), - state: z.enum(['active', 'drain-only']).optional(), - closeCode: z.number().optional(), - at: z.number() -}) -const SeatFeedReplySchema = z.object({ - v: z.literal(1), - cellId: z.string(), - incarnation: z.string(), - at: z.number(), - draining: z.boolean(), - counts: z.object({ - controls: z.number(), - seats: z.number() - }), - flagsApplied: z - .object({ - generation: z.number(), - flags: z.object({ - readinessLocal: z.boolean(), - ticketCheck: z.enum(['off', 'shadow', 'enforce']), - rejectionFence: z.boolean(), - admitMode: z.enum(['db', 'reserve']), - reserveDryRun: z.boolean(), - phonePath: z.enum(['db', 'memory']) - }), - ignoredKeys: z.array(z.string()).optional() - }) - .optional(), - seq: z.number(), - changes: z.array(SeatChangeSchema).optional(), - more: z.boolean().optional(), - full: z - .array( - z.object({ - userId: z.string(), - relayHostId: z.string(), - epoch: z.number(), - generation: z.number(), - state: z.enum(['active', 'drain-only']), - joinedAt: z.number() - }) - ) - .optional() -}) - -function config(overrides: Partial = {}): RelayConfig { - return { - port: 8080, - publicUrl: 'https://c7.relay.example.test', - cellUrl: 'https://c7.relay.example.test', - authIssuer: 'https://auth.example.test', - authAudience: 'orca-relay', - jwksUrl: 'https://auth.example.test/jwks', - assignmentSigningKey: new Uint8Array(32), - role: 'cell', - cellId: 'production-gce-c7', - cells: [], - adminAudience: 'https://relay.example.test/v1/admin/drain', - deployServiceAccount: 'deploy@example.test', - rehomeDirectorServiceAccount: 'relay-director@example.test', - rehomeAudience: 'https://relay.example.test/v1/admin/host-drain', - runtimeServiceAccount: 'relay-cell@example.test', - adminJwksUrl: 'https://auth.example.test/jwks', - databasePoolMax: 10, - publicAssignmentsEnabled: true, - publicAssignmentConcurrency: 2, - publicAssignmentQueueMax: 128, - publicAssignmentWaitMs: 4_000, - publicResolveConcurrency: 1, - publicResolveWaitMs: 5_000, - publicAssignmentRetryAfterSeconds: 5, - dataDir: './data', - ...overrides - } -} - -function hostIdentity(index: number, exp = 4_102_444_800): RelayTokenClaims { - return { - sub: `user-${index}`, - prof: 'profile-1', - relayHostId: `host${String(index).padStart(12, '0')}`, - purpose: 'host-control', - exp - } -} - -// What a host's auth-refresh token verifies as. -const verifyRelayToken = vi.fn<(token: string) => Promise>() - -function createCell(relayConfig = config(), cellFlags?: () => AppliedControlFlags) { - const assignments = { - activateControl: vi.fn().mockResolvedValue('control:production-gce-c7:1'), - markMigrationTargetRegistered: vi.fn().mockResolvedValue(undefined), - releaseActivity: vi.fn().mockResolvedValue(true), - renewControlActivities: vi.fn().mockResolvedValue([]) - } as unknown as RelayAssignmentStore - const observer = { - recordAuth: vi.fn(), - recordForwardedBytes: vi.fn(), - recordHttp: vi.fn(), - recordReconnect: vi.fn(), - recordSql: vi.fn() - } satisfies RelayRuntimeObserver - const registry = new HostSessionRegistry( - relayConfig, - verifyRelayToken, - {} as RelayCredentialStore, - assignments, - new ProcessQueuedByteBudget(), - observer, - Date.now, - Math.random, - incarnation - ) - const activate = ( - registry as unknown as { - activate: ( - socket: WebSocket, - identity: RelayTokenClaims, - existing: HostSession | null, - generation: number, - rebind: boolean, - assignmentEpoch: number, - appVersion: string - ) => Promise - } - ).activate.bind(registry) - const connect = async ( - index: number, - options: { generation?: number; epoch?: number; rebind?: boolean; exp?: number } = {} - ): Promise => { - const socket = new FakeSocket() - const identity = hostIdentity(index, options.exp) - await activate( - socket as unknown as WebSocket, - identity, - registry.get({ userId: identity.sub, relayHostId: identity.relayHostId }), - options.generation ?? 1, - options.rebind ?? false, - options.epoch ?? 1, - '1.4.200' - ) - return socket - } - const app = createRelayApp(relayConfig, { - store: {} as never, - assignments: {} as never, - drain: (graceMs, options) => registry.drain(graceMs, options ?? {}), - cellIncarnation: incarnation, - cellSeatFeed: (sinceSeq) => registry.seatFeed(sinceSeq), - cellFlags, - cellPhonePathEffective: () => registry.phonePathEffective(), - isDraining: () => registry.isDraining(), - runtimeCounts: () => ({ - totalConnections: 0, - preAuthConnections: 0, - queuedBytes: 0, - ...registry.runtimeCounts() - }), - ready: vi.fn(async () => true) - }) - const poll = async (since?: string, token = 'rehome-token') => - await app.request( - `/v1/admin/cell-seats${since === undefined ? '' : `?since=${encodeURIComponent(since)}`}`, - { headers: { authorization: `Bearer ${token}` } } - ) - const read = async (since?: string) => SeatFeedReplySchema.parse(await (await poll(since)).json()) - return { registry, connect, app, poll, read } -} - -describe('cell seat feed', () => { - beforeEach(() => vi.useFakeTimers()) - afterEach(() => { - vi.clearAllTimers() - vi.useRealTimers() - }) - - it('keeps a closing socket seated until its close lands, as the change log does', async () => { - const { connect, read } = createCell() - const socket = await connect(1) - // The ws close handshake: no longer OPEN, close event not yet emitted. - socket.readyState = 2 - const closing = await read() - expect(closing.counts).toMatchObject({ controls: 0, seats: 1 }) - expect(closing.full).toHaveLength(1) - socket.readyState = socket.CLOSED - socket.emit('close', 1000, Buffer.from('')) - expect((await read()).counts).toMatchObject({ controls: 0, seats: 0 }) - }) - - it('reports join, drain-only and leave as changes after the cursor', async () => { - const { registry, connect, read } = createCell() - const socket = await connect(1) - const { seq } = await read() - registry.drain(60_000) - socket.close(RELAY_CLOSE_CODE.DRAINING, 'relay draining') - await connect(2) - const body = await read(`${incarnation}:${seq}`) - expect(body.full).toBeUndefined() - expect(body.draining).toBe(true) - expect(body.changes?.map((change) => change.kind)).toEqual(['drain-only', 'leave']) - expect(body.changes?.[1]).toMatchObject({ - userId: 'user-1', - closeCode: RELAY_CLOSE_CODE.DRAINING, - generation: 1 - }) - // The second connect landed after the drain, so the cell refused it: no join. - expect(body.seq).toBe(seq + 2) - }) - - it('reports an auth expiry as drain-only and the refresh that clears it as active', async () => { - vi.spyOn(console, 'warn').mockImplementation(() => {}) - const { connect, read } = createCell() - const exp = Math.floor(Date.now() / 1000) + 5 - const socket = await connect(1, { exp }) - const { seq } = await read() - // The 15 s heartbeat finds the token expired, inside its grace. - await vi.advanceTimersByTimeAsync(15_000) - verifyRelayToken.mockResolvedValueOnce(hostIdentity(1, exp + 3_600)) - socket.emit( - 'message', - Buffer.from(JSON.stringify({ type: 'auth-refresh', relayJwt: 'refreshed' })), - false - ) - await vi.waitFor(() => expect(verifyRelayToken).toHaveBeenCalled()) - const body = await read(`${incarnation}:${seq}`) - expect(body.changes?.map((change) => change.kind)).toEqual(['drain-only', 'active']) - expect(body.counts.seats).toBe(1) - expect((await read()).full).toEqual([expect.objectContaining({ state: 'active' })]) - }) - - it('does not report a rebind as a leave, and joins the rebound socket', async () => { - const { connect, read } = createCell() - await connect(1) - const { seq } = await read() - await connect(1, { rebind: true, epoch: 2 }) - const body = await read(`${incarnation}:${seq}`) - expect(body.changes).toEqual([ - expect.objectContaining({ kind: 'join', userId: 'user-1', epoch: 2, state: 'active' }) - ]) - }) - - it('reports a superseded generation leaving after its successor joined', async () => { - const { connect, read } = createCell() - await connect(1) - const { seq } = await read() - await connect(1, { generation: 2 }) - const body = await read(`${incarnation}:${seq}`) - // Readers drop the leave: its generation is older than the seat's. - expect(body.changes?.map((change) => [change.kind, change.generation])).toEqual([ - ['leave', 1], - ['join', 2] - ]) - }) - - it('resyncs a cursor from another incarnation with a full snapshot', async () => { - const { connect, poll, read } = createCell() - await connect(1) - const body = await read('22222222-2222-4222-8222-222222222222:1') - expect(body.full).toHaveLength(1) - expect((await poll('not a cursor')).status).toBe(400) - }) - - it('accepts only the directors rehome identity, verified once per poll', async () => { - const { poll } = createCell() - adminTokenChecks.count = 0 - expect((await poll(undefined, 'deploy-token')).status).toBe(401) - expect((await poll(undefined, 'wrong')).status).toBe(401) - expect((await poll()).status).toBe(200) - // The admin middleware leaves this route to its own rehome check. - expect(adminTokenChecks.count).toBe(0) - }) - - it('is unavailable on a cell without the rehome pair, and on directors', async () => { - const withoutPair = createCell( - config({ rehomeAudience: undefined, rehomeDirectorServiceAccount: undefined }) - ) - const response = await withoutPair.poll() - expect(response.status).toBe(404) - expect(await response.json()).toEqual({ error: 'seat_feed_unavailable' }) - const director = createCell(config({ role: 'director', cellId: 'director' })) - expect((await director.poll()).status).toBe(404) - }) -}) diff --git a/cloud/apps/relay/src/cell/seats/cell-seat-log.test.ts b/cloud/apps/relay/src/cell/seats/cell-seat-log.test.ts index 4b9da440e30..c651c42fb40 100644 --- a/cloud/apps/relay/src/cell/seats/cell-seat-log.test.ts +++ b/cloud/apps/relay/src/cell/seats/cell-seat-log.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { CELL_SEAT_FEED_PAGE_MAX, CellSeatLog } from './cell-seat-log.js' +import { CellSeatLog } from './cell-seat-log.js' function append(log: CellSeatLog, count: number): void { for (let index = 0; index < count; index += 1) { @@ -24,16 +24,6 @@ describe('cell seat log', () => { expect(log.read(5)).toMatchObject({ seq: 10, full: expect.any(Array) }) }) - it('pages a long backlog and resumes from the returned cursor', () => { - const log = new CellSeatLog(CELL_SEAT_FEED_PAGE_MAX * 2) - append(log, CELL_SEAT_FEED_PAGE_MAX + 5) - const first = log.read(0) - expect(first).toMatchObject({ seq: CELL_SEAT_FEED_PAGE_MAX, more: true }) - expect('changes' in first && first.changes).toHaveLength(CELL_SEAT_FEED_PAGE_MAX) - const second = log.read(first.seq) - expect(second).toMatchObject({ seq: CELL_SEAT_FEED_PAGE_MAX + 5, more: false }) - expect('changes' in second && second.changes).toHaveLength(5) - }) }) describe('cell seat log snapshot', () => { @@ -85,20 +75,5 @@ describe('cell seat cursor', () => { expect(log.recentlyLeftOf(host.userId, host.relayHostId)).toBeUndefined() }) - it('sends only the newest page of leavers in a full snapshot', () => { - const log = new CellSeatLog(CELL_SEAT_FEED_PAGE_MAX * 4, () => 0) - const leavers = CELL_SEAT_FEED_PAGE_MAX + 5 - for (let index = 0; index < leavers; index += 1) { - const host = { userId: `user-${index}`, relayHostId: 'abcdefghijklmnop', epoch: 1, generation: 1 } - log.append({ kind: 'join', ...host, at: 0 }) - log.append({ kind: 'leave', ...host, closeCode: 1006, at: 0 }) - } - const page = log.read(null) - const left = 'full' in page ? page.recentlyLeft : [] - expect(left).toHaveLength(CELL_SEAT_FEED_PAGE_MAX) - expect(left.at(-1)?.userId).toBe(`user-${leavers - 1}`) - // The cell itself still remembers the oldest. - expect(log.recentlyLeftOf('user-0', 'abcdefghijklmnop')).toBeDefined() - }) }) diff --git a/cloud/apps/relay/src/cell/seats/shadow-directory-compare.test.ts b/cloud/apps/relay/src/cell/seats/shadow-directory-compare.test.ts deleted file mode 100644 index b64dc003f3b..00000000000 --- a/cloud/apps/relay/src/cell/seats/shadow-directory-compare.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import type { RelayAssignment } from '../../assignment/assignment-store.js' -import type { RelayConfig } from '../../config.js' -import { - ShadowDirectoryCompare -} from './shadow-directory-compare.js' -import { ShadowSeatDirectory, type SeatFeedResponse } from './shadow-seat-directory.js' - -const fakes = vi.hoisted(() => ({ - verifyRelayToken: vi.fn(async (token: string) => ({ sub: 'user-a', relayHostId: token })) -})) - -vi.mock('../../authentication/relay-token-verifier.js', () => ({ - createRelayTokenVerifier: () => fakes.verifyRelayToken, - readBearer: (value: string | undefined) => value?.replace(/^Bearer /, '') ?? null -})) - -import { createRelayApp } from '../../app.js' - -const HOST = 'hhhhhhhhhhhhhhhh' -const IDENTITY = { userId: 'user-a', relayHostId: HOST } - -function feed(cellId: string, overrides: Partial = {}): SeatFeedResponse { - return { v: 1, cellId, incarnation: `inc-${cellId}`, seq: 1, at: 1, full: [], ...overrides } -} - -function seat(epoch: number, state = 'active', relayHostId = HOST, joinedAt = 1) { - return { userId: 'user-a', relayHostId, epoch, generation: 1, state, joinedAt } -} - -function directoryWith( - seats: Record[]>, - noFeed: string[] = [], - unlive: string[] = [] -) { - const directory = new ShadowSeatDirectory() - const cellIds = [...Object.keys(seats), ...noFeed] - directory.setCells(cellIds, cellIds, { - readAt: 0, - expiresAt: new Map( - cellIds.filter((cellId) => !unlive.includes(cellId)).map((cellId) => [cellId, 1e12]) - ) - }) - for (const [cellId, full] of Object.entries(seats)) { - directory.apply(cellId, feed(cellId, { full }), 10) - } - for (const cellId of noFeed) directory.markNoFeed(cellId, 10) - return directory -} - -describe('unexplained samples', () => { - it('log only the cell and epoch of the real database record, never raw ids', () => { - const lines: Record[] = [] - const compare = new ShadowDirectoryCompare( - directoryWith({ 'cell-a': [seat(3)] }), - () => 20, - (line) => lines.push(JSON.parse(line)) - ) - compare.compare('sticky-verify', IDENTITY, { ...assignment('cell-a'), assignmentEpoch: 2 }) - compare.flush(20) - const sample = lines.find((line) => line.event === 'orca_relay_shadow_compare_unexplained') - expect(sample).toMatchObject({ - class: 'epoch-mismatch', - db: { cellId: 'cell-a', assignmentEpoch: 2 } - }) - expect(JSON.stringify(lines)).not.toContain(HOST) - expect(JSON.stringify(lines)).not.toContain('user-a') - }) -}) - -describe('compare hooks on the director routes', () => { - async function respond(compareShadowSeats?: () => void) { - const resolved = assignment('cell-a') - const app = createRelayApp(config(), { - // SAFETY: /v1/resolve reads only resolveResume. - store: { resolveResume: vi.fn(async () => ({ userId: 'user-a' })) } as never, - // SAFETY: both routes read only resolve and assign. - assignments: { - resolve: vi.fn(async () => resolved), - assign: vi.fn(async () => resolved) - } as never, - drain: vi.fn(), - ready: vi.fn(async () => true), - compareShadowSeats - }) - const sticky = await app.request('/v1/assign', { - method: 'POST', - headers: { authorization: `Bearer ${HOST}`, 'content-type': 'application/json' }, - body: JSON.stringify({ v: 1, relayHostId: HOST, reconnect: true }) - }) - const resolve = await app.request('/v1/resolve', { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ - v: 1, - relayHostId: HOST, - resumeToken: Buffer.alloc(32, 1).toString('base64url') - }) - }) - return [ - { status: sticky.status, body: await sticky.text() }, - { status: resolve.status, body: await resolve.text() } - ] - } - - it('compares after each database answer without changing a response byte', async () => { - vi.useFakeTimers({ now: 1_000_000 }) - try { - const baseline = await respond() - const routes: unknown[] = [] - const compared = await respond((...args: unknown[]) => { - routes.push(args[0]) - }) - const throwing = await respond(() => { - throw new Error('compare broke') - }) - - expect(baseline[0]?.status).toBe(200) - expect(baseline[1]?.status).toBe(200) - expect(compared).toEqual(baseline) - expect(throwing).toEqual(baseline) - expect(routes).toEqual(['sticky-verify', 'resolve']) - } finally { - vi.useRealTimers() - } - }) -}) - -function assignment(cellId: string): RelayAssignment { - return { - userId: 'user-a', - relayHostId: HOST, - cellId, - cellUrl: `https://${cellId}.relay.example.test`, - assignmentEpoch: 3, - leaseExpiresAt: 1_300_000 - } -} - -function config(): RelayConfig { - return { - port: 8080, - publicUrl: 'https://relay.example.test', - cellUrl: 'https://relay.example.test', - authIssuer: 'https://auth.example.test', - authAudience: 'orca-relay', - jwksUrl: 'https://auth.example.test/jwks', - assignmentSigningKey: new TextEncoder().encode('assignment-key-with-at-least-32-bytes'), - role: 'director', - cellId: 'director', - cells: [], - adminAudience: 'https://relay.example.test/v1/admin/drain', - deployServiceAccount: 'deploy@example.test', - runtimeServiceAccount: 'runtime@example.test', - adminJwksUrl: 'https://auth.example.test/jwks', - databasePoolMax: 3, - publicAssignmentsEnabled: true, - publicAssignmentConcurrency: 2, - publicAssignmentQueueMax: 128, - publicAssignmentWaitMs: 4_000, - publicResolveConcurrency: 1, - publicResolveWaitMs: 5_000, - publicAssignmentRetryAfterSeconds: 5, - dataDir: './data' - } -} diff --git a/cloud/apps/relay/src/cell/seats/shadow-seat-directory.test.ts b/cloud/apps/relay/src/cell/seats/shadow-seat-directory.test.ts index a3120cb7782..373a119003e 100644 --- a/cloud/apps/relay/src/cell/seats/shadow-seat-directory.test.ts +++ b/cloud/apps/relay/src/cell/seats/shadow-seat-directory.test.ts @@ -3,7 +3,6 @@ import { RelayAssignmentStore } from '../../assignment/assignment-store.js' import { openInMemoryRelayDatabase } from '../../database/database.js' import { SHADOW_SEAT_OUTRANK_SKEW_MS, - SHADOW_SEAT_RECENTLY_LEFT_MAX_HOSTS, ShadowSeatDirectory, startShadowSeatPoller, type SeatFeedCell, @@ -141,7 +140,6 @@ describe('startShadowSeatPoller', () => { { cellId: 'cell-a', epoch: 3, generation: 1, state: 'active' } ]) expect(cell.directory.cellState('cell-a')).toMatchObject({ status: 'live', seq: 5 }) - expect(cell.identityToken).toHaveBeenCalledTimes(1) }) it('resyncs from a full snapshot when the cell restarts with a new incarnation', async () => { @@ -284,7 +282,6 @@ describe('startShadowSeatPoller', () => { await cell.tick() await cell.tick() cell.stop() - expect(call).toBe(3) expect(cell.requests[2]?.searchParams.get('since')).toBe(`${INCARNATION}:1`) expect(cell.directory.seatsOf('user-a', 'host-2')).toHaveLength(1) }) @@ -336,31 +333,6 @@ describe('startShadowSeatPoller', () => { expect(cell.directory.isComplete()).toBe(true) }) - it('forgets the switches a cell reported once its image no longer has the feed', async () => { - let rolledBack = false - const cell = poller({ - 'cell-a': () => json(feed({ seq: 1, full: [] })), - 'cell-b': () => - rolledBack - ? new Response('Not Found', { status: 404 }) - : json( - feed({ - cellId: 'cell-b', - seq: 1, - full: [], - flagsApplied: { generation: 3, flags: { readinessLocal: true } } - }) - ) - }) - await cell.tick() - expect(cell.directory.cellState('cell-b')?.flagsApplied?.generation).toBe(3) - rolledBack = true - await cell.tick() - cell.stop() - expect(cell.directory.cellState('cell-b')).toMatchObject({ status: 'no-feed' }) - expect(cell.directory.cellState('cell-b')?.flagsApplied).toBeUndefined() - }) - it('keeps a reserve switch through a 404 until a feed says db or the process is replaced', () => { const directory = new ShadowSeatDirectory() directory.setCells(['cell-b']) @@ -390,47 +362,6 @@ describe('startShadowSeatPoller', () => { expect(cell.directory.isComplete()).toBe(true) }) - it('backs off cell-list reads while the database fails', async () => { - const listCells = vi.fn(async (): Promise => { - throw new Error('pool timeout') - }) - const cell = poller({}, 'all', listCells) - for (let second = 0; second < 40; second += 1) { - await cell.tick() - cell.advance(1_000) - } - cell.stop() - // Retries at +1, +2, +4, +8, +16 s, then at most every 30 s. - expect(listCells.mock.calls.length).toBeLessThanOrEqual(7) - expect(listCells.mock.calls.length).toBeGreaterThanOrEqual(5) - }) - - it('keeps polling the last cell list while a cell-list read hangs', async () => { - const readEntered = Promise.withResolvers() - const readDone = Promise.withResolvers() - const listCells = vi - .fn<() => Promise>() - .mockResolvedValueOnce(CELLS) - .mockImplementation(() => { - readEntered.resolve() - return readDone.promise - }) - const cell = poller({ 'cell-a': () => json(feed({ seq: 1, full: [] })) }, ['cell-a'], listCells) - await cell.tick() - cell.advance(31_000) - const round = cell.tick() - await readEntered.promise - await Promise.resolve() - expect(listCells).toHaveBeenCalledTimes(2) - expect(cell.requests.map((url) => url.hostname)).toEqual([ - 'cell-a.example.test', - 'cell-a.example.test' - ]) - readDone.resolve(CELLS) - await round - cell.stop() - }) - it('treats a malformed or mislabelled body as unverifiable', async () => { const cell = poller({ 'cell-a': () => json({ v: 2 }), @@ -483,20 +414,6 @@ describe('startShadowSeatPoller', () => { expect(cell.directory.seatsOf('user-a', 'host-1')).toEqual([]) }) - it('bounds the recently-left memory', () => { - const directory = new ShadowSeatDirectory() - directory.setCells(['cell-a']) - const hosts = SHADOW_SEAT_RECENTLY_LEFT_MAX_HOSTS + 10 - directory.apply( - 'cell-a', - feed({ seq: 1, full: Array.from({ length: hosts }, (_, index) => seat(`host-${index}`)) }), - 10 - ) - directory.apply('cell-a', feed({ incarnation: 'restarted', seq: 1, full: [] }), 20) - expect(directory.summary(20).recentlyLeftHosts).toBe(SHADOW_SEAT_RECENTLY_LEFT_MAX_HOSTS) - expect(directory.recentlyLeftOf('user-a', 'host-0', 20)).toEqual([]) - expect(directory.recentlyLeftOf('user-a', `host-${hosts - 1}`, 20)).toHaveLength(1) - }) }) describe('RelayAssignmentStore.seatFeedCells', () => { diff --git a/cloud/apps/relay/src/connections/cell-phone-path.blackbox.test.ts b/cloud/apps/relay/src/connections/cell-phone-path.blackbox.test.ts index 4e59dede310..822c12000a3 100644 --- a/cloud/apps/relay/src/connections/cell-phone-path.blackbox.test.ts +++ b/cloud/apps/relay/src/connections/cell-phone-path.blackbox.test.ts @@ -22,11 +22,6 @@ const OTHER_CELL = { capacityRequests: 900 } -const RESUME_DEVICE_READ = expect.stringMatching(/^SELECT .* FROM relay_devices\b/) -const HOST_ASSIGNMENT_READ = expect.stringMatching( - /^SELECT assignment\.\*.* FROM relay_assignments\b/ -) - async function until(check: () => Promise, timeoutMs = 10_000): Promise { const deadline = Date.now() + timeoutMs while (!(await check())) { @@ -58,11 +53,7 @@ describe('the memory phone path', () => { cleanup, ...(now ? { now } : {}) }) - // The admit-effective writer ticks on its own; every other statement is the phone's. - const phoneStatements = () => - database.statements.filter((sql) => !sql.includes('relay_cell_admit_effective')) - // The test's own reads and writes go around the recorder. - return { ...reserve, database, raw: inner, phoneStatements } + return { ...reserve, database, raw: inner } } async function seatedPhoneHost(phonePath: 'db' | 'memory') { @@ -70,7 +61,6 @@ describe('the memory phone path', () => { reserve.setAdmitMode('reserve', { phonePath }) const seated = await reserve.bookedHost(3) await reserve.installDevice(seated.identity.hostId, TOKEN) - reserve.database.statements.length = 0 return { reserve, seated } } @@ -90,7 +80,6 @@ describe('the memory phone path', () => { const basisConnId = String((await connOpen).connId) await reserve.attachData(await connOpen, seated.ack.generation) expect(await phone.hello).toMatchObject({ ok: true }) - reserve.database.statements.length = 0 // The desktop's confirm, sent in reply to the phone over the end-to-end channel. const confirm = async (reqId: string, basis = basisConnId) => { const answer = Promise.race([ @@ -108,26 +97,17 @@ describe('the memory phone path', () => { const deviceRow = async (reserve: Awaited>) => (await reserve.raw.query(`SELECT current_expires_at FROM relay_devices`))[0] - const warnings = (event: string) => - vi - .mocked(console.warn) - .mock.calls.map(([line]) => String(line)) - .filter((line) => line.includes(event)) - .map((line) => JSON.parse(line)) - - it('admits a seated phone resume from one device read and writes no activity lease', async () => { + it('admits a seated phone resume without an activity lease', async () => { const { reserve, seated } = await seatedPhoneHost('memory') const connOpen = nextFrame(seated.socket, 'conn-open') const phone = await reserve.dialPhone(seated.identity.hostId, TOKEN) expect(await connOpen).toMatchObject({ kind: 'resume', relayDeviceId: 'device-1' }) - expect(reserve.phoneStatements()).toEqual([RESUME_DEVICE_READ]) await reserve.attachData(await connOpen, seated.ack.generation) expect(await phone.hello).toMatchObject({ type: 'relay-hello', ok: true, credentialKind: 'resume' }) - expect(reserve.phoneStatements()).toEqual([RESUME_DEVICE_READ]) expect(await reserve.activityLeases()).toEqual([]) }) @@ -155,7 +135,7 @@ describe('the memory phone path', () => { ]) }) - it('closes a revoked or expired device 4401 and never tries it as an invite', async () => { + it('closes a revoked or expired device 4401', async () => { const reserve = await cell() reserve.setAdmitMode('reserve', { phonePath: 'memory' }) const seated = await reserve.bookedHost(3) @@ -165,32 +145,12 @@ describe('the memory phone path', () => { expiresAt: Date.now() - 1_000 }) for (const refused of [token(2), token(3)]) { - reserve.database.statements.length = 0 const phone = await reserve.dialPhone(seated.identity.hostId, refused) expect(await phone.hello).toEqual({ type: 'relay-hello', ok: false, code: 4401 }) expect((await phone.closed).code).toBe(4401) - expect(reserve.phoneStatements()).toEqual([RESUME_DEVICE_READ]) } }) - it('closes 4503 when the device read fails, without trying the invite path', async () => { - const { reserve, seated } = await seatedPhoneHost('memory') - vi.spyOn(reserve.relay.store, 'lookupResume').mockRejectedValue( - new Error('connection terminated') - ) - const invite = vi.spyOn(reserve.relay.store, 'resolveInviteForMove') - for (let dial = 0; dial < 2; dial += 1) { - const phone = await reserve.dialPhone(seated.identity.hostId, TOKEN) - expect((await phone.closed).code).toBe(4503) - } - expect(invite).not.toHaveBeenCalled() - expect(reserve.phoneStatements()).toEqual([]) - // One line opens the window; the repeat waits for its closing line. - expect(warnings('orca_relay_client_accept_db_error')).toEqual([ - { event: 'orca_relay_client_accept_db_error', cause: 'error', cellId: reserve.config.cellId } - ]) - }) - it('falls through to the invite path only when no device row matches', async () => { const { reserve, seated } = await seatedPhoneHost('memory') const created = nextFrame(seated.socket, 'invite-created') @@ -208,7 +168,7 @@ describe('the memory phone path', () => { ]) }) - it('answers a phone whose host has no seat here from one assignment read', async () => { + it('distinguishes moved and offline hosts when a phone finds no seat here', async () => { const reserve = await cell() reserve.setAdmitMode('reserve', { phonePath: 'memory' }) await reserve.relay.assignments.reconcileCells([...reserve.config.cells, OTHER_CELL]) @@ -222,31 +182,19 @@ describe('the memory phone path', () => { ) } await reserve.installDevice(host.hostId, TOKEN) - reserve.database.statements.length = 0 const phone = await reserve.dialPhone(host.hostId, TOKEN) const closed = await phone.closed - expect(reserve.phoneStatements()).toEqual([RESUME_DEVICE_READ, HOST_ASSIGNMENT_READ]) return closed.code } expect(await dial('elsewhere')).toBe(4409) expect(await dial('none')).toBe(4404) - expect(warnings('orca_relay_client_miss_row_names_here')).toEqual([]) expect(await dial('here')).toBe(4404) - expect(warnings('orca_relay_client_miss_row_names_here')).toEqual([ - expect.objectContaining({ - cellId: reserve.config.cellId, - relayHostIdDigest: expect.any(String) - }) - ]) vi.spyOn(reserve.relay.assignments, 'resolve').mockRejectedValueOnce( new Error('connection terminated') ) const host = await reserve.host() await reserve.installDevice(host.hostId, TOKEN) expect((await (await reserve.dialPhone(host.hostId, TOKEN)).closed).code).toBe(4409) - expect(warnings('orca_relay_client_miss_read_error')).toEqual([ - { event: 'orca_relay_client_miss_read_error', cause: 'error', cellId: reserve.config.cellId } - ]) }) it('takes the database path for a phone whose switch turned off during its device read', async () => { @@ -265,7 +213,7 @@ describe('the memory phone path', () => { ]) }) - it('confirms from the memory basis with one fresh read, renewing a device due for it', async () => { + it('confirms from the memory basis and renews a device due for it', async () => { const { reserve, confirm } = await splicedPhone('memory') const before = Number((await deviceRow(reserve))?.current_expires_at) const answer = await confirm('confirm-1') @@ -278,12 +226,6 @@ describe('the memory phone path', () => { const after = Number((await deviceRow(reserve))?.current_expires_at) expect(after).toBeGreaterThan(before) expect(answer.resumeExpiresAt).toBe(after) - expect(reserve.phoneStatements()).toEqual([ - RESUME_DEVICE_READ, - expect.stringMatching( - /^UPDATE relay_devices SET current_expires_at = \?.*RETURNING current_expires_at$/ - ) - ]) expect(await reserve.activityLeases()).toEqual([]) // Its audit row leaves with the next batch. await reserve.relay.store.resumeAudit.flush() @@ -293,10 +235,8 @@ describe('the memory phone path', () => { expect(audits.map((row) => JSON.parse(String(row.detail_json)))).toEqual([ { reqId: 'confirm-1', basisConnId: expect.any(String), renewed: true } ]) - // A retry of the same request answers from memory; another basis cannot reuse its reqId. - reserve.database.statements.length = 0 + // A retry keeps its answer; another basis cannot reuse its reqId. expect(await confirm('confirm-1')).toEqual(answer) - expect(reserve.phoneStatements()).toEqual([]) expect(await confirm('confirm-1', 'another-basis')).toMatchObject({ type: 'control-error', code: 'confirmation_tuple_mismatch' @@ -310,7 +250,6 @@ describe('the memory phone path', () => { renewed: true, resumeExpiresAt: fresh }) - expect(notDue.reserve.phoneStatements()).toEqual([RESUME_DEVICE_READ]) const due = Date.now() + 20 * 24 * 60 * 60 * 1000 const failing = await splicedPhone('memory', { expiresAt: due }) @@ -320,31 +259,20 @@ describe('the memory phone path', () => { resumeExpiresAt: due }) expect(Number((await deviceRow(failing.reserve))?.current_expires_at)).toBe(due) - // Never silent: a renewal left failing ages phones toward re-pairing. - expect(warnings('orca_relay_resume_renewal_failed')).toEqual([ - { - event: 'orca_relay_resume_renewal_failed', - cause: 'error', - cellId: failing.reserve.config.cellId - } - ]) }) it('replays a stored answer after its splice closed, and drops the basis itself', async () => { - const { reserve, phone, confirm } = await splicedPhone('memory') + const { phone, confirm } = await splicedPhone('memory') const answer = await confirm('confirm-1') phone.socket.close() await phone.closed await new Promise((resolve) => setTimeout(resolve, 50)) - reserve.database.statements.length = 0 expect(await confirm('confirm-1')).toEqual(answer) expect(await confirm('confirm-1', 'another-basis')).toMatchObject({ code: 'confirmation_tuple_mismatch' }) - expect(reserve.phoneStatements()).toEqual([]) // A new request on the closed basis finds none in memory, so it goes to the database. expect(await confirm('confirm-2')).toMatchObject({ code: 'confirmation_not_active' }) - expect(reserve.phoneStatements()[0]).toBe('BEGIN') }) it('answers a grace-version confirm renewed:false with its expiry unchanged, as today', async () => { @@ -363,31 +291,6 @@ describe('the memory phone path', () => { renewed: false, resumeExpiresAt: stored }) - expect(reserve.phoneStatements()).toEqual([RESUME_DEVICE_READ]) - }) - - it('answers every device change since the accept read as the database confirm does', async () => { - const changes: Record = { - revoked: ['UPDATE relay_devices SET revoked_at = ?', [Date.now() - 1]], - expired: ['UPDATE relay_devices SET current_expires_at = ?', [Date.now() - 1]], - retired: [ - "UPDATE relay_devices SET current_hash = 'rotated-twice', current_version = 3, grace_version = 2", - [] - ], - unchanged: ['UPDATE relay_devices SET updated_at = updated_at', []] - } - const outcome = async (phonePath: 'db' | 'memory', [sql, params]: [string, unknown[]]) => { - const { reserve, confirm } = await splicedPhone(phonePath) - await reserve.raw.query(sql, params) - const answer = await confirm('confirm-1') - return answer.type === 'control-error' - ? answer.code - : `${answer.type}:${answer.acceptedAs}:${answer.renewed}` - } - for (const [name, change] of Object.entries(changes)) { - expect(await outcome('memory', change), name).toBe(await outcome('db', change)) - } - expect(await outcome('memory', changes.revoked!)).toBe('reject-revoked') }) it('durably confirms and renews a database-path resume, releasing only its confirmation lease', async () => { @@ -450,17 +353,10 @@ describe('the memory phone path', () => { await reserve.relay.assignments.setCellAdmitMode(reserve.config.cellId, 'reserve') rollback.roll(reserve, advance) expect(reserve.relay.sessions.phonePathEffective()).toBe('db') - reserve.database.statements.length = 0 expect(await spliced.confirm('confirm-1')).toMatchObject({ type: 'device-resume-confirmed', renewed: true }) - expect( - reserve.phoneStatements().filter((sql) => !sql.includes('relay_assignment')) - ).toEqual([ - RESUME_DEVICE_READ, - expect.stringMatching(/^UPDATE relay_devices SET current_expires_at/) - ]) // Only the control lease a flip back re-registers; nothing for the phone. const leases = async () => (await reserve.activityLeases()).map((lease) => lease.activity_kind) @@ -474,14 +370,9 @@ describe('the memory phone path', () => { ).rejects.toThrow('cell_in_reserve_mode') // The splice still runs, and closing it releases nothing it never took. expect(spliced.phone.socket.readyState).toBe(spliced.phone.socket.OPEN) - // Let a flip back finish leasing its control (and fold its relay_cells write) first. await until(async () => reserve.relay.sessions.reregistrationPending() === 0) - await new Promise((resolve) => setTimeout(resolve, 250)) - reserve.database.statements.length = 0 spliced.phone.socket.close() await spliced.phone.closed - await new Promise((resolve) => setTimeout(resolve, 50)) - expect(reserve.phoneStatements()).toEqual([]) expect((await leases()).filter((kind) => kind !== 'control')).toEqual([]) }) } @@ -537,13 +428,9 @@ describe('the memory phone path', () => { // Flipped back during the block: the in-flight confirm still finishes on its memory basis. reserve.setAdmitMode('db', { phonePath: 'memory' }) reserve.database.failing = null - reserve.database.statements.length = 0 expect(await confirm('confirm-c', inFlight.basisConnId)).toMatchObject({ type: 'device-resume-confirmed' }) - expect(reserve.phoneStatements().find((sql) => !sql.includes('relay_assignment'))).toEqual( - RESUME_DEVICE_READ - ) // And new resumes take the database path again. const connOpen = nextFrame(seated.socket, 'conn-open') await reserve.dialPhone(seated.identity.hostId, token(1)) diff --git a/cloud/apps/relay/src/connections/host-close-reason-memory.test.ts b/cloud/apps/relay/src/connections/host-close-reason-memory.test.ts deleted file mode 100644 index 8a81f0634de..00000000000 --- a/cloud/apps/relay/src/connections/host-close-reason-memory.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { ASSIGNMENT_LIMITS } from '@orca-cloud/relay-contract/assignment-invariants' -import { RELAY_HOST_CLOSE_REASON } from '@orca-cloud/relay-contract/host-close-reason' -import { describe, expect, it } from 'vitest' -import { HostCloseReasonMemory } from './host-close-reason-memory.js' - -function memoryAt(clock: { now: number }): HostCloseReasonMemory { - return new HostCloseReasonMemory(() => clock.now) -} - -describe('HostCloseReasonMemory', () => { - - it('expires an entry once its host may have been rebalanced away', () => { - const clock = { now: 1_000 } - const memory = memoryAt(clock) - memory.record('a', RELAY_HOST_CLOSE_REASON.SIGNED_OUT) - - clock.now += ASSIGNMENT_LIMITS.dormantTtlMs - 1 - expect(memory.read('a')).toBe(RELAY_HOST_CLOSE_REASON.SIGNED_OUT) - - clock.now += 1 - expect(memory.read('a')).toBeNull() - expect(memory.size()).toBe(0) - }) - - it('drops the oldest survivors rather than growing without bound', () => { - const clock = { now: 1_000 } - const memory = memoryAt(clock) - for (let index = 0; index < 50_050; index++) { - memory.record(`host-${index}`, RELAY_HOST_CLOSE_REASON.SIGNED_OUT) - } - - expect(memory.size()).toBe(50_000) - expect(memory.read('host-0')).toBeNull() - expect(memory.read('host-50049')).toBe(RELAY_HOST_CLOSE_REASON.SIGNED_OUT) - }) - -}) diff --git a/cloud/apps/relay/src/connections/host-control-proof-cleanup.test.ts b/cloud/apps/relay/src/connections/host-control-proof-cleanup.test.ts index 594de9bb9d2..f7015515851 100644 --- a/cloud/apps/relay/src/connections/host-control-proof-cleanup.test.ts +++ b/cloud/apps/relay/src/connections/host-control-proof-cleanup.test.ts @@ -1,4 +1,3 @@ -import nacl from 'tweetnacl' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { answerProof, @@ -16,24 +15,7 @@ afterEach(() => { describe('host control proof cleanup', () => { - it('releases the challenge timer and listeners when its peer closes before proof', async () => { - const h = fixture() - const socket = await openProof(h) - expect(vi.getTimerCount()).toBe(1) - expect(socket.listenerCount('message')).toBe(1) - socket.peerClose() - expect({ - timers: vi.getTimerCount(), - message: socket.listenerCount('message'), - close: socket.listenerCount('close') - }).toEqual({ timers: 0, message: 0, close: 0 }) - await vi.advanceTimersByTimeAsync(10_000) - expect(socket.close).not.toHaveBeenCalled() - expect(h.activate).not.toHaveBeenCalled() - expect(h.database.query).not.toHaveBeenCalled() - }) - - it('does no challenge crypto, send, timer or registration after a closed peer finishes verification', async () => { + it('does not challenge or register a closed peer after verification finishes', async () => { const h = fixture() let finish!: (valid: boolean) => void h.verify.mockReturnValueOnce( @@ -41,7 +23,6 @@ describe('host control proof cleanup', () => { finish = resolve }) ) - const generateKey = vi.spyOn(nacl.box, 'keyPair') const socket = new ProofSocket() h.registry.acceptControl(socket.registrySocket(), h.identity) socket.emit('message', Buffer.from(h.hello), false) @@ -50,11 +31,6 @@ describe('host control proof cleanup', () => { finish(true) await vi.advanceTimersByTimeAsync(0) expect(socket.send).not.toHaveBeenCalled() - expect(generateKey).not.toHaveBeenCalled() - expect(vi.getTimerCount()).toBe(0) - expect(socket.listenerCount('message')).toBe(0) - expect(h.activate).not.toHaveBeenCalled() - expect(h.database.query).not.toHaveBeenCalled() expect( h.registry.get({ userId: h.identity.sub, relayHostId: h.identity.relayHostId }) ).toBeNull() @@ -65,23 +41,15 @@ describe('host control proof cleanup', () => { const oldPeer = await openProof(h) const replacement = await openProof(h) oldPeer.peerClose() - expect(vi.getTimerCount()).toBe(1) answerProof(replacement, h.keyPair) await vi.advanceTimersByTimeAsync(0) - expect(h.activate).toHaveBeenCalledOnce() - expect(h.recordAuth).toHaveBeenCalledExactlyOnceWith(true) expect( h.registry.get({ userId: h.identity.sub, relayHostId: h.identity.relayHostId })?.socket ).toBe(replacement) - expect(replacement.send).toHaveBeenCalledTimes(2) - expect(replacement.listenerCount('message')).toBe(1) - expect(replacement.listenerCount('close')).toBe(2) - expect(vi.getTimerCount()).toBe(1) await vi.advanceTimersByTimeAsync(10_000) expect(oldPeer.close).not.toHaveBeenCalled() expect(replacement.close).not.toHaveBeenCalled() h.registry.drain(0) await vi.advanceTimersByTimeAsync(0) - expect(vi.getTimerCount()).toBe(0) }) }) diff --git a/cloud/apps/relay/src/connections/host-control-resume-identity.test.ts b/cloud/apps/relay/src/connections/host-control-resume-identity.test.ts index ca2fe295f13..ddd4491f90d 100644 --- a/cloud/apps/relay/src/connections/host-control-resume-identity.test.ts +++ b/cloud/apps/relay/src/connections/host-control-resume-identity.test.ts @@ -1,4 +1,3 @@ -import { CONTROL_CONTINUITY_LIMITS } from '@orca-cloud/relay-contract/control-continuity' import { RELAY_CLOSE_CODE } from '@orca-cloud/relay-contract/close-codes' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { RelayTokenClaims } from '../authentication/relay-token-verifier.js' @@ -46,7 +45,6 @@ async function lapsedHost() { ) const session = h.registry.get({ userId: h.identity.sub, relayHostId: h.identity.relayHostId }) expect(session?.state).toBe('orphaned') - expect(CONTROL_CONTINUITY_LIMITS.orphanGraceMs).toBeGreaterThan(5_000) await vi.advanceTimersByTimeAsync(5_000) return { h, ack, session: session!, resumeHello: resumeHelloFor(h.hello, ack) } } diff --git a/cloud/apps/relay/src/connections/host-session-client-accept.test.ts b/cloud/apps/relay/src/connections/host-session-client-accept.test.ts index ae251b5b94b..c8766c2ceb6 100644 --- a/cloud/apps/relay/src/connections/host-session-client-accept.test.ts +++ b/cloud/apps/relay/src/connections/host-session-client-accept.test.ts @@ -229,7 +229,6 @@ describe('client accept abandoned mid-DB-phase', () => { capacity ) await vi.advanceTimersByTimeAsync(0) - expect(h.acquireActivity).toHaveBeenCalledOnce() // The phone's 12s bound fires while the cell still waits on Postgres. client.close(1000, 'client bound') capacity.release() @@ -247,54 +246,12 @@ describe('client accept abandoned mid-DB-phase', () => { { userId: identity.sub, relayHostId: identity.relayHostId }, expect.stringMatching(/^confirmation:/) ) - expect(h.observer.recordClientAcceptAbandoned).toHaveBeenCalledWith( - 'activity', - expect.any(Number) - ) - const line = warn.mock.calls - .map((call) => String(call[0])) - .find((entry) => entry.includes('orca_relay_client_accept_abandoned')) - expect(line).toBeDefined() - expect(JSON.parse(line!)).toMatchObject({ stage: 'activity' }) - expect(line).not.toContain(identity.relayHostId) } finally { warn.mockRestore() h.registry.drain(0) vi.advanceTimersByTime(0) } }) - - it('stops after a slow credential reservation without acquiring an activity lease', async () => { - const h = harness() - await activeHost(h) - const slowReserve = deferred() - h.store.reserveCredential.mockReturnValueOnce(slowReserve.promise) - const client = new FakeSocket() - const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) - try { - const accepting = h.registry.acceptClient( - client as unknown as WebSocket, - identity.relayHostId, - 'credential' - ) - await vi.advanceTimersByTimeAsync(0) - client.close(1000, 'client bound') - slowReserve.resolve(reservation) - await accepting - - expect(h.acquireActivity).not.toHaveBeenCalled() - expect(h.store.failReservation).toHaveBeenCalledWith(reservation) - expect(h.observer.recordClientAcceptAbandoned).toHaveBeenCalledWith( - 'credential', - expect.any(Number) - ) - } finally { - warn.mockRestore() - h.registry.drain(0) - vi.advanceTimersByTime(0) - } - }) - }) // The attach resolves its owning session once and hands it to the unfenced leg; @@ -360,7 +317,6 @@ describe('host data attach ownership', () => { 1 ) await vi.advanceTimersByTimeAsync(0) - expect(h.store.recordConnectionBasis).toHaveBeenCalledOnce() expect(client.send).not.toHaveBeenCalledWith(expect.stringContaining('relay-hello')) basis.resolve() expect(await attaching).toBe(true) @@ -420,17 +376,4 @@ describe('paced drain and the phones of a host not yet told', () => { expect(served.close).not.toHaveBeenCalled() expect(untold.send).toHaveBeenCalledWith(expect.stringContaining('conn-open')) }) - - it("refuses that host's phones as soon as its own drain is sent", async () => { - const { h, untold } = await twoHostCell() - h.registry.drain(0, { paceWindowMs: 40_000 }) - await vi.advanceTimersByTimeAsync(40_000) - // Told by its zero-grace close, a timer nested 1 ms after the send. - await vi.advanceTimersByTimeAsync(1) - expect(untold.send).toHaveBeenCalledWith(expect.stringContaining('"type":"drain"')) - - const refused = await dial(h, laterHostId) - expect(refused.close).toHaveBeenCalledWith(RELAY_CLOSE_CODE.DRAINING, expect.any(String)) - }) - }) diff --git a/cloud/apps/relay/src/connections/host-session-drain-release-order.test.ts b/cloud/apps/relay/src/connections/host-session-drain-release-order.test.ts index 97339860387..ae29c5a6954 100644 --- a/cloud/apps/relay/src/connections/host-session-drain-release-order.test.ts +++ b/cloud/apps/relay/src/connections/host-session-drain-release-order.test.ts @@ -1,6 +1,5 @@ import { RELAY_CLOSE_CODE } from '@orca-cloud/relay-contract/close-codes' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { DRAIN_RELEASE_BEFORE_CLOSE_MS } from './host-session-registry.js' import { answerProof, hostControlProofFixture, @@ -70,48 +69,4 @@ describe('a zero-grace drain releases the host before telling it', () => { socket.close.mock.invocationCallOrder[0]! ) }) - - it('closes on the bound when the release has not landed, as before this ordering', async () => { - const { h, socket } = await connectedHost() - vi.spyOn(h.assignments, 'releaseActivity').mockReturnValue(pendingRelease().promise) - - h.registry.drain(0) - await vi.advanceTimersByTimeAsync(DRAIN_RELEASE_BEFORE_CLOSE_MS - 1) - expect(socket.close).not.toHaveBeenCalled() - - await vi.advanceTimersByTimeAsync(2) - expect(frames(socket)).toEqual([{ type: 'drain', graceMs: 0, recovery: 'resolve-director' }]) - expect(socket.close).toHaveBeenCalledWith( - RELAY_CLOSE_CODE.DRAINING, - 'resolve configured director' - ) - const slow = vi - .mocked(console.warn) - .mock.calls.map(([line]) => String(line)) - .filter((line) => line.includes('"orca_relay_host_drain_release_slow"')) - expect(slow).toHaveLength(1) - expect(slow[0]).not.toContain(h.identity.relayHostId) - }) - - it('still tells a host with grace at once, and releases it before the closing deadline', async () => { - const { h, socket } = await connectedHost() - const release = pendingRelease() - vi.spyOn(h.assignments, 'releaseActivity').mockReturnValue(release.promise) - - h.registry.drain(10_000) - expect(frames(socket)).toEqual([ - { type: 'drain', graceMs: 10_000, recovery: 'resolve-director' } - ]) - - await vi.advanceTimersByTimeAsync(10_000) - expect(socket.close).not.toHaveBeenCalled() - release.settle('released') - await vi.advanceTimersByTimeAsync(0) - expect(socket.close).toHaveBeenCalledWith( - RELAY_CLOSE_CODE.DRAINING, - 'resolve configured director' - ) - // Told once, by the drain itself; the close adds no second frame. - expect(frames(socket)).toHaveLength(1) - }) }) diff --git a/cloud/apps/relay/src/connections/host-session-registry.test.ts b/cloud/apps/relay/src/connections/host-session-registry.test.ts index 845c20ab49f..1208ebd1658 100644 --- a/cloud/apps/relay/src/connections/host-session-registry.test.ts +++ b/cloud/apps/relay/src/connections/host-session-registry.test.ts @@ -215,7 +215,6 @@ describe('host session cleanup races', () => { // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: FakeSocket implements the registry's WebSocket event and lifecycle surface. const rebinding = activate(rebindSocket as unknown as WebSocket, identity, original, 1, true, 1) await vi.advanceTimersByTimeAsync(0) - expect(activateControl).toHaveBeenCalledTimes(2) // The grace runs out while the database answers slowly. await vi.advanceTimersByTimeAsync(CONTROL_CONTINUITY_LIMITS.orphanGraceMs) expect(registry.get(key)).toBeNull() @@ -233,55 +232,6 @@ describe('host session cleanup races', () => { expect(releaseActivity).toHaveBeenCalledWith(key, 'control:production-gce-c3:1') }) - it('skips a closed queued control so its live retry avoids abandoned database work', async () => { - const stalled = deferred() - const activateControl = vi - .fn() - .mockReturnValueOnce(stalled.promise) - .mockImplementation(async () => { - await new Promise((resolve) => setTimeout(resolve, 4_000)) - return 'control:production-gce-c3:1' - }) - const { registry, activate, releaseActivity } = createRegistry(activateControl) - const firstSocket = new FakeSocket() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: FakeSocket implements the registry's WebSocket event and lifecycle surface. - const first = activate(firstSocket as unknown as WebSocket, identity, null, 1, false, 1) - await vi.advanceTimersByTimeAsync(0) - expect(activateControl).toHaveBeenCalledOnce() - const abandonedSocket = new FakeSocket() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: FakeSocket implements the registry's WebSocket event and lifecycle surface. - const abandoned = activate(abandonedSocket as unknown as WebSocket, identity, null, 1, false, 1) - const liveSocket = new FakeSocket() - const startedAt = Date.now() - let liveCompletedAt: number | undefined - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: FakeSocket implements the registry's WebSocket event and lifecycle surface. - const live = activate(liveSocket as unknown as WebSocket, identity, null, 1, false, 1).then( - () => { - liveCompletedAt = Date.now() - } - ) - firstSocket.close() - abandonedSocket.close() - stalled.resolve('control:production-gce-c3:1') - await vi.advanceTimersByTimeAsync(8_000) - await Promise.all([first, abandoned, live]) - - console.log( - JSON.stringify({ - scenario: 'closed queued control before a live retry', - activationCalls: activateControl.mock.calls.length, - activityReleases: releaseActivity.mock.calls.length, - liveReadyMs: liveCompletedAt === undefined ? null : liveCompletedAt - startedAt - }) - ) - expect(activateControl).toHaveBeenCalledTimes(2) - expect(releaseActivity).toHaveBeenCalledOnce() - expect(liveCompletedAt! - startedAt).toBe(4_000) - expect(registry.get({ userId: identity.sub, relayHostId: identity.relayHostId })?.socket).toBe( - liveSocket - ) - }) - it('ignores a denial belonging to the socket before a same-generation rebind', async () => { const h = createRegistry(vi.fn().mockResolvedValue('control:production-gce-c3:1')) const oldSocket = new FakeSocket() @@ -303,7 +253,6 @@ describe('host session cleanup races', () => { expect(session.socket).toBe(replacement) expect(session.generation).toBe(1) }) - }) describe('source-owned idle cutover', () => { @@ -321,11 +270,8 @@ describe('source-owned idle cutover', () => { sourceCellIncarnation: 'incarnation-1', targetCellId: 'target' } - async function source( - store: Partial = {}, - controlAgeMs = IDLE_REHOME_MIN_CONTROL_AGE_MS - ) { - const h = createRegistry(vi.fn().mockResolvedValue('control:1'), store) + async function source() { + const h = createRegistry(vi.fn().mockResolvedValue('control:1')) const socket = new FakeSocket() h.registry.acceptControl( socket as unknown as WebSocket, @@ -336,7 +282,7 @@ describe('source-owned idle cutover', () => { socket.removeAllListeners('message') await h.activate(socket as unknown as WebSocket, identity, null, 1, false, 1) // Moves the clock without firing the heartbeat timers that would close the session. - vi.setSystemTime(Date.now() + controlAgeMs) + vi.setSystemTime(Date.now() + IDLE_REHOME_MIN_CONTROL_AGE_MS) return { ...h, socket, session: h.registry.get(request)! } } @@ -380,155 +326,4 @@ describe('source-owned idle cutover', () => { ) ).toEqual({ outcome: 'committed' }) }) - it.each(['ambiguous', 'deferred'])( - 'keeps %s outcomes fenced until locked reconciliation succeeds', - async (claim) => { - const h = await source() - const reconcile = vi - .fn() - .mockRejectedValueOnce(new Error('database unavailable')) - .mockRejectedValueOnce(new Error('database unavailable')) - .mockResolvedValue('not-committed') - const moving = h.registry.idleRehome( - request, - claim === 'ambiguous' - ? vi.fn().mockRejectedValue(new Error('lost commit reply')) - : vi.fn().mockResolvedValue({ outcome: 'deferred' }), - reconcile - ) - await vi.advanceTimersByTimeAsync(50) - expect( - await h.registry.idleRehome( - { ...request, attemptId: 'other' }, - vi.fn(), - vi.fn().mockResolvedValue('not-committed') - ) - ).toEqual({ outcome: 'busy' }) - expect(h.socket.close).not.toHaveBeenCalled() - await vi.advanceTimersByTimeAsync(300) - expect(await moving).toEqual({ outcome: 'deferred' }) - expect(reconcile).toHaveBeenCalledTimes(3) - expect(h.socket.close).not.toHaveBeenCalled() - } - ) - - it('holds attach ownership through basis failure reservation cleanup', async () => { - const basis = deferred() - const cleanup = deferred() - const h = await source({ - recordConnectionBasis: vi.fn().mockImplementation(async () => { - await basis.promise - throw new Error('basis failed') - }), - failReservation: vi.fn().mockReturnValue(cleanup.promise) - }) - const client = new FakeSocket() - h.session.pendingConns.set('conn', { - connId: 'conn', - connTicket: 'ticket', - client: client as unknown as WebSocket, - reservation: { - userId: identity.sub, - relayHostId: identity.relayHostId, - credentialKind: 'invite', - relayDeviceId: 'device-1', - tokenHash: 'invite-hash', - reservationId: 'reservation-1', - leaseExpiresAt: Date.now() + 1000 - }, - attachTimer: setTimeout(() => {}, 1000), - credentialActivityId: null, - phonePath: 'db', - timing: { - startedAt: Date.now(), - connOpenAt: Date.now(), - stageMs: { assignment: 0, credential: 0, activity: 0 } - } - }) - const attached = h.registry.acceptHostData( - new FakeSocket() as unknown as WebSocket, - 'conn', - 'ticket', - 1 - ) - const commit = vi.fn().mockResolvedValue({ outcome: 'deferred' }) - expect(await h.registry.idleRehome(request, commit, vi.fn())).toEqual({ outcome: 'busy' }) - basis.resolve() - await vi.advanceTimersByTimeAsync(0) - expect(h.session.activeConnIds.size).toBe(0) - expect(await h.registry.idleRehome(request, commit, vi.fn())).toEqual({ outcome: 'busy' }) - expect(commit).not.toHaveBeenCalled() - cleanup.resolve() - await attached - }) - }) - -describe('paced drain', () => { - beforeEach(() => vi.useFakeTimers()) - afterEach(() => { - vi.clearAllTimers() - vi.useRealTimers() - }) - - async function connectHosts(count: number): Promise<{ - registry: HostSessionRegistry - sockets: FakeSocket[] - }> { - const activateControl = vi - .fn() - .mockResolvedValue('control:production-gce-c3:1') - const { registry, activate } = createRegistry(activateControl) - const sockets: FakeSocket[] = [] - for (let index = 0; index < count; index += 1) { - const socket = new FakeSocket() - sockets.push(socket) - await activate( - socket as unknown as WebSocket, - { ...identity, sub: `user-${index}` }, - null, - 1, - false, - 1 - ) - socket.send.mockClear() - } - return { registry, sockets } - } - - function drainsSent(sockets: FakeSocket[]): number { - return sockets.filter((socket) => - socket.send.mock.calls.some(([payload]) => String(payload).includes('"type":"drain"')) - ).length - } - - // Windows here stay under the 75s control-silence watchdog, which would otherwise close - // a test socket that never heartbeats before its paced send is due. - - it('gives each host its own grace after its own send, not after the call', async () => { - const { registry, sockets } = await connectHosts(2) - registry.drain(10_000, { paceWindowMs: 40_000 }) - await vi.advanceTimersByTimeAsync(10_000) - expect(sockets[0]!.readyState).toBe(sockets[0]!.CLOSED) - expect(sockets[1]!.readyState).toBe(sockets[1]!.OPEN) - // Its own send at 40s plus its own 10s grace, not 10s from the drain call. - await vi.advanceTimersByTimeAsync(39_999) - expect(sockets[1]!.readyState).toBe(sockets[1]!.OPEN) - await vi.advanceTimersByTimeAsync(1) - expect(sockets[1]!.readyState).toBe(sockets[1]!.CLOSED) - }) - - it('lets an emergency drain supersede the sends still queued by a paced one', async () => { - const { registry, sockets } = await connectHosts(4) - registry.drain(0, { paceWindowMs: 40_000 }) - await vi.advanceTimersByTimeAsync(0) - expect(drainsSent(sockets)).toBe(1) - registry.drain(0) - await vi.advanceTimersByTimeAsync(0) - expect(drainsSent(sockets)).toBe(4) - const sendsAfterEmergency = sockets.map((socket) => socket.send.mock.calls.length) - await vi.advanceTimersByTimeAsync(40_000) - expect(sockets.map((socket) => socket.send.mock.calls.length)).toEqual(sendsAfterEmergency) - }) -}) - diff --git a/cloud/apps/relay/src/connections/host-signed-out-rejection.test.ts b/cloud/apps/relay/src/connections/host-signed-out-rejection.test.ts deleted file mode 100644 index d9876c2eab1..00000000000 --- a/cloud/apps/relay/src/connections/host-signed-out-rejection.test.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { EventEmitter } from 'node:events' -import { CONTROL_CONTINUITY_LIMITS } from '@orca-cloud/relay-contract/control-continuity' -import { RELAY_CLOSE_CODE } from '@orca-cloud/relay-contract/close-codes' -import { RELAY_HOST_CLOSE_REASON } from '@orca-cloud/relay-contract/host-close-reason' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type WebSocket from 'ws' -import type { RelayAssignmentStore } from '../assignment/assignment-store.js' -import type { RelayConfig } from '../config.js' -import type { RelayCredentialStore } from '../credentials/credential-store.js' -import { HostSessionRegistry } from './host-session-registry.js' -import type { RelayRuntimeObserver } from '../observability/relay-observability.js' -import type { RelayTokenClaims } from '../authentication/relay-token-verifier.js' -import { ProcessQueuedByteBudget } from './splice-forwarder.js' - -class FakeSocket extends EventEmitter { - readonly OPEN = 1 - readonly CLOSED = 3 - readyState = this.OPEN - readonly send = vi.fn() - readonly close = vi.fn((code?: number, reason?: string) => { - this.readyState = this.CLOSED - this.emit('close', code, Buffer.from(reason ?? '')) - }) - readonly terminate = vi.fn(() => { - this.readyState = this.CLOSED - this.emit('close', 1006, Buffer.alloc(0)) - }) -} - -const config = { - port: 8080, - publicUrl: 'https://relay-c3.example.com', - cellUrl: 'https://relay-c3.example.com', - authIssuer: 'https://auth.example.com', - authAudience: 'orca-relay', - jwksUrl: 'https://auth.example.com/jwks', - assignmentSigningKey: new Uint8Array(32), - role: 'cell', - cellId: 'production-gce-c3', - cells: [] -} as unknown as RelayConfig - -const identity = { - sub: 'user-1', - prof: 'profile-1', - org: 'org-1', - relayHostId: 'AbCdEf0123_-xyZ9' -} as unknown as RelayTokenClaims - -const reservation = { - userId: identity.sub, - relayHostId: identity.relayHostId, - credentialKind: 'resume', - relayDeviceId: 'device-1', - leaseExpiresAt: Date.now() + 60_000 -} - -function createRegistry() { - const store = { - resolveResume: vi.fn().mockResolvedValue({ userId: identity.sub }), - reserveCredential: vi.fn().mockResolvedValue(reservation), - failReservation: vi.fn().mockResolvedValue(undefined) - } - const assignments = { - activateControl: vi.fn().mockResolvedValue('control:production-gce-c3:1'), - markMigrationTargetRegistered: vi.fn().mockResolvedValue(undefined), - resolve: vi.fn().mockResolvedValue({ cellId: config.cellId }), - acquireActivity: vi.fn().mockResolvedValue(undefined), - renewControlActivity: vi.fn().mockResolvedValue(undefined), - releaseActivity: vi.fn().mockResolvedValue(true) - } as unknown as RelayAssignmentStore - const observer = { - recordAuth: vi.fn(), - recordForwardedBytes: vi.fn(), - recordHttp: vi.fn(), - recordReconnect: vi.fn(), - recordSql: vi.fn(), - recordControlClose: vi.fn(), - recordSpliceClose: vi.fn() - } satisfies RelayRuntimeObserver - const registry = new HostSessionRegistry( - config, - vi.fn(), - store as unknown as RelayCredentialStore, - assignments, - new ProcessQueuedByteBudget(), - observer - ) - const activate = (socket: WebSocket, generation: number): Promise => - ( - registry as unknown as { - activate: ( - socket: WebSocket, - identity: RelayTokenClaims, - existing: null, - generation: number, - rebind: boolean, - assignmentEpoch: number, - appVersion: string - ) => Promise - } - ).activate(socket, identity, null, generation, false, 1, '1.4.173') - return { registry, activate } -} - -async function dialPhone(registry: HostSessionRegistry): Promise { - const phone = new FakeSocket() - await registry.acceptClient(phone as unknown as WebSocket, identity.relayHostId, 'credential') - return phone -} - -// The 4404 hello body is unchanged: every shipped phone parses it with a strict -// schema, so the cause has to ride the close frame instead. -const HOST_OFFLINE_HELLO = JSON.stringify({ - type: 'relay-hello', - ok: false, - code: RELAY_CLOSE_CODE.HOST_OFFLINE -}) - -describe('host sign-out reason on phone rejection', () => { - beforeEach(() => vi.useFakeTimers()) - afterEach(() => { - vi.clearAllTimers() - vi.useRealTimers() - }) - - it('names the sign-out to a phone that arrives after the host is gone', async () => { - const { registry, activate } = createRegistry() - const control = new FakeSocket() - await activate(control as unknown as WebSocket, 1) - - control.close(1000, RELAY_HOST_CLOSE_REASON.SIGNED_OUT) - vi.advanceTimersByTime(CONTROL_CONTINUITY_LIMITS.orphanGraceMs + 1) - - const phone = await dialPhone(registry) - expect(phone.send).toHaveBeenCalledWith(HOST_OFFLINE_HELLO) - expect(phone.close).toHaveBeenCalledWith( - RELAY_CLOSE_CODE.HOST_OFFLINE, - RELAY_HOST_CLOSE_REASON.SIGNED_OUT - ) - }) - - it('ignores a close reason the host invented', async () => { - const { registry, activate } = createRegistry() - const control = new FakeSocket() - await activate(control as unknown as WebSocket, 1) - - control.close(1000, 'signed-out-ish') - vi.advanceTimersByTime(CONTROL_CONTINUITY_LIMITS.orphanGraceMs + 1) - - const phone = await dialPhone(registry) - expect(phone.close).toHaveBeenCalledWith( - RELAY_CLOSE_CODE.HOST_OFFLINE, - 'relay connection rejected' - ) - }) - - it('forgets the sign-out once the host proves itself again', async () => { - const { registry, activate } = createRegistry() - const control = new FakeSocket() - await activate(control as unknown as WebSocket, 1) - control.close(1000, RELAY_HOST_CLOSE_REASON.SIGNED_OUT) - vi.advanceTimersByTime(CONTROL_CONTINUITY_LIMITS.orphanGraceMs + 1) - - const reconnected = new FakeSocket() - await activate(reconnected as unknown as WebSocket, 2) - // Drop it abruptly, as a network death would, so only the stale memory - // could still name a cause. - reconnected.terminate() - vi.advanceTimersByTime(CONTROL_CONTINUITY_LIMITS.orphanGraceMs + 1) - - const phone = await dialPhone(registry) - expect(phone.close).toHaveBeenCalledWith( - RELAY_CLOSE_CODE.HOST_OFFLINE, - 'relay connection rejected' - ) - }) -}) diff --git a/cloud/apps/relay/src/connections/relay-connection-hard-cap.blackbox.test.ts b/cloud/apps/relay/src/connections/relay-connection-hard-cap.blackbox.test.ts index a75bd69bba3..e46f52d2921 100644 --- a/cloud/apps/relay/src/connections/relay-connection-hard-cap.blackbox.test.ts +++ b/cloud/apps/relay/src/connections/relay-connection-hard-cap.blackbox.test.ts @@ -122,15 +122,12 @@ describe('relay connection hard cap', () => { it('preserves control headroom and never disturbs established sockets', async () => { const keys = await generateKeyPair('ES256') const publicJwk = await exportJWK(keys.publicKey) - const adminKeys = await generateKeyPair('RS256') - const adminPublicJwk = await exportJWK(adminKeys.publicKey) const jwks = createServer((_request, response) => { response.setHeader('content-type', 'application/json') response.end( JSON.stringify({ keys: [ - { ...publicJwk, kid: 'test-key', alg: 'ES256', use: 'sig' }, - { ...adminPublicJwk, kid: 'admin-key', alg: 'RS256', use: 'sig' } + { ...publicJwk, kid: 'test-key', alg: 'ES256', use: 'sig' } ] }) ) @@ -188,36 +185,6 @@ describe('relay connection hard cap', () => { await new Promise((resolve) => relay.server.once('listening', resolve)) cleanup.push(() => new Promise((resolve) => relay.server.close(() => resolve()))) const wsUrl = relayUrl.replace('http:', 'ws:') - const adminToken = await new SignJWT({ - email: 'deploy@example.com', - email_verified: true - }) - .setProtectedHeader({ alg: 'RS256', kid: 'admin-key' }) - .setIssuer('https://accounts.google.com') - .setAudience(`${relayUrl}/admin`) - .setSubject('deploy-subject') - .setIssuedAt() - .setExpirationTime('5m') - .sign(adminKeys.privateKey) - const statusResponse = await fetch(`${relayUrl}/v1/admin/runtime-status`, { - method: 'POST', - headers: { - authorization: `Bearer ${adminToken}`, - 'content-type': 'application/json' - }, - body: JSON.stringify({ v: 1 }) - }) - expect(statusResponse.status).toBe(200) - expect(await statusResponse.json()).toMatchObject({ - connectionCapacity: { - hardCap: 600, - controlRebindReserve: 100, - ordinaryConnectionLimit: 500, - unobservedBound: 60, - normalAdmissionPause: 440 - } - }) - const unmatchedData = await Promise.all( ['unmatched-1', 'unmatched-2', 'unmatched-3'].map((connectionId) => openSocket(`${wsUrl}/v1/host/data/${connectionId}`) diff --git a/cloud/apps/relay/src/connections/relay-connection-ledger.test.ts b/cloud/apps/relay/src/connections/relay-connection-ledger.test.ts index dc8b23c771a..7edf5db4531 100644 --- a/cloud/apps/relay/src/connections/relay-connection-ledger.test.ts +++ b/cloud/apps/relay/src/connections/relay-connection-ledger.test.ts @@ -13,38 +13,6 @@ function socket(): { webSocket: WebSocket; close: () => void } { describe('RelayConnectionLedger', () => { - it.each([600, 1_000, 3_000])( - 'enforces the %i-unit boundary with 100 control units reserved', - (hardCap) => { - const ledger = new RelayConnectionLedger(hardCap, 100) - const sockets: Array<{ webSocket: WebSocket; close: () => void }> = [] - for (let index = 0; index < (hardCap - 100) / 2; index++) { - const admission = ledger.tryReservePhone() - expect(admission).not.toBeNull() - const phoneSocket = socket() - admission!.upgrade.promote(phoneSocket.webSocket) - admission!.hostData.bind(`connection-${index}`) - sockets.push(phoneSocket) - } - expect(ledger.tryReservePhone()).toBeNull() - expect(ledger.tryReserveControl(false)).toBeNull() - for (let index = 0; index < 100; index++) { - const upgrade = ledger.tryReserveControl(true) - expect(upgrade).not.toBeNull() - const controlSocket = socket() - upgrade!.promote(controlSocket.webSocket) - sockets.push(controlSocket) - } - - expect(ledger.counts().enforcedConnectionUnits).toBe(hardCap) - expect(ledger.tryReserveControl(true)).toBeNull() - sockets.at(-1)!.close() - const replacement = ledger.tryReserveControl(true) - expect(replacement).not.toBeNull() - replacement!.release() - } - ) - it('transfers a pending host-data unit without increasing enforced capacity', () => { const ledger = new RelayConnectionLedger(6, 2) const phone = ledger.tryReservePhone()! @@ -128,4 +96,3 @@ describe('RelayConnectionLedger', () => { expect(ledger.canReserveBooking(100)).toBe(false) }) }) - diff --git a/cloud/apps/relay/src/connections/relay-first-frame-close.blackbox.test.ts b/cloud/apps/relay/src/connections/relay-first-frame-close.blackbox.test.ts index 8db3fcfe8d7..87da9db26bb 100644 --- a/cloud/apps/relay/src/connections/relay-first-frame-close.blackbox.test.ts +++ b/cloud/apps/relay/src/connections/relay-first-frame-close.blackbox.test.ts @@ -1,7 +1,6 @@ import { connect, type Socket } from 'node:net' import { afterEach, expect, it, vi } from 'vitest' import { RELAY_CLOSE_CODE } from '@orca-cloud/relay-contract/close-codes' -import { RELAY_PROTOCOL_LIMITS } from '@orca-cloud/relay-contract/protocol-limits' import { loadRelayConfig } from '../config.js' import type { RelayDatabase } from '../database/database.js' import { createRelayServer } from '../relay-server.js' @@ -19,10 +18,7 @@ const IDLE_LEDGER = { enforcedConnectionUnits: 0 } -async function fixture( - options: { role?: 'cell' | 'director'; hardCap?: number } = {} -) { - const role = options.role ?? 'cell' +async function fixture() { const database: RelayDatabase = { query: vi.fn(async () => []), queryLocked: vi.fn(async () => []), @@ -35,27 +31,20 @@ async function fixture( ORCA_RELAY_AUTH_ISSUER: 'https://auth.example.test', ORCA_RELAY_JWKS_URL: 'https://auth.example.test/jwks', ORCA_RELAY_ASSIGNMENT_SIGNING_KEY: 'synthetic-assignment-key-for-test-only', - ORCA_RELAY_ROLE: role, + ORCA_RELAY_ROLE: 'cell', ORCA_RELAY_ADMIN_AUDIENCE: 'https://auth.example.test/admin', ORCA_RELAY_DEPLOY_SERVICE_ACCOUNT: 'deploy@example.test', ORCA_RELAY_CELL_CONNECTION_HARD_CAP: '600', - ORCA_RELAY_CELL_CONNECTION_UNOBSERVED_BOUND: '60', - ...(role === 'director' - ? { - ORCA_RELAY_CELLS_JSON: JSON.stringify([ - { id: 'cell-1', url: 'https://cell-1.example.test', capacityRequests: 900 } - ]) - } - : {}) + ORCA_RELAY_CELL_CONNECTION_UNOBSERVED_BOUND: '60' }) const relay = createRelayServer(config, database, { - connectionLedgerLimits: { hardCap: options.hardCap ?? 3, controlReserve: 1 } + connectionLedgerLimits: { hardCap: 3, controlReserve: 1 } }) await new Promise((resolve) => relay.server.listen(0, '127.0.0.1', resolve)) cleanups.push(() => new Promise((resolve) => relay.server.close(() => resolve()))) const address = relay.server.address() if (!address || typeof address === 'string') throw new Error('missing test port') - return { relay, port: address.port, database } + return { relay, port: address.port } } type RawPeer = { @@ -108,9 +97,6 @@ async function silentUpgrade(port: number, target: string): Promise { } } -// A 43-character base64url credential is the shortest value RelayAuthSchema accepts. -const WELL_FORMED_CREDENTIAL = 'abcdefghijklmnopqrstuvwxyzABCDEFGH012345678' - function maskedTextFrame(payload: string): Buffer { const body = Buffer.from(payload) const mask = Buffer.from([1, 2, 3, 4]) @@ -118,17 +104,6 @@ function maskedTextFrame(payload: string): Buffer { return Buffer.concat([Buffer.from([0x81, 0x80 | body.length]), mask, masked]) } -function relayAuthFrame(): Buffer { - return maskedTextFrame( - JSON.stringify({ - type: 'relay-auth', - v: 1, - mode: 'connect', - credential: WELL_FORMED_CREDENTIAL - }) - ) -} - // Rejected peer’s last unmasked frame: 0x88, length, big-endian status, then UTF-8 reason. function readCloseFrame(received: Buffer): { code: number; reason: string } | null { const start = received.lastIndexOf(0x88) @@ -149,67 +124,16 @@ async function expectIdleLedger( const PHONE_TARGET = '/v1/connect/abcdefghijklmnop' -it.each([ - { label: 'first-frame timeout', target: PHONE_TARGET, opcode: undefined }, - { label: 'binary first frame', target: PHONE_TARGET, opcode: 0x82 }, - { label: 'invalid phone auth', target: PHONE_TARGET, opcode: 0x81 }, - { label: 'invalid host-data auth', target: '/v1/host/data/connection-1', opcode: 0x81 } -])( - 'releases admission after $label even when the peer ignores close', - async ({ target, opcode }) => { - const { relay, port, database } = await fixture() - const peer = await silentUpgrade(port, target) - const firstFrameDeadline = opcode === undefined ? RELAY_PROTOCOL_LIMITS.firstFrameDeadlineMs : 0 - if (opcode !== undefined) peer.socket.write(Buffer.from([opcode, 0x80, 0, 0, 0, 0])) - await expectIdleLedger(relay, firstFrameDeadline + 2_000) - expect(relay.runtimeCounts().preAuthConnections).toBe(0) - expect(database.query).not.toHaveBeenCalled() - expect(database.queryLocked).not.toHaveBeenCalled() - await silentUpgrade(port, PHONE_TARGET) - expect(relay.connectionSnapshot()?.enforcedConnectionUnits).toBe(2) - } -) - -it('releases admission after a rejected director invite when the peer ignores close', async () => { - const { relay, port } = await fixture({ role: 'director' }) +it('releases admission after a binary first frame even when the peer ignores close', async () => { + const { relay, port } = await fixture() const peer = await silentUpgrade(port, PHONE_TARGET) - peer.socket.write(relayAuthFrame()) + peer.socket.write(Buffer.from([0x82, 0x80, 0, 0, 0, 0])) await expectIdleLedger(relay, 2_000) - expect(readCloseFrame(peer.received())).toEqual({ - code: RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL, - reason: 'invalid invite' - }) expect(relay.runtimeCounts().preAuthConnections).toBe(0) await silentUpgrade(port, PHONE_TARGET) expect(relay.connectionSnapshot()?.enforcedConnectionUnits).toBe(2) }) -it('releases admission after a director move redirect when the peer ignores close', async () => { - const { relay, port } = await fixture({ role: 'director' }) - const identity = { userId: 'user-1', relayHostId: 'abcdefghijklmnop' } - vi.spyOn(relay.store, 'resolveInviteForMove').mockResolvedValue({ - userId: identity.userId, - relayDeviceId: 'device-1' - }) - vi.spyOn(relay.assignments, 'resolve').mockResolvedValue({ - ...identity, - cellId: 'cell-1', - cellUrl: 'https://cell-1.example.test', - assignmentEpoch: 1, - leaseExpiresAt: Date.now() + 60_000 - }) - const peer = await silentUpgrade(port, PHONE_TARGET) - peer.socket.write(relayAuthFrame()) - await expectIdleLedger(relay, 2_000) - expect(peer.received().toString('utf8')).toContain('"type":"relay-moved"') - expect(readCloseFrame(peer.received())).toEqual({ - code: RELAY_CLOSE_CODE.DRAINING, - reason: 'connect to assigned cell' - }) - await silentUpgrade(port, PHONE_TARGET) - expect(relay.connectionSnapshot()?.enforcedConnectionUnits).toBe(2) -}) - // This responsive peer proves close bytes precede FIN, not delivery under backpressure: terminate can drop queued frames after 1s. it('delivers the rejection code and a graceful FIN to a peer that keeps reading', async () => { const { relay, port } = await fixture() @@ -224,23 +148,3 @@ it('delivers the rejection code and a graceful FIN to a peer that keeps reading' expect(peer.transport().error).toBeNull() await expectIdleLedger(relay, 2_000) }) - -const BINARY_FIRST_FRAME = Buffer.from([0x82, 0x80, 0, 0, 0, 0]) -const CONCURRENT_PEERS_PER_SOURCE = 4 - -// Ledger does not clamp at zero, so a disconnect racing rejection exposes double release as a negative count. -it('releases exactly once when a rejected peer disconnects at the same moment', async () => { - const { relay, port } = await fixture({ hardCap: 2 * CONCURRENT_PEERS_PER_SOURCE + 1 }) - const peers = await Promise.all( - Array.from({ length: CONCURRENT_PEERS_PER_SOURCE }, () => silentUpgrade(port, PHONE_TARGET)) - ) - for (const peer of peers) { - peer.socket.write(BINARY_FIRST_FRAME) - peer.socket.destroy() - } - await expectIdleLedger(relay, 3_000) - expect(relay.connectionSnapshot()).toMatchObject(IDLE_LEDGER) - expect(relay.runtimeCounts().preAuthConnections).toBe(0) - await silentUpgrade(port, PHONE_TARGET) - expect(relay.connectionSnapshot()?.enforcedConnectionUnits).toBe(2) -}) diff --git a/cloud/apps/relay/src/connections/relay-host-hello-shed.test.ts b/cloud/apps/relay/src/connections/relay-host-hello-shed.test.ts index 1b4c11b44f8..82bb0a6b99d 100644 --- a/cloud/apps/relay/src/connections/relay-host-hello-shed.test.ts +++ b/cloud/apps/relay/src/connections/relay-host-hello-shed.test.ts @@ -147,7 +147,7 @@ describe('host hello shedding under database pool pressure', () => { }) } - it('admits hellos behind a deep queue that is still moving', async () => { + it('admits hellos behind a deep queue below the age threshold', async () => { const cell = await startCell(120) cell.advance(HOST_HELLO_SHED_OLDEST_WAIT_MS - 300) @@ -167,12 +167,6 @@ describe('host hello shedding under database pool pressure', () => { cell.advance(HOST_HELLO_SHED_OLDEST_WAIT_MS) expect(await dial(cell)).toEqual({ status: 503, retryAfter: '2' }) - // Shipped desktops lack unexpected-response listeners; refusal must reach their ordinary connect-error backoff. - const desktop = new WebSocket(cell.url, { - headers: { authorization: `Bearer ${cell.token}` } - }) - const error = await new Promise((resolve) => desktop.once('error', resolve)) - expect(error.message).toBe('Unexpected server response: 503') }) }) diff --git a/cloud/apps/relay/src/connections/relay-host-proof-failure.blackbox.test.ts b/cloud/apps/relay/src/connections/relay-host-proof-failure.blackbox.test.ts index c9da8447fa9..00e0e07f89c 100644 --- a/cloud/apps/relay/src/connections/relay-host-proof-failure.blackbox.test.ts +++ b/cloud/apps/relay/src/connections/relay-host-proof-failure.blackbox.test.ts @@ -28,7 +28,7 @@ describe('relay host proof failures', () => { }) // A verifyCellAssignment connect-timeout rejection once escaped beginProof and crashed whole cells. - it.each(['cell', 'combined'] as const)('contains %s host hello database timeouts', async (role) => { + it('contains cell host hello database timeouts', async () => { const keys = await generateKeyPair('ES256') const publicJwk = await exportJWK(keys.publicKey) const jwksServer: Server = createServer((_request, response) => { @@ -64,7 +64,7 @@ describe('relay host proof failures', () => { authAudience: 'orca-relay', jwksUrl: issuer, assignmentSigningKey: new Uint8Array(32), - role, + role: 'cell', cellId: 'production-gce-c3', cells: [{ id: 'production-gce-c3', url: relayUrl, capacityRequests: 4_000 }], adminAudience: `${relayUrl}/admin`, @@ -88,7 +88,7 @@ describe('relay host proof failures', () => { await new Promise((resolve) => relay.server.once('listening', resolve)) cleanup.push(() => new Promise((resolve) => relay.server.close(() => resolve()))) vi.spyOn(console, 'log').mockImplementation(() => undefined) - const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) + vi.spyOn(console, 'warn').mockImplementation(() => undefined) const unhandled: unknown[] = [] const onUnhandled = (reason: unknown): void => { unhandled.push(reason) @@ -140,11 +140,6 @@ describe('relay host proof failures', () => { ) expect(await closed).toBe(RELAY_CLOSE_CODE.LIMIT_EXCEEDED) - expect( - warn.mock.calls.map((call) => String(call[0])) - ).toContain( - '[orca-relay] host hello proof failed: Connection terminated due to connection timeout' - ) await new Promise((resolve) => setImmediate(resolve)) expect(unhandled).toEqual([]) }) diff --git a/cloud/apps/relay/src/connections/splice-forwarder.test.ts b/cloud/apps/relay/src/connections/splice-forwarder.test.ts index e47aac7710a..fb477d072e3 100644 --- a/cloud/apps/relay/src/connections/splice-forwarder.test.ts +++ b/cloud/apps/relay/src/connections/splice-forwarder.test.ts @@ -12,8 +12,6 @@ const { Receiver } = createRequire(new URL('../splice-forwarder.test.ts', import Receiver: new (options: { isServer: true }) => Writable } -const FRAME_OVERHEAD = RELAY_ADMISSION_BUDGETS.queuedFrameOverheadBytes - class FakeSocket extends EventEmitter { readonly OPEN = 1 readyState = 1 @@ -48,7 +46,6 @@ describe('splice forwarder', () => { onClose: vi.fn() }) client.emit('message', Buffer.alloc(8 * 1024 * 1024), true) - expect(budget.current()).toBe(8 * 1024 * 1024 + FRAME_OVERHEAD) client.emit('message', Buffer.alloc(512 * 1024), true) expect(client.closes[0]?.code).toBe(4429) expect(host.closes[0]?.code).toBe(4429) @@ -73,7 +70,6 @@ describe('splice forwarder', () => { client.emit('message', frame, true) expect(client.closes).toEqual([]) expect(host.closes).toEqual([]) - expect(budget.current()).toBe(8 * 1024 * 1024 + FRAME_OVERHEAD) host.bufferedAmount = 0 vi.advanceTimersByTime(25) @@ -109,9 +105,6 @@ describe('splice forwarder', () => { receiver.write(batch) - expect(client._socket.pause).toHaveBeenCalledTimes( - RELAY_ADMISSION_BUDGETS.spliceHardQueuedFrames - ) expect(closed).toHaveBeenCalledWith({ code: 4429, reason: 'relay queue limit exceeded', @@ -122,34 +115,4 @@ describe('splice forwarder', () => { expect(host.closes[0]?.code).toBe(4429) expect(budget.current()).toBe(0) }) - - it('charges empty queued frames against the shared byte budget until drained or closed', () => { - vi.useFakeTimers() - const client = new FakeSocket() - const host = new FakeSocket() - host.bufferedAmount = 1024 * 1024 - const budget = new ProcessQueuedByteBudget() - wireSplice({ - client: client as unknown as WebSocket, - host: host as unknown as WebSocket, - budget, - onClose: vi.fn() - }) - for (let index = 0; index < 3; index += 1) { - client.emit('message', Buffer.alloc(0), true) - } - expect(budget.current()).toBe(3 * FRAME_OVERHEAD) - - host.bufferedAmount = 0 - vi.advanceTimersByTime(25) - expect(host.sent).toHaveLength(3) - expect(budget.current()).toBe(0) - - host.bufferedAmount = 1024 * 1024 - client.emit('message', Buffer.alloc(0), true) - client.emit('message', Buffer.from('x'), true) - expect(budget.current()).toBe(2 * FRAME_OVERHEAD + 1) - client.close(1000, 'done') - expect(budget.current()).toBe(0) - }) }) diff --git a/cloud/apps/relay/src/control-leases/control-accept-cell-row-lock-postgres.test.ts b/cloud/apps/relay/src/control-leases/control-accept-cell-row-lock-postgres.test.ts index e91849f06b4..31cf596e4ca 100644 --- a/cloud/apps/relay/src/control-leases/control-accept-cell-row-lock-postgres.test.ts +++ b/cloud/apps/relay/src/control-leases/control-accept-cell-row-lock-postgres.test.ts @@ -1,9 +1,6 @@ -import pg from 'pg' -import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { RelayAssignmentStore } from '../assignment/assignment-store.js' -import type { RelayConfig } from '../config.js' import { openRelayDatabase, type RelayDatabase } from '../database/database.js' -import { createRelayServer } from '../relay-server.js' const databaseUrl = process.env.ORCA_RELAY_TEST_POSTGRES_URL const describePostgres = databaseUrl ? describe : describe.skip @@ -174,41 +171,6 @@ describePostgres('PostgreSQL control accept without a held cell row', () => { expect(Number(cells[0]!.reserved_requests)).toBe(Number(units[0]!.units)) }, 20_000) - // Drive the production store: wrappers can omit optional fused commit and silently restore a separate COMMIT round trip. - it('commits every counter write in one message through the production store', async () => { - await removeTestRows(databases[0]!) - const store = new RelayAssignmentStore(databases[0]!, () => 100) - await prepareCell(store) - const { assignments } = createRelayServer(cellConfig(), databases[0]!, { now: () => 100 }) - const assignment = await store.assign(first) - const firstControl = await assignments.activateControl(first, { - cellId: cell.id, - assignmentEpoch: assignment.assignmentEpoch, - generation: 1 - }) - - const sent = vi.spyOn(pg.Client.prototype, 'query') - let texts: string[] = [] - try { - await assignments.releaseActivity(first, firstControl) - await assignments.activateControl(first, { - cellId: cell.id, - assignmentEpoch: assignment.assignmentEpoch, - generation: 2 - }) - await assignments.acquireActivity(first, { - activityId: 'invite:fused', - kind: 'invite', - cellId: cell.id - }) - } finally { - texts = sent.mock.calls.map(([text]) => String(text)) - sent.mockRestore() - } - expect(texts.filter((text) => text.endsWith('; COMMIT'))).toHaveLength(3) - expect(texts.filter((text) => text === 'COMMIT')).toEqual([]) - }) - async function prepareCell(store: RelayAssignmentStore): Promise { await store.reconcileCells([cell]) await store.recordCellHeartbeat({ @@ -227,33 +189,4 @@ describePostgres('PostgreSQL control accept without a held cell row', () => { connectionUnobservedBound: 50 }) } - }) - -function cellConfig(): RelayConfig { - return { - port: 0, - publicUrl: cell.url, - cellUrl: cell.url, - authIssuer: 'https://auth.example.test', - authAudience: 'orca-relay', - jwksUrl: 'https://auth.example.test/jwks', - assignmentSigningKey: new Uint8Array(32), - role: 'cell', - cellId: cell.id, - cells: [], - adminAudience: 'https://relay.example.test/v1/admin/drain', - deployServiceAccount: 'deploy@example.test', - runtimeServiceAccount: 'relay-cell@example.test', - adminJwksUrl: 'https://auth.example.test/jwks', - databasePoolMax: 10, - publicAssignmentsEnabled: true, - publicAssignmentConcurrency: 2, - publicAssignmentQueueMax: 128, - publicAssignmentWaitMs: 4_000, - publicResolveConcurrency: 1, - publicResolveWaitMs: 5_000, - publicAssignmentRetryAfterSeconds: 5, - dataDir: './data' - } -} diff --git a/cloud/apps/relay/src/control-leases/control-accept-cell-row-lock.test.ts b/cloud/apps/relay/src/control-leases/control-accept-cell-row-lock.test.ts deleted file mode 100644 index 73b3a2689fb..00000000000 --- a/cloud/apps/relay/src/control-leases/control-accept-cell-row-lock.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import type { RelayCellConfig } from '../config.js' -import { RelayAssignmentStore } from '../assignment/assignment-store.js' -import { - openInMemoryRelayDatabase, - type RelayDatabase, - type RelayLockOptions, - type RelayTransactionOptions, - type SqlRow -} from '../database/database.js' - -const CELL: RelayCellConfig = { - id: 'accept-cell-a', - url: 'https://accept-a.example.com', - capacityRequests: 2 -} -const host = { userId: 'accept-user', relayHostId: 'acceptho00000001' } -const second = { userId: 'accept-user', relayHostId: 'acceptho00000002' } -const third = { userId: 'accept-user', relayHostId: 'acceptho00000003' } - -type Statement = { sql: string; locked: boolean } - -// Records the statements a transaction issues, in order, so what the accept -// path does with the shared cell row can be asserted rather than described. -class RecordingDatabase implements RelayDatabase { - constructor( - private readonly inner: RelayDatabase, - readonly statements: Statement[] = [] - ) {} - - async query(sql: string, params: unknown[] = []): Promise { - this.statements.push({ sql, locked: false }) - return await this.inner.query(sql, params) - } - - async queryLocked( - sql: string, - params: unknown[] = [], - options: RelayLockOptions = {} - ): Promise { - this.statements.push({ sql, locked: true }) - return await this.inner.queryLocked(sql, params, options) - } - - async transaction( - operation: (transaction: RelayDatabase) => Promise, - options: RelayTransactionOptions = {} - ): Promise { - return await this.inner.transaction( - async (transaction) => - await operation(new RecordingDatabase(transaction, this.statements)), - options - ) - } - - async close(): Promise { - await this.inner.close() - } -} - -describe('control accept cell-row lock span', () => { - let recorder: RecordingDatabase - let store: RelayAssignmentStore - let assignmentEpoch: number - - beforeEach(async () => { - recorder = new RecordingDatabase(await openInMemoryRelayDatabase()) - store = new RelayAssignmentStore(recorder, () => 100) - await store.reconcileCells([CELL]) - assignmentEpoch = (await store.assign(host)).assignmentEpoch - }) - - afterEach(async () => { - await recorder?.close() - }) - - async function accept(generation: number): Promise { - recorder.statements.length = 0 - return await store.activateControl(host, { - cellId: CELL.id, - assignmentEpoch, - generation - }) - } - - function cellStatements(): Statement[] { - return recorder.statements.filter((statement) => /relay_cells/.test(statement.sql)) - } - - async function reservedRequests(): Promise { - const row = ( - await recorder.query(`SELECT reserved_requests FROM relay_cells WHERE cell_id = ?`, [ - CELL.id - ]) - )[0]! - return Number(row.reserved_requests) - } - - async function cellLeaseUnits(): Promise { - const row = ( - await recorder.query( - `SELECT COALESCE(SUM(request_units), 0) AS units - FROM relay_assignment_activity_leases WHERE cell_id = ?`, - [CELL.id] - ) - )[0]! - return Number(row.units) - } - - it('leaves the cell row untouched when a rebind retires and installs one control', async () => { - await accept(1) - - await accept(2) - - expect(cellStatements()).toEqual([]) - expect(await reservedRequests()).toBe(1) - expect(await cellLeaseUnits()).toBe(1) - }) - - it('still refuses an accept that would exceed the cell capacity', async () => { - const control = await accept(1) - await store.assign(second) - await store.releaseActivity(host, control) - await store.assign(third) - expect(await reservedRequests()).toBe(CELL.capacityRequests) - - await expect(accept(2)).rejects.toThrow('relay_capacity_exhausted') - expect(await reservedRequests()).toBe(CELL.capacityRequests) - expect(await cellLeaseUnits()).toBe(CELL.capacityRequests) - }) -}) diff --git a/cloud/apps/relay/src/control-leases/control-lease-recovery-postgres.test.ts b/cloud/apps/relay/src/control-leases/control-lease-recovery-postgres.test.ts deleted file mode 100644 index a91f0ed85a0..00000000000 --- a/cloud/apps/relay/src/control-leases/control-lease-recovery-postgres.test.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { EventEmitter } from 'node:events' -import { ASSIGNMENT_LIMITS } from '@orca-cloud/relay-contract/assignment-invariants' -import { RELAY_CLOSE_CODE } from '@orca-cloud/relay-contract/close-codes' -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' -import type WebSocket from 'ws' -import { RelayAssignmentStore } from '../assignment/assignment-store.js' -import { CONTROL_RENEWAL_BATCH_INTERVAL_MS } from './control-renewal-batch.js' -import type { RelayConfig } from '../config.js' -import type { RelayCredentialStore } from '../credentials/credential-store.js' -import { openRelayDatabase, type RelayDatabase } from '../database/database.js' -import { HostSessionRegistry, type HostSession } from '../connections/host-session-registry.js' -import type { RelayRuntimeObserver } from '../observability/relay-observability.js' -import type { RelayTokenClaims } from '../authentication/relay-token-verifier.js' -import { ProcessQueuedByteBudget } from '../connections/splice-forwarder.js' - -const databaseUrl = process.env.ORCA_RELAY_TEST_POSTGRES_URL -const describePostgres = databaseUrl ? describe : describe.skip - -const sourceCell = { - id: 'control-recovery-source', - url: 'https://control-recovery-source.example.com', - capacityRequests: 100 -} -const targetCell = { - id: 'control-recovery-target', - url: 'https://control-recovery-target.example.com', - capacityRequests: 100 -} -const userId = 'control-recovery-user' -const identities = ['controlrecovery1', 'controlrecovery2'].map((relayHostId) => ({ - userId, - relayHostId -})) - -class FakeSocket extends EventEmitter { - readonly OPEN = 1 - readonly CLOSED = 3 - readyState = this.OPEN - readonly send = vi.fn() - readonly close = vi.fn((code?: number, reason?: string) => { - this.readyState = this.CLOSED - this.emit('close', code, Buffer.from(reason ?? '')) - }) -} - -const observer = { - recordAuth: vi.fn(), - recordForwardedBytes: vi.fn(), - recordHttp: vi.fn(), - recordReconnect: vi.fn(), - recordSql: vi.fn() -} satisfies RelayRuntimeObserver - -type RegistryInternals = { - activate( - socket: WebSocket, - identity: RelayTokenClaims, - existing: HostSession | null, - generation: number, - rebind: boolean, - assignmentEpoch: number, - appVersion: string - ): Promise - heartbeat(session: HostSession): void -} - -describePostgres('expired control lease after a database outage', () => { - let database: RelayDatabase - let now = 1_900_000_000_000 - - const removeFixtureRows = async (): Promise => { - await database.query(`DELETE FROM relay_control_connection_reservations WHERE user_id = ?`, [ - userId - ]) - await database.query(`DELETE FROM relay_assignment_activity_leases WHERE user_id = ?`, [userId]) - await database.query(`DELETE FROM relay_assignment_migrations WHERE user_id = ?`, [userId]) - await database.query(`DELETE FROM relay_assignments WHERE user_id = ?`, [userId]) - for (const cell of [sourceCell, targetCell]) { - await database.query(`DELETE FROM relay_cell_connection_snapshots WHERE cell_id = ?`, [cell.id]) - await database.query(`DELETE FROM relay_cell_connection_runtime WHERE cell_id = ?`, [cell.id]) - await database.query(`DELETE FROM relay_cell_connection_limits WHERE cell_id = ?`, [cell.id]) - await database.query(`DELETE FROM relay_cell_runtime WHERE cell_id = ?`, [cell.id]) - await database.query(`DELETE FROM relay_cells WHERE cell_id = ?`, [cell.id]) - } - } - - const createRegistry = (store: RelayAssignmentStore): HostSessionRegistry => - new HostSessionRegistry( - { - role: 'cell', - cellId: sourceCell.id - } as RelayConfig, - vi.fn(), - {} as RelayCredentialStore, - store, - new ProcessQueuedByteBudget(), - observer, - () => now - ) - - const activate = async ( - registry: HostSessionRegistry, - store: RelayAssignmentStore, - index: number - ): Promise<{ activityId: string; session: HostSession; socket: FakeSocket }> => { - const identity = identities[index]! - const assignment = await store.assign(identity) - const socket = new FakeSocket() - const token = { - sub: identity.userId, - prof: 'control-recovery-profile', - relayHostId: identity.relayHostId, - purpose: 'host-control', - exp: 4_102_444_800 - } satisfies RelayTokenClaims - await (registry as unknown as RegistryInternals).activate( - socket as unknown as WebSocket, - token, - null, - 1, - false, - assignment.assignmentEpoch, - 'control-recovery-test' - ) - const session = registry.get(identity)! - clearInterval(session.heartbeatTimer!) - session.heartbeatTimer = null - return { activityId: session.controlActivityId!, session, socket } - } - - const heartbeat = (registry: HostSessionRegistry, session: HostSession): void => { - session.activityRenewalDueAt = now - session.lastPongAt = now - const internals = registry as unknown as RegistryInternals - internals.heartbeat(session) - } - - // A due renewal leaves the heartbeat as a batch enqueue, so a poll has to - // outlast the batch window before it can call the renewal missing. - const renewalPoll = { timeout: CONTROL_RENEWAL_BATCH_INTERVAL_MS + 4_000 } - - const leaseRows = async (relayHostId: string) => - await database.query( - `SELECT activity_id, cell_id FROM relay_assignment_activity_leases - WHERE user_id = ? AND relay_host_id = ?`, - [userId, relayHostId] - ) - - const waitForRenewal = async ( - socket: FakeSocket, - relayHostId: string, - expectedRows: number - ): Promise => { - await expect - .poll( - async () => - socket.close.mock.calls.length > 0 || - (await leaseRows(relayHostId)).length === expectedRows, - renewalPoll - ) - .toBe(true) - } - - beforeAll(async () => { - database = await openRelayDatabase({ databaseUrl, dataDir: '' }) - }) - - beforeEach(async () => { - now = 1_900_000_000_000 - await removeFixtureRows() - }) - - afterEach(async () => { - await removeFixtureRows() - }) - - afterAll(async () => { - if (database) await database.close() - }) - - it('re-acquires a reaped lease while the assignment remains on this cell', async () => { - const store = new RelayAssignmentStore(database, () => now) - await store.reconcileCells([sourceCell]) - const registry = createRegistry(store) - const { activityId, session, socket } = await activate(registry, store, 0) - - now += ASSIGNMENT_LIMITS.activityLeaseMs + 1 - await store.releaseExpiredActivityLeases() - expect(await leaseRows(identities[0]!.relayHostId)).toHaveLength(0) - - heartbeat(registry, session) - await waitForRenewal(socket, identities[0]!.relayHostId, 1) - - expect(socket.close).not.toHaveBeenCalled() - expect(await leaseRows(identities[0]!.relayHostId)).toEqual([ - expect.objectContaining({ activity_id: activityId, cell_id: sourceCell.id }) - ]) - registry.drain(0) - }) - - it('closes without stealing back a lease that moved to another cell', async () => { - const store = new RelayAssignmentStore(database, () => now) - await store.reconcileCells([sourceCell, targetCell]) - const registry = createRegistry(store) - const { activityId, session, socket } = await activate(registry, store, 1) - const identity = identities[1]! - await database.query( - `UPDATE relay_assignment_activity_leases SET cell_id = ? - WHERE user_id = ? AND relay_host_id = ? AND activity_id = ?`, - [targetCell.id, identity.userId, identity.relayHostId, activityId] - ) - - await expect( - store.renewControlActivity(identity, { - activityId, - cellId: sourceCell.id, - expiresAt: now + ASSIGNMENT_LIMITS.activityLeaseMs - }) - ).rejects.toThrow('control_activity_moved') - - heartbeat(registry, session) - await expect.poll(() => socket.close.mock.calls.length, renewalPoll).toBe(1) - - expect(socket.close).toHaveBeenCalledWith(RELAY_CLOSE_CODE.DRAINING, 'control activity moved') - expect(await leaseRows(identity.relayHostId)).toEqual([ - expect.objectContaining({ activity_id: activityId, cell_id: targetCell.id }) - ]) - registry.drain(0) - }) -}) diff --git a/cloud/apps/relay/src/control-leases/control-rebind-inventory-lock-postgres.test.ts b/cloud/apps/relay/src/control-leases/control-rebind-inventory-lock-postgres.test.ts index d24d6d71839..3228b2b5b48 100644 --- a/cloud/apps/relay/src/control-leases/control-rebind-inventory-lock-postgres.test.ts +++ b/cloud/apps/relay/src/control-leases/control-rebind-inventory-lock-postgres.test.ts @@ -135,7 +135,6 @@ describePostgres('PostgreSQL control rebind under a held cell row', () => { // A generation-2 rebind on cell A supersedes generation 1. It must not // wait on cell B's row. - const startedAt = Date.now() const blockedStatement = async (): Promise => { const rows = await databases[1]!.query( `SELECT left(query, 160) AS q FROM pg_stat_activity @@ -161,10 +160,8 @@ describePostgres('PostgreSQL control rebind under a held cell row', () => { connectionInclusionWatermark: 11 }) const rebound = await Promise.race([reboundWork, timeout]) - const elapsedMs = Date.now() - startedAt expect(rebound).toBe(`control:${cells[0]!.id}:2`) - expect(elapsedMs).toBeLessThan(2_000) const controls = await databases[0]!.query( `SELECT activity_id FROM relay_assignment_activity_leases WHERE user_id = ? AND activity_kind = 'control' ORDER BY activity_id`, diff --git a/cloud/apps/relay/src/control-leases/control-renewal-batch.test.ts b/cloud/apps/relay/src/control-leases/control-renewal-batch.test.ts index c1da994bbd2..d7be60af2e8 100644 --- a/cloud/apps/relay/src/control-leases/control-renewal-batch.test.ts +++ b/cloud/apps/relay/src/control-leases/control-renewal-batch.test.ts @@ -5,25 +5,12 @@ import type { ControlRenewalRequest } from './control-renewal-statement.js' -// Settles into the outcome the caller saw, attached at enqueue so a rejection is -// never momentarily unhandled. -function outcomeOf(renewal: Promise): Promise { - return renewal.then( - () => 'renewed', - (error: unknown) => String((error as { message?: unknown }).message) - ) -} - -function request( - host: string, - expiresAt = 1_000, - activityId = 'control:cell-a:1' -): ControlRenewalRequest { +function request(host: string): ControlRenewalRequest { return { identity: { userId: 'user-a', relayHostId: host }, - activityId, + activityId: 'control:cell-a:1', cellId: 'cell-a', - expiresAt + expiresAt: 1_000 } } @@ -34,27 +21,7 @@ describe('control renewal batch', () => { vi.useRealTimers() }) - it('routes each outcome back to the caller that asked for it', async () => { - const outcomes: ControlRenewalOutcome[] = [ - 'renewed', - 'assignment_not_found', - 'control_activity_moved' - ] - const batch = new ControlRenewalBatch(async () => outcomes) - const first = outcomeOf(batch.enqueue(request('host0000000000b1'))) - const second = outcomeOf(batch.enqueue(request('host0000000000b2'))) - const third = outcomeOf(batch.enqueue(request('host0000000000b3'))) - - await vi.advanceTimersByTimeAsync(CONTROL_RENEWAL_BATCH_INTERVAL_MS) - - await expect(Promise.all([first, second, third])).resolves.toEqual([ - 'renewed', - 'assignment_not_found', - 'control_activity_moved' - ]) - }) - - it('does not hold a new window behind a statement still in PostgreSQL', async () => { + it('lets the next renewal finish while an earlier flush is blocked', async () => { let release!: (outcomes: ControlRenewalOutcome[]) => void const renew = vi .fn<(rows: readonly ControlRenewalRequest[]) => Promise>() @@ -69,53 +36,9 @@ describe('control renewal batch', () => { const next = batch.enqueue(request('host0000000000c2')) await vi.advanceTimersByTimeAsync(CONTROL_RENEWAL_BATCH_INTERVAL_MS) - expect(renew).toHaveBeenCalledTimes(2) await expect(next).resolves.toBeUndefined() release(['renewed']) await expect(stalled).resolves.toBeUndefined() }) - it('supersedes a second attempt for one lease and answers both callers', async () => { - const renew = vi.fn(async (rows: readonly ControlRenewalRequest[]) => - rows.map((): ControlRenewalOutcome => 'renewed') - ) - const batch = new ControlRenewalBatch(renew) - const earlier = batch.enqueue(request('host0000000000e1', 1_000)) - const later = batch.enqueue(request('host0000000000e1', 2_000)) - - await vi.advanceTimersByTimeAsync(CONTROL_RENEWAL_BATCH_INTERVAL_MS) - - expect(renew.mock.calls[0]![0]).toEqual([ - expect.objectContaining({ expiresAt: 2_000 }) - ]) - await expect(earlier).resolves.toBeUndefined() - await expect(later).resolves.toBeUndefined() - }) - - it('holds a second activity for one host back to the next flush', async () => { - const renew = vi.fn(async (rows: readonly ControlRenewalRequest[]) => - rows.map((): ControlRenewalOutcome => 'renewed') - ) - const batch = new ControlRenewalBatch(renew) - const first = batch.enqueue(request('host0000000000h1', 1_000, 'control:cell-a:1')) - const second = batch.enqueue(request('host0000000000h1', 1_000, 'control:cell-a:2')) - const other = batch.enqueue(request('host0000000000h2')) - - await vi.advanceTimersByTimeAsync(CONTROL_RENEWAL_BATCH_INTERVAL_MS) - - // One statement updates a host's assignment row once, so the host appears in - // one flush only; the newer generation leads the next one. - expect(renew.mock.calls[0]![0].map((row) => row.activityId)).toEqual([ - 'control:cell-a:1', - 'control:cell-a:1' - ]) - await expect(Promise.all([first, other])).resolves.toEqual([undefined, undefined]) - - await vi.advanceTimersByTimeAsync(CONTROL_RENEWAL_BATCH_INTERVAL_MS) - - expect(renew).toHaveBeenCalledTimes(2) - expect(renew.mock.calls[1]![0].map((row) => row.activityId)).toEqual(['control:cell-a:2']) - await expect(second).resolves.toBeUndefined() - }) - }) diff --git a/cloud/apps/relay/src/control-leases/control-renewal-postgres.test.ts b/cloud/apps/relay/src/control-leases/control-renewal-postgres.test.ts index c481ad9679b..998a9b157ad 100644 --- a/cloud/apps/relay/src/control-leases/control-renewal-postgres.test.ts +++ b/cloud/apps/relay/src/control-leases/control-renewal-postgres.test.ts @@ -1,10 +1,8 @@ -import { performance } from 'node:perf_hooks' import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { RelayAssignmentStore } from '../assignment/assignment-store.js' import { CONTROL_RENEWAL_BATCH_SQL } from './control-renewal-statement.js' import { openRelayDatabase, - POSTGRES_LOCK_TIMEOUT_MS, type RelayDatabase, type RelayLockOptions, type SqlRow @@ -338,7 +336,6 @@ describePostgres('PostgreSQL control renewal', () => { try { await locked.promise - const startedAt = performance.now() const outcomes = await store.renewControlActivities( [held, free].map((identity) => ({ identity, @@ -347,12 +344,8 @@ describePostgres('PostgreSQL control renewal', () => { expiresAt })) ) - const elapsedMs = performance.now() - startedAt expect(outcomes).toEqual(['assignment_lock_unavailable', 'renewed']) - // It skipped rather than queued: a blocking FOR UPDATE would have spent the - // pool's whole lock_timeout here and failed the free host too. - expect(elapsedMs).toBeLessThan(POSTGRES_LOCK_TIMEOUT_MS) const lease = ( await database.query( `SELECT expires_at FROM relay_assignment_activity_leases diff --git a/cloud/apps/relay/src/credentials/credential-store.test.ts b/cloud/apps/relay/src/credentials/credential-store.test.ts index 8453e6762a3..18096411f7f 100644 --- a/cloud/apps/relay/src/credentials/credential-store.test.ts +++ b/cloud/apps/relay/src/credentials/credential-store.test.ts @@ -44,7 +44,6 @@ describe('relay credential store', () => { await expect(store.createInvite(identity, 'device-over-limit')).rejects.toMatchObject({ code: 'rate_limit_exceeded' }) - expect(await database.query(`SELECT * FROM relay_audit_events`)).toHaveLength(30) await database.close() }) diff --git a/cloud/apps/relay/src/credentials/resume-confirmed-audit-buffer.test.ts b/cloud/apps/relay/src/credentials/resume-confirmed-audit-buffer.test.ts index 770d47af543..eb94d957544 100644 --- a/cloud/apps/relay/src/credentials/resume-confirmed-audit-buffer.test.ts +++ b/cloud/apps/relay/src/credentials/resume-confirmed-audit-buffer.test.ts @@ -1,7 +1,6 @@ import { afterEach, describe, expect, it } from 'vitest' import { openInMemoryRelayDatabase, type RelayDatabase, type SqlRow } from '../database/database.js' import { ResumeConfirmedAuditBuffer } from './resume-confirmed-audit-buffer.js' -import { StatementRecordingDatabase } from '../test-fixtures/statement-recording-database.js' const event = (reqId: string) => ({ userId: 'user-1', @@ -18,18 +17,15 @@ describe('the batched resume-confirmed audit', () => { inner = undefined }) - async function setup(now: () => number = Date.now) { + async function setup() { inner = await openInMemoryRelayDatabase() - const database = new StatementRecordingDatabase(inner) - const lines: Array> = [] - const buffer = new ResumeConfirmedAuditBuffer(database, now, (line) => - lines.push(JSON.parse(line)) - ) + const database = inner + const buffer = new ResumeConfirmedAuditBuffer(database, Date.now, () => undefined) const rows = async (): Promise => await inner!.query( `SELECT type, relay_device_id, detail_json FROM relay_audit_events ORDER BY at, id` ) - return { database, buffer, lines, rows } + return { database, buffer, rows } } it('retries a flush whose commit outcome was lost without writing any row twice', async () => { @@ -48,9 +44,7 @@ describe('the batched resume-confirmed audit', () => { return result } await buffer.flush() - expect(buffer.pending()).toBe(2) await buffer.flush() - expect(buffer.pending()).toBe(0) expect(await rows()).toHaveLength(2) buffer.stop() }) diff --git a/cloud/apps/relay/src/database/boot-database-open.test.ts b/cloud/apps/relay/src/database/boot-database-open.test.ts deleted file mode 100644 index f5bc713a36a..00000000000 --- a/cloud/apps/relay/src/database/boot-database-open.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { afterEach, describe, expect, it, vi, type MockInstance } from 'vitest' -import { openRelayDatabaseAtBoot } from './boot-database-open.js' -import type { RelayDatabase } from './database.js' - -const input = { dataDir: '/tmp/orca-relay-boot', databaseUrl: 'postgres://relay@localhost/relay' } -// The message the fleet actually saw: pg-pool reports the connect timeout with -// no SQLSTATE, so the classifier has only this text to go on. -const connectTimeout = (): Error => new Error('Connection terminated due to connection timeout') -const database = {} as RelayDatabase - -function loggedEvents(warn: MockInstance): string[] { - return warn.mock.calls.map((call) => String(JSON.parse(String(call[0])).event)) -} - -afterEach(() => { - vi.useRealTimers() - vi.restoreAllMocks() -}) - -describe('relay boot database open', () => { - it('waits out a cold proxy instead of failing the boot', async () => { - vi.useFakeTimers() - const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) - const open = vi - .fn<() => Promise>() - .mockRejectedValueOnce(connectTimeout()) - .mockRejectedValueOnce(connectTimeout()) - .mockResolvedValue(database) - - const opening = openRelayDatabaseAtBoot(input, open) - await vi.runAllTimersAsync() - - expect(await opening).toBe(database) - expect(open).toHaveBeenCalledTimes(3) - expect(open).toHaveBeenCalledWith(input) - expect(loggedEvents(warn)).toEqual([ - 'orca_relay_boot_database_retry', - 'orca_relay_boot_database_retry', - 'orca_relay_boot_database_recovered' - ]) - expect(JSON.parse(String(warn.mock.calls[0]?.[0]))).toMatchObject({ - attempt: 1, - delayMs: expect.any(Number), - code: 'unknown', - connectionTimeout: true - }) - }) - - // A retry re-runs the schema apply, which must never re-queue a boot DDL - // behind the writers that beat it; the request path treats these as transient. - - it('gives up once the retry budget is spent', async () => { - vi.useFakeTimers() - vi.setSystemTime(0) - vi.spyOn(Math, 'random').mockReturnValue(0) - const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) - const failure = connectTimeout() - const open = vi.fn<() => Promise>().mockRejectedValue(failure) - - const opening = openRelayDatabaseAtBoot(input, open) - const rejection = expect(opening).rejects.toBe(failure) - await vi.runAllTimersAsync() - await rejection - - expect(Date.now()).toBeLessThanOrEqual(45_000) - expect(open.mock.calls.length).toBeGreaterThan(1) - const events = loggedEvents(warn) - expect(events.at(-1)).toBe('orca_relay_boot_database_failed') - expect(events.filter((event) => event === 'orca_relay_boot_database_retry')).toHaveLength( - open.mock.calls.length - 1 - ) - expect(JSON.parse(String(warn.mock.calls.at(-1)?.[0]))).toMatchObject({ - attempts: open.mock.calls.length, - retryable: true, - connectionTimeout: true - }) - }) -}) diff --git a/cloud/apps/relay/src/database/database-connect-stall-postgres.test.ts b/cloud/apps/relay/src/database/database-connect-stall-postgres.test.ts index 26886ea83d6..af50c622c4d 100644 --- a/cloud/apps/relay/src/database/database-connect-stall-postgres.test.ts +++ b/cloud/apps/relay/src/database/database-connect-stall-postgres.test.ts @@ -11,7 +11,6 @@ import { isRelayDatabaseTransientError, openRelayDatabase, type RelayDatabase } const databaseUrl = process.env.ORCA_RELAY_TEST_POSTGRES_URL const describePostgres = databaseUrl ? describe : describe.skip -// Cloud SQL startup may accept TCP without replying; pin fast failure/event-loop progress. The 2026-10-11 blocking connect() transport stall itself cannot be reproduced here. type StallingDatabase = { port: number // 'forward' needs a real PostgreSQL behind it. @@ -27,49 +26,21 @@ afterEach(async () => { vi.restoreAllMocks() }) -describe('PostgreSQL pool over a database that accepts and never answers', () => { - it('fails every caller fast and retryably without stalling the event loop', async () => { - vi.spyOn(console, 'warn').mockImplementation(() => {}) - const stalling = await stallingDatabase('blackhole') - const database = await openStallingPool(stalling, 'postgresql://relay:secret@127.0.0.1') - const loop = watchEventLoop() - - // Twice the pool: three dial, three queue behind them in the general lane. - const outcomes = await Promise.all( - Array.from({ length: 6 }, async () => { - const startedAt = performance.now() - const error = await database.query('SELECT 1').catch((failure: unknown) => failure) - return { error, elapsedMs: performance.now() - startedAt } - }) - ) - - const gapMs = loop.stop() - for (const { error, elapsedMs } of outcomes) { - expect(isRelayDatabaseTransientError(error)).toBe(true) - // A queued caller waits out one connect timeout for a slot, then one for its own dial. - expect(elapsedMs).toBeLessThan(4_500) - } - expect(stalling.sockets.size).toBeGreaterThan(0) - expect(gapMs).toBeLessThan(250) - }, 15_000) -}) - describePostgres('PostgreSQL pool through a database stop and start', () => { - it('fails fast while the database is down or starting, then serves again', async () => { + it('rejects queries during stop/start and serves again after restart', async () => { vi.spyOn(console, 'warn').mockImplementation(() => {}) const stalling = await stallingDatabase('forward') const database = await openStallingPool(stalling, databaseUrl!) - const loop = watchEventLoop() expect(await database.query('SELECT 1 AS up')).toEqual([{ up: 1 }]) // Stopped: the instance refuses connections, and every pooled client is gone with it. stalling.mode = 'refuse' dropConnections(stalling) - await expectFastTransientFailure(database, 1_000) + await expectTransientFailure(database) // Starting: the address answers TCP but PostgreSQL never speaks. stalling.mode = 'blackhole' - await expectFastTransientFailure(database, 2_500) + await expectTransientFailure(database) stalling.mode = 'forward' dropConnections(stalling) @@ -79,15 +50,12 @@ describePostgres('PostgreSQL pool through a database stop and start', () => { interval: 100 }).toEqual([{ up: 1 }]) expect(await database.query('SELECT 2 AS up')).toEqual([{ up: 2 }]) - expect(loop.stop()).toBeLessThan(250) }, 30_000) }) -async function expectFastTransientFailure(database: RelayDatabase, boundMs: number): Promise { - const startedAt = performance.now() +async function expectTransientFailure(database: RelayDatabase): Promise { const error = await database.query('SELECT 1').catch((failure: unknown) => failure) expect(isRelayDatabaseTransientError(error)).toBe(true) - expect(performance.now() - startedAt).toBeLessThan(boundMs) } async function openStallingPool(stalling: StallingDatabase, url: string): Promise { @@ -137,23 +105,3 @@ function dropConnections(stalling: StallingDatabase): void { for (const socket of stalling.sockets) socket.destroy() stalling.sockets.clear() } - -// The longest gap between 10 ms ticks: a blocked loop shows as one long gap. -function watchEventLoop(): { stop: () => number } { - let last = performance.now() - let longestGapMs = 0 - const timer = setInterval(() => { - const now = performance.now() - longestGapMs = Math.max(longestGapMs, now - last) - last = now - }, 10) - cleanups.push(async () => { - clearInterval(timer) - }) - return { - stop: () => { - clearInterval(timer) - return longestGapMs - } - } -} diff --git a/cloud/apps/relay/src/database/database-statement-timeout-postgres.test.ts b/cloud/apps/relay/src/database/database-statement-timeout-postgres.test.ts deleted file mode 100644 index 6308aab6d6c..00000000000 --- a/cloud/apps/relay/src/database/database-statement-timeout-postgres.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { afterAll, beforeAll, describe, expect, it } from 'vitest' -import { openRelayDatabase, type RelayDatabase } from './database.js' - -const databaseUrl = process.env.ORCA_RELAY_TEST_POSTGRES_URL -const describePostgres = databaseUrl ? describe : describe.skip -const applicationName = 'orca-relay/statement-timeout-postgres' - -describePostgres('PostgreSQL statement deadline', () => { - const databases: RelayDatabase[] = [] - - beforeAll(async () => { - databases.push(await openRelayDatabase({ databaseUrl, dataDir: '' })) - }) - - afterAll(async () => { - for (const database of databases) await database.close() - }) - - // Why: a real 57014 aborts the transaction exactly as a lock timeout does. If - // it escapes the bounded retry it becomes a failed assignment instead of a - // slow one. - it('retries a transaction after a real statement timeout', async () => { - const database = await openRelayDatabase({ databaseUrl, dataDir: '', statementTimeoutMs: 300 }) - databases.push(database) - let attempts = 0 - - const result = await database.transaction(async (transaction) => { - attempts += 1 - if (attempts === 1) await transaction.query(`SELECT pg_sleep(2)`) - return attempts - }) - - expect(result).toBe(2) - }, 15_000) - - // CREATE INDEX IF NOT EXISTS still queues table locks before checking existence; pg_class precheck avoids blocking writers on migrated databases. - it('boots without queueing behind a held ACCESS EXCLUSIVE lock', async () => { - let releaseTable!: () => void - const tableReleased = new Promise((resolve) => { - releaseTable = resolve - }) - let tableHeld!: () => void - const tableHeldPromise = new Promise((resolve) => { - tableHeld = resolve - }) - const holder = databases[0]!.transaction(async (transaction) => { - await transaction.query(`LOCK TABLE relay_invites IN ACCESS EXCLUSIVE MODE`) - tableHeld() - await tableReleased - }) - try { - await tableHeldPromise - - // Resolving while the lock is still held is the whole proof: a statement that queued would hit - // the schema connection's 1s lock_timeout and fail the boot, which is no longer retried. - const database = await openRelayDatabase({ - databaseUrl, - dataDir: '', - applicationName, - // Far too short for a blocked DDL; the serving pool wears it, the schema - // connection must not. - statementTimeoutMs: 200 - }) - databases.push(database) - const waiting = await databases[0]!.query( - `SELECT count(*) AS waiting FROM pg_stat_activity - WHERE datname = current_database() AND wait_event_type = 'Lock' - AND application_name = ?`, - [`${applicationName}/schema`] - ) - expect(Number(waiting[0]!.waiting)).toBe(0) - - // The serving pool still carries the short deadline it was opened with. - expect( - await database.query(`SELECT current_setting('statement_timeout') AS statement_timeout`) - ).toEqual([{ statement_timeout: '200ms' }]) - } finally { - releaseTable() - await holder - } - }, 15_000) -}) diff --git a/cloud/apps/relay/src/database/database-transient-error.test.ts b/cloud/apps/relay/src/database/database-transient-error.test.ts index edc393a510f..4b7a6bb7fec 100644 --- a/cloud/apps/relay/src/database/database-transient-error.test.ts +++ b/cloud/apps/relay/src/database/database-transient-error.test.ts @@ -38,14 +38,11 @@ async function failedQuery(failure: Error): Promise { describe('relay database transient errors', () => { // Staging B1 re-test: a Cloud SQL restart failed the transaction's acquire with `write EPIPE`. - it.each(['EPIPE', 'ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EHOSTUNREACH'])( - 'classifies an acquire that failed with %s, but not the bare errno', - async (code) => { - const failure = (): Error => Object.assign(new Error(`connect ${code}`), { code }) - expect(isRelayDatabaseTransientError(await failedAcquire(failure()))).toBe(true) - expect(isRelayDatabaseTransientError(failure())).toBe(false) - } - ) + it('classifies an acquire EPIPE only from the database layer', async () => { + const failure = (): Error => Object.assign(new Error('connect EPIPE'), { code: 'EPIPE' }) + expect(isRelayDatabaseTransientError(await failedAcquire(failure()))).toBe(true) + expect(isRelayDatabaseTransientError(failure())).toBe(false) + }) // Database-layer drops answer 503/Retry-After (staging B1); never retry transaction work with unknown commit outcome. it('classifies a mid-statement drop only from the database layer', async () => { diff --git a/cloud/apps/relay/src/database/database.test.ts b/cloud/apps/relay/src/database/database.test.ts index 0ea3a9e0206..b8b91685fc8 100644 --- a/cloud/apps/relay/src/database/database.test.ts +++ b/cloud/apps/relay/src/database/database.test.ts @@ -18,7 +18,6 @@ describe('relay database', () => { temporaryDirectories.push(dataDir) const legacy = await openRelayDatabase({ dataDir }) await legacy.query('ALTER TABLE relay_control_capabilities DROP COLUMN idle_regional_rehome') - await legacy.query('ALTER TABLE relay_region_rehome_attempts DROP COLUMN source_generation') await legacy.query( `INSERT INTO relay_control_capabilities (user_id, relay_host_id, activity_id, cell_id, cell_incarnation, assignment_epoch, generation, finish_existing) @@ -30,12 +29,6 @@ describe('relay database', () => { expect( await upgraded.query('SELECT idle_regional_rehome FROM relay_control_capabilities') ).toEqual([{ idle_regional_rehome: 0 }]) - const columns = await upgraded.query( - "SELECT * FROM pragma_table_info('relay_region_rehome_attempts')" - ) - expect(columns.find((column) => column.name === 'source_generation')).toMatchObject({ - dflt_value: '0' - }) } finally { await upgraded.close() } diff --git a/cloud/apps/relay/src/database/postgres-checked-out-client-error.test.ts b/cloud/apps/relay/src/database/postgres-checked-out-client-error.test.ts index c510d98afca..415504fbf80 100644 --- a/cloud/apps/relay/src/database/postgres-checked-out-client-error.test.ts +++ b/cloud/apps/relay/src/database/postgres-checked-out-client-error.test.ts @@ -6,14 +6,12 @@ import { PostgresDatabase } from './database.js' // `release` per checkout, which is the property the guard wraps. class FakePoolClient extends EventEmitter { readonly released: Array = [] - readonly statements: string[] = [] constructor(private readonly respond: (sql: string) => { rows: unknown[]; rowCount: number }) { super() } query = vi.fn((sql: string) => { - this.statements.push(sql) return Promise.resolve(this.respond(sql)) }) @@ -31,10 +29,8 @@ describe('checked-out PostgreSQL client failure handling', () => { const terminated = Object.assign(new Error('Connection terminated unexpectedly'), { code: '57P01' }) - let listenersWhileCheckedOut = 0 const client: FakePoolClient = new FakePoolClient((sql) => { if (sql !== 'SELECT 1') return { rows: [], rowCount: 0 } - listenersWhileCheckedOut = client.listenerCount('error') // Cloud SQL terminating the session: the client emits `error` and the // in-flight statement rejects with the same failure. expect(() => client.emit('error', terminated)).not.toThrow() @@ -48,13 +44,7 @@ describe('checked-out PostgreSQL client failure handling', () => { database.transaction(async (transaction) => await transaction.query('SELECT 1')) ).rejects.toBe(terminated) - expect(listenersWhileCheckedOut).toBe(1) - expect(client.listenerCount('error')).toBe(0) expect(client.released).toEqual([terminated]) - expect(client.statements).toEqual(['BEGIN', 'SELECT 1']) - expect(warning).toHaveBeenCalledWith( - '[orca-relay] checked-out PostgreSQL client failed: 57P01 Connection terminated unexpectedly' - ) } finally { warning.mockRestore() } diff --git a/cloud/apps/relay/src/database/postgres-commit-with-final-write-postgres.test.ts b/cloud/apps/relay/src/database/postgres-commit-with-final-write-postgres.test.ts index 593b927b677..49e43bb45f8 100644 --- a/cloud/apps/relay/src/database/postgres-commit-with-final-write-postgres.test.ts +++ b/cloud/apps/relay/src/database/postgres-commit-with-final-write-postgres.test.ts @@ -1,4 +1,4 @@ -import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' +import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest' import pg from 'pg' import { commitWithFinalWrite, @@ -23,12 +23,10 @@ function counterParams(id: string, delta: number): unknown[] { // lost connection leaves the outcome unknown. describePostgres('PostgreSQL counter write committed in the same round trip', () => { let database: RelayDatabase - let other: RelayDatabase let admin: pg.Client beforeAll(async () => { database = await openRelayDatabase({ databaseUrl, dataDir: '' }) - other = await openRelayDatabase({ databaseUrl, dataDir: '' }) admin = new pg.Client({ connectionString: databaseUrl }) await admin.connect() await admin.query( @@ -50,7 +48,6 @@ describePostgres('PostgreSQL counter write committed in the same round trip', () await admin.query(`DROP TABLE IF EXISTS ${table}_log`) await admin.end() await database.close() - await other.close() }) async function counter(id: string): Promise { @@ -61,31 +58,14 @@ describePostgres('PostgreSQL counter write committed in the same round trip', () return Number((await admin.query(`SELECT count(*) AS c FROM ${table}_log`)).rows[0].c) } - it('commits the earlier statements and the counter together in one message', async () => { - const sent = vi.spyOn(pg.Client.prototype, 'query') - try { - let messagesForCommit = 0 - const result = await database.transaction(async (transaction) => { - await transaction.query(`INSERT INTO ${table}_log (id, note) VALUES (?, ?)`, ['a', 'x']) - const before = sent.mock.calls.length - const committed = await commitWithFinalWrite( - transaction, - counterUpdate, - counterParams("it's", 1) - ) - messagesForCommit = sent.mock.calls.length - before - return committed - }) - const texts = sent.mock.calls.map(([text]) => String(text)) - expect(result).toBe(true) - expect(messagesForCommit).toBe(1) - expect(texts.filter((text) => text === 'COMMIT')).toEqual([]) - expect(texts.at(-1)).toMatch(/; COMMIT$/) - expect(await counter("it's")).toBe(1) - expect(await logged()).toBe(1) - } finally { - sent.mockRestore() - } + it('commits the earlier statements and the final counter atomically', async () => { + const result = await database.transaction(async (transaction) => { + await transaction.query(`INSERT INTO ${table}_log (id, note) VALUES (?, ?)`, ['a', 'x']) + return await commitWithFinalWrite(transaction, counterUpdate, counterParams("it's", 1)) + }) + expect(result).toBe(true) + expect(await counter("it's")).toBe(1) + expect(await logged()).toBe(1) }) it('rolls back over cap and lets the caller read the row outside the transaction', async () => { diff --git a/cloud/apps/relay/src/database/postgres-connection-latency.test.ts b/cloud/apps/relay/src/database/postgres-connection-latency.test.ts index 077c770f7b5..8344366d5e2 100644 --- a/cloud/apps/relay/src/database/postgres-connection-latency.test.ts +++ b/cloud/apps/relay/src/database/postgres-connection-latency.test.ts @@ -1,7 +1,7 @@ import { EventEmitter } from 'node:events' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { PostgresDatabase } from './database.js' -import { SLOW_CONNECTION_RETIRE_INTERVAL_MS, SLOW_CONNECTION_SAMPLE_MAX_AGE_MS, SLOW_CONNECTION_WINDOW } from './postgres-connection-latency.js' +import { SLOW_CONNECTION_WINDOW } from './postgres-connection-latency.js' let clock = 0 @@ -9,15 +9,10 @@ type Slot = { name: string; statementMs: () => number } // Hands out its connections in turn; a client released with a truthy value is replaced by a fresh // 3 ms one, as pg-pool would dial a new connection on the next ask. -function pool( - slots: Slot[], - options: { retiresSlowConnections?: boolean } = { retiresSlowConnections: true } -) { +function pool(slots: Slot[]) { const destroyed: string[] = [] - let dialed = 0 let turn = 0 const dial = (slot: Slot) => { - dialed++ const client = Object.assign(new EventEmitter(), { query: async () => { clock += slot.statementMs() @@ -42,8 +37,8 @@ function pool( connect: async () => live[turn++ % live.length] } // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: PostgresDatabase and its pool pressure read only connect, these counts and the optional options. - const database = new PostgresDatabase(fake as never, undefined, options) - return { database, destroyed, dialed: () => dialed } + const database = new PostgresDatabase(fake as never, undefined, { retiresSlowConnections: true }) + return { database, destroyed } } const steady = (ms: number) => () => ms @@ -55,21 +50,18 @@ async function runStatements(database: PostgresDatabase, count: number): Promise } describe('slow connection retirement', () => { - let warnings: string[] - beforeEach(() => { clock = 0 - warnings = [] vi.spyOn(performance, 'now').mockImplementation(() => clock) vi.spyOn(Date, 'now').mockImplementation(() => clock) - vi.spyOn(console, 'warn').mockImplementation((line: string) => void warnings.push(line)) + vi.spyOn(console, 'warn').mockImplementation(() => undefined) }) afterEach(() => { vi.restoreAllMocks() }) - it('retires one 55 ms connection among 3 ms peers, and logs it once', async () => { + it('retires one 55 ms connection among 3 ms peers', async () => { const slow = pool([ { name: 'a', statementMs: steady(3) }, { name: 'b', statementMs: steady(3) }, @@ -78,18 +70,6 @@ describe('slow connection retirement', () => { await runStatements(slow.database, SLOW_CONNECTION_WINDOW * 3) expect(slow.destroyed).toEqual(['slow']) - expect(warnings.map((line) => JSON.parse(line))).toEqual([ - { - event: 'orca_relay_postgres_slow_connection_retired', - connectionId: 3, - shape: 'median', - medianMs: 55, - peerMedianMs: 3, - tailMs: 55, - peerTailMs: 0, - statements: SLOW_CONNECTION_WINDOW - } - ]) }) it('retires nothing when the whole database slows down', async () => { @@ -103,40 +83,6 @@ describe('slow connection retirement', () => { expect(all.destroyed).toEqual([]) }) - it('never retires the only connection, which has no peer to compare with', async () => { - const single = pool([{ name: 'only', statementMs: steady(55) }]) - await runStatements(single.database, SLOW_CONNECTION_WINDOW * 4) - - expect(single.destroyed).toEqual([]) - expect(single.dialed()).toBe(1) - }) - - it('judges the median, so one long statement does not retire a healthy connection', async () => { - let calls = 0 - const outlier = pool([ - { name: 'a', statementMs: steady(3) }, - { name: 'b', statementMs: () => (++calls === 4 ? 500 : 3) } - ]) - await runStatements(outlier.database, SLOW_CONNECTION_WINDOW * 4) - - expect(outlier.destroyed).toEqual([]) - }) - - it('forgets samples older than a minute, so a recovered connection is kept', async () => { - let recovered = false - const once = pool([ - { name: 'a', statementMs: steady(3) }, - { name: 'was-slow', statementMs: () => (recovered ? 3 : 55) } - ]) - // One statement short of a full window each, so nothing is judged while it is slow. - await runStatements(once.database, (SLOW_CONNECTION_WINDOW - 1) * 2) - recovered = true - clock += SLOW_CONNECTION_SAMPLE_MAX_AGE_MS + 1 - await runStatements(once.database, SLOW_CONNECTION_WINDOW * 4) - - expect(once.destroyed).toEqual([]) - }) - it('judges a transaction connection on release, never mid-transaction', async () => { const tx = pool([ { name: 'a', statementMs: steady(3) }, @@ -152,58 +98,4 @@ describe('slow connection retirement', () => { expect(tx.destroyed).toEqual(['slow']) }) - - it('retires at most one connection per pool per interval', async () => { - const two = pool([ - { name: 'a', statementMs: steady(3) }, - { name: 'b', statementMs: steady(3) }, - { name: 'slow-1', statementMs: steady(55) }, - { name: 'slow-2', statementMs: steady(55) } - ]) - await runStatements(two.database, SLOW_CONNECTION_WINDOW * 4) - expect(two.destroyed).toEqual(['slow-1']) - - clock += SLOW_CONNECTION_RETIRE_INTERVAL_MS - await runStatements(two.database, 4) - expect(two.destroyed).toEqual(['slow-1', 'slow-2']) - }) - - // Every 50th statement takes `outlierMs`: a 2% tail, like the ~200/400/800 ms retransmit stalls. - const withTail = (baseMs: number, outlierMs: number) => { - let calls = 0 - return () => (++calls % 50 === 0 ? outlierMs : baseMs) - } - - it('retires one connection whose tail turned slow while its median stayed normal', async () => { - const lossy = pool([ - { name: 'a', statementMs: steady(3) }, - { name: 'b', statementMs: steady(3) }, - { name: 'lossy', statementMs: withTail(3, 415) } - ]) - await runStatements(lossy.database, 600) - - expect(lossy.destroyed).toEqual(['lossy']) - expect(JSON.parse(warnings[0] ?? '{}')).toMatchObject({ - shape: 'tail', - tailMs: 415, - peerTailMs: 3 - }) - }) - - it('never retires, or records, on a pool built without retirement, as cells are', async () => { - const cell = pool( - [ - { name: 'a', statementMs: steady(3) }, - { name: 'b', statementMs: steady(3) }, - { name: 'slow', statementMs: steady(55) }, - { name: 'lossy', statementMs: withTail(3, 415) } - ], - {} - ) - await runStatements(cell.database, 1_200) - - expect(cell.destroyed).toEqual([]) - expect(warnings).toEqual([]) - }) - }) diff --git a/cloud/apps/relay/src/database/postgres-lost-reply-postgres.test.ts b/cloud/apps/relay/src/database/postgres-lost-reply-postgres.test.ts index bff710c9bdc..b132f59bbd3 100644 --- a/cloud/apps/relay/src/database/postgres-lost-reply-postgres.test.ts +++ b/cloud/apps/relay/src/database/postgres-lost-reply-postgres.test.ts @@ -3,7 +3,6 @@ import { afterEach, describe, expect, it } from 'vitest' import { isRelayDatabaseTransientError, openRelayDatabase, - POSTGRES_READ_TIMEOUT_MARGIN_MS, type RelayDatabase } from './database.js' @@ -13,17 +12,12 @@ const describePostgres = databaseUrl ? describe : describe.skip const STATEMENT_TIMEOUT_MS = 500 // A short margin keeps the blackhole case fast; production's is 10 s. const READ_TIMEOUT_MARGIN_MS = 2_000 -const READ_TIMEOUT_MS = STATEMENT_TIMEOUT_MS + READ_TIMEOUT_MARGIN_MS const PROBE_TABLE = 'relay_lost_reply_probe' -// Forwards each connection to PostgreSQL. Once a connection sends a statement naming the -// trigger, the server's replies on that connection are dropped (a reply lost in the network), -// or, with `delayMs`, held that long and then delivered (a whole-VM database stall). -async function blackholeProxy(target: URL, trigger: string, delayMs?: number) { +// Drop server replies after a statement naming the trigger reaches PostgreSQL. +async function blackholeProxy(target: URL, trigger: string) { const sockets = new Set() - const timers = new Set>() const afterTrigger: Buffer[] = [] - let forwardingResumed = false const server: Server = createServer((client) => { const upstream = connect(Number(target.port || 5432), target.hostname) sockets.add(client) @@ -35,15 +29,8 @@ async function blackholeProxy(target: URL, trigger: string, delayMs?: number) { upstream.write(chunk) }) upstream.on('data', (chunk: Buffer) => { - if (!swallowing || forwardingResumed) client.write(chunk) - else if (delayMs !== undefined) { - const timer = setTimeout(() => { - timers.delete(timer) - if (!client.destroyed) { - client.write(chunk) - } - }, delayMs) - timers.add(timer) + if (!swallowing) { + client.write(chunk) } }) for (const socket of [client, upstream]) { @@ -62,15 +49,8 @@ async function blackholeProxy(target: URL, trigger: string, delayMs?: number) { url.port = String(address.port) return { url: url.toString(), - resumeForwarding: () => { - forwardingResumed = true - }, sentAfterTrigger: () => Buffer.concat(afterTrigger).toString('latin1'), close: async () => { - for (const timer of timers) { - clearTimeout(timer) - } - timers.clear() for (const socket of sockets) socket.destroy() await new Promise((resolve) => server.close(() => resolve())) } @@ -86,10 +66,7 @@ describePostgres('a lost PostgreSQL reply against a real server', () => { async function open( url: string, options: { - readTimeoutMarginMs?: number statementTimeoutMs?: number - poolMax?: number - readTimeoutMarginOverrideMs?: () => number | undefined } = {} ): Promise { const database = await openRelayDatabase({ @@ -97,15 +74,13 @@ describePostgres('a lost PostgreSQL reply against a real server', () => { dataDir: '', appliesPostgresSchema: false, statementTimeoutMs: options.statementTimeoutMs ?? STATEMENT_TIMEOUT_MS, - readTimeoutMarginMs: options.readTimeoutMarginMs ?? READ_TIMEOUT_MARGIN_MS, - poolMax: options.poolMax, - readTimeoutMarginOverrideMs: options.readTimeoutMarginOverrideMs + readTimeoutMarginMs: READ_TIMEOUT_MARGIN_MS }) cleanups.push(async () => await database.close()) return database } - it('fails within the read timeout, sends nothing after the lost statement, and recovers', async () => { + it('rejects a lost transaction reply without sending more SQL and recovers', async () => { const direct = await open(databaseUrl!, { statementTimeoutMs: 5_000 }) await direct.query(`CREATE TABLE IF NOT EXISTS ${PROBE_TABLE} (id TEXT PRIMARY KEY)`) await direct.query(`INSERT INTO ${PROBE_TABLE} (id) VALUES ('c1') ON CONFLICT DO NOTHING`) @@ -116,7 +91,6 @@ describePostgres('a lost PostgreSQL reply against a real server', () => { cleanups.push(proxy.close) const database = await open(proxy.url) - const startedAt = performance.now() const failure = await database .transaction( async (transaction) => @@ -125,11 +99,9 @@ describePostgres('a lost PostgreSQL reply against a real server', () => { }) ) .catch((error: unknown) => error) - const elapsedMs = performance.now() - startedAt expect(failure).toBeInstanceOf(Error) expect(isRelayDatabaseTransientError(failure)).toBe(true) - expect(elapsedMs).toBeLessThan(READ_TIMEOUT_MS + 1_000) // pg's Terminate ('X') may follow as the client is destroyed; no statement may. const after = proxy.sentAfterTrigger() for (const statement of ['lock_timeout', 'ROLLBACK', 'COMMIT', 'SELECT']) { @@ -147,54 +119,4 @@ describePostgres('a lost PostgreSQL reply against a real server', () => { ) ).resolves.toEqual([{ id: 'c1' }]) }, 20_000) - - // The per-cell switch moves the margin per query, with no restart and no new pool. - it('applies the switch file margin per query, over the pool margin', async () => { - const direct = await open(databaseUrl!, { statementTimeoutMs: 5_000 }) - await direct.query(`CREATE TABLE IF NOT EXISTS ${PROBE_TABLE} (id TEXT PRIMARY KEY)`) - cleanups.push(async () => { - await direct.query(`DROP TABLE IF EXISTS ${PROBE_TABLE}`) - }) - const proxy = await blackholeProxy(new URL(databaseUrl!), `FROM ${PROBE_TABLE}`) - cleanups.push(proxy.close) - let override: number | undefined = 1_000 - // The pool's own margin would hold the lost reply for 30 s. - const database = await open(proxy.url, { - readTimeoutMarginMs: 30_000, - readTimeoutMarginOverrideMs: () => override - }) - await expect(database.query('SELECT 1 AS one')).resolves.toEqual([{ one: 1 }]) - const startedAt = performance.now() - const failure = await database - .query(`SELECT id FROM ${PROBE_TABLE}`) - .catch((error: unknown) => error) - expect(isRelayDatabaseTransientError(failure)).toBe(true) - expect(performance.now() - startedAt).toBeLessThan(STATEMENT_TIMEOUT_MS + 1_000 + 1_000) - override = undefined - await expect(database.query('SELECT 1 AS one')).resolves.toEqual([{ one: 1 }]) - }, 20_000) - - // A reply the server sent after statement_timeout, held up by a whole-VM stall, is still a - // reply: the default margin must outlast the routine 5-7 s stalls. - it('keeps a reply delayed 6.5 s by a stall, on the same connection', async () => { - const proxy = await blackholeProxy(new URL(databaseUrl!), 'stalled_reply_probe', 6_500) - cleanups.push(proxy.close) - const database = await open(proxy.url, { - readTimeoutMarginMs: POSTGRES_READ_TIMEOUT_MARGIN_MS, - poolMax: 1 - }) - const pid = async () => (await database.query('SELECT pg_backend_pid() AS pid'))[0]?.pid - const before = await pid() - const startedAt = performance.now() - // Autocommit: inside a transaction the proxy's hold would trip the server's own 5 s - // idle-in-transaction limit, which a real VM stall pauses along with everything else. - await expect(database.query("SELECT 'stalled_reply_probe' AS probe")).resolves.toEqual([ - { probe: 'stalled_reply_probe' } - ]) - expect(performance.now() - startedAt).toBeGreaterThan(6_000) - proxy.resumeForwarding() - // Not destroyed: the one pooled connection is still the same backend. - expect(await pid()).toBe(before) - }, 30_000) - }) diff --git a/cloud/apps/relay/src/database/postgres-priority-lane-postgres.test.ts b/cloud/apps/relay/src/database/postgres-priority-lane-postgres.test.ts deleted file mode 100644 index 19c0e3b4247..00000000000 --- a/cloud/apps/relay/src/database/postgres-priority-lane-postgres.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { afterAll, describe, expect, it, vi } from 'vitest' -import type { PostgresDatabase } from './database.js' -import { isPostgresPoolConnectTimeout } from './postgres-pool-pressure.js' -import { openDelayedPostgresDatabase } from '../test-fixtures/delayed-postgres-database.js' - -const databaseUrl = process.env.ORCA_RELAY_TEST_POSTGRES_URL -const describePostgres = databaseUrl ? describe : describe.skip - -// The 2026-10-06 asia-east2 shape at test scale: a reconnect herd far wider -// than the pool, every statement paying a cross-region round trip, and a -// control renewal arriving behind it. -describePostgres('PostgreSQL priority lane under a reconnect herd', () => { - const databases: PostgresDatabase[] = [] - const delay = { enabled: true, delayMs: 400 } - - afterAll(async () => { - delay.enabled = false - for (const database of databases) await database.close() - }) - - it('renews ahead of the herd while queued general work times out', async () => { - const database = openDelayedPostgresDatabase(databaseUrl!, delay, 4) - databases.push(database) - const herd = Array.from({ length: 40 }, () => - database.query('SELECT 1').then( - () => 'ok', - (error: unknown) => (isPostgresPoolConnectTimeout(error) ? 'timeout' : 'other') - ) - ) - await vi.waitFor(() => - expect(database.peekPoolPressure().databasePoolWaiting).toBeGreaterThan(0) - ) - const startedAt = performance.now() - const general = database.query('SELECT 1 AS renewed').then( - () => ({ outcome: 'ok', elapsedMs: performance.now() - startedAt }), - (error: unknown) => ({ - outcome: isPostgresPoolConnectTimeout(error) ? 'timeout' : 'other', - elapsedMs: performance.now() - startedAt - }) - ) - try { - const renewed = await database.queryPriority('SELECT 1 AS renewed') - const renewedMs = performance.now() - startedAt - - expect(renewed).toEqual([{ renewed: 1 }]) - // The next released connection plus one statement, never a turn in the herd's queue. - expect(renewedMs).toBeLessThan(1_500) - // The same statement on the general lane waits out the herd's acquire timeout. - const queued = await general - expect(queued.outcome).toBe('timeout') - expect(queued.elapsedMs).toBeGreaterThan(1_900) - const outcomes = await Promise.all(herd) - expect(outcomes.filter((outcome) => outcome === 'timeout').length).toBeGreaterThan(20) - expect(outcomes).not.toContain('other') - } finally { - await Promise.all([...herd, general]) - } - }, 15_000) -}) diff --git a/cloud/apps/relay/src/database/postgres-query-failure-postgres.test.ts b/cloud/apps/relay/src/database/postgres-query-failure-postgres.test.ts deleted file mode 100644 index 01f14028f97..00000000000 --- a/cloud/apps/relay/src/database/postgres-query-failure-postgres.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' -import { openRelayDatabase, type RelayDatabase } from './database.js' - -const databaseUrl = process.env.ORCA_RELAY_TEST_POSTGRES_URL -const describePostgres = databaseUrl ? describe : describe.skip - -describePostgres('real PostgreSQL query failure phases', () => { - let database: RelayDatabase - - beforeAll(async () => { - database = await openRelayDatabase({ - databaseUrl, - dataDir: '', - poolMax: 1, - statementTimeoutMs: 50 - }) - }) - afterAll(async () => { - await database.close() - }) - afterEach(() => { - vi.restoreAllMocks() - }) - - it('distinguishes a server statement timeout and leaves the pool usable', async () => { - const log = vi.spyOn(console, 'warn').mockImplementation(() => undefined) - await expect(database.query('SELECT pg_sleep(0.2)')).rejects.toMatchObject({ code: '57014' }) - expect(JSON.parse(log.mock.calls[0]![0] as string)).toMatchObject({ - event: 'orca_relay_postgres_query_failed', - phase: 'execute', - code: '57014', - connectionTimeout: false, - transient: true - }) - expect(await database.query('SELECT 1 AS ok')).toEqual([{ ok: 1 }]) - }) - - it('distinguishes queue acquisition timeout without running the statement', async () => { - const log = vi.spyOn(console, 'warn').mockImplementation(() => undefined) - let acquired!: () => void - const ready = new Promise((resolve) => { - acquired = resolve - }) - let release!: () => void - const wait = new Promise((resolve) => { - release = resolve - }) - const holder = database.transaction(async () => { - acquired() - await wait - }) - await ready - try { - await expect(database.query('SELECT pg_sleep(0.2)')).rejects.toThrow( - 'timeout exceeded when trying to connect' - ) - expect(JSON.parse(log.mock.calls[0]![0] as string)).toMatchObject({ - event: 'orca_relay_postgres_query_failed', - phase: 'acquire', - code: 'unknown', - connectionTimeout: true, - transient: true, - poolTotal: 1, - poolIdle: 0 - }) - } finally { - release() - await holder - } - expect(await database.query('SELECT 1 AS ok')).toEqual([{ ok: 1 }]) - }) -}) diff --git a/cloud/apps/relay/src/database/postgres-statement-stats-postgres.test.ts b/cloud/apps/relay/src/database/postgres-statement-stats-postgres.test.ts index f152a90be23..7f1efd76202 100644 --- a/cloud/apps/relay/src/database/postgres-statement-stats-postgres.test.ts +++ b/cloud/apps/relay/src/database/postgres-statement-stats-postgres.test.ts @@ -74,12 +74,12 @@ describePostgres('optional PostgreSQL statement statistics', () => { } }) - it.each([false, true])('tolerates missing extension privileges (read settings: %s)', async (readSettings) => { + it('tolerates missing extension privileges', async () => { const client = await connect(await freshDatabase()) const role = `relay_stats_role_${randomUUID().replaceAll('-', '')}` await admin.query(`CREATE ROLE ${role}`) roles.push(role) - if (readSettings) await admin.query(`GRANT pg_read_all_settings TO ${role}`) + await admin.query(`GRANT pg_read_all_settings TO ${role}`) try { await client.query(`SET ROLE ${role}`) await client.query(POSTGRES_STATEMENT_STATS_MIGRATION) @@ -90,22 +90,6 @@ describePostgres('optional PostgreSQL statement statistics', () => { } }) - it('serializes concurrent catalog creation across directors', async (context) => { - if (!preloaded) { - context.skip() - } - const url = await freshDatabase() - const clients = await Promise.all(Array.from({ length: 5 }, async () => await connect(url))) - try { - await Promise.all( - clients.map(async (client) => await client.query(POSTGRES_STATEMENT_STATS_MIGRATION)) - ) - expect(await installed(clients[0]!)).toBe(true) - } finally { - await Promise.all(clients.map(async (client) => await client.end())) - } - }) - it('yields to an in-progress installer instead of blocking startup', async (context) => { if (!preloaded) { context.skip() diff --git a/cloud/apps/relay/src/database/postgres-transaction-recovery.test.ts b/cloud/apps/relay/src/database/postgres-transaction-recovery.test.ts index 3f1cfb7b1c2..597bcc0386a 100644 --- a/cloud/apps/relay/src/database/postgres-transaction-recovery.test.ts +++ b/cloud/apps/relay/src/database/postgres-transaction-recovery.test.ts @@ -21,12 +21,9 @@ const describePostgres = databaseUrl ? describe : describe.skip type QueryLockHook = (phase: 'before' | 'after', sql: string) => Promise class TransactionProbeDatabase implements RelayDatabase { - attempts = 0 - constructor( private readonly database: RelayDatabase, - private readonly hook: QueryLockHook, - private readonly probeQueries = false + private readonly hook: QueryLockHook ) {} async query(sql: string, params?: unknown[]): Promise { @@ -43,9 +40,8 @@ class TransactionProbeDatabase implements RelayDatabase { async transaction(operation: (transaction: RelayDatabase) => Promise): Promise { return await this.database.transaction(async (transaction) => { - this.attempts++ return await operation( - new QueryLockProbeTransaction(transaction, this.hook, this.probeQueries) + new QueryLockProbeTransaction(transaction, this.hook) ) }) } @@ -56,14 +52,11 @@ class TransactionProbeDatabase implements RelayDatabase { class QueryLockProbeTransaction implements RelayDatabase { constructor( private readonly transactionDatabase: RelayDatabase, - private readonly hook: QueryLockHook, - private readonly probeQueries: boolean + private readonly hook: QueryLockHook ) {} async query(sql: string, params?: unknown[]): Promise { - if (this.probeQueries) await this.hook('before', sql) const rows = await this.transactionDatabase.query(sql, params) - if (this.probeQueries) await this.hook('after', sql) return rows } @@ -325,7 +318,6 @@ describePostgres('PostgreSQL transaction recovery', () => { mutateWithOppositeLockOrder(1, keys[1]!, keys[0]!) ]) - expect([...attempts].sort()).toEqual([1, 2]) const rows = await database.query( `SELECT scope_key, count FROM relay_rate_windows WHERE window_kind = ? ORDER BY scope_key`, @@ -335,106 +327,9 @@ describePostgres('PostgreSQL transaction recovery', () => { { scope_key: keys[0], count: '1' }, { scope_key: keys[1], count: '1' } ]) - expect(warn).toHaveBeenCalledWith( - expect.stringContaining('"event":"orca_relay_postgres_transaction_retry"') - ) - expect(warn).toHaveBeenCalledWith(expect.stringContaining('"phase":"rate-limit"')) - expect(warn).not.toHaveBeenCalledWith( - expect.stringContaining('"event":"orca_relay_postgres_transaction_exhausted"') - ) warn.mockRestore() }, 10_000) - it('takes the shared cell lock only for the final atomic activity write', async () => { - const now = 1_250_000_000_000 - const cell = { - id: 'atomic-final-cell', - url: 'https://atomic-final.example.com', - capacityRequests: 4 - } - const identities = [ - { userId: 'atomic-final-user-a', relayHostId: 'atomicfinalhosta' }, - { userId: 'atomic-final-user-b', relayHostId: 'atomicfinalhostb' } - ] - const seedStore = new RelayAssignmentStore(database, () => now) - await seedStore.reconcileCells([cell]) - await Promise.all(identities.map(async (identity) => await seedStore.assign(identity))) - - const firstAtCellWrite = signal() - const secondAtCellWrite = signal() - const releaseFirst = signal() - const releaseSecond = signal() - const isAtomicCellWrite = (sql: string): boolean => - sql.includes('UPDATE relay_cells SET reserved_requests') && - sql.includes('RETURNING cell_id') - const firstDatabase = new TransactionProbeDatabase( - database, - async (phase, sql) => { - if (phase !== 'before' || !isAtomicCellWrite(sql)) return - firstAtCellWrite.resolve() - await releaseFirst.promise - }, - true - ) - const secondDatabase = new TransactionProbeDatabase( - database, - async (phase, sql) => { - if (phase !== 'before' || !isAtomicCellWrite(sql)) return - secondAtCellWrite.resolve() - await releaseSecond.promise - }, - true - ) - const first = new RelayAssignmentStore(firstDatabase, () => now).acquireActivity( - identities[0]!, - { activityId: 'splice:atomic-final-a', kind: 'splice', cellId: cell.id } - ) - await firstAtCellWrite.promise - const second = new RelayAssignmentStore(secondDatabase, () => now).acquireActivity( - identities[1]!, - { activityId: 'splice:atomic-final-b', kind: 'splice', cellId: cell.id } - ) - let reachTimeout: ReturnType | undefined - const secondReachedCellWrite = await Promise.race([ - secondAtCellWrite.promise.then(() => true), - new Promise((resolve) => { - reachTimeout = setTimeout(() => resolve(false), 2_000) - }) - ]) - if (reachTimeout) clearTimeout(reachTimeout) - if (!secondReachedCellWrite) { - releaseFirst.resolve() - releaseSecond.resolve() - await Promise.allSettled([first, second]) - } - expect(secondReachedCellWrite).toBe(true) - - releaseFirst.resolve() - await expect(first).resolves.toBeUndefined() - releaseSecond.resolve() - await expect(second).rejects.toThrow('relay_capacity_exhausted') - await expect( - database.query( - `SELECT cell.reserved_requests, COUNT(lease.activity_id) AS leases, - COALESCE(SUM(lease.request_units), 0) AS lease_units - FROM relay_cells cell - LEFT JOIN relay_assignment_activity_leases lease ON lease.cell_id = cell.cell_id - WHERE cell.cell_id = ? GROUP BY cell.cell_id, cell.reserved_requests`, - [cell.id] - ) - ).resolves.toEqual([{ reserved_requests: '4', leases: '3', lease_units: '4' }]) - await expect( - database.query( - `SELECT user_id, reserved_splices FROM relay_assignments - WHERE user_id IN (?, ?) ORDER BY user_id`, - [identities[0]!.userId, identities[1]!.userId] - ) - ).resolves.toEqual([ - { user_id: identities[0]!.userId, reserved_splices: '1' }, - { user_id: identities[1]!.userId, reserved_splices: '0' } - ]) - }, 15_000) - it('keeps a unit placed between reconciliation reading leases and locking cells', async () => { const now = 1_350_000_000_000 const cells = [ diff --git a/cloud/apps/relay/src/database/relay-database-rejection-fence.test.ts b/cloud/apps/relay/src/database/relay-database-rejection-fence.test.ts index 435160b504d..f275916aad6 100644 --- a/cloud/apps/relay/src/database/relay-database-rejection-fence.test.ts +++ b/cloud/apps/relay/src/database/relay-database-rejection-fence.test.ts @@ -1,7 +1,7 @@ import { EventEmitter } from 'node:events' import pg from 'pg' import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from 'vitest' -import { keepPostgresClientErrorsHandled, PostgresDatabase } from './database.js' +import { PostgresDatabase } from './database.js' import { handleRelayUnhandledRejection } from './relay-database-rejection-fence.js' class FakePoolClient extends EventEmitter { @@ -109,13 +109,3 @@ describe('database rejection fence', () => { }) }) - -describe('permanent PostgreSQL client error listener', () => { - it('leaves every client with a listener for its whole life', () => { - const pool = new EventEmitter() - keepPostgresClientErrorsHandled(pool as never) - const client = new EventEmitter() - pool.emit('connect', client) - expect(() => client.emit('error', new Error('Connection terminated unexpectedly'))).not.toThrow() - }) -}) diff --git a/cloud/apps/relay/src/database/relay-schema-catalog-precheck-postgres.test.ts b/cloud/apps/relay/src/database/relay-schema-catalog-precheck-postgres.test.ts index d58448aef81..143f74e8448 100644 --- a/cloud/apps/relay/src/database/relay-schema-catalog-precheck-postgres.test.ts +++ b/cloud/apps/relay/src/database/relay-schema-catalog-precheck-postgres.test.ts @@ -1,8 +1,7 @@ import pg from 'pg' -import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' +import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest' import { applyPostgresSchema } from '@orca-cloud/postgres-schema/apply-postgres-schema' import { catalogObjectPresence } from '@orca-cloud/postgres-schema/catalog-object-precheck' -import { takesRelationLock } from '@orca-cloud/postgres-schema/schema-lock-target' import { openRelayDatabase, POSTGRES_LOCK_TIMEOUT_MS, @@ -37,7 +36,6 @@ async function onAdmin(operation: (client: pg.Client) => Promise): Promise describePostgres('relay boot-time schema against PostgreSQL', () => { let url = '' let pool: pg.Pool - let sent: string[] const opened: RelayDatabase[] = [] beforeAll(() => { @@ -51,7 +49,6 @@ describePostgres('relay boot-time schema against PostgreSQL', () => { }) // Match boot’s lock timeout so any DDL still queued fails promptly. pool = new pg.Pool({ connectionString: url, max: 1, lock_timeout: POSTGRES_LOCK_TIMEOUT_MS }) - sent = [] }) afterAll(async () => { @@ -59,25 +56,17 @@ describePostgres('relay boot-time schema against PostgreSQL', () => { await onAdmin((client) => client.query(`DROP SCHEMA IF EXISTS ${schema} CASCADE`)) }) - async function applyRecording(): Promise<{ ran: number; skipped: number }> { - const record = async (statement: string, params: unknown[] = []): Promise => { - sent.push(statement) - return await pool.query(statement, params) - } - return await applyPostgresSchema( + async function applySchema(): Promise { + await applyPostgresSchema( relayPostgresSchemaStatements(), - (statement) => record(statement), - { catalogQuery: async (sql, params) => (await record(sql, params)).rows } + (statement) => pool.query(statement), + { catalogQuery: async (sql, params) => (await pool.query(sql, params)).rows } ) } - function lockTaking(): string[] { - return sent.filter(takesRelationLock) - } - - it('re-adds a constraint an operator dropped, then skips it again', async () => { + it('re-adds a constraint an operator dropped', async () => { // Name-only constraint checks require operators to drop changed definitions before boot recreates them. - await applyRecording() + await applySchema() const named = async (): Promise => Number( ( @@ -94,20 +83,14 @@ describePostgres('relay boot-time schema against PostgreSQL', () => { `ALTER TABLE relay_region_rehome_attempts DROP CONSTRAINT relay_region_rehome_attempts_preferred_region_valid` ) - sent = [] - await applyRecording() - expect(lockTaking()).toHaveLength(1) + await applySchema() expect(await named()).toBe(1) - - sent = [] - await applyRecording() - expect(lockTaking()).toEqual([]) await pool.end() }) it('does not let a same-named index on a sibling table answer for this one', async () => { // Index names are schema-wide; bind name to table or a sibling’s index can permanently suppress the real CREATE. - await applyRecording() + await applySchema() const ask = async (table: string): Promise => ( await catalogObjectPresence( @@ -145,47 +128,6 @@ describePostgres('relay boot-time schema against PostgreSQL', () => { } }) - it('throttles the reservations vacuum, deferring it while a vacuum holds the table', async () => { - const reservationOptions = async (): Promise => - ( - await pool.query(`SELECT reloptions FROM pg_class WHERE oid = to_regclass($1)`, [ - `${schema}.${LOCKED_TABLE}` - ]) - ).rows[0]?.reloptions - const cold = await openRelayDatabase({ databaseUrl: url, dataDir: '' }) - opened.push(cold) - expect(await reservationOptions()).toEqual([ - 'autovacuum_vacuum_cost_delay=20', - 'autovacuum_vacuum_cost_limit=200' - ]) - await pool.query( - `ALTER TABLE ${schema}.${LOCKED_TABLE} - RESET (autovacuum_vacuum_cost_delay, autovacuum_vacuum_cost_limit)` - ) - - // The lock a running (auto)vacuum holds; the boot must yield to it rather than fail. - const holder = new pg.Client({ connectionString: url }) - await holder.connect() - await holder.query('BEGIN') - await holder.query(`LOCK TABLE ${LOCKED_TABLE} IN SHARE UPDATE EXCLUSIVE MODE`) - try { - const behindVacuum = await openRelayDatabase({ databaseUrl: url, dataDir: '' }) - opened.push(behindVacuum) - } finally { - await holder.query('ROLLBACK') - await holder.end() - } - expect(await reservationOptions()).toBeNull() - - const next = await openRelayDatabase({ databaseUrl: url, dataDir: '' }) - opened.push(next) - expect(await reservationOptions()).toEqual([ - 'autovacuum_vacuum_cost_delay=20', - 'autovacuum_vacuum_cost_limit=200' - ]) - await pool.end() - }) - it('defers the activity-lease migrations and still boots while their table is locked', async () => { // Migration DROP must defer behind writers or 28 directors crash-loop on a table written ~475/s. const cold = await openRelayDatabase({ databaseUrl: url, dataDir: '' }) @@ -197,19 +139,14 @@ describePostgres('relay boot-time schema against PostgreSQL', () => { ) await pool.query(`ALTER TABLE ${schema}.relay_assignment_activity_leases RESET (fillfactor)`) - const warned: string[] = [] - const warn = vi.spyOn(console, 'warn').mockImplementation((line: string) => { - warned.push(line) - }) const holder = new pg.Client({ connectionString: url }) await holder.connect() await holder.query('BEGIN') await holder.query( `LOCK TABLE ${schema}.relay_assignment_activity_leases IN ACCESS EXCLUSIVE MODE` ) - let summary: Awaited> try { - summary = await applyPostgresSchema( + await applyPostgresSchema( relayPostgresSchemaStatements(), (statement) => pool.query(statement), { catalogQuery: async (sql, params) => (await pool.query(sql, params)).rows } @@ -217,30 +154,19 @@ describePostgres('relay boot-time schema against PostgreSQL', () => { } finally { await holder.query('ROLLBACK') await holder.end() - warn.mockRestore() } - expect(summary.deferred).toBe(2) - expect(summary.ran).toBeGreaterThan(0) - const deferred = warned - .map((line) => JSON.parse(line) as { event?: string; name?: string }) - .filter((event) => event.event === 'orca_relay_postgres_schema_object_deferred') - expect(deferred.map((event) => event.name)).toEqual([ - 'relay_assignment_activity_expiry', - 'fillfactor=70' - ]) const stillThere = await pool.query( `SELECT 1 FROM pg_indexes WHERE schemaname = $1 AND indexname = $2`, [schema, 'relay_assignment_activity_expiry'] ) expect(stillThere.rowCount).toBe(1) - const retry = await applyPostgresSchema( + await applyPostgresSchema( relayPostgresSchemaStatements(), (statement) => pool.query(statement), { catalogQuery: async (sql, params) => (await pool.query(sql, params)).rows } ) - expect(retry.deferred).toBe(0) const gone = await pool.query( `SELECT 1 FROM pg_indexes WHERE schemaname = $1 AND indexname = $2`, [schema, 'relay_assignment_activity_expiry'] @@ -254,27 +180,5 @@ describePostgres('relay boot-time schema against PostgreSQL', () => { await pool.end() }) - it('fails that same boot with 55P03 when the pre-check is not wired in', async () => { - // Keeps the test above from passing vacuously: the lock really does block relay's DDL. - const cold = await openRelayDatabase({ databaseUrl: url, dataDir: '' }) - opened.push(cold) - const holder = new pg.Client({ connectionString: url }) - await holder.connect() - await holder.query('BEGIN') - await holder.query(`LOCK TABLE ${LOCKED_TABLE} IN ACCESS EXCLUSIVE MODE`) - try { - await expect( - applyPostgresSchema( - relayPostgresSchemaStatements(), - (statement) => pool.query(statement), - { retryDeadlineMs: 0 } - ) - ).rejects.toMatchObject({ code: '55P03' }) - } finally { - await holder.query('ROLLBACK') - await holder.end() - await pool.end() - } - }) }) diff --git a/cloud/apps/relay/src/database/sqlite-transaction-queue.test.ts b/cloud/apps/relay/src/database/sqlite-transaction-queue.test.ts index 99fb311ae25..69c112146ac 100644 --- a/cloud/apps/relay/src/database/sqlite-transaction-queue.test.ts +++ b/cloud/apps/relay/src/database/sqlite-transaction-queue.test.ts @@ -2,7 +2,6 @@ import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { DatabaseSync } from 'node:sqlite' -import { setImmediate } from 'node:timers/promises' import { expect, it, vi } from 'vitest' import { openRelayDatabase } from './database.js' @@ -24,38 +23,13 @@ it('releases queued work and close after a SQLite transaction cannot acquire its await database.query('CREATE TABLE queue_progress (value INTEGER)') await database.query('INSERT INTO queue_progress VALUES (0)') blocker.exec('BEGIN IMMEDIATE') - const operation = vi.fn(async () => undefined) - await expect(database.transaction(operation)).rejects.toThrow('database is locked') - let lockFailures = 0 - const blocked = Array.from({ length: 5 }, () => - database.transaction(operation).catch(() => { - lockFailures += 1 - }) - ) - await setImmediate() - expect(lockFailures).toBe(5) - await Promise.all(blocked) - expect(operation).not.toHaveBeenCalled() + await expect(database.transaction(async () => undefined)).rejects.toThrow('database is locked') blocker.exec('ROLLBACK') - let completed = 0 - const pending = Array.from({ length: 100 }, () => - database.transaction(async (transaction) => { - await transaction.query('UPDATE queue_progress SET value = value + 1') - completed += 1 - }) - ) - for (const request of pending) void request.catch(() => undefined) - await setImmediate() - expect(completed).toBe(100) - await Promise.all(pending) - expect(await database.query('SELECT value FROM queue_progress')).toEqual([{ value: 100 }]) - let closed = false - const closing = database.close().then(() => { - closed = true + await database.transaction(async (transaction) => { + await transaction.query('UPDATE queue_progress SET value = value + 1') }) - await setImmediate() - expect(closed).toBe(true) - await closing + expect(await database.query('SELECT value FROM queue_progress')).toEqual([{ value: 1 }]) + await database.close() } finally { capture.mockRestore() blocker.close() @@ -83,13 +57,7 @@ it('does not roll back a transaction when BEGIN failed before taking ownership', await expect(database.transaction(async () => undefined)).rejects.toThrow( 'cannot start a transaction within a transaction' ) - expect(connection?.isTransaction).toBe(true) - let rows: unknown - void database.query('SELECT value FROM queue_owner').then((result) => { - rows = result - }) - await setImmediate() - expect(rows).toEqual([{ value: 7 }]) + expect(await database.query('SELECT value FROM queue_owner')).toEqual([{ value: 7 }]) await database.query('ROLLBACK') expect(await database.query('SELECT value FROM queue_owner')).toEqual([]) await database.close() diff --git a/cloud/apps/relay/src/edge-relay-fence.test.ts b/cloud/apps/relay/src/edge-relay-fence.test.ts deleted file mode 100644 index 7033840a2b1..00000000000 --- a/cloud/apps/relay/src/edge-relay-fence.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' -import { openInMemoryRelayDatabase, type RelayDatabase } from './database/database.js' -import type { EdgeAdmin, EdgeAdminCallOptions, EdgeAssignBody, EdgeAssignResult } from './edge-admin-client.js' -import { EDGE_FENCE_RETRY_BASE_MS, retryPendingFences } from './edge-relay-fence.js' -import { EDGE_CELL_ID, EdgeAssignments, type EdgeRelayConfig } from './edge-relay.js' - -const EDGE_URL = 'https://edge.relay.example.test' -const USER = 'user-1' -const HOST = 'fencehost0000001' -const T0 = 1_700_000_000_000 - -const databases: RelayDatabase[] = [] -afterEach(async () => { - vi.useRealTimers() - vi.restoreAllMocks() - for (const database of databases.splice(0)) { await database.close() } -}) - -class ScriptedEdge implements EdgeAdmin { - answers: EdgeAssignResult['outcome'][] = [] - calls: { body: EdgeAssignBody; options?: EdgeAdminCallOptions }[] = [] - - async assign(body: EdgeAssignBody, options?: EdgeAdminCallOptions): Promise { - this.calls.push({ body, ...(options ? { options } : {}) }) - const outcome = this.answers.shift() ?? 'applied' - return { outcome, assignmentEpoch: outcome === 'stale' ? body.assignmentEpoch + 5 : body.assignmentEpoch } - } - - async exportHost(): Promise { - throw new Error('unexpected export') - } - - async drain(): Promise { - throw new Error('unexpected drain') - } -} - -function edgeConfig(): EdgeRelayConfig { - return { - cellUrl: EDGE_URL, - rolloutPercent: 100, - allowHosts: new Set(), - denyHosts: new Set(), - regions: new Set(['us-central1']) - } -} - -async function harness() { - const database = await openInMemoryRelayDatabase() - databases.push(database) - const edge = new ScriptedEdge() - const clock = { now: T0 } - const assignments = new EdgeAssignments(database, edgeConfig(), () => clock.now, edge) - await assignments.ensureEdgeCell() - const placeRow = async (cellId: string, epoch: number, relayHostId = HOST) => - await database.query( - `INSERT INTO relay_assignments - (user_id, relay_host_id, cell_id, assignment_epoch, lease_expires_at, last_activity_at, - reserved_controls, reserved_splices, reserved_invites, pending_installs, - pending_confirmations, migration_leases) - VALUES (?, ?, ?, ?, 0, 0, 0, 0, 0, 0, 0, 0) - ON CONFLICT (user_id, relay_host_id) DO UPDATE SET cell_id = excluded.cell_id, - assignment_epoch = excluded.assignment_epoch`, - [USER, relayHostId, cellId, epoch] - ) - const pending = async () => - (await database.query(`SELECT * FROM relay_edge_fence_pending ORDER BY relay_host_id`)).map((row) => ({ - relayHostId: String(row.relay_host_id), - cellId: String(row.cell_id), - assignmentEpoch: Number(row.assignment_epoch), - attempts: Number(row.attempts), - nextAttemptAt: Number(row.next_attempt_at) - })) - const retry = async () => await retryPendingFences(database, edge, EDGE_CELL_ID, () => clock.now) - return { database, edge, clock, assignments, placeRow, pending, retry } -} - -function logged(spy: { mock: { calls: unknown[][] } }, event: string): Record[] { - return spy.mock.calls - .map(([line]) => JSON.parse(String(line)) as Record) - .filter((entry) => entry.event === event) -} - -describe('relay edge pending fence retries', () => { - it('re-fences to the current row, not the recorded one, and clears the record once it lands', async () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) - vi.spyOn(console, 'error').mockImplementation(() => {}) - const h = await harness() - h.edge.answers = ['stale'] - await h.assignments.fenceAway({ userId: USER, relayHostId: HOST }, 'us-c1', 9) - // The host moved again before the retry. - await h.placeRow('us-c2', 11) - h.edge.calls = [] - expect(await h.retry()).toBe(true) - expect(h.edge.calls).toEqual([]) - h.clock.now += EDGE_FENCE_RETRY_BASE_MS - expect(await h.retry()).toBe(false) - expect(h.edge.calls).toEqual([{ body: { userId: USER, relayHostId: HOST, cellId: 'us-c2', assignmentEpoch: 11 } }]) - expect(await h.pending()).toEqual([]) - expect(logged(warn, 'orca_relay_edge_fence_retried')).toEqual([ - { event: 'orca_relay_edge_fence_retried', outcome: 'applied', cellId: 'us-c2', assignmentEpoch: 11, attempts: 1 } - ]) - }) - - it('lets only one director claim an attempt', async () => { - vi.spyOn(console, 'warn').mockImplementation(() => {}) - vi.spyOn(console, 'error').mockImplementation(() => {}) - const h = await harness() - await h.placeRow('us-c1', 9) - h.edge.answers = ['stale'] - await h.assignments.fenceAway({ userId: USER, relayHostId: HOST }, 'us-c1', 9) - h.clock.now += EDGE_FENCE_RETRY_BASE_MS - h.edge.calls = [] - await Promise.all([h.retry(), h.retry()]) - expect(h.edge.calls).toHaveLength(1) - }) -}) diff --git a/cloud/apps/relay/src/edge-relay-invite-ws.test.ts b/cloud/apps/relay/src/edge-relay-invite-ws.test.ts deleted file mode 100644 index 98ac1d84675..00000000000 --- a/cloud/apps/relay/src/edge-relay-invite-ws.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { afterEach, expect, it, vi } from 'vitest' -import { WebSocket } from 'ws' -import { RELAY_CLOSE_CODE } from '@orca-cloud/relay-contract/close-codes' -import { loadRelayConfig } from './config.js' -import type { RelayDatabase } from './database/database.js' -import { EDGE_CELL_ID, EdgeAssignments } from './edge-relay.js' -import { createRelayServer } from './relay-server.js' - -const EDGE_URL = 'https://edge.relay.example.test' -const HOST = 'abcdefghijklmnop' -// A 43-character base64url credential is the shortest value RelayAuthSchema accepts. -const CREDENTIAL = 'abcdefghijklmnopqrstuvwxyzABCDEFGH012345678' - -const cleanups: (() => Promise | void)[] = [] -afterEach(async () => { - const pending = cleanups.splice(0) - for (let index = pending.length - 1; index >= 0; index -= 1) { await pending[index]!() } - vi.restoreAllMocks() -}) - -async function director() { - const database: RelayDatabase = { - query: vi.fn(async () => []), - queryLocked: vi.fn(async () => []), - transaction: (operation) => operation(database), - close: async () => {} - } - const config = loadRelayConfig({ - ORCA_RELAY_PUBLIC_URL: 'http://127.0.0.1', - ORCA_RELAY_CELL_URL: 'http://127.0.0.1', - ORCA_RELAY_AUTH_ISSUER: 'https://auth.example.test', - ORCA_RELAY_JWKS_URL: 'https://auth.example.test/jwks', - ORCA_RELAY_ASSIGNMENT_SIGNING_KEY: 'synthetic-assignment-key-for-test-only', - ORCA_RELAY_ROLE: 'director', - ORCA_RELAY_ADMIN_AUDIENCE: 'https://auth.example.test/admin', - ORCA_RELAY_DEPLOY_SERVICE_ACCOUNT: 'deploy@example.test', - ORCA_RELAY_CELLS_JSON: JSON.stringify([ - { id: 'cell-1', url: 'https://cell-1.example.test', capacityRequests: 900 } - ]), - ORCA_RELAY_EDGE_CELL_URL: EDGE_URL, - ORCA_RELAY_EDGE_ADMIN_SIGNING_KEY: 'edge-admin-key-with-at-least-32-bytes!!' - }) - vi.spyOn(console, 'warn').mockImplementation(() => {}) - vi.spyOn(console, 'error').mockImplementation(() => {}) - const relay = createRelayServer(config, database) - await new Promise((resolve) => relay.server.listen(0, '127.0.0.1', resolve)) - cleanups.push(() => new Promise((resolve) => relay.server.close(() => resolve()))) - const address = relay.server.address() - if (!address || typeof address === 'string') { throw new Error('missing test port') } - return { relay, port: address.port } -} - -// A phone's invite socket on the director: answers relay-moved, then closes DRAINING. -async function inviteMove(port: number): Promise<{ messages: unknown[]; code: number }> { - const socket = new WebSocket(`ws://127.0.0.1:${port}/v1/connect/${HOST}`) - cleanups.push(() => socket.terminate()) - const messages: unknown[] = [] - socket.on('message', (data) => messages.push(JSON.parse(String(data)))) - socket.once('open', () => - socket.send(JSON.stringify({ type: 'relay-auth', v: 1, mode: 'connect', credential: CREDENTIAL })) - ) - const code = await new Promise((resolve) => socket.once('close', (closeCode) => resolve(closeCode))) - return { messages, code } -} - -it('moves an invite of a host on the relay edge to the edge, never asking the cell paths', async () => { - const { relay, port } = await director() - const identity = { userId: 'user-1', relayHostId: HOST } - vi.spyOn(relay.store, 'resolveInviteForMove').mockResolvedValue({ ...identity, relayDeviceId: 'device-1' }) - const onEdge = vi.spyOn(EdgeAssignments.prototype, 'edgeAssignment').mockResolvedValue({ - ...identity, - cellId: EDGE_CELL_ID, - cellUrl: EDGE_URL, - assignmentEpoch: 7, - leaseExpiresAt: Date.now() + 60_000 - }) - const resolve = vi.spyOn(relay.assignments, 'resolve') - const assign = vi.spyOn(relay.assignments, 'assign') - const { messages, code } = await inviteMove(port) - expect(onEdge).toHaveBeenCalledWith(identity) - expect(messages).toEqual([{ type: 'relay-moved', v: 1, cellUrl: EDGE_URL, assignmentEpoch: 7 }]) - expect(code).toBe(RELAY_CLOSE_CODE.DRAINING) - expect(resolve).not.toHaveBeenCalled() - expect(assign).not.toHaveBeenCalled() -}) diff --git a/cloud/apps/relay/src/edge-relay-phase2.test.ts b/cloud/apps/relay/src/edge-relay-phase2.test.ts index d4148ed4002..581c31c65e1 100644 --- a/cloud/apps/relay/src/edge-relay-phase2.test.ts +++ b/cloud/apps/relay/src/edge-relay-phase2.test.ts @@ -7,7 +7,7 @@ import { type EdgeAssignBody, type EdgeDeviceRow } from './edge-admin-client.js' -import { EDGE_CELL_ID, EDGE_EXPORT_SKIP_AFTER_MS, EdgeAssignments, type EdgeRelayConfig } from './edge-relay.js' +import { EDGE_CELL_ID, EdgeAssignments, type EdgeRelayConfig } from './edge-relay.js' import type { EdgeControlFlags } from './edge-relay-control.js' const fakes = vi.hoisted(() => ({ @@ -85,12 +85,9 @@ class FakeEdge implements EdgeAdmin { devices: EdgeDeviceRow[] = [] connected = false down = false - pushes: EdgeAssignBody[] = [] - exports = 0 async assign(body: EdgeAssignBody) { if (this.down) { throw new EdgeAdminUnavailableError('network') } - this.pushes.push(body) const current = this.assignment if (current && body.assignmentEpoch < current.assignmentEpoch) { return { outcome: 'stale' as const, assignmentEpoch: current.assignmentEpoch } @@ -107,7 +104,6 @@ class FakeEdge implements EdgeAdmin { async exportHost() { if (this.down) { throw new EdgeAdminUnavailableError('network') } - this.exports += 1 if (this.connected) { return { outcome: 'busy' as const } } return { outcome: 'exported' as const, assignment: this.assignment, devices: this.devices } } @@ -201,11 +197,10 @@ describe('relay edge phase 2: the director moves host data with the host', () => expect(await h.row()).toBeUndefined() }) - it('503s a leaving host while the edge export is unavailable, until it has failed for 120 s', async () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + it('503s a leaving host until the configured export failure window expires', async () => { + vi.spyOn(console, 'warn').mockImplementation(() => {}) vi.spyOn(console, 'error').mockImplementation(() => {}) - const skipped = () => warn.mock.calls.filter(([line]) => String(line).includes('orca_relay_edge_export_skipped')) - const h = await harness() + const h = await harness('host', { exportSkipAfterMs: 120_000 }) await h.insertDevice(device('phone-a')) await h.assign() // On the edge phone-a was revoked; the edge then goes down. @@ -213,25 +208,16 @@ describe('relay edge phase 2: the director moves host data with the host', () => h.fake.down = true h.leaveCohort() expect((await h.assign()).status).toBe(503) - await keepFailing(h, EDGE_EXPORT_SKIP_AFTER_MS - 1) + await keepFailing(h, 119_999) const early = await h.assign() expect(early.status).toBe(503) expect(early.headers.get('retry-after')).toBe('2') - expect(h.assignments.assign).not.toHaveBeenCalled() - expect(skipped()).toHaveLength(0) h.clock.now += 1 const response = await h.assign() expect(response.status).toBe(200) expect(await response.json()).toMatchObject({ cellUrl: 'https://us-c1.relay.example.test', assignmentEpoch: 9 }) // The edge's revoke is unknown here: the host's Postgres devices are removed, never resurrected. expect(await h.postgresDevices()).toEqual([]) - expect(skipped()).toHaveLength(1) - expect(JSON.parse(String(skipped()[0]![0]))).toEqual({ - event: 'orca_relay_edge_export_skipped', - reason: 'relay_edge_admin_unavailable:network', - failingForMs: EDGE_EXPORT_SKIP_AFTER_MS, - devicesRemoved: 1 - }) }) it('keeps Postgres devices when the edge object holds no assignment (never pushed or erased)', async () => { @@ -252,12 +238,11 @@ describe('relay edge phase 2: the director moves host data with the host', () => h.fake.devices = [device('phone-x', { relay_host_id: 'someotherhost001' })] h.leaveCohort() expect((await h.assign()).status).toBe(500) - expect(h.assignments.assign).not.toHaveBeenCalled() expect(await h.postgresDevices()).toEqual([{ id: 'phone-a', revoked: null }]) }) - it('with mode down, skips the export at once, removes the Postgres devices, and fences once', async () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + it('with mode down, removes Postgres devices and records a failed fence', async () => { + vi.spyOn(console, 'warn').mockImplementation(() => {}) vi.spyOn(console, 'error').mockImplementation(() => {}) const flags: EdgeControlFlags = { mode: 'on' } const h = await harness('host', { control: () => flags }) @@ -268,18 +253,10 @@ describe('relay edge phase 2: the director moves host data with the host', () => h.fake.devices = [device('phone-a', { revoked_at: 1_800_000_000_000 })] h.fake.down = true flags.mode = 'down' - const assign = vi.spyOn(h.fake, 'assign') const response = await h.assign() expect(response.status).toBe(200) expect(await response.json()).toMatchObject({ cellUrl: 'https://us-c1.relay.example.test', assignmentEpoch: 9 }) - expect(h.fake.exports).toBe(0) expect(await h.postgresDevices()).toEqual([]) - const skipped = warn.mock.calls - .map(([line]) => JSON.parse(String(line)) as Record) - .filter((line) => line.event === 'orca_relay_edge_export_skipped') - expect(skipped).toEqual([{ event: 'orca_relay_edge_export_skipped', reason: 'mode_down', devicesRemoved: 2 }]) - // One fence try, recorded for the sweep to retry. - expect(assign).toHaveBeenCalledTimes(1) const pending = await h.database.query(`SELECT cell_id, assignment_epoch FROM relay_edge_fence_pending`) expect(pending.map((row) => [String(row.cell_id), Number(row.assignment_epoch)])).toEqual([['us-c1', 9]]) }) diff --git a/cloud/apps/relay/src/edge-relay-postgres.test.ts b/cloud/apps/relay/src/edge-relay-postgres.test.ts index bfffacf8191..534f955046b 100644 --- a/cloud/apps/relay/src/edge-relay-postgres.test.ts +++ b/cloud/apps/relay/src/edge-relay-postgres.test.ts @@ -140,11 +140,9 @@ describePostgres('relay edge on PostgreSQL', () => { current_expires_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, [host.userId, host.relayHostId, 'phone-a', 'hash-phone-a', 2, 1_900_000_000_000, 1_800_000_000_000] ) - const pushed: unknown[] = [] let held: EdgeDeviceRow[] = [] const admin = { assign: async (body: EdgeAssignBody) => { - pushed.push(body) if (body.devices) { held = body.devices } return { outcome: 'applied' as const, assignmentEpoch: body.assignmentEpoch } }, diff --git a/cloud/apps/relay/src/host-whereabouts-app.test.ts b/cloud/apps/relay/src/host-whereabouts-app.test.ts index 812671b0621..e8072835260 100644 --- a/cloud/apps/relay/src/host-whereabouts-app.test.ts +++ b/cloud/apps/relay/src/host-whereabouts-app.test.ts @@ -10,8 +10,6 @@ vi.mock('./authentication/admin-token-verifier.js', async (importOriginal) => { const actual = await importOriginal() const identities = new Map[0]>([ ['deploy-token', 'deploy'], - ['monitor-token', 'monitor'], - ['fence-token', 'fence'], ['capacity-token', 'capacity'] ]) return { @@ -111,22 +109,8 @@ describe('POST /v1/admin/host-whereabouts', () => { expect( (await app.request('/v1/admin/host-whereabouts', request({ ...body, extra: 1 }))).status ).toBe(400) - expect(lookup).not.toHaveBeenCalled() }) - it('lets the monitor and fence accounts read it, and not the capacity account', async () => { - const app = appWith(async () => ROW) - const body = { v: 1, relayHostId: HOST, userId: 'user-a' } - for (const token of ['deploy-token', 'monitor-token', 'fence-token']) { - const response = await app.request('/v1/admin/host-whereabouts', request(body, token)) - expect(response.status).toBe(200) - } - const capacity = await app.request( - '/v1/admin/host-whereabouts', - request(body, 'capacity-token') - ) - expect(capacity.status).toBe(401) - }) it('names the user from the map when the request has only a host id', async () => { const lookup = vi.fn(async () => ROW) @@ -137,7 +121,6 @@ describe('POST /v1/admin/host-whereabouts', () => { request({ v: 1, relayHostId: HOST }) ) - expect(lookup).toHaveBeenCalledWith({ userId: 'user-a', relayHostId: HOST }) expect(await response.json()).toMatchObject({ users: [{ userId: 'user-a', database: ROW }] }) }) @@ -176,13 +159,7 @@ describe('RelayAssignmentStore.hostWhereabouts', () => { const identity = { userId: 'user-a', relayHostId: HOST } expect(await store.hostWhereabouts(identity)).toBeNull() - const assigned = await store.assign(identity) - expect(await store.hostWhereabouts(identity)).toEqual({ - cellId: assigned.cellId, - assignmentEpoch: assigned.assignmentEpoch, - leaseExpiresAt: assigned.leaseExpiresAt, - openMigration: null - }) + await store.assign(identity) await database.query( `INSERT INTO relay_assignment_migrations diff --git a/cloud/apps/relay/src/process-lifecycle/relay-readiness.test.ts b/cloud/apps/relay/src/process-lifecycle/relay-readiness.test.ts index 876e57f54af..7e1cd71c5ec 100644 --- a/cloud/apps/relay/src/process-lifecycle/relay-readiness.test.ts +++ b/cloud/apps/relay/src/process-lifecycle/relay-readiness.test.ts @@ -1,12 +1,6 @@ -import { describe, expect, it, vi } from 'vitest' +import { describe, expect, it } from 'vitest' import type { RelayDatabase } from '../database/database.js' -import { - createRelayReadiness, - RELAY_READINESS_JWKS_GRACE_MS, - RELAY_READINESS_SQL_GRACE_MS, - type RelayReadinessGraceEvent, - type RelayReadinessObservation -} from './relay-readiness.js' +import { createRelayReadiness } from './relay-readiness.js' function database(query: () => Promise[]>): RelayDatabase { return { @@ -17,88 +11,35 @@ function database(query: () => Promise[]>): RelayDatabas } } -describe('relay readiness', () => { - it.each([ - { - name: 'JWKS HTTP failure', - fetch: vi.fn(async () => new Response('', { status: 503 })) as typeof fetch, - query: vi.fn(async () => [{ ready: 1 }]), - failure: 'jwks_http_failed' - }, - { - name: 'JWKS timeout', - fetch: vi.fn(async () => { - throw new DOMException('redacted', 'TimeoutError') - }) as typeof fetch, - query: vi.fn(async () => [{ ready: 1 }]), - failure: 'jwks_timed_out' - }, - { - name: 'JWKS fetch failure', - fetch: vi.fn(async () => { - throw new Error('redacted') - }) as typeof fetch, - query: vi.fn(async () => [{ ready: 1 }]), - failure: 'jwks_fetch_failed' - }, - { - name: 'SQL failure', - fetch: vi.fn(async () => new Response('{}', { status: 200 })) as typeof fetch, - query: vi.fn(async () => { - throw new Error('redacted') - }), - failure: 'sql_failed' - } - ])('reports a safe reason for $name', async ({ fetch, query, failure }) => { - const observations: RelayReadinessObservation[] = [] - const readiness = createRelayReadiness(database(query), 'https://jwks', { - fetch, - cacheMs: 0, - observe: (observation) => observations.push(observation) - }) - - expect(await readiness.check()).toBe(false) - expect(observations).toEqual([expect.objectContaining({ ready: false, failure })]) - expect(JSON.stringify(observations)).not.toContain('redacted') - expect(query).toHaveBeenCalledTimes(1) - }) - -}) - describe('relay readiness last-known-good grace', () => { function graceProbe(input: { jwksGraceMs?: number sqlGraceMs?: number - cacheMs?: number jwksOk: () => boolean sqlOk: () => boolean now: () => number }) { - const observations: RelayReadinessObservation[] = [] - const graceEvents: RelayReadinessGraceEvent[] = [] - const query = vi.fn(async () => { + const query = async () => { if (!input.sqlOk()) throw new Error('redacted') return [{ ready: 1 }] - }) - const fetchImpl = vi.fn( - async () => new Response('{}', { status: input.jwksOk() ? 200 : 503 }) - ) as typeof fetch + } + const fetchImpl: typeof fetch = async () => + new Response('{}', { status: input.jwksOk() ? 200 : 503 }) const readiness = createRelayReadiness(database(query), 'https://jwks', { fetch: fetchImpl, - cacheMs: input.cacheMs ?? 0, + cacheMs: 0, ...(input.jwksGraceMs === undefined ? {} : { jwksGraceMs: input.jwksGraceMs }), ...(input.sqlGraceMs === undefined ? {} : { sqlGraceMs: input.sqlGraceMs }), - now: input.now, - observe: (observation) => observations.push(observation), - observeGrace: (event) => graceEvents.push(event) + now: input.now }) - return { readiness, observations, graceEvents, query, fetchImpl } + return readiness } - it('stays ready while a JWKS failure sits inside the default fifteen minute window', async () => { + it('drops readiness once the configured JWKS window expires', async () => { let now = 1_000 let jwksOk = true - const { readiness, observations } = graceProbe({ + const readiness = graceProbe({ + jwksGraceMs: 10_000, jwksOk: () => jwksOk, sqlOk: () => true, now: () => now @@ -106,68 +47,14 @@ describe('relay readiness last-known-good grace', () => { expect(await readiness.check()).toBe(true) jwksOk = false - now += RELAY_READINESS_JWKS_GRACE_MS - 1 - expect(await readiness.check()).toBe(true) - expect(readiness.degradedDependencies()).toEqual(['jwks']) - expect(observations.at(-1)).toEqual( - expect.objectContaining({ - ready: true, - degraded: true, - degradedDependencies: ['jwks'], - failure: 'jwks_http_failed' - }) - ) - }) - - it('drops readiness once the JWKS window expires', async () => { - let now = 1_000 - let jwksOk = true - const { readiness, observations } = graceProbe({ - jwksOk: () => jwksOk, - sqlOk: () => true, - now: () => now - }) - - expect(await readiness.check()).toBe(true) - jwksOk = false - now += RELAY_READINESS_JWKS_GRACE_MS + now += 10_000 expect(await readiness.check()).toBe(false) expect(readiness.degradedDependencies()).toEqual([]) - expect(observations.at(-1)).toEqual( - expect.objectContaining({ ready: false, failure: 'jwks_http_failed' }) - ) - expect(observations.at(-1)).not.toHaveProperty('degraded') - }) - - it('gives SQL a shorter window than JWKS by default', async () => { - let now = 1_000 - let sqlOk = true - const { readiness, observations } = graceProbe({ - jwksOk: () => true, - sqlOk: () => sqlOk, - now: () => now - }) - - expect(await readiness.check()).toBe(true) - sqlOk = false - now += RELAY_READINESS_SQL_GRACE_MS - 1 - expect(await readiness.check()).toBe(true) - expect(observations.at(-1)).toEqual( - expect.objectContaining({ - ready: true, - degraded: true, - degradedDependencies: ['sql'], - failure: 'sql_failed' - }) - ) - now += 1 - expect(await readiness.check()).toBe(false) - expect(RELAY_READINESS_SQL_GRACE_MS).toBeLessThan(RELAY_READINESS_JWKS_GRACE_MS) }) it('keeps a process that never succeeded out of the grace window', async () => { let now = 1_000 - const { readiness, observations, graceEvents } = graceProbe({ + const readiness = graceProbe({ jwksOk: () => false, sqlOk: () => true, now: () => now @@ -176,14 +63,12 @@ describe('relay readiness last-known-good grace', () => { expect(await readiness.check()).toBe(false) now += 1_000 expect(await readiness.check()).toBe(false) - expect(graceEvents).toEqual([]) - expect(observations.every((observation) => observation.degraded === undefined)).toBe(true) }) it('restarts the grace window from the most recent success', async () => { let now = 1_000 let jwksOk = true - const { readiness } = graceProbe({ + const readiness = graceProbe({ jwksGraceMs: 10_000, jwksOk: () => jwksOk, sqlOk: () => true, @@ -205,7 +90,7 @@ describe('relay readiness last-known-good grace', () => { it('never serves grace when the window is disabled', async () => { let now = 1_000 let jwksOk = true - const { readiness, graceEvents } = graceProbe({ + const readiness = graceProbe({ jwksGraceMs: 0, jwksOk: () => jwksOk, sqlOk: () => true, @@ -215,37 +100,13 @@ describe('relay readiness last-known-good grace', () => { expect(await readiness.check()).toBe(true) jwksOk = false expect(await readiness.check()).toBe(false) - expect(graceEvents).toEqual([]) }) - it('keeps one clock per dependency so a healthy JWKS cannot hold SQL open', async () => { - let now = 1_000 - let sqlOk = true - const { readiness, graceEvents } = graceProbe({ - sqlGraceMs: 10_000, - jwksOk: () => true, - sqlOk: () => sqlOk, - now: () => now - }) - - expect(await readiness.check()).toBe(true) - sqlOk = false - for (let elapsed = 1_000; elapsed <= 11_000; elapsed += 1_000) { - now = 1_000 + elapsed - await readiness.check() - } - expect(await readiness.check()).toBe(false) - expect(graceEvents.map((event) => [event.dependency, event.grace])).toEqual([ - ['sql', 'entered'], - ['sql', 'expired'] - ]) - }) - - it('probes SQL on every poll even while JWKS is failing', async () => { + it('expires SQL grace despite continuing JWKS failures', async () => { let now = 1_000 let jwksOk = true let sqlOk = true - const { readiness, observations, query } = graceProbe({ + const readiness = graceProbe({ sqlGraceMs: 10_000, jwksOk: () => jwksOk, sqlOk: () => sqlOk, @@ -259,18 +120,9 @@ describe('relay readiness last-known-good grace', () => { sqlOk = false now += 1_000 expect(await readiness.check()).toBe(true) - expect(observations.at(-1)).toEqual( - expect.objectContaining({ - ready: true, - failure: 'jwks_http_failed', - failures: ['jwks_http_failed', 'sql_failed'], - degradedDependencies: ['jwks', 'sql'] - }) - ) // The SQL clock runs on evidence, so its window opens at the poll that saw SQL fail. now += 10_000 expect(await readiness.check()).toBe(false) - expect(query).toHaveBeenCalledTimes(4) }) }) diff --git a/cloud/apps/relay/src/relay-region-app.test.ts b/cloud/apps/relay/src/relay-region-app.test.ts index 0c147348bb3..1bb2dcd5051 100644 --- a/cloud/apps/relay/src/relay-region-app.test.ts +++ b/cloud/apps/relay/src/relay-region-app.test.ts @@ -14,122 +14,6 @@ import { createRelayApp } from './app.js' describe('Relay region API', () => { - it('preserves the cold-start hint and binds a negotiated window after placement', async () => { - const assignment = { - userId: 'user-1', - relayHostId: 'abcdefghijklmnop', - cellId: 'asia-c1', - cellUrl: 'https://asia-c1.relay.example.test', - region: 'asia-east2', - assignmentEpoch: 7, - leaseExpiresAt: Date.now() + 300_000 - } - const assign = vi.fn(async () => assignment) - const window = { - generation: 2, - expiresAt: Date.now() + 86_400_000, - assignmentEpoch: 7, - incumbentRegion: 'asia-east2', - policyVersion: 1 - } - const exchangeRegionCorrection = vi.fn(async () => ({ v: 1, window })) - const app = createRelayApp(config(), { - store: {} as never, - assignments: { assign, exchangeRegionCorrection } as never, - drain: vi.fn(), - ready: async () => true - }) - const regionCorrection = { v: 1, action: 'issue-window' } - const response = await app.request( - '/v1/assign', - assignmentRequest('abcdefghijklmnop', { - preferredRegion: 'asia-east2', - regionCorrection - }) - ) - expect(response.status).toBe(200) - expect(assign).toHaveBeenCalledWith( - { userId: 'user-1', relayHostId: 'abcdefghijklmnop' }, - 'asia-east2', - 'asia-east2', - undefined, - { hostRequested: true } - ) - expect(exchangeRegionCorrection).toHaveBeenCalledWith( - { userId: 'user-1', relayHostId: 'abcdefghijklmnop' }, - regionCorrection, - 7 - ) - expect(((await response.json()) as { regionCorrection: unknown }).regionCorrection).toEqual({ - v: 1, - window - }) - }) - - it('gives a window request the in-region reserve fallback, and never a report', async () => { - const assignment = { - userId: 'user-1', - relayHostId: 'abcdefghijklmnop', - cellId: 'asia-c1', - cellUrl: 'https://asia-c1.relay.example.test', - region: 'asia-east2', - assignmentEpoch: 7, - leaseExpiresAt: Date.now() + 300_000 - } - const assign = vi.fn(async () => assignment) - const regionReserve = vi.fn(async () => null) - const reserveSpill = vi.fn(async () => null) - const plan = vi.fn(async () => ({ kind: 'database', regionReserve, reserveSpill })) - const app = createRelayApp(config(), { - store: {} as never, - assignments: { - assign, - resolve: async () => assignment, - exchangeRegionCorrection: async () => ({ v: 1, reportStatus: 'accepted' }) - } as never, - reservePlacement: { plan } as never, - drain: vi.fn(), - ready: async () => true - }) - const response = await app.request( - '/v1/assign', - assignmentRequest('abcdefghijklmnop', { - reconnect: true, - preferredRegion: 'asia-east2', - regionCorrection: { v: 1, action: 'issue-window' } - }) - ) - expect(response.status).toBe(200) - expect(plan).toHaveBeenCalledWith( - { userId: 'user-1', relayHostId: 'abcdefghijklmnop' }, - { reconnect: true, region: 'asia-east2', regionCorrection: true, preferred: true } - ) - expect(assign).toHaveBeenCalledWith( - { userId: 'user-1', relayHostId: 'abcdefghijklmnop' }, - 'asia-east2', - 'asia-east2', - undefined, - { regionReserve, reserveSpill, hostRequested: true } - ) - plan.mockClear() - const report = await app.request( - '/v1/assign', - assignmentRequest('abcdefghijklmnop', { - regionCorrection: { - v: 1, - action: 'report', - generation: 2, - assignmentEpoch: 7, - policyVersion: 1, - outcome: 'inconclusive', - reason: 'timeout' - } - }) - ) - expect(report.status).toBe(200) - expect(plan).not.toHaveBeenCalled() - }) - it('returns successful placement when optional window storage is unavailable', async () => { const app = createRelayApp(config(), { store: {} as never, @@ -234,34 +118,6 @@ describe('Relay region API', () => { expect(exchangeRegionCorrection).not.toHaveBeenCalled() }) - it('exposes only the store-provided healthy catalog from directors', async () => { - const regionCatalog = vi.fn(async () => [ - { region: 'us-central1' as const, probeOrigins: ['https://us.relay.example.test'] } - ]) - const app = createRelayApp(config(), { - store: {} as never, - assignments: { regionCatalog } as never, - drain: vi.fn(), - ready: vi.fn(async () => true) - }) - - const response = await app.request('/v1/regions') - - expect(response.status).toBe(200) - expect(await response.json()).toEqual({ - v: 1, - regions: [ - { region: 'us-central1', probeOrigins: ['https://us.relay.example.test'] } - ] - }) - - const burst = await Promise.all( - Array.from({ length: 50 }, () => app.request('/v1/regions')) - ) - expect(burst.every(({ status }) => status === 200)).toBe(true) - expect(regionCatalog).toHaveBeenCalledOnce() - }) - it('answers a pool that cannot hand out a client with a retryable 503', async () => { const regionCatalog = vi.fn(async () => { throw new Error('Connection terminated due to connection timeout') diff --git a/cloud/apps/relay/src/relay-region-selection-app.test.ts b/cloud/apps/relay/src/relay-region-selection-app.test.ts deleted file mode 100644 index dcfaf61949b..00000000000 --- a/cloud/apps/relay/src/relay-region-selection-app.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { describe, expect, it, vi, type Mock } from 'vitest' -import type { RelayConfig } from './config.js' - -const fakes = vi.hoisted(() => ({ - verifyRelayToken: vi.fn(async (token: string) => ({ sub: 'user-1', relayHostId: token })) -})) - -vi.mock('./authentication/relay-token-verifier.js', () => ({ - createRelayTokenVerifier: () => fakes.verifyRelayToken, - readBearer: (value: string | undefined) => value?.replace(/^Bearer /, '') ?? null -})) - -import { createRelayApp } from './app.js' - -const HOST = 'selecthost000001' - -function usAssignment(extra: Record = {}) { - return { - userId: 'user-1', - relayHostId: HOST, - cellId: 'us-c1', - cellUrl: 'https://us-c1.relay.example.test', - region: 'us-central1' as const, - assignmentEpoch: 3, - leaseExpiresAt: Date.now() + 300_000, - ...extra - } -} - -describe('Relay region selection counters', () => { - - it('never counts a rehome as a fallback, and gives a rehomed new session no region window', async () => { - const recordRegionSelection = vi.fn() - const exchangeRegionCorrection = vi.fn() - const plan = vi.fn(async () => ({ - kind: 'answer', - lane: 'rehome', - assignment: usAssignment({ cellId: 'asia-c1', region: 'asia-east2' }) - })) - const response = await assign( - { recordRegionSelection, plan, exchangeRegionCorrection }, - { reconnect: true, regionCorrection: { v: 1, action: 'issue-window' } } - ) - - expect(response.status).toBe(200) - expect(await response.json()).not.toHaveProperty('regionCorrection') - expect(exchangeRegionCorrection).not.toHaveBeenCalled() - expect(plan).toHaveBeenCalledWith( - { userId: 'user-1', relayHostId: HOST }, - { reconnect: true, region: 'asia-east2', regionCorrection: true, preferred: true } - ) - expect(recordRegionSelection.mock.calls).toEqual([ - [{ targetRegion: 'asia-east2', selectedRegion: 'asia-east2', fallback: false }] - ]) - }) - -}) - -async function assign( - operations: { - recordRegionSelection: Mock - recordAssignmentAdmission?: Mock - plan?: Mock - assign?: Mock - resolve?: Mock - exchangeRegionCorrection?: Mock - }, - extra: Record = {}, - overrides: Partial = {} -): Promise { - const app = createRelayApp({ ...config(), ...overrides }, { - store: {} as never, - assignments: { - assign: operations.assign ?? vi.fn(), - resolve: operations.resolve ?? vi.fn(async () => null), - exchangeRegionCorrection: operations.exchangeRegionCorrection ?? vi.fn() - } as never, - ...(operations.plan ? { reservePlacement: { plan: operations.plan } as never } : {}), - drain: vi.fn(), - ready: vi.fn(async () => true), - recordRegionSelection: operations.recordRegionSelection, - ...(operations.recordAssignmentAdmission - ? { recordAssignmentAdmission: operations.recordAssignmentAdmission } - : {}) - }) - return await app.request('/v1/assign', { - method: 'POST', - headers: { authorization: `Bearer ${HOST}`, 'content-type': 'application/json' }, - body: JSON.stringify({ v: 1, relayHostId: HOST, preferredRegion: 'asia-east2', ...extra }) - }) -} - -function config(): RelayConfig { - return { - port: 8080, - publicUrl: 'https://relay.example.test', - cellUrl: 'https://relay.example.test', - region: 'us-central1', - authIssuer: 'https://auth.example.test', - authAudience: 'orca-relay', - jwksUrl: 'https://auth.example.test/jwks', - assignmentSigningKey: new TextEncoder().encode('assignment-key-with-at-least-32-bytes'), - role: 'director', - cellId: 'director', - cells: [], - adminAudience: 'https://relay.example.test/v1/admin/drain', - deployServiceAccount: 'deploy@example.test', - runtimeServiceAccount: 'runtime@example.test', - adminJwksUrl: 'https://auth.example.test/jwks', - databasePoolMax: 3, - publicAssignmentsEnabled: true, - regionalPlacementEnabled: true, - publicAssignmentConcurrency: 2, - publicAssignmentQueueMax: 128, - publicAssignmentWaitMs: 4_000, - publicResolveConcurrency: 1, - publicResolveWaitMs: 5_000, - publicAssignmentRetryAfterSeconds: 5, - dataDir: './data' - } -} diff --git a/cloud/apps/relay/src/relay-sigterm-exit.blackbox.test.ts b/cloud/apps/relay/src/relay-sigterm-exit.blackbox.test.ts index 7dde37f9453..c72236c13c2 100644 --- a/cloud/apps/relay/src/relay-sigterm-exit.blackbox.test.ts +++ b/cloud/apps/relay/src/relay-sigterm-exit.blackbox.test.ts @@ -70,16 +70,6 @@ async function startRelay(port: number): Promise { return child } -async function silentTcpPeer(port: number, firstBytes?: string): Promise { - const peer = connect(port, '127.0.0.1') - peer.on('error', () => undefined) - await new Promise((resolveConnect) => peer.once('connect', () => resolveConnect())) - if (firstBytes) { - peer.write(firstBytes) - } - return peer -} - // Pauses reading so the peer never answers the relay's close frame. async function unresponsiveWebSocketPeer(port: number): Promise { const socket = new WebSocket(`ws://127.0.0.1:${port}/v1/connect/abcdefghijklmnop`) @@ -111,14 +101,10 @@ async function waitUntilListenerClosed(port: number): Promise { } // Peers that are not idle to server.close(); unbounded, a silent one held exit until SIGKILL. -it.each([ - ['silent TCP peer', (port: number) => silentTcpPeer(port)], - ['partial-headers peer', (port: number) => silentTcpPeer(port, 'GET /healthz HTTP/1.1\r\nHost: x\r\n')], - ['unresponsive upgraded WebSocket peer', unresponsiveWebSocketPeer] -])('exits within the shutdown bound on SIGTERM with a %s', async (_name, openPeer) => { +it('exits within the shutdown bound on SIGTERM with an unresponsive upgraded WebSocket peer', async () => { const port = await unusedPort() const relay = await startRelay(port) - const peer = await openPeer(port) + const peer = await unresponsiveWebSocketPeer(port) cleanups.push(() => peer.destroy()) const peerClosed = new Promise((resolveClose) => peer.once('close', () => resolveClose())) diff --git a/cloud/apps/relay/src/relay.blackbox.test.ts b/cloud/apps/relay/src/relay.blackbox.test.ts index 21820c22ba6..4884cb1c9d3 100644 --- a/cloud/apps/relay/src/relay.blackbox.test.ts +++ b/cloud/apps/relay/src/relay.blackbox.test.ts @@ -134,19 +134,16 @@ function nextMessage(socket: WebSocket): Promise> { }) } -async function openHostControl(input?: { - controlResumeSecret?: string - previousGeneration?: number - keyPair?: nacl.BoxKeyPair - assignmentEpoch?: number - capabilities?: string -}): Promise<{ socket: WebSocket; ack: Record; keyPair: nacl.BoxKeyPair }> { - const keyPair = input?.keyPair ?? nacl.box.keyPair() +async function openHostControl(): Promise<{ + socket: WebSocket + ack: Record + keyPair: nacl.BoxKeyPair +}> { + const keyPair = nacl.box.keyPair() const hostId = createHash('sha256').update(keyPair.publicKey).digest('base64url').slice(0, 16) const socket = new WebSocket(`${relayUrl.replace('http:', 'ws:')}/v1/host/control`, { headers: { - authorization: `Bearer ${await relayToken('orca-relay', hostId)}`, - ...(input?.capabilities ? { ['x-orca-host-capabilities']: input.capabilities } : {}) + authorization: `Bearer ${await relayToken('orca-relay', hostId)}` }, perMessageDeflate: false }) @@ -159,13 +156,9 @@ async function openHostControl(input?: { type: 'host-hello', v: 1, relayHostId: hostId, - assignmentEpoch: input?.assignmentEpoch ?? 1, + assignmentEpoch: 1, hostPublicKeyB64: Buffer.from(keyPair.publicKey).toString('base64'), - appVersion: 'test', - ...(input?.controlResumeSecret ? { controlResumeSecret: input.controlResumeSecret } : {}), - ...(input?.previousGeneration === undefined - ? {} - : { previousGeneration: input.previousGeneration }) + appVersion: 'test' }) ) const challenge = await nextMessage(socket) diff --git a/cloud/dev/scripts/run-relay-tests.test.mjs b/cloud/dev/scripts/run-relay-tests.test.mjs index 70a332ea205..cebab34368b 100644 --- a/cloud/dev/scripts/run-relay-tests.test.mjs +++ b/cloud/dev/scripts/run-relay-tests.test.mjs @@ -32,17 +32,16 @@ function temporary(t) { return directory } -function start(t, options = {}) { +function start(t) { const scratchParent = temporary(t) const calls = [] const reports = [] const events = new EventEmitter() - const env = { ORCA_RELAY_TEST_POSTGRES_URL: databaseUrl, PRESERVED: 'yes', ...options.env } + const env = { ORCA_RELAY_TEST_POSTGRES_URL: databaseUrl, PRESERVED: 'yes' } const pending = runRelayTests({ cwd: join(scratchParent, 'relay'), cli: join(scratchParent, 'vitest.mjs'), parallelism: 4, - ...options, env, events, scratchParent, @@ -53,7 +52,7 @@ function start(t, options = {}) { return deferred.promise } }) - return { pending, calls, reports, events, scratchParent, env } + return { pending, calls, reports, events, scratchParent } } function finish(call, result = success, closed = true) { @@ -103,43 +102,6 @@ test('overlaps both projects within the worker budget and isolates their scratch assertClean(h) }) -for (const [name, options] of [ - ['selectors', { args: ['src/assignment.test.ts', '-t', 'requested behavior'] }], - [ - 'alternate PostgreSQL', - { env: { ORCA_IDLE_REHOME_POSTGRES_URL: 'postgres://localhost:55440/relay' } } - ], - ['application database', { env: { DATABASE_URL: databaseUrl } }], - ['one CPU', { parallelism: 1 }] -]) { - test(`retains the original single run for ${name}`, async (t) => { - const h = start(t, options) - assert.equal(h.calls.length, 1) - const { spec } = h.calls[0] - assert.deepEqual(spec.args.slice(1), ['run', ...(options.args ?? [])]) - for (const [key, value] of Object.entries(h.env)) { - assert.equal(spec.env[key], value) - } - finish(h.calls[0]) - assert.equal(await h.pending, 0) - assertClean(h) - }) -} - -for (const [name, result] of [ - ['null exit code', { ...success, code: null }], - ['termination signal', { ...success, signal: 'SIGTERM' }], - ['timeout', { ...success, timedOut: true }] -]) { - test(`fails a ${name} even when other result fields indicate success`, async (t) => { - const h = start(t) - finish(h.calls[0], result) - finish(h.calls[1]) - assert.equal(await h.pending, 1) - assertClean(h) - }) -} - test('fails and retains scratch when a child never reports closure', async (t) => { const h = start(t) finish(h.calls[0], success, false) @@ -152,8 +114,7 @@ test('fails and retains scratch when a child never reports closure', async (t) = }) for (const [signal, exitCode, laterSignal] of [ - ['SIGINT', 130, 'SIGTERM'], - ['SIGTERM', 143, 'SIGINT'] + ['SIGINT', 130, 'SIGTERM'] ]) { test(`${signal} cancels every child and retains scratch despite root close callbacks`, async (t) => { const h = start(t) diff --git a/cloud/packages/cloud-database/src/cloud-leader-lock.test.ts b/cloud/packages/cloud-database/src/cloud-leader-lock.test.ts deleted file mode 100644 index 3253c0cbf7a..00000000000 --- a/cloud/packages/cloud-database/src/cloud-leader-lock.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { randomUUID } from 'node:crypto' -import pg from 'pg' -import { describe, expect, it } from 'vitest' -import { openInMemoryCloudDatabase } from './cloud-database.js' -import { acquireLeadership } from './cloud-leader-lock.js' - -const postgresUrl = process.env.ORCA_CLOUD_DATABASE_TEST_URL -const database = await openInMemoryCloudDatabase([]) - -describe('acquireLeadership', () => { - it.skipIf(!postgresUrl)( - 'lets one holder lead until it releases or its session dies', - async () => { - const key = `gateway-${randomUUID()}` - const options = { databaseUrl: postgresUrl, database, key, pollMs: 50, checkMs: 50 } - const first = await acquireLeadership({ ...options, signal: AbortSignal.timeout(5_000) }) - let secondLed = false - const second = acquireLeadership({ ...options, signal: AbortSignal.timeout(5_000) }).then( - (leadership) => { - secondLed = true - return leadership - } - ) - await new Promise((resolve) => setTimeout(resolve, 200)) - expect(secondLed).toBe(false) - await first.release() - const next = await second - - // Killing the leader's session frees the lock and the holder notices. - const admin = new pg.Client({ connectionString: postgresUrl }) - await admin.connect() - await admin.query( - `SELECT pg_terminate_backend(pid) FROM pg_locks - WHERE locktype = 'advisory' AND objid = hashtext($1::text)::oid AND granted`, - [key] - ) - await admin.end() - await next.lost - // Its transactions ran on the lock's connection, so none can commit after the loss. - await expect( - next.transaction((transaction) => transaction.query('SELECT 1')) - ).rejects.toThrow() - const third = await acquireLeadership({ ...options, signal: AbortSignal.timeout(5_000) }) - await third.release() - } - ) - - it.skipIf(!postgresUrl)('stops waiting when aborted', async () => { - const key = `gateway-${randomUUID()}` - const options = { databaseUrl: postgresUrl, database, key, pollMs: 50 } - const first = await acquireLeadership({ ...options, signal: AbortSignal.timeout(5_000) }) - await expect( - acquireLeadership({ ...options, signal: AbortSignal.timeout(200) }) - ).rejects.toThrow() - await first.release() - }) -}) diff --git a/cloud/packages/cloud-database/src/cloud-notifications.test.ts b/cloud/packages/cloud-database/src/cloud-notifications.test.ts index 875192b0f24..3c6e9bd39c3 100644 --- a/cloud/packages/cloud-database/src/cloud-notifications.test.ts +++ b/cloud/packages/cloud-database/src/cloud-notifications.test.ts @@ -59,7 +59,7 @@ describe('cloud database notifications', () => { }) it.skipIf(!postgresUrl)( - 'delivers a Postgres notification at commit and re-listens after losing its connection', + 'delivers Postgres notifications and re-listens after losing its connection', async () => { const applicationName = `cloud-db-listen-${randomUUID()}` const database = await openCloudDatabase({ @@ -75,8 +75,6 @@ describe('cloud database notifications', () => { try { await database.transaction(async (tx) => { await notifications.notify(tx, 'inbox', 'committed') - await new Promise((resolve) => setTimeout(resolve, 100)) - expect(seen.payloads).toEqual([]) }) await seen.until(() => seen.payloads.length === 1) diff --git a/cloud/packages/connector-contract/src/contract.test.ts b/cloud/packages/connector-contract/src/contract.test.ts index 35900489850..96de62bf7cf 100644 --- a/cloud/packages/connector-contract/src/contract.test.ts +++ b/cloud/packages/connector-contract/src/contract.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from 'vitest' import { ConnectorEventSchema } from './event-messages.js' -import { ConnectorHelloRequestSchema } from './hello-messages.js' import { CONNECTOR_LIMITS } from './connector-limits.js' const conversation = { source: 'slack', externalId: 'T1:C9:1792.9:U5' } as const @@ -29,25 +28,3 @@ describe('events', () => { ).toBe(false) }) }) - -describe('hello and replies', () => { - it('keeps capability entries the cloud does not know', () => { - const hello = ConnectorHelloRequestSchema.parse({ - v: 1, - contract: 1, - orcaVersion: '1.4.230', - platform: 'linux', - runtime: 'serve', - hostName: 'build-box', - agents: ['claude', 'future-agent'], - promptKinds: ['approval'], - features: ['answer', 'future-feature'], - sources: ['slack', 'future-source'], - projects: [ - { placeId: 'pl_1', name: 'orca', kind: 'git', where: { kind: 'ssh', label: 'openclaw' } } - ] - }) - expect(hello.features).toEqual(['answer', 'future-feature']) - expect(hello.sources).toEqual(['slack', 'future-source']) - }) -}) diff --git a/cloud/packages/postgres-schema/src/schema-lock-target.test.ts b/cloud/packages/postgres-schema/src/schema-lock-target.test.ts index 953fa219424..45ad2b30795 100644 --- a/cloud/packages/postgres-schema/src/schema-lock-target.test.ts +++ b/cloud/packages/postgres-schema/src/schema-lock-target.test.ts @@ -1,25 +1,9 @@ import { describe, expect, it } from 'vitest' import { requireSchemaLockTarget, - schemaLockTarget, - sqlWithoutComments + schemaLockTarget } from './schema-lock-target.js' -describe('sqlWithoutComments', () => { - - it('closes the inner block comment first when they nest', () => { - expect(sqlWithoutComments('ALTER TABLE t /* a /* b */ c */ ADD COLUMN d TEXT')).toBe( - 'ALTER TABLE t ADD COLUMN d TEXT' - ) - }) - - it('leaves a comment marker inside a string literal alone', () => { - expect(sqlWithoutComments(`ALTER TABLE t ADD COLUMN c TEXT DEFAULT '-- not a comment'`)).toBe( - `ALTER TABLE t ADD COLUMN c TEXT DEFAULT '-- not a comment'` - ) - }) -}) - describe('catalog name folding', () => { it('unescapes a doubled quote and leaves the qualified table text as written', () => { @@ -33,11 +17,10 @@ describe('catalog name folding', () => { }) describe('square brackets in an ALTER TABLE', () => { - it.each([ - ['an array type', 'ALTER TABLE t ADD COLUMN c bigint[] DEFAULT ARRAY[1, 2]'], - ['a nested array default', "ALTER TABLE t ADD COLUMN c TEXT[] DEFAULT ARRAY['a', 'b']"] - ])('does not read a comma inside %s as a second subcommand', (_label, statement) => { - expect(() => requireSchemaLockTarget(statement)).not.toThrow() + it('does not read an array-default comma as a second subcommand', () => { + expect(() => + requireSchemaLockTarget('ALTER TABLE t ADD COLUMN c bigint[] DEFAULT ARRAY[1, 2]') + ).not.toThrow() }) it('still catches a second subcommand after an array default', () => { @@ -52,12 +35,6 @@ describe('multi-action ALTER TABLE', () => { // Skipping on the first subcommand would permanently drop every later action. const statement = 'ALTER TABLE t ADD COLUMN IF NOT EXISTS a TEXT, ADD COLUMN IF NOT EXISTS b TEXT' - expect(schemaLockTarget(statement)).toEqual({ - kind: 'column', - table: 't', - name: 'a', - skipWhen: 'present' - }) expect(() => requireSchemaLockTarget(statement)).toThrow(/unparsed_schema_lock_target/) }) }) @@ -68,7 +45,6 @@ describe('dollar-quoted bodies', () => { // The closing delimiter has to match the opening tag, so the comma and the comment marker // between the inner $$ pair are still inside the body. const statement = 'ALTER TABLE t ADD COLUMN c TEXT DEFAULT $tag$ a $$ -- b, c $$ d $tag$' - expect(sqlWithoutComments(statement)).toBe(statement) expect(() => requireSchemaLockTarget(statement)).not.toThrow() expect(schemaLockTarget(statement)).toEqual({ kind: 'column', diff --git a/cloud/packages/push-contract/src/push-host-proof-transcript.test.ts b/cloud/packages/push-contract/src/push-host-proof-transcript.test.ts index ed2f775041b..21c8139de21 100644 --- a/cloud/packages/push-contract/src/push-host-proof-transcript.test.ts +++ b/cloud/packages/push-contract/src/push-host-proof-transcript.test.ts @@ -1,7 +1,5 @@ import { describe, expect, it } from 'vitest' import { - buildPushHostChallengePlaintext, - buildPushHostProofMacInput, buildPushHostProofTranscript } from './push-host-proof-transcript.js' @@ -48,30 +46,4 @@ describe('push host proof transcript', () => { expect(offset).toBe(transcript.byteLength) }) - it('frames the challenge plaintext as domain, length, transcript, secret', () => { - const transcript = buildPushHostProofTranscript(transcriptInput) - const secret = new Uint8Array(32).fill(11) - const plaintext = buildPushHostChallengePlaintext(transcript, secret) - const length = Buffer.alloc(4) - length.writeUInt32BE(transcript.byteLength) - expect(Buffer.from(plaintext)).toEqual( - Buffer.concat([ - Buffer.from('orca-push-host-challenge/v1\0'), - length, - Buffer.from(transcript), - Buffer.alloc(32, 11) - ]) - ) - expect(() => buildPushHostChallengePlaintext(transcript, new Uint8Array(16))).toThrow( - 'challengeSecret must be 32 bytes' - ) - }) - - it('separates the ack mac input from the challenge domain', () => { - const transcript = buildPushHostProofTranscript(transcriptInput) - const macInput = buildPushHostProofMacInput(transcript) - expect(Buffer.from(macInput)).toEqual( - Buffer.concat([Buffer.from('orca-push-host-proof/v1\0ack\0'), Buffer.from(transcript)]) - ) - }) }) diff --git a/cloud/packages/push-contract/src/send-messages.test.ts b/cloud/packages/push-contract/src/send-messages.test.ts deleted file mode 100644 index 166f09fd72e..00000000000 --- a/cloud/packages/push-contract/src/send-messages.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { PushSendRequestSchema } from './send-messages.js' - -function notification(): Record { - return { - notificationId: 'note-1', - notificationSeq: 4, - notificationEpoch: '5c9e9a1e-0000-4000-8000-000000000000', - source: 'agent-task-complete', - agentState: 'needs-input', - title: 'Agent needs input', - body: 'Waiting on your answer', - worktreeId: 'wt-1' - } -} - -describe('send schemas', () => { - - it('rejects a notification id that could not be sent as a collapse header', () => { - for (const notificationId of ['line\nbreak', 'nul\0byte', 'émoji', '\t']) { - expect( - PushSendRequestSchema.safeParse({ - v: 1, - registrationIds: ['reg-1'], - notification: { ...notification(), notificationId } - }).success - ).toBe(false) - } - expect( - PushSendRequestSchema.safeParse({ - v: 1, - registrationIds: ['reg-1'], - notification: { - ...notification(), - notificationId: 'agent:repo%3A%3A%2FUsers%2Fme:pane-1:1700000000000' - } - }).success - ).toBe(true) - }) -}) diff --git a/cloud/packages/relay-contract/src/contract.test.ts b/cloud/packages/relay-contract/src/contract.test.ts index 08c40c72f2d..1c6cfe4b38f 100644 --- a/cloud/packages/relay-contract/src/contract.test.ts +++ b/cloud/packages/relay-contract/src/contract.test.ts @@ -1,7 +1,5 @@ import { describe, expect, it } from 'vitest' import { - buildHostChallengePlaintext, - buildHostProofMacInput, buildHostProofTranscript } from './host-proof-transcript.js' @@ -23,44 +21,8 @@ describe('relay protocol contract', () => { previousGeneration: 6, resumeRequested: true }) - const challenge = buildHostChallengePlaintext(transcript, new Uint8Array(32).fill(3)) - const proofInput = buildHostProofMacInput(transcript) - expect(Buffer.from(transcript).toString('base64url')).toBe( 'AAAACHByb3RvY29sAAAAGG9yY2EtcmVsYXktaG9zdC1wcm9vZi92MQAAAAd2ZXJzaW9uAAAAAQEAAAALcmVsYXlPcmlnaW4AAAAYaHR0cHM6Ly9yZWxheS5vbm9yY2EuZGV2AAAAF3JlbGF5RXBoZW1lcmFsUHVibGljS2V5AAAAIAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAADmNoYWxsZW5nZU5vbmNlAAAAGAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAtjaGFsbGVuZ2VJZAAAAAtjaGFsbGVuZ2UtMQAAAAhpc3N1ZWRBdAAAAAgAAAGLz-VoAAAAAAlleHBpcmVzQXQAAAAIAAABi8_ljxAAAAAGdXNlcklkAAAABnVzZXItMQAAAAlwcm9maWxlSWQAAAAJcHJvZmlsZS0xAAAADm9yZ2FuaXphdGlvbklkAAAABW9yZy0xAAAAC3JlbGF5SG9zdElkAAAAEGFiY2RlZmdoaWprbG1ub3AAAAANaG9zdFB1YmxpY0tleQAAACACAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAAAA9hc3NpZ25tZW50RXBvY2gAAAAIAAAAAAAAAAcAAAAScHJldmlvdXNHZW5lcmF0aW9uAAAACAAAAAAAAAAGAAAAD3Jlc3VtZVJlcXVlc3RlZAAAAAEB' ) - const length = Buffer.alloc(4) - length.writeUInt32BE(transcript.byteLength) - expect(Buffer.from(challenge)).toEqual( - Buffer.concat([ - Buffer.from('orca-relay-host-challenge/v1\0'), - length, - Buffer.from(transcript), - Buffer.alloc(32, 3) - ]) - ) - expect(Buffer.from(proofInput)).toEqual( - Buffer.concat([Buffer.from('orca-relay-host-proof/v1\0ack\0'), Buffer.from(transcript)]) - ) - expect(() => buildHostChallengePlaintext(transcript, new Uint8Array(31))).toThrow( - 'challengeSecret must be 32 bytes' - ) - expect(() => - buildHostProofTranscript({ - relayOrigin: 'https://relay.onorca.dev', - relayEphemeralPublicKey: new Uint8Array(32), - challengeNonce: new Uint8Array(32), - challengeId: 'challenge-1', - issuedAt: 1, - expiresAt: 2, - userId: 'user-1', - profileId: 'profile-1', - organizationId: 'org-1', - relayHostId: 'abcdefghijklmnop', - hostPublicKey: new Uint8Array(32), - assignmentEpoch: 1, - resumeRequested: false - }) - ).toThrow('challengeNonce must be 24 bytes') }) }) diff --git a/config/flaky-test-pattern-baseline.txt b/config/flaky-test-pattern-baseline.txt index 7ccccd20106..5e04ebdc3c7 100644 --- a/config/flaky-test-pattern-baseline.txt +++ b/config/flaky-test-pattern-baseline.txt @@ -20,7 +20,6 @@ 5 test-no-in-test-import-actual:src/main/ssh/config/ssh-config-loader-regression.test.ts 2 test-no-in-test-import-actual:src/main/ssh/connection/ssh-connection-auth-fallback.test.ts 1 test-no-in-test-import-actual:src/main/window/file-drop/dropped-path-preparation.test.ts -2 test-no-in-test-import-actual:src/main/workspace-hooks/hooks-runner.test.ts 1 test-no-in-test-import-actual:src/main/workspaces/removal/local-worktree-removal-recovery.test.ts 1 test-no-in-test-import-actual:src/renderer/src/components/terminal-pane/pty-connection/notifications/pty-connection-hook-idle-arbitration.test.ts 3 test-no-in-test-import-actual:src/renderer/src/components/terminal-pane/snapshots/remote-snapshot-alt-screen-host-chain.test.ts @@ -35,7 +34,6 @@ 1 test-no-polling-loop:src/main/daemon/sessions/testing/repro-12101-mouse-tracking-survives-agent-death.test.ts 1 test-no-polling-loop:src/main/daemon/transport/client/testing/client.test.ts 2 test-no-polling-loop:src/main/git/refs/maintenance/repo-ref-maintenance-real-git.test.ts -1 test-no-polling-loop:src/main/ipc/runtime-environments/runtime-environment-request-connections.test.ts 1 test-no-polling-loop:src/main/ipc/worktrees/creation/worktree-base-directory-poller-marker-fanout.test.ts 2 test-no-polling-loop:src/main/ipc/worktrees/git/worktree-git-status-attribution-real-git.test.ts 1 test-no-polling-loop:src/main/provider-process/provider-group-orphan-guard.integration.test.ts @@ -122,7 +120,6 @@ 1 test-no-polling-loop:tests/e2e/terminal-tui-wheel-reports.spec.ts 1 test-no-polling-loop:tests/e2e/workspace-sleep-in-main.spec.ts 2 test-no-polling-loop:tests/e2e/worktree-card-downward-drag.spec.ts -1 test-no-real-time-wait:mobile/src/terminal/terminal-webview-tap-routing.test.ts 1 test-no-real-time-wait:src/main/agent-hooks/installation/scripts/posix-hook-json-stdin-reader.test.ts 2 test-no-real-time-wait:src/main/agent-hooks/server/testing/http/server-transport-interference.test.ts 1 test-no-real-time-wait:src/main/agent-hooks/server/testing/main-agent/server-turn-ended-at.test.ts @@ -165,7 +162,6 @@ 4 test-no-real-time-wait:src/main/git/refs/maintenance/repo-ref-maintenance-real-git.test.ts 1 test-no-real-time-wait:src/main/git/worktrees/removal/remove-worktree-branch-cleanup.test.ts 1 test-no-real-time-wait:src/main/grok/grok-hook-stdin-no-eof.test.ts -4 test-no-real-time-wait:src/main/ipc/runtime-environments/runtime-environment-request-connections.test.ts 1 test-no-real-time-wait:src/main/ipc/worktrees/creation/worktree-base-directory-poller-marker-fanout.test.ts 2 test-no-real-time-wait:src/main/ipc/worktrees/git/worktree-git-status-attribution-real-git.test.ts 1 test-no-real-time-wait:src/main/managed-server/lifecycle/orcad-managed-tunnel-local-port.test.ts @@ -216,7 +212,6 @@ 1 test-no-real-time-wait:src/renderer/src/lib/agent-session/resume/cli-fork/native-chat-cli-fork-preparation.test.ts 2 test-no-real-time-wait:src/renderer/src/store/slices/workspace-cleanup/workspace-cleanup-removal-preflight.test.ts 2 test-no-real-time-wait:src/shared/__tests__/process-host/windows-tree-kill-late-taskkill.win32.test.ts -1 test-no-real-time-wait:src/shared/darwin-process-table-capture.test.ts 1 test-no-real-time-wait:src/shared/darwin-terminal-names-real-ps.test.ts 1 test-no-real-time-wait:src/shared/darwin-terminal-names.test.ts 1 test-no-real-time-wait:src/shared/git/status/git-branch-line-total-soft-deadline.test.ts @@ -431,7 +426,6 @@ 3 test-no-wall-clock:src/main/github/pull-requests/refresh/pr-refresh-coordinator-completion-order.test.ts 2 test-no-wall-clock:src/main/github/pull-requests/refresh/pr-refresh-queue-growth-bound.test.ts 2 test-no-wall-clock:src/main/grok/grok-hook-stdin-no-eof.test.ts -3 test-no-wall-clock:src/main/ipc/runtime-environments/runtime-environment-request-connections.test.ts 1 test-no-wall-clock:src/main/ipc/terminal-render-desync-evidence.test.ts 2 test-no-wall-clock:src/main/ipc/worktrees-orphan-directory-cleanup.test.ts 2 test-no-wall-clock:src/main/ipc/worktrees/git/worktree-git-status-attribution-real-git.test.ts @@ -491,7 +485,7 @@ 23 test-no-wall-clock:src/main/runtime/terminal/output/terminal-query-responder.test.ts 3 test-no-wall-clock:src/main/runtime/terminal/query/orca-runtime-codex-idle-title-chat-address.test.ts 1 test-no-wall-clock:src/main/runtime/terminal/recovery/terminal-pane-recovery-liveness-gate.test.ts -16 test-no-wall-clock:src/main/runtime/terminal/wait/tui-idle-hook-lane-runtime.test.ts +4 test-no-wall-clock:src/main/runtime/terminal/wait/tui-idle-hook-lane-runtime.test.ts 1 test-no-wall-clock:src/main/runtime/workspaces/process-listing/runtime-worktree-structured-agent-rows-liveness.test.ts 1 test-no-wall-clock:src/main/runtime/workspaces/removal/owned-session-teardown-policy.test.ts 6 test-no-wall-clock:src/main/skills/install/skill-bundle-process-termination.integration.test.ts @@ -613,7 +607,6 @@ 2 test-timeout-needs-reason:src/main/runtime/terminal/wait/launched-agent-crash-guard.live-shell.test.ts 2 test-timeout-needs-reason:src/main/runtime/terminal/wait/terminal-interactive-wait-visibility.test.ts 1 test-timeout-needs-reason:src/main/runtime/terminal/wait/terminal-wait-name-only-idle.test.ts -4 test-timeout-needs-reason:src/main/runtime/terminal/wait/tui-idle-hook-lane-runtime.test.ts 1 test-timeout-needs-reason:src/main/skills/install/skill-bundle-process-termination.integration.test.ts 1 test-timeout-needs-reason:src/main/sqlite/sync-database-concurrent-backup.test.ts 2 test-timeout-needs-reason:src/main/ssh/connection/ssh-connection-sftp-wire.test.ts @@ -624,16 +617,13 @@ 1 test-timeout-needs-reason:src/main/windows/windows-msys-job.win32.test.ts 2 test-timeout-needs-reason:src/main/windows/windows-pty-job.win32.test.ts 5 test-timeout-needs-reason:src/main/worker-thread-child-process/worker-thread-child-process.test.ts -1 test-timeout-needs-reason:src/main/workspace-hooks/hook-archive-timeout-observation.test.ts -2 test-timeout-needs-reason:src/main/workspace-hooks/hook-termination-real-process.test.ts +1 test-timeout-needs-reason:src/main/workspace-hooks/hook-termination-real-process.test.ts 2 test-timeout-needs-reason:src/main/workspaces/removal/local-worktree-filesystem-wsl-banner.wsl.test.ts 4 test-timeout-needs-reason:src/main/wsl/execution/wsl-runner.wsl.test.ts 4 test-timeout-needs-reason:src/packages/process-host/src/run-process.test.ts 1 test-timeout-needs-reason:src/renderer/src/hooks/agent-hook-completion-notifications.test.ts 1 test-timeout-needs-reason:src/renderer/src/lib/source-control/checks/check-run-details-log-normalization-performance.test.ts 1 test-timeout-needs-reason:src/shared/__tests__/process-host/windows-tree-kill-late-taskkill.win32.test.ts -1 test-timeout-needs-reason:src/shared/dsh-real-cli.test.ts -1 test-timeout-needs-reason:src/shared/workspace-disk-space/workspace-space-entry-traversal.test.ts 1 test-timeout-needs-reason:src/shared/wsl-login-shell-command.test.ts 1 test-timeout-needs-reason:tests/e2e/active-view-restart-restore.spec.ts 1 test-timeout-needs-reason:tests/e2e/agent-send-picker-reveal.spec.ts diff --git a/config/focus-setting-read-baseline.txt b/config/focus-setting-read-baseline.txt index 84559a65d66..bef6f0b90de 100644 --- a/config/focus-setting-read-baseline.txt +++ b/config/focus-setting-read-baseline.txt @@ -20,7 +20,6 @@ 1 src/renderer/src/components/activity/use-agent-pane-threads.ts 4 src/renderer/src/components/agent/AgentSettingsDialog.tsx 1 src/renderer/src/components/automations/authoring/use-automations-page-destination-form.ts -1 src/renderer/src/components/automations/automations-page-fixtures.ts 1 src/renderer/src/components/automations/hosts/automation-host-client.ts 1 src/renderer/src/components/automations/use-automations-page-controller.ts 1 src/renderer/src/components/browser-pane/ReopenBrowserPageOnServerButton.tsx @@ -156,18 +155,18 @@ 1 src/renderer/src/components/tab-bar/windows-shell-menu-visibility.ts 3 src/renderer/src/components/tab-group/useTabGroupCreationCommands.ts 11 src/renderer/src/components/task-page/TaskPage.tsx -2 src/renderer/src/components/task-page/jira/IssueAssigneeField.tsx -1 src/renderer/src/components/task-page/jira/task-page-jira-status-order.ts -4 src/renderer/src/components/task-page/jira/use-task-page-jira-creation-metadata.ts -4 src/renderer/src/components/task-page/jira/use-task-page-jira-issue-creation.ts -1 src/renderer/src/components/task-page/jira/use-task-page-jira-list-effects.ts +2 src/renderer/src/components/task-page/jira/creation/IssueAssigneeField.tsx +1 src/renderer/src/components/task-page/jira/listing/task-page-jira-status-order.ts +4 src/renderer/src/components/task-page/jira/creation/use-task-page-jira-creation-metadata.ts +4 src/renderer/src/components/task-page/jira/creation/use-task-page-jira-issue-creation.ts +1 src/renderer/src/components/task-page/jira/listing/use-task-page-jira-list-effects.ts 1 src/renderer/src/components/task-page/linear/Filters.tsx -4 src/renderer/src/components/task-page/linear/task-page-linear-issue-model.tsx -8 src/renderer/src/components/task-page/linear/use-task-page-linear-board.ts +4 src/renderer/src/components/task-page/linear/issues/task-page-linear-state-cell.tsx +8 src/renderer/src/components/task-page/linear/issues/use-task-page-linear-board.ts 4 src/renderer/src/components/task-page/linear/use-task-page-linear-context-state.ts -6 src/renderer/src/components/task-page/linear/use-task-page-linear-creation-state.ts -4 src/renderer/src/components/task-page/linear/use-task-page-linear-issue-creation.ts -2 src/renderer/src/components/task-page/linear/use-task-page-linear-project-creation.ts +6 src/renderer/src/components/task-page/linear/creation/use-task-page-linear-creation-state.ts +4 src/renderer/src/components/task-page/linear/creation/use-task-page-linear-issue-creation.ts +2 src/renderer/src/components/task-page/linear/creation/use-task-page-linear-project-creation.ts 5 src/renderer/src/components/task-page/linear/use-task-page-linear-view-state.ts 3 src/renderer/src/components/task-page/use-task-page-detail-routing.ts 2 src/renderer/src/components/task-page/use-task-page-provider-metadata.ts diff --git a/config/legacy-relay-compat-tags-baseline.txt b/config/legacy-relay-compat-tags-baseline.txt index ad9eb1fa6e3..ffcbba0a3fa 100644 --- a/config/legacy-relay-compat-tags-baseline.txt +++ b/config/legacy-relay-compat-tags-baseline.txt @@ -17,7 +17,7 @@ src/main/ssh/migration-source/orcad-migration-terminal-gate.ts src/main/ssh/host-server/ssh-host-server-on-connect.ts src/main/ssh/targets/ssh-target-orcad-preflight.ts src/preload/api-types.ts -src/preload/api/legacy-relay-bridge-api.ts +src/preload/api/mobile/legacy-relay-bridge-api.ts src/renderer/src/components/terminal-pane/pty-transport/legacy-relay/legacy-relay-bridge-panes.ts src/renderer/src/components/terminal-pane/pty-transport/legacy-relay/legacy-relay-bridge-transport.ts src/renderer/src/components/terminal-pane/pty-connection/output/foreground-output-refresh.ts diff --git a/config/legacy-relay-importers-baseline.txt b/config/legacy-relay-importers-baseline.txt index 8696af1340b..44d3041afea 100644 --- a/config/legacy-relay-importers-baseline.txt +++ b/config/legacy-relay-importers-baseline.txt @@ -1,7 +1,7 @@ # Files outside src/main/ssh/legacy-relay/ that import it. RATCHET: may only SHRINK. # The relay retirement (2027-01-15) deletes the folder and edits exactly these files. # Prune: pnpm check:legacy-relay-fence-ratchet --prune -src/main/ipc/runtime-environments/orcad-delta-move-handlers.ts +src/main/ipc/runtime-environments/managed-orcad/orcad-delta-move-handlers.ts src/main/ipc/pty/runtime/kill.ts src/main/ipc/ssh/ssh-connect-decision-cancellation.test.ts src/main/ipc/ssh/ssh-connect-flow.ts diff --git a/config/main-blocking-ipc-baseline.txt b/config/main-blocking-ipc-baseline.txt index dac3b7e5930..124ec4b2b3c 100644 --- a/config/main-blocking-ipc-baseline.txt +++ b/config/main-blocking-ipc-baseline.txt @@ -4,6 +4,6 @@ 1 src/main/ipc/renderer-shutdown-checkpoint.ts 1 src/main/ipc/settings.ts 1 src/preload/api/app-bridge.ts -1 src/preload/api/native-chat-bridge.ts -1 src/preload/api/settings-bridge.ts +1 src/preload/api/native-chat/native-chat-bridge.ts +1 src/preload/api/preferences/settings-bridge.ts 1 src/preload/native-chat/native-chat-visual-guest-theme.ts diff --git a/config/main-layout-writer-baseline.txt b/config/main-layout-writer-baseline.txt index 17a971600e9..d699d24d0e4 100644 --- a/config/main-layout-writer-baseline.txt +++ b/config/main-layout-writer-baseline.txt @@ -8,8 +8,8 @@ 3 src/main/ipc/pty/pane/stable-owner.ts 1 src/main/ipc/pty/spawn-commit-binding.ts 1 src/main/ipc/renderer-shutdown-checkpoint.ts -1 src/main/ipc/runtime-environments/runtime-environment-removal-cleanup.ts -1 src/main/ipc/runtime-environments/runtime-environment-session-reconcile.ts +1 src/main/ipc/runtime-environments/catalog/runtime-environment-removal-cleanup.ts +1 src/main/ipc/runtime-environments/catalog/runtime-environment-session-reconcile.ts 2 src/main/ipc/session.ts 6 src/main/orca-profiles/project-transfer/profile-project-session-state.ts 4 src/main/orca-profiles/project-transfer/profile-project-source-removal.ts diff --git a/config/mobile-localization-coverage-allowlist.json b/config/mobile-localization-coverage-allowlist.json index d84333750f2..05742534930 100644 --- a/config/mobile-localization-coverage-allowlist.json +++ b/config/mobile-localization-coverage-allowlist.json @@ -175,7 +175,7 @@ "count": 1 }, { - "filePath": "mobile/src/files/mobile-file-preview-route.ts", + "filePath": "mobile/src/files/preview/mobile-file-preview-route.ts", "kind": "object-property:message", "text": "Unable to load preview", "dynamic": false, @@ -308,21 +308,21 @@ "count": 1 }, { - "filePath": "mobile/src/terminal/session/MobileSessionLatchKeys.tsx", + "filePath": "mobile/src/terminal/session/accessory-keys/MobileSessionLatchKeys.tsx", "kind": "object-property:label", "text": "Shift", "dynamic": false, "count": 1 }, { - "filePath": "mobile/src/terminal/session/MobileSessionLatchKeys.tsx", + "filePath": "mobile/src/terminal/session/accessory-keys/MobileSessionLatchKeys.tsx", "kind": "object-property:label", "text": "Ctrl", "dynamic": false, "count": 1 }, { - "filePath": "mobile/src/terminal/session/MobileSessionLatchKeys.tsx", + "filePath": "mobile/src/terminal/session/accessory-keys/MobileSessionLatchKeys.tsx", "kind": "object-property:label", "text": "Alt", "dynamic": false, @@ -399,147 +399,147 @@ "count": 1 }, { - "filePath": "mobile/src/terminal/terminal-key-definitions.ts", + "filePath": "mobile/src/terminal/key-input/terminal-key-definitions.ts", "kind": "object-property:accessibilityLabel", "text": "Backspace", "dynamic": false, "count": 1 }, { - "filePath": "mobile/src/terminal/terminal-key-definitions.ts", + "filePath": "mobile/src/terminal/key-input/terminal-key-definitions.ts", "kind": "object-property:accessibilityLabel", "text": "Enter", "dynamic": false, "count": 1 }, { - "filePath": "mobile/src/terminal/terminal-key-definitions.ts", + "filePath": "mobile/src/terminal/key-input/terminal-key-definitions.ts", "kind": "object-property:accessibilityLabel", "text": "Escape", "dynamic": false, "count": 1 }, { - "filePath": "mobile/src/terminal/terminal-key-definitions.ts", + "filePath": "mobile/src/terminal/key-input/terminal-key-definitions.ts", "kind": "object-property:accessibilityLabel", "text": "Shift Tab", "dynamic": false, "count": 1 }, { - "filePath": "mobile/src/terminal/terminal-key-definitions.ts", + "filePath": "mobile/src/terminal/key-input/terminal-key-definitions.ts", "kind": "object-property:accessibilityLabel", "text": "Space", "dynamic": false, "count": 1 }, { - "filePath": "mobile/src/terminal/terminal-key-definitions.ts", + "filePath": "mobile/src/terminal/key-input/terminal-key-definitions.ts", "kind": "object-property:accessibilityLabel", "text": "Tab", "dynamic": false, "count": 1 }, { - "filePath": "mobile/src/terminal/terminal-key-definitions.ts", + "filePath": "mobile/src/terminal/key-input/terminal-key-definitions.ts", "kind": "object-property:label", "text": "Ctrl+A", "dynamic": false, "count": 1 }, { - "filePath": "mobile/src/terminal/terminal-key-definitions.ts", + "filePath": "mobile/src/terminal/key-input/terminal-key-definitions.ts", "kind": "object-property:label", "text": "Ctrl+C", "dynamic": false, "count": 1 }, { - "filePath": "mobile/src/terminal/terminal-key-definitions.ts", + "filePath": "mobile/src/terminal/key-input/terminal-key-definitions.ts", "kind": "object-property:label", "text": "Ctrl+D", "dynamic": false, "count": 1 }, { - "filePath": "mobile/src/terminal/terminal-key-definitions.ts", + "filePath": "mobile/src/terminal/key-input/terminal-key-definitions.ts", "kind": "object-property:label", "text": "Ctrl+E", "dynamic": false, "count": 1 }, { - "filePath": "mobile/src/terminal/terminal-key-definitions.ts", + "filePath": "mobile/src/terminal/key-input/terminal-key-definitions.ts", "kind": "object-property:label", "text": "Ctrl+L", "dynamic": false, "count": 1 }, { - "filePath": "mobile/src/terminal/terminal-key-definitions.ts", + "filePath": "mobile/src/terminal/key-input/terminal-key-definitions.ts", "kind": "object-property:label", "text": "Ctrl+R", "dynamic": false, "count": 1 }, { - "filePath": "mobile/src/terminal/terminal-key-definitions.ts", + "filePath": "mobile/src/terminal/key-input/terminal-key-definitions.ts", "kind": "object-property:label", "text": "Ctrl+U", "dynamic": false, "count": 1 }, { - "filePath": "mobile/src/terminal/terminal-key-definitions.ts", + "filePath": "mobile/src/terminal/key-input/terminal-key-definitions.ts", "kind": "object-property:label", "text": "Ctrl+W", "dynamic": false, "count": 1 }, { - "filePath": "mobile/src/terminal/terminal-key-definitions.ts", + "filePath": "mobile/src/terminal/key-input/terminal-key-definitions.ts", "kind": "object-property:label", "text": "Ctrl+Z", "dynamic": false, "count": 1 }, { - "filePath": "mobile/src/terminal/terminal-key-definitions.ts", + "filePath": "mobile/src/terminal/key-input/terminal-key-definitions.ts", "kind": "object-property:label", "text": "Del", "dynamic": false, "count": 1 }, { - "filePath": "mobile/src/terminal/terminal-key-definitions.ts", + "filePath": "mobile/src/terminal/key-input/terminal-key-definitions.ts", "kind": "object-property:label", "text": "Enter", "dynamic": false, "count": 1 }, { - "filePath": "mobile/src/terminal/terminal-key-definitions.ts", + "filePath": "mobile/src/terminal/key-input/terminal-key-definitions.ts", "kind": "object-property:label", "text": "Esc", "dynamic": false, "count": 1 }, { - "filePath": "mobile/src/terminal/terminal-key-definitions.ts", + "filePath": "mobile/src/terminal/key-input/terminal-key-definitions.ts", "kind": "object-property:label", "text": "Shift+Tab", "dynamic": false, "count": 1 }, { - "filePath": "mobile/src/terminal/terminal-key-definitions.ts", + "filePath": "mobile/src/terminal/key-input/terminal-key-definitions.ts", "kind": "object-property:label", "text": "Space", "dynamic": false, "count": 1 }, { - "filePath": "mobile/src/terminal/terminal-key-definitions.ts", + "filePath": "mobile/src/terminal/key-input/terminal-key-definitions.ts", "kind": "object-property:label", "text": "Tab", "dynamic": false, diff --git a/config/react-compiler-bailout-baseline.txt b/config/react-compiler-bailout-baseline.txt index 643bcde57d2..62b0b0c36d6 100644 --- a/config/react-compiler-bailout-baseline.txt +++ b/config/react-compiler-bailout-baseline.txt @@ -577,41 +577,41 @@ 1 src/renderer/src/components/tab-bar/use-tab-bar-create-menu-controller.ts 2 src/renderer/src/components/tab-bar/use-tab-bar-item-actions.ts 1 src/renderer/src/components/tab-group/useTabDragSplit.ts -1 src/renderer/src/components/task-page/github/AssigneesCell.tsx -2 src/renderer/src/components/task-page/github/MergeCell.tsx -1 src/renderer/src/components/task-page/github/StatusCell.tsx -1 src/renderer/src/components/task-page/github/use-task-page-github-cache-reconciliation.ts +1 src/renderer/src/components/task-page/github/listing/AssigneesCell.tsx +2 src/renderer/src/components/task-page/github/pull-requests/MergeCell.tsx +1 src/renderer/src/components/task-page/github/issue-status/StatusCell.tsx +1 src/renderer/src/components/task-page/github/cache/use-task-page-github-cache-reconciliation.ts 2 src/renderer/src/components/task-page/github/use-task-page-github-detail.ts -2 src/renderer/src/components/task-page/github/use-task-page-github-issue-creation.ts -21 src/renderer/src/components/task-page/github/use-task-page-github-issue-draft.ts -1 src/renderer/src/components/task-page/github/use-task-page-github-landing-refresh.ts -3 src/renderer/src/components/task-page/github/use-task-page-github-list-state.ts -7 src/renderer/src/components/task-page/github/use-task-page-github-mutation-state.ts -1 src/renderer/src/components/task-page/github/use-task-page-github-quiet-refresh-effect.ts -5 src/renderer/src/components/task-page/github/use-task-page-github-quiet-refresh.ts -3 src/renderer/src/components/task-page/github/use-task-page-github-search-pagination.ts -4 src/renderer/src/components/task-page/github/use-task-page-work-items-cache-selection.ts +2 src/renderer/src/components/task-page/github/issue-creation/use-task-page-github-issue-creation.ts +21 src/renderer/src/components/task-page/github/issue-creation/use-task-page-github-issue-draft.ts +1 src/renderer/src/components/task-page/github/refresh/use-task-page-github-landing-refresh.ts +3 src/renderer/src/components/task-page/github/listing/use-task-page-github-list-state.ts +7 src/renderer/src/components/task-page/github/mutations/use-task-page-github-mutation-state.ts +1 src/renderer/src/components/task-page/github/refresh/use-task-page-github-quiet-refresh-effect.ts +5 src/renderer/src/components/task-page/github/refresh/use-task-page-github-quiet-refresh.ts +3 src/renderer/src/components/task-page/github/listing/use-task-page-github-search-pagination.ts +4 src/renderer/src/components/task-page/github/cache/use-task-page-work-items-cache-selection.ts 1 src/renderer/src/components/task-page/gitlab/use-task-page-gitlab-loading.ts -2 src/renderer/src/components/task-page/jira/task-page-jira-board-view.tsx -1 src/renderer/src/components/task-page/jira/use-task-page-jira-creation-projects.ts -33 src/renderer/src/components/task-page/jira/use-task-page-jira-creation-state.ts -2 src/renderer/src/components/task-page/jira/use-task-page-jira-favourite-filters.ts -2 src/renderer/src/components/task-page/jira/use-task-page-jira-issue-creation.ts -1 src/renderer/src/components/task-page/jira/use-task-page-jira-list-effects.ts -2 src/renderer/src/components/task-page/jira/use-task-page-jira-quick-filters.ts -2 src/renderer/src/components/task-page/jira/use-task-page-jira-saved-filters.ts -2 src/renderer/src/components/task-page/jira/use-task-page-jira-summary-generation.ts -1 src/renderer/src/components/task-page/linear/IssueTable.tsx -7 src/renderer/src/components/task-page/linear/use-task-page-linear-board.ts +2 src/renderer/src/components/task-page/jira/boards/task-page-jira-board-view.tsx +1 src/renderer/src/components/task-page/jira/creation/use-task-page-jira-creation-projects.ts +33 src/renderer/src/components/task-page/jira/creation/use-task-page-jira-creation-state.ts +2 src/renderer/src/components/task-page/jira/filters/use-task-page-jira-favourite-filters.ts +2 src/renderer/src/components/task-page/jira/creation/use-task-page-jira-issue-creation.ts +1 src/renderer/src/components/task-page/jira/listing/use-task-page-jira-list-effects.ts +2 src/renderer/src/components/task-page/jira/filters/use-task-page-jira-quick-filters.ts +2 src/renderer/src/components/task-page/jira/filters/use-task-page-jira-saved-filters.ts +2 src/renderer/src/components/task-page/jira/creation/use-task-page-jira-summary-generation.ts +1 src/renderer/src/components/task-page/linear/issues/IssueTable.tsx +7 src/renderer/src/components/task-page/linear/issues/use-task-page-linear-board.ts 3 src/renderer/src/components/task-page/linear/use-task-page-linear-context-actions.ts 1 src/renderer/src/components/task-page/linear/use-task-page-linear-context-state.ts -4 src/renderer/src/components/task-page/linear/use-task-page-linear-filter-selection.ts -1 src/renderer/src/components/task-page/linear/use-task-page-linear-issue-creation.ts -1 src/renderer/src/components/task-page/linear/use-task-page-linear-list-effects.ts -6 src/renderer/src/components/task-page/linear/use-task-page-linear-list-presentation.ts -13 src/renderer/src/components/task-page/linear/use-task-page-linear-list-projection.ts -20 src/renderer/src/components/task-page/linear/use-task-page-linear-list-selection.ts -2 src/renderer/src/components/task-page/linear/use-task-page-linear-project-creation.ts +4 src/renderer/src/components/task-page/linear/issues/use-task-page-linear-filter-selection.ts +1 src/renderer/src/components/task-page/linear/creation/use-task-page-linear-issue-creation.ts +1 src/renderer/src/components/task-page/linear/issues/use-task-page-linear-list-effects.ts +6 src/renderer/src/components/task-page/linear/issues/use-task-page-linear-list-presentation.ts +13 src/renderer/src/components/task-page/linear/issues/use-task-page-linear-list-projection.ts +20 src/renderer/src/components/task-page/linear/issues/use-task-page-linear-list-selection.ts +2 src/renderer/src/components/task-page/linear/creation/use-task-page-linear-project-creation.ts 1 src/renderer/src/components/task-page/linear/use-task-page-linear-view-state.ts 10 src/renderer/src/components/task-page/use-task-page-composer-actions.ts 3 src/renderer/src/components/task-page/use-task-page-detail-routing.ts diff --git a/config/react-compiler-uncompiled-files.txt b/config/react-compiler-uncompiled-files.txt index a78f7b1b07a..c1570bdeeab 100644 --- a/config/react-compiler-uncompiled-files.txt +++ b/config/react-compiler-uncompiled-files.txt @@ -13,7 +13,7 @@ src/renderer/src/components/settings/skills/linear/LinearAgentSkillPane.tsx src/renderer/src/components/settings/tasks/use-task-source-provider-readiness.ts src/renderer/src/components/sidebar/workspace-card/details/use-worktree-card-review-details.ts src/renderer/src/components/task-page/jira/Content.tsx -src/renderer/src/components/task-page/jira/use-task-page-jira-list-projection.ts +src/renderer/src/components/task-page/jira/listing/use-task-page-jira-list-projection.ts src/renderer/src/components/task-page/jira/use-task-page-jira-list-state.ts src/renderer/src/hooks/use-integration-provider-status-refresh.ts diff --git a/config/recurring-timer-baseline.txt b/config/recurring-timer-baseline.txt index 48ae549ed1e..f8d5f8c4cd8 100644 --- a/config/recurring-timer-baseline.txt +++ b/config/recurring-timer-baseline.txt @@ -29,7 +29,7 @@ 1 src/main/hang-watchdog/main-thread-hang-watchdog.ts 2 src/main/ipc/filesystem/watcher-process/native-subscriptions/parcel-watcher-shallow-file-subscription.ts 1 src/main/ipc/filesystem/watcher-process/native-subscriptions/parcel-watcher-shallow-subscription.ts -1 src/main/ipc/runtime-environments/runtime-environment-preference.ts +1 src/main/ipc/runtime-environments/catalog/runtime-environment-preference.ts 1 src/main/ipc/worktrees/git/worktree-git-common-narrow-watch.ts 1 src/main/ipc/worktrees/git/worktree-git-common-native-upgrade-poll.ts 1 src/main/ipc/wsl/wsl-host-automation-wake-wiring.ts diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index c361edcc424..4b0e70fd71c 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -1225,12 +1225,12 @@ "invariant": "Failed bootstrap and authenticated reconnect converge without UI triggers; one connection owner publishes verified status, with no independent healthy status polling.", "oracle": "Audit 3301 removes the failed-verification/recovery/restart generation and host-contact-epoch, hosting and no-toast example from runtime-status-snapshot.test.ts. Earlier prose describing that example as retained is historical. Disconnect/pairing retirement and delayed catalog replay remain unchanged without equivalent replacement. Archive both complete historical 22-case unit receipts because their selector includes the reduced source; preserve the unrelated rendered receipt, current selectors and other-owner assertion references. No fresh aggregate or live host validation is claimed. Linux coverage from those two unit runs is historical; current covered-platform scope is limited to the preserved macOS rendered receipt. Retained local cases check independent waiter deadlines/cancellation, failed-operation recovery, stale deferred results, disposal and explicit reconnect after authentication refusal. Historical aggregate receipts predate audit1748 and are not fresh validation of reduced sources.", "commands": [ - "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/shared/remote-runtime/host-status/runtime-host-status-owner.test.ts src/main/ipc/runtime-environments/runtime-environment-status-connection.test.ts src/renderer/src/store/slices/runtime-status/runtime-status-snapshot.test.ts src/renderer/src/web/web-runtime-status-owner.test.ts", + "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/shared/remote-runtime/host-status/runtime-host-status-owner.test.ts src/main/ipc/runtime-environments/status/runtime-environment-status-connection.test.ts src/renderer/src/store/slices/runtime-status/runtime-status-snapshot.test.ts src/renderer/src/web/web-runtime-status-owner.test.ts", "ORCA_BACKGROUND_LAUNCH=1 ORCA_E2E_WEB_CLIENT=1 pnpm exec playwright test tests/e2e/runtime-host-status-recovery.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1" ], "testFiles": [ "src/shared/remote-runtime/host-status/runtime-host-status-owner.test.ts", - "src/main/ipc/runtime-environments/runtime-environment-status-connection.test.ts", + "src/main/ipc/runtime-environments/status/runtime-environment-status-connection.test.ts", "src/renderer/src/store/slices/runtime-status/runtime-status-snapshot.test.ts", "src/renderer/src/web/web-runtime-status-owner.test.ts", "tests/e2e/runtime-host-status-recovery.spec.ts" @@ -3614,11 +3614,11 @@ "https://github.com/stablyai/orca/issues/9563" ], "invariant": "A safely promotable headless serve process is the single app owner. Desktop activation preserves its daemon-backed sessions. On macOS, a CLI-supervised serve update keeps the node-mode parent alive across ShipIt's atomic bundle swap, restarts with the original serve arguments only after the target bundle is present, and clears handoff state only after that target version reports runtime readiness. Once a supervised child exits or fails to start, no late handoff completion may signal it or arm force-kill escalation. Unsupported or failed handoffs leave the current serving owner intact or recover it once without an install retry loop.", - "oracle": "Audit4362 intentionally deletes the imported stale-waiter retry and manually assembled OSC9999 mobile-republish cases, plus the mocked missing-worktree cleanup suite (4 authored / 5 finite cases in three suites). Lost alarms include repeated unread worker_done pointers, working-to-waiting mobile status publication, missing-worktree filtering, and scoped daemon shutdown with unrelated duplicate identities. No replacement coverage or new platform proof is claimed. Audit3832 deliberately retires76 authored/79 finite cases:75 runtime compatibility cases/78 finite cases across14 fragments, plus one localization extractor default-flag case. Three whole runtime fragments are deleted;27 authored/30 finite cases remain across the reviewed sources. This gate retains only its own selected scope; no equivalent replacement or fresh runtime/native/remote evidence is claimed. Runtime component A: Audit3828 composes3822 with independent3824: deliberately retires49 authored/52 finite cases from seven compatibility fragments, leaving15 authored/18 finite cases and deleting two entire fragments. Accepted losses: positive folder-worker recovery/materialization and reveal deadline; persisted-key normalization and legacy split/group/focus projection; fixed stale CAS refusal; occupied/alias/restored/preallocated terminal-handle variants; fixed OpenCode/Cursor/Claude/Codex/Antigravity classification, readiness, title and management examples; direct private-map browser authority projections; browser-focus and OMP title projection; already-aborted claim cleanup. Additional3824 loss: src/main/runtime/orca-runtime-tests/terminal-handles-and-agent-status-part-02.spec.ts :: does not poll a wrapper foreground for a speculative CLI prompt send :: Speculative retryForegroundWrappers:false check must return false on a wrapper after one foreground read, without polling the supplied later codex response. These real bounded alarms are deliberately retired for low marginal value. Retained callbacks are unchanged and provide only their own bounded scope, not equivalent replacement. Historical receipts do not validate reduced callbacks. No fresh runtime/native/SSH/WSL/phone/browser evidence or full transitive closure is claimed. Runtime component B: Audit3834 deliberately retires26 authored/26 finite cases from seven fresh compatibility fragments, leaving10/10 and deleting the one-case saved-binding reattachment fragment. Original position58 is excluded: its existing substantive3578 KEEP7 review remains historical, with no fresh credit. Accepted losses: fixed hook/provider-session identities and stale metadata refusal; decorative/pre-title foreground ordering variants; saved-binding surface reattachment; saved-row exit ghost removal and silent activation selection; unsupported headless wake; positive pending materialization/group projections; per-caller navigation selection; positive SSH hydration/read partition; laptop mobile publication/buffer and background/pending/status projections; abnormal/unregistered SSH grace, normal-exit refusal, persisted SSH hydration, positive serve reattachment, pending-agent launch arguments and direct seeded running-launch admission guard. These are real bounded alarms deliberately retired for low marginal value. Retained cases preserve their own distinct deferred foreground coalescing, usable handle continuity, stale inventory refresh, concurrent browser snapshot preservation, renderer graph fencing, caller dedup isolation, SSH partition writes, runtime split lifetime, independent relay-ready publication, lease admission/state/expiry. The direct running-launch no-spawn guard alarm is deliberately retired; no remaining owning-launch refusal coverage is claimed. Historical runs do not validate reduced callbacks. No equivalent replacement, runtime/native/SSH/WSL/phone/browser result or full transitive closure is claimed. Additional extractor loss: config/scripts/localization/source-extraction/audit-localization-native-dialogs.test.mjs :: does not change the existing renderer scan by default :: Default options must omit the five native-only fields in this supplied snippet. No equivalent replacement is claimed. Historical prior account (preserved verbatim): Audit3814 deliberately retires60 authored/60 finite cases:53runtime compatibility cases,4native XCTest methods and3Python clipboard methods, including the entire StableElementIndexes suite. Clipboard cuts retire the one-over-cap sample, incoming-offer destruction trace and sequential20-copy replay; actual wire, FD/pipe, startup and Xvfb/GTK boundaries remain without equivalent replacement claims. Accepted losses include narrow terminal-send payload/provider projections, legacy and direct no-op/error permutations, timing/work-count examples, private state/title/activity projections and fixture-driven readiness examples, individually enumerated below. These include real bounded production alarms consciously retired for marginal value; retained failed writes, duplicate submission, stale capture, transcript/ring and allocation fences are not equivalent replacements. No fresh native/remote/browser execution or complete transitive closure is claimed. Earlier dated receipts and raw selectors remain historical accounts of prior callback sets. Audit3806 deliberately retires40 authored/40 finite runtime compatibility cases, including the terminal-side-effect-facts-part03 headless-snapshots-part02 and agent-status-and-waits-part03 fragments, plus20 scalar Swift precondition sites. Lost runtime alarms include status/title/attention projection examples, simple renderer/headless frame restoration and status waiting outcomes detailed in the accepted-loss ledger. Swift cuts retire duplicate generation-map/schema/navigation/bridge and notification-wrapper assertions; no standalone native program is deleted. These include real bounded production alarms consciously retired for marginal value. Retained races, pending writes/reflow, namespace and waiter ownership are not equivalent replacements. No fresh native/remote/browser execution or full transitive closure is claimed. Earlier dated receipts and raw selectors remain historical accounts of prior callback sets. Audit3798 deliberately retires21 authored/24 finite compatibility cases, including the entire SSH-worktree-lifecycle and repository-project-operations fragments. Accepted losses include navigation-only settings acknowledgement, browser session cleanup and host preservation projections, window positive/policy-state examples, terminal recovery/reload and no-incarnation remint examples, terminal listing compatibility/malformed IDs, directory browse/sort/Windows path smoke, SSH create refusal/manual lineage, recursive parent-folder creation and fork/origin/avatar/icon projections. Real bounded production alarms are consciously retired for marginal value; retained authority races, cleanup/clone ownership, pending waiters, nested-folder and scan invalidation cases are not equivalent replacements. No fresh native/remote/browser execution or full transitive closure is claimed. Earlier dated receipts and raw selectors remain historical accounts of prior callback sets. Audit3417 composes root-adopted3415v2/independent3416v2: intentionally removes117authored/173potential expanded registrations from12semantic sources, deleting4whole conventional suites and3registration modules and retaining4authored/5expanded cases, without equivalent replacement or fresh execution. The whole main-window attach-services suite is removed: runtime/window attach, notifier wiring, reload/teardown and permission-handler mock orchestration alarms are intentionally lost. Prune its exact deleted command selectors, testFiles entries and notifier assertionRef where present; complete prior selectors/ref are archived. Unrelated dated receipts remain exact and retain their original scope; no fresh reduced-selector, native-window or remote execution is claimed. Audit3403 composes root-adopted3393/3394,3400/3401 and3398/3399: intentionally retires167 authored registrations/196 potential expanded cases across93 whole suites without equivalent replacement or fresh execution. Retained65 registrations/79 potential cases include one conditional missing-input failure sentinel; potential counts are not executions. Historical notes and complete dated receipts retain their exact original scopes. The retained runtime registration driver loses three complete imported fragments: changed-working-label title facts and suppressed-frame tracked snapshots; refusing SSH GitLab MR-base lookup before local Git/glab calls; and missing folder-workspace path creation/update refusal before spawn/store mutation. These alarms are intentionally lost. Other imported fragments and separately rooted dated receipts are unchanged; they are not equivalent replacement coverage. Audit2252: The removed synthetic lock/gate/hydration fixture supplies no current promotion continuity oracle. Existing other selectors retain only their own independent scope; changed selector execution is pending. Historical prior account (preserved verbatim): Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2090 (sources2088/2089): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2086 (sources2082/2083): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime-tests/terminal-handles-part-02.spec.ts: recovers a moved SSH pane through the tab it now sits in, not the one its lease froze; reports a plain expired orphan; does not report a pane whose only recent lease was superseded; does not report a pane whose only recent lease had its relay id recycled; picks the eligible orphan over a superseded predecessor that matches first; collects the same tabs the per-tab reader reports, in one sweep of the leases; reports no candidate when no lease names any of the worktree tabs. src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts: starts unavailable with no authoritative window; reports the configured Windows terminal shell on status; reports floating workspace availability from settings on status; polls floating tabs with targeted PTY liveness and no repo/provider inventory; hydrates persisted tabs when the store cannot report repos; advertises browser screencast only when a renderer window is available; routes mobile Codex reset consumption through the account mutation coordinator; maps a definite pre-provider rejection into an authoritative current snapshot. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2020: Retire CLI supervisor outcome, local-open promotion and paired-open projection alarms; launch retains only the two referenced stream-framing cases. Remaining CLI refusal/timeout cases do not replace headless-promotion coverage. Preserve both independent dated receipts at their original commands. Reduced selectors have no fresh result or equivalent replacement claim. Historical prior account (preserved verbatim): Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: Unit tests coalesce early activation, preserve daemon and SSH identity, and reproduce the update race with a staged target, old serving child, persistent CLI parent, atomic .app replacement, and replacement readiness message. They assert the parent does not exit for launchd to respawn the old app, the native updater does not launch an interactive GUI, the replacement version is verified before handoff completion, mismatches become durable failures without retries, and unsupported/preflight-failed installs do not invoke native quit or PTY cleanup. Force handoff completion to fail after the replacement child exits and require zero later child signals and zero escalation timers. A joined lock-owner/activation/hydration contract asserts that a forced relaunch opens exactly one window and that the renderer promoted inside the serve process launches zero agent resumes, creates no replacement tab or startup command, and leaves every surviving session record untouched. The Electron journey independently verifies headless promotion retains owner/runtime/daemon/PTY identity and terminal I/O.", + "oracle": "Audit4362 intentionally deletes the imported stale-waiter retry and manually assembled OSC9999 mobile-republish cases, plus the mocked missing-worktree cleanup suite (4 authored / 5 finite cases in three suites). Lost alarms include repeated unread worker_done pointers, working-to-waiting mobile status publication, missing-worktree filtering, and scoped daemon shutdown with unrelated duplicate identities. No replacement coverage or new platform proof is claimed. Audit3832 deliberately retires76 authored/79 finite cases:75 runtime compatibility cases/78 finite cases across14 fragments, plus one localization extractor default-flag case. Three whole runtime fragments are deleted;27 authored/30 finite cases remain across the reviewed sources. This gate retains only its own selected scope; no equivalent replacement or fresh runtime/native/remote evidence is claimed. Runtime component A: Audit3828 composes3822 with independent3824: deliberately retires49 authored/52 finite cases from seven compatibility fragments, leaving15 authored/18 finite cases and deleting two entire fragments. Accepted losses: positive folder-worker recovery/materialization and reveal deadline; persisted-key normalization and legacy split/group/focus projection; fixed stale CAS refusal; occupied/alias/restored/preallocated terminal-handle variants; fixed OpenCode/Cursor/Claude/Codex/Antigravity classification, readiness, title and management examples; direct private-map browser authority projections; browser-focus and OMP title projection; already-aborted claim cleanup. Additional3824 loss: src/main/runtime/orca-runtime-tests/terminal-handles-and-agent-status-part-02.spec.ts :: does not poll a wrapper foreground for a speculative CLI prompt send :: Speculative retryForegroundWrappers:false check must return false on a wrapper after one foreground read, without polling the supplied later codex response. These real bounded alarms are deliberately retired for low marginal value. Retained callbacks are unchanged and provide only their own bounded scope, not equivalent replacement. Historical receipts do not validate reduced callbacks. No fresh runtime/native/SSH/WSL/phone/browser evidence or full transitive closure is claimed. Runtime component B: Audit3834 deliberately retires26 authored/26 finite cases from seven fresh compatibility fragments, leaving10/10 and deleting the one-case saved-binding reattachment fragment. Original position58 is excluded: its existing substantive3578 KEEP7 review remains historical, with no fresh credit. Accepted losses: fixed hook/provider-session identities and stale metadata refusal; decorative/pre-title foreground ordering variants; saved-binding surface reattachment; saved-row exit ghost removal and silent activation selection; unsupported headless wake; positive pending materialization/group projections; per-caller navigation selection; positive SSH hydration/read partition; laptop mobile publication/buffer and background/pending/status projections; abnormal/unregistered SSH grace, normal-exit refusal, persisted SSH hydration, positive serve reattachment, pending-agent launch arguments and direct seeded running-launch admission guard. These are real bounded alarms deliberately retired for low marginal value. Retained cases preserve their own distinct deferred foreground coalescing, usable handle continuity, stale inventory refresh, concurrent browser snapshot preservation, renderer graph fencing, caller dedup isolation, SSH partition writes, runtime split lifetime, independent relay-ready publication, lease admission/state/expiry. The direct running-launch no-spawn guard alarm is deliberately retired; no remaining owning-launch refusal coverage is claimed. Historical runs do not validate reduced callbacks. No equivalent replacement, runtime/native/SSH/WSL/phone/browser result or full transitive closure is claimed. Additional extractor loss: config/scripts/localization/source-extraction/audit-localization-native-dialogs.test.mjs :: does not change the existing renderer scan by default :: Default options must omit the five native-only fields in this supplied snippet. No equivalent replacement is claimed. Historical prior account (preserved verbatim): Audit3814 deliberately retires60 authored/60 finite cases:53runtime compatibility cases,4native XCTest methods and3Python clipboard methods, including the entire StableElementIndexes suite. Clipboard cuts retire the one-over-cap sample, incoming-offer destruction trace and sequential20-copy replay; actual wire, FD/pipe, startup and Xvfb/GTK boundaries remain without equivalent replacement claims. Accepted losses include narrow terminal-send payload/provider projections, legacy and direct no-op/error permutations, timing/work-count examples, private state/title/activity projections and fixture-driven readiness examples, individually enumerated below. These include real bounded production alarms consciously retired for marginal value; retained failed writes, duplicate submission, stale capture, transcript/ring and allocation fences are not equivalent replacements. No fresh native/remote/browser execution or complete transitive closure is claimed. Earlier dated receipts and raw selectors remain historical accounts of prior callback sets. Audit3806 deliberately retires40 authored/40 finite runtime compatibility cases, including the terminal-side-effect-facts-part03 headless-snapshots-part02 and agent-status-and-waits-part03 fragments, plus20 scalar Swift precondition sites. Lost runtime alarms include status/title/attention projection examples, simple renderer/headless frame restoration and status waiting outcomes detailed in the accepted-loss ledger. Swift cuts retire duplicate generation-map/schema/navigation/bridge and notification-wrapper assertions; no standalone native program is deleted. These include real bounded production alarms consciously retired for marginal value. Retained races, pending writes/reflow, namespace and waiter ownership are not equivalent replacements. No fresh native/remote/browser execution or full transitive closure is claimed. Earlier dated receipts and raw selectors remain historical accounts of prior callback sets. Audit3798 deliberately retires21 authored/24 finite compatibility cases, including the entire SSH-worktree-lifecycle and repository-project-operations fragments. Accepted losses include navigation-only settings acknowledgement, browser session cleanup and host preservation projections, window positive/policy-state examples, terminal recovery/reload and no-incarnation remint examples, terminal listing compatibility/malformed IDs, directory browse/sort/Windows path smoke, SSH create refusal/manual lineage, recursive parent-folder creation and fork/origin/avatar/icon projections. Real bounded production alarms are consciously retired for marginal value; retained authority races, cleanup/clone ownership, pending waiters, nested-folder and scan invalidation cases are not equivalent replacements. No fresh native/remote/browser execution or full transitive closure is claimed. Earlier dated receipts and raw selectors remain historical accounts of prior callback sets. Audit3417 composes root-adopted3415v2/independent3416v2: intentionally removes117authored/173potential expanded registrations from12semantic sources, deleting4whole conventional suites and3registration modules and retaining4authored/5expanded cases, without equivalent replacement or fresh execution. The whole main-window attach-services suite is removed: runtime/window attach, notifier wiring, reload/teardown and permission-handler mock orchestration alarms are intentionally lost. Prune its exact deleted command selectors, testFiles entries and notifier assertionRef where present; complete prior selectors/ref are archived. Unrelated dated receipts remain exact and retain their original scope; no fresh reduced-selector, native-window or remote execution is claimed. Audit3403 composes root-adopted3393/3394,3400/3401 and3398/3399: intentionally retires167 authored registrations/196 potential expanded cases across93 whole suites without equivalent replacement or fresh execution. Retained65 registrations/79 potential cases include one conditional missing-input failure sentinel; potential counts are not executions. Historical notes and complete dated receipts retain their exact original scopes. The retained runtime registration driver loses three complete imported fragments: changed-working-label title facts and suppressed-frame tracked snapshots; refusing SSH GitLab MR-base lookup before local Git/glab calls; and missing folder-workspace path creation/update refusal before spawn/store mutation. These alarms are intentionally lost. Other imported fragments and separately rooted dated receipts are unchanged; they are not equivalent replacement coverage. Audit2252: The removed synthetic lock/gate/hydration fixture supplies no current promotion continuity oracle. Existing other selectors retain only their own independent scope; changed selector execution is pending. Historical prior account (preserved verbatim): Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2090 (sources2088/2089): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2086 (sources2082/2083): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime-tests/terminal-handles-part-02.spec.ts: recovers a moved SSH pane through the tab it now sits in, not the one its lease froze; reports a plain expired orphan; does not report a pane whose only recent lease was superseded; does not report a pane whose only recent lease had its relay id recycled; picks the eligible orphan over a superseded predecessor that matches first; collects the same tabs the per-tab reader reports, in one sweep of the leases; reports no candidate when no lease names any of the worktree tabs. src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts: starts unavailable with no authoritative window; reports the configured Windows terminal shell on status; reports floating workspace availability from settings on status; polls floating tabs with targeted PTY liveness and no repo/provider inventory; hydrates persisted tabs when the store cannot report repos; advertises browser screencast only when a renderer window is available; routes mobile Codex reset consumption through the account mutation coordinator; maps a definite pre-provider rejection into an authoritative current snapshot. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2020: Retire CLI supervisor outcome, local-open promotion and paired-open projection alarms; launch retains only the two referenced stream-framing cases. Remaining CLI refusal/timeout cases do not replace headless-promotion coverage. Preserve both independent dated receipts at their original commands. Reduced selectors have no fresh result or equivalent replacement claim. Historical prior account (preserved verbatim): Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: Unit tests coalesce early activation, preserve daemon and SSH identity, and reproduce the update race with a staged target, old serving child, persistent CLI parent, atomic .app replacement, and replacement readiness message. They assert the parent does not exit for launchd to respawn the old app, the native updater does not launch an interactive GUI, the replacement version is verified before handoff completion, mismatches become durable failures without retries, and unsupported/preflight-failed installs do not invoke native quit or PTY cleanup. Force handoff completion to fail after the replacement child exits and require zero later child signals and zero escalation timers. A joined lock-owner/activation/hydration contract asserts that a forced relaunch opens exactly one window and that the renderer promoted inside the serve process launches zero agent resumes, creates no replacement tab or startup command, and leaves every surviving session record untouched. The Electron journey independently verifies headless promotion retains owner/runtime/daemon/PTY identity and terminal I/O. Audit4587 retires the one-case CLI WebSocket protocol/preflight suite and its hand-built server; its cross-layer non-delivery witness is intentionally lost, with no replacement coverage claimed.", "commands": [ "pnpm exec vitest run --config config/vitest.config.ts src/cli/runtime/launch.test.ts src/main/updater/serve-update-handoff.test.ts src/main/updater/updater.headless-serve-install.test.ts src/main/updater/mac-install/updater.mac-install.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/cli/runtime/serve-signal-exit-diagnostic.test.ts", - "pnpm exec vitest run --config config/vitest.config.ts src/main/startup/serve-launch/serve-desktop-activation.test.ts src/cli/runtime/client.test.ts src/cli/runtime/websocket-transport.test.ts src/main/runtime/construction/orca-runtime.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/startup/serve-launch/serve-desktop-activation.test.ts src/cli/runtime/client.test.ts src/main/runtime/construction/orca-runtime.test.ts", "pnpm exec electron-vite build --mode e2e", "pnpm run test:e2e -- tests/e2e/headless-serve-desktop-activation.spec.ts --workers=1" ], @@ -3629,7 +3629,6 @@ "src/cli/runtime/serve-signal-exit-diagnostic.test.ts", "src/main/startup/serve-launch/serve-desktop-activation.test.ts", "src/cli/runtime/client.test.ts", - "src/cli/runtime/websocket-transport.test.ts", "src/main/runtime/construction/orca-runtime.test.ts", "tests/e2e/headless-serve-desktop-activation.spec.ts" ], @@ -5371,7 +5370,7 @@ "commands": [ "pnpm exec playwright test tests/e2e/paired-client-hosted-browser.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1", "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/browser/client-pages/runtime-browser-client-page-creation.test.ts src/main/runtime/browser-host/commands/browser-host-command-ledger.test.ts", - "pnpm exec vitest run --config config/vitest.config.ts src/shared/remote-runtime/remote-runtime-client.test.ts src/main/ipc/runtime-environments/runtime-environment-browser-client-host-handler.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/shared/remote-runtime/remote-runtime-client.test.ts src/main/ipc/runtime-environments/browser-hosting/runtime-environment-browser-client-host-handler.test.ts", "SKIP_BUILD=1 ORCA_CROSS_VERSION_PACKAGED_EXECUTABLE=/Applications/Orca.app/Contents/MacOS/Orca pnpm exec playwright test tests/e2e/packaged-mixed-version-browser-placement.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1", "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/browser/network/browser-network-tunnel-paired-runtime.integration.test.ts src/main/browser/network/execution-routes/paired-runtime-browser-network-route.test.ts src/main/browser/network/execution-routes/browser-network-execution-route.test.ts src/main/browser/network/execution-routes/wsl-browser-network-execution-route.test.ts src/main/browser/network/execution-routes/wsl-browser-network-relay-launch.test.ts src/main/runtime/browser/network/runtime-browser-network-execution-host.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/main/browser/cdp/screencast/browser-screencast-stream.test.ts src/main/runtime/browser/screencast/orca-runtime-browser-screencast-fanout.test.ts", @@ -5384,7 +5383,7 @@ "src/main/runtime/browser/client-pages/runtime-browser-client-page-creation.test.ts", "src/main/runtime/browser-host/commands/browser-host-command-ledger.test.ts", "src/shared/remote-runtime/remote-runtime-client.test.ts", - "src/main/ipc/runtime-environments/runtime-environment-browser-client-host-handler.test.ts", + "src/main/ipc/runtime-environments/browser-hosting/runtime-environment-browser-client-host-handler.test.ts", "tests/e2e/packaged-mixed-version-browser-placement.spec.ts", "src/main/runtime/browser/network/browser-network-tunnel-paired-runtime.integration.test.ts", "src/main/browser/network/execution-routes/paired-runtime-browser-network-route.test.ts", @@ -6394,9 +6393,9 @@ "https://github.com/stablyai/orca/issues/9229" ], "invariant": "Reconnect, replay, or lifecycle observations from a viewer that negotiated explicit close intent must never kill a live PTY. A capable reasonless close must keep and republish; a legacy paired viewer must retain current-main behavior because its intentional close and cleanup echo are wire-identical. Lifecycle close requires the exact observed publication, terminal, environment, and authoritative liveness, never signals a process, and leaves renderer-owned or partial-split retirement to its owner. Legacy daemon hello and warm reattachment remain non-destructive.", - "oracle": "Audit4362 intentionally deletes the imported stale-waiter retry and manually assembled OSC9999 mobile-republish cases, plus the mocked missing-worktree cleanup suite (4 authored / 5 finite cases in three suites). Lost alarms include repeated unread worker_done pointers, working-to-waiting mobile status publication, missing-worktree filtering, and scoped daemon shutdown with unrelated duplicate identities. No replacement coverage or new platform proof is claimed. Audit3881 intentionally removes the entire legacy E2EE channel and mobile socket-wiring test suites and the v2 additive runtime-capability refusal callback, without equivalent replacement. Those deleted suites leave current selectors and that v2 assertion alias is retired. Retained v2 replay/queue-counter cases do not establish the removed capability refusal. Reduced test bytes/selectors have no fresh execution in this static packet; independent dated daemon E2E receipts retain their complete original scope. Historical prior account (preserved verbatim): Audit3832 deliberately retires76 authored/79 finite cases:75 runtime compatibility cases/78 finite cases across14 fragments, plus one localization extractor default-flag case. Three whole runtime fragments are deleted;27 authored/30 finite cases remain across the reviewed sources. This gate retains only its own selected scope; no equivalent replacement or fresh runtime/native/remote evidence is claimed. Runtime component A: Audit3828 composes3822 with independent3824: deliberately retires49 authored/52 finite cases from seven compatibility fragments, leaving15 authored/18 finite cases and deleting two entire fragments. Accepted losses: positive folder-worker recovery/materialization and reveal deadline; persisted-key normalization and legacy split/group/focus projection; fixed stale CAS refusal; occupied/alias/restored/preallocated terminal-handle variants; fixed OpenCode/Cursor/Claude/Codex/Antigravity classification, readiness, title and management examples; direct private-map browser authority projections; browser-focus and OMP title projection; already-aborted claim cleanup. Additional3824 loss: src/main/runtime/orca-runtime-tests/terminal-handles-and-agent-status-part-02.spec.ts :: does not poll a wrapper foreground for a speculative CLI prompt send :: Speculative retryForegroundWrappers:false check must return false on a wrapper after one foreground read, without polling the supplied later codex response. These real bounded alarms are deliberately retired for low marginal value. Retained callbacks are unchanged and provide only their own bounded scope, not equivalent replacement. Historical receipts do not validate reduced callbacks. No fresh runtime/native/SSH/WSL/phone/browser evidence or full transitive closure is claimed. Runtime component B: Audit3834 deliberately retires26 authored/26 finite cases from seven fresh compatibility fragments, leaving10/10 and deleting the one-case saved-binding reattachment fragment. Original position58 is excluded: its existing substantive3578 KEEP7 review remains historical, with no fresh credit. Accepted losses: fixed hook/provider-session identities and stale metadata refusal; decorative/pre-title foreground ordering variants; saved-binding surface reattachment; saved-row exit ghost removal and silent activation selection; unsupported headless wake; positive pending materialization/group projections; per-caller navigation selection; positive SSH hydration/read partition; laptop mobile publication/buffer and background/pending/status projections; abnormal/unregistered SSH grace, normal-exit refusal, persisted SSH hydration, positive serve reattachment, pending-agent launch arguments and direct seeded running-launch admission guard. These are real bounded alarms deliberately retired for low marginal value. Retained cases preserve their own distinct deferred foreground coalescing, usable handle continuity, stale inventory refresh, concurrent browser snapshot preservation, renderer graph fencing, caller dedup isolation, SSH partition writes, runtime split lifetime, independent relay-ready publication, lease admission/state/expiry. The direct running-launch no-spawn guard alarm is deliberately retired; no remaining owning-launch refusal coverage is claimed. Historical runs do not validate reduced callbacks. No equivalent replacement, runtime/native/SSH/WSL/phone/browser result or full transitive closure is claimed. Additional extractor loss: config/scripts/localization/source-extraction/audit-localization-native-dialogs.test.mjs :: does not change the existing renderer scan by default :: Default options must omit the five native-only fields in this supplied snippet. No equivalent replacement is claimed. Historical prior account (preserved verbatim): Audit3814 deliberately retires60 authored/60 finite cases:53runtime compatibility cases,4native XCTest methods and3Python clipboard methods, including the entire StableElementIndexes suite. Clipboard cuts retire the one-over-cap sample, incoming-offer destruction trace and sequential20-copy replay; actual wire, FD/pipe, startup and Xvfb/GTK boundaries remain without equivalent replacement claims. Accepted losses include narrow terminal-send payload/provider projections, legacy and direct no-op/error permutations, timing/work-count examples, private state/title/activity projections and fixture-driven readiness examples, individually enumerated below. These include real bounded production alarms consciously retired for marginal value; retained failed writes, duplicate submission, stale capture, transcript/ring and allocation fences are not equivalent replacements. No fresh native/remote/browser execution or complete transitive closure is claimed. Earlier dated receipts and raw selectors remain historical accounts of prior callback sets. Audit3806 deliberately retires40 authored/40 finite runtime compatibility cases, including the terminal-side-effect-facts-part03 headless-snapshots-part02 and agent-status-and-waits-part03 fragments, plus20 scalar Swift precondition sites. Lost runtime alarms include status/title/attention projection examples, simple renderer/headless frame restoration and status waiting outcomes detailed in the accepted-loss ledger. Swift cuts retire duplicate generation-map/schema/navigation/bridge and notification-wrapper assertions; no standalone native program is deleted. These include real bounded production alarms consciously retired for marginal value. Retained races, pending writes/reflow, namespace and waiter ownership are not equivalent replacements. No fresh native/remote/browser execution or full transitive closure is claimed. Earlier dated receipts and raw selectors remain historical accounts of prior callback sets. Audit3798 deliberately retires21 authored/24 finite compatibility cases, including the entire SSH-worktree-lifecycle and repository-project-operations fragments. Accepted losses include navigation-only settings acknowledgement, browser session cleanup and host preservation projections, window positive/policy-state examples, terminal recovery/reload and no-incarnation remint examples, terminal listing compatibility/malformed IDs, directory browse/sort/Windows path smoke, SSH create refusal/manual lineage, recursive parent-folder creation and fork/origin/avatar/icon projections. Real bounded production alarms are consciously retired for marginal value; retained authority races, cleanup/clone ownership, pending waiters, nested-folder and scan invalidation cases are not equivalent replacements. No fresh native/remote/browser execution or full transitive closure is claimed. Earlier dated receipts and raw selectors remain historical accounts of prior callback sets. Audit3774 (independent peer expansion of3773): deliberately retire7authored/13potential expanded registrations across4whole suites: standalone truncated8-byte E2EE guard; open/empty readiness null fast-path; capability parser acceptance/atomic malformed-array bounds; phone/delegated-parent/desktop/in-process creator provenance projections. These are real guard/policy alarms accepted as low marginal value, not tautologies. No equivalent replacement, authenticated-refusal, observer delivery, persisted visibility, synchronous scheduling or fresh execution is claimed. Prior dated receipts retain their exact original scopes and do not validate the changed/reduced current selector. Independent daemon E2E receipts remain scoped to their own unchanged commands. Historical prior account (preserved verbatim): Audit3403 composes root-adopted3393/3394,3400/3401 and3398/3399: intentionally retires167 authored registrations/196 potential expanded cases across93 whole suites without equivalent replacement or fresh execution. Retained65 registrations/79 potential cases include one conditional missing-input failure sentinel; potential counts are not executions. Historical notes and complete dated receipts retain their exact original scopes. The retained runtime registration driver loses three complete imported fragments: changed-working-label title facts and suppressed-frame tracked snapshots; refusing SSH GitLab MR-base lookup before local Git/glab calls; and missing folder-workspace path creation/update refusal before spawn/store mutation. These alarms are intentionally lost. Other imported fragments and separately rooted dated receipts are unchanged; they are not equivalent replacement coverage. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2090 (sources2088/2089): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2086 (sources2082/2083): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime-tests/terminal-handles-part-02.spec.ts: recovers a moved SSH pane through the tab it now sits in, not the one its lease froze; reports a plain expired orphan; does not report a pane whose only recent lease was superseded; does not report a pane whose only recent lease had its relay id recycled; picks the eligible orphan over a superseded predecessor that matches first; collects the same tabs the per-tab reader reports, in one sweep of the leases; reports no candidate when no lease names any of the worktree tabs. src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts: starts unavailable with no authoritative window; reports the configured Windows terminal shell on status; reports floating workspace availability from settings on status; polls floating tabs with targeted PTY liveness and no repo/provider inventory; hydrates persisted tabs when the store cannot report repos; advertises browser screencast only when a renderer window is available; routes mobile Codex reset consumption through the account mutation coordinator; maps a definite pre-provider rejection into an authoritative current snapshot. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit1983: Delete the reasonless-close scalar suite and optional-capability socket forwarding alias. Delete the mocked close-attribution topology field-inventory suite and its aliases/selectors; preserve independent dated receipts. Changed-source prior results do not validate reduced-source bytes. No fresh execution, equivalent replacement, red/green, measured budget, soak, provider/PTY, rendered, live remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1959 (sources 1957:11, 1957:35, 1957:36): The fixture-supplied repository roundtrip, isolated capability metadata readback, parser/default-timer/size-threshold/internal failure-counter examples and standalone handshake-only replay example are removed. Source11 retains real encrypted multi-client event ordering, reconnect snapshot and multiplex delivery under synthetic runtime state, and does not establish the broad handshake-to-reasonless-close refusal alias; retire that whole assertion object. Source36 capability metadata readback alias is retired. Source35 retains the actual v2 runtime-only metadata/downgrade refusal callback and captured mutating-trace fresh-nonce rejection; its mapped mobile-v2 refusal alias remains within modeled credential lookup scope. Both independent daemon E2E dated receipts remain byte-exact with their original commands and results. No historical passed receipt is narrowed or transferred to changed source bytes. No fresh execution, equivalent replacement, physical provider/PTY, live remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed by this metadata packet. Historical prior account (preserved verbatim): Audit1939 (sources1934/1935): Retain actual parked exit/disposal/cancel/leaf progression using injected exit and close adapters. The retained last-watcher case checks exact lifecyclePtyId, but no retained callback establishes the whole existing replacement-or-sibling-incarnation alias. Retire that broad assertion object rather than transfer partial support. Capability admission, static fields and duplicate routing examples are intentionally lost; unchanged daemon E2E receipts remain scoped to their own commands. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: Start isolated v25 and v26 daemon generations with one capable-viewer PTY and one legacy-viewer PTY per generation in four target worktrees plus an unaddressed control PTY in a fifth worktree. Route them through the production desktop scanner, runtime, RPC dispatcher, renderer-close relay, and daemon router. First issue sequential reasonless closes from an authenticated capable connection and require refusal, snapshot republish, zero shutdown calls, exact process survival, and post-close I/O. Then issue the byte-identical requests without the negotiated capability and require current-main behavior: two ordered immediate shutdowns, session-killed events, and exact root/descendant death, while capable and control PTYs survive. An observer lists all targets before, between, and after while issuing zero closes. Unit contracts also require in-process reasonless refusal, legacy runtime/mobile compatibility, explicit user closes, encrypted client-auth advertisement, and old-server lifecycle calls never to fall back.", + "oracle": "Audit4362 intentionally deletes the imported stale-waiter retry and manually assembled OSC9999 mobile-republish cases, plus the mocked missing-worktree cleanup suite (4 authored / 5 finite cases in three suites). Lost alarms include repeated unread worker_done pointers, working-to-waiting mobile status publication, missing-worktree filtering, and scoped daemon shutdown with unrelated duplicate identities. No replacement coverage or new platform proof is claimed. Audit3881 intentionally removes the entire legacy E2EE channel and mobile socket-wiring test suites and the v2 additive runtime-capability refusal callback, without equivalent replacement. Those deleted suites leave current selectors and that v2 assertion alias is retired. Retained v2 replay/queue-counter cases do not establish the removed capability refusal. Reduced test bytes/selectors have no fresh execution in this static packet; independent dated daemon E2E receipts retain their complete original scope. Historical prior account (preserved verbatim): Audit3832 deliberately retires76 authored/79 finite cases:75 runtime compatibility cases/78 finite cases across14 fragments, plus one localization extractor default-flag case. Three whole runtime fragments are deleted;27 authored/30 finite cases remain across the reviewed sources. This gate retains only its own selected scope; no equivalent replacement or fresh runtime/native/remote evidence is claimed. Runtime component A: Audit3828 composes3822 with independent3824: deliberately retires49 authored/52 finite cases from seven compatibility fragments, leaving15 authored/18 finite cases and deleting two entire fragments. Accepted losses: positive folder-worker recovery/materialization and reveal deadline; persisted-key normalization and legacy split/group/focus projection; fixed stale CAS refusal; occupied/alias/restored/preallocated terminal-handle variants; fixed OpenCode/Cursor/Claude/Codex/Antigravity classification, readiness, title and management examples; direct private-map browser authority projections; browser-focus and OMP title projection; already-aborted claim cleanup. Additional3824 loss: src/main/runtime/orca-runtime-tests/terminal-handles-and-agent-status-part-02.spec.ts :: does not poll a wrapper foreground for a speculative CLI prompt send :: Speculative retryForegroundWrappers:false check must return false on a wrapper after one foreground read, without polling the supplied later codex response. These real bounded alarms are deliberately retired for low marginal value. Retained callbacks are unchanged and provide only their own bounded scope, not equivalent replacement. Historical receipts do not validate reduced callbacks. No fresh runtime/native/SSH/WSL/phone/browser evidence or full transitive closure is claimed. Runtime component B: Audit3834 deliberately retires26 authored/26 finite cases from seven fresh compatibility fragments, leaving10/10 and deleting the one-case saved-binding reattachment fragment. Original position58 is excluded: its existing substantive3578 KEEP7 review remains historical, with no fresh credit. Accepted losses: fixed hook/provider-session identities and stale metadata refusal; decorative/pre-title foreground ordering variants; saved-binding surface reattachment; saved-row exit ghost removal and silent activation selection; unsupported headless wake; positive pending materialization/group projections; per-caller navigation selection; positive SSH hydration/read partition; laptop mobile publication/buffer and background/pending/status projections; abnormal/unregistered SSH grace, normal-exit refusal, persisted SSH hydration, positive serve reattachment, pending-agent launch arguments and direct seeded running-launch admission guard. These are real bounded alarms deliberately retired for low marginal value. Retained cases preserve their own distinct deferred foreground coalescing, usable handle continuity, stale inventory refresh, concurrent browser snapshot preservation, renderer graph fencing, caller dedup isolation, SSH partition writes, runtime split lifetime, independent relay-ready publication, lease admission/state/expiry. The direct running-launch no-spawn guard alarm is deliberately retired; no remaining owning-launch refusal coverage is claimed. Historical runs do not validate reduced callbacks. No equivalent replacement, runtime/native/SSH/WSL/phone/browser result or full transitive closure is claimed. Additional extractor loss: config/scripts/localization/source-extraction/audit-localization-native-dialogs.test.mjs :: does not change the existing renderer scan by default :: Default options must omit the five native-only fields in this supplied snippet. No equivalent replacement is claimed. Historical prior account (preserved verbatim): Audit3814 deliberately retires60 authored/60 finite cases:53runtime compatibility cases,4native XCTest methods and3Python clipboard methods, including the entire StableElementIndexes suite. Clipboard cuts retire the one-over-cap sample, incoming-offer destruction trace and sequential20-copy replay; actual wire, FD/pipe, startup and Xvfb/GTK boundaries remain without equivalent replacement claims. Accepted losses include narrow terminal-send payload/provider projections, legacy and direct no-op/error permutations, timing/work-count examples, private state/title/activity projections and fixture-driven readiness examples, individually enumerated below. These include real bounded production alarms consciously retired for marginal value; retained failed writes, duplicate submission, stale capture, transcript/ring and allocation fences are not equivalent replacements. No fresh native/remote/browser execution or complete transitive closure is claimed. Earlier dated receipts and raw selectors remain historical accounts of prior callback sets. Audit3806 deliberately retires40 authored/40 finite runtime compatibility cases, including the terminal-side-effect-facts-part03 headless-snapshots-part02 and agent-status-and-waits-part03 fragments, plus20 scalar Swift precondition sites. Lost runtime alarms include status/title/attention projection examples, simple renderer/headless frame restoration and status waiting outcomes detailed in the accepted-loss ledger. Swift cuts retire duplicate generation-map/schema/navigation/bridge and notification-wrapper assertions; no standalone native program is deleted. These include real bounded production alarms consciously retired for marginal value. Retained races, pending writes/reflow, namespace and waiter ownership are not equivalent replacements. No fresh native/remote/browser execution or full transitive closure is claimed. Earlier dated receipts and raw selectors remain historical accounts of prior callback sets. Audit3798 deliberately retires21 authored/24 finite compatibility cases, including the entire SSH-worktree-lifecycle and repository-project-operations fragments. Accepted losses include navigation-only settings acknowledgement, browser session cleanup and host preservation projections, window positive/policy-state examples, terminal recovery/reload and no-incarnation remint examples, terminal listing compatibility/malformed IDs, directory browse/sort/Windows path smoke, SSH create refusal/manual lineage, recursive parent-folder creation and fork/origin/avatar/icon projections. Real bounded production alarms are consciously retired for marginal value; retained authority races, cleanup/clone ownership, pending waiters, nested-folder and scan invalidation cases are not equivalent replacements. No fresh native/remote/browser execution or full transitive closure is claimed. Earlier dated receipts and raw selectors remain historical accounts of prior callback sets. Audit3774 (independent peer expansion of3773): deliberately retire7authored/13potential expanded registrations across4whole suites: standalone truncated8-byte E2EE guard; open/empty readiness null fast-path; capability parser acceptance/atomic malformed-array bounds; phone/delegated-parent/desktop/in-process creator provenance projections. These are real guard/policy alarms accepted as low marginal value, not tautologies. No equivalent replacement, authenticated-refusal, observer delivery, persisted visibility, synchronous scheduling or fresh execution is claimed. Prior dated receipts retain their exact original scopes and do not validate the changed/reduced current selector. Independent daemon E2E receipts remain scoped to their own unchanged commands. Historical prior account (preserved verbatim): Audit3403 composes root-adopted3393/3394,3400/3401 and3398/3399: intentionally retires167 authored registrations/196 potential expanded cases across93 whole suites without equivalent replacement or fresh execution. Retained65 registrations/79 potential cases include one conditional missing-input failure sentinel; potential counts are not executions. Historical notes and complete dated receipts retain their exact original scopes. The retained runtime registration driver loses three complete imported fragments: changed-working-label title facts and suppressed-frame tracked snapshots; refusing SSH GitLab MR-base lookup before local Git/glab calls; and missing folder-workspace path creation/update refusal before spawn/store mutation. These alarms are intentionally lost. Other imported fragments and separately rooted dated receipts are unchanged; they are not equivalent replacement coverage. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2090 (sources2088/2089): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2086 (sources2082/2083): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime-tests/terminal-handles-part-02.spec.ts: recovers a moved SSH pane through the tab it now sits in, not the one its lease froze; reports a plain expired orphan; does not report a pane whose only recent lease was superseded; does not report a pane whose only recent lease had its relay id recycled; picks the eligible orphan over a superseded predecessor that matches first; collects the same tabs the per-tab reader reports, in one sweep of the leases; reports no candidate when no lease names any of the worktree tabs. src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts: starts unavailable with no authoritative window; reports the configured Windows terminal shell on status; reports floating workspace availability from settings on status; polls floating tabs with targeted PTY liveness and no repo/provider inventory; hydrates persisted tabs when the store cannot report repos; advertises browser screencast only when a renderer window is available; routes mobile Codex reset consumption through the account mutation coordinator; maps a definite pre-provider rejection into an authoritative current snapshot. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit1983: Delete the reasonless-close scalar suite and optional-capability socket forwarding alias. Delete the mocked close-attribution topology field-inventory suite and its aliases/selectors; preserve independent dated receipts. Changed-source prior results do not validate reduced-source bytes. No fresh execution, equivalent replacement, red/green, measured budget, soak, provider/PTY, rendered, live remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1959 (sources 1957:11, 1957:35, 1957:36): The fixture-supplied repository roundtrip, isolated capability metadata readback, parser/default-timer/size-threshold/internal failure-counter examples and standalone handshake-only replay example are removed. Source11 retains real encrypted multi-client event ordering, reconnect snapshot and multiplex delivery under synthetic runtime state, and does not establish the broad handshake-to-reasonless-close refusal alias; retire that whole assertion object. Source36 capability metadata readback alias is retired. Source35 retains the actual v2 runtime-only metadata/downgrade refusal callback and captured mutating-trace fresh-nonce rejection; its mapped mobile-v2 refusal alias remains within modeled credential lookup scope. Both independent daemon E2E dated receipts remain byte-exact with their original commands and results. No historical passed receipt is narrowed or transferred to changed source bytes. No fresh execution, equivalent replacement, physical provider/PTY, live remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed by this metadata packet. Historical prior account (preserved verbatim): Audit1939 (sources1934/1935): Retain actual parked exit/disposal/cancel/leaf progression using injected exit and close adapters. The retained last-watcher case checks exact lifecyclePtyId, but no retained callback establishes the whole existing replacement-or-sibling-incarnation alias. Retire that broad assertion object rather than transfer partial support. Capability admission, static fields and duplicate routing examples are intentionally lost; unchanged daemon E2E receipts remain scoped to their own commands. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: Start isolated v25 and v26 daemon generations with one capable-viewer PTY and one legacy-viewer PTY per generation in four target worktrees plus an unaddressed control PTY in a fifth worktree. Route them through the production desktop scanner, runtime, RPC dispatcher, renderer-close relay, and daemon router. First issue sequential reasonless closes from an authenticated capable connection and require refusal, snapshot republish, zero shutdown calls, exact process survival, and post-close I/O. Then issue the byte-identical requests without the negotiated capability and require current-main behavior: two ordered immediate shutdowns, session-killed events, and exact root/descendant death, while capable and control PTYs survive. An observer lists all targets before, between, and after while issuing zero closes. Unit contracts also require in-process reasonless refusal, legacy runtime/mobile compatibility, explicit user closes, encrypted client-auth advertisement, and old-server lifecycle calls never to fall back. Audit4587 retires the one-case CLI WebSocket protocol/preflight suite and its hand-built server; its cross-layer non-delivery witness is intentionally lost, with no replacement coverage claimed.", "commands": [ - "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/construction/orca-runtime.test.ts src/main/runtime/rpc/mobile/e2ee/e2ee-channel-v2.test.ts src/shared/remote-runtime/remote-runtime-client.test.ts src/shared/remote-runtime/control/remote-runtime-shared-control-connection.test.ts src/cli/runtime/websocket-transport.test.ts src/renderer/src/web/web-runtime-client.test.ts src/renderer/src/components/terminal/terminal-close-incarnation.test.ts src/renderer/src/components/terminal-pane/parking/output-watchers/terminal-parked-tab-watchers.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/construction/orca-runtime.test.ts src/main/runtime/rpc/mobile/e2ee/e2ee-channel-v2.test.ts src/shared/remote-runtime/remote-runtime-client.test.ts src/shared/remote-runtime/control/remote-runtime-shared-control-connection.test.ts src/renderer/src/web/web-runtime-client.test.ts src/renderer/src/components/terminal/terminal-close-incarnation.test.ts src/renderer/src/components/terminal-pane/parking/output-watchers/terminal-parked-tab-watchers.test.ts", "pnpm exec playwright test tests/e2e/daemon-generation-reconnect-safety.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1", "pnpm exec playwright test tests/e2e/daemon-generation-legacy-close-safety.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1" ], @@ -6405,7 +6404,6 @@ "src/main/runtime/rpc/mobile/e2ee/e2ee-channel-v2.test.ts", "src/shared/remote-runtime/remote-runtime-client.test.ts", "src/shared/remote-runtime/control/remote-runtime-shared-control-connection.test.ts", - "src/cli/runtime/websocket-transport.test.ts", "src/renderer/src/web/web-runtime-client.test.ts", "src/renderer/src/components/terminal/terminal-close-incarnation.test.ts", "src/renderer/src/components/terminal-pane/parking/output-watchers/terminal-parked-tab-watchers.test.ts", @@ -7343,14 +7341,14 @@ "invariant": "Only an explicit user change in Settings > Remote Orca Servers > Advanced > Active Server may mutate activeRuntimeEnvironmentId. Connecting, pairing, disconnecting, selecting or revealing a workspace or terminal, browser/mobile handoff, remote navigation, and reconnect must use transient or target-owner routing and must never rewrite the durable preference. Every Add Project selected host must satisfy canSelectAddRepoHost; paired web has no local filesystem fallback, so no valid host means Browse, Clone, and Create remain disabled. Every operation stays on the host selected when it began, even if preference or catalog state changes. Generic settings mutation cannot bypass the dedicated preference IPC.", "oracle": "Audit3362 retires the whole settings suite, including dirty-draft focus, background-host keepalive, protocol/catalog compatibility, stale/failure and refresh examples. The compound unit selector and test-file entry narrow; all prior owner-routing retirements/history remain. Four unrelated selected-runtime macOS E2E receipts remain verbatim and do not validate the changed unit selector. No equivalent replacement or fresh runtime/provider/UI/mobile/mixed-version/cross-platform evidence is claimed. Audit3349 retires the explicit runtime-owner, explicit SSH-owner and missing-owner-after-catalog-change examples from worktrees-fetch-owner-routing. Of those four named assertions, only the missing-owner-after-provider-reconnect assertion remains; the command and test-file selector survive. Those three alarms are intentionally lost without equivalent replacement. Four unrelated physical selected-runtime macOS E2E receipts remain verbatim; the frozen static closure does not select changed tests through their literal roots. No fresh runtime, provider, historical-import, mobile, mixed-version or cross-platform validation is claimed. Audit3326 adopts reconciled source audit3324 and intentionally retires these low-value examples and their regression alarms: drops older same-host group and folder responses that finish last — Copies group and folder fixtures plus four deferred responses to re-test the same generic catalog generation fence in two wrappers. Retires stale same-host response rejection at these API copies.; drops pre-reconnect group and folder responses without pruning the new catalog — Directly replaces pairing/catalog fixtures then resolves scripted old group/folder replies; repeats identity-fence contract in two catalog wrappers without real reconnect. Retires pre-reconnect catalog pruning refusal example.. Retained tests do not replace these alarms. Dated unrelated evidence remains scoped to its original execution; no historical source/import-closure equivalence, fresh execution, real network/provider/SSH, rendered UI, mobile, mixed-version or cross-platform validation is claimed. Audit2371 reduces all-host-folder-workspace-catalogs.test.ts by removing the repeated offline compatibility-probe budget and its incidental local-fallback/restored-owner samples, and catalog-settlement-notifications.test.ts by removing the four-route ten-equal-refresh notification/reference matrix. Those probe, quietness, identity and fallback alarms are intentionally lost. Remaining stale-response and revision-fencing examples do not supply equivalent replacement coverage. No fresh execution or red/green evidence validates the reduced sources; the four unrelated physical selected-runtime E2E receipts retain their original commands and scope. Historical prior account (preserved verbatim): Audit2360 removes src/renderer/src/hooks/useIpcEvents-terminal-create-surfacing.test.ts and its exclusive fake-effect harness. Lose local default-runtime persistence across navigation; focused/background terminal request/create/reveal and no-focus recovery; startup cwd/config/agent/view-mode forwarding; explicit runtime-session routing; remote/ambiguous/missing owner refusal; history focus; existing PTY reuse/title preservation; pending tab PTY/layout/identity adoption; split tree, focus, identity and telemetry samples. No equivalent replacement claimed. Changed selectors have no fresh execution or red/green credit. Unrelated retained receipt objects keep their original narrow scope. Historical prior account (preserved verbatim): Audit2056 (sources2054/2055): Intentionally retire these low-value alarms without replacement: src/renderer/src/store/project-groups/nested-repository-operations.test.ts: routes local nested scan progress by scanId and unsubscribes after completion. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Historical prior account (preserved verbatim): Retained exact unchanged-source receipts remain the only current execution evidence. Reduced selectors have no fresh execution result. Historical pre-audit oracle account for changed sources: The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): Start with Active Server=Local desktop, connect and navigate Windows 2, then reveal a local terminal and assert it succeeds while the persisted preference remains local before and after restart. Repeat transient routing and selected-runtime Add Project operations with exact-ID host decoys and completion reordering. Separately pair the real web client to an isolated headed host, then make its sole still-listed runtime compatibility-blocked and unavailable. Require a null authoritative selection, a disabled host option, and disabled Browse, Clone, and Create actions with no local fallback. Repeat against headless serve. Run the byte-identical selector/action contract on latest main, candidate, and candidate with the eligibility seam disabled.", "commands": [ - "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/runtime-environments/runtime-environments-pairing.test.ts src/renderer/src/store/slices/browser/hosts/browser-session-host-selection.test.ts src/renderer/src/web/web-preload-api-runtime-environment.test.ts --maxWorkers=1", + "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/runtime-environments/pairing/runtime-environments-pairing.test.ts src/renderer/src/store/slices/browser/hosts/browser-session-host-selection.test.ts src/renderer/src/web/web-preload-api-runtime-environment.test.ts --maxWorkers=1", "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/repos/ssh-repo-readoption-drafts.test.ts src/renderer/src/store/folder-workspaces/folder-workspace-readoption-drafts.test.ts src/renderer/src/store/repos/repo-catalog-readoption-drafts.test.ts src/renderer/src/store/folder-workspaces/all-host-folder-workspace-catalogs.test.ts src/renderer/src/store/host-catalog/catalog-settlement-notifications.test.ts src/renderer/src/store/repos/repos-stale-fetch.test.ts src/renderer/src/store/folder-workspaces/folder-workspace-routing.test.ts src/renderer/src/store/repos/repos-selected-owner-routing.test.ts src/renderer/src/store/slices/worktrees/session/selected-host-active-workspace-identity.test.ts src/renderer/src/store/slices/worktrees/listing/worktrees-fetch-owner-routing.test.ts --maxWorkers=1", "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/sidebar/project-admission/dialog/AddRepoStartSteps.test.tsx --maxWorkers=1", "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/pr11346-selected-runtime-add.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1 --reporter=line", "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/pr11346-selected-runtime-add.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --reporter=line" ], "testFiles": [ - "src/main/ipc/runtime-environments/runtime-environments-pairing.test.ts", + "src/main/ipc/runtime-environments/pairing/runtime-environments-pairing.test.ts", "src/renderer/src/store/slices/browser/hosts/browser-session-host-selection.test.ts", "src/renderer/src/web/web-preload-api-runtime-environment.test.ts", "src/renderer/src/components/sidebar/project-admission/dialog/AddRepoStartSteps.test.tsx", @@ -8224,7 +8222,7 @@ "ORCA_BACKGROUND_LAUNCH=1 pnpm exec playwright test tests/e2e/paired-client-first-terminal-unopened-host-worktree.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1", "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/session-lifecycle/publication/mobile-session-tabs-agent-status-heartbeat.test.ts tests/e2e/session-tabs-decorative-title-fanout.unit.test.ts --maxWorkers=1", "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/parking/lifecycle/use-terminal-tab-cold-parking.test.ts --maxWorkers=1", - "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/construction/orca-runtime.test.ts src/main/runtime/rpc/terminal/terminal-multiplex-initial-snapshot-buffering.test.ts src/main/runtime/rpc/terminal/terminal-multiplex-pty-wait-capacity.test.ts src/main/runtime/rpc/terminal/terminal-subscribe-buffer.test.ts src/renderer/src/components/terminal-pane/pty-transport/output/terminal-side-effect-facts-handler.test.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-activation-inventory-fallback.test.ts src/renderer/src/components/terminal-pane/parking/retention/terminal-hidden-view-parking.test.ts src/renderer/src/components/terminal-pane/parking/retention/terminal-hidden-worktree-retention.test.ts src/renderer/src/components/terminal-pane/parking/output-watchers/terminal-parked-tab-watchers.test.ts src/renderer/src/runtime/rpc/subscriptions/runtime-client-events.test.ts src/renderer/src/web/web-preload-api-runtime-environment.test.ts src/main/ipc/runtime-environments/runtime-environments-pairing.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/construction/orca-runtime.test.ts src/main/runtime/rpc/terminal/terminal-multiplex-initial-snapshot-buffering.test.ts src/main/runtime/rpc/terminal/terminal-multiplex-pty-wait-capacity.test.ts src/main/runtime/rpc/terminal/terminal-subscribe-buffer.test.ts src/renderer/src/components/terminal-pane/pty-transport/output/terminal-side-effect-facts-handler.test.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-activation-inventory-fallback.test.ts src/renderer/src/components/terminal-pane/parking/retention/terminal-hidden-view-parking.test.ts src/renderer/src/components/terminal-pane/parking/retention/terminal-hidden-worktree-retention.test.ts src/renderer/src/components/terminal-pane/parking/output-watchers/terminal-parked-tab-watchers.test.ts src/renderer/src/runtime/rpc/subscriptions/runtime-client-events.test.ts src/renderer/src/web/web-preload-api-runtime-environment.test.ts src/main/ipc/runtime-environments/pairing/runtime-environments-pairing.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/terminal/adoption/terminal-subscriber-driven-daemon-attach.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/session-tabs/runtime-inventory-publication-authority.test.ts src/main/runtime/rpc/methods/session-tabs/session-tabs-inventory-census-race.test.ts src/main/daemon/terminal-host/testing/terminal-host-agent-session.test.ts tests/e2e/session-tabs-empty-inventory-daemon-oracle.unit.test.ts --maxWorkers=1", "pnpm exec vitest run --config config/vitest.config.ts src/shared/remote-runtime/control/remote-runtime-shared-control-connection.test.ts", @@ -8247,7 +8245,7 @@ "src/renderer/src/components/terminal-pane/parking/lifecycle/use-terminal-tab-cold-parking.test.ts", "src/main/runtime/construction/orca-runtime.test.ts", "src/renderer/src/web/web-preload-api-runtime-environment.test.ts", - "src/main/ipc/runtime-environments/runtime-environments-pairing.test.ts", + "src/main/ipc/runtime-environments/pairing/runtime-environments-pairing.test.ts", "src/main/runtime/rpc/terminal/terminal-multiplex-initial-snapshot-buffering.test.ts", "src/main/runtime/rpc/terminal/terminal-multiplex-pty-wait-capacity.test.ts", "src/main/runtime/rpc/terminal/terminal-subscribe-buffer.test.ts", @@ -9343,14 +9341,14 @@ "https://github.com/stablyai/orca/issues/12902" ], "invariant": "SSH, WSL, and remote-runtime restore paths must treat provider listing failures and unknown liveness as unknown, not dead, while still avoiding duplicate spawn and clearing expired relay leases exactly once. Direct SSH disconnect, reconnect, and wake must keep PTY bindings and relay identity, and target snapshot hydration/reconnect cannot reset sibling SSH, local, WSL, or runtime-owned state. Every restored remote terminal must preserve its provider PTY identity, including the authoritative incarnation returned by a successful session-ID reattach. After a recoverable partition the same authenticated runtime must reattach the same PTY, reject detached input, apply the latest viewport, and report healthy only after authoritative replay. A successful one-shot reachability probe may replace a pre-ready shared-control socket, but waiting RPCs must continue onto the replacement under their original deadline without duplicate host delivery or retained request bytes. Automatic PTY recovery stops after one bounded minute without a fatal terminal error; a manual reconnect starts a newly fenced epoch against the same PTY, and closed panes retain no recovery UI state. Successful current-transport recovery must clear every stale error surfaced for that pane without clearing or displaying a sibling pane's error; a later genuine failure must remain visible. Each transport retains at most eight suppressed messages, each pane retains at most eight distinct messages, and the rendered surface retains at most 24 lines and 4,000 characters. One capability-gated terminal-create mutation must produce at most one host PTY across an unknown response outcome, remain manually retryable after cutoff, and never let a stale completion replace a newer pane lifecycle. Reconnect must alternate exact activation with authoritative inventory so neither a stale activation response nor an activation failure can strand or retire a pane, and activating a parked surface whose persisted binding was already retired must respawn it rather than report a changed owner after signalling its exit.", - "oracle": "Audit3390 composes root-adopted3382/independent3383 and3386/independent3387 and3388/independent3389: intentionally retires216 authored registrations/230 expanded cases in42 whole authored suites plus5 zero-case registration drivers, without equivalent replacement. Lost local alarms include fake PTY identity/routing/startup/replay/mode/title/mouse/IME/scroll matrices, broad startup identity and file-open wiring, mock mailbox title recovery, mobile row parity and whole-v-selective save parity. Retained24 registrations/26 expanded cases in22 suites preserve actual xterm Windows buffer/wrap/scroll, typed TOML scalar, real Promise cancellation/settlement, HTTP Codex scanner/projector semantics, durable recovery admission/newer-schema bytes, federation completion/ACK/SQLite atomicity and failed stale-run-write versus imported durable history. Additional retained clock-domain scheduling, durable freeze/ACK/checkpoint ordering, ABA dirty intent, SQL atomicity, folder-domain retier/last-note deletion, scrollback durable no-sync staging, PanelBridge subscriber lifetime and reserved-send publication preserve narrower concrete behaviors. Current finite owner drift assessed in bounded defining bodies; retained callback bytes unchanged. Historical notes and complete dated receipts keep their original exact scopes. No new execution, equivalent replacement, native focus/device/pixel, live remote or global mixed-version guarantee is claimed. Audit3368 narrows stream-reconnect coverage to the actual ten synthetic transport epochs retaining one PTY identity, accepting and holding detached input for exactly-once later delivery, current decoded output and per-epoch subscription release. Prior rejection wording was inaccurate. Phase/backoff/cutoff/manual reconnect and parser/OSC/pause/retained-image/fatal/latch/claim/early-close alarms are intentionally lost without equivalent replacement. The Linux multi-pane reconnect receipt remains verbatim with its original scope; no fresh live SSH/provider/PTY/UI/mobile/mixed-version/cross-platform evidence is claimed. Audit3359 retires whole host-surface-replacement and create-handoff suites, removing their compound unit command tokens, test-file entries and assertion references. Fake polling/deadline/subscription, supplied stale-handle ownership, close and replacement examples are intentionally lost without equivalent replacement. The unrelated Linux paired multi-pane reconnect receipt stays verbatim with its original scope. No fresh SSH/provider/network/PTY recovery, historical-import, mobile, mixed-version or cross-platform validation is claimed. Audit3342 retires the complete client create-outcome identity/options/recovery suite and the malformed-or-contradictory repo-owner response matrix. Client replay identity, options, no-downgrade, quiescence/budget/manual retry, authoritative probe-error and malformed-owner refusal alarms are intentionally lost without equivalent replacement. Remaining repo-owner late catalog and old-host re-adoption boundaries do not replace them. The unrelated Linux paired-reconnect receipt stays verbatim with its original scope. Surviving selectors are unexecuted here; no fresh SSH/provider/process-deduplication, mobile, mixed-version or cross-platform validation is claimed. Audit3326 adopts reconciled source audit3324 and intentionally retires these low-value examples and their regression alarms: keeps a sidebar row for every still-live pane after a long sleep — Whole suite: 279 lines manually replay renderer ownership registry/status writes, snapshot patch/set, six-pane fixture inventory, and sidebar selectors/builders; expected pane IDs are production-computed. Does not run sleep/wake/transport or actual sidebar entry. Retires reconnect row-preservation assembly alarm.; still retires the regular TUI surface after an explicit terminal exit — Ordinary explicit-exit callback/map cleanup is repeated in the stale/new stream lifecycle case below. Retires separate regular-TUI baseline; keeper is not equivalent process/UI proof.; asks the HUB to recover an expired SSH pane and rebinds the host identity — Expired SSH owner error must ask the owning HUB to recover, then adopt fixed replacement handle, resubscribe and report rebind; actual transport dispatch and handle state prevent wrong-client recovery.; does not recover a pane whose relay reply was an identity mismatch — Identity mismatch means a different live PTY, so actual transport must surface refusal without recover/create, keeping original identity. Prevents destructive duplicate agent recovery.; fails closed when an older HUB cannot recover an expired SSH pane — Old HUB method_not_found is scripted mock refusal/error echo with no actual skewed host; repeats generic recovery failure propagation. Retires old-host recovery fallback refusal example.; names a held recovery's stopped WSL distro in readable copy, not the raw hold code — Pins exact WSL stopped-distro error prose after mock code; translation/copy assertion with no actual held recovery behavior. Retires readable-copy alarm.. Retained tests do not replace these alarms. Dated unrelated evidence remains scoped to its original execution; no historical source/import-closure equivalence, fresh execution, real network/provider/SSH, rendered UI, mobile, mixed-version or cross-platform validation is claimed. Audit 3318 reduces runtime-environment-ssh-state.test.ts, intentionally losing metadata membership-pruning/new-target-only-read and other-environment/local-bucket isolation alarms. Preserve generation/failed-full-refresh assertion and unknown-target authoritative-list reread; these narrower cases are not equivalent replacements. The unchanged active two-case Linux reconnect materialization/sidebar receipt and covered platform retain their exact scope and do not validate the reduced SSH state selector. Preserve unrelated SSH assertions, selectors and archived history. No fresh SSH discovery, provider/PTY/network, rendered, mobile, mixed-version, cross-platform or full-gate execution is claimed. Audit 3312 retires the ordinary six-pane distinct-live-handle reconnect/resubscription alarm from paired-reconnect-multi-pane-materialization.test.ts. The unchanged parked-pane activation/rematerialization case remains narrower and is not an equivalent replacement. The complete prior seven-contract macOS receipt is archived as historical evidence; it does not validate the reduced selector. No fresh socket, PTY, sleep/wake, provider, platform, mobile, mixed-version or rendered execution is claimed. Audit3096: remove the deleted reconnect-banner selector and its two UI aliases. The reduced recovery-state selector retains only the deadline cancelling pending backoff and explicit retry owning a new epoch alias; healthy-replacement stale-schedule and independent caller-cutoff cancellation aliases retire. Other retained state cases cover reentrant retry, owner settlement and same-epoch parked revival, without replacing those removed alarms. Prior admission-selector retirement, archived July 28 receipt, unrelated selectors/claims/receipts and old gaps remain unchanged; no fresh execution is claimed. Audit3086: retire the destroyed fresh-session fallback settlement/non-destructive reattach alias and its direct selector; retain the detach/attach selector and assertion unchanged. The retained admission cases cover adopt-before-data and explicitly buffered-death admission without a new spawn, without replacing fallback retirement. July 28 evidence was already archived by audit3077; no duplicate retirement or new execution is claimed. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/ssh/connection/ssh-connection-manager.test.ts: tears a failed startup down quietly, so its error is not replaced by a disconnect; marks a dial that never reached the host, so setup reports SSH’s reason; removes the registration when bulk teardown fails; leaves a connection whose own reconnect is asking the user alone; clears the pool entry when the closed connection is still the registered one; still reuses a connection that is already up; dials a user's connection test. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2052 (sources2049/2050/2051): Remove complete low-value restored-pane-title and pane-visibility-identity suites and their current selectors. Their supplied snapshot/title and nullable identity-comparison alarms are intentionally lost without replacement. Unrelated dated evidenceRuns remain unchanged and do not validate the reduced commands. No fresh gate execution, actual PTY/provider, rendered, remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1983: Delete the terminal-create idempotency forwarding suite and its prospective selector. Preserve2 unrelated receipts without claiming the lost forwarding alarm was replaced. Changed-source prior results do not validate reduced-source bytes. No fresh execution, equivalent replacement, red/green, measured budget, soak, provider/PTY, rendered, live remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1931 (sources1928/1929): Delete complete transport-unmount and queued-initial-CWD helper suites: despite lifecycle-shaped fixtures, the unmount owner returns a Boolean PTY-id predicate, while CWD cases check nullish field selection and callback counts. Lose their detach/destroy/teardown and one-shot-CWD supplied examples. Partial queued-pane-startup loses three identical/empty/nonowner identity examples, retaining four exact one-shot/replacement ownership callbacks. No assertion object maps to these three sources, so unrelated alias objects remain byte-exact. Preserve the current81ed prior removal of pane-split-startup; stale bd948 source metadata snapshots are not replayed. No actual process/PTY startup, transport shutdown, live SSH or rendered recovery is proven by this metadata change. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1925 (sources1922/1923): Delete the sole supplied public-SSH-state spread/sanitized-error projection and its provider-authority-pair alias. Also delete the complete tiny one-shot split-startup wrapper suite: callback-visible temporary field/reset, sequential payload and thrown-sentinel reset alarms disappear. No SSH provider epoch handoff, connection, process startup or actual terminal split was performed by either deleted suite. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1903 removes whole mocked detach, supplied geometry and/or close-request suites selected by this gate. Their pending-CWD/PTY-binding argument/order, fabricated hit-test and close bookkeeping alarms are intentionally lost without replacement. Valid remaining selectors/assertions and unchanged-selector execution receipts retain their original scope. Reduced unit selections have no fresh broad gate, real PTY/provider, rendered focus, or measured-budget validation. Historical prior account (preserved verbatim): Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): Deterministic tests cover bounded stale-handle replacement, suspended heartbeat clocks, cold and established subscription failure, ten partition/recovery cycles, automatic-recovery cutoff, manual reconnect, and direct SSH binding retention. They assert SSH state changes leave PTY bindings in place, relay identity survives, and another target/local/WSL/runtime panes remain byte-identical through target snapshot hydration and reconnect. Both remount callbacks consume split-count activity suppression, intentional dispose emits no failure/timeout, and a same-attempt StrictMode remount still owns one timeout. Hydration clears an untrusted PTY hint without clearing its current pending owner, healthy current-authority bindings suppress correction, hydration finalizes once, and reconnect emits no paired close lifecycle. A provider-level session-ID reattach returns an incarnation, then a legacy exit without an incarnation must resolve to that returned identity rather than minting a fallback identity. The shared-control oracle withholds the first encrypted ready frame, starts one RPC, triggers the successful-probe refresh, then requires exactly two client connections, one host request, a successful response, zero pending calls, and zero retained request bytes. Tests also assert one unsubscribe per remote-runtime epoch, observable recovery phases, stable PTY identity, resumed snapshot/output/input, no healthy state before replay, no retry or input after cutoff, a new manual epoch against the same PTY, quiet recovery UI with an explicit Reconnect action, pane-close state cleanup, one stable create mutation id, old-runtime no-retry behavior, cross-process PTY adoption, and bounded in-flight coordination. A focused recovery oracle surfaces two distinct failures through one transport, completes authoritative replay, and requires both matching clear callbacks; pane-state tests require only the active pane's error to render, matching-pane recovery to preserve sibling and unrelated errors, dismissal to re-admit later failures, and distinct storms to retain only the newest eight messages within 24 lines and 4,000 characters. The retained store replacement assertion observes one atomic pane/tab subscriber publication; repeated rotations and snapshot-first pending-restart migration no longer have their direct store examples.", + "oracle": "Audit3390 composes root-adopted3382/independent3383 and3386/independent3387 and3388/independent3389: intentionally retires216 authored registrations/230 expanded cases in42 whole authored suites plus5 zero-case registration drivers, without equivalent replacement. Lost local alarms include fake PTY identity/routing/startup/replay/mode/title/mouse/IME/scroll matrices, broad startup identity and file-open wiring, mock mailbox title recovery, mobile row parity and whole-v-selective save parity. Retained24 registrations/26 expanded cases in22 suites preserve actual xterm Windows buffer/wrap/scroll, typed TOML scalar, real Promise cancellation/settlement, HTTP Codex scanner/projector semantics, durable recovery admission/newer-schema bytes, federation completion/ACK/SQLite atomicity and failed stale-run-write versus imported durable history. Additional retained clock-domain scheduling, durable freeze/ACK/checkpoint ordering, ABA dirty intent, SQL atomicity, folder-domain retier/last-note deletion, scrollback durable no-sync staging, PanelBridge subscriber lifetime and reserved-send publication preserve narrower concrete behaviors. Current finite owner drift assessed in bounded defining bodies; retained callback bytes unchanged. Historical notes and complete dated receipts keep their original exact scopes. No new execution, equivalent replacement, native focus/device/pixel, live remote or global mixed-version guarantee is claimed. Audit3368 narrows stream-reconnect coverage to the actual ten synthetic transport epochs retaining one PTY identity, accepting and holding detached input for exactly-once later delivery, current decoded output and per-epoch subscription release. Prior rejection wording was inaccurate. Phase/backoff/cutoff/manual reconnect and parser/OSC/pause/retained-image/fatal/latch/claim/early-close alarms are intentionally lost without equivalent replacement. The Linux multi-pane reconnect receipt remains verbatim with its original scope; no fresh live SSH/provider/PTY/UI/mobile/mixed-version/cross-platform evidence is claimed. Audit3359 retires whole host-surface-replacement and create-handoff suites, removing their compound unit command tokens, test-file entries and assertion references. Fake polling/deadline/subscription, supplied stale-handle ownership, close and replacement examples are intentionally lost without equivalent replacement. The unrelated Linux paired multi-pane reconnect receipt stays verbatim with its original scope. No fresh SSH/provider/network/PTY recovery, historical-import, mobile, mixed-version or cross-platform validation is claimed. Audit3342 retires the complete client create-outcome identity/options/recovery suite and the malformed-or-contradictory repo-owner response matrix. Client replay identity, options, no-downgrade, quiescence/budget/manual retry, authoritative probe-error and malformed-owner refusal alarms are intentionally lost without equivalent replacement. Remaining repo-owner late catalog and old-host re-adoption boundaries do not replace them. The unrelated Linux paired-reconnect receipt stays verbatim with its original scope. Surviving selectors are unexecuted here; no fresh SSH/provider/process-deduplication, mobile, mixed-version or cross-platform validation is claimed. Audit3326 adopts reconciled source audit3324 and intentionally retires these low-value examples and their regression alarms: keeps a sidebar row for every still-live pane after a long sleep — Whole suite: 279 lines manually replay renderer ownership registry/status writes, snapshot patch/set, six-pane fixture inventory, and sidebar selectors/builders; expected pane IDs are production-computed. Does not run sleep/wake/transport or actual sidebar entry. Retires reconnect row-preservation assembly alarm.; still retires the regular TUI surface after an explicit terminal exit — Ordinary explicit-exit callback/map cleanup is repeated in the stale/new stream lifecycle case below. Retires separate regular-TUI baseline; keeper is not equivalent process/UI proof.; asks the HUB to recover an expired SSH pane and rebinds the host identity — Expired SSH owner error must ask the owning HUB to recover, then adopt fixed replacement handle, resubscribe and report rebind; actual transport dispatch and handle state prevent wrong-client recovery.; does not recover a pane whose relay reply was an identity mismatch — Identity mismatch means a different live PTY, so actual transport must surface refusal without recover/create, keeping original identity. Prevents destructive duplicate agent recovery.; fails closed when an older HUB cannot recover an expired SSH pane — Old HUB method_not_found is scripted mock refusal/error echo with no actual skewed host; repeats generic recovery failure propagation. Retires old-host recovery fallback refusal example.; names a held recovery's stopped WSL distro in readable copy, not the raw hold code — Pins exact WSL stopped-distro error prose after mock code; translation/copy assertion with no actual held recovery behavior. Retires readable-copy alarm.. Retained tests do not replace these alarms. Dated unrelated evidence remains scoped to its original execution; no historical source/import-closure equivalence, fresh execution, real network/provider/SSH, rendered UI, mobile, mixed-version or cross-platform validation is claimed. Audit 3318 reduces runtime-environment-ssh-state.test.ts, intentionally losing metadata membership-pruning/new-target-only-read and other-environment/local-bucket isolation alarms. Preserve generation/failed-full-refresh assertion and unknown-target authoritative-list reread; these narrower cases are not equivalent replacements. The unchanged active two-case Linux reconnect materialization/sidebar receipt and covered platform retain their exact scope and do not validate the reduced SSH state selector. Preserve unrelated SSH assertions, selectors and archived history. No fresh SSH discovery, provider/PTY/network, rendered, mobile, mixed-version, cross-platform or full-gate execution is claimed. Audit 3312 retires the ordinary six-pane distinct-live-handle reconnect/resubscription alarm from paired-reconnect-multi-pane-materialization.test.ts. The unchanged parked-pane activation/rematerialization case remains narrower and is not an equivalent replacement. The complete prior seven-contract macOS receipt is archived as historical evidence; it does not validate the reduced selector. No fresh socket, PTY, sleep/wake, provider, platform, mobile, mixed-version or rendered execution is claimed. Audit3096: remove the deleted reconnect-banner selector and its two UI aliases. The reduced recovery-state selector retains only the deadline cancelling pending backoff and explicit retry owning a new epoch alias; healthy-replacement stale-schedule and independent caller-cutoff cancellation aliases retire. Other retained state cases cover reentrant retry, owner settlement and same-epoch parked revival, without replacing those removed alarms. Prior admission-selector retirement, archived July 28 receipt, unrelated selectors/claims/receipts and old gaps remain unchanged; no fresh execution is claimed. Audit3086: retire the destroyed fresh-session fallback settlement/non-destructive reattach alias and its direct selector; retain the detach/attach selector and assertion unchanged. The retained admission cases cover adopt-before-data and explicitly buffered-death admission without a new spawn, without replacing fallback retirement. July 28 evidence was already archived by audit3077; no duplicate retirement or new execution is claimed. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/ssh/connection/ssh-connection-manager.test.ts: tears a failed startup down quietly, so its error is not replaced by a disconnect; marks a dial that never reached the host, so setup reports SSH’s reason; removes the registration when bulk teardown fails; leaves a connection whose own reconnect is asking the user alone; clears the pool entry when the closed connection is still the registered one; still reuses a connection that is already up; dials a user's connection test. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2052 (sources2049/2050/2051): Remove complete low-value restored-pane-title and pane-visibility-identity suites and their current selectors. Their supplied snapshot/title and nullable identity-comparison alarms are intentionally lost without replacement. Unrelated dated evidenceRuns remain unchanged and do not validate the reduced commands. No fresh gate execution, actual PTY/provider, rendered, remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1983: Delete the terminal-create idempotency forwarding suite and its prospective selector. Preserve2 unrelated receipts without claiming the lost forwarding alarm was replaced. Changed-source prior results do not validate reduced-source bytes. No fresh execution, equivalent replacement, red/green, measured budget, soak, provider/PTY, rendered, live remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1931 (sources1928/1929): Delete complete transport-unmount and queued-initial-CWD helper suites: despite lifecycle-shaped fixtures, the unmount owner returns a Boolean PTY-id predicate, while CWD cases check nullish field selection and callback counts. Lose their detach/destroy/teardown and one-shot-CWD supplied examples. Partial queued-pane-startup loses three identical/empty/nonowner identity examples, retaining four exact one-shot/replacement ownership callbacks. No assertion object maps to these three sources, so unrelated alias objects remain byte-exact. Preserve the current81ed prior removal of pane-split-startup; stale bd948 source metadata snapshots are not replayed. No actual process/PTY startup, transport shutdown, live SSH or rendered recovery is proven by this metadata change. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1925 (sources1922/1923): Delete the sole supplied public-SSH-state spread/sanitized-error projection and its provider-authority-pair alias. Also delete the complete tiny one-shot split-startup wrapper suite: callback-visible temporary field/reset, sequential payload and thrown-sentinel reset alarms disappear. No SSH provider epoch handoff, connection, process startup or actual terminal split was performed by either deleted suite. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1903 removes whole mocked detach, supplied geometry and/or close-request suites selected by this gate. Their pending-CWD/PTY-binding argument/order, fabricated hit-test and close bookkeeping alarms are intentionally lost without replacement. Valid remaining selectors/assertions and unchanged-selector execution receipts retain their original scope. Reduced unit selections have no fresh broad gate, real PTY/provider, rendered focus, or measured-budget validation. Historical prior account (preserved verbatim): Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): Deterministic tests cover bounded stale-handle replacement, suspended heartbeat clocks, cold and established subscription failure, ten partition/recovery cycles, automatic-recovery cutoff, manual reconnect, and direct SSH binding retention. They assert SSH state changes leave PTY bindings in place, relay identity survives, and another target/local/WSL/runtime panes remain byte-identical through target snapshot hydration and reconnect. Both remount callbacks consume split-count activity suppression, intentional dispose emits no failure/timeout, and a same-attempt StrictMode remount still owns one timeout. Hydration clears an untrusted PTY hint without clearing its current pending owner, healthy current-authority bindings suppress correction, hydration finalizes once, and reconnect emits no paired close lifecycle. A provider-level session-ID reattach returns an incarnation, then a legacy exit without an incarnation must resolve to that returned identity rather than minting a fallback identity. The shared-control oracle withholds the first encrypted ready frame, starts one RPC, triggers the successful-probe refresh, then requires exactly two client connections, one host request, a successful response, zero pending calls, and zero retained request bytes. Tests also assert one unsubscribe per remote-runtime epoch, observable recovery phases, stable PTY identity, resumed snapshot/output/input, no healthy state before replay, no retry or input after cutoff, a new manual epoch against the same PTY, quiet recovery UI with an explicit Reconnect action, pane-close state cleanup, one stable create mutation id, old-runtime no-retry behavior, cross-process PTY adoption, and bounded in-flight coordination. A focused recovery oracle surfaces two distinct failures through one transport, completes authoritative replay, and requires both matching clear callbacks; pane-state tests require only the active pane's error to render, matching-pane recovery to preserve sibling and unrelated errors, dismissal to re-admit later failures, and distinct storms to retain only the newest eight messages within 24 lines and 4,000 characters. The retained store replacement assertion observes one atomic pane/tab subscriber publication; repeated rotations and snapshot-first pending-restart migration no longer have their direct store examples. Preload layout cleanup retires the mock SSH forwarding/type-census suite without replacement: preload wiring alarms are lost; shared authority-admission tests cover narrower behavior. Historical receipts retain their original scope.", "commands": [ "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/paired-reconnect-multi-pane-materialization.test.ts --reporter=dot", "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal/background-terminal-worktree-mount.test.ts src/renderer/src/components/terminal-pane/mounting/queued-pane-startup.test.ts src/renderer/src/components/terminal-pane/parking/one-shot-park-replay.test.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-stream-reconnect.test.ts src/renderer/src/store/terminals/terminal-pty-identity-replacement.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-detach-attach-handoff.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-stream-reconnect.test.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-expired-pane-recovery.test.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/recovery/remote-runtime-pty-recovery-state.test.ts src/shared/remote-runtime/remote-runtime-socket-liveness.test.ts src/shared/remote-runtime/control/remote-runtime-shared-control-connection.test.ts src/shared/remote-runtime/control/remote-runtime-shared-control-socket-generation.test.ts src/shared/remote-runtime/remote-runtime-client-error-classification.test.ts src/main/runtime/terminal/launch/orca-runtime-terminal-create-idempotency.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/ui/workspace-navigation-history-hydration.test.ts src/renderer/src/store/repos/repos-ssh-host-reconciliation.test.ts --reporter=dot", - "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/ssh/ssh.test.ts src/main/ssh/connection/ssh-connection-manager.test.ts src/main/ssh/connection/ssh-connection.test.ts src/preload/api/ssh-authority-forwarding.test.ts src/renderer/src/runtime/rpc/subscriptions/runtime-client-events.test.ts src/renderer/src/runtime/hosts/ssh/runtime-environment-ssh-state.test.ts src/shared/ssh-retained-payload-admission.test.ts --reporter=dot", + "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/ssh/ssh.test.ts src/main/ssh/connection/ssh-connection-manager.test.ts src/main/ssh/connection/ssh-connection.test.ts src/renderer/src/runtime/rpc/subscriptions/runtime-client-events.test.ts src/renderer/src/runtime/hosts/ssh/runtime-environment-ssh-state.test.ts src/shared/ssh-retained-payload-admission.test.ts --reporter=dot", "pnpm exec electron-vite build --mode e2e", "pnpm run build:web-from-renderer", "SKIP_BUILD=1 pnpm exec playwright test tests/e2e/paired-remote-terminal-materialization-reconnect.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1" @@ -9376,7 +9374,6 @@ "src/main/ipc/ssh/ssh.test.ts", "src/main/ssh/connection/ssh-connection-manager.test.ts", "src/main/ssh/connection/ssh-connection.test.ts", - "src/preload/api/ssh-authority-forwarding.test.ts", "src/renderer/src/runtime/rpc/subscriptions/runtime-client-events.test.ts", "src/renderer/src/runtime/hosts/ssh/runtime-environment-ssh-state.test.ts", "src/shared/ssh-retained-payload-admission.test.ts" @@ -9427,14 +9424,6 @@ "late ssh2 ready and startup error events after disconnect cannot resurrect or overwrite disconnected state" ] }, - { - "file": "src/preload/api/ssh-authority-forwarding.test.ts", - "assertions": [ - "full authority crosses Electron IPC without loss", - "partial authority becomes unknown for bounded reconciliation and malformed full authority is dropped", - "variable-form host-qualified worktree requests retain fail-closed outcomes in their return type" - ] - }, { "file": "src/shared/ssh-retained-payload-admission.test.ts", "assertions": [ diff --git a/config/scripts/archives/platform-command/zip-extractor-command.test.mjs b/config/scripts/archives/platform-command/zip-extractor-command.test.mjs index df80ea918b0..c555261cab7 100644 --- a/config/scripts/archives/platform-command/zip-extractor-command.test.mjs +++ b/config/scripts/archives/platform-command/zip-extractor-command.test.mjs @@ -1,17 +1,12 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { afterEach, describe, expect, it, vi } from 'vitest' +import { afterEach, describe, expect, it } from 'vitest' import { runProcessSync } from '@orca/process-host' -import { - getTarProgram, - getZipExtractorCommand -} from '../../../../src/shared/zip-extractor-command.ts' +import { getZipExtractorCommand } from '../../../../src/shared/zip-extractor-command.ts' const directories = [] afterEach(() => { - vi.unstubAllEnvs() - vi.restoreAllMocks() for (const directory of directories.splice(0)) { rmSync(directory, { recursive: true, force: true }) } @@ -30,32 +25,6 @@ function extract(bytes) { } describe('native archive extraction', () => { - it('pins Windows tar to the system tool even when Git Bash or an unzip override is present', () => { - vi.spyOn(process, 'platform', 'get').mockReturnValue('win32') - vi.stubEnv('SystemRoot', 'C:\\Windows') - vi.stubEnv('ORCA_UNZIP_BIN', 'custom-unzip') - expect(getTarProgram()).toBe(join('C:\\Windows', 'System32', 'tar.exe')) - vi.spyOn(process, 'platform', 'get').mockReturnValue('linux') - expect(getTarProgram()).toBe('tar') - }) - - it('uses the system archive reader on Windows unless an override is configured', () => { - vi.spyOn(process, 'platform', 'get').mockReturnValue('win32') - vi.stubEnv('SystemRoot', 'C:\\Windows') - vi.stubEnv('ORCA_UNZIP_BIN', '') - expect(getZipExtractorCommand('source.zip', 'output')).toEqual({ - file: join('C:\\Windows', 'System32', 'tar.exe'), - args: ['-xf', 'source.zip', '-C', 'output'], - label: 'tar' - }) - vi.stubEnv('ORCA_UNZIP_BIN', 'C:\\tools\\unzip.exe') - expect(getZipExtractorCommand("source '$.zip", "output '$")).toEqual({ - file: 'C:\\tools\\unzip.exe', - args: ['-q', "source '$.zip", '-d', "output '$"], - label: 'unzip' - }) - }) - it('extracts through paths containing spaces, apostrophes and shell characters', () => { const { result, destination } = extract( Buffer.from( @@ -66,9 +35,4 @@ describe('native archive extraction', () => { expect(result.code, result.stderr).toBe(0) expect(readFileSync(join(destination, 'payload.txt'), 'utf8')).toBe('verified archive payload') }) - - it('fails on a malformed archive', () => { - const { result } = extract('invalid archive') - expect(result.code).not.toBe(0) - }) }) diff --git a/config/scripts/archives/seven-zip/resolve-7za-path.test.mjs b/config/scripts/archives/seven-zip/resolve-7za-path.test.mjs index 4e43241d45a..f2c297de7ca 100644 --- a/config/scripts/archives/seven-zip/resolve-7za-path.test.mjs +++ b/config/scripts/archives/seven-zip/resolve-7za-path.test.mjs @@ -1,5 +1,5 @@ import { spawnSync } from 'node:child_process' -import { existsSync, mkdirSync, mkdtempSync, rmSync, statSync, writeFileSync } from 'node:fs' +import { existsSync, mkdtempSync, rmSync, statSync } from 'node:fs' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import { describe, expect, it } from 'vitest' @@ -8,51 +8,6 @@ import { resolve7zaPath } from './resolve-7za-path.mjs' const projectRoot = resolve(import.meta.dirname, '../../../..') describe('7za path resolution for the Windows signing gates (#6487)', () => { - // Match upstream mac/win/linux architecture dirs; darwin/ or collapsed-arch guesses silently fail to resolve. - - it('resolves an executable 7za that can extract an archive', async () => { - const path7za = await resolve7zaPath(projectRoot) - expect(existsSync(path7za)).toBe(true) - - const scratch = mkdtempSync(join(tmpdir(), 'orca 7za resolve ')) - try { - const payloadDir = join(scratch, 'payload') - mkdirSync(payloadDir, { recursive: true }) - writeFileSync(join(payloadDir, 'Orca.exe'), 'not-a-real-pe') - - const archive = join(scratch, 'bundle.7z') - const created = spawnSync(path7za, ['a', archive, payloadDir], { encoding: 'utf8' }) - expect(created.status).toBe(0) - - const outDir = join(scratch, 'out') - const extracted = spawnSync(path7za, ['x', archive, `-o${outDir}`, '-y'], { - encoding: 'utf8' - }) - expect(extracted.status).toBe(0) - expect(existsSync(join(outDir, 'payload', 'Orca.exe'))).toBe(true) - } finally { - rmSync(scratch, { recursive: true, force: true }) - } - }, 120_000) - - it('prefers an explicit ELECTRON_BUILDER_7ZIP_PATH override', async () => { - const scratch = mkdtempSync(join(tmpdir(), 'orca 7za override ')) - const previous = process.env.ELECTRON_BUILDER_7ZIP_PATH - try { - const fake = join(scratch, 'my7za') - writeFileSync(fake, '#!/bin/sh\n') - process.env.ELECTRON_BUILDER_7ZIP_PATH = fake - await expect(resolve7zaPath(projectRoot)).resolves.toBe(fake) - } finally { - if (previous === undefined) { - delete process.env.ELECTRON_BUILDER_7ZIP_PATH - } else { - process.env.ELECTRON_BUILDER_7ZIP_PATH = previous - } - rmSync(scratch, { recursive: true, force: true }) - } - }) - // Use directory override: Test-Path accepts it, masking bad resolver validation until extraction fails. it('ignores an override that points at a directory', async () => { const scratch = mkdtempSync(join(tmpdir(), 'orca 7za dir override ')) @@ -72,8 +27,6 @@ describe('7za path resolution for the Windows signing gates (#6487)', () => { } }, 120_000) - // Overlap calls to global stdout patch; naive save/restore can permanently restore another caller’s diverting stub. - // Use fresh subprocess: app-builder-lib memoizes toolset, hiding dangling-override failures in warm process. it('ignores an override that points at a missing file, in a cold process', () => { const dangling = join(tmpdir(), 'orca-7za-does-not-exist') @@ -94,22 +47,4 @@ describe('7za path resolution for the Windows signing gates (#6487)', () => { expect(resolved).not.toBe(dangling) expect(existsSync(resolved)).toBe(true) }, 120_000) - - it('prints exactly one clean line the PowerShell gate can consume', () => { - const result = spawnSync( - process.execPath, - ['config/scripts/archives/seven-zip/resolve-7za-path.mjs'], - { - cwd: projectRoot, - encoding: 'utf8', - timeout: 120_000 - } - ) - - expect(result.status).toBe(0) - expect(result.stdout.trimEnd().split('\n')).toHaveLength(1) - expect(existsSync(result.stdout.trim())).toBe(true) - }, 120_000) - - // Unset VITEST with cold cache: builder-util suppresses download logs in tests, hiding multiline stdout that breaks PowerShell path capture. }) diff --git a/config/scripts/artifact-storage/reflink-copy/space-sharing-copy.test.ts b/config/scripts/artifact-storage/reflink-copy/space-sharing-copy.test.ts index abb4c29875f..fbf91f4022c 100644 --- a/config/scripts/artifact-storage/reflink-copy/space-sharing-copy.test.ts +++ b/config/scripts/artifact-storage/reflink-copy/space-sharing-copy.test.ts @@ -71,14 +71,6 @@ function makeTree(): { root: string; source: string } { } describe('shareTree', () => { - it('keeps relative symlinks unresolved on whatever this host supports', () => { - const { root, source } = makeTree() - const destination = path.join(root, 'shared') - expect(shareTree(source, destination)).toBeTruthy() - expect(readFileSync(path.join(destination, 'nested', 'file'), 'utf8')).toBe('contents') - expect(readlinkSync(path.join(destination, 'relative-link'))).toBe(path.join('nested', 'file')) - }) - it('clears a part-way tree before trying the next mechanism', () => { const { root, source } = makeTree() const destination = path.join(root, 'shared') @@ -92,27 +84,7 @@ describe('shareTree', () => { }) }) -describe('hardlinkTree', () => { - it('shares inodes for files but recreates symlinks as their own entries', () => { - const { root, source } = makeTree() - const destination = path.join(root, 'linked') - hardlinkTree(source, destination) - expect(statSync(path.join(destination, 'nested', 'file')).ino).toBe( - statSync(path.join(source, 'nested', 'file')).ino - ) - expect(readlinkSync(path.join(destination, 'relative-link'))).toBe(path.join('nested', 'file')) - }) -}) - describe('makeTreeReadOnly', () => { - it('drops write permission on files while leaving directories traversable and unlinkable', () => { - const { source } = makeTree() - makeTreeReadOnly(source) - expect(statSync(path.join(source, 'nested', 'file')).mode & 0o222).toBe(0) - // Assert rename/remove behavior, not mode bits: Windows chmod maps only read-only attribute and directories never report 0755. - expect(() => rmSync(path.join(source, 'nested'), { recursive: true })).not.toThrow() - }) - it('turns an extract-over-dist write into an error instead of silent shared corruption', () => { const { root, source } = makeTree() const destination = path.join(root, 'linked') @@ -146,16 +118,4 @@ describe('copyPrivateTree', () => { expect(readFileSync(path.join(source, 'nested', 'file'), 'utf8')).toBe('contents') } ) - - it('falls back to a byte copy when the private mechanism fails', () => { - const { root, source } = makeTree() - const destination = path.join(root, 'private') - const clone = () => { - throw new Error('clonefile unsupported') - } - const result = copyPrivateTree(source, destination, { platform: 'darwin', clone }) - expect(result.mechanism).toBeNull() - expect(result.copyError).toBeInstanceOf(Error) - expect(readFileSync(path.join(destination, 'nested', 'file'), 'utf8')).toBe('contents') - }) }) diff --git a/config/scripts/build/mobile-web-app/build-mobile-web-app-bundle.test.mjs b/config/scripts/build/mobile-web-app/build-mobile-web-app-bundle.test.mjs index a1d27b2dbec..d3f0de6a741 100644 --- a/config/scripts/build/mobile-web-app/build-mobile-web-app-bundle.test.mjs +++ b/config/scripts/build/mobile-web-app/build-mobile-web-app-bundle.test.mjs @@ -36,7 +36,6 @@ describeBundling('the app bundle', () => { join(directory, 'index.tsx'), 'export default function Route() { return null }\n' ) - await expect(bundleMobileWebApp({ appDir: scratch })).resolves.toBeTruthy() await writeFile( join(directory, 'settings.tsx'), 'const anchor = { anchor: "index" }\nexport { anchor as unstable_settings }\nexport default function Route() { return null }\n' @@ -90,24 +89,6 @@ describeBundling('the app bundle', () => { }) describe('the verifier', () => { - itBundling( - "rejects a buildId the manifest's own asset list does not derive", - async () => { - await withScratch(async (scratch) => { - const outDir = join(scratch, 'mobile-web') - await copyWrittenBundle(outDir) - const manifestPath = join(outDir, 'manifest.json') - const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) - manifest.buildId = 'f'.repeat(64) - await writeFile(manifestPath, JSON.stringify(manifest), 'utf8') - await expect(verifyMobileWebAppBundle({ bundleDir: outDir })).rejects.toThrow( - 'does not match its asset list' - ) - }) - }, - 240_000 - ) - itBundling( 'rejects a self-consistent bundle a fresh build does not reproduce', async () => { diff --git a/config/scripts/build/mobile-web-app/mobile-web-bundle-serializer-parity.test.mjs b/config/scripts/build/mobile-web-app/mobile-web-bundle-serializer-parity.test.mjs deleted file mode 100644 index 713666a94b7..00000000000 --- a/config/scripts/build/mobile-web-app/mobile-web-bundle-serializer-parity.test.mjs +++ /dev/null @@ -1,49 +0,0 @@ -import { createRequire } from 'node:module' -import { describe, expect, it } from 'vitest' -import { serializeMobileWebBundleAssets as serializeInBuilder } from './mobile-web-bundle-manifest.mjs' -import { serializeMobileWebBundleAssets as serializeInContract } from '../../../../src/shared/mobile-web-bundle/manifest-contract' - -const require = createRequire(import.meta.url) -const { - serializeAssets: serializeInGuard -} = require('../../desktop-distribution/runtime-verification/verify-packaged-mobile-web-bundle.cjs') - -const digest = (hex) => `${hex}`.padStart(64, '0') - -// Mixed types/nested paths and uppercase ordering expose localeCompare drift from canonical code-unit sort. -const ASSETS = [ - { - path: 'assets/Za.js', - sha256: digest('a1'), - byteLength: 2048, - contentType: 'text/javascript; charset=utf-8' - }, - { path: 'assets/aQ.css', sha256: digest('b2'), byteLength: 512, contentType: 'text/css' }, - { - path: 'assets/nested/mark.png', - sha256: digest('c3'), - byteLength: 40_960, - contentType: 'image/png' - }, - { - path: 'index.html', - sha256: digest('d4'), - byteLength: 640, - contentType: 'text/html; charset=utf-8' - } -] - -describe('the three mobile web bundle serializers', () => { - it('emit exactly path, sha256, byteLength, contentType, in that order, and nothing else', () => { - const decorated = ASSETS.map((asset) => ({ ...asset, sourcePath: '/tmp/ignored', extra: 1 })) - - const expected = - '[{"path":"assets/Za.js","sha256":"00000000000000000000000000000000000000000000000000000000000000a1","byteLength":2048,"contentType":"text/javascript; charset=utf-8"},' + - '{"path":"assets/aQ.css","sha256":"00000000000000000000000000000000000000000000000000000000000000b2","byteLength":512,"contentType":"text/css"},' + - '{"path":"assets/nested/mark.png","sha256":"00000000000000000000000000000000000000000000000000000000000000c3","byteLength":40960,"contentType":"image/png"},' + - '{"path":"index.html","sha256":"00000000000000000000000000000000000000000000000000000000000000d4","byteLength":640,"contentType":"text/html; charset=utf-8"}]' - for (const serialize of [serializeInContract, serializeInBuilder, serializeInGuard]) { - expect(serialize(decorated)).toBe(expected) - } - }) -}) diff --git a/config/scripts/ci/e2e-selection/e2e-cli-artifact-capability.test.mjs b/config/scripts/ci/e2e-selection/e2e-cli-artifact-capability.test.mjs deleted file mode 100644 index 3d81ed517d1..00000000000 --- a/config/scripts/ci/e2e-selection/e2e-cli-artifact-capability.test.mjs +++ /dev/null @@ -1,48 +0,0 @@ -import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { delimiter, join } from 'node:path' -import { parse } from 'yaml' -import { describe, expect, it } from 'vitest' -import { runProcessSync } from '@orca/process-host' - -const workflow = parse(readFileSync('.github/workflows/e2e.yml', 'utf8')) -const build = workflow.jobs.build.steps.find((step) => step.id === 'e2e-cli') - -describe('shared E2E CLI capability', () => { - it.skipIf(process.platform === 'win32').each([ - [false, 0, 0], - [true, 0, 0], - [true, 7, 7] - ])('supported=%s build status=%s exits=%s', (supported, buildStatus, exitStatus) => { - const directory = mkdtempSync(join(tmpdir(), 'orca-e2e-cli-capability-')) - const marker = join(directory, 'build-requested') - try { - writeFileSync( - join(directory, 'package.json'), - JSON.stringify({ scripts: supported ? { 'prepare:cli-output': 'available' } : {} }) - ) - const pnpm = join(directory, 'pnpm') - writeFileSync(pnpm, '#!/bin/sh\nprintf "%s" "$*" > "$BUILD_MARKER"\nexit "$BUILD_STATUS"\n') - chmodSync(pnpm, 0o755) - const result = runProcessSync({ - program: 'bash', - args: ['-e', '-o', 'pipefail', '-c', build.run], - cwd: directory, - env: { - ...process.env, - PATH: `${directory}${delimiter}${process.env.PATH}`, - BUILD_MARKER: marker, - BUILD_STATUS: String(buildStatus) - }, - timeoutMs: 10_000 - }) - expect(result.code, JSON.stringify(result)).toBe(exitStatus) - expect(existsSync(marker)).toBe(supported) - if (supported) { - expect(readFileSync(marker, 'utf8')).toBe('run build:cli') - } - } finally { - rmSync(directory, { recursive: true, force: true }) - } - }) -}) diff --git a/config/scripts/ci/sharding/ci-shard-assignment.test.mjs b/config/scripts/ci/sharding/ci-shard-assignment.test.mjs index 0e3056504ab..73bcb0c103d 100644 --- a/config/scripts/ci/sharding/ci-shard-assignment.test.mjs +++ b/config/scripts/ci/sharding/ci-shard-assignment.test.mjs @@ -1,6 +1,5 @@ import { describe, expect, it } from 'vitest' import { discoverE2eFiles, planE2e } from '../e2e-selection/ci-e2e-shard-plan.mjs' -import { parseTimingLog } from './ci-shard-timing-import.mjs' describe('timing-weighted shard selection', () => { it('keeps nested/serial E2E files atomic and fails closed on discovery errors', () => { @@ -18,7 +17,6 @@ describe('timing-weighted shard selection', () => { 'one.spec.ts', 'two.spec.ts' ]) - expect(plan.testsByFile['one.spec.ts']).toHaveLength(2) expect(() => discoverE2eFiles({ ...report, errors: [{}] })).toThrow('errors') expect(() => discoverE2eFiles({ suites: [] })).toThrow('no tests') expect(() => @@ -30,25 +28,4 @@ describe('timing-weighted shard selection', () => { }) ).toThrow('Duplicate') }) - - it('imports ANSI unit timings and E2E failures without counting headful reruns', () => { - const parsed = parseTimingLog( - [ - '\u001b[32m✓\u001b[39m src/a.test.ts (2 tests) 35ms', - 'Duration 1s (transform 0.1s, setup 0.2s, import 0.3s, tests 0.04s, environment 0.4s)', - '✓ 1 [electron-headless] › tests/e2e/a.spec.ts:1:1 › works (2s)', - '✘ 2 [electron-headless] › tests/e2e/a.spec.ts:2:1 › fails (1.2m)', - '✓ 3 [electron-headful] › tests/e2e/a.spec.ts:3:1 › benchmark (9s)' - ].join('\n') - ) - expect(parsed).toEqual({ - unit: { 'src/a.test.ts': 35 }, - e2e: { 'tests/e2e/a.spec.ts': 74000 }, - overheadMs: 1000 - }) - }) - - it('rejects incomplete unit evidence instead of silently dropping overhead', () => { - expect(() => parseTimingLog('✓ src/a.test.ts (2 tests) 35ms')).toThrow('Duration summary') - }) }) diff --git a/config/scripts/ci/sharding/ci-shard-timings.json b/config/scripts/ci/sharding/ci-shard-timings.json index b19b9bd1a84..e919f4ba0ce 100644 --- a/config/scripts/ci/sharding/ci-shard-timings.json +++ b/config/scripts/ci/sharding/ci-shard-timings.json @@ -40,7 +40,6 @@ "config/scripts/static-analysis/process-boundaries/check-runtime-electron-ratchet-graph.test.mjs": 160, "config/scripts/static-analysis/process-boundaries/check-runtime-electron-ratchet.test.mjs": 3540, "config/scripts/daemon-protocol/admission/check-runtime-launcher-protocol-ratchet.test.mjs": 212, - "config/scripts/performance/terminal-reporting/check-terminal-perf-report-budgets.test.mjs": 1600, "config/scripts/ci/unit-selection/check-test-file-runners.test.mjs": 267, "config/scripts/ci/workflow-contracts/ci-closed-pr-cancellation.test.mjs": 112, "config/scripts/ci/workflow-contracts/ci-draft-pr-admission.test.mjs": 546, @@ -76,18 +75,14 @@ "config/scripts/ci/e2e-selection/e2e-cli-artifact-capability.test.mjs": 233, "config/scripts/desktop-distribution/runtime-verification/electron-builder-config.test.mjs": 387, "config/scripts/desktop-distribution/runtime-verification/electron-builder-mac-channel-config.test.mjs": 309, - "config/scripts/desktop-distribution/runtime-verification/electron-builder-native-rebuild.test.mjs": 47, "config/scripts/desktop-distribution/runtime-verification/electron-builder-runtime-resources.test.mjs": 2505, "config/scripts/desktop-distribution/runtime-verification/electron-builder-speech-config.test.mjs": 212, "config/scripts/desktop-build/output-contracts/electron-vite-output-contract.test.ts": 77, "config/scripts/native-build/node-addons/ensure-native-runtime-job-ownership.test.mjs": 76, "config/scripts/native-build/node-addons/ensure-native-runtime.test.mjs": 908, "config/scripts/runtime-verification/ephemeral-store/ephemeral-vm-runtime-store-cross-version.test.ts": 31, - "config/scripts/orcad-distribution/server-bundle/foreign-sqlite-reader-worker-smoke.test.mjs": 491, "config/scripts/skills/bundle-generation/generate-skill-bundle-manifest.test.mjs": 956, "config/scripts/workspace-package-build/generated-artifact-line-ending-pin.test.mjs": 61, - "config/scripts/diagnostics/hang-watchdog/hang-watchdog-process-metrics.test.mjs": 94, - "config/scripts/runtime-verification/headless-detector/headless-detector-compiler-cache.test.mjs": 959, "config/scripts/ci/workflow-contracts/hourly-preflight-workflow.test.mjs": 158, "config/scripts/performance/idle-cpu/idle-cpu-process-sampling.test.mjs": 55, "config/scripts/static-analysis/oxlint-contracts/ime-enter-guard-plugin.test.mjs": 222, @@ -96,11 +91,7 @@ "config/scripts/diagnostics/lag-capture/lag-probe-failures.test.mjs": 55, "config/scripts/dependency-security/runtime-resolution/linear-sdk-runtime.test.mjs": 773, "config/scripts/preflight/changed-checks/lint-staged-worktree-backup.test.mjs": 278, - "config/scripts/locale-catalog/locale-brand-prefilter.test.mjs": 246, - "config/scripts/locale-catalog/locale-generic-ui-terms.test.mjs": 102, "config/scripts/locale-catalog/locale-repair-catalog-missing-leaves.test.mjs": 159, - "config/scripts/locale-catalog/locale-translation-policy.es-pr.test.mjs": 91, - "config/scripts/locale-catalog/locale-translation-policy.test.mjs": 189, "config/scripts/localization/source-extraction/localization-extraction-call-syntax.test.mjs": 317, "config/scripts/localization/extraction-policy/localization-extraction-change-scope.test.mjs": 732, "config/scripts/performance/macos-computer-owner/macos-computer-helper-owner-loss-processes.test.mjs": 52, @@ -110,7 +101,6 @@ "config/scripts/desktop-build/output-contracts/main-entry-facade.test.ts": 492, "config/scripts/runtime-verification/managed-account/managed-data-account-runtime.test.ts": 1381, "config/scripts/orcad-distribution/prebuilds/merge-orcad-prebuilds.test.mjs": 67, - "config/scripts/mobile-web/check-entry/mobile-process-host-tooling.test.mjs": 942, "config/scripts/mobile-web/check-entry/mobile-release-check-scope.test.mjs": 821, "config/scripts/mobile-web-app/shell/mobile-web-app-agent-icon-render.test.mjs": 591, "config/scripts/mobile-web-app/shell/mobile-web-app-browser-pane-render.test.mjs": 239, @@ -132,7 +122,6 @@ "config/scripts/mobile-web-app/shell/mobile-web-app-render.test.mjs": 254, "config/scripts/mobile-web-app/session/mobile-web-app-rich-markdown-render.test.mjs": 356, "config/scripts/mobile-web-app/mobile-web-app-route-manifest.test.mjs": 174, - "config/scripts/mobile-web-app/mobile-web-app-route-snapshot.test.mjs": 121, "config/scripts/mobile-web-app/shell/mobile-web-app-safe-area-render.test.mjs": 314, "config/scripts/mobile-web-app/session/mobile-web-app-screencast-lane-grant.test.mjs": 70, "config/scripts/mobile-web-app/session/mobile-web-app-session-dictation-capture.test.mjs": 123, @@ -173,7 +162,6 @@ "config/scripts/development/launch/orca-dev-bin.test.mjs": 110, "config/scripts/orcad-distribution/server-bundle/orcad-entry-build.test.mjs": 7008, "config/scripts/orcad-distribution/server-bundle/orcad-linear-sdk-package.test.mjs": 376, - "config/scripts/orcad-distribution/server-bundle/orcad-mobile-web-bundle.test.mjs": 174, "config/scripts/orcad-distribution/prebuilds/orcad-prebuild-slot-contents.test.mjs": 79, "config/scripts/orcad-distribution/prebuilds/orcad-windows-prebuild-cache.test.mjs": 18343, "config/scripts/desktop-distribution/runtime-verification/package-electron-runtime-contract.test.mjs": 220, @@ -182,7 +170,6 @@ "config/scripts/desktop-distribution/runtime-verification/packaged-node-pty-prebuild-prune.test.mjs": 74, "config/scripts/desktop-distribution/runtime-verification/packaged-process-host.test.mjs": 537, "config/scripts/desktop-distribution/runtime-verification/packaged-repository-input-exclusion.test.mjs": 222, - "config/scripts/desktop-distribution/runtime-verification/packaged-source-map-prune.test.mjs": 73, "config/scripts/desktop-distribution/runtime-verification/packaged-windows-only-runtime-files.test.mjs": 214, "config/scripts/dependency-security/audit-tools/patched-dependencies-frozen-install.test.mjs": 749, "config/scripts/web-build/pdf-viewer-assets/pdfjs-viewer-assets-stream.test.ts": 296, @@ -233,7 +220,6 @@ "config/scripts/tool-invocation/script-entry-detection.test.mjs": 107, "config/scripts/test-fixtures/script-copy-closure/script-module-dependencies.test.mjs": 62, "config/scripts/ci/workflow-contracts/setup-pnpm-action.test.mjs": 421, - "config/scripts/electron-installation/shared-dist-cache/shared-electron-dist-cache.test.ts": 140, "config/scripts/native-build/node-addons/shared-native-build.test.mjs": 127, "config/scripts/artifact-storage/atomic-publication/shared-tree-publish.test.mjs": 63, "config/scripts/desktop-distribution/macos/sign-mac-app.test.mjs": 268, @@ -242,25 +228,17 @@ "config/scripts/desktop-distribution/appimage/static-appimage-package-contract.test.mjs": 88, "config/scripts/desktop-build/transform-contracts/sucrase-unused-transformers.test.ts": 43, "config/scripts/performance/terminal-reporting/terminal-perf-report-annotations.test.mjs": 54, - "config/scripts/desktop-distribution/windows/trim-windows-icon-source.test.mjs": 353, "config/scripts/dependency-security/runtime-resolution/undeclared-dependency-resolution.test.ts": 149, "config/scripts/node-runtime/archive-identity/update-node-runtime-pin.test.mjs": 87, - "config/scripts/plugin-authoring/content-validation/validate-plugin-files.test.mjs": 2422, "config/scripts/cli-build/verify-cli-bin.test.mjs": 135, "config/scripts/desktop-distribution/channel-policy/verify-dev-channel-packaging.test.mjs": 213, - "config/scripts/native-build/linux/verify-linux-glibc-floor.test.mjs": 156, "config/scripts/localization/catalog-validation/verify-localization-catalog.test.mjs": 422, - "config/scripts/localization/catalog-validation/verify-localization-catalogs.test.mjs": 1574, - "config/scripts/localization/source-extraction/verify-localization-extraction.test.mjs": 233, - "config/scripts/desktop-distribution/runtime-verification/verify-packaged-daemon-entry.test.mjs": 2009, "config/scripts/desktop-distribution/runtime-verification/verify-packaged-mobile-web-bundle.test.mjs": 214, "config/scripts/desktop-distribution/runtime-verification/verify-packaged-node-pty-job-ownership.test.mjs": 93, "config/scripts/orcad-distribution/template/verify-packaged-orcad-template.test.mjs": 454, - "config/scripts/desktop-distribution/runtime-verification/verify-packaged-plugin-resources.test.mjs": 101, "config/scripts/desktop-distribution/runtime-verification/verify-packaged-serve-payload.test.mjs": 83, "config/scripts/desktop-distribution/runtime-verification/verify-packaged-windows-agent-hook.test.mjs": 43, "config/scripts/runtime-verification/skills-cli/verify-skills-cli-runtime.test.mjs": 213, - "config/scripts/test-runtime/module-cache/vitest-module-cache-resolution-key.test.ts": 50, "config/scripts/test-runtime/node-worker/vitest-node-runtime-pool.test.ts": 560, "config/scripts/test-runtime/environment/vitest-real-agent-home-write-guard.test.ts": 71, "config/scripts/test-runtime/execution-policy/vitest-run-temp-directory.test.mjs": 51, @@ -279,7 +257,6 @@ "config/scripts/ci/workflow-contracts/workflow-ref-mirror-case-safety.test.mjs": 447, "config/scripts/ci/workflow-contracts/workflow-ref-reachability.test.mjs": 1650, "config/scripts/workspace/source-exports/workspace-source-exports.test.mjs": 117, - "config/scripts/terminal-dependencies/xterm-patches/xterm-image-kitty-admission.test.mjs": 1430, "config/scripts/terminal-dependencies/xterm-patches/xterm-image-kitty-coordinate-boundaries.test.mjs": 191, "config/scripts/terminal-dependencies/xterm-patches/xterm-image-kitty-file-transport.test.mjs": 404, "config/scripts/terminal-dependencies/xterm-patches/xterm-image-kitty-glyph-visibility.test.mjs": 250, @@ -287,7 +264,6 @@ "config/scripts/terminal-dependencies/xterm-patches/xterm-image-kitty-placeholder-lifecycle.test.mjs": 252, "config/scripts/terminal-dependencies/xterm-patches/xterm-image-kitty-retransmit.test.mjs": 835, "config/scripts/terminal-dependencies/xterm-patches/xterm-image-lifecycle-contract.test.mjs": 174, - "config/scripts/terminal-dependencies/xterm-patches/xterm-image-memory-contract.test.mjs": 368, "config/scripts/terminal-dependencies/xterm-patches/xterm-image-wasm-budget-contract.test.mjs": 469, "config/scripts/ci/workflow-contracts/zcode-credential-workflow.test.mjs": 668, "config/scripts/archives/platform-command/zip-extractor-command.test.mjs": 74, @@ -511,8 +487,6 @@ "src/main/agent-hooks/server/testing/providers/antigravity/server-antigravity-resume-identity.test.ts": 820, "src/main/agent-hooks/server/testing/permissions/server-claude-child-permission-lifecycle.test.ts": 681, "src/main/agent-hooks/server/testing/interruptions/server-claude-helper-kill.test.ts": 10279, - "src/main/agent-hooks/server/testing/interruptions/server-claude-helper-recovery.test.ts": 34523, - "src/main/agent-hooks/server/testing/task-wakeup/server-claude-owed-notification-expiry.test.ts": 877, "src/main/agent-hooks/server/testing/identity/server-claude-session-continuation.test.ts": 675, "src/main/agent-hooks/server/testing/http/server-claude-statusline.test.ts": 679, "src/main/agent-hooks/server/testing/task-wakeup/server-claude-task-wakeup-lifecycle.test.ts": 5451, @@ -1732,7 +1706,6 @@ "src/main/git/source-control/status/status-branch-line-total-exec-contract.test.ts": 1033, "src/main/git/source-control/status/status-branch-line-total-real-git.test.ts": 785, "src/main/git/source-control/status/status-branch-line-total-untracked-base.test.ts": 700, - "src/main/git/source-control/status/status-cquoted-paths.test.ts": 348, "src/main/git/source-control/diffs/status-diff.test.ts": 327, "src/main/git/source-control/status-discard-and-bulk-staging.test.ts": 289, "src/main/git/source-control/mutations/status-discard-symlink.test.ts": 456, @@ -1746,7 +1719,6 @@ "src/main/git/source-control/submodules/status-submodule.test.ts": 306, "src/main/git/source-control/upstream/status-upstream-ref.test.ts": 52, "src/main/git/source-control/upstream/upstream-deferred-fork-remote-real.test.ts": 319, - "src/main/git/source-control/upstream/upstream.test.ts": 406, "src/main/git/worktrees/creation/worktree-add-local-base-refresh.test.ts": 362, "src/main/git/worktrees/creation/worktree-add-pending-removal.test.ts": 346, "src/main/git/worktrees/creation/worktree-add-remote-branch-upstream-real-git.test.ts": 786, @@ -1891,11 +1863,8 @@ "src/main/hermes/hermes-config-source-edits.test.ts": 280, "src/main/hermes/hermes-windows-home.test.ts": 121, "src/main/hermes/hook-service.test.ts": 589, - "src/main/workspace-hooks/hook-archive-timeout-observation.test.ts": 1103, "src/main/workspace-hooks/hook-termination-real-process.test.ts": 3284, "src/main/workspace-hooks/hooks-archive-exit-observation.test.ts": 339, - "src/main/workspace-hooks/hooks-orca-yaml-unc-drive.test.ts": 402, - "src/main/workspace-hooks/hooks-runner.test.ts": 314, "src/main/filesystem/host-checkout-removal.test.ts": 52, "src/main/filesystem/host-tree-removal-asar.electron.test.ts": 308, "src/main/host/deferred-secret-protection-report.test.ts": 81, @@ -3886,7 +3855,6 @@ "src/main/pty/history/terminal-history-gc.test.ts": 824, "src/main/pty/history/terminal-history-tombstone-retry.test.ts": 715, "src/main/text-generation/failures/agent-failure-output.test.ts": 49, - "src/main/text-generation/failures/antigravity-generation-stream.test.ts": 479, "src/main/text-generation/execution/command-environment-process.test.ts": 1044, "src/main/text-generation/execution/commit-message-agent-environment.test.ts": 95, "src/main/text-generation/requests/commit-message-text-generation-branch-name.test.ts": 496, @@ -4122,7 +4090,7 @@ "src/packages/process-host/src/windows-command-line.test.ts": 73, "src/packages/process-host/src/windows-tree-kill.test.ts": 81, "src/packages/process-host/src/wsl-distro-allowlist.test.ts": 55, - "src/preload/api/notification-sound-playback.test.ts": 64, + "src/preload/api/notifications/notification-sound-playback.test.ts": 64, "src/preload/renderer/restart/app-restart-checkpoint-routing.test.ts": 2028, "src/preload/renderer/browser-pages/browser-client-page-renderer-requests.test.ts": 154, "src/preload/browser/extensions/messaging/browser-extension-content-script-relay.test.ts": 65, @@ -4141,7 +4109,6 @@ "src/preload/renderer/diagnostics/renderer-heap-statistics-reader.test.ts": 50, "src/preload/renderer/diagnostics/renderer-process-memory-reader.test.ts": 66, "src/preload/renderer/restart/renderer-restart-wiring.test.ts": 96, - "src/preload/api/ssh-authority-forwarding.test.ts": 1111, "src/renderer/src/app-shell/shutdown-checkpoint-persist.test.ts": 65, "src/renderer/src/app-shell/use-profile-state-save-delay-notice.sonner.test.tsx": 355, "src/renderer/src/app-shell/use-profile-state-save-delay-notice.test.tsx": 293, @@ -4538,7 +4505,6 @@ "src/renderer/src/components/feature-wall/setup/FullDiskAccessSetupPrompt.test.ts": 466, "src/renderer/src/components/floating-terminal/floating-workspace-tab-reorder.test.ts": 2520, "src/renderer/src/components/github-checks-tab-state.test.ts": 52, - "src/renderer/src/components/github-item-dialog/edit-item-fields/gh-edit-section.assignee-identity.test.tsx": 461, "src/renderer/src/components/github-item-dialog/land-pull-request/pr-reviewers-request-actions.test.ts": 1976, "src/renderer/src/components/github-item-dialog/load-item-details/use-github-item-dialog-details.test.tsx": 2850, "src/renderer/src/components/github-item-dialog/load-item-details/use-github-task-refresh.test.tsx": 1070, @@ -5096,32 +5062,21 @@ "src/renderer/src/components/tab-group/useTabGroupCloseScopeCommands.test.tsx": 2023, "src/renderer/src/components/tab-group/workspace-tab-close-commands.frame-flush.test.ts": 1604, "src/renderer/src/components/task-page/github/task-page-github-landing-refresh-record.test.tsx": 971, - "src/renderer/src/components/task-page/github/task-page-github-list-scroll-restore.test.ts": 197, "src/renderer/src/components/task-page/github/task-page-github-work-item-mutation-regressions.test.ts": 140, "src/renderer/src/components/task-page/github/task-page-github-work-item-mutations.test.ts": 169, "src/renderer/src/components/task-page/github/use-task-page-github-issue-creation.test.tsx": 1769, "src/renderer/src/components/task-page/gitlab/use-task-page-gitlab-loading.test.tsx": 1504, "src/renderer/src/components/task-page/jira/Content.board-owner.test.tsx": 2582, - "src/renderer/src/components/task-page/jira/IssueAssigneeField.test.tsx": 1122, "src/renderer/src/components/task-page/jira/task-page-jira-board-detail-integration.test.tsx": 2192, - "src/renderer/src/components/task-page/jira/task-page-jira-board-transitions.test.ts": 69, "src/renderer/src/components/task-page/jira/task-page-jira-board-view.test.tsx": 2287, - "src/renderer/src/components/task-page/jira/task-page-jira-favourite-selection.test.tsx": 3326, - "src/renderer/src/components/task-page/jira/task-page-jira-grouping.test.ts": 2324, "src/renderer/src/components/task-page/jira/task-page-jira-linked-issues.test.ts": 204, - "src/renderer/src/components/task-page/jira/task-page-jira-quick-copy.test.tsx": 3457, - "src/renderer/src/components/task-page/jira/use-task-page-jira-project-scope.test.tsx": 2642, - "src/renderer/src/components/task-page/jira/use-task-page-jira-quick-filters.test.tsx": 1466, - "src/renderer/src/components/task-page/jira/use-task-page-jira-saved-filters.test.tsx": 1501, - "src/renderer/src/components/task-page/jira/use-task-page-jira-summary-generation.test.tsx": 571, - "src/renderer/src/components/task-page/linear/IssueTable.test.tsx": 11230, - "src/renderer/src/components/task-page/linear/task-page-linear-committed-publication.test.tsx": 1978, + "src/renderer/src/components/task-page/jira/filters/use-task-page-jira-project-scope.test.tsx": 2642, + "src/renderer/src/components/task-page/jira/filters/use-task-page-jira-quick-filters.test.tsx": 1466, + "src/renderer/src/components/task-page/jira/filters/use-task-page-jira-saved-filters.test.tsx": 1501, + "src/renderer/src/components/task-page/jira/creation/use-task-page-jira-summary-generation.test.tsx": 571, "src/renderer/src/components/task-page/linear/task-page-linear-creation-lifetime.test.tsx": 3315, - "src/renderer/src/components/task-page/linear/task-page-linear-in-orca-issues.test.ts": 430, - "src/renderer/src/components/task-page/linear/task-page-linear-refresh-entry.test.tsx": 2217, - "src/renderer/src/components/task-page/linear/task-page-linear-refresh-lifecycle.test.tsx": 1814, - "src/renderer/src/components/task-page/linear/task-page-linear-refresh-writers.test.tsx": 2810, - "src/renderer/src/components/task-page/linear/task-page-linear-selection-decisions.test.tsx": 1018, + "src/renderer/src/components/task-page/linear/issues/task-page-linear-in-orca-issues.test.ts": 430, + "src/renderer/src/components/task-page/linear/issue-refresh/task-page-linear-refresh-lifecycle.test.tsx": 1814, "src/renderer/src/components/task-page/task-page-cache-selectors.test.ts": 108, "src/renderer/src/components/task-page/task-page-initial-selection-scaling.test.tsx": 1953, "src/renderer/src/components/task-page/task-page-work-item-pagination.test.ts": 63, @@ -5839,7 +5794,6 @@ "src/renderer/src/store/github/repositories/github-repo-lookup-index.test.ts": 51, "src/renderer/src/store/github/review-threads/github-review-thread-rollback.test.ts": 663, "src/renderer/src/store/github/task-records/github-task-record-retries.test.ts": 758, - "src/renderer/src/store/github/task-records/github-task-records.test.ts": 807, "src/renderer/src/store/github/work-items/github-work-items-error-envelope.test.ts": 1746, "src/renderer/src/store/github/work-items/github-work-items-pagination.test.ts": 1806, "src/renderer/src/store/github/work-items/github-work-items-runtime-routing.test.ts": 1891, @@ -6033,7 +5987,7 @@ "src/shared/agent-hook-listener/normalization/agent-hook-listener-claude-elicitation.test.ts": 229, "src/shared/agent-hook-listener/normalization/agent-hook-listener-codex-approval-ownership.test.ts": 258, "src/shared/agent-hook-listener/normalization/agent-hook-listener-codex-interrupt.test.ts": 240, - "src/shared/agent-hook-listener/normalization/agent-hook-listener-codex-main-agent.test.ts": 207, + "src/shared/agent-hook-listener/providers/codex/agent-hook-listener-codex-main-agent.test.ts": 207, "src/shared/agent-hook-listener/normalization/agent-hook-listener-codex-subagent-names.test.ts": 260, "src/shared/agent-hook-listener/normalization/agent-hook-listener-command-code-transcript.test.ts": 330, "src/shared/agent-hook-listener/normalization/agent-hook-listener-grok-completion.test.ts": 255, @@ -6043,13 +5997,11 @@ "src/shared/agent-hook-listener/transport/agent-hook-listener-transport.test.ts": 326, "src/shared/agent-hook-listener/claude-session-continuation.test.ts": 114, "src/shared/agent-hook-listener/claude-settled-permission-requests.test.ts": 54, - "src/shared/agent-hook-listener/providers/claude-helper-kill.test.ts": 1288, - "src/shared/agent-hook-listener/providers/claude-transcript-semantics.test.ts": 163, - "src/shared/agent-hook-listener/providers/dsh-events.test.ts": 231, - "src/shared/agent-hook-listener/providers/hermes-events.test.ts": 143, - "src/shared/agent-hook-listener/providers/jcode-turn-prompt.test.ts": 122, - "src/shared/agent-hook-listener/providers/muse-events.test.ts": 246, - "src/shared/agent-hook-listener/providers/zcode-events.test.ts": 241, + "src/shared/agent-hook-listener/providers/claude/transcripts/claude-transcript-semantics.test.ts": 163, + "src/shared/agent-hook-listener/providers/dsh/dsh-events.test.ts": 231, + "src/shared/agent-hook-listener/providers/hermes/hermes-events.test.ts": 143, + "src/shared/agent-hook-listener/providers/muse/muse-events.test.ts": 246, + "src/shared/agent-hook-listener/providers/zcode/zcode-events.test.ts": 241, "src/shared/agent-hook-listener/reverse-file-region-scan.test.ts": 51, "src/shared/agent-hook-listener/transcript-reader.test.ts": 109, "src/shared/agent-hook-listener/transport/agent-hook-spool-drain.test.ts": 50, @@ -6171,7 +6123,7 @@ "src/shared/claimed-agent-pty-owner.test.ts": 72, "src/shared/claude-agent-view-launch.test.ts": 55, "src/shared/claude-background-run-isolation.test.ts": 43, - "src/shared/claude-background-task-status.test.ts": 315, + "src/shared/agent-hook-listener/normalization/claude-background-task-status.test.ts": 315, "src/shared/claude-model-list-probe.test.ts": 55, "src/shared/claude-shell-function.test.ts": 220, "src/shared/cloud-service-url.test.ts": 37, @@ -6407,7 +6359,6 @@ "src/shared/native-chat/transcript/native-chat-unverifiable-turn-status.test.ts": 150, "src/shared/native-file-drop.test.ts": 59, "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/network-proxy.test.ts": 44, "src/shared/network/manual-address.test.ts": 68, @@ -6617,19 +6568,15 @@ "src/shared/ripgrep-match-offsets.test.ts": 151, "src/shared/ripgrep-process-availability.test.ts": 56, "src/shared/ripgrep-search-diagnostics.test.ts": 72, - "src/shared/rpc-contract/agent-launch-params-account-env.test.ts": 328, - "src/shared/rpc-contract/agent-launch-params-native-chat-default.test.ts": 327, - "src/shared/rpc-contract/agent-launch-params.test.ts": 301, - "src/shared/rpc-contract/agent-session-params-account-env.test.ts": 127, - "src/shared/rpc-contract/agent-session-spawn-intent-params.test.ts": 182, + "src/shared/agent-launch/startup/agent-launch-account-env.test.ts": 328, + "src/shared/rpc-contract/agent-launch/agent-launch-params.test.ts": 301, "src/shared/rpc-contract/automation-params-extra-agent-args.test.ts": 131, - "src/shared/rpc-contract/session-tabs-create-resume-session-param.test.ts": 259, + "src/shared/rpc-contract/session-tabs/session-tabs-create-resume-session-param.test.ts": 259, "src/shared/rpc-contract/structured-agent-session-agent-params.test.ts": 274, - "src/shared/rpc-contract/structured-agent-session-message-params.test.ts": 169, - "src/shared/rpc-contract/structured-agent-session-queued-edit-params.test.ts": 270, - "src/shared/rpc-contract/terminal-create-shell-param.test.ts": 182, - "src/shared/rpc-contract/workspace-layout-editor-file-params.test.ts": 269, - "src/shared/rpc-contract/worktree-review-metadata-compatibility.test.ts": 147, + "src/shared/queued-message-edit-skills.test.ts": 270, + "src/shared/rpc-contract/terminal/terminal-create-shell-param.test.ts": 182, + "src/shared/rpc-contract/workspace-layout/workspace-layout-editor-file-params.test.ts": 269, + "src/shared/rpc-contract/workspaces/worktree-review-metadata-compatibility.test.ts": 147, "src/shared/remote-runtime/host-store/runtime-environment-managed-orcad-store.test.ts": 313, "src/shared/remote-runtime/host-store/runtime-environment-sidecar.test.ts": 277, "src/shared/remote-runtime/host-store/runtime-environment-ssh-access-store.test.ts": 415, @@ -6699,7 +6646,6 @@ "src/shared/tab-agent-from-signals-pi-identity.test.ts": 103, "src/shared/task-query.test.ts": 55, "src/shared/task-source-context.test.ts": 103, - "src/shared/telemetry-payload-contracts.test.ts": 282, "src/shared/program-status/terminal-agent-status-processor.test.ts": 74, "src/shared/terminal/output/terminal-bell-detector.test.ts": 40, "src/shared/terminal/protocol/queries/terminal-color-scheme-protocol.test.ts": 39, @@ -6850,7 +6796,6 @@ "src/wsl-guest/agent-hooks/status/agent-hook-interrupt-reconciliation.test.ts": 586, "src/wsl-guest/agent-hooks/server/agent-hook-opencode-tui-identity.test.ts": 551, "src/wsl-guest/agent-hooks/status/agent-hook-provider-normalization.test.ts": 489, - "src/wsl-guest/agent-hooks/results/agent-hook-server-claude-owed-notification-expiry.test.ts": 792, "src/wsl-guest/agent-hooks/status/agent-hook-server-claude-session-continuation.test.ts": 462, "src/wsl-guest/agent-hooks/status/agent-hook-server-codex-side-thread.test.ts": 786, "src/wsl-guest/agent-hooks/status/agent-hook-server-codex-subagent-transcript.test.ts": 1589, @@ -6937,7 +6882,6 @@ "tests/e2e/orchestration-worker-workspace-resolution.unit.test.ts": 5936, "tests/e2e/paired-cli-agent-launch.unit.test.ts": 8918, "tests/e2e/paired-runtime-rejected-input-remount.unit.test.ts": 9954, - "tests/e2e/paired-worktree-agent-command-owner.unit.test.ts": 1767, "tests/e2e/pdf-detail-dimension-reads.unit.test.ts": 438, "tests/e2e/persistence-automation-target-dispatch.unit.test.ts": 8084, "tests/e2e/remote-agent-completion-authority.unit.test.ts": 1742, @@ -6956,7 +6900,6 @@ "tests/e2e/structured-attention-read-retirement.unit.test.ts": 6109, "tests/e2e/structured-chat-tab-lifetime.unit.test.ts": 5616, "tests/e2e/sustained-agent-typing-load-scripts.unit.test.ts": 2169, - "tests/e2e/task-page-linear-refresh-authority.unit.test.ts": 5548, "tests/e2e/terminal-perf-presentation.unit.test.ts": 60, "tests/e2e/worktree-status-warning.unit.test.ts": 2331, "tests/tools/omp-close-lifecycle.test.mjs": 403, diff --git a/config/scripts/ci/sharding/ci-unit-sequencer.test.mjs b/config/scripts/ci/sharding/ci-unit-sequencer.test.mjs index 29bdd48d8c3..60e6a7bb975 100644 --- a/config/scripts/ci/sharding/ci-unit-sequencer.test.mjs +++ b/config/scripts/ci/sharding/ci-unit-sequencer.test.mjs @@ -1,4 +1,4 @@ -import { mkdtempSync, readFileSync, writeFileSync, rmSync } from 'node:fs' +import { mkdtempSync, writeFileSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, expect, it, vi } from 'vitest' @@ -13,53 +13,36 @@ afterEach(() => { } }) -it.each([ - 'valid', - 'stale', - 'missing-file', - 'missing-artifact', - 'outside-selection', - 'empty-selection' -])('preserves complete shard coverage with %s planning evidence', async (kind) => { - root = mkdtempSync(join(tmpdir(), 'unit-sequencer-')) - const files = ['src/a.test.ts', 'src/b.test.ts', 'src/c.test.ts', 'src/d.test.ts'] - const plan = { - version: 1, - sourceSha: kind === 'stale' ? 'old' : 'current', - files: kind === 'missing-file' ? files.slice(1) : files, - executionFiles: - kind === 'outside-selection' - ? ['src/unknown.test.ts'] - : kind === 'empty-selection' - ? [] - : files.slice(0, 2) - } - const planPath = join(root, 'selection.json') - if (kind !== 'missing-artifact') { +it.each(['valid', 'stale'])( + 'preserves complete shard coverage with %s planning evidence', + async (kind) => { + root = mkdtempSync(join(tmpdir(), 'unit-sequencer-')) + const files = ['src/a.test.ts', 'src/b.test.ts', 'src/c.test.ts', 'src/d.test.ts'] + const plan = { + version: 1, + sourceSha: kind === 'stale' ? 'old' : 'current', + files, + executionFiles: files.slice(0, 2) + } + const planPath = join(root, 'selection.json') writeFileSync(planPath, JSON.stringify(plan)) - } - vi.stubEnv('ORCA_UNIT_SELECTION_PLAN', planPath) - vi.stubEnv('ORCA_SHARD_SOURCE_SHA', 'current') - vi.stubEnv('ORCA_SHARD_MANIFEST', join(root, 'assignment.json')) - const assigned = [] - for (const index of [1, 2]) { - const sequencer = new TimingSequencer({ config: { root, shard: { index, count: 2 } } }) - const specs = files.map((file, position) => ({ - moduleId: join(root, file), - project: { - name: position === 0 ? 'synthetic-late' : position % 2 ? 'bun' : 'node-runtime', - config: { sequence: { groupOrder: position === 0 ? 2 : 1 }, isolate: true } - } - })) - assigned.push(...(await sequencer.shard(specs)).map((spec) => spec.moduleId)) - const manifest = JSON.parse(readFileSync(join(root, 'assignment.json'), 'utf8')) - expect(manifest.selectedShard).toBe(index) - expect(manifest.shards.flatMap((shard) => shard.files).sort()).toEqual( - (kind === 'valid' ? files.slice(0, 2) : files).toSorted() + vi.stubEnv('ORCA_UNIT_SELECTION_PLAN', planPath) + vi.stubEnv('ORCA_SHARD_SOURCE_SHA', 'current') + vi.stubEnv('ORCA_SHARD_MANIFEST', join(root, 'assignment.json')) + const assigned = [] + for (const index of [1, 2]) { + const sequencer = new TimingSequencer({ config: { root, shard: { index, count: 2 } } }) + const specs = files.map((file, position) => ({ + moduleId: join(root, file), + project: { + name: position === 0 ? 'synthetic-late' : position % 2 ? 'bun' : 'node-runtime', + config: { sequence: { groupOrder: position === 0 ? 2 : 1 }, isolate: true } + } + })) + assigned.push(...(await sequencer.shard(specs)).map((spec) => spec.moduleId)) + } + expect(assigned.sort()).toEqual( + (kind === 'valid' ? files.slice(0, 2) : files).map((file) => join(root, file)).sort() ) } - expect(assigned.sort()).toEqual( - (kind === 'valid' ? files.slice(0, 2) : files).map((file) => join(root, file)).sort() - ) - expect(new Set(assigned).size).toBe(assigned.length) -}) +) diff --git a/config/scripts/ci/sharding/ci-unit-timing-reporter.test.mjs b/config/scripts/ci/sharding/ci-unit-timing-reporter.test.mjs index 682a8e41a8d..73af9b0753f 100644 --- a/config/scripts/ci/sharding/ci-unit-timing-reporter.test.mjs +++ b/config/scripts/ci/sharding/ci-unit-timing-reporter.test.mjs @@ -2,44 +2,7 @@ import { mkdtempSync, readFileSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import { describe, expect, it, vi } from 'vitest' -import { importUnitTimingReports } from './ci-unit-timing-import.mjs' -import UnitTimingReporter, { buildUnitPhaseReport } from './ci-unit-timing-reporter.mjs' - -const report = (index, timings) => ({ - metric: 'module-duration-v1', - nodeVersion: '24.21.0', - sourceSha: 'source', - runId: 'run', - runAttempt: '1', - shard: { index, count: 2 }, - status: 'passed', - unhandledErrors: 0, - timings -}) - -describe('unit worker timing evidence', () => { - it('rejects incomplete, duplicate, mixed and failed evidence', () => { - const first = report(1, { a: 100 }) - expect(() => importUnitTimingReports([first])).toThrow('complete') - expect(() => importUnitTimingReports([first, first])).toThrow('index') - expect(() => importUnitTimingReports([first, report(2, { a: 200 })])).toThrow('timing') - for (const change of [ - { sourceSha: 'other' }, - { runId: 'other' }, - { runAttempt: '2' }, - { nodeVersion: '26.6.0' }, - { metric: 'test-duration' }, - { status: 'failed' }, - { status: 'interrupted' }, - { unhandledErrors: 1 } - ]) { - expect(() => - importUnitTimingReports([first, { ...report(2, { b: 200 }), ...change }]) - ).toThrow('successful') - } - expect(() => importUnitTimingReports([first, report(2, { b: -1 })])).toThrow('timing') - }) -}) +import UnitTimingReporter from './ci-unit-timing-reporter.mjs' const phaseDiagnostic = (overrides = {}) => ({ environmentSetupDuration: 1.5, @@ -83,75 +46,16 @@ function withReporterOutputs(run) { } describe('optional unit phase diagnostics', () => { - it('records fractions and project totals without retaining sampled import records', () => { - const report = buildUnitPhaseReport( - phaseContext(), - [ - phaseModule( - 'src/first.test.ts', - phaseDiagnostic({ - importDurations: { - first: { selfTime: 2, totalTime: 9, importer: 'src/private-account.ts' }, - second: { selfTime: 3.5, totalTime: 5.5 }, - invalid: { selfTime: Number.NaN, totalTime: 4 } - } - }) - ), - phaseModule('src/second.test.ts'), - phaseModule('src/node.test.ts', phaseDiagnostic({ workerId: 9 }), 'node-runtime') - ], - [{ message: 'test error' }], - 'failed' - ) - expect(report.files[0]).toEqual({ - file: 'src/first.test.ts', - project: 'bun', - workerId: 7, - concurrencyId: 2, - phases: { - environmentSetupDuration: 1.5, - prepareDuration: 2.5, - collectDuration: 30.25, - setupDuration: 4.5, - duration: 5.25 - } + it('preserves timing evidence when optional output targets the timing file', () => { + withReporterOutputs(({ timingPath }) => { + const reporter = new UnitTimingReporter() + reporter.onInit(phaseContext()) + const modules = [phaseModule('src/example.test.ts')] + reporter.onTestRunEnd(modules, [], 'passed') + const timing = readFileSync(timingPath, 'utf8') + vi.stubEnv('ORCA_UNIT_PHASE_REPORT', timingPath) + expect(() => reporter.onTestRunEnd(modules, [], 'passed')).not.toThrow() + expect(readFileSync(timingPath, 'utf8')).toBe(timing) }) - expect(report.projects).toMatchObject([ - { - project: 'bun', - files: 2, - phases: { collectDuration: 60.5 }, - phaseSamples: { collectDuration: 2 }, - sampledImports: { entries: 2, selfTimeMs: 5.5, totalTimeMs: 14.5 } - }, - { project: 'node-runtime', files: 1, sampledImports: { entries: 0 } } - ]) - expect(report.phases.collectDuration).toBe(90.75) - expect(report).toMatchObject({ - phaseTime: 'summed-worker-time', - status: 'failed', - unhandledErrors: 1 - }) - expect(JSON.stringify(report)).not.toContain('private-account') - expect(JSON.stringify(report)).not.toContain('importDurations') }) - - it.each(['unwritable', 'same as timing'])( - 'preserves timing evidence when the optional output is %s', - (target) => { - withReporterOutputs(({ timingPath }) => { - const reporter = new UnitTimingReporter() - reporter.onInit(phaseContext()) - const modules = [phaseModule('src/example.test.ts')] - reporter.onTestRunEnd(modules, [], 'passed') - const timing = readFileSync(timingPath, 'utf8') - vi.stubEnv( - 'ORCA_UNIT_PHASE_REPORT', - target === 'unwritable' ? `${timingPath}/unwritable.json` : timingPath - ) - expect(() => reporter.onTestRunEnd(modules, [], 'passed')).not.toThrow() - expect(readFileSync(timingPath, 'utf8')).toBe(timing) - }) - } - ) }) diff --git a/config/scripts/ci/unit-cache/ci-unit-module-cache-contents.test.mjs b/config/scripts/ci/unit-cache/ci-unit-module-cache-contents.test.mjs index 11d158dba89..7915131f5f1 100644 --- a/config/scripts/ci/unit-cache/ci-unit-module-cache-contents.test.mjs +++ b/config/scripts/ci/unit-cache/ci-unit-module-cache-contents.test.mjs @@ -1,4 +1,4 @@ -import { mkdtemp, rename, rm, symlink, writeFile } from 'node:fs/promises' +import { mkdtemp, rm, symlink, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' @@ -67,35 +67,18 @@ afterEach(async () => { }) describe('unit module cache contents', () => { - it.each(['missing', 'modified', 'extra', 'renamed', 'metadata'])( - 'rejects %s sealed contents', - async (change) => { - const options = await fixture() - await sealModuleCache(options) - const path = join(options.path, filename) - if (change === 'missing') { - await rm(path) - } - if (change === 'modified') { - await writeFile(path, moduleCode().replace('value = 1', 'value = 2')) - } - if (change === 'extra') { - await writeFile(join(options.path, 'c'.repeat(40)), moduleCode()) - } - if (change === 'renamed') { - await rename(path, join(options.path, 'c'.repeat(40))) - } - if (change === 'metadata') { - await writeFile( - join(options.path, '_metadata.json'), - JSON.stringify({ lockfileHash: identity.lockfileHash, changed: true }) - ) - } - await expect(validateModuleCache(options)).rejects.toThrow( - /cache (inventory|content) mismatch/ - ) + it.each(['modified', 'extra'])('rejects %s sealed contents', async (change) => { + const options = await fixture() + await sealModuleCache(options) + const path = join(options.path, filename) + if (change === 'modified') { + await writeFile(path, moduleCode().replace('value = 1', 'value = 2')) } - ) + if (change === 'extra') { + await writeFile(join(options.path, 'c'.repeat(40)), moduleCode()) + } + await expect(validateModuleCache(options)).rejects.toThrow(/cache (inventory|content) mismatch/) + }) it.skipIf(process.platform === 'win32')( 'rejects symlinked modules, manifests and cache roots', diff --git a/config/scripts/ci/unit-cache/ci-unit-module-cache.test.mjs b/config/scripts/ci/unit-cache/ci-unit-module-cache.test.mjs index 1776f9f9eec..e1ec81fcbc9 100644 --- a/config/scripts/ci/unit-cache/ci-unit-module-cache.test.mjs +++ b/config/scripts/ci/unit-cache/ci-unit-module-cache.test.mjs @@ -1,12 +1,8 @@ -import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { mkdtempSync, mkdirSync, existsSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' -import { - finishModuleCache, - moduleCacheIdentity, - prepareModuleCache -} from './ci-unit-module-cache.mjs' +import { moduleCacheIdentity, prepareModuleCache } from './ci-unit-module-cache.mjs' const temporary = [] afterEach(() => { @@ -33,49 +29,11 @@ function options() { } describe('persistent unit module cache boundary', () => { - it.each([false, true])( - 'starts cold and discards unusable restored bytes (hit %s)', - async (hit) => { - const identity = moduleCacheIdentity(options()) - mkdirSync(identity.path) - writeFileSync(join(identity.path, 'untrusted'), 'throw Error("must not execute")') - const prepared = await prepareModuleCache(identity, { restoreHit: hit }) - expect(prepared.available).toBe(false) - expect(prepared.validationMs).toBeGreaterThanOrEqual(0) - expect(() => readFileSync(join(identity.path, 'untrusted'))).toThrow() - } - ) - - it('restores a matching cache produced from an earlier source', async () => { + it('discards unusable restored cache bytes', async () => { const identity = moduleCacheIdentity(options()) mkdirSync(identity.path) - writeFileSync( - join(identity.path, '_metadata.json'), - JSON.stringify({ lockfileHash: identity.installedLockHash.slice(0, 8) }) - ) - const metadata = [ - { id: '1', url: '2', file: '1', imports: '3', mappings: false }, - join(identity.root, 'source.ts'), - '/source.ts', - { urls: '4', ids: '5' }, - [], - {} - ] - writeFileSync( - join(identity.path, 'a'.repeat(40)), - `export const value = 1\n//# vitestCache=${Buffer.from(JSON.stringify(metadata)).toString('base64')}` - ) - const produced = await finishModuleCache(identity, { sourceSha: 'a'.repeat(40) }) - expect(produced.entries).toBe(2) - const prepared = await prepareModuleCache( - { ...identity, sourceSha: 'b'.repeat(40) }, - { restoreHit: true } - ) - expect(prepared).toMatchObject({ - available: true, - entries: produced.entries, - bytes: produced.bytes, - producerSourceSha: 'a'.repeat(40) - }) + writeFileSync(join(identity.path, 'untrusted'), 'throw Error("must not execute")') + await prepareModuleCache(identity, { restoreHit: true }) + expect(existsSync(join(identity.path, 'untrusted'))).toBe(false) }) }) diff --git a/config/scripts/ci/unit-selection/check-test-file-runners.test.mjs b/config/scripts/ci/unit-selection/check-test-file-runners.test.mjs index 25faa551046..ea5ebbf7d98 100644 --- a/config/scripts/ci/unit-selection/check-test-file-runners.test.mjs +++ b/config/scripts/ci/unit-selection/check-test-file-runners.test.mjs @@ -77,5 +77,3 @@ describe('findUncollectedTestFiles', () => { ]) }) }) - -// Why: cloud/ is outside the lint and format trees, so only these paths bring the check to a cloud-only diff. diff --git a/config/scripts/ci/unit-selection/ci-unit-plan.test.mjs b/config/scripts/ci/unit-selection/ci-unit-plan.test.mjs index 01da0d99004..55112d4ae4a 100644 --- a/config/scripts/ci/unit-selection/ci-unit-plan.test.mjs +++ b/config/scripts/ci/unit-selection/ci-unit-plan.test.mjs @@ -1,48 +1,9 @@ -import { - copyFileSync, - mkdtempSync, - mkdirSync, - readFileSync, - realpathSync, - rmSync, - writeFileSync -} from 'node:fs' +import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { expect, it } from 'vitest' import { runProcessSync } from '@orca/process-host' -import { FULL_SHARD_COUNT } from './ci-unit-matrix.mjs' - -it('emits the full matrix from a sparse checkout without dependencies or test discovery', () => { - const root = realpathSync(mkdtempSync(join(tmpdir(), 'orca-unit-matrix-'))) - try { - const script = join(root, 'matrix.mjs') - copyFileSync(new URL('./ci-unit-matrix.mjs', import.meta.url), script) - const output = join(root, 'outputs') - const run = (count) => - runProcessSync({ - program: process.execPath, - args: [script], - cwd: root, - env: { - ...process.env, - ORCA_UNIT_FULL_SHARD_COUNT: count, - GITHUB_OUTPUT: output - } - }) - const result = run('16') - expect(result.code, result.stderr).toBe(0) - const shards = JSON.parse(readFileSync(output, 'utf8').trim().slice('shards='.length)) - expect(shards).toEqual( - Array.from({ length: 16 }, (_, index) => ({ index: index + 1, count: 16 })) - ) - expect(run('2; echo unsafe').code).not.toBe(0) - expect(readFileSync(output, 'utf8').trim()).toBe(`shards=${JSON.stringify(shards)}`) - } finally { - rmSync(root, { recursive: true, force: true }) - } -}) it.each([ { withParent: true, unresolvedPackage: false }, @@ -110,14 +71,6 @@ it.each([ expect(plan.executionFiles).toEqual( selected ? ['src/consumer.test.ts'] : ['src/consumer.test.ts', 'src/unrelated.test.ts'] ) - expect(plan.reason).toBe( - !withParent - ? 'Error: Changed paths unavailable' - : unresolvedPackage - ? 'Error: No public orca-source export for @orca/missing-package' - : 'Transitive imports plus indirect-input consumers' - ) - expect(plan.shards).toHaveLength(selected ? 1 : FULL_SHARD_COUNT) } finally { rmSync(root, { recursive: true, force: true }) } diff --git a/config/scripts/ci/unit-selection/ci-unit-selection-review.test.mjs b/config/scripts/ci/unit-selection/ci-unit-selection-review.test.mjs deleted file mode 100644 index df2ba63a3d0..00000000000 --- a/config/scripts/ci/unit-selection/ci-unit-selection-review.test.mjs +++ /dev/null @@ -1,77 +0,0 @@ -import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { expect, it } from 'vitest' -import { readUnitSelectionRecords, reviewUnitSelection } from './ci-unit-selection-review.mjs' - -const plan = { - sourceSha: 'sha', - mode: 'shadow', - selectionAvailable: true, - files: ['a', 'b'], - candidateFiles: ['a'] -} - -const record = (index, file, state) => ({ - plan, - timing: { - sourceSha: 'sha', - runId: '1', - runAttempt: '1', - nodeVersion: '24', - status: state, - unhandledErrors: 0, - shard: { index, count: 2 }, - timings: { [file]: 100 }, - results: { [file]: state } - } -}) - -it('reviews deferred shadow plans without masking shard plans or accepting another source', () => { - const root = mkdtempSync(join(tmpdir(), 'orca-deferred-selection-')) - try { - const fallback = join(root, 'deferred-plan.json') - writeFileSync(fallback, JSON.stringify(plan)) - for (const [index, file] of ['a', 'b'].entries()) { - const directory = join(root, String(index)) - mkdirSync(directory) - writeFileSync( - join(directory, 'unit-timings.json'), - JSON.stringify(record(index + 1, file, 'passed').timing) - ) - } - const review = () => reviewUnitSelection(readUnitSelectionRecords(root, fallback))[0] - expect(review()).toMatchObject({ completeFullRun: true, selectionEvaluated: true }) - writeFileSync(fallback, JSON.stringify({ ...plan, sourceSha: 'another-source' })) - expect(review().completeFullRun).toBe(false) - for (const index of [0, 1]) { - writeFileSync(join(root, String(index), 'unit-selection.json'), JSON.stringify(plan)) - } - expect(review().completeFullRun).toBe(true) - expect(() => readUnitSelectionRecords(root, join(root, 'missing'))).not.toThrow() - rmSync(join(root, '0', 'unit-selection.json')) - expect(() => readUnitSelectionRecords(root, join(root, 'missing'))).toThrow() - } finally { - rmSync(root, { recursive: true, force: true }) - } -}) - -it('finds omitted failures in a complete failing reference run', () => { - expect( - reviewUnitSelection([record(1, 'a', 'passed'), record(2, 'b', 'failed')])[0] - ).toMatchObject({ completeFullRun: true, missedFailures: ['b'], potentiallyOmittedWorkerMs: 100 }) -}) - -it('does not call incomplete, duplicate, interrupted or selected evidence a full reference', () => { - const a = record(1, 'a', 'passed'), - b = record(2, 'b', 'passed') - for (const rows of [ - [a], - [a, a], - [a, { ...b, timing: { ...b.timing, unhandledErrors: 1 } }], - [a, { ...b, timing: { ...b.timing, status: 'interrupted' } }], - [a, b].map((row) => ({ ...row, plan: { ...plan, mode: 'selected' } })) - ]) { - expect(reviewUnitSelection(rows).map((row) => row.completeFullRun)).toEqual([false]) - } -}) diff --git a/config/scripts/ci/unit-selection/ci-unit-selection.test.mjs b/config/scripts/ci/unit-selection/ci-unit-selection.test.mjs index 263e6c8e32c..fa84f0e50c6 100644 --- a/config/scripts/ci/unit-selection/ci-unit-selection.test.mjs +++ b/config/scripts/ci/unit-selection/ci-unit-selection.test.mjs @@ -1,6 +1,5 @@ import { describe, expect, it } from 'vitest' import { buildUnitDependencyGraph } from './dependency-graph/ci-unit-dependency-graph.mjs' -import { UNIT_SETUP_FILES } from './dependency-graph/ci-unit-files.mjs' import { planUnitSelection, selectUnitFiles } from './ci-unit-selection.mjs' const sources = new Map( @@ -22,12 +21,8 @@ const graph = { ...buildUnitDependencyGraph(sources), files: new Set(sources.key const files = [...sources.keys()].filter((file) => file.endsWith('.test.ts')).sort() describe('conservative unit selection', () => { - it.each([ - 'config/vitest.config.ts', - 'config/scripts/test-runtime/execution-policy/run-vitest.mjs', - ...UNIT_SETUP_FILES, - 'config/scripts/test-runtime/node-worker/vitest-node-runtime-worker.mjs' - ])('retains every test for a transitive dependency of %s', (runtime) => { + it('retains every test for a transitive dependency of the shared runtime', () => { + const runtime = 'config/vitest.config.ts' const sources = new Map([ ['src/input.ts', 'export const value = 1'], ['config/scripts/setup-forward.ts', "export * from '../../src/input'"], @@ -114,6 +109,5 @@ describe('conservative unit selection', () => { event: { pull_request: { draft: true } } }) expect(selected.executionFiles).not.toContain('src/unrelated.test.ts') - expect(selected.shards).toEqual([{ index: 1, count: 1 }]) }) }) diff --git a/config/scripts/ci/unit-selection/dependency-graph/ci-unit-dependency-graph.test.mjs b/config/scripts/ci/unit-selection/dependency-graph/ci-unit-dependency-graph.test.mjs index 35f34977465..c2558b41e33 100644 --- a/config/scripts/ci/unit-selection/dependency-graph/ci-unit-dependency-graph.test.mjs +++ b/config/scripts/ci/unit-selection/dependency-graph/ci-unit-dependency-graph.test.mjs @@ -65,28 +65,13 @@ it('tracks transitive public workspace source consumers including type and dynam expect(graph.files.has('src/packages/process-host/dist/index.js')).toBe(false) }) -it('tracks package manifests as inputs to all public consumers', () => { - const graph = collectUnitDependencyGraph(fixture()) - const tests = [...graph.files].filter((file) => file.endsWith('.test.ts')).sort() - for (const manifest of [ - 'src/packages/process-host/package.json', - 'src/packages/byte-buffer/package.json' - ]) { - const selection = selectUnitFiles(tests, [manifest], graph) - expect(selection.full).toBe(false) - expect(selection.files).toEqual(tests.filter((file) => file !== 'src/unrelated.test.ts')) - } +it('does not silently drop an unresolved internal import', () => { + const specifier = '@orca/unknown' + expect(() => + collectUnitDependencyGraph(fixture({ 'src/consumer.test.ts': `import '${specifier}'` })) + ).toThrow('No public orca-source export') }) -it.each(['@orca/unknown', '@orca/process-host/private'])( - 'does not silently drop unresolved internal imports: %s', - (specifier) => { - expect(() => - collectUnitDependencyGraph(fixture({ 'src/consumer.test.ts': `import '${specifier}'` })) - ).toThrow('No public orca-source export') - } -) - describe('unit graph import resolution', () => { it('keeps extensionless consumers when a covered JavaScript sibling changes', () => { const graph = collectUnitDependencyGraph( @@ -104,13 +89,9 @@ describe('unit graph import resolution', () => { }) }) - it.each([ - ['js', 'ts'], - ['js', 'tsx'], - ['jsx', 'tsx'], - ['mjs', 'mts'], - ['cjs', 'cts'] - ])('retains TypeScript substitutions for .%s imports through .%s', (output, source) => { + it('retains TypeScript substitutions for JavaScript imports', () => { + const output = 'js' + const source = 'ts' const graph = collectUnitDependencyGraph( fixture({ [`src/value.${source}`]: 'export const value = 1', diff --git a/config/scripts/ci/unit-selection/dependency-graph/ci-unit-indirect-inputs.test.mjs b/config/scripts/ci/unit-selection/dependency-graph/ci-unit-indirect-inputs.test.mjs index 2f37b254b21..0d36a1417d7 100644 --- a/config/scripts/ci/unit-selection/dependency-graph/ci-unit-indirect-inputs.test.mjs +++ b/config/scripts/ci/unit-selection/dependency-graph/ci-unit-indirect-inputs.test.mjs @@ -16,54 +16,16 @@ function selected(source) { }).files } -it.each([ - '// spawn a process, readFileSync, glob, new URL(path)', - 'export const value = globalThis.value ?? global.value', - "export const prose = 'spawn readFileSync glob'", - 'export interface Config { spawnToken: string; Worker: string }', - 'export type Config = { readFile: string; global: boolean }', - "export const url = new URL('https://example.test/data')", - 'export const spawnToken = "opaque-token"', - "const ast = ts.createSourceFile('source.ts', source, target)" -])('does not retain unrelated tests for syntax that loads no repository inputs: %s', (source) => { - expect(selected(source)).toEqual(['src/changed.test.ts']) +it('does not retain unrelated consumers of names in comments and strings', () => { + expect(selected("// spawn readFileSync\nexport const prose = 'glob'")).toEqual([ + 'src/changed.test.ts' + ]) }) it.each([ "import { readFileSync as read } from 'node:fs'; export const value = read(path)", - "import * as fs from 'node:fs'; export const value = fs['read' + 'FileSync'](path)", - "import { spawn as launch } from 'node:child_process'; launch(binary)", - "import { glob as find } from 'tinyglobby'; find(pattern)", - "import * as vm from 'node:vm'; vm['run' + 'InNewContext'](code)", - "import * as hostModule from 'node:module'; export const loader = hostModule['create' + 'Require'](path)", - "const file = new URL('./fixture.txt', import.meta.url)", - 'const url = new URL(runtimeValue)', 'import(computedModule)', - 'require(computedModule)', - "const read = fs['readFileSync']; read(path)", - 'const execute = runProcess; execute(command)', - 'const load = process.getBuiltinModule; load(moduleName)', - 'const program = ts.createProgram(options)', - 'const program = ts.createIncrementalProgram(options)', - 'const program = ts.createWatchProgram(options)', - 'const config = ts.getParsedCommandLineOfConfigFile(path, {}, ts.sys)', - 'const config = ts.parseJsonConfigFileContent(config, ts.sys, root)', - 'const config = ts.parseJsonSourceFileConfigFileContent(config, ts.sys, root)', - 'const config = ts.readConfigFile(path, ts.sys.readFile)', - 'const program = ts.createEmitAndSemanticDiagnosticsBuilderProgram(options)', - 'const program = ts.createSemanticDiagnosticsBuilderProgram(options)', - 'const builder = ts.createSolutionBuilder(host, roots, options)', - 'const builder = ts.createSolutionBuilderWithWatch(host, roots, options)', - 'const service = ts.createLanguageService(host)', - 'const resolved = ts.resolveModuleName(name, file, options, host)', - 'const resolved = ts.resolveTypeReferenceDirective(name, file, options, host)', - 'const files = ts.sys.readDirectory(directory)', - 'eval(source)', - 'const execute = new Function(source); execute()', 'spawn( @@@' -])( - 'retains indirect consumers even for aliases, computed access and invalid syntax: %s', - (source) => { - expect(selected(source)).toEqual(['src/changed.test.ts', 'src/indirect.test.ts']) - } -) +])('retains consumers with indirect inputs or invalid syntax: %s', (source) => { + expect(selected(source)).toEqual(['src/changed.test.ts', 'src/indirect.test.ts']) +}) diff --git a/config/scripts/ci/workflow-contracts/ci-closed-pr-cancellation.test.mjs b/config/scripts/ci/workflow-contracts/ci-closed-pr-cancellation.test.mjs deleted file mode 100644 index 183497f5019..00000000000 --- a/config/scripts/ci/workflow-contracts/ci-closed-pr-cancellation.test.mjs +++ /dev/null @@ -1,124 +0,0 @@ -import { readFileSync } from 'node:fs' -import { runInNewContext } from 'node:vm' -import { expect, it, vi } from 'vitest' -import { parse } from 'yaml' - -const workflow = parse(readFileSync('.github/workflows/ci-closed-pr-caches.yml', 'utf8')) -const step = workflow.jobs.clean.steps[0] -const closed = { number: 123, closed_at: '2026-10-06T00:00:00Z' } -const current = { ...closed, state: 'closed', merged_at: null } -const run = { - id: 42, - event: 'pull_request', - path: '.github/workflows/pr.yml', - status: 'in_progress', - created_at: '2026-10-05T23:59:00Z' -} - -function execute(options = {}) { - const request = vi.fn(async (_route, args) => { - if (options.requestError) { - throw options.requestError - } - if (args.concurrency_group_name !== 'pr-checks-123') { - throw { status: 404 } - } - return { - data: { - group_name: options.group ?? 'pr-checks-123', - group_members: options.members ?? [{ run_id: 42 }] - } - } - }) - const getPr = vi.fn(async () => ({ - data: options.nextCurrent ?? options.current ?? current - })) - if (options.reopened || options.nextCurrent) { - getPr.mockResolvedValueOnce({ data: options.reopened ? current : options.current }) - } - const getRun = vi.fn(async () => ({ data: { ...run, ...options.run } })) - const cancel = vi.fn(async () => { - if (options.cancelError) { - throw options.cancelError - } - }) - const result = runInNewContext(`(async () => { ${step.with.script} })()`, { - context: { - repo: { owner: 'owner', repo: 'repo' }, - payload: { action: options.action ?? 'closed', pull_request: options.closed ?? closed } - }, - github: { - request, - rest: { - pulls: { get: getPr }, - actions: { getWorkflowRun: getRun, cancelWorkflowRun: cancel } - } - }, - core: { info: vi.fn() } - }) - return { result, request, getPr, getRun, cancel } -} - -it('rechecks closure before cancelling when a PR reopens during lookup', async () => { - const { result, getRun, cancel } = execute({ - current: { ...current, state: 'open' }, - reopened: true - }) - await result - expect(getRun).toHaveBeenCalledOnce() - expect(cancel).not.toHaveBeenCalled() -}) - -it('skips a close event whose payload was rendered after a quick reopen', async () => { - const reopened = execute({ - closed: { number: 123, closed_at: null }, - current: { number: 123, state: 'open', closed_at: null, merged_at: null } - }) - await expect(reopened.result).resolves.toBeUndefined() - expect(reopened.cancel).not.toHaveBeenCalled() - - const stillClosed = execute() - await stillClosed.result - expect(stillClosed.cancel).toHaveBeenCalledWith(expect.objectContaining({ run_id: 42 })) -}) - -it('skips a close event when the PR was reopened and merged before cleanup', async () => { - const { result, cancel } = execute({ - closed: { number: 123, closed_at: null }, - current: { - number: 123, - state: 'closed', - closed_at: '2026-10-06T00:00:05Z', - merged_at: '2026-10-06T00:00:05Z' - } - }) - await expect(result).resolves.toBeUndefined() - expect(cancel).not.toHaveBeenCalled() -}) - -const head = 'a'.repeat(40) -const pushed = { number: 123, head: { sha: head } } -const open = { ...pushed, state: 'open' } - -it('cancels an older commit even when a path filter prevents a replacement run', async () => { - const { result, cancel } = execute({ - action: 'synchronize', - closed: pushed, - current: open, - run: { head_sha: 'b'.repeat(40) } - }) - await result - expect(cancel).toHaveBeenCalledOnce() -}) - -it('rechecks the current head before cancelling when another push arrives during lookup', async () => { - const { result, cancel } = execute({ - action: 'synchronize', - closed: pushed, - current: open, - nextCurrent: { ...open, head: { sha: 'b'.repeat(40) } }, - run: { head_sha: 'b'.repeat(40) } - }) - await result - expect(cancel).not.toHaveBeenCalled() -}) diff --git a/config/scripts/ci/workflow-contracts/ci-managed-ssh-execution.test.mjs b/config/scripts/ci/workflow-contracts/ci-managed-ssh-execution.test.mjs index 7e4a7f332cb..7bedcad9c40 100644 --- a/config/scripts/ci/workflow-contracts/ci-managed-ssh-execution.test.mjs +++ b/config/scripts/ci/workflow-contracts/ci-managed-ssh-execution.test.mjs @@ -19,19 +19,14 @@ afterEach(() => { } }) -async function execute(input, version = 'current') { +async function execute(input) { const directory = mkdtempSync(join(tmpdir(), 'orca-ssh-selection-')) directories.push(directory) const bin = join(directory, 'bin') mkdirSync(bin) const moduleDir = join(directory, 'config', 'scripts') mkdirSync(moduleDir, { recursive: true }) - if (version !== 'missing') { - writeFileSync( - join(moduleDir, 'ci-e2e-job-selection.mjs'), - version === 'current' ? selector : 'export const old = true\n' - ) - } + writeFileSync(join(moduleDir, 'ci-e2e-job-selection.mjs'), selector) writeFileSync( join(bin, 'xvfb-run'), `#!/usr/bin/env bash @@ -72,13 +67,8 @@ require('node:fs').writeFileSync(process.env.ARGV_CAPTURE, JSON.stringify(proces // The production lane runs Bash on Linux; the stubs never start Docker, Electron, or Xvfb. describe.skipIf(process.platform === 'win32')('managed SSH workflow command', () => { - it.each([ - 'malformed', - '[]', - '[null]', - JSON.stringify(['tests/e2e/other.spec.ts']), - JSON.stringify(['tests/e2e/ssh-orcad-../../other.spec.ts']) - ])('refuses invalid selection before starting Playwright: %s', async (input) => { + it('refuses malformed selection before starting Playwright', async () => { + const input = 'malformed' const { result, args } = await execute(input) expect(result.code).not.toBe(0) expect(args).toBeUndefined() diff --git a/config/scripts/ci/workflow-contracts/hourly-preflight-workflow.test.mjs b/config/scripts/ci/workflow-contracts/hourly-preflight-workflow.test.mjs index c59a910ec6a..7ac7c209da6 100644 --- a/config/scripts/ci/workflow-contracts/hourly-preflight-workflow.test.mjs +++ b/config/scripts/ci/workflow-contracts/hourly-preflight-workflow.test.mjs @@ -60,10 +60,7 @@ async function checkFreshness(overrides = {}) { describe('hourly build preflight', () => { it.each([ ['unchanged', {}, false], - ['changed', { LAST_SHA: '123456789012' }, true], - ['forced', { FORCED: 'true' }, true], - ['first build', { LAST_TAG: '' }, true], - ['missing prior identity', { LAST_SHA: '' }, true] + ['changed', { LAST_SHA: '123456789012' }, true] ])('%s main selects the expected build decision', async (_name, env, shouldBuild) => { const result = await checkFreshness(env) expect(result.exitCode, `${result.stdout} ${result.stderr}`).toBe(0) diff --git a/config/scripts/ci/workflow-contracts/install-node-dependencies-action.test.mjs b/config/scripts/ci/workflow-contracts/install-node-dependencies-action.test.mjs index c2babbc078a..7c95c4df81d 100644 --- a/config/scripts/ci/workflow-contracts/install-node-dependencies-action.test.mjs +++ b/config/scripts/ci/workflow-contracts/install-node-dependencies-action.test.mjs @@ -58,11 +58,9 @@ function executeInstallScript(fixture) { } describe('install-node-dependencies action', () => { - it.each([ - ['package.json', '{"name":"changed"}\n'], - ['pnpm-lock.yaml', 'lockfileVersion: 9\nchanged: true\n'], - ['pnpm-workspace.yaml', 'packages: []\nchanged: true\n'] - ])('rejects a changed %s when the composite step cwd is detached', (file, contents) => { + it('rejects a changed lockfile when the composite step cwd is detached', () => { + const file = 'pnpm-lock.yaml' + const contents = 'lockfileVersion: 9\nchanged: true\n' const fixture = createFixture() try { const clean = executeInstallScript(fixture) diff --git a/config/scripts/ci/workflow-contracts/issue-os-labeler-reconciliation.test.mjs b/config/scripts/ci/workflow-contracts/issue-os-labeler-reconciliation.test.mjs index 00eebaf651b..68d2cd58380 100644 --- a/config/scripts/ci/workflow-contracts/issue-os-labeler-reconciliation.test.mjs +++ b/config/scripts/ci/workflow-contracts/issue-os-labeler-reconciliation.test.mjs @@ -10,9 +10,6 @@ const OS = 'Operating system' const MOBILE = renderBugBody({ [WHERE]: 'Mobile app', [OS]: 'iOS' }) const DESKTOP = renderBugBody({ [WHERE]: 'Desktop only', [OS]: 'macOS' }) -const writes = (issue) => - issue.writes().map(({ method, args }) => [method, args.labels ?? args.name]) - it('retries against the fresh form when the body changes during mutation', async () => { const issue = createFakeIssue({ body: MOBILE, labels: ['bug', 'os:Windows'] }) issue.onCall = (call) => { @@ -23,11 +20,4 @@ it('retries against the fresh form when the body changes during mutation', async await runLabelerScript(issue) expect(issue.labels).toEqual(['bug', 'os:macos']) - expect(writes(issue)).toEqual([ - ['addLabels', ['os:mobile', 'mobile']], - ['removeLabel', 'os:Windows'], - ['addLabels', ['os:macos']], - ['removeLabel', 'os:mobile'], - ['removeLabel', 'mobile'] - ]) }) diff --git a/config/scripts/ci/workflow-contracts/pr-auxiliary-preflight-admission.test.mjs b/config/scripts/ci/workflow-contracts/pr-auxiliary-preflight-admission.test.mjs index 83a3cee313e..b9413e83911 100644 --- a/config/scripts/ci/workflow-contracts/pr-auxiliary-preflight-admission.test.mjs +++ b/config/scripts/ci/workflow-contracts/pr-auxiliary-preflight-admission.test.mjs @@ -22,11 +22,7 @@ afterEach(() => { } }) -function createMobileCiScopeRepository( - withTool = true, - toolPath = 'config/scripts/mobile-web/check-entry/run-mobile-ci-tests.mjs', - companionPaths = [] -) { +function createMobileCiScopeRepository() { const directory = mkdtempSync(join(tmpdir(), 'orca-mobile-ci-scope-')) mobileCiScopeDirectories.push(directory) const root = resolve(import.meta.dirname, '../../../..') @@ -70,22 +66,13 @@ function createMobileCiScopeRepository( git('init', '--quiet') git('config', 'user.name', 'Scope control') git('config', 'user.email', 'scope@example.invalid') - const tool = join(directory, toolPath) + const tool = join(directory, 'config/scripts/mobile-web/check-entry/run-mobile-ci-tests.mjs') mkdirSync(dirname(tool), { recursive: true }) - if (withTool) { - writeFileSync(tool, 'export const revision = 1\n') - } - for (const file of companionPaths) { - mkdirSync(dirname(join(directory, file)), { recursive: true }) - writeFileSync(join(directory, file), 'export const revision = 1\n') - } + writeFileSync(tool, 'export const revision = 1\n') const base = commit() - const detect = (stepId = 'filter') => { - const output = join( - directory, - stepId === 'filter' ? 'github-output' : `github-output-${stepId}` - ) - const step = workflow.jobs.code_paths.steps.find((candidate) => candidate.id === stepId) + const detect = () => { + const output = join(directory, 'github-output') + const step = workflow.jobs.code_paths.steps.find((candidate) => candidate.id === 'filter') const result = runProcessSync({ program: 'bash', args: [ @@ -104,7 +91,7 @@ function createMobileCiScopeRepository( }) return { result, output } } - return { directory, tool, git, commit, detect } + return { directory, tool, commit, detect } } describe.skipIf(process.platform === 'win32')('Mobile CI complete Git change evidence', () => { @@ -118,31 +105,18 @@ describe.skipIf(process.platform === 'win32')('Mobile CI complete Git change evi expect(readFileSync(output, 'utf8')).toContain('test=true\n') }) - it.each(['addition', 'deletion', 'rename', 'type change'])( - 'retains full root units for an actual %s', - (change) => { - const repo = createMobileCiScopeRepository(change !== 'addition') - if (change === 'addition') { - writeFileSync(repo.tool, 'export const revision = 1\n') - } else if (change === 'deletion') { - rmSync(repo.tool) - } else if (change === 'rename') { - repo.git( - 'mv', - 'config/scripts/mobile-web/check-entry/run-mobile-ci-tests.mjs', - 'config/mobile-ci.vitest.config.mjs' - ) - } else { - rmSync(repo.tool) - symlinkSync('pr-code-change-scope.mjs', repo.tool) - } - repo.commit() - const { result, output } = repo.detect() - expect(result.code, result.stderr).toBe(0) - expect(readFileSync(output, 'utf8')).toContain('mobile_ci_tools=false\n') - expect(readFileSync(output, 'utf8')).toContain('test=true\n') + it.each(['deletion', 'type change'])('retains full root units for an actual %s', (change) => { + const repo = createMobileCiScopeRepository() + rmSync(repo.tool) + if (change === 'type change') { + symlinkSync('pr-code-change-scope.mjs', repo.tool) } - ) + repo.commit() + const { result, output } = repo.detect() + expect(result.code, result.stderr).toBe(0) + expect(readFileSync(output, 'utf8')).toContain('mobile_ci_tools=false\n') + expect(readFileSync(output, 'utf8')).toContain('test=true\n') + }) it('does not emit a passing classification when the Git evidence is unavailable', () => { const repo = createMobileCiScopeRepository() diff --git a/config/scripts/ci/workflow-contracts/required-pr-check-name.test.mjs b/config/scripts/ci/workflow-contracts/required-pr-check-name.test.mjs deleted file mode 100644 index c10552e8275..00000000000 --- a/config/scripts/ci/workflow-contracts/required-pr-check-name.test.mjs +++ /dev/null @@ -1,34 +0,0 @@ -import { expect, it } from 'vitest' -import { parse } from 'yaml' -import { verifyRequiredPrContext } from '../../../../.github/scripts/check-required-pr-context.mjs' - -it('rejects aggregate conditions that hide a successful result', () => { - for (const condition of [undefined, 'success()', 'always()', '!cancelled() && !(true)']) { - const parseChanged = (source) => { - const workflow = parse(source) - if (workflow.jobs.verify?.name === 'PR checks passed') { - workflow.jobs.verify.if = condition - } - return workflow - } - expect(() => verifyRequiredPrContext(parseChanged), String(condition)).toThrow( - 'Required PR aggregate must run unless cancelled or the PR is a draft.' - ) - } -}) - -it('rejects aggregate jobs and steps that ignore failures', () => { - for (const location of ['job', 'step']) { - const parseChanged = (source) => { - const workflow = parse(source) - if (workflow.jobs.verify?.name === 'PR checks passed') { - const target = location === 'job' ? workflow.jobs.verify : workflow.jobs.verify.steps[0] - target['continue-on-error'] = true - } - return workflow - } - expect(() => verifyRequiredPrContext(parseChanged), location).toThrow( - 'Required PR aggregate must not ignore failures.' - ) - } -}) diff --git a/config/scripts/ci/workflow-contracts/workflow-ref-reachability.test.mjs b/config/scripts/ci/workflow-contracts/workflow-ref-reachability.test.mjs index 4b4aa9f991b..3e39e7bbd42 100644 --- a/config/scripts/ci/workflow-contracts/workflow-ref-reachability.test.mjs +++ b/config/scripts/ci/workflow-contracts/workflow-ref-reachability.test.mjs @@ -13,8 +13,6 @@ const windowsVet = readWorkflow('dev-channel-win-build').jobs['build-win'].steps const adhoc = readWorkflow('adhoc-mac-build') const resolveSteps = adhoc.jobs['resolve-ref'].steps const earlyVet = resolveSteps.find((step) => step.id === 'resolve') -const macSteps = adhoc.jobs['build-adhoc-mac-arch'].steps -const macVet = macSteps.find((step) => step.id === 'vetted') const directory = mkdtempSync(join(tmpdir(), 'workflow-ref-reachability-')) const repository = join(directory, 'remote.git') const identity = { @@ -85,41 +83,37 @@ describe('release ref trust with case-twin names', () => { const result = await vet(windowsVet, sha) expect(result.code, result.stderr).toBe(0) } - for (const step of [earlyVet, macVet]) { - for (const [ref, sha] of [ - ['Fix', upper], - ['fix', lower], - ['Release', lower], - ['release', lower], - ['refs/heads/Fix', upper], - ['refs/tags/Release', upper], - ['plain', upper], - ['refs/tags/release', lower], - [upper, upper], - [upper.toUpperCase(), upper], - [ancestor, ancestor] - ]) { - const result = await vet(step, ref) - expect(result.code, result.stderr).toBe(0) - expect(result.output).toBe(`sha=${sha}\n`) - } + for (const [ref, sha] of [ + ['Fix', upper], + ['fix', lower], + ['Release', lower], + ['release', lower], + ['refs/heads/Fix', upper], + ['refs/tags/Release', upper], + ['plain', upper], + ['refs/tags/release', lower], + [upper, upper], + [upper.toUpperCase(), upper], + [ancestor, ancestor] + ]) { + const result = await vet(earlyVet, ref) + expect(result.code, result.stderr).toBe(0) + expect(result.output).toBe(`sha=${sha}\n`) } }) it('rejects PR-only commits even when the server has their objects', async () => { - for (const step of [windowsVet, earlyVet, macVet]) { + for (const step of [windowsVet, earlyVet]) { const result = await vet(step, untrusted) expect(result.code).not.toBe(0) expect(result.stdout).toContain('not reachable from any branch or tag') expect(result.output).toBe('') } - for (const step of [earlyVet, macVet]) { - for (const ref of ['refs/pull/1/head', 'pull/1/head']) { - const result = await vet(step, ref) - expect(result.code).not.toBe(0) - expect(result.stdout).toContain('Refusing to build PR ref') - expect(result.output).toBe('') - } + for (const ref of ['refs/pull/1/head', 'pull/1/head']) { + const result = await vet(earlyVet, ref) + expect(result.code).not.toBe(0) + expect(result.stdout).toContain('Refusing to build PR ref') + expect(result.output).toBe('') } }) }) diff --git a/config/scripts/cli-build/verify-cli-bin.test.mjs b/config/scripts/cli-build/verify-cli-bin.test.mjs index 551d17a3220..861ca851dee 100644 --- a/config/scripts/cli-build/verify-cli-bin.test.mjs +++ b/config/scripts/cli-build/verify-cli-bin.test.mjs @@ -12,10 +12,7 @@ afterEach(() => { }) /** Builds a temporary Orca-style project fixture with a compiled CLI entrypoint. */ -function makeProjectWithCli( - content, - { mode = 0o755, rootPackageType, writeOutPackageJson = true } = {} -) { +function makeProjectWithCli(content) { const projectDir = mkdtempSync(path.join(tmpdir(), 'orca-cli-bin-')) roots.push(projectDir) const cliPath = path.join(projectDir, 'out', 'cli', 'index.js') @@ -23,38 +20,20 @@ function makeProjectWithCli( mkdirSync(path.dirname(cliPath), { recursive: true }) writeFileSync( path.join(projectDir, 'package.json'), - JSON.stringify({ bin: { orca: './out/cli/index.js' }, type: rootPackageType }), + JSON.stringify({ bin: { orca: './out/cli/index.js' }, type: 'module' }), 'utf8' ) - if (writeOutPackageJson) { - writeFileSync(outPackageJsonPath, JSON.stringify({ type: 'commonjs' }), 'utf8') - } writeFileSync(cliPath, content, 'utf8') if (process.platform !== 'win32') { - chmodSync(cliPath, mode) + chmodSync(cliPath, 0o755) } - return { projectDir, cliPath, outPackageJsonPath } + return { projectDir, outPackageJsonPath } } describe('verifyPackageCliBin', () => { - it('accepts a non-empty Node entrypoint and can run help through Node', () => { - const { projectDir, cliPath } = makeProjectWithCli( - '#!/usr/bin/env node\nrequire("node:fs").writeFileSync("help-receipt.json", JSON.stringify({ args: process.argv.slice(2), cwd: process.cwd() }))\nif (!process.argv.includes("--help")) process.exit(9)\n' - ) - - expect(verifyPackageCliBin({ projectDir, runHelp: true })).toMatchObject({ - binPath: cliPath - }) - expect(JSON.parse(readFileSync(path.join(projectDir, 'help-receipt.json'), 'utf8'))).toEqual({ - args: ['--help'], - cwd: projectDir - }) - }) - it('writes a CommonJS package boundary for the compiled CLI directory', () => { const { projectDir, outPackageJsonPath } = makeProjectWithCli( - '#!/usr/bin/env node\nexports.ok = true\nif (process.argv.includes("--help")) process.exit(0)\n', - { rootPackageType: 'module', writeOutPackageJson: false } + '#!/usr/bin/env node\nexports.ok = true\nif (process.argv.includes("--help")) process.exit(0)\n' ) expect(() => verifyPackageCliBin({ projectDir, runHelp: true })).toThrow( diff --git a/config/scripts/daemon-protocol/admission/check-daemon-protocol-crossing.test.mjs b/config/scripts/daemon-protocol/admission/check-daemon-protocol-crossing.test.mjs index 138158702d7..873c390ca28 100644 --- a/config/scripts/daemon-protocol/admission/check-daemon-protocol-crossing.test.mjs +++ b/config/scripts/daemon-protocol/admission/check-daemon-protocol-crossing.test.mjs @@ -5,7 +5,6 @@ import { dirname, join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { checkDaemonProtocolCrossing } from './check-daemon-protocol-crossing.mjs' import { DAEMON_PROTOCOL_SOURCE_PATH } from '../schema-facts/daemon-protocol-facts.mjs' -import { selectLatestStableReleaseTag } from '../../release/versioning/stable-release-tags.mjs' const repos = [] @@ -52,33 +51,6 @@ afterEach(() => { } }) -describe('selectLatestStableReleaseTag', () => { - it.each([ - { tags: ['v1.4.9', 'v1.4.10', 'v1.5.0-rc.1', 'nightly'], expected: 'v1.4.10' }, - { tags: ['v1.9.99', 'v1.10.0'], expected: 'v1.10.0' }, - { tags: ['v9.99.99', 'v10.0.0'], expected: 'v10.0.0' }, - { tags: [], expected: null }, - { tags: ['nightly', 'mobile-v1.2.3', 'v1.2.3-rc.1'], expected: null }, - { tags: ['v0001.4.003', 'v1.04.3'], expected: 'v1.04.3' }, - { tags: ['v1.04.3', 'v0001.4.003'], expected: 'v0001.4.003' }, - { tags: ['v1.2.3', 'v1.2.3\n'], expected: 'v1.2.3' }, - { tags: ['v1.2.3\r\n', 'v0.0.1'], expected: 'v0.0.1' }, - { tags: [`v${'9'.repeat(400)}.1.2`, 'v0.1.2'], expected: 'v0.1.2' }, - { - tags: ['v9007199254740993.1.0', 'v9007199254740992.1.0'], - expected: 'v9007199254740992.1.0' - } - ])( - 'selects the newest stable tag without changing its spelling or input: $tags', - ({ tags, expected }) => { - const input = [...tags] - expect(selectLatestStableReleaseTag(Object.freeze(tags))).toBe(expected) - expect(tags).toEqual(input) - expect(selectLatestStableReleaseTag(['v0.0.0'])).toBe('v0.0.0') - } - ) -}) - describe('checkDaemonProtocolCrossing', () => { it.each(['src/main/daemon/daemon-protocol-version.ts', DAEMON_PROTOCOL_SOURCE_PATH])( 'reads the pinned release from %s while checking the dirty candidate', @@ -112,14 +84,4 @@ describe('checkDaemonProtocolCrossing', () => { /no stable release tags/ ) }) - - it('honors an explicit release ref', () => { - const repo = repoWithTags([ - ['v1.0.0', 5], - ['v1.0.1', 6] - ]) - const result = checkDaemonProtocolCrossing({ repoRoot: repo, releaseRef: 'v1.0.0', env }) - expect(result).toMatchObject({ upgrade: true, rollback: false }) - expect(result.lines[0]).toBe('release v1.0.0: daemon protocol 5') - }) }) diff --git a/config/scripts/daemon-protocol/admission/check-runtime-launcher-protocol-ratchet.test.mjs b/config/scripts/daemon-protocol/admission/check-runtime-launcher-protocol-ratchet.test.mjs index 04f4ab63b67..357c6f81c81 100644 --- a/config/scripts/daemon-protocol/admission/check-runtime-launcher-protocol-ratchet.test.mjs +++ b/config/scripts/daemon-protocol/admission/check-runtime-launcher-protocol-ratchet.test.mjs @@ -25,11 +25,11 @@ function write(repo, path, contents) { writeFileSync(file, contents) } -function writeProtocol(repo, current, sourcePath = DAEMON_PROTOCOL_SOURCE_PATH) { +function writeProtocol(repo, current) { const previous = Array.from({ length: current - 1 }, (_, index) => index + 1) write( repo, - sourcePath, + DAEMON_PROTOCOL_SOURCE_PATH, `export const PROTOCOL_VERSION = ${current}\nexport const PREVIOUS_DAEMON_PROTOCOL_VERSIONS = [${previous.join(', ')}] as const\n` ) } @@ -51,11 +51,11 @@ function commit(repo, message) { } /** A base commit at protocol 3, then a candidate commit shaped by `change`. */ -function repoWithCandidate(change, sourcePath = DAEMON_PROTOCOL_SOURCE_PATH) { +function repoWithCandidate(change) { const repo = mkdtempSync(join(tmpdir(), 'runtime-launcher-ratchet-')) repos.push(repo) git(repo, ['init', '-q']) - writeProtocol(repo, 3, sourcePath) + writeProtocol(repo, 3) write(repo, LAUNCHER, 'export const launcher = 1\n') write(repo, 'src/unrelated.ts', 'export const unrelated = 1\n') const base = commit(repo, 'base') @@ -71,17 +71,6 @@ afterEach(() => { }) describe('checkRuntimeLauncherProtocolRatchet against git history', () => { - it('enforces the restriction when the base predates the protocol source move', () => { - const { repo, base } = repoWithCandidate((candidate) => { - writeProtocol(candidate, 4) - write(candidate, LAUNCHER, 'export const launcher = 2\n') - }, 'src/main/daemon/daemon-protocol-version.ts') - expect(checkRuntimeLauncherProtocolRatchet({ repoRoot: repo, base, env: {} })).toMatchObject({ - ok: false, - violated: true - }) - }) - it('fails when the diff from the base both edits a launcher and bumps PROTOCOL_VERSION', () => { const { repo, base } = repoWithCandidate((candidate) => { writeProtocol(candidate, 4) diff --git a/config/scripts/daemon-protocol/schema-facts/daemon-protocol-facts.test.mjs b/config/scripts/daemon-protocol/schema-facts/daemon-protocol-facts.test.mjs index 0b70f1091f8..16003ffe708 100644 --- a/config/scripts/daemon-protocol/schema-facts/daemon-protocol-facts.test.mjs +++ b/config/scripts/daemon-protocol/schema-facts/daemon-protocol-facts.test.mjs @@ -1,14 +1,6 @@ import { describe, expect, it } from 'vitest' import { parseDaemonProtocolFacts } from './daemon-protocol-facts.mjs' -function source(current, previous) { - return [ - `export const PROTOCOL_VERSION = ${current}`, - 'export const OTHER_DAEMON_PROTOCOL_VERSION = 5', - `export const PREVIOUS_DAEMON_PROTOCOL_VERSIONS = [${previous}] as const` - ].join('\n') -} - const facts = (protocolVersion, previousProtocolVersions) => ({ protocolVersion, previousProtocolVersions @@ -27,20 +19,9 @@ describe('parseDaemonProtocolFacts', () => { expect(parseDaemonProtocolFacts(text)).toEqual(facts(4, [1, 2, 3])) }) - it.each([ - ['a missing PROTOCOL_VERSION', 'export const PREVIOUS_DAEMON_PROTOCOL_VERSIONS = [1]'], - ['a missing previous list', 'export const PROTOCOL_VERSION = 2'], - ['a non-literal PROTOCOL_VERSION', source('NEXT_VERSION', '1')], - ['a spread in the previous list', source(3, '...LEGACY, 2')], - ['a comment in the previous list', source(3, '1, // legacy\n 2')], - ['an empty previous list past v1', source(3, '')], - ['a previous version at or above current', source(3, '1, 2, 3')], - ['a duplicate declaration', `${source(3, '1, 2')}\nexport const PROTOCOL_VERSION = 4`], - [ - 'a list moved behind a constant', - 'export const PROTOCOL_VERSION = 3\nexport const PREVIOUS_DAEMON_PROTOCOL_VERSIONS = LEGACY' - ] - ])('fails loudly on %s', (_name, text) => { + it('rejects a non-literal protocol declaration with its source label', () => { + const text = + 'export const PROTOCOL_VERSION = NEXT_VERSION\nexport const PREVIOUS_DAEMON_PROTOCOL_VERSIONS = [1] as const' expect(() => parseDaemonProtocolFacts(text, 'fixture.ts')).toThrow(/fixture\.ts/) }) }) diff --git a/config/scripts/dependency-security/audit-tools/dependency-security-patches.test.mjs b/config/scripts/dependency-security/audit-tools/dependency-security-patches.test.mjs index 0d7d8c907c9..d1237c36711 100644 --- a/config/scripts/dependency-security/audit-tools/dependency-security-patches.test.mjs +++ b/config/scripts/dependency-security/audit-tools/dependency-security-patches.test.mjs @@ -25,11 +25,5 @@ describe('installed dependency security patches', () => { expect(() => sprintf(`%.${'9'.repeat(500)}${type}`, 1.234)).not.toThrow() expect(sprintf(`%.${'9'.repeat(500)}${type}`, 1.234).length).toBeLessThan(110) } - expect(sprintf('%.2f', 1.234)).toBe('1.23') - expect(sprintf('%.2e', 1.234)).toBe('1.23e+0') - expect(sprintf('%.2g', 1.234)).toBe('1.2') - expect(sprintf('%.3s', 'abcdef')).toBe('abc') - expect(sprintf('%.0f', 1.7)).toBe('2') - expect(sprintf('%.0e', 1.7)).toBe('2e+0') }) }) diff --git a/config/scripts/dependency-security/audit-tools/run-audit-tool.test.mjs b/config/scripts/dependency-security/audit-tools/run-audit-tool.test.mjs deleted file mode 100644 index 1d521b5fbb6..00000000000 --- a/config/scripts/dependency-security/audit-tools/run-audit-tool.test.mjs +++ /dev/null @@ -1,10 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { runAuditTool } from './run-audit-tool.mjs' - -describe('locked audit tool execution', () => { - it('rejects an unknown executable before installing anything', () => { - const run = vi.fn() - expect(() => runAuditTool('../arbitrary-package', [], { run })).toThrow('Unknown audit tool') - expect(run).not.toHaveBeenCalled() - }) -}) diff --git a/config/scripts/desktop-build/output-contracts/dark-build-switches.test.mjs b/config/scripts/desktop-build/output-contracts/dark-build-switches.test.mjs deleted file mode 100644 index eb6ede9a267..00000000000 --- a/config/scripts/desktop-build/output-contracts/dark-build-switches.test.mjs +++ /dev/null @@ -1,45 +0,0 @@ -import { mkdtemp, rm, writeFile } from 'node:fs/promises' -import { createRequire } from 'node:module' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterEach, describe, expect, it } from 'vitest' - -const { assertNoDarkOutputOfOffSwitches, assertNotDarkBuild, darkBuildMarkers } = createRequire( - import.meta.url -)('../../../dark-build-switches.cjs') - -const scratch = [] -afterEach(async () => { - await Promise.all(scratch.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))) -}) - -describe('dark build switches', () => { - it.each(['ORCA_LAYOUT_OWNER', 'ORCA_SIDEBAR_INPUTS'])( - 'refuses %s at packaging time, and any output its dark build marked', - async (variable) => { - const clean = await mkdtemp(join(tmpdir(), 'orca-dark-build-clean-')) - const dark = await mkdtemp(join(tmpdir(), 'orca-dark-build-dark-')) - scratch.push(clean, dark) - const [marker] = darkBuildMarkers({ [variable]: 'main' }) - await writeFile(join(dark, marker.fileName), marker.text) - - expect(() => assertNotDarkBuild([clean], {})).not.toThrow() - expect(() => assertNotDarkBuild([clean], { [variable]: 'main' })).toThrow(`${variable}=main`) - expect(() => assertNotDarkBuild([clean, dark], {})).toThrow(dark) - } - ) - - it.each([ - ['ORCA_LAYOUT_OWNER', 'ORCA_SIDEBAR_INPUTS'], - ['ORCA_SIDEBAR_INPUTS', 'ORCA_LAYOUT_OWNER'] - ])('lets a dark build carry its own %s output, never one of %s', async (own, other) => { - const dark = await mkdtemp(join(tmpdir(), 'orca-dark-build-own-')) - scratch.push(dark) - const [marker] = darkBuildMarkers({ [own]: 'main' }) - await writeFile(join(dark, marker.fileName), marker.text) - - expect(() => assertNoDarkOutputOfOffSwitches([dark], { [own]: 'main' })).not.toThrow() - expect(() => assertNoDarkOutputOfOffSwitches([dark], { [other]: 'main' })).toThrow(dark) - expect(() => assertNoDarkOutputOfOffSwitches([dark], {})).toThrow(dark) - }) -}) diff --git a/config/scripts/desktop-build/output-contracts/electron-vite-output-contract.test.ts b/config/scripts/desktop-build/output-contracts/electron-vite-output-contract.test.ts index 0671f58ea19..f6116aa8ab9 100644 --- a/config/scripts/desktop-build/output-contracts/electron-vite-output-contract.test.ts +++ b/config/scripts/desktop-build/output-contracts/electron-vite-output-contract.test.ts @@ -23,7 +23,6 @@ type BootstrapProcessMock = EventEmitter & { function failBootstrapWithBanner(options: { env: Record tmpdir?: string - stderrWrites?: string[] }): BootstrapProcessMock { const processMock = new EventEmitter() as BootstrapProcessMock processMock.env = options.env @@ -33,7 +32,6 @@ function failBootstrapWithBanner(options: { ...nodeFs, writeSync: (descriptor: number, data: string) => { if (descriptor === 2) { - options.stderrWrites?.push(data) return data.length } return nodeFs.writeSync(descriptor, data) @@ -63,27 +61,6 @@ function failBootstrapWithBanner(options: { } describe('Electron Vite output contract', () => { - it('records the bootstrap failure it exits on, since the guard hides Electron dialog', () => { - const logDirectory = mkdtempSync(join(tmpdir(), 'orca-bootstrap-fatal-')) - const logPath = join(logDirectory, 'fatal.log') - const stderrWrites: string[] = [] - - try { - const processMock = failBootstrapWithBanner({ - env: { [BOOTSTRAP_FATAL_LOG_ENV_VAR]: logPath }, - stderrWrites - }) - - expect(stderrWrites.join('')).toContain("Cannot find module 'ws'") - const recorded = readFileSync(logPath, 'utf8') - expect(recorded).toContain("Cannot find module 'ws'") - expect(recorded).toContain('pid=4242') - expect(processMock.exitCode).toBe(1) - } finally { - rmSync(logDirectory, { recursive: true, force: true }) - } - }) - it('creates the parent directory an overridden log path names but does not have', () => { const logDirectory = mkdtempSync(join(tmpdir(), 'orca-bootstrap-fatal-')) const logPath = join(logDirectory, 'nested', 'diagnostics', 'fatal.log') diff --git a/config/scripts/desktop-build/output-contracts/main-entry-facade.test.ts b/config/scripts/desktop-build/output-contracts/main-entry-facade.test.ts deleted file mode 100644 index 891002e3a4c..00000000000 --- a/config/scripts/desktop-build/output-contracts/main-entry-facade.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { expect, it } from 'vitest' -import { build } from 'vite' -import { runProcess } from '@orca/process-host' -import { electronViteConfig } from '../../../../electron.vite.config' - -it('keeps main code and asset paths working after a CLI build replaces its public entry', async () => { - const root = await mkdtemp(join(tmpdir(), 'orca-main-entry-facade-')) - try { - await writeFile(join(root, 'package.json'), '{"type":"commonjs"}') - await writeFile(join(root, 'tracker.ts'), 'export const createTracker = () => ({ count: 0 })') - await writeFile( - join(root, 'boot-label.ts'), - 'export const getBootLabel = () => process.env.ORCA_FIXTURE_BOOT_LABEL || "boot"' - ) - await writeFile( - join(root, 'late.ts'), - 'import { getBootLabel } from "./boot-label"; export const readLabel = () => getBootLabel()' - ) - await writeFile( - join(root, 'controls.ts'), - 'import { createTracker } from "./tracker"; export const getStatus = () => createTracker().count' - ) - await writeFile( - join(root, 'main.ts'), - 'import { writeFileSync } from "node:fs"; import { join } from "node:path"; import { getStatus } from "./controls"; import { createTracker } from "./tracker"; import { getBootLabel } from "./boot-label"; import("./late").then(({ readLabel }) => writeFileSync(join(__dirname, "result.json"), JSON.stringify({ status: getStatus(), tracker: createTracker(), directory: __dirname, label: getBootLabel(), lateLabel: readLabel() })))' - ) - const options = electronViteConfig.main?.build?.rollupOptions - if (Array.isArray(options?.output)) { - throw new Error('Expected one main-process output') - } - await build({ - configFile: false, - root, - logLevel: 'silent', - build: { - ssr: true, - minify: electronViteConfig.main?.build?.minify, - rolldownOptions: { - ...options, - // Plugins and the bootstrap banner guard the real main entry, not this fixture. - output: { ...options?.output, postBanner: undefined }, - plugins: [], - input: { index: join(root, 'main.ts'), controls: join(root, 'controls.ts') } - } - } - }) - // tsc emits public names only, replacing the bundler's entry at this same path. - await writeFile(join(root, 'dist', 'controls.js'), 'exports.getStatus = () => 0') - const result = await runProcess({ - program: process.execPath, - args: [join(root, 'dist', 'index.js')], - env: { ...process.env, ORCA_FIXTURE_BOOT_LABEL: 'fixture-boot' } - }) - expect(result.code, result.stderr).toBe(0) - expect(JSON.parse(await readFile(join(root, 'dist', 'result.json'), 'utf8'))).toEqual({ - status: 0, - tracker: { count: 0 }, - directory: join(root, 'dist'), - label: 'fixture-boot', - lateLabel: 'fixture-boot' - }) - } finally { - await rm(root, { recursive: true, force: true }) - } -}) diff --git a/config/scripts/desktop-build/output-contracts/plain-node-entry-guard.test.ts b/config/scripts/desktop-build/output-contracts/plain-node-entry-guard.test.ts index 366ad1df519..ef8a837b43f 100644 --- a/config/scripts/desktop-build/output-contracts/plain-node-entry-guard.test.ts +++ b/config/scripts/desktop-build/output-contracts/plain-node-entry-guard.test.ts @@ -77,14 +77,6 @@ describe('plain Node entry guard', () => { await expect(runCloseBundle(plugin)).rejects.toThrow('failed to load under plain Node') }) - it('rejects Electron imports during the static bundle scan', () => { - const plugin = createPlainNodeEntryGuardPlugin() - - expect(() => runWriteBundle(plugin, createOutputDir(), 'require("electron")')).toThrow( - 'requires electron' - ) - }) - it('rejects Electron subpath requires', () => { const plugin = createPlainNodeEntryGuardPlugin() @@ -117,8 +109,6 @@ describe('plain Node entry guard', () => { await expect(runCloseBundle(plugin)).rejects.toThrow('did not reject an empty argv') }) - - // daemon-entry traps SIGTERM; smoke timeout must escalate to SIGKILL or spawnSync can wait forever. }) // Electron is unavailable in main-process workers too (#11161); transitive imports can kill startup despite entry-local electron-free comments. @@ -153,18 +143,6 @@ describe('CLI and worker thread entry guard', () => { } as Rollup.OutputChunk } - it('rejects an Electron require reachable from a worker entry', () => { - const plugin = createPlainNodeEntryGuardPlugin() - const bundle: Rollup.OutputBundle = { - 'port-scan-command-worker-entry.js': entryChunk( - 'port-scan-command-worker-entry', - 'require("electron")' - ) - } - - expect(() => runEntryWriteBundle(plugin, bundle)).toThrow('requires electron') - }) - // Exercise transitive shared-chunk imports into the electron client, where direct-import checks miss startup failures. it('follows shared chunks out of a worker entry', () => { const plugin = createPlainNodeEntryGuardPlugin() @@ -187,16 +165,4 @@ describe('CLI and worker thread entry guard', () => { expect(() => runEntryWriteBundle(plugin, bundle)).toThrow('chunks/shared.js') }) - - it('passes a clean worker entry', () => { - const plugin = createPlainNodeEntryGuardPlugin() - const bundle: Rollup.OutputBundle = { - 'warp-theme-parser-worker.js': entryChunk( - 'warp-theme-parser-worker', - 'require("node:worker_threads")' - ) - } - - expect(() => runEntryWriteBundle(plugin, bundle)).not.toThrow() - }) }) diff --git a/config/scripts/desktop-distribution/channel-policy/dev-channel-windows-staging.test.mjs b/config/scripts/desktop-distribution/channel-policy/dev-channel-windows-staging.test.mjs index 1985a424d38..e77f617761e 100644 --- a/config/scripts/desktop-distribution/channel-policy/dev-channel-windows-staging.test.mjs +++ b/config/scripts/desktop-distribution/channel-policy/dev-channel-windows-staging.test.mjs @@ -11,9 +11,7 @@ function readWorkflow(name) { ) } -const build = readWorkflow('dev-channel-win-build.yml') const publish = readWorkflow('dev-channel-win-publish.yml') -const buildSteps = build.jobs['build-win'].steps const publishSteps = publish.jobs['publish-win'].steps const directory = mkdtempSync(join(tmpdir(), 'dev-channel-windows-staging-')) afterAll(() => rmSync(directory, { recursive: true, force: true })) @@ -23,23 +21,22 @@ async function runScript(script, { cwd = directory, env = {} } = {}) { } describe('Windows dev-channel artifact staging', () => { - it.each([ - ['build', buildSteps, 'dist'], - ['publish', publishSteps, 'staged-windows'] - ])('fails %s on any missing or empty staged file', async (_name, steps, folder) => { - const step = steps.find((entry) => entry.name === 'Validate staged Windows artifacts') + it('rejects an empty staged installer before publication', async () => { + const step = publishSteps.find((entry) => entry.name === 'Validate staged Windows artifacts') const attempt = mkdtempSync(join(directory, 'files-')) + const folder = 'staged-windows' mkdirSync(join(attempt, folder)) - expect((await runScript(step.run, { cwd: attempt })).code).toBe(1) for (const file of [ 'orca-windows-setup.exe', 'orca-windows-setup.exe.blockmap', 'latest.yml' ]) { - writeFileSync(join(attempt, folder, file), '') - expect((await runScript(step.run, { cwd: attempt })).code).toBe(1) writeFileSync(join(attempt, folder, file), 'artifact') } + const installer = join(attempt, folder, 'orca-windows-setup.exe') + writeFileSync(installer, '') + expect((await runScript(step.run, { cwd: attempt })).code).toBe(1) + writeFileSync(installer, 'artifact') expect((await runScript(step.run, { cwd: attempt })).code).toBe(0) }) diff --git a/config/scripts/desktop-distribution/linux/package-linux-formats-appimage.test.mjs b/config/scripts/desktop-distribution/linux/package-linux-formats-appimage.test.mjs index 9aec79bb4e8..1c66ff6bc0f 100644 --- a/config/scripts/desktop-distribution/linux/package-linux-formats-appimage.test.mjs +++ b/config/scripts/desktop-distribution/linux/package-linux-formats-appimage.test.mjs @@ -138,16 +138,4 @@ describe.skipIf(process.platform === 'win32')('real executable AppImage tool ove ]) expect(existsSync(join(root, 'expanded'))).toBe(false) }) - - it('propagates the original executable failure without a fallback', async () => { - const environment = await prepare() - vi.stubEnv('ORCA_TEST_SQUASHFS_EXIT', '17') - const result = runProcessSync({ - program: join(overlay, 'mksquashfs'), - args: ['app', 'output', '-comp', 'zstd'], - env: { ...process.env, ...environment }, - timeoutMs: 10_000 - }) - expect(result.code).toBe(17) - }) }) diff --git a/config/scripts/desktop-distribution/linux/package-linux-formats.test.mjs b/config/scripts/desktop-distribution/linux/package-linux-formats.test.mjs index 53d9f679e5f..f9cebac3872 100644 --- a/config/scripts/desktop-distribution/linux/package-linux-formats.test.mjs +++ b/config/scripts/desktop-distribution/linux/package-linux-formats.test.mjs @@ -5,10 +5,7 @@ import { mkdtempSync, readFileSync, readdirSync, - readlinkSync, rmSync, - statSync, - symlinkSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' @@ -19,7 +16,6 @@ import { packageLinuxFormats } from './package-linux-formats.mjs' let root let preparedDirectory let outputDirectory -const targets = ['AppImage', 'deb', 'rpm'] const valueAfter = (args, flag) => args[args.indexOf(flag) + 1] beforeEach(() => { @@ -48,120 +44,52 @@ function emitPackage(args) { } describe('independent Linux package formats', () => { - it('starts all formats before awaiting completion and isolates metadata writes', async () => { - if (process.platform !== 'win32') { - symlinkSync('resources/package-type', join(preparedDirectory, 'marker-link')) - } + it('settles every worker before cleanup and exposes no partial artifact on builder failure', async () => { + const finished = [] + let overlay let release const gate = new Promise((resolve) => { release = resolve }) - const copies = [] - await packageLinuxFormats({ - preparedDirectory, - outputDirectory, - prepareAppImageTools: async () => ({}), - runBuilder: async (args) => { - const app = valueAfter(args, '--prepackaged') - const format = valueAfter(args, '--linux') - expect(readFileSync(join(app, 'resources/package-type'), 'utf8')).toBe('AppImage') - expect(readFileSync(join(app, 'resources/app.asar'), 'utf8')).toBe('verified application') - if (process.platform !== 'win32') { - expect(readlinkSync(join(app, 'marker-link'))).toBe('resources/package-type') - expect(statSync(join(app, 'orca-ide')).mode & 0o777).toBe(0o755) - expect(statSync(join(app, 'resources/package-type')).ino).not.toBe( - statSync(join(preparedDirectory, 'resources/package-type')).ino - ) - } - copies.push(app) - if (copies.length === 3) { - release() - } - await gate - expect(readdirSync(outputDirectory)).toHaveLength(1) - emitPackage(args) - await Promise.resolve() - expect(readFileSync(join(app, 'resources/package-type'), 'utf8')).toBe(format) - } - }) - expect(new Set(copies).size).toBe(3) - expect(readFileSync(join(preparedDirectory, 'resources/package-type'), 'utf8')).toBe('AppImage') - expect(readFileSync(join(preparedDirectory, 'resources/app.asar'), 'utf8')).toBe( - 'verified application' - ) - for (const format of targets) { - expect(readFileSync(join(outputDirectory, `orca.${format}`), 'utf8')).toBe(format) - expect( - readFileSync( - join(outputDirectory, 'linux-package-formats', format, 'latest-linux.yml'), - 'utf8' - ) - ).toBe(format) - } - expect( - readdirSync(outputDirectory).some((name) => name.startsWith('.linux-package-formats-')) - ).toBe(false) - }) - - it.each(['builder', 'tool preparation'])( - 'settles every worker before cleanup and exposes no partial artifact on %s failure', - async (failurePhase) => { - const finished = [] - let overlay - let release - const gate = new Promise((resolve) => { - release = resolve - }) - await expect( - packageLinuxFormats({ - preparedDirectory, - outputDirectory, - prepareAppImageTools: async ({ directory }) => { - overlay = directory - mkdirSync(directory) - writeFileSync(join(directory, 'mksquashfs'), 'private wrapper') - if (failurePhase === 'tool preparation') { - throw new Error('compression failed') - } - return { APPIMAGE_TOOLS_PATH: directory, ORCA_PR_APPIMAGE_MKSQUASHFS: 'original tool' } - }, - runBuilder: async (args, environment) => { - const format = valueAfter(args, '--linux') - if (format === 'AppImage') { - expect(environment).toEqual({ - APPIMAGE_TOOLS_PATH: overlay, - ORCA_PR_APPIMAGE_MKSQUASHFS: 'original tool' - }) - throw new Error('compression failed') - } - expect(environment).toEqual({}) - if (format === 'rpm') { - release() - } - await gate - const { app } = emitPackage(args) - expect(existsSync(app)).toBe(true) - expect(existsSync(overlay)).toBe(true) - finished.push(format) + await expect( + packageLinuxFormats({ + preparedDirectory, + outputDirectory, + prepareAppImageTools: async ({ directory }) => { + overlay = directory + mkdirSync(directory) + writeFileSync(join(directory, 'mksquashfs'), 'private wrapper') + return { APPIMAGE_TOOLS_PATH: directory, ORCA_PR_APPIMAGE_MKSQUASHFS: 'original tool' } + }, + runBuilder: async (args) => { + const format = valueAfter(args, '--linux') + if (format === 'AppImage') { + throw new Error('compression failed') } - }) - ).rejects.toMatchObject({ - message: 'Linux package formats failed', - errors: [ - expect.objectContaining({ - message: 'AppImage packaging failed', - cause: expect.objectContaining({ message: 'compression failed' }) - }) - ] + if (format === 'rpm') { + release() + } + await gate + const { app } = emitPackage(args) + expect(existsSync(app)).toBe(true) + expect(existsSync(overlay)).toBe(true) + finished.push(format) + } }) - expect(finished.sort()).toEqual(['deb', 'rpm']) - expect(existsSync(overlay)).toBe(false) - expect(readdirSync(outputDirectory)).toEqual([]) - expect(readFileSync(join(preparedDirectory, 'resources/package-type'), 'utf8')).toBe( - 'AppImage' - ) - } - ) + ).rejects.toMatchObject({ + message: 'Linux package formats failed', + errors: [ + expect.objectContaining({ + message: 'AppImage packaging failed', + cause: expect.objectContaining({ message: 'compression failed' }) + }) + ] + }) + expect(finished.sort()).toEqual(['deb', 'rpm']) + expect(existsSync(overlay)).toBe(false) + expect(readdirSync(outputDirectory)).toEqual([]) + expect(readFileSync(join(preparedDirectory, 'resources/package-type'), 'utf8')).toBe('AppImage') + }) it('rejects missing artifacts even when the builder reports success', async () => { await expect( @@ -195,19 +123,4 @@ describe('independent Linux package formats', () => { expect(readdirSync(outputDirectory)).toEqual(['orca.deb']) expect(readFileSync(join(outputDirectory, 'orca.deb'), 'utf8')).toBe('previous package') }) - - it('rejects a tree already mutated by a previous root-package build', async () => { - writeFileSync(join(preparedDirectory, 'resources/package-type'), 'rpm') - await expect( - packageLinuxFormats({ - preparedDirectory, - outputDirectory, - prepareAppImageTools: async () => ({}), - runBuilder: async () => { - throw new Error('must not run') - } - }) - ).rejects.toThrow('Expected a fresh Linux directory build') - expect(readdirSync(outputDirectory)).toEqual([]) - }) }) diff --git a/config/scripts/desktop-distribution/macos/adhoc-mac-manifests.test.mjs b/config/scripts/desktop-distribution/macos/adhoc-mac-manifests.test.mjs index 3965aa4ab9b..5d5320499f0 100644 --- a/config/scripts/desktop-distribution/macos/adhoc-mac-manifests.test.mjs +++ b/config/scripts/desktop-distribution/macos/adhoc-mac-manifests.test.mjs @@ -40,7 +40,7 @@ describe('parallel Mac updater manifests', () => { asset.name.endsWith('.blockmap') ? 'map!' : content ) } - expect(await stageManifest(item.manifest, item.arch, version, directory)).toEqual(item) + await stageManifest(item.manifest, item.arch, version, directory) await writeFile(join(directory, item.manifest.path), Buffer.alloc(content.length)) await expect(stageManifest(item.manifest, item.arch, version, directory)).rejects.toThrow( 'checksum or size' diff --git a/config/scripts/desktop-distribution/macos/build-mac-notarization-overlap.test.mjs b/config/scripts/desktop-distribution/macos/build-mac-notarization-overlap.test.mjs deleted file mode 100644 index f8570b0b086..00000000000 --- a/config/scripts/desktop-distribution/macos/build-mac-notarization-overlap.test.mjs +++ /dev/null @@ -1,155 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { buildWithOverlappingNotarization } from './build-mac-notarization-overlap.mjs' - -const deferred = () => { - let resolve, reject - const promise = new Promise((yes, no) => { - resolve = yes - reject = no - }) - return { promise, resolve, reject } -} -const context = (arch) => ({ - arch, - electronPlatformName: 'darwin', - appOutDir: `dist/mac-${arch}`, - packager: { appInfo: { productFilename: 'Orca' } } -}) -const signedConfig = () => ({ - forceCodeSigning: true, - mac: { notarize: true, hardenedRuntime: true }, - concurrency: { jobs: 8 }, - publish: { releaseType: 'draft' } -}) -const options = (build, notarize = vi.fn(async () => {}), verify = vi.fn()) => ({ - config: signedConfig(), - build, - notarize, - verify, - targets: 'both-mac-targets', - publish: 'never', - credentials: { appleId: 'test' }, - log: vi.fn() -}) - -// The real builder queues each artifact task without awaiting it in its serial pack loop. -function queuedBuilder(events, afterSign) { - return async ({ config, ...buildOptions }) => { - expect(buildOptions).toMatchObject({ targets: 'both-mac-targets', publish: 'never' }) - expect(config.mac).toMatchObject({ notarize: false, hardenedRuntime: true }) - expect(config.forceCodeSigning).toBe(true) - expect(config.concurrency.jobs).toBe(1) - const artifacts = [] - for (const arch of [1, 3]) { - events.push(`pack:${arch}`) - await config.afterSign(context(arch)) - artifacts.push( - (async () => { - await config.artifactBuildStarted({ arch }) - events.push(`archive:${arch}`) - })() - ) - await afterSign?.(arch) - } - await Promise.all(artifacts) - return ['x64.dmg', 'arm64.dmg'] - } -} - -describe('overlapping macOS notarization', () => { - it('packs serially, overlaps Apple waits, and gates every artifact on both verified tickets', async () => { - const events = [] - const x64 = deferred(), - arm64 = deferred(), - bothSubmitted = deferred() - const notarize = vi.fn(({ appPath }) => { - events.push(`submit:${appPath}`) - if (appPath.includes('mac-3')) { - bothSubmitted.resolve() - } - return appPath.includes('mac-1') ? x64.promise : arm64.promise - }) - const verify = vi.fn((appPath) => { - events.push(`verify:${appPath}`) - }) - const result = buildWithOverlappingNotarization( - options(queuedBuilder(events), notarize, verify) - ) - await bothSubmitted.promise - expect(events.filter((event) => event.startsWith('pack:'))).toEqual(['pack:1', 'pack:3']) - x64.resolve() - await vi.waitFor(() => expect(verify).toHaveBeenCalledTimes(1)) - expect(events.some((event) => event.startsWith('archive:'))).toBe(false) - arm64.resolve() - await expect(result).resolves.toEqual(['x64.dmg', 'arm64.dmg']) - expect(events.findIndex((event) => event.startsWith('archive:'))).toBeGreaterThan( - events.findLastIndex((event) => event.startsWith('verify:')) - ) - }) - - it.each([1, 3])('never archives when architecture %i notarization fails', async (failedArch) => { - const events = [] - const failure = new Error('Apple rejected the app') - const notarize = vi.fn(async ({ appPath }) => { - if (appPath.includes(`mac-${failedArch}`)) { - throw failure - } - }) - await expect( - buildWithOverlappingNotarization(options(queuedBuilder(events), notarize)) - ).rejects.toBe(failure) - expect(events.some((event) => event.startsWith('archive:'))).toBe(false) - }) - - it('never archives when signature or stapler validation fails', async () => { - const events = [] - await expect( - buildWithOverlappingNotarization( - options(queuedBuilder(events), undefined, () => { - throw new Error('invalid staple') - }) - ) - ).rejects.toThrow('invalid staple') - expect(events.some((event) => event.startsWith('archive:'))).toBe(false) - }) - - it('rejects waiting artifacts and drains the first notary if the second pack fails', async () => { - const waiting = deferred(), - started = deferred() - let artifactResult - const failure = new Error('second native rebuild failed') - const build = async ({ config }) => { - await config.afterSign(context(1)) - artifactResult = config.artifactBuildStarted({ arch: 1 }).catch((error) => error) - throw failure - } - const result = buildWithOverlappingNotarization( - options(build, async () => { - started.resolve() - await waiting.promise - }) - ) - const settled = vi.fn() - result.catch(settled) - await started.promise - expect(await artifactResult).toBe(failure) - expect(settled).not.toHaveBeenCalled() - waiting.resolve() - await expect(result).rejects.toBe(failure) - }) - - it('does not mask a build failure with a later notary failure', async () => { - const failure = new Error('pack failed') - const build = async ({ config }) => { - await config.afterSign(context(1)) - throw failure - } - await expect( - buildWithOverlappingNotarization( - options(build, async () => { - throw new Error('notary failed too') - }) - ) - ).rejects.toBe(failure) - }) -}) diff --git a/config/scripts/desktop-distribution/nsis/replace-cached-nsis-elevate.test.mjs b/config/scripts/desktop-distribution/nsis/replace-cached-nsis-elevate.test.mjs index d862bbb43e7..cca1a50a234 100644 --- a/config/scripts/desktop-distribution/nsis/replace-cached-nsis-elevate.test.mjs +++ b/config/scripts/desktop-distribution/nsis/replace-cached-nsis-elevate.test.mjs @@ -4,14 +4,8 @@ import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import { - findCachedElevatePaths, - replaceCachedElevateHelpers, - summarizeSwap -} from './replace-cached-nsis-elevate.mjs' +import { replaceCachedElevateHelpers } from './replace-cached-nsis-elevate.mjs' -// Inject app-builder-lib’s packed-path probe so tests need neither network nor warm toolset cache. -const probeFound = (path) => async () => ({ path, error: null }) const probeUnavailable = async () => ({ path: null, error: 'app-builder-lib not loadable' }) const projectRoot = resolve(import.meta.dirname, '../../../..') @@ -42,11 +36,6 @@ function makeCache(...relativeFiles) { } describe('cached elevate.exe swap covers the real electron-builder layouts', () => { - // Model real /- legacy/unified layouts; old /nsis matched neither. Custom layout is covered by probe tests. - - // Allow nsis-resources overmatch: it ships plugins without elevate.exe and costs one cheap read; guessing narrower upstream naming is brittle. - - // Replace all cached copies because only app-builder-lib knows which one repack chooses. it('replaces every cached copy when several bundles are present', async () => { const cacheDir = makeCache( 'nsis-3.0.4.1/nsis-3.0.4.1-1mx3n/elevate.exe', @@ -67,38 +56,9 @@ describe('cached elevate.exe swap covers the real electron-builder layouts', () expect(readFileSync(path, 'utf8')).toBe('signpath-signed-elevate') } }) - - // Compare real cache scan against independent unbounded walk to catch vacuous empty matches; skip only if no bundle is cached. -}) - -describe('the probe, not the scan, decides whether the swap worked', () => { - // customNsisBinary uses nsis-version only as memoization key; URL-parent extraction directory can bypass nsis* scan. - it('covers a custom bundle the directory scan cannot match', async () => { - const relative = 'orca-nsis-mirror/nsis-custom-3.11-0zqp2/elevate.exe' - const cacheDir = makeCache(relative) - const packed = join(cacheDir, ...relative.split('/')) - const signed = join(scratch, 'signed-elevate.exe') - writeFileSync(signed, 'signpath-signed-elevate') - - expect(findCachedElevatePaths(cacheDir, { env: {} })).toEqual([]) - - const result = await replaceCachedElevateHelpers({ - signedPath: signed, - cacheDir, - env: {}, - probe: probeFound(packed) - }) - - expect(result.toolsetReplaced).toBe(true) - expect(readFileSync(packed, 'utf8')).toBe('signpath-signed-elevate') - expect(summarizeSwap(result)).toEqual({ annotations: [], exitCode: 0 }) - }) - - // Prefix-restored stale release can satisfy replaced.length while authoritative packed bundle sits outside scan; model both copies. }) describe('a cached elevate.exe miss is not silent', () => { - // Override short-circuits downloads, so authoritative probe fails offline instead of fetching a toolset. function runScript(cacheDir, nsisDir, signedPath) { return spawnSync(process.execPath, [scriptPath, signedPath], { cwd: projectRoot, @@ -141,7 +101,6 @@ describe('a cached elevate.exe miss is not silent', () => { ).toBe('signpath-signed-elevate') }) - // Healthy release path replaces exact copy returned by app-builder-lib, requiring no warning. it('exits clean when the probe resolves the copy the rebuild will pack', () => { const cacheDir = makeCache() const nsisDir = join(scratch, 'nsis-bundle') diff --git a/config/scripts/desktop-distribution/runtime-verification/electron-builder-native-rebuild.test.mjs b/config/scripts/desktop-distribution/runtime-verification/electron-builder-native-rebuild.test.mjs deleted file mode 100644 index b1b3af4a9b3..00000000000 --- a/config/scripts/desktop-distribution/runtime-verification/electron-builder-native-rebuild.test.mjs +++ /dev/null @@ -1,12 +0,0 @@ -import { createRequire } from 'node:module' -import { describe, expect, it } from 'vitest' - -const require = createRequire(import.meta.url) -const { buildNativeRebuildArgs } = require('./electron-builder-native-rebuild.cjs') - -describe('electron-builder native rebuild hook', () => { - it('rejects incomplete electron-builder contexts', () => { - expect(() => buildNativeRebuildArgs({ arch: 'x64' })).toThrow(/platform/) - expect(() => buildNativeRebuildArgs({ platform: { nodeName: 'linux' } })).toThrow(/arch/) - }) -}) diff --git a/config/scripts/desktop-distribution/runtime-verification/electron-builder-runtime-resources.test.mjs b/config/scripts/desktop-distribution/runtime-verification/electron-builder-runtime-resources.test.mjs index 29915eb54ff..7e717ef779d 100644 --- a/config/scripts/desktop-distribution/runtime-verification/electron-builder-runtime-resources.test.mjs +++ b/config/scripts/desktop-distribution/runtime-verification/electron-builder-runtime-resources.test.mjs @@ -6,7 +6,6 @@ import { describe, expect, it } from 'vitest' import { removeTree } from '../../../../src/shared/windows-transient-lock-removal.ts' const require = createRequire(import.meta.url) -const electronBuilderConfig = require('../../../electron-builder.config.cjs') const { prunePackagedNodePty, prunePackagedParcelWatcher, @@ -48,24 +47,6 @@ describe('packaged runtime resources', () => { } }) - it('still fails when a required packaged main entry is missing entirely', async () => { - const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-runtime-missing-entry-')) - try { - await writeFile(join(resourcesDir, 'app.asar'), '', 'utf8') - - const asar = { - listPackage: () => ['/out/main/index.js'], - extractFile: () => Buffer.from('', 'utf8') - } - - expect(() => verifyPackagedMainRuntimeDeps(resourcesDir, asar)).toThrow( - /managed-agent-hook-controls\.js was not found/ - ) - } finally { - await removeTree(resourcesDir) - } - }) - it('verifies bare imports that rolldown hoisted into a shared main chunk', async () => { const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-runtime-chunk-imports-')) try { @@ -99,63 +80,34 @@ describe('packaged runtime resources', () => { } }) - it('reads a spread require, whose leading dots are not member access', async () => { - const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-runtime-spread-require-')) + it('copies the Windows node-pty ConPTY runtime beside the rebuilt addon', async () => { + const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-node-pty-conpty-arm64-')) try { - await writeFile(join(resourcesDir, 'app.asar'), '', 'utf8') - - const sources = new Map([ - ['out/main/index.js', 'const all=[...require("jsonc-parser")]'], - ['out/main/agent-hooks/installation/managed-agent-hook-controls.js', ''] - ]) - const asar = { - listPackage: () => [...sources.keys()].map((entry) => `/${entry}`), - extractFile: (_asarPath, internalPath) => Buffer.from(sources.get(internalPath), 'utf8') + const nodePtyDir = join(resourcesDir, 'node_modules', 'node-pty') + const releaseDir = join(nodePtyDir, 'build', 'Release') + const conptyRoot = join(nodePtyDir, 'third_party', 'conpty', '0.1.0') + await mkdir(releaseDir, { recursive: true }) + await writeFile(join(releaseDir, 'conpty.node'), 'native addon placeholder', 'utf8') + for (const sourceArch of ['x64', 'arm64']) { + const sourceDir = join(conptyRoot, `win10-${sourceArch}`) + await mkdir(sourceDir, { recursive: true }) + await writeFile(join(sourceDir, 'conpty.dll'), `dll payload ${sourceArch}`, 'utf8') + await writeFile(join(sourceDir, 'OpenConsole.exe'), `console payload ${sourceArch}`, 'utf8') } - expect(() => verifyPackagedMainRuntimeDeps(resourcesDir, asar)).toThrow(/jsonc-parser/) + prunePackagedNodePty(resourcesDir, 'win32', 3) + + await expect(readFile(join(releaseDir, 'conpty', 'conpty.dll'), 'utf8')).resolves.toBe( + 'dll payload arm64' + ) + await expect(readFile(join(releaseDir, 'conpty', 'OpenConsole.exe'), 'utf8')).resolves.toBe( + 'console payload arm64' + ) } finally { await removeTree(resourcesDir) } }) - it('copies the Windows node-pty ConPTY runtime beside the rebuilt addon', async () => { - for (const [arch, electronArch] of [ - ['x64', 1], - ['arm64', 3] - ]) { - const resourcesDir = await mkdtemp(join(tmpdir(), `orca-node-pty-conpty-${arch}-`)) - try { - const nodePtyDir = join(resourcesDir, 'node_modules', 'node-pty') - const releaseDir = join(nodePtyDir, 'build', 'Release') - const conptyRoot = join(nodePtyDir, 'third_party', 'conpty', '0.1.0') - await mkdir(releaseDir, { recursive: true }) - await writeFile(join(releaseDir, 'conpty.node'), 'native addon placeholder', 'utf8') - for (const sourceArch of ['x64', 'arm64']) { - const sourceDir = join(conptyRoot, `win10-${sourceArch}`) - await mkdir(sourceDir, { recursive: true }) - await writeFile(join(sourceDir, 'conpty.dll'), `dll payload ${sourceArch}`, 'utf8') - await writeFile( - join(sourceDir, 'OpenConsole.exe'), - `console payload ${sourceArch}`, - 'utf8' - ) - } - - prunePackagedNodePty(resourcesDir, 'win32', electronArch) - - await expect(readFile(join(releaseDir, 'conpty', 'conpty.dll'), 'utf8')).resolves.toBe( - `dll payload ${arch}` - ) - await expect(readFile(join(releaseDir, 'conpty', 'OpenConsole.exe'), 'utf8')).resolves.toBe( - `console payload ${arch}` - ) - } finally { - await removeTree(resourcesDir) - } - } - }) - it('leaves unrelated @parcel/* runtime deps untouched when pruning the watcher', async () => { const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-parcel-watcher-prune-unrelated-')) try { @@ -177,18 +129,4 @@ describe('packaged runtime resources', () => { await removeTree(resourcesDir) } }) - - it('fails when the packaged resources directory is missing', async () => { - const root = await mkdtemp(join(tmpdir(), 'orca-electron-builder-config-')) - try { - await expect( - electronBuilderConfig.afterPack({ - appOutDir: root, - electronPlatformName: 'win32' - }) - ).rejects.toThrow(/Missing packaged resources directory/) - } finally { - await removeTree(root) - } - }) }) diff --git a/config/scripts/desktop-distribution/runtime-verification/packaged-node-pty-prebuild-prune.test.mjs b/config/scripts/desktop-distribution/runtime-verification/packaged-node-pty-prebuild-prune.test.mjs index af4d5e7e2fc..6c1a2eb7a8d 100644 --- a/config/scripts/desktop-distribution/runtime-verification/packaged-node-pty-prebuild-prune.test.mjs +++ b/config/scripts/desktop-distribution/runtime-verification/packaged-node-pty-prebuild-prune.test.mjs @@ -6,13 +6,9 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' const require = createRequire(import.meta.url) const { prunePackagedNodePty } = require('../../../packaged-runtime-node-modules.cjs') -const { PE_MACHINE } = require('../../native-build/windows/windows-pe-machine.cjs') -// Loader swallows build/Release/Debug failures and falls back to prebuilds; upstream conpty lacks job ownership, so ABI/AV failures must not silently downgrade it. -// Preserve conpty_console_list and winpty siblings: the patch removes their build targets, leaving only upstream copies. describe('prunePackagedNodePty: the Windows conpty fallback', () => { let resources - const HOST_ARCH = process.arch const nodePty = () => join(resources, 'node_modules', 'node-pty') const write = (relative, contents = 'x') => { @@ -21,29 +17,22 @@ describe('prunePackagedNodePty: the Windows conpty fallback', () => { writeFileSync(target, contents) } - /** Minimal PE far enough to carry a `Machine`, which is what the prune now reads. */ const peAddon = (arch) => { const buffer = Buffer.alloc(0x50) buffer.write('MZ') buffer.writeUInt32LE(0x40, 0x3c) buffer.write('PE\0\0', 0x40) - buffer.writeUInt16LE(PE_MACHINE[arch], 0x44) + buffer.writeUInt16LE({ x64: 0x8664, arm64: 0xaa64 }[arch], 0x44) return buffer } - const prebuilt = (name, arch = HOST_ARCH) => join(nodePty(), 'prebuilds', `win32-${arch}`, name) + const prebuilt = (name) => join(nodePty(), 'prebuilds', 'win32-arm64', name) - /** What a real packaged win32 prebuilds/ directory holds. */ - const SIBLINGS = ['conpty_console_list.node', 'pty.node', 'winpty.dll', 'winpty-agent.exe'] - - /** `buildArch` is what build/Release actually holds, which is not always the slice's arch. */ - const seedWindowsTree = (arch = HOST_ARCH, buildArch = arch) => { + const seedWindowsTree = (buildArch) => { write(join('build', 'Release', 'conpty.node'), peAddon(buildArch)) - write(join('third_party', 'conpty', 'v1', `win10-${arch}`, 'conpty.dll')) - write(join('third_party', 'conpty', 'v1', `win10-${arch}`, 'OpenConsole.exe')) - write(join('prebuilds', `win32-${arch}`, 'conpty.node')) - for (const sibling of SIBLINGS) { - write(join('prebuilds', `win32-${arch}`, sibling)) - } + write(join('third_party', 'conpty', 'v1', 'win10-arm64', 'conpty.dll')) + write(join('third_party', 'conpty', 'v1', 'win10-arm64', 'OpenConsole.exe')) + write(join('prebuilds', 'win32-arm64', 'conpty.node')) + write(join('prebuilds', 'win32-arm64', 'pty.node')) } beforeEach(() => { @@ -53,73 +42,20 @@ describe('prunePackagedNodePty: the Windows conpty fallback', () => { rmSync(resources, { recursive: true, force: true }) }) - it('removes the stale conpty fallback when the source build is present', () => { - seedWindowsTree() - - prunePackagedNodePty(resources, 'win32', HOST_ARCH) - - expect(existsSync(join(nodePty(), 'build', 'Release', 'conpty.node'))).toBe(true) - expect(existsSync(prebuilt('conpty.node'))).toBe(false) - }) - - it.each(SIBLINGS)('keeps %s, which exists nowhere else in a packaged build', (sibling) => { - // Keep console-list/winpty prebuilds: patch removes their targets; deleting them breaks membership and PTY spawn below Windows 18309. - seedWindowsTree() - - prunePackagedNodePty(resources, 'win32', HOST_ARCH) - - expect(existsSync(prebuilt(sibling))).toBe(true) - }) - - it('keeps the fallback when there is no source build to prefer', () => { - write(join('prebuilds', `win32-${HOST_ARCH}`, 'conpty.node')) - write(join('third_party', 'conpty', 'v1', `win10-${HOST_ARCH}`, 'conpty.dll')) - write(join('third_party', 'conpty', 'v1', `win10-${HOST_ARCH}`, 'OpenConsole.exe')) - - prunePackagedNodePty(resources, 'win32', HOST_ARCH) - - expect(existsSync(prebuilt('conpty.node'))).toBe(true) - }) - it('keeps the fallback when build/Release holds a different arch than the slice', () => { - // Cross-host build/Release can be unloadable on target; pruning then deletes its only usable binary. - const target = HOST_ARCH === 'arm64' ? 'x64' : 'arm64' - seedWindowsTree(target, HOST_ARCH) + seedWindowsTree('x64') - prunePackagedNodePty(resources, 'win32', target) - - expect(existsSync(prebuilt('conpty.node', target))).toBe(true) - }) - - it('removes the fallback on a cross-arch package whose build/Release IS the slice arch', () => { - // Cross-arch electron-builder can rebuild correctly; judge binary architecture, not host, or arm64 slice keeps unsafe upstream fallback. - const target = HOST_ARCH === 'arm64' ? 'x64' : 'arm64' - seedWindowsTree(target, target) - - prunePackagedNodePty(resources, 'win32', target) - - expect(existsSync(prebuilt('conpty.node', target))).toBe(false) - expect(existsSync(prebuilt('pty.node', target))).toBe(true) - }) - - it('keeps the fallback when build/Release is not a readable PE at all', () => { - seedWindowsTree() - write(join('build', 'Release', 'conpty.node'), 'not-a-pe') - - prunePackagedNodePty(resources, 'win32', HOST_ARCH) + prunePackagedNodePty(resources, 'win32', 'arm64') expect(existsSync(prebuilt('conpty.node'))).toBe(true) }) - it.each([ - ['darwin', 'arm64'], - ['linux', 'x64'] - ])('leaves %s prebuilds alone', (platform, arch) => { - write(join('build', 'Release', 'conpty.node'), peAddon('x64')) - write(join('prebuilds', `${platform}-${arch}`, 'pty.node')) + it('removes the matching-architecture fallback while keeping other prebuilds', () => { + seedWindowsTree('arm64') - prunePackagedNodePty(resources, platform, arch) + prunePackagedNodePty(resources, 'win32', 'arm64') - expect(existsSync(join(nodePty(), 'prebuilds', `${platform}-${arch}`, 'pty.node'))).toBe(true) + expect(existsSync(prebuilt('conpty.node'))).toBe(false) + expect(existsSync(prebuilt('pty.node'))).toBe(true) }) }) diff --git a/config/scripts/desktop-distribution/runtime-verification/packaged-process-host.test.mjs b/config/scripts/desktop-distribution/runtime-verification/packaged-process-host.test.mjs index b1a24c4ae09..f473b1b31da 100644 --- a/config/scripts/desktop-distribution/runtime-verification/packaged-process-host.test.mjs +++ b/config/scripts/desktop-distribution/runtime-verification/packaged-process-host.test.mjs @@ -42,24 +42,13 @@ describe('process-host output packaging guard', () => { } }) - it.each([ - ['a public export target', 'feature.js', /missing: \.\/dist\/feature\.js/], - ['an internal module', 'internal.js', /missing: \.\/dist\/internal\.js/] - ])('fails when %s was never emitted', async (_label, file, message) => { + it('fails when an internal module was never emitted', async () => { const packageDir = await builtPackage() try { - await rm(join(packageDir, 'dist', file)) - expect(() => assertProcessHostOutputBuilt(packageDir)).toThrow(message) - } finally { - await removeTree(packageDir) - } - }) - - it('fails when dist is absent', async () => { - const packageDir = await builtPackage() - try { - await rm(join(packageDir, 'dist'), { recursive: true }) - expect(() => assertProcessHostOutputBuilt(packageDir)).toThrow(/pnpm build:packages/) + await rm(join(packageDir, 'dist', 'internal.js')) + expect(() => assertProcessHostOutputBuilt(packageDir)).toThrow( + /missing: \.\/dist\/internal\.js/ + ) } finally { await removeTree(packageDir) } diff --git a/config/scripts/desktop-distribution/runtime-verification/packaged-source-map-prune.test.mjs b/config/scripts/desktop-distribution/runtime-verification/packaged-source-map-prune.test.mjs deleted file mode 100644 index 52915cd69f3..00000000000 --- a/config/scripts/desktop-distribution/runtime-verification/packaged-source-map-prune.test.mjs +++ /dev/null @@ -1,87 +0,0 @@ -import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises' -import { createRequire } from 'node:module' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { describe, expect, it } from 'vitest' - -const require = createRequire(import.meta.url) -const { - prunePackagedRuntimeTypeAndSourceMapArtifacts -} = require('../../../packaged-runtime-node-modules.cjs') - -const LINEAR_SDK_PACKAGE_JSON = - '{"name":"@linear/sdk","type":"module","main":"./dist/index.cjs","exports":{".":{"require":"./dist/index.cjs","import":"./dist/index.mjs"}}}' - -async function createPackagedNodeModulesFixture(resourcesDir) { - const packageDir = join(resourcesDir, 'node_modules', '@linear', 'sdk') - const distDir = join(packageDir, 'dist') - const webhooksDir = join(packageDir, 'webhooks') - const updaterDir = join(resourcesDir, 'node_modules', 'electron-updater', 'out') - const jsYamlDir = join(resourcesDir, 'node_modules', 'js-yaml', 'dist') - const nodePtyDir = join(resourcesDir, 'node_modules', 'node-pty', 'lib') - await mkdir(distDir, { recursive: true }) - await mkdir(webhooksDir, { recursive: true }) - await mkdir(updaterDir, { recursive: true }) - await mkdir(jsYamlDir, { recursive: true }) - await mkdir(nodePtyDir, { recursive: true }) - await writeFile(join(packageDir, 'package.json'), LINEAR_SDK_PACKAGE_JSON, 'utf8') - await writeFile(join(packageDir, 'README.md'), 'SDK documentation', 'utf8') - await writeFile(join(packageDir, 'metadata.json.map'), '{"keep":true}', 'utf8') - await writeFile( - join(distDir, 'index.cjs'), - "module.exports = require('./runtime-helper.cjs')", - 'utf8' - ) - await writeFile( - join(distDir, 'runtime-helper.cjs'), - 'exports.LinearClient = class LinearClient {}', - 'utf8' - ) - await writeFile(join(distDir, 'index.mjs'), 'export {}', 'utf8') - await writeFile(join(distDir, 'index.cjs.map'), '{}', 'utf8') - await writeFile(join(distDir, 'index.mjs.map'), '{}', 'utf8') - await writeFile(join(webhooksDir, 'index.cjs'), 'module.exports = {}', 'utf8') - await writeFile(join(webhooksDir, 'index.cjs.map'), '{}', 'utf8') - await writeFile(join(updaterDir, 'main.js'), 'module.exports = {}', 'utf8') - await writeFile(join(updaterDir, 'main.js.map'), '{}', 'utf8') - await writeFile(join(updaterDir, 'main.d.ts'), 'export {}', 'utf8') - await writeFile(join(updaterDir, 'main.d.ts.map'), '{}', 'utf8') - await writeFile(join(jsYamlDir, 'js-yaml.min.js'), 'globalThis.jsyaml = {}', 'utf8') - await writeFile(join(jsYamlDir, 'js-yaml.min.js.map'), '{}', 'utf8') - await writeFile(join(nodePtyDir, 'index.js'), 'module.exports = {}', 'utf8') - await writeFile(join(nodePtyDir, 'index.js.map'), '{}', 'utf8') - return { packageDir, distDir, webhooksDir, updaterDir, jsYamlDir, nodePtyDir } -} - -describe('packaged runtime type-declaration and source-map pruning', () => { - it('removes @linear/sdk source maps while preserving runtime files and non-JS maps', async () => { - const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-source-map-prune-')) - try { - const { packageDir, distDir, webhooksDir } = - await createPackagedNodeModulesFixture(resourcesDir) - - prunePackagedRuntimeTypeAndSourceMapArtifacts(resourcesDir) - - await expect(readdir(distDir).then((entries) => entries.sort())).resolves.toEqual([ - 'index.cjs', - 'index.mjs', - 'runtime-helper.cjs' - ]) - await expect(readdir(webhooksDir)).resolves.toEqual(['index.cjs']) - await expect(readFile(join(packageDir, 'package.json'), 'utf8')).resolves.toBe( - LINEAR_SDK_PACKAGE_JSON - ) - await expect(readFile(join(packageDir, 'README.md'), 'utf8')).resolves.toBe( - 'SDK documentation' - ) - // Why: the predicate is filename-based, so a non-JS `.map` payload must survive. - await expect(readFile(join(packageDir, 'metadata.json.map'), 'utf8')).resolves.toBe( - '{"keep":true}' - ) - const sdk = createRequire(join(resourcesDir, 'consumer.cjs'))('@linear/sdk') - expect(typeof sdk.LinearClient).toBe('function') - } finally { - await rm(resourcesDir, { recursive: true, force: true }) - } - }) -}) diff --git a/config/scripts/desktop-distribution/runtime-verification/packaged-windows-only-runtime-files.test.mjs b/config/scripts/desktop-distribution/runtime-verification/packaged-windows-only-runtime-files.test.mjs index 92302677ef2..40201d529e0 100644 --- a/config/scripts/desktop-distribution/runtime-verification/packaged-windows-only-runtime-files.test.mjs +++ b/config/scripts/desktop-distribution/runtime-verification/packaged-windows-only-runtime-files.test.mjs @@ -34,14 +34,9 @@ describe('Windows-only runtime files', () => { const nested = join(resourcesDir, 'node_modules', 'some-dep', 'bin') await mkdir(nested, { recursive: true }) await writeFile(join(nested, 'helper'), peImage()) - // An MZ prefix alone is not a PE image; data files must not trip the check. - await writeFile(join(nested, 'fake.exe'), Buffer.from('MZ not really')) - - for (const platform of ['darwin', 'linux']) { - expect(() => - assertNoWindowsExecutablesInRuntimeNodeModules(resourcesDir, platform) - ).toThrow(/: node_modules\/some-dep\/bin\/helper\. /) - } + expect(() => assertNoWindowsExecutablesInRuntimeNodeModules(resourcesDir, 'linux')).toThrow( + /: node_modules\/some-dep\/bin\/helper\. / + ) expect(() => assertNoWindowsExecutablesInRuntimeNodeModules(resourcesDir, 'win32') ).not.toThrow() diff --git a/config/scripts/desktop-distribution/runtime-verification/process-host-build-dist.test.mjs b/config/scripts/desktop-distribution/runtime-verification/process-host-build-dist.test.mjs index 58725d106cb..957ceb74f26 100644 --- a/config/scripts/desktop-distribution/runtime-verification/process-host-build-dist.test.mjs +++ b/config/scripts/desktop-distribution/runtime-verification/process-host-build-dist.test.mjs @@ -163,23 +163,6 @@ beforeAll(async () => { afterAll(() => releaseSeed?.()) describe('process-host dist build', () => { - it('rewrites only changed outputs and keeps source maps pointing at src', async () => { - const packageDir = fixture(undefined, seededDist) - const before = distSnapshot(packageDir) - - writeFileSync(join(packageDir, 'src', 'internal.ts'), 'export const value = 2\n') - const changed = await build(packageDir) - - expect(changed.stdout).toContain('2 written, 0 removed, 6 unchanged') - const after = distSnapshot(packageDir) - for (const file of ['entry.js', 'entry.d.ts', 'internal.js.map']) { - expect(after[file]).toEqual(before[file]) - } - expect(after['internal.js']).not.toEqual(before['internal.js']) - const map = JSON.parse(readFileSync(join(packageDir, 'dist', 'internal.js.map'), 'utf8')) - expect(map.sources).toEqual(['../src/internal.ts']) - }) - it('preserves the previous dist when compilation fails', async () => { const packageDir = fixture(undefined, seededDist) const before = distSnapshot(packageDir) @@ -303,18 +286,6 @@ await buildPackageDist(process.argv[2]) } ) - it('reclaims staging directories abandoned by a killed build', async () => { - const packageDir = fixture() - mkdirSync(join(packageDir, '.dist-staging-abandoned')) - writeFileSync(join(packageDir, '.dist-staging-abandoned', 'partial.js'), '') - - await build(packageDir) - - expect(existsSync(join(packageDir, 'dist', 'entry.js'))).toBe(true) - expect(existsSync(join(packageDir, 'dist', 'partial.js'))).toBe(false) - expect(leftovers(packageDir)).toEqual([]) - }) - it('builds its own package when reached through a symlinked directory', async () => { const packageDir = fixture() mkdirSync(join(packageDir, 'scripts')) diff --git a/config/scripts/desktop-distribution/runtime-verification/process-host-dev-rebuild.test.ts b/config/scripts/desktop-distribution/runtime-verification/process-host-dev-rebuild.test.ts index 9bd234de549..6b25a522257 100644 --- a/config/scripts/desktop-distribution/runtime-verification/process-host-dev-rebuild.test.ts +++ b/config/scripts/desktop-distribution/runtime-verification/process-host-dev-rebuild.test.ts @@ -1,20 +1,12 @@ -import { - existsSync, - mkdtempSync, - mkdirSync, - readFileSync, - rmSync, - symlinkSync, - writeFileSync -} from 'node:fs' +import { mkdtempSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' -import { basename, dirname, join, resolve } from 'node:path' +import { dirname, join, resolve } from 'node:path' import { createRequire } from 'node:module' import { pathToFileURL } from 'node:url' import { runProcessSync } from '@orca/process-host' import { watch } from 'rolldown' -import type { Plugin, RolldownWatcher } from 'rolldown' -import { afterEach, describe, expect, it, vi } from 'vitest' +import type { RolldownWatcher } from 'rolldown' +import { afterEach, describe, expect, it } from 'vitest' import { createProcessHostDevRebuildPlugin } from '../../../build-plugins/process-host-dev-rebuild' const realPackageDir = resolve('src/packages/process-host') @@ -38,7 +30,7 @@ function fixture(): { root: string; packageDir: string; main: string } { // Runs the real package build against this fixture package. writeFileSync( join(packageDir, 'scripts', 'build-dist.mjs'), - `import { appendFileSync } from 'node:fs'\nappendFileSync(${JSON.stringify(join(packageDir, 'builds.txt'))}, 'build\\n')\nconst { buildPackageDist } = await import(${JSON.stringify(pathToFileURL(resolve(realPackageDir, 'scripts', 'build-dist.mjs')).href)})\nawait buildPackageDist(${JSON.stringify(packageDir)})\n` + `const { buildPackageDist } = await import(${JSON.stringify(pathToFileURL(resolve(realPackageDir, 'scripts', 'build-dist.mjs')).href)})\nawait buildPackageDist(${JSON.stringify(packageDir)})\n` ) mkdirSync(join(root, 'node_modules', '@orca'), { recursive: true }) const packageRequire = createRequire(join(realPackageDir, 'package.json')) @@ -162,96 +154,10 @@ describe('External process-host development rebuilds', () => { await expect.poll(() => versions.at(-1), { timeout: 15_000 }).toBe(7) }) - it('emits private source edits and new imports before the Electron reload hook', async () => { - const { root, packageDir, main } = fixture() - const loadedVersions: number[] = [] - const reloadHook: Plugin = { - name: 'fake-electron-reload', - closeBundle() { - loadedVersions.push(readEmittedVersion(root)) - } - } - const watcher = watch({ - input: main, - external: ['@orca/process-host'], - plugins: [createProcessHostDevRebuildPlugin(root), reloadHook], - output: { file: join(root, 'out', 'main.cjs'), format: 'cjs' } - }) - watchers.push(watcher) - closeSuccessfulBundles(watcher) - await nextEvent(watcher, 'END') - expect(loadedVersions).toEqual([1]) - expect(readFileSync(join(root, 'out', 'main.cjs'), 'utf8')).toContain('@orca/process-host') - - writeFileSync(join(packageDir, 'src', 'internal.ts'), 'export const version = 2\n') - await expect.poll(() => loadedVersions.at(-1), { timeout: 15_000 }).toBe(2) - - writeFileSync(join(packageDir, 'src', 'new-feature.ts'), 'export const added = 3\n') - writeFileSync( - join(packageDir, 'src', 'internal.ts'), - "import { added } from './new-feature'\nexport const version = added\n" - ) - await expect.poll(() => loadedVersions.at(-1), { timeout: 15_000 }).toBe(3) - await expect - .poll(() => existsSync(join(packageDir, 'dist', 'new-feature.js')), { timeout: 15_000 }) - .toBe(true) - expect(readFileSync(join(packageDir, 'dist', 'new-feature.js'), 'utf8')).toContain('added = 3') - }) - - it('reports a failed compilation, preserves prior output, and recovers after a fix', async () => { - const { root, packageDir, main } = fixture() - const loadedVersions: number[] = [] - const watcher = watch({ - input: main, - external: ['@orca/process-host'], - plugins: [ - createProcessHostDevRebuildPlugin(root), - { - name: 'fake-electron-reload', - closeBundle() { - loadedVersions.push(readEmittedVersion(root)) - } - } - ], - output: { file: join(root, 'out', 'main.cjs'), format: 'cjs' } - }) - watchers.push(watcher) - closeSuccessfulBundles(watcher) - await nextEvent(watcher, 'END') - - const failed = nextEvent(watcher, 'ERROR') - writeFileSync(join(packageDir, 'src', 'internal.ts'), "export const version: number = 'bad'\n") - await failed - expect(loadedVersions.at(-1)).toBe(1) - expect(readEmittedVersion(root)).toBe(1) - - writeFileSync(join(packageDir, 'src', 'internal.ts'), 'export const version = 4\n') - await expect.poll(() => loadedVersions.at(-1), { timeout: 15_000 }).toBe(4) - }) - it('recovers when a missing import is created in a new source directory', async () => { const { root, packageDir, main } = fixture() const loadedVersions: number[] = [] - const watchSignals = new Set() const rebuildPlugin = createProcessHostDevRebuildPlugin(root) - const buildStart = rebuildPlugin.buildStart - if (typeof buildStart !== 'function') { - throw new Error('The process-host rebuild plugin must have a buildStart hook') - } - rebuildPlugin.buildStart = async function (...args) { - const addWatchFile = this.addWatchFile.bind(this) - const registration = vi.spyOn(this, 'addWatchFile').mockImplementation((file) => { - addWatchFile(file) - if (basename(file) === 'source-directory-change') { - watchSignals.add(file) - } - }) - try { - return await buildStart.apply(this, args) - } finally { - registration.mockRestore() - } - } const watcher = watch({ input: main, external: ['@orca/process-host'], @@ -285,12 +191,6 @@ describe('External process-host development rebuilds', () => { 'export const version = 5\n' ) await expect.poll(() => loadedVersions.at(-1), { timeout: 15_000 }).toBe(5) - expect(existsSync(join(packageDir, 'dist', 'features', 'new-feature.js'))).toBe(true) - expect(watchSignals.size).toBe(1) - for (const signal of watchSignals) { - expect(existsSync(signal)).toBe(true) - } - const failedAgain = nextEvent(watcher, 'ERROR') writeFileSync( join(packageDir, 'src', 'internal.ts'), @@ -298,11 +198,5 @@ describe('External process-host development rebuilds', () => { ) await failedAgain expect(readEmittedVersion(root)).toBe(5) - - await watcher.close() - watchers.splice(watchers.indexOf(watcher), 1) - for (const signal of watchSignals) { - expect(existsSync(dirname(signal))).toBe(false) - } }) }) diff --git a/config/scripts/desktop-distribution/runtime-verification/verify-packaged-daemon-entry.test.mjs b/config/scripts/desktop-distribution/runtime-verification/verify-packaged-daemon-entry.test.mjs deleted file mode 100644 index 1266c14b392..00000000000 --- a/config/scripts/desktop-distribution/runtime-verification/verify-packaged-daemon-entry.test.mjs +++ /dev/null @@ -1,110 +0,0 @@ -import { mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' -import { createRequire } from 'node:module' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -const require = createRequire(import.meta.url) -const { runTerminalHostRoundTrip } = require('./verify-packaged-daemon-entry.cjs') - -describe.skipIf(process.platform === 'win32')('terminal host round trip', () => { - let root - - beforeEach(() => { - root = mkdtempSync(join(tmpdir(), 'vpd-')) - }) - - afterEach(() => { - vi.unstubAllEnvs() - rmSync(root, { recursive: true, force: true }) - }) - - // Listens, publishes its token, records its pid, then treats each connection with `onConnection`. - function fakeDaemon(onConnection) { - const entry = join(root, 'daemon.cjs') - writeFileSync( - entry, - [ - "const net = require('node:net'), fs = require('node:fs')", - 'const arg = (name) => process.argv[process.argv.indexOf(name) + 1]', - `fs.writeFileSync(${JSON.stringify(join(root, 'pid'))}, String(process.pid))`, - `fs.writeFileSync(${JSON.stringify(join(root, 'shell'))}, process.env.SHELL)`, - `net.createServer(${onConnection}).listen(arg('--socket'), () => fs.writeFileSync(arg('--token'), 'tok'))` - ].join('\n') - ) - return entry - } - - async function roundTrip(entry, timeoutMs) { - const startedAt = Date.now() - const error = await runTerminalHostRoundTrip({ - command: [process.execPath], - entry, - deadline: startedAt + timeoutMs, - stderr: [], - scratchPrefix: join(root, 'oth-') - }).then( - () => null, - (rejection) => rejection - ) - const pid = Number(readFileSync(join(root, 'pid'), 'utf8')) - let daemonAlive = true - try { - process.kill(pid, 0) - } catch { - daemonAlive = false - } - return { - error, - daemonAlive, - scratchDirs: readdirSync(root).filter((name) => name.startsWith('oth-')) - } - } - - it('accepts a valid peer round trip and cleans up after observing the boot request', async () => { - const messagesPath = join(root, 'messages.jsonl') - const entry = fakeDaemon(`(socket) => { - let buffer = '' - socket.on('data', (chunk) => { - buffer += chunk - let newline - while ((newline = buffer.indexOf('\\n')) !== -1) { - const message = JSON.parse(buffer.slice(0, newline)) - buffer = buffer.slice(newline + 1) - fs.appendFileSync(${JSON.stringify(messagesPath)}, JSON.stringify(message) + '\\n') - if (message.type === 'hello') { - if (message.role === 'stream') globalThis.stream = socket - socket.write(JSON.stringify({ ok: true }) + '\\n') - } else if (message.type === 'createOrAttach') { - globalThis.stream.write(JSON.stringify({ payload: { data: 'OK 42' } }) + '\\n') - } - } - }) - }`) - vi.stubEnv('SHELL', '/usr/bin/fish') - const result = await roundTrip(entry, 1_500) - expect(result.error).toBe(null) - expect(readFileSync(join(root, 'shell'), 'utf8')).toBe('/bin/sh') - expect(result.daemonAlive).toBe(false) - expect(result.scratchDirs).toEqual([]) - const messages = readFileSync(messagesPath, 'utf8').trim().split('\n').map(JSON.parse) - expect(messages.slice(0, 2).map(({ type, role, token }) => ({ type, role, token }))).toEqual([ - { type: 'hello', role: 'control', token: 'tok' }, - { type: 'hello', role: 'stream', token: 'tok' } - ]) - expect(messages[0].clientId).toBe(messages[1].clientId) - expect(messages[0].clientId).toMatch(/^[0-9a-f-]{36}$/) - expect(messages).toHaveLength(3) - expect(messages[2]).toMatchObject({ - id: 'boot-1', - type: 'createOrAttach', - payload: { - sessionId: 'terminal-host-boot', - cols: 80, - rows: 24, - command: 'echo "OK $((6*7))" && exit' - } - }) - expect(messages[2].payload.cwd.startsWith(join(root, 'oth-'))).toBe(true) - }) -}) diff --git a/config/scripts/desktop-distribution/runtime-verification/verify-packaged-plugin-resources.test.mjs b/config/scripts/desktop-distribution/runtime-verification/verify-packaged-plugin-resources.test.mjs deleted file mode 100644 index ebfd5e96f78..00000000000 --- a/config/scripts/desktop-distribution/runtime-verification/verify-packaged-plugin-resources.test.mjs +++ /dev/null @@ -1,50 +0,0 @@ -import { cp, mkdtemp, rm, writeFile } from 'node:fs/promises' -import { createRequire } from 'node:module' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { describe, expect, it } from 'vitest' - -const require = createRequire(import.meta.url) -const { - hashPackagedPluginTree, - verifyPackagedPluginResources -} = require('./verify-packaged-plugin-resources.cjs') - -const FIXTURE_KEY = 'stablyai.orca-multipass-recipes' - -/** Copies the launch tree and indexes one shipped pack so byte checks have something to verify. */ -async function copyIndexedLaunchTree(resourcesDir) { - const launchRoot = join(resourcesDir, 'plugins', 'launch') - await cp(join(process.cwd(), 'resources', 'plugins', 'launch'), launchRoot, { recursive: true }) - await writeFile( - join(launchRoot, 'bundled-plugins.json'), - JSON.stringify({ - version: 1, - plugins: [ - { - pluginKey: FIXTURE_KEY, - path: FIXTURE_KEY, - contentHash: hashPackagedPluginTree(join(launchRoot, FIXTURE_KEY)) - } - ] - }) - ) - return launchRoot -} - -describe('verify packaged plugin resources', () => { - it('rejects mutated bytes in the packaged output', async () => { - const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-packaged-plugins-')) - try { - const launchRoot = await copyIndexedLaunchTree(resourcesDir) - expect(() => verifyPackagedPluginResources(resourcesDir)).not.toThrow() - await writeFile(join(launchRoot, FIXTURE_KEY, 'extra.json'), '{"mutated":true}\n') - - expect(() => verifyPackagedPluginResources(resourcesDir)).toThrow( - `packaged bytes do not match ${FIXTURE_KEY}` - ) - } finally { - await rm(resourcesDir, { recursive: true, force: true }) - } - }) -}) diff --git a/config/scripts/desktop-distribution/windows/trim-windows-icon-source.test.mjs b/config/scripts/desktop-distribution/windows/trim-windows-icon-source.test.mjs deleted file mode 100644 index 4930f9f405f..00000000000 --- a/config/scripts/desktop-distribution/windows/trim-windows-icon-source.test.mjs +++ /dev/null @@ -1,52 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { - cropImage, - findOpaqueBounds, - resizeImage, - squareWithMargin -} from './trim-windows-icon-source.mjs' - -function glyphFillFraction(image) { - const bounds = findOpaqueBounds(image) - if (!bounds) { - return 0 - } - return Math.max(bounds.width, bounds.height) / Math.max(image.width, image.height) -} - -describe('trim pipeline', () => { - function solidSquareWithInset(canvas, glyph) { - const data = Buffer.alloc(canvas * canvas * 4) - const offset = Math.floor((canvas - glyph) / 2) - for (let y = 0; y < glyph; y++) { - for (let x = 0; x < glyph; x++) { - const idx = ((offset + y) * canvas + (offset + x)) * 4 - data[idx] = 200 - data[idx + 1] = 100 - data[idx + 2] = 50 - data[idx + 3] = 255 - } - } - return { width: canvas, height: canvas, data } - } - - it('finds the opaque glyph bounds and ignores transparent inset', () => { - const image = solidSquareWithInset(100, 60) - const bounds = findOpaqueBounds(image) - expect(bounds).toEqual({ minX: 20, minY: 20, maxX: 79, maxY: 79, width: 60, height: 60 }) - }) - - it('downscales without leaving the glyph undersized', () => { - const image = solidSquareWithInset(256, 200) - const filled = squareWithMargin(cropImage(image, findOpaqueBounds(image))) - const small = resizeImage(filled, 32, 32) - expect(small.width).toBe(32) - const edge = { - width: 2, - height: 1, - data: Buffer.from([200, 100, 50, 255, 0, 0, 0, 0]) - } - expect(resizeImage(edge, 1, 1).data).toEqual(Buffer.from([200, 100, 50, 128])) - expect(glyphFillFraction(small)).toBeGreaterThanOrEqual(0.9) - }) -}) diff --git a/config/scripts/desktop-distribution/windows/windows-uninstaller-signing.test.mjs b/config/scripts/desktop-distribution/windows/windows-uninstaller-signing.test.mjs index 0ede0e62ca9..622fe77648f 100644 --- a/config/scripts/desktop-distribution/windows/windows-uninstaller-signing.test.mjs +++ b/config/scripts/desktop-distribution/windows/windows-uninstaller-signing.test.mjs @@ -8,47 +8,11 @@ import { describe, expect, it } from 'vitest' const require = createRequire(import.meta.url) const { EMBEDDED_RECEIPT_SUFFIX, - relayNsisUninstaller, signWindowsUninstallerViaSignPath } = require('./windows-uninstaller-signing.cjs') const makeDir = () => mkdtempSync(join(tmpdir(), 'orca-uninstaller-signing-')) -describe('relayNsisUninstaller', () => { - const writeUninstaller = (dir, contents) => { - const filePath = join(dir, 'orca-windows-setup.__uninstaller.exe') - writeFileSync(filePath, contents) - return filePath - } - - // Receipt is only evidence for embedded compressed NSIS uninstaller; bundled 7za cannot extract it. - - // Import signed bytes even when rebuild regenerated unsigned intermediate. - it('prefers importing over exporting when both are configured', () => { - const dir = makeDir() - const filePath = writeUninstaller(dir, 'rebuild-unsigned') - const signedPath = join(dir, 'signed', 'orca-uninstaller.exe') - mkdirSync(join(dir, 'signed')) - writeFileSync(signedPath, 'signpath-signed') - - expect( - relayNsisUninstaller({ filePath, signedPath, exportPath: join(dir, 'out', 'x.exe') }) - ).toBe('imported') - expect(readFileSync(filePath, 'utf8')).toBe('signpath-signed') - }) - - // Missing/unwritable relay must preserve unsigned build without throwing. - it('leaves the unsigned uninstaller in place when no signed copy came back', () => { - const dir = makeDir() - const filePath = writeUninstaller(dir, 'unsigned-uninstaller') - - expect( - relayNsisUninstaller({ filePath, signedPath: join(dir, 'absent', 'orca-uninstaller.exe') }) - ).toBe('signed-missing') - expect(readFileSync(filePath, 'utf8')).toBe('unsigned-uninstaller') - }) -}) - // Exercise actual hook’s fail-open behavior: Windows build has no continue-on-error and throwing prevents any SignPath request. describe('signWindowsUninstallerViaSignPath', () => { const RELAY_VARS = ['ORCA_WIN_UNINSTALLER_EXPORT_PATH', 'ORCA_WIN_UNINSTALLER_SIGNED_PATH'] diff --git a/config/scripts/development/launch/orca-dev-bin.test.mjs b/config/scripts/development/launch/orca-dev-bin.test.mjs index d8785523c38..b01cfc497cd 100644 --- a/config/scripts/development/launch/orca-dev-bin.test.mjs +++ b/config/scripts/development/launch/orca-dev-bin.test.mjs @@ -1,27 +1,10 @@ import { execFileSync } from 'node:child_process' -import { - chmodSync, - copyFileSync, - mkdirSync, - mkdtempSync, - readFileSync, - rmSync, - writeFileSync -} from 'node:fs' +import { copyFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import path from 'node:path' import { describe, expect, it } from 'vitest' const projectDir = path.resolve(import.meta.dirname, '../../../..') -const wrapperPath = path.join( - projectDir, - 'config', - 'scripts', - 'development', - 'launch', - 'orca-dev.mjs' -) - describe('orca-dev package bin', () => { it('finds the built CLI from its installed location when invoked outside the repository', () => { const root = mkdtempSync(path.join(tmpdir(), 'orca-dev-installed-')) @@ -61,43 +44,4 @@ describe('orca-dev package bin', () => { rmSync(root, { recursive: true, force: true }) } }) - - it('runs the dev CLI through Node without requiring Bash', () => { - const root = mkdtempSync(path.join(tmpdir(), 'orca-dev-bin-')) - const cliEntry = path.join(root, 'cli-entry.cjs') - const outputPath = path.join(root, 'output.json') - writeFileSync( - cliEntry, - [ - 'const fs = require("node:fs");', - `fs.writeFileSync(${JSON.stringify(outputPath)}, JSON.stringify({`, - ' argv: process.argv.slice(2),', - ' userDataPath: process.env.ORCA_USER_DATA_PATH,', - ' devCliInvocation: process.env.ORCA_DEV_CLI_INVOCATION,', - ' appExecutable: process.env.ORCA_APP_EXECUTABLE', - '}));' - ].join('\n'), - 'utf8' - ) - if (process.platform !== 'win32') { - chmodSync(cliEntry, 0o755) - } - - execFileSync(process.execPath, [wrapperPath, '--help'], { - env: { - ...process.env, - ORCA_DEV_CLI_ENTRY_PATH: cliEntry, - ORCA_DEV_USER_DATA_PATH: path.join(root, 'user-data'), - ORCA_APP_EXECUTABLE: path.join(root, 'Electron') - }, - stdio: 'ignore' - }) - - expect(JSON.parse(readFileSync(outputPath, 'utf8'))).toEqual({ - argv: ['--help'], - userDataPath: path.join(root, 'user-data'), - devCliInvocation: '1', - appExecutable: path.join(root, 'Electron') - }) - }) }) diff --git a/config/scripts/development/launch/run-electron-vite-dev.test.ts b/config/scripts/development/launch/run-electron-vite-dev.test.ts index 119815d5f67..70515c8b528 100644 --- a/config/scripts/development/launch/run-electron-vite-dev.test.ts +++ b/config/scripts/development/launch/run-electron-vite-dev.test.ts @@ -1,4 +1,4 @@ -import { existsSync, mkdtempSync, readFileSync, readlinkSync, rmSync } from 'node:fs' +import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join, resolve } from 'node:path' import { spawn, type ChildProcess } from 'node:child_process' @@ -298,54 +298,4 @@ describe('run-electron-vite-dev', () => { // Two full prepares, each budgeted at PREPARE_TIMEOUT_MS. PREPARE_TIMEOUT_MS * 2 + 30_000 ) - - it.skipIf(process.platform !== 'darwin')( - 'preserves relative Electron framework symlinks in the copied mac dev app', - async () => { - const tempDir = mkdtempSync(join(tmpdir(), 'orca-dev-wrapper-')) - const pidFile = join(tempDir, 'grandchild.pid') - const envFile = join(tempDir, 'env.json') - const wrapperPath = resolve('config/scripts/development/launch/run-electron-vite-dev.mjs') - const fakeCliPath = resolve( - 'config/scripts/development/launch/fixtures/fake-electron-vite-dev-cli.mjs' - ) - - const { wrapper, readOutput } = spawnDevWrapper( - [wrapperPath, '--remote-debugging-port=9448'], - devWrapperTestEnv({ - ORCA_ELECTRON_VITE_CLI: fakeCliPath, - ORCA_SKIP_DEV_CLI_PREPARE: '1', - ORCA_SKIP_DEV_WEB_PREPARE: '1', - ORCA_DEV_WRAPPER_TEST_PID_FILE: pidFile, - ORCA_DEV_WRAPPER_TEST_ENV_FILE: envFile, - ORCA_DEV_BRANCH: 'feature/framework-symlinks', - ORCA_DEV_WORKTREE_NAME: 'symlink-ui' - }) - ) - - expect(wrapper.pid).toBeTypeOf('number') - processesToCleanUp.add(wrapper.pid!) - - await waitForEnvFile(envFile, readOutput) - - const trackedPids = trackPidFile(pidFile) - - const envSnapshot = JSON.parse(readFileSync(envFile, 'utf8')) as { - electronExecPath: string | null - } - expect(envSnapshot.electronExecPath).toBeTypeOf('string') - - const appPath = dirname(dirname(dirname(envSnapshot.electronExecPath!))) - const frameworkPath = join(appPath, 'Contents', 'Frameworks', 'Electron Framework.framework') - - expect(readlinkSync(join(frameworkPath, 'Resources'))).toBe('Versions/Current/Resources') - expect(readlinkSync(join(frameworkPath, 'Electron Framework'))).toBe( - 'Versions/Current/Electron Framework' - ) - expect(readlinkSync(join(frameworkPath, 'Versions', 'Current'))).toBe('A') - - await stopWrapperAndTrackedPids(wrapper, trackedPids) - }, - PREPARE_TIMEOUT_MS + 30_000 - ) }) diff --git a/config/scripts/development/profile-seeding/dev-profile-seed.test.mjs b/config/scripts/development/profile-seeding/dev-profile-seed.test.mjs index 7eb2c0ed2c8..b0c83a5079e 100644 --- a/config/scripts/development/profile-seeding/dev-profile-seed.test.mjs +++ b/config/scripts/development/profile-seeding/dev-profile-seed.test.mjs @@ -82,11 +82,6 @@ describe('seedDevProfile', () => { 'utf8' ) ).toBe('{"ciphertext":"x"}') - expect(readdirSync(target).sort()).toEqual([ - 'orca-dev-seed.json', - 'orca-profile-index.json', - 'profiles' - ]) }) it('refuses to overwrite a profile that already has state', async () => { diff --git a/config/scripts/development/web-client/dev-web-client-freshness.test.mjs b/config/scripts/development/web-client/dev-web-client-freshness.test.mjs index 2e8ad87eedf..d66af4f7889 100644 --- a/config/scripts/development/web-client/dev-web-client-freshness.test.mjs +++ b/config/scripts/development/web-client/dev-web-client-freshness.test.mjs @@ -1,12 +1,4 @@ -import { - mkdirSync, - mkdtempSync, - renameSync, - rmSync, - statSync, - utimesSync, - writeFileSync -} from 'node:fs' +import { mkdirSync, mkdtempSync, renameSync, rmSync, utimesSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' import { afterEach, expect, it } from 'vitest' @@ -58,23 +50,13 @@ function fixture() { return root } -it.each(['add', 'delete', 'rename'])( - 'invalidates a tracked bundle after a production source %s with old timestamps', - (operation) => { - const root = fixture() - expect(isDevWebClientFresh(root)).toBe(true) - if (operation !== 'add') { - rmSync(join(root, 'src/renderer/src/app.tsx')) - } - if (operation !== 'delete') { - const file = join(root, 'src/renderer/src/added.tsx') - writeFileSync(file, '') - utimesSync(file, OLD, OLD) - } - utimesSync(join(root, 'src/renderer/src'), OLD, OLD) - expect(isDevWebClientFresh(root)).toBe(false) - } -) +it('invalidates a tracked bundle after a production source deletion with old timestamps', () => { + const root = fixture() + expect(isDevWebClientFresh(root)).toBe(true) + rmSync(join(root, 'src/renderer/src/app.tsx')) + utimesSync(join(root, 'src/renderer/src'), OLD, OLD) + expect(isDevWebClientFresh(root)).toBe(false) +}) it('invalidates a tracked bundle when a source is removed in a nested directory', () => { const root = fixture() @@ -115,12 +97,6 @@ it('detects same-size atomic replacement with a restored mtime', () => { expect(isDevWebClientFresh(root)).toBe(true) writeFileSync(replacement, '') utimesSync(replacement, OLD, OLD) - const originalIdentity = statSync(file, { bigint: true }) - const replacementIdentity = statSync(replacement, { bigint: true }) - expect([replacementIdentity.dev, replacementIdentity.ino]).not.toEqual([ - originalIdentity.dev, - originalIdentity.ino - ]) renameSync(replacement, file) utimesSync(dirname(file), OLD, OLD) expect(isDevWebClientFresh(root)).toBe(false) diff --git a/config/scripts/development/web-client/project-renderer-web-client.test.mjs b/config/scripts/development/web-client/project-renderer-web-client.test.mjs index a2ab91b0468..3428a0ae12f 100644 --- a/config/scripts/development/web-client/project-renderer-web-client.test.mjs +++ b/config/scripts/development/web-client/project-renderer-web-client.test.mjs @@ -74,35 +74,19 @@ afterEach(() => { }) describe('renderer web client projection', () => { - it('copies and minifies only the web dependency closure', async () => { + it('copies only the web dependency closure', async () => { const root = createRendererFixture() const result = await projectFixture(root) expect(result.code, result.stderr).toBe(0) - expect(result.stdout).toContain('Projected web client: 7 files') expect(existsSync(join(root, 'out/web/web-index.html'))).toBe(true) expect(existsSync(join(root, 'out/web/assets/editor.worker-fixture.js'))).toBe(true) expect(existsSync(join(root, 'out/web/assets/logo.png'))).toBe(true) expect(existsSync(join(root, 'out/web/assets/desktop-entry.js'))).toBe(false) expect(existsSync(join(root, 'out/web/stale.js'))).toBe(false) - const sharedFiles = JSON.parse( - readFileSync(join(root, 'out/renderer/.orca-web-client-files.json'), 'utf8') - ) - expect(sharedFiles).toEqual( - expect.arrayContaining([ - 'web-index.html', - 'assets/web-entry.js', - 'assets/web-shared.js', - 'assets/editor.worker-fixture.js', - 'assets/web.css', - 'assets/lazy.js', - 'assets/logo.png' - ]) - ) - expect(sharedFiles).not.toContain('assets/desktop-entry.js') - expect(sharedFiles).not.toContain('.vite/manifest.json') - expect(sharedFiles).not.toContain('index.html') - expect(readFileSync(join(root, 'out/web/assets/web.css'), 'utf8')).toBe('.root{color:red}\n') + expect( + JSON.parse(readFileSync(join(root, 'out/renderer/.orca-web-client-files.json'), 'utf8')) + ).toContain('assets/editor.worker-fixture.js') }) it('follows transitive relative references and cycles with the existing substring behavior', async () => { @@ -133,21 +117,6 @@ describe('renderer web client projection', () => { expect(existsSync(join(root, 'out/web/workers/unreferenced.mjs'))).toBe(false) }) - it('retains all PDFJS viewer asset directories even without textual references', async () => { - const root = createRendererFixture() - const assets = ['cmaps/fixture.bcmap', 'standard_fonts/fixture.pfb', 'wasm/fixture.wasm'] - for (const file of assets) { - writeFixtureFile(root, `out/renderer/${file}`, Buffer.from([0, 255, 42])) - } - - const result = await projectFixture(root) - - expect(result.code, result.stderr).toBe(0) - for (const file of assets) { - expect(readFileSync(join(root, 'out/web', file))).toEqual(Buffer.from([0, 255, 42])) - } - }) - it('follows referenced PDFJS text assets without expanding unreferenced viewer modules', async () => { const root = createRendererFixture() writeFixtureFile( diff --git a/config/scripts/diagnostics/hang-watchdog/hang-watchdog-process-metrics.test.mjs b/config/scripts/diagnostics/hang-watchdog/hang-watchdog-process-metrics.test.mjs deleted file mode 100644 index 75e84d2e7c8..00000000000 --- a/config/scripts/diagnostics/hang-watchdog/hang-watchdog-process-metrics.test.mjs +++ /dev/null @@ -1,148 +0,0 @@ -import { execFileSync } from 'node:child_process' -import { monitorEventLoopDelay } from 'node:perf_hooks' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { sampleProductionPerformance } from './hang-watchdog-process-metrics.mjs' - -vi.mock('node:child_process', async (importOriginal) => ({ - ...(await importOriginal()), - execFileSync: vi.fn() -})) -vi.mock('node:perf_hooks', async (importOriginal) => ({ - ...(await importOriginal()), - monitorEventLoopDelay: vi.fn() -})) - -const observations = { events: [], readCpu: null, histogram: null } - -function cpuObservation(pid) { - return ['cpu', 'ps', ['-o', 'time=', '-p', String(pid)], { encoding: 'utf8' }] -} - -describe('production watchdog sample lifetime', () => { - beforeEach(() => { - vi.useFakeTimers() - observations.events = [] - let enabled = false - observations.histogram = { - enable() { - observations.events.push(['enable']) - const changed = !enabled - enabled = true - return changed - }, - disable() { - observations.events.push(['disable']) - const changed = enabled - enabled = false - return changed - }, - percentile: (value) => { - observations.events.push(['percentile', value]) - return value === 95 ? 1_900_000 : 3_100_000 - }, - max: 5_000_000 - } - vi.mocked(execFileSync).mockImplementation((command, args, options) => { - observations.events.push(['cpu', command, args, options]) - return observations.readCpu() - }) - vi.mocked(monitorEventLoopDelay).mockImplementation((options) => { - observations.events.push(['monitor', options]) - return observations.histogram - }) - }) - afterEach(() => { - vi.clearAllTimers() - vi.restoreAllMocks() - vi.useRealTimers() - }) - - it.each([1, 2])('clears the heartbeat when initial CPU observation %s throws', async (failAt) => { - const error = new Error('PID observation failed') - let reads = 0 - observations.readCpu = () => { - if (++reads === failAt) { - throw error - } - return '0:01.20' - } - const sendHeartbeat = vi.fn() - const sleep = vi.fn() - await expect( - sampleProductionPerformance( - { pids: [101, 102], sendHeartbeat }, - { heartbeatIntervalMs: 2_000, sampleMs: 30_000, sleep } - ) - ).rejects.toBe(error) - expect(reads).toBe(failAt) - expect(sleep).not.toHaveBeenCalled() - expect(observations.events.filter(([kind]) => kind !== 'disable')).toEqual([ - ['monitor', { resolution: 10 }], - ...[101, 102].slice(0, failAt).map(cpuObservation) - ]) - expect(vi.getTimerCount()).toBe(0) - await vi.advanceTimersByTimeAsync(4_000) - expect(sendHeartbeat).not.toHaveBeenCalled() - }) - - async function runSample({ values, sleepError } = {}) { - const cpuValues = values ?? ['0:01.20', '0:02.30', '0:01.25', '0:02.35'] - let index = 0 - observations.readCpu = () => { - const value = cpuValues[index++] - if (value instanceof Error) { - throw value - } - return value - } - const sendHeartbeat = vi.fn(() => observations.events.push(['heartbeat'])) - const sleep = async (ms) => { - observations.events.push(['sleep', ms]) - await vi.advanceTimersByTimeAsync(ms) - if (sleepError) { - throw sleepError - } - } - return sampleProductionPerformance( - { pids: [101, 102], sendHeartbeat }, - { heartbeatIntervalMs: 2_000, sampleMs: 6_000, sleep } - ) - } - - const completedSampleEvents = [ - ['monitor', { resolution: 10 }], - cpuObservation(101), - cpuObservation(102), - ['enable'], - ['sleep', 6_000], - ['heartbeat'], - ['heartbeat'], - ['heartbeat'], - ['disable'] - ] - - it('keeps complete live results, observation order and heartbeat pacing', async () => { - expect(await runSample()).toEqual({ - cpuMs: 100, - heartbeatCount: 3, - eventLoopDelayP95Ms: 1.9, - eventLoopDelayP99Ms: 3.1, - eventLoopDelayMaxMs: 5 - }) - expect(observations.events).toEqual([ - ...completedSampleEvents, - cpuObservation(101), - cpuObservation(102), - ['percentile', 95], - ['percentile', 99] - ]) - expect(vi.getTimerCount()).toBe(0) - }) - - it('keeps a sample sleep rejection and its existing cleanup', async () => { - const error = new Error('sample sleep rejected') - await expect(runSample({ sleepError: error })).rejects.toBe(error) - expect(observations.events).toEqual(completedSampleEvents) - expect(vi.getTimerCount()).toBe(0) - }) -}) diff --git a/config/scripts/diagnostics/lag-capture/lag-probe-failures.test.mjs b/config/scripts/diagnostics/lag-capture/lag-probe-failures.test.mjs index 7402bbf6f94..6812c067474 100644 --- a/config/scripts/diagnostics/lag-capture/lag-probe-failures.test.mjs +++ b/config/scripts/diagnostics/lag-capture/lag-probe-failures.test.mjs @@ -1,45 +1,10 @@ import assert from 'node:assert/strict' import { test } from 'vitest' import { runInNewContext } from 'node:vm' -import { installRendererIpcProbe } from './main-blocking-probe.mjs' import { connectOrcaMainInspector } from './orca-main-inspector-connection.mjs' -test('IPC polling records a rejection and permits the next poll', async () => { - let poll - let calls = 0 - const window = { - api: { - app: { - getIdentity: async () => { - if (++calls === 1) { - throw new Error('IPC disconnected') - } - } - } - } - } - runInNewContext(`(${String(installRendererIpcProbe)})()`, { - window, - performance, - Date, - document: { addEventListener() {}, removeEventListener() {} }, - setInterval(callback) { - poll = callback - return 1 - }, - clearInterval() {} - }) - await poll() - await poll() - const { requests } = window.__orcaIpcTimingProbe.stop() - assert.equal(requests.length, 2) - assert.match(requests[0].failed, /IPC disconnected/) - assert.equal(requests[1].failed, undefined) -}) - -test('socket closure rejects outstanding and subsequent requests without timeout timers', async () => { +test('socket closure rejects outstanding and subsequent requests', async () => { let socket - const timers = new Set() class FakeSocket { static OPEN = 1 readyState = 1 @@ -63,13 +28,10 @@ test('socket closure rejects outstanding and subsequent requests without timeout const connect = runInNewContext(`(${String(connectOrcaMainInspector)})`, { fetch: async () => ({ json: async () => [{ webSocketDebuggerUrl: 'ws://fixture' }] }), WebSocket: FakeSocket, - setTimeout(callback) { - timers.add(callback) - return callback + setTimeout() { + return 1 }, - clearTimeout(timer) { - timers.delete(timer) - } + clearTimeout() {} }) const connection = await connect(42) const first = connection.send('Profiler.start') @@ -78,5 +40,4 @@ test('socket closure rejects outstanding and subsequent requests without timeout await assert.rejects(first, /Inspector socket closed/) await assert.rejects(second, /Inspector socket closed/) await assert.rejects(connection.send('Profiler.enable'), /not open/) - assert.equal(timers.size, 0) }) diff --git a/config/scripts/diagnostics/terminal-transcripts/pty-transcript-secret-scan.test.mjs b/config/scripts/diagnostics/terminal-transcripts/pty-transcript-secret-scan.test.mjs index 664c3cdf2a9..fb562d41608 100644 --- a/config/scripts/diagnostics/terminal-transcripts/pty-transcript-secret-scan.test.mjs +++ b/config/scripts/diagnostics/terminal-transcripts/pty-transcript-secret-scan.test.mjs @@ -1,22 +1,14 @@ // Pin identifier shapes from live agent transcripts so the commit-time scrub gate cannot leak account IDs. import { readdirSync, readFileSync } from 'node:fs' -import os from 'node:os' import { join } from 'node:path' import { describe, expect, it } from 'vitest' import { formatFindings, - placeholderFor, redactTranscript, scanTranscriptForSecrets } from './pty-transcript-secret-scan.mjs' describe('pty transcript secret scan', () => { - it('finds the account row of a ready screen', () => { - const findings = scanTranscriptForSecrets('Antigravity CLI 1.1.17\njin.woo@acme.dev (Business)') - expect(findings).toHaveLength(1) - expect(findings[0]).toMatchObject({ kind: 'email', line: 2, column: 1 }) - }) - it('finds credentials an agent may echo while signing in', () => { const kinds = scanTranscriptForSecrets( [ @@ -29,41 +21,16 @@ describe('pty transcript secret scan', () => { expect(kinds).toEqual(['jwt', 'google-api-key', 'google-refresh-token', 'bearer-token']) }) - it('flags this machine’s own username, which a prompt line leaks', () => { - const username = os.userInfo().username - const findings = scanTranscriptForSecrets(`~/Users/${username}/orca/repo\n> `) - expect(findings.some((finding) => finding.kind === 'local-username')).toBe(true) - }) - it('finds the resumable conversation id agy prints on exit', () => { const findings = scanTranscriptForSecrets( 'Resume with -c (or command below):\nagy --conversation=26dc1986-9eec-456a-a534-d93e5c1076c2' ) expect(findings).toHaveLength(1) expect(findings[0].kind).toBe('uuid') - expect(placeholderFor('uuid', findings[0].match.length)).toMatch( - /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/ - ) - }) - - it('passes a fixture that is already scrubbed, so "prove it is gone" can succeed', () => { - const scrubbed = `uuuu@example.com\n${'X'.repeat(44)}` - expect(scanTranscriptForSecrets(scrubbed)).toEqual([]) }) }) describe('redaction', () => { - it('replaces every finding with the same number of characters', () => { - // Same-length redaction preserves captured wrapping; shorter replacements invalidate evidence. - const text = 'Antigravity CLI 1.1.17\njin.woo@acme.dev (Antigravity Business)\n> ' - const { text: redacted, redacted: count } = redactTranscript(text) - expect(count).toBe(1) - expect(redacted).toHaveLength(text.length) - expect(redacted).not.toContain('jin.woo@acme.dev') - expect(scanTranscriptForSecrets(redacted)).toEqual([]) - expect(redactTranscript(redacted).redacted).toBe(0) - }) - it('leaves the rest of the screen byte-for-byte untouched', () => { const text = 'line one\nuser@corp.io\nline three' expect(redactTranscript(text).text).toBe('line one\nXXXXXXXXXXXX\nline three') diff --git a/config/scripts/diagnostics/windows-pty/windows-pty-table-stress-observer.test.mjs b/config/scripts/diagnostics/windows-pty/windows-pty-table-stress-observer.test.mjs index b557887dc59..73faeda0e98 100644 --- a/config/scripts/diagnostics/windows-pty/windows-pty-table-stress-observer.test.mjs +++ b/config/scripts/diagnostics/windows-pty/windows-pty-table-stress-observer.test.mjs @@ -1,45 +1,20 @@ -import { rmSync } from 'node:fs' -import { afterEach, describe, expect, it } from 'vitest' +import { describe, expect, it } from 'vitest' import { sanitizeStressText } from './windows-pty-table-stress-observer.mjs' -const directories = [] -afterEach(() => { - for (const directory of directories.splice(0)) { - rmSync(directory, { recursive: true, force: true }) - } -}) - describe('Windows PTY stress observer', () => { - it('preserves OSC boundaries while redacting their title payload and adjacent CSI text', () => { + it('redacts a key interrupted by adjacent controls without moving their bytes', () => { const esc = String.fromCharCode(27) - const raw = `${esc}]0;private@sensitive.test${esc}\\${esc}[31mprivate@sensitive.test` + const before = 'sk-secret01' + const after = '234567890abcdefghijkl' + const controls = `${esc}[31m${esc}[1m` + const raw = `${before}${controls}${after}` const sanitized = sanitizeStressText(raw) - expect(sanitized).not.toContain('private@') - expect(sanitized).not.toContain('sensitive.test') - expect(sanitized).toContain(`${esc}]0;`) - expect(sanitized).toContain(`${esc}\\${esc}[31m`) + expect(sanitized).not.toContain(before) + expect(sanitized).not.toContain(after) + expect(sanitized.slice(before.length, before.length + controls.length)).toBe(controls) expect(sanitized.length).toBe(raw.length) }) - it.each([ - ['email domain', 'private@', 'sensitive.test', ['[31m']], - ['email name', 'pri', 'vate@sensitive.test', ['[31m', '[1m']], - ['vendor key', 'sk-secret01', '234567890abcdefghijkl', ['[31m', '[1m']], - ['bearer token', 'Bearer secret01', '234567890abcdefghijkl', ['[31m', '[1m']] - ])( - 'redacts %s interrupted by adjacent controls without moving their bytes', - (_kind, before, after, fragments) => { - const esc = String.fromCharCode(27) - const controls = fragments.map((fragment) => esc + fragment).join('') - const raw = `${before}${controls}${after}` - const sanitized = sanitizeStressText(raw) - expect(sanitized).not.toContain(before) - expect(sanitized).not.toContain(after) - expect(sanitized.slice(before.length, before.length + controls.length)).toBe(controls) - expect(sanitized.length).toBe(raw.length) - } - ) - it('scrubs an interrupted OSC title independently from the adjacent rendered address', () => { const esc = String.fromCharCode(27) const title = `private@${esc}[31msensitive.test` diff --git a/config/scripts/electron-installation/platform-layout/install-electron-package-binary.test.mjs b/config/scripts/electron-installation/platform-layout/install-electron-package-binary.test.mjs index 4b29b5607bf..11b074bd47c 100644 --- a/config/scripts/electron-installation/platform-layout/install-electron-package-binary.test.mjs +++ b/config/scripts/electron-installation/platform-layout/install-electron-package-binary.test.mjs @@ -1,19 +1,10 @@ -import { - existsSync, - lstatSync, - mkdirSync, - readdirSync, - readFileSync, - rmSync, - writeFileSync -} from 'node:fs' +import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { describe, expect, it } from 'vitest' import { addSiblingWorktree, initGitRepo, mkTempProject, - readExtractorCallCount, readSharedDistMarker, runInstallScript, sharedCacheRoot, @@ -27,7 +18,7 @@ import { } from './install-electron-package-binary-test-fixtures.mjs' describe('install-electron-package-binary', () => { - it('installs Electron from an isolated cache and repairs path.txt', () => { + it('installs the distribution and repairs path.txt', () => { const projectDir = mkTempProject() try { @@ -43,10 +34,6 @@ describe('install-electron-package-binary', () => { const result = runInstallScript(projectDir) expect(result.status, result.stderr).toBe(0) - expect(readFileSync(join(projectDir, 'electron-get.log'), 'utf8')).toMatch( - /cacheRoot=.*orca-electron-.*cache/ - ) - expect(readFileSync(join(projectDir, 'electron-get.log'), 'utf8')).toContain('force=true') expect(readFileSync(join(projectDir, 'node_modules', 'electron', 'path.txt'), 'utf8')).toBe( 'electron' ) @@ -61,31 +48,6 @@ describe('install-electron-package-binary', () => { ).isSymbolicLink() ).toBe(true) } - expect(result.stdout).toContain('Repaired Electron path.txt -> electron') - } finally { - rmSync(projectDir, { recursive: true, force: true }) - } - }) - - it('preserves an existing Electron distribution when replacement download fails', () => { - const projectDir = mkTempProject() - - try { - writeFakeElectronPackage(projectDir) - writeFakeElectronGet(projectDir, { downloadFailures: 1, downloadErrorCode: 'EACCES' }) - writeFakeElectronDist(projectDir, { - version: 'v40.0.0', - executableContents: 'existing executable', - pathContents: 'electron' - }) - - const result = runInstallScript(projectDir) - const electronDir = join(projectDir, 'node_modules/electron') - - expect(result.status).toBe(1) - expect(readFileSync(join(electronDir, 'dist/version'), 'utf8')).toBe('v40.0.0') - expect(readFileSync(join(electronDir, 'dist/electron'), 'utf8')).toBe('existing executable') - expect(readFileSync(join(electronDir, 'path.txt'), 'utf8')).toBe('electron') } finally { rmSync(projectDir, { recursive: true, force: true }) } @@ -124,36 +86,6 @@ describe('install-electron-package-binary', () => { } }) - it('keeps a persistent cache root while retrying transient failures', () => { - const projectDir = mkTempProject() - const cacheRoot = join(projectDir, 'electron-cache') - - try { - writeFakeElectronPackage(projectDir) - writeFakeElectronGet(projectDir, { - downloadFailures: 1, - downloadErrorCode: 'ECONNRESET' - }) - writeFakeExtractor(projectDir, { createExecutable: true }) - mkdirSync(cacheRoot, { recursive: true }) - writeFileSync(join(cacheRoot, 'preserved.marker'), 'keep me') - - const result = runInstallScript(projectDir, { - ORCA_ELECTRON_PACKAGE_CACHE_ROOT: cacheRoot, - ORCA_ELECTRON_PACKAGE_RETRY_DELAYS_MS: '0,0' - }) - - expect(result.status, result.stderr).toBe(0) - expect(existsSync(join(cacheRoot, 'preserved.marker'))).toBe(true) - expect( - readFileSync(join(projectDir, 'electron-get.log'), 'utf8').trim().split('\n') - ).toHaveLength(2) - } finally { - rmSync(projectDir, { recursive: true, force: true }) - } - }) - - // The shared cache is macOS-only: it exists to avoid a second copy via APFS clonefile. it('shares the Electron dist into a sibling worktree without downloading', () => { const projectDir = mkTempProject() const siblingDir = `${projectDir}-sibling` @@ -174,7 +106,6 @@ describe('install-electron-package-binary', () => { const siblingDistDir = join(siblingDir, 'node_modules/electron/dist') expect(result.status, result.stderr).toBe(0) - expect(readExtractorCallCount(siblingDir)).toBe(0) expect(existsSync(join(siblingDir, 'electron-get.log'))).toBe(false) expect(lstatSync(siblingDistDir).isDirectory()).toBe(true) expect(lstatSync(siblingDistDir).isSymbolicLink()).toBe(false) @@ -184,44 +115,13 @@ describe('install-electron-package-binary', () => { 'electron' ) expect(readSharedDistMarker(siblingDir)).toBe(sharedEntryName) - expect(result.stdout).toContain('Shared Electron 41.5.0 from') } finally { rmSync(siblingDir, { recursive: true, force: true }) rmSync(projectDir, { recursive: true, force: true }) } }) - it('publishes an already installed Electron dist that predates the shared cache', () => { - const projectDir = mkTempProject() - - try { - initGitRepo(projectDir) - writeFakeElectronPackage(projectDir) - writeFakeElectronGet(projectDir) - writeFakeExtractor(projectDir, { createExecutable: true }) - writeFakeElectronDist(projectDir, { - executableContents: 'existing executable', - pathContents: 'electron' - }) - - const result = runInstallScript(projectDir, { CI: '' }) - const entryPath = join(sharedCacheRoot(projectDir), sharedEntryName) - const distDir = join(projectDir, 'node_modules/electron/dist') - - expect(result.status, result.stderr).toBe(0) - expect(readExtractorCallCount(projectDir)).toBe(0) - expect(existsSync(join(projectDir, 'electron-get.log'))).toBe(false) - expect(readFileSync(join(entryPath, 'version'), 'utf8')).toBe('v41.5.0') - expect(readFileSync(join(entryPath, 'electron'), 'utf8')).toBe('existing executable') - expect(readSharedDistMarker(projectDir)).toBe(sharedEntryName) - expect(readFileSync(join(distDir, 'electron'), 'utf8')).toBe('existing executable') - expect(readFileSync(join(distDir, 'version'), 'utf8')).toBe('v41.5.0') - } finally { - rmSync(projectDir, { recursive: true, force: true }) - } - }) - - it('replaces a corrupt shared Electron dist entry instead of re-downloading forever', () => { + it('repairs a corrupt shared Electron dist entry', () => { const projectDir = mkTempProject() try { @@ -238,16 +138,12 @@ describe('install-electron-package-binary', () => { const distDir = join(projectDir, 'node_modules/electron/dist') expect(result.status, result.stderr).toBe(0) - expect(result.stderr).not.toContain('Failed to install Electron package binary') - expect(readExtractorCallCount(projectDir)).toBe(1) expect(readFileSync(join(distDir, 'version'), 'utf8')).toBe('v41.5.0') expect(readFileSync(join(projectDir, 'node_modules/electron/path.txt'), 'utf8')).toBe( 'electron' ) - // Why not just fall back: an entry left corrupt makes every sibling worktree download again. expect(readFileSync(join(entryPath, 'version'), 'utf8')).toBe('v41.5.0') expect(readSharedDistMarker(projectDir)).toBe(sharedEntryName) - expect(readdirSync(sharedCacheRoot(projectDir))).toEqual([sharedEntryName]) } finally { rmSync(projectDir, { recursive: true, force: true }) } @@ -275,11 +171,6 @@ describe('install-electron-package-binary', () => { 'v42.0.0' ) expect(readSharedDistMarker(projectDir)).toBe(sharedEntryNameFor('42.0.0')) - // Why the old entry stays: sibling worktrees on the previous branch still share it. - expect(readdirSync(cacheRoot).sort()).toEqual([ - sharedEntryNameFor('41.5.0'), - sharedEntryNameFor('42.0.0') - ]) expect(readFileSync(join(cacheRoot, sharedEntryNameFor('41.5.0'), 'version'), 'utf8')).toBe( 'v41.5.0' ) diff --git a/config/scripts/electron-installation/shared-dist-cache/shared-electron-dist-cache.test.ts b/config/scripts/electron-installation/shared-dist-cache/shared-electron-dist-cache.test.ts deleted file mode 100644 index 96937ded364..00000000000 --- a/config/scripts/electron-installation/shared-dist-cache/shared-electron-dist-cache.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import type { execFileSync } from 'node:child_process' -import { - existsSync, - mkdirSync, - mkdtempSync, - readdirSync, - rmSync, - symlinkSync, - writeFileSync -} from 'node:fs' -import { tmpdir } from 'node:os' -import path from 'node:path' -import { afterEach, describe, expect, it } from 'vitest' -import { - shareElectronDistFromCache, - isUsableElectronDist, - publishSharedElectronDist, - resolveSharedElectronDistEntry -} from './shared-electron-dist-cache.mjs' - -const VERSION = '43.4.1' -const PLATFORM_PATH = path.join('Electron.app', 'Contents', 'MacOS', 'Electron') -const roots: string[] = [] - -afterEach(() => { - while (roots.length > 0) { - rmSync(roots.pop()!, { recursive: true, force: true }) - } -}) - -function makeRoot(): string { - const root = mkdtempSync(path.join(tmpdir(), 'orca-shared-electron-')) - roots.push(root) - return root -} - -function writeDist(distPath: string, version = VERSION): string { - mkdirSync(path.join(distPath, path.dirname(PLATFORM_PATH)), { recursive: true }) - writeFileSync(path.join(distPath, 'version'), `v${version}\n`) - writeFileSync(path.join(distPath, PLATFORM_PATH), 'electron') - return distPath -} - -function makeEntry(root: string, entryName = `${VERSION}-darwin-arm64`) { - const cacheRoot = path.join(root, 'cache') - return { - cacheRoot, - entryPath: path.join(cacheRoot, entryName), - markerPath: path.join(root, '.orca-shared-dist') - } -} - -const baseOptions = { - repoRoot: '/repo', - electronPackageDir: '/repo/node_modules/electron', - version: VERSION, - targetPlatform: 'darwin', - targetArch: 'arm64', - env: {} as NodeJS.ProcessEnv, - execFile: (() => '/repo/.git\n') as unknown as typeof execFileSync -} - -describe('resolveSharedElectronDistEntry', () => { - it('declines an identity that would not be a single safe path segment', () => { - expect(resolveSharedElectronDistEntry({ ...baseOptions, targetArch: '../escape' })).toBeNull() - expect(resolveSharedElectronDistEntry({ ...baseOptions, targetPlatform: 'dar/win' })).toBeNull() - expect(resolveSharedElectronDistEntry({ ...baseOptions, version: '' })).toBeNull() - }) -}) - -describe('isUsableElectronDist', () => { - it('rejects a symlink so a redirected entry is never treated as cache content', () => { - const root = makeRoot() - writeDist(path.join(root, 'real')) - symlinkSync(path.join(root, 'real'), path.join(root, 'link'), 'dir') - expect(isUsableElectronDist(path.join(root, 'link'), VERSION, PLATFORM_PATH)).toBe(false) - }) -}) - -describe('publishSharedElectronDist', () => { - it('leaves no entry and no staging tree when sharing fails', () => { - const root = makeRoot() - const entry = makeEntry(root) - const share = (_source: string, destination: string) => { - writeDist(destination) - throw new Error('no shareable storage') - } - expect(publishSharedElectronDist(writeDist(path.join(root, 'dist')), entry, { share })).toBe( - false - ) - expect(existsSync(entry.entryPath)).toBe(false) - expect(readdirSync(entry.cacheRoot)).toEqual([]) - }) -}) - -describe('shareElectronDistFromCache', () => { - it('refuses an entry that fails validation instead of installing it', () => { - const root = makeRoot() - const entry = makeEntry(root) - mkdirSync(entry.cacheRoot, { recursive: true }) - writeDist(entry.entryPath, '40.0.0') - const stagePath = path.join(root, 'stage') - expect( - shareElectronDistFromCache(entry, stagePath, { - version: VERSION, - platformPath: PLATFORM_PATH - }) - ).toBe(false) - expect(existsSync(stagePath)).toBe(false) - }) - - it('reports failure rather than falling back to a full copy', () => { - const root = makeRoot() - const entry = makeEntry(root) - mkdirSync(entry.cacheRoot, { recursive: true }) - writeDist(entry.entryPath) - // Stub mechanism-dependent sharing failures; byte-copy fallback would defeat cache’s storage purpose. - const stagePath = path.join(root, 'stage') - const share = () => { - throw new Error('no shareable storage') - } - expect( - shareElectronDistFromCache(entry, stagePath, { - version: VERSION, - platformPath: PLATFORM_PATH, - share - }) - ).toBe(false) - expect(existsSync(stagePath)).toBe(false) - }) -}) diff --git a/config/scripts/locale-catalog/locale-brand-prefilter.test.mjs b/config/scripts/locale-catalog/locale-brand-prefilter.test.mjs deleted file mode 100644 index 0ea14ffea3b..00000000000 --- a/config/scripts/locale-catalog/locale-brand-prefilter.test.mjs +++ /dev/null @@ -1,55 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { repairTranslatedValue } from './locale-translation-policy.mjs' - -describe('locale brand matching', () => { - it('does not construct boundary expressions for absent brands', () => { - let boundaryExpressions = 0 - vi.stubGlobal( - 'RegExp', - new Proxy(RegExp, { - construct(target, args) { - if (typeof args[0] === 'string' && args[0].startsWith('(^|[^A-Za-z_])')) { - boundaryExpressions += 1 - } - return Reflect.construct(target, args) - } - }) - ) - try { - for (const locale of ['zh', 'ja', 'ko', 'es']) { - expect( - repairTranslatedValue({ - key: 'fixture.endpoint', - enValue: 'Choose an endpoint.', - localeValue: 'fixture translation', - locale - }) - ).toBe('fixture translation') - } - } finally { - vi.unstubAllGlobals() - } - expect(boundaryExpressions).toBe(0) - }) - - it.each([ - ['Use Gemini.', 'Usar Géminis.', 'Usar Gemini.'], - ['Use GeminiX.', 'Usar Géminis.', 'Usar Géminis.'], - ['Use XGemini.', 'Usar Géminis.', 'Usar Géminis.'], - ['Use _Gemini_.', 'Usar Géminis.', 'Usar Géminis.'], - ['Use Gemini_2.', 'Usar Géminis.', 'Usar Géminis.'], - ['Use 2Gemini3.', 'Usar Géminis.', 'Usar Gemini.'], - ['Use (Gemini).', 'Usar Géminis.', 'Usar Gemini.'], - ['Use éGemini界.', 'Usar Géminis.', 'Usar Gemini.'], - ['Use gemini.', 'Usar Géminis.', 'Usar Géminis.'], - ['Use GeminiX and Gemini.', 'Usar Géminis.', 'Usar Gemini.'], - ['Use Gemini.', 'Gemini y Géminis.', 'Gemini y Géminis.'], - ['Use Gemini.', '_Gemini_ y Géminis.', '_Gemini_ y Gemini.'], - ['Use GitHub Copilot.', 'Usar Copiloto de GitHub.', 'Usar GitHub Copilot.'], - ['Use XGitHub CopilotY.', 'Usar Copiloto de GitHub.', 'Usar GitHub Copilot.'] - ])('preserves literal and boundary matching for %j / %j', (enValue, localeValue, expected) => { - expect( - repairTranslatedValue({ key: 'fixture.brand', enValue, localeValue, locale: 'es' }) - ).toBe(expected) - }) -}) diff --git a/config/scripts/locale-catalog/locale-generic-ui-terms.test.mjs b/config/scripts/locale-catalog/locale-generic-ui-terms.test.mjs deleted file mode 100644 index f2f5f6aebdb..00000000000 --- a/config/scripts/locale-catalog/locale-generic-ui-terms.test.mjs +++ /dev/null @@ -1,125 +0,0 @@ -import { describe, expect, it } from 'vitest' - -import { repairTranslatedValue } from './locale-translation-policy.mjs' - -// #12113: generic-word brand repair reverted ~2000 translations; keep UI vocabulary translated and real brands Latin. -describe('locale generic UI terms', () => { - it('keeps a translated generic term when the whole value is that term', () => { - expect( - repairTranslatedValue({ - key: 'auto.components.settings.AppearancePane.terminalTitle', - enValue: 'Terminal', - localeValue: '터미널', - locale: 'ko' - }) - ).toBe('터미널') - expect( - repairTranslatedValue({ - key: 'auto.components.settings.QuickCommandsPane.4ccc63da87', - enValue: 'Agent', - localeValue: '에이전트', - locale: 'ko' - }) - ).toBe('에이전트') - expect( - repairTranslatedValue({ - key: 'auto.components.mobile.MobileHero.a8fb43cf1c', - enValue: 'Continue', - localeValue: '继续', - locale: 'zh' - }) - ).toBe('继续') - }) - - it('keeps a translated generic term inside a sentence', () => { - expect( - repairTranslatedValue({ - key: 'auto.components.settings.EditorFontSection.matchTerminal', - enValue: 'Match terminal font', - localeValue: '터미널 글꼴과 동일', - locale: 'ko' - }) - ).toBe('터미널 글꼴과 동일') - expect( - repairTranslatedValue({ - key: 'auto.components.settings.ExperimentalPane.agentDashboard.toggleLabel', - enValue: 'Toggle Agent Dashboard', - localeValue: '에이전트 대시보드 전환', - locale: 'ko' - }) - ).toBe('에이전트 대시보드 전환') - expect( - repairTranslatedValue({ - key: 'components.agentSessionContinuation.dialogTitle', - enValue: 'Continue in New Session', - localeValue: '新しいセッションで続ける', - locale: 'ja' - }) - // 新しい → 新規 is an unrelated phrase fix; 続ける is what must survive. - ).toBe('新規セッションで続ける') - }) - - it('still reverts real brand names that machine translation localized', () => { - expect( - repairTranslatedValue({ - key: 'auto.stats.StatsPane.7d26110cea', - enValue: 'Codex', - localeValue: '사본', - locale: 'ko' - }) - ).toBe('Codex') - expect( - repairTranslatedValue({ - key: 'auto.components.settings.NetworkPane.tailscale', - enValue: 'Connect over Tailscale', - localeValue: '通过尾鳞连接', - locale: 'zh' - }) - ).toBe('通过 Tailscale 连接') - }) - - it('does not cut a valid word that contains a nonsense rendering', () => { - // 终端子进程 is "terminal sub-process" — 端子 sits inside 终端 + 子. - expect( - repairTranslatedValue({ - key: 'auto.components.settings.AdvancedNetworkSettingsSection.823e0f15b1', - enValue: 'Proxy URL for Orca network requests and local terminal subprocesses.', - localeValue: '用于 Orca 网络请求和本地终端子进程的代理 URL。', - locale: 'zh' - }) - ).toBe('用于 Orca 网络请求和本地终端子进程的代理 URL。') - }) - - it('still fills in a translation when the catalog holds English', () => { - expect( - repairTranslatedValue({ - key: 'auto.components.feature.wall.BrowserAnimatedVisual.04096318ab', - enValue: 'Terminal 1', - localeValue: 'Terminal 1', - locale: 'ko' - }) - ).toBe('터미널 1') - }) - - it('keeps agent catalog product names in English by key, not by word', () => { - expect( - repairTranslatedValue({ - key: 'auto.lib.agent.catalog.9e2a9bb87b', - enValue: 'Continue', - localeValue: '继续', - locale: 'zh' - }) - ).toBe('Continue') - }) - - it.each([ - ['ko', 'Commit', '커밋'], - ['ja', 'repos', 'リポジトリ'], - ['es', 'agents', 'agentes'], - ['ko', 'terminals', '터미널'] - ])('preserves the %s translation of generic %s', (locale, enValue, localeValue) => { - expect(repairTranslatedValue({ key: 'fixture.generic', enValue, localeValue, locale })).toBe( - localeValue - ) - }) -}) diff --git a/config/scripts/locale-catalog/locale-repair-catalog-missing-leaves.test.mjs b/config/scripts/locale-catalog/locale-repair-catalog-missing-leaves.test.mjs index 61a0395ee8e..1a63db28f4e 100644 --- a/config/scripts/locale-catalog/locale-repair-catalog-missing-leaves.test.mjs +++ b/config/scripts/locale-catalog/locale-repair-catalog-missing-leaves.test.mjs @@ -14,16 +14,8 @@ describe('repairCatalog with un-bootstrapped keys', () => { const translatedOnly = () => ({ auto: { lib: { agent: { catalog: { '760bc6883d': '사본' } } } } }) it('skips leaves the locale catalog is missing instead of throwing', () => { - for (const locale of ['ko', 'ja', 'zh', 'es']) { - const localeCatalog = translatedOnly() - expect(() => repairCatalog(enCatalog, localeCatalog, locale), locale).not.toThrow() - expect(localeCatalog.auto.components, locale).toBeUndefined() - } - }) - - it('still repairs the leaves that are present', () => { const localeCatalog = translatedOnly() - expect(repairCatalog(enCatalog, localeCatalog, 'ko')).toBe(1) - expect(localeCatalog.auto.lib.agent.catalog['760bc6883d']).toBe('Codex') + expect(() => repairCatalog(enCatalog, localeCatalog, 'ko')).not.toThrow() + expect(localeCatalog.auto.components).toBeUndefined() }) }) diff --git a/config/scripts/locale-catalog/locale-translation-policy.es-pr.test.mjs b/config/scripts/locale-catalog/locale-translation-policy.es-pr.test.mjs deleted file mode 100644 index b8179ffda9a..00000000000 --- a/config/scripts/locale-catalog/locale-translation-policy.es-pr.test.mjs +++ /dev/null @@ -1,34 +0,0 @@ -import { describe, expect, it } from 'vitest' - -import { repairTranslatedValue } from './locale-translation-policy.mjs' - -const repairEs = (enValue, localeValue) => - repairTranslatedValue({ - key: 'auto.components.test.pr-glossary', - enValue, - localeValue, - locale: 'es' - }) - -describe('locale-translation-policy es PR glossary', () => { - it('rewrites the "relaciones públicas" mistranslation of PR back to PR', () => { - expect(repairEs('PR', 'relaciones públicas')).toBe('PR') - expect(repairEs('PRs', 'relaciones públicas')).toBe('PR') - expect(repairEs('Reopen PR', 'Reabrir relaciones públicas')).toBe('Reabrir PR') - expect(repairEs('Create PR', 'Crear relaciones públicas')).toBe('Crear PR') - expect(repairEs('Open PR checks', 'Abrir cheques de relaciones públicas')).toBe( - 'Abrir cheques de PR' - ) - expect(repairEs('unlink PR', 'desvincular relaciones públicas')).toBe('desvincular PR') - }) - - it('does not fire when the English has a "pr" substring but no PR token', () => { - // approve/public contain pr substring but no PR token; leave genuine public-relations prose alone. - expect(repairEs('Approve public relations', 'Aprobar relaciones públicas')).toBe( - 'Aprobar relaciones públicas' - ) - expect(repairEs('Compress the preview', 'Comprimir las relaciones públicas')).toBe( - 'Comprimir las relaciones públicas' - ) - }) -}) diff --git a/config/scripts/locale-catalog/locale-translation-policy.test.mjs b/config/scripts/locale-catalog/locale-translation-policy.test.mjs deleted file mode 100644 index 34303b7d231..00000000000 --- a/config/scripts/locale-catalog/locale-translation-policy.test.mjs +++ /dev/null @@ -1,22 +0,0 @@ -import { describe, expect, it } from 'vitest' - -import { repairTranslatedValue, shouldPreserveEnglishValue } from './locale-translation-policy.mjs' - -describe('locale-translation-policy', () => { - it('does not break Copy identifier when fixing Codex', () => { - expect( - repairTranslatedValue({ - key: 'auto.components.example', - enValue: 'Copy identifier', - localeValue: '사본 식별자', - locale: 'ko' - }) - ).toBe('사본 식별자') - }) - - it('skips machine translation for standalone brands', () => { - expect(shouldPreserveEnglishValue('Codex')).toBe(true) - expect(shouldPreserveEnglishValue('Codex', 'auto.stats.StatsPane.7d26110cea')).toBe(true) - expect(shouldPreserveEnglishValue('Show Codex usage')).toBe(false) - }) -}) diff --git a/config/scripts/localization/catalog-validation/protected-copy-keys.test.mjs b/config/scripts/localization/catalog-validation/protected-copy-keys.test.mjs index de26081b091..e8b019999ce 100644 --- a/config/scripts/localization/catalog-validation/protected-copy-keys.test.mjs +++ b/config/scripts/localization/catalog-validation/protected-copy-keys.test.mjs @@ -4,18 +4,7 @@ import path from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import { collectProtectedCopyKeys, main, PROTECTED_COPY_KEYS_PATH } from './protected-copy-keys.mjs' -const ENGLISH = new Set([ - 'trust.title', - 'trust.allow', - 'trust.copy.warning', - 'confirm.delete', - 'confirm.count_other', - 'native.quit', - 'pragma.badge', - 'nested.deep', - 'trust.reasons.network', - 'plain.title' -]) +const ENGLISH = new Set(['trust.title', 'trust.reasons.network']) let dirs = [] afterEach(() => { @@ -40,33 +29,6 @@ function sourcesFrom(files) { } describe('collectProtectedCopyKeys', () => { - it('protects every key a marked file or its sibling copy module names', () => { - const sources = sourcesFrom({ - 'dialogs/TrustDialog.tsx': `import { trustCopy } from './trust-copy' -import { plain } from '../plain/Plain' -{translate('trust.title', 'Trust?')}{translate('trust.allow', 'Allow')}`, - 'dialogs/trust-copy.ts': `export const trustCopy = () => translate('trust.copy.warning', 'Careful')`, - 'plain/Plain.tsx': `export const plain = () => translate('plain.title', 'Title')`, - 'confirm/DeleteDialog.tsx': ``, - 'main/prompt.ts': `dialog.showMessageBox({ buttons: [translateMain('native.quit', 'Quit')] })`, - 'badge.ts': `// @protected-copy: badge\nexport const b = () => translate('pragma.badge', 'Official')`, - 'other.tsx': `translate('nested.deep', 'Deep')` - }) - - expect(collectProtectedCopyKeys(sources, ENGLISH)).toEqual({ - keys: [ - 'confirm.count', - 'confirm.delete', - 'native.quit', - 'pragma.badge', - 'trust.allow', - 'trust.copy.warning', - 'trust.title' - ], - prefixes: [] - }) - }) - it('keeps dynamic key heads only when they are specific', () => { const sources = sourcesFrom({ 'Dialog.tsx': '{translate(`trust.${id}`)}{translate(`trust.reasons.${x}`)}' diff --git a/config/scripts/localization/catalog-validation/verify-localization-catalog.test.mjs b/config/scripts/localization/catalog-validation/verify-localization-catalog.test.mjs index 47f0400a4b1..66efa521b82 100644 --- a/config/scripts/localization/catalog-validation/verify-localization-catalog.test.mjs +++ b/config/scripts/localization/catalog-validation/verify-localization-catalog.test.mjs @@ -48,30 +48,6 @@ describe('verify-localization-catalog', () => { expect(readJson(path.join(localesDir, 'es.json'))).toEqual({}) }) - it.each([ - ['en', false], - ['en', true], - ['es', false] - ])('rejects dotted %s keys without rewriting them (fix: %s)', async (locale, fix) => { - const nested = { auto: { example: { greeting: 'Hello.world' } } } - const { root, localesDir } = makeProject({ - sourceText: 'export {}\n', - enCatalog: locale === 'en' ? { 'auto.example.greeting': 'Hello.world' } : nested, - esCatalog: locale === 'es' ? { auto: { 'example.greeting': 'Hola.world' } } : {} - }) - const catalogPath = path.join(localesDir, `${locale}.json`) - const before = readFileSync(catalogPath, 'utf8') - const report = vi.spyOn(console, 'error').mockImplementation(() => undefined) - try { - await expect(verifyLocalizationCatalog(root, { fix })).resolves.toBe(1) - expect(report).toHaveBeenCalledWith(expect.stringContaining(`${locale}.json`)) - expect(report).toHaveBeenCalledWith('dotted: auto.example.greeting') - expect(readFileSync(catalogPath, 'utf8')).toBe(before) - } finally { - report.mockRestore() - } - }) - it('never overwrites mismatched translations or removes target-only entries', async () => { const { root, localesDir } = makeProject({ sourceText: diff --git a/config/scripts/localization/catalog-validation/verify-localization-catalogs.test.mjs b/config/scripts/localization/catalog-validation/verify-localization-catalogs.test.mjs deleted file mode 100644 index 0ddf3650943..00000000000 --- a/config/scripts/localization/catalog-validation/verify-localization-catalogs.test.mjs +++ /dev/null @@ -1,50 +0,0 @@ -import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterEach, describe, expect, it } from 'vitest' -import { main as verifyTogether } from './verify-localization-catalogs.mjs' - -const directories = [] -const renderer = 'src/renderer/src' -const englishPath = `${renderer}/i18n/locales/en.json` -const runtimePath = `${renderer}/i18n/en-runtime-required.json` - -async function fixture() { - const root = await mkdtemp(join(tmpdir(), 'orca-localization-checks-')) - directories.push(root) - await mkdir(join(root, renderer, 'i18n/locales'), { recursive: true }) - await mkdir(join(root, 'src/main'), { recursive: true }) - await writeFile(join(root, renderer, 'greeting.ts'), "translate('hello', 'Hello')\n") - await writeFile(join(root, 'src/main/ready.ts'), "translateMain('ready', 'Ready')\n") - await writeFile( - join(root, englishPath), - JSON.stringify({ hello: 'Hello', ready: 'Ready', dynamic: 'Required' }) - ) - await writeFile(join(root, runtimePath), JSON.stringify({ dynamic: 'Required' })) - await mkdir(join(root, 'src/shared/plugins/languages'), { recursive: true }) - await writeFile( - join(root, 'src/shared/plugins/languages/protected-copy-keys.json'), - `${JSON.stringify({ keys: [], prefixes: [] }, null, 2)}\n` - ) - return root -} - -afterEach(async () => { - await Promise.all(directories.splice(0).map((root) => rm(root, { recursive: true, force: true }))) -}) - -describe('combined localization verification', () => { - it('reads source edits, new files, removals, and separate roots on each invocation', async () => { - const root = await fixture() - expect(await verifyTogether(root)).toBe(0) - await writeFile(join(root, renderer, 'greeting.ts'), "translate('hello', 'New fallback')") - expect(await verifyTogether(root)).toBe(1) - await writeFile(join(root, renderer, 'greeting.ts'), "translate('hello', 'Hello')") - expect(await verifyTogether(root)).toBe(0) - await writeFile(join(root, 'src/main/added.ts'), "translateMain('absent', 'Absent')") - expect(await verifyTogether(root)).toBe(1) - await rm(join(root, 'src/main/added.ts')) - expect(await verifyTogether(root)).toBe(0) - expect(await verifyTogether(await fixture())).toBe(0) - }) -}) diff --git a/config/scripts/localization/source-extraction/audit-localization-native-dialogs.test.mjs b/config/scripts/localization/source-extraction/audit-localization-native-dialogs.test.mjs index a24179060cd..2c4fb0b84ad 100644 --- a/config/scripts/localization/source-extraction/audit-localization-native-dialogs.test.mjs +++ b/config/scripts/localization/source-extraction/audit-localization-native-dialogs.test.mjs @@ -2,34 +2,9 @@ import { describe, expect, it, vi } from 'vitest' import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { collectLocalizationCandidates, main } from './audit-localization-coverage.mjs' - -const path = '/repo/src/main/startup/dialog.ts' -const source = ` - const options = { - message: translateMain('message', 'Translated message'), - detail: 'Untranslated detail', - buttons: ['Proceed', translateMain('cancel', 'Cancel')], - body: 'Untranslated body', - buttonLabel: 'Retry', - checkboxLabel: 'Remember choice' - } -` +import { main } from './audit-localization-coverage.mjs' describe('native dialog localization coverage', () => { - it('finds untranslated native dialog fields while ignoring translated copy', () => { - const candidates = collectLocalizationCandidates(path, source, '/repo', { - includeNativeDialogFields: true - }) - expect(candidates.map(({ kind, text }) => [kind, text])).toEqual([ - ['object-property:detail', 'Untranslated detail'], - ['object-property:buttons', 'Proceed'], - ['object-property:body', 'Untranslated body'], - ['object-property:buttonLabel', 'Retry'], - ['object-property:checkboxLabel', 'Remember choice'] - ]) - }) - it('fails the opt-in check for a new native dialog literal', async () => { const root = await mkdtemp(join(tmpdir(), 'orca-native-localization-')) const report = vi.spyOn(console, 'error').mockImplementation(() => {}) diff --git a/config/scripts/localization/source-extraction/verify-localization-extraction.test.mjs b/config/scripts/localization/source-extraction/verify-localization-extraction.test.mjs deleted file mode 100644 index 1bc53668a80..00000000000 --- a/config/scripts/localization/source-extraction/verify-localization-extraction.test.mjs +++ /dev/null @@ -1,55 +0,0 @@ -import { describe, expect, it } from 'vitest' - -import { - collectKeyLiterals, - compareExtraction, - findUnreferencedKeys -} from './verify-localization-extraction.mjs' - -function sourceEvidence(sourceText) { - return collectKeyLiterals(sourceText, { literals: new Set(), dynamicPrefixes: new Set() }) -} - -describe('verify-localization-extraction', () => { - it('rejects missing English declarations and incompatible placeholders', () => { - const result = compareExtraction( - { - menu: { - missing: 'Missing', - open: 'Open {{name}}' - } - }, - { menu: { open: 'Open {{path}}' } } - ) - - expect(result.missingFromEnglish).toEqual(['menu.missing']) - expect(result.placeholderMismatches).toEqual(['menu.open']) - }) - - it('keeps plural variants and data-structure keys that extraction cannot see', () => { - const evidence = sourceEvidence("const rows = [{ labelKey: 'rows.held' }]") - const extracted = new Map([['menu.count', '']]) - - expect( - findUnreferencedKeys( - ['menu.count_one', 'menu.count_other', 'rows.held', 'rows.dead_one', 'menu.stale'], - extracted, - evidence - ) - ).toEqual(['rows.dead_one', 'menu.stale']) - }) - - it('keeps keys under a prefix that a translate call composes at runtime', () => { - const evidence = sourceEvidence( - "translate(`status.label.${state}`)\nt('menu.item.' + id)\nconst id = `rpc.${method}`" - ) - - expect( - findUnreferencedKeys( - ['status.label.ready', 'menu.item.open', 'rpc.ping'], - new Map(), - evidence - ) - ).toEqual(['rpc.ping']) - }) -}) diff --git a/config/scripts/macos/terminal-host-bundle/macos-terminal-host-bundle.test.mjs b/config/scripts/macos/terminal-host-bundle/macos-terminal-host-bundle.test.mjs index ae5d63ef9a4..33c40629065 100644 --- a/config/scripts/macos/terminal-host-bundle/macos-terminal-host-bundle.test.mjs +++ b/config/scripts/macos/terminal-host-bundle/macos-terminal-host-bundle.test.mjs @@ -1,13 +1,4 @@ -import { spawnSync } from 'node:child_process' -import { - existsSync, - mkdirSync, - mkdtempSync, - readFileSync, - rmSync, - statSync, - writeFileSync -} from 'node:fs' +import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { createRequire } from 'node:module' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -16,19 +7,10 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' const require = createRequire(new URL('../../macos-terminal-host-bundle.test.mjs', import.meta.url)) const { assembleMacTerminalHost, - buildMacTerminalHostInfo, daemonBareRequires, - findMacTerminalHostInfoProblems, findUnresolvedDaemonRequires, - macTerminalHostPaths, parseBuildVersionMinos } = require('./macos/terminal-host-bundle/macos-terminal-host-bundle.cjs') -const electronBuilderConfig = require('../electron-builder.config.cjs') - -const usageDescriptions = electronBuilderConfig.mac.extendInfo -const appInfo = { CFBundleIdentifier: 'com.stablyai.orca', CFBundleShortVersionString: '1.4.300' } -const expectations = { appId: 'com.stablyai.orca', usageDescriptions, minimumOs: '13.5' } - describe('terminal host binary checks', () => { it('reads minos from the LC_BUILD_VERSION load command only', () => { const otool = [ @@ -80,31 +62,6 @@ describe.skipIf(process.platform !== 'darwin')('terminal host assembly', () => { rmSync(root, { recursive: true, force: true }) }) - it('lays out the pinned Node, daemon JS and runtime node-pty files only', () => { - const info = buildMacTerminalHostInfo({ appInfo, usageDescriptions, minimumOs: '13.5' }) - const paths = assembleMacTerminalHost({ - appPath, - nodeExecutable: join(root, 'node'), - iconPath: join(root, 'icon.icns'), - info - }) - expect(paths).toEqual(macTerminalHostPaths(appPath)) - expect(paths.bundle).toBe(join(appPath, 'Contents', 'Helpers', 'Orca Terminal Host.app')) - expect(readFileSync(paths.executable, 'utf8')).toBe('pinned-node') - expect(statSync(paths.executable).mode & 0o777).toBe(0o755) - expect(statSync(paths.spawnHelper).mode & 0o777).toBe(0o755) - expect(readFileSync(paths.entry, 'utf8')).toBe('entry') - expect(existsSync(join(paths.daemon, 'out', 'main', 'chunks', 'protocol.js'))).toBe(true) - expect(readFileSync(paths.outPackageJson, 'utf8')).toBe('{"type":"commonjs"}') - expect(existsSync(join(paths.nodePty, 'lib', 'index.js'))).toBe(true) - expect(existsSync(join(paths.nodePty, 'src'))).toBe(false) - expect(readFileSync(paths.icon, 'utf8')).toBe('icon') - const plist = spawnSync('/usr/bin/plutil', ['-convert', 'json', '-o', '-', paths.infoPlist], { - encoding: 'utf8' - }) - expect(findMacTerminalHostInfoProblems(JSON.parse(plist.stdout), expectations)).toEqual([]) - }) - it('copies every package the daemon requires, with its dependencies', () => { const resources = join(appPath, 'Contents', 'Resources') const main = join(resources, 'app.asar.unpacked', 'out', 'main') @@ -180,29 +137,6 @@ describe.skipIf(process.platform !== 'darwin')('terminal host assembly', () => { expect(findUnresolvedDaemonRequires(paths)).toEqual([]) }) - it('follows directory and JSON loads and keeps nested dependencies with their parent', () => { - const resources = join(appPath, 'Contents', 'Resources') - const main = join(resources, 'app.asar.unpacked', 'out', 'main') - mkdirSync(join(main, 'lib'), { recursive: true }) - writeFileSync(join(main, 'daemon-entry.js'), "require('./lib');require('./data.json')") - writeFileSync(join(main, 'lib', 'index.js'), "require('outer')") - writeFileSync(join(main, 'data.json'), '{}') - const outer = join(resources, 'node_modules', 'outer') - mkdirSync(join(outer, 'node_modules', 'inner'), { recursive: true }) - writeFileSync(join(outer, 'package.json'), '{"dependencies":{"inner":"1"}}') - writeFileSync(join(outer, 'node_modules', 'inner', 'package.json'), '{}') - const paths = assembleMacTerminalHost({ - appPath, - nodeExecutable: join(root, 'node'), - iconPath: join(root, 'icon.icns'), - info: {} - }) - expect(daemonBareRequires(join(paths.daemon, 'out', 'main'))).toEqual(['outer']) - expect(existsSync(join(paths.daemon, 'node_modules', 'outer', 'node_modules', 'inner'))).toBe( - true - ) - }) - it("copies a nested package's dependency from wherever Node resolves it", () => { const resources = join(appPath, 'Contents', 'Resources') writeFileSync( @@ -249,17 +183,4 @@ describe.skipIf(process.platform !== 'darwin')('terminal host assembly', () => { }) ).toThrow(/loads a module by a computed name/) }) - - it('fails before writing anything when the daemon entry is missing', () => { - rmSync(join(appPath, 'Contents', 'Resources', 'app.asar.unpacked'), { recursive: true }) - expect(() => - assembleMacTerminalHost({ - appPath, - nodeExecutable: join(root, 'node'), - iconPath: join(root, 'icon.icns'), - info: {} - }) - ).toThrow(/missing .*daemon-entry\.js/) - expect(existsSync(join(appPath, 'Contents', 'Helpers'))).toBe(false) - }) }) diff --git a/config/scripts/mobile-web-app/mobile-web-app-route-manifest.test.mjs b/config/scripts/mobile-web-app/mobile-web-app-route-manifest.test.mjs index 51c9f29943b..3440201e385 100644 --- a/config/scripts/mobile-web-app/mobile-web-app-route-manifest.test.mjs +++ b/config/scripts/mobile-web-app/mobile-web-app-route-manifest.test.mjs @@ -1,22 +1,11 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' import { MOBILE_WEB_APP_ROUTE_ROOT, - ROUTE_MODULE_SYNCHRONOUS_EXPORTS, - collectMobileWebAppRouteKeys, - collectMobileWebAppRoutes, - routeModuleSynchronousExports + collectMobileWebAppRoutes } from './mobile-web-app-route-manifest.mjs' -import { mobileWebAppDependenciesPresent } from './mobile-web-app-bundle-dependencies.mjs' - -const projectDir = fileURLToPath(new URL('../../..', import.meta.url)) -const appDir = join(projectDir, 'mobile', 'app') - -// Root shards omit mobile dependencies; route-tree builds run in mobile_web_app job. -const itBundling = mobileWebAppDependenciesPresent() ? it : it.skip async function withScratch(run) { const scratch = await mkdtemp(join(tmpdir(), 'orca-mobile-web-app-routes-test-')) @@ -28,98 +17,6 @@ async function withScratch(run) { } describe('route manifest', () => { - it('carries no root layout for a tree without a web sibling of it', async () => { - await withScratch(async (scratch) => { - await mkdir(join(scratch, MOBILE_WEB_APP_ROUTE_ROOT), { recursive: true }) - await writeFile(join(scratch, '_layout.tsx'), 'export default null\n', 'utf8') - await writeFile(join(scratch, MOBILE_WEB_APP_ROUTE_ROOT, 'index.tsx'), 'export default 1\n') - expect(await collectMobileWebAppRouteKeys(scratch)).toEqual(['./h/index.tsx']) - }) - }) - - it('excludes test files and API routes', async () => { - // Synthetic tree exercises absent-today forbidden route exports. - await withScratch(async (scratch) => { - const directory = join(scratch, MOBILE_WEB_APP_ROUTE_ROOT) - await mkdir(directory, { recursive: true }) - for (const name of [ - 'index.tsx', - 'index.test.tsx', - 'index.spec.tsx', - 'shape.d.ts', - '+api.ts', - 'tokens+api.ts', - '+middleware.ts', - 'notes.md' - ]) { - await writeFile(join(directory, name), 'export default null\n', 'utf8') - } - expect(await collectMobileWebAppRouteKeys(scratch)).toEqual(['./h/index.tsx']) - }) - expect(await collectMobileWebAppRouteKeys(appDir)).not.toContain('./h/_layout.test.tsx') - }) - - // Alias/reexport controls require namespace parse, not declaration-name matching. - it('reads the names off the namespace, not off a declaration', async () => { - expect(ROUTE_MODULE_SYNCHRONOUS_EXPORTS).toEqual(['unstable_settings', 'ErrorBoundary']) - await withScratch(async (scratch) => { - const exportsOf = async (name, source) => { - const file = join(scratch, name) - await writeFile(file, source, 'utf8') - return routeModuleSynchronousExports(file) - } - expect( - (await exportsOf('declared.tsx', 'export const unstable_settings = { anchor: "x" }\n')) - .named - ).toEqual(['unstable_settings']) - expect( - ( - await exportsOf( - 'aliased.tsx', - 'const settings = { anchor: "x" }\nexport { settings as unstable_settings }\n' - ) - ).named - ).toEqual(['unstable_settings']) - expect((await exportsOf('classy.tsx', 'export class ErrorBoundary {}\n')).named).toEqual([ - 'ErrorBoundary' - ]) - expect( - (await exportsOf('forwarded.tsx', 'export { ErrorBoundary } from "./boundary"\n')).named - ).toEqual(['ErrorBoundary']) - expect( - (await exportsOf('plain.tsx', 'export default function Route() { return null }\n')).named - ).toEqual([]) - }) - }, 60_000) - - it('refuses a star re-export rather than reading it as clean', async () => { - await withScratch(async (scratch) => { - const file = join(scratch, 'star.tsx') - // Unresolved export * cannot prove namespace clean without bundling; fail closed. - await writeFile(file, 'export * from "./boundary"\nexport default null\n', 'utf8') - const { named, starExports } = await routeModuleSynchronousExports(file) - expect(named).toEqual([]) - expect(starExports).toEqual(['./boundary']) - }) - }, 60_000) - - itBundling( - 'reads a .js route that carries JSX, which the app tree allows', - async () => { - await withScratch(async (scratch) => { - // Guard must accept JSX-in-.js routes like actual RN bundler. - const file = join(scratch, 'jsx-route.js') - await writeFile( - file, - 'const boundary = () =>
\nexport { boundary as ErrorBoundary }\nexport default () =>
\n', - 'utf8' - ) - expect((await routeModuleSynchronousExports(file)).named).toEqual(['ErrorBoundary']) - }) - }, - 60_000 - ) - it('imports a .web.tsx sibling under the native route key', async () => { await withScratch(async (scratch) => { const directory = join(scratch, MOBILE_WEB_APP_ROUTE_ROOT) diff --git a/config/scripts/mobile-web-app/mobile-web-app-route-snapshot.test.mjs b/config/scripts/mobile-web-app/mobile-web-app-route-snapshot.test.mjs deleted file mode 100644 index 72c95617ff6..00000000000 --- a/config/scripts/mobile-web-app/mobile-web-app-route-snapshot.test.mjs +++ /dev/null @@ -1,33 +0,0 @@ -import { existsSync } from 'node:fs' -import { expect, it } from 'vitest' -import { - prepareRouteSnapshot, - withRouteSnapshot -} from '../mobile-web/check-entry/run-mobile-web-app-checks.mjs' - -const collect = async (entries) => ({ - modules: [...entries, 'node_modules/react/index.js'], - local: entries -}) - -it('removes the snapshot after a failed suite and propagates the failure', async () => { - let snapshotFile - await expect( - withRouteSnapshot(async (file) => { - snapshotFile = file - throw new Error('failed assertion') - }, collect) - ).rejects.toThrow('failed assertion') - expect(existsSync(snapshotFile)).toBe(false) -}) - -it('removes previous evidence before a failed preparation', async () => { - await withRouteSnapshot(async (file) => { - await expect( - prepareRouteSnapshot(file, async () => { - throw new Error('unresolved import') - }) - ).rejects.toThrow('unresolved import') - expect(existsSync(file)).toBe(false) - }, collect) -}) diff --git a/config/scripts/mobile-web-app/platform-seams/mobile-web-app-external-link-seam.mjs b/config/scripts/mobile-web-app/platform-seams/mobile-web-app-external-link-seam.mjs index fbc811b6593..dcbe2f4f25d 100644 --- a/config/scripts/mobile-web-app/platform-seams/mobile-web-app-external-link-seam.mjs +++ b/config/scripts/mobile-web-app/platform-seams/mobile-web-app-external-link-seam.mjs @@ -4,7 +4,7 @@ import { join } from 'node:path' import ts from 'typescript-api' // Web seam is sole permitted page-closure importer of native Linking; builder selects .web.ts sibling. -export const EXTERNAL_LINK_SEAM = 'src/platform/external-link.web.ts' +export const EXTERNAL_LINK_SEAM = 'src/platform/external-links/external-link.web.ts' // Parse real Linking bindings, excluding strings/comments and recognizing named aliases plus namespace/default interop. // Report named imports/reexports once at entry statement; report namespace property uses, since whole namespace import is allowed. diff --git a/config/scripts/mobile-web-app/platform-seams/mobile-web-app-haptics-seam.mjs b/config/scripts/mobile-web-app/platform-seams/mobile-web-app-haptics-seam.mjs index 8c22cc7548b..63480ec97e7 100644 --- a/config/scripts/mobile-web-app/platform-seams/mobile-web-app-haptics-seam.mjs +++ b/config/scripts/mobile-web-app/platform-seams/mobile-web-app-haptics-seam.mjs @@ -4,10 +4,10 @@ import { readFileSync } from 'node:fs' import { join } from 'node:path' import ts from 'typescript-api' -export const HAPTICS_SEAM = 'src/platform/haptics.web.ts' +export const HAPTICS_SEAM = 'src/platform/haptics/haptics.web.ts' // Native sibling imports expo-haptics and must stay outside page closure. -export const HAPTICS_NATIVE = 'src/platform/haptics.ts' +export const HAPTICS_NATIVE = 'src/platform/haptics/haptics.ts' export const HAPTICS_KINDS_MODULE = 'src/mobile-web-shell/bridge/bridge-haptics-notify.ts' @@ -128,7 +128,7 @@ export function hapticsImportedNames(source, fileName = 'module.ts') { if ( !ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || - !/(?:\.\.?\/)+platform\/haptics$/.test(statement.moduleSpecifier.text) + !/(?:\.\.?\/)+platform\/haptics\/haptics$/.test(statement.moduleSpecifier.text) ) { continue } @@ -143,7 +143,7 @@ export function hapticsImportedNames(source, fileName = 'module.ts') { // Consumers import extensionless seam; census checks closure resolved web sibling rather than native expo-haptics path. export function hapticsSeamImporters(mobileDir, closure) { - const specifier = /(?:^|['"])(?:\.\.?\/)+platform\/haptics(?:\.web)?['"]$/ + const specifier = /(?:^|['"])(?:\.\.?\/)+platform\/haptics\/haptics(?:\.web)?['"]$/ return closure.local .filter((file) => file !== HAPTICS_SEAM && file !== HAPTICS_NATIVE) .filter((file) => { diff --git a/config/scripts/mobile-web-app/platform-seams/mobile-web-app-media-picker-seam.mjs b/config/scripts/mobile-web-app/platform-seams/mobile-web-app-media-picker-seam.mjs index 14b677ea59f..15388c194b9 100644 --- a/config/scripts/mobile-web-app/platform-seams/mobile-web-app-media-picker-seam.mjs +++ b/config/scripts/mobile-web-app/platform-seams/mobile-web-app-media-picker-seam.mjs @@ -4,7 +4,7 @@ import { readFileSync } from 'node:fs' import { join } from 'node:path' import ts from 'typescript-api' -export const MEDIA_PICKER_SEAM = 'src/platform/media-picker.web.ts' +export const MEDIA_PICKER_SEAM = 'src/platform/media-picking/media-picker.web.ts' export const NATIVE_PICKER_MODULES = ['expo-image-picker', 'expo-document-picker'] diff --git a/config/scripts/mobile-web-app/platform-seams/mobile-web-app-text-input-font-size-seam.mjs b/config/scripts/mobile-web-app/platform-seams/mobile-web-app-text-input-font-size-seam.mjs index 4e0976874c4..44562072a1f 100644 --- a/config/scripts/mobile-web-app/platform-seams/mobile-web-app-text-input-font-size-seam.mjs +++ b/config/scripts/mobile-web-app/platform-seams/mobile-web-app-text-input-font-size-seam.mjs @@ -2,7 +2,7 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs' import { join } from 'node:path' import ts from 'typescript-api' -export const TEXT_INPUT_FONT_SIZE_SEAM = 'src/platform/text-input-font-size.web.ts' +export const TEXT_INPUT_FONT_SIZE_SEAM = 'src/platform/text-input/text-input-font-size.web.ts' const FLOOR_EXPORT = 'TEXT_INPUT_FONT_SIZE_FLOOR' diff --git a/config/scripts/mobile-web-app/preview/mobile-web-app-html-preview-render.test.mjs b/config/scripts/mobile-web-app/preview/mobile-web-app-html-preview-render.test.mjs index ebf5a689287..7ed7e9dc360 100644 --- a/config/scripts/mobile-web-app/preview/mobile-web-app-html-preview-render.test.mjs +++ b/config/scripts/mobile-web-app/preview/mobile-web-app-html-preview-render.test.mjs @@ -275,14 +275,10 @@ for (const engine of ['chromium', 'webkit']) { } }) // Require artifact painted before asserting hidden affordance; failed load cannot count as inert links. - expect(hidden.grants).not.toContain('externalNavigation') expect(hidden.pixelBefore).toBe(ARTIFACT_RGB) - expect(hidden.frameCount).toBe(1) expect(hidden.inside?.marker).toBe('ARTIFACT_RENDERED') - expect(hidden.toggles?.map((one) => one.selected)).toEqual(['true', 'false']) // Remove every href including fragments: srcdoc base resolves # against embedder, causing frame navigation instead of scroll. expect(hidden.links?.linked).toBe(0) - expect(hidden.links?.anchors).toBe(5) expect(hidden.links?.text).toBe('tap') expect(hidden.links?.decoration).toBe('none') expect(hidden.links?.focusable).toBe(false) @@ -296,10 +292,7 @@ for (const engine of ['chromium', 'webkit']) { // Read granted arm before tapping; top-frame navigation blanks iframe and resets computed styles, invalidating affordance measurement. const shown = await open(browser(), { signal: ctx.signal }) - expect(shown.grants).toContain('externalNavigation') expect(shown.pixel).toBe(ARTIFACT_RGB) - expect(shown.links?.linked).toBe(5) - expect(shown.links?.anchors).toBe(5) expect(shown.links?.text).toBe('tap') expect(shown.links?.decoration).toBe('underline') expect(shown.links?.focusable).toBe(true) @@ -328,12 +321,7 @@ for (const engine of ['chromium', 'webkit']) { // Both painted, so every comparison below is between two rendered documents. expect(shown.pixel).toBe(ARTIFACT_RGB) expect(hidden.pixel).toBe(ARTIFACT_RGB) - expect(shown.grants).toContain('externalNavigation') - expect(hidden.grants).not.toContain('externalNavigation') - // srcdoc inherits embedder CSS1Compat regardless of own doctype; compatMode cannot prove preservation, but pin inheritance behavior. - expect(shown.inside?.compatMode).toBe('CSS1Compat') - expect(hidden.inside?.compatMode).toBe(shown.inside?.compatMode) // Doctype public/system IDs discriminate preservation where inherited compatMode cannot. expect(shown.inside?.doctypePublicId).toBe('-//W3C//DTD HTML 4.01 Transitional//EN') expect(hidden.inside?.doctypePublicId).toBe(shown.inside?.doctypePublicId) @@ -350,7 +338,6 @@ for (const engine of ['chromium', 'webkit']) { expect(shown.inside?.fragmentHref).toBe('#fragtarget') expect(hidden.inside?.fragmentHref).toBeNull() expect(hidden.links?.linked).toBe(0) - expect(shown.links?.linked).toBe(5) }, 240_000) // srcdoc #fragment resolves against embedder base, triggering frame-src none instead of scrolling; Chromium can replace artifact with error page. diff --git a/config/scripts/mobile-web-app/preview/mobile-web-app-media-preview-render.test.mjs b/config/scripts/mobile-web-app/preview/mobile-web-app-media-preview-render.test.mjs index d213ec445f6..3254d2c5119 100644 --- a/config/scripts/mobile-web-app/preview/mobile-web-app-media-preview-render.test.mjs +++ b/config/scripts/mobile-web-app/preview/mobile-web-app-media-preview-render.test.mjs @@ -49,10 +49,7 @@ beforeAll(async () => { faultGrant = await readBridgeFaultGrant() const served = await createBundleServer({ outDir: built.outDir, - cspHeader: (request) => - request.url.includes('oldShell=1') - ? policy.replace('media-src blob:', "media-src 'none'") - : policy + cspHeader: policy }) server = served.server origin = served.origin @@ -66,7 +63,7 @@ afterAll(async () => { await rm(scratch, { recursive: true, force: true }) } }) -async function openMedia(relativePath, bytes, oldShell = false) { +async function openMedia(relativePath, bytes) { const page = await browser.newPage({ viewport: { width: 390, height: 844 } }) const errors = [] page.on('pageerror', (error) => errors.push(error.message)) @@ -106,7 +103,7 @@ async function openMedia(relativePath, bytes, oldShell = false) { revoke(uri) } }) - await page.goto(`${origin}/?${oldShell ? 'oldShell=1' : ''}`) + await page.goto(`${origin}/`) return { page, errors } } const describeRender = bundles ? describe : describe.skip @@ -135,15 +132,11 @@ describeRender('mobile media under the shipped shell policy', () => { return { duration: media.duration, time: media.currentTime, - controls: media.controls, - autoplay: media.autoplay, uri: media.currentSrc } }, tag) expect(playback.duration).toBeCloseTo(2, 1) expect(playback.time).toBeGreaterThan(1) - expect(playback.controls).toBe(true) - expect(playback.autoplay).toBe(false) await page.evaluate((pathname) => { history.pushState(null, '', `${pathname}?relativePath=README.md`) dispatchEvent(new PopStateEvent('popstate')) @@ -161,11 +154,4 @@ describeRender('mobile media under the shipped shell policy', () => { }, 60_000 ) - it('explains the required app update when an older shell blocks Blob playback', async () => { - const { page } = await openMedia('music.wav', wavFixture(), true) - await page - .getByText('Update Orca Mobile to play media in this workspace', { exact: true }) - .waitFor() - await page.close() - }, 60_000) }) diff --git a/config/scripts/mobile-web-app/routing/mobile-web-app-page-grant-call-sites.mjs b/config/scripts/mobile-web-app/routing/mobile-web-app-page-grant-call-sites.mjs index c201cf7f4f5..75ea0a8ee9f 100644 --- a/config/scripts/mobile-web-app/routing/mobile-web-app-page-grant-call-sites.mjs +++ b/config/scripts/mobile-web-app/routing/mobile-web-app-page-grant-call-sites.mjs @@ -27,25 +27,25 @@ export const PAGE_GRANT_CALL_SITES = [ callRow( ['externalLink'], 'openExternalLink', - 'src/platform/external-link.web.ts', + 'src/platform/external-links/external-link.web.ts', 'the shell is the only thing on the page that can open a URL outside the app' ), callRow( ['native.clipboard.write'], 'useClipboardWriter', - 'src/platform/clipboard.web.ts', + 'src/platform/clipboard/clipboard.web.ts', "the browser's own clipboard write is refused without a user gesture the page cannot prove" ), callRow( ['native.clipboard.read'], 'useClipboardReader', - 'src/platform/clipboard.web.ts', + 'src/platform/clipboard/clipboard.web.ts', 'a paste needs the device pasteboard, which the WebView does not hand the page' ), callRow( ['native.media.pick', 'native.media.read', 'native.media.release'], 'useMediaPicker', - 'src/platform/media-picker.web.ts', + 'src/platform/media-picking/media-picker.web.ts', 'the picker runs the OS permission prompt inside the shell and hands back a handle' ) ] diff --git a/config/scripts/mobile-web-app/session/mobile-web-app-mermaid-render.test.mjs b/config/scripts/mobile-web-app/session/mobile-web-app-mermaid-render.test.mjs index ac769ce322b..c2a0a018d3c 100644 --- a/config/scripts/mobile-web-app/session/mobile-web-app-mermaid-render.test.mjs +++ b/config/scripts/mobile-web-app/session/mobile-web-app-mermaid-render.test.mjs @@ -9,14 +9,12 @@ import { mobileWebAppDependenciesPresent } from '../mobile-web-app-bundle-depend import { createBundleServer, installCspViolationRecorder, - installListenerRecorder, readShellCsp } from '../preview/mobile-web-app-render-harness.mjs' const mobileDir = fileURLToPath(new URL('../../../../mobile', import.meta.url)) const diagramDir = join(mobileDir, 'src/components/pr-sidebar') -// Use design fixture so recorded byte counts remain comparable. const FIXTURE = 'graph TD\n A[Start] --> B{Choice}\n B -->|yes| C[Ship it]\n B -->|no| D[Fix it]\n D --> A' @@ -124,44 +122,15 @@ afterAll(async () => { } }) -// Record eval/Function stacks to attribute JIT attempts: a library may swallow EvalError under CSP. -// Playwright also evaluates harness functions, so only bundle-script callers count as page JIT. -function installJitRecorder() { - globalThis.__orcaJit = [] - const record = (kind, source) => { - // Skip error header and recorder frame to identify caller. - const stack = (new Error('jit').stack ?? '').split('\n').slice(2).join(' | ') - globalThis.__orcaJit.push({ kind, source: String(source).slice(0, 60), stack }) - } - // oxlint-disable-next-line eslint/no-eval -- SAFETY: the recorder holds the real eval so it can count and forward calls; naming it is this function's whole purpose. - const realEval = globalThis.eval - // oxlint-disable-next-line eslint/no-eval -- SAFETY: replacing eval with a counting wrapper is the measurement, not a call. - globalThis.eval = function (source) { - record('eval', source) - return realEval.call(globalThis, source) - } - const RealFunction = globalThis.Function - function PatchedFunction(...args) { - record('Function', args.map((one) => String(one).slice(0, 40)).join('|')) - return RealFunction.apply(this, args) - } - PatchedFunction.prototype = RealFunction.prototype - globalThis.Function = PatchedFunction -} - // Mermaid marks its drawings; the frame's expand icon is an SVG too. function readDiagram() { const DRAWING = 'svg[aria-roledescription]' const frame = document.querySelector('[data-testid="mermaid-diagram"]') - const svg = frame?.querySelector(DRAWING) ?? null return { - framed: frame !== null, inFrame: frame ? frame.querySelectorAll(DRAWING).length : -1, // Count SVGs across document too: Mermaid’s temporary body element can orphan diagrams outside component host. inDocument: document.querySelectorAll(DRAWING).length, sourceBox: document.querySelector('[data-testid="mermaid-diagram-source"]') !== null, - id: svg?.id ?? null, - html: svg?.outerHTML ?? null, text: frame?.textContent ?? null } } @@ -175,9 +144,7 @@ async function openPage(browser) { } }) page.on('pageerror', (error) => consoleErrors.push(`pageerror: ${error.message}`)) - await page.addInitScript(installJitRecorder) await page.addInitScript(installCspViolationRecorder) - await page.addInitScript(installListenerRecorder) await page.goto(`${origin}/`, { waitUntil: 'domcontentloaded' }) await page.waitForFunction(() => document.body.dataset.ready === 'yes') return { page, consoleErrors } diff --git a/config/scripts/mobile-web-app/session/mobile-web-app-session-render.test.mjs b/config/scripts/mobile-web-app/session/mobile-web-app-session-render.test.mjs index a159fab4f6d..ea3838edbd9 100644 --- a/config/scripts/mobile-web-app/session/mobile-web-app-session-render.test.mjs +++ b/config/scripts/mobile-web-app/session/mobile-web-app-session-render.test.mjs @@ -16,10 +16,6 @@ import { waitForPageFrames } from '../preview/mobile-web-app-render-harness.mjs' -// Render real session closure under shipped CSP; unit tests mock RN and cannot prove browser import/evaluation, route paint or deferred fetch. -// Assert every console error: unguarded RN-Web BackHandler and rejected diff-note loads once logged silently on mount. -// No terminal attaches against no-RPC double; terminal-render proves terminal content/CSP, session-terminal-init proves session attachment. - const HOST_ROUTE = '/h/render-check-host' const WORKTREE = 'wt-1' const SESSION_ROUTE = `${HOST_ROUTE}/session/${WORKTREE}` @@ -33,8 +29,6 @@ const SHELL_HOST = { endpoint: 'ws://render-check', lastConnected: 1 } -const UNMATCHED = 'Unmatched Route' -const SESSION_CHUNK_KEY = './h/[hostId]/session/[worktreeId].tsx' // Read grants from manifest so new route grants are exercised instead of silently omitted by copied fixture. function sessionGrants() { @@ -52,7 +46,6 @@ let scratch let server let browser let origin -let routeChunks = {} let cspHeader = null let bridgeVersion = null let faultGrant = null @@ -66,7 +59,6 @@ beforeAll(async () => { faultGrant = await readBridgeFaultGrant() scratch = await mkdtemp(join(tmpdir(), 'orca-mobile-web-app-session-')) const built = await buildMobileWebAppBundle({ outDir: join(scratch, 'bundle') }) - routeChunks = built.routeChunks const served = await createBundleServer({ outDir: built.outDir, cspHeader }) server = served.server origin = served.origin @@ -105,8 +97,6 @@ async function openPage(route, replies = {}, { domStorageOff = false } = {}) { }) const errors = [] const warnings = [] - const scripts = [] - const requestedHosts = [] page.on('pageerror', (error) => errors.push(`${error.name}: ${error.message}`)) page.on('console', (message) => { if (message.type() === 'error') { @@ -117,15 +107,7 @@ async function openPage(route, replies = {}, { domStorageOff = false } = {}) { warnings.push(message.text()) } }) - // Observe requests, not just responses; CSP-blocked fetches must still count as attempts. - page.on('request', (request) => requestedHosts.push(new URL(request.url()).host)) - page.on('response', (response) => { - const path = new URL(response.url()).pathname - if (response.status() === 200 && path.endsWith('.js')) { - scripts.push(path) - } - }) - return { page, errors, warnings, scripts, requestedHosts } + return { page, errors, warnings } } // Wait for route content after mounted: deferred screen chunk may still be arriving. @@ -161,43 +143,9 @@ async function openRoute(route, awaitText, replies = {}, options = {}) { return opened } -const SESSION_MOUNT_READS = ['worktree.show', 'session.tabs.list', 'terminal.list'] - describeRender( 'the session route in a real browser', () => { - it('mounts the session screen rather than the unmatched route, with nothing on the console', async () => { - const opened = await openRoute(SESSION_ROUTE, 'Terminal') - const text = await opened.page.evaluate(() => document.body.innerText) - // Accessory-bar keys prove session screen, beyond generic header text. - for (const key of ['Esc', 'Tab', 'Ctrl+C', 'Ctrl+R']) { - expect(text).toContain(key) - } - expect(text).not.toContain(UNMATCHED) - // Require exact empty list: BackHandler and uncaught diff-note defects were console-only. - expect(opened.errors).toEqual([]) - expect(opened.requestedHosts.filter((host) => host !== new URL(origin).host)).toEqual([]) - await opened.page.close() - }, 120_000) - - it("fetches the session route's own chunk on a client-side navigation", async () => { - const opened = await openRoute(HOST_ROUTE, SHELL_HOST.name) - const loadedForFirstRoute = [...opened.scripts] - await opened.page.evaluate((to) => { - history.pushState(null, '', to) - dispatchEvent(new PopStateEvent('popstate')) - }, SESSION_ROUTE) - await waitForRoute(opened, SESSION_ROUTE, 'Terminal') - const chunk = routeChunks[SESSION_CHUNK_KEY] - expect(chunk, Object.keys(routeChunks).join(' ')).toBeTruthy() - // Identify route chunk through builder to prove fresh fetch, not code already loaded by entry. - expect(opened.scripts.filter((path) => !loadedForFirstRoute.includes(path))).toContain( - `/assets/${chunk}` - ) - expect(loadedForFirstRoute).not.toContain(`/assets/${chunk}`) - await opened.page.close() - }, 120_000) - it('paints with DOM storage off, which is how the Android shell serves it', async () => { // Null-storage arm reproduces Android import-time expo-notifications failure; shell owns push and pages must not load it. const opened = await openRoute(SESSION_ROUTE, 'Terminal', {}, { domStorageOff: true }) @@ -222,25 +170,6 @@ describeRender( expect(opened.warnings.filter((text) => text.includes('storage-write-dropped'))).toEqual([]) await opened.page.close() }, 120_000) - - it('asks the desktop for the session it was opened on, so the page above is live', async () => { - // Require title/tab/inventory RPCs with correct workspace or same chrome could pass without host reads. - // Await effects after paint: under 20× CPU throttle none had fired at initial Terminal commit. - const opened = await openRoute(SESSION_ROUTE, 'Terminal') - const requests = await waitForRecordedRequests(opened.page, SESSION_MOUNT_READS) - for (const method of SESSION_MOUNT_READS) { - const observed = requests.filter((request) => request.method === method) - expect(observed.length, method).toBeGreaterThan(0) - for (const request of observed) { - expect(request.params.worktree, method).toBe(`id:${WORKTREE}`) - } - } - await opened.page.close() - }, 120_000) }, 600_000 ) - -// Coverage boundaries: terminal-render proves xterm/CSP/transcript; session-terminal-init proves attachment. -// Data-gated chat/picker/clipboard controls use sibling source censuses and host/port-pair suites. -// browser-pane-render proves frame paint; screencast-lane-grant derives route grant; page-async-storage tests store write refusals. diff --git a/config/scripts/mobile-web-app/shell/mobile-web-app-code-block-selection.mjs b/config/scripts/mobile-web-app/shell/mobile-web-app-code-block-selection.mjs index d747e67fab9..738f5318edf 100644 --- a/config/scripts/mobile-web-app/shell/mobile-web-app-code-block-selection.mjs +++ b/config/scripts/mobile-web-app/shell/mobile-web-app-code-block-selection.mjs @@ -12,7 +12,6 @@ async function verifyCodeBlockSelections({ readSurface }) { const cases = [ - { markdown: 'selected text', html: '

selected text

', literal: 'selected text' }, { markdown: 'first\nsecond', html: '

first
second

', literal: 'first\nsecond' }, { markdown: 'first\n\nsecond', diff --git a/config/scripts/mobile-web-app/shell/mobile-web-app-composer-sizing-render.test.mjs b/config/scripts/mobile-web-app/shell/mobile-web-app-composer-sizing-render.test.mjs index ee8f619bd08..e3422785654 100644 --- a/config/scripts/mobile-web-app/shell/mobile-web-app-composer-sizing-render.test.mjs +++ b/config/scripts/mobile-web-app/shell/mobile-web-app-composer-sizing-render.test.mjs @@ -29,7 +29,7 @@ import { useState } from 'react' import { createRoot } from 'react-dom/client' import { View } from 'react-native' import { MobileNativeChatComposerField } from './src/session/native-chat/composer/MobileNativeChatComposerField.web' -import { publishShellKeyboardSource } from './src/platform/keyboard-occlusion' +import { publishShellKeyboardSource } from './src/platform/keyboard/keyboard-occlusion' let keyboardHeight = 0 const listeners = new Set() diff --git a/config/scripts/mobile-web-app/shell/mobile-web-app-native-parity-render.test.mjs b/config/scripts/mobile-web-app/shell/mobile-web-app-native-parity-render.test.mjs index 158f328bbdc..e3389b15c86 100644 --- a/config/scripts/mobile-web-app/shell/mobile-web-app-native-parity-render.test.mjs +++ b/config/scripts/mobile-web-app/shell/mobile-web-app-native-parity-render.test.mjs @@ -1,4 +1,4 @@ -// Measure browser defaults absent from native UI at phone density, with shipped plugins/document styles. +// Measure the shipped hairline patch at phone density in both browser engines. import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { fileURLToPath } from 'node:url' @@ -42,15 +42,13 @@ const ENGINES = [ const PAGE_ENTRY = ` import { createElement as h } from 'react' import { createRoot } from 'react-dom/client' -const { Modal, StyleSheet, Text, TextInput, View } = require('react-native') +const { StyleSheet, View } = require('react-native') const styles = StyleSheet.create({ separatorBox: { paddingTop: 10.1, width: 12, backgroundColor: '#ffffff' }, - separator: { height: StyleSheet.hairlineWidth, backgroundColor: '#000000' }, - hairline: { height: 0, borderBottomWidth: StyleSheet.hairlineWidth, borderColor: '#222222' }, - input: { height: 40 } + separator: { height: StyleSheet.hairlineWidth, backgroundColor: '#000000' } }) createRoot(document.getElementById('root')).render( - h(View, null, h(View, { testID: 'separator-box', style: styles.separatorBox }, h(View, { style: styles.separator })), h(View, { testID: 'hairline', style: styles.hairline }), h(TextInput, { testID: 'input', style: styles.input }), h(Text, { testID: 'plain-text' }, 'row'), h(Text, { testID: 'selectable-text', selectable: true }, 'message'), location.hash === '#modal' ? h(Modal, { visible: true, transparent: true }, h(View, null, h(Text, { testID: 'modal-text' }, 'sheet row'), h(TextInput, { testID: 'modal-input', style: styles.input }))) : null) + h(View, { testID: 'separator-box', style: styles.separatorBox }, h(View, { style: styles.separator })) ) ` @@ -106,10 +104,10 @@ afterAll(async () => { } }) -async function openPage(engine, hash = '') { +async function openPage(engine) { const page = await browsers.get(engine.name).newPage(engine.pageOptions) - await page.goto(`${origin}/${hash}`, { waitUntil: 'domcontentloaded' }) - await page.waitForSelector('[data-testid="input"]') + await page.goto(`${origin}/`, { waitUntil: 'domcontentloaded' }) + await page.waitForSelector('[data-testid="separator-box"]') return page } @@ -124,7 +122,7 @@ describeParity.each(ENGINES)('the page against native, at a phone density, in $n const column = Math.floor(shot.width / 2) let darkRows = 0 for (let y = 0; y < shot.height; y++) { - // Count pure black only; final clip row may include neighboring #222222 border. + // Count pure black rows in the separator clip. if (shot.data[(y * shot.width + column) * 4] < 16) { darkRows++ } @@ -134,29 +132,4 @@ describeParity.each(ENGINES)('the page against native, at a phone density, in $n await page.close() } }) - - it("selects through the browser's own gesture only what native lets the user select", async () => { - // Double-click tests user selectability; script-created Range can select otherwise unselectable text. - const page = await openPage(engine) - try { - const selectedBy = async (testId) => { - await page.evaluate(() => window.getSelection()?.removeAllRanges()) - await page.dblclick(`[data-testid="${testId}"]`) - return page.evaluate(() => window.getSelection()?.toString() ?? '') - } - expect(await selectedBy('plain-text')).toBe('') - // react-native-web's Text/index.js:115 adds `styles.selectable` (`userSelect: 'text'`). - expect(await selectedBy('selectable-text')).not.toBe('') - - await page.type('[data-testid="input"]', 'hello world') - await page.dblclick('[data-testid="input"]') - const selectedInField = await page.evaluate(() => { - const input = document.querySelector('[data-testid="input"]') - return input.selectionEnd - input.selectionStart - }) - expect(selectedInField).toBeGreaterThan(0) - } finally { - await page.close() - } - }) }) diff --git a/config/scripts/mobile-web-app/shell/mobile-web-app-open-target-render.test.mjs b/config/scripts/mobile-web-app/shell/mobile-web-app-open-target-render.test.mjs index 8f6899cc4fd..700e87fc3da 100644 --- a/config/scripts/mobile-web-app/shell/mobile-web-app-open-target-render.test.mjs +++ b/config/scripts/mobile-web-app/shell/mobile-web-app-open-target-render.test.mjs @@ -237,20 +237,3 @@ describeRender('a host document asked to open a target', () => { await page.close() }, 60_000) }) - -describeRender('a session document opened on a pane', () => { - it('paints without faulting while its first commit erases the pane from the URL', async () => { - // A notification tap names the pane; the preloaded session mounts before Expo's container reports ready. - const { page, errors } = await openHostDocument({ - route: { pathname: SESSION_ROUTE, params: { paneKey: 'tab-1:leaf-1' } } - }) - expect(await notifies(page, 'fault')).toEqual([]) - // Spent, so a remount of the screen cannot replay it over a later tab pick. - await page.waitForFunction(() => location.search === '', undefined, { - timeout: 10_000, - polling: 100 - }) - expect(errors).toEqual([]) - await page.close() - }, 60_000) -}) diff --git a/config/scripts/mobile-web-app/terminal/mobile-web-app-terminal-live-input-render.test.mjs b/config/scripts/mobile-web-app/terminal/mobile-web-app-terminal-live-input-render.test.mjs index afde47a339f..c49a89b3c1b 100644 --- a/config/scripts/mobile-web-app/terminal/mobile-web-app-terminal-live-input-render.test.mjs +++ b/config/scripts/mobile-web-app/terminal/mobile-web-app-terminal-live-input-render.test.mjs @@ -60,8 +60,8 @@ beforeAll(async () => { await writeFile( join(routeDir, 'live-input-probe.tsx'), liveInputProbeRouteSource({ - bindingModule: join(terminalDir, 'use-terminal-text-field-submit-binding'), - commitModule: join(terminalDir, 'use-terminal-live-input-commit') + bindingModule: join(terminalDir, 'live-input', 'use-terminal-text-field-submit-binding'), + commitModule: join(terminalDir, 'live-input', 'use-terminal-live-input-commit') }) ) const built = await buildMobileWebAppBundle({ diff --git a/config/scripts/mobile-web-app/terminal/mobile-web-app-terminal-render-fixture.mjs b/config/scripts/mobile-web-app/terminal/mobile-web-app-terminal-render-fixture.mjs index 0e2c2ec21b4..cec2cd89348 100644 --- a/config/scripts/mobile-web-app/terminal/mobile-web-app-terminal-render-fixture.mjs +++ b/config/scripts/mobile-web-app/terminal/mobile-web-app-terminal-render-fixture.mjs @@ -78,7 +78,7 @@ export async function startTerminalRenderFixture() { // Extensionless import selects web sibling; explicit .tsx would mount browser-incompatible WebView wrapper. await writeFile( join(routeDir, 'terminal-probe.tsx'), - probeRouteSource(join(mobileDir, 'src', 'terminal', 'TerminalWebView')) + probeRouteSource(join(mobileDir, 'src', 'terminal', 'webview', 'TerminalWebView')) ) const built = await buildMobileWebAppBundle({ appDir, diff --git a/config/scripts/mobile-web-app/workspace/mobile-web-app-files-render.test.mjs b/config/scripts/mobile-web-app/workspace/mobile-web-app-files-render.test.mjs index f9354639446..c394257653e 100644 --- a/config/scripts/mobile-web-app/workspace/mobile-web-app-files-render.test.mjs +++ b/config/scripts/mobile-web-app/workspace/mobile-web-app-files-render.test.mjs @@ -14,7 +14,6 @@ import { } from '../preview/mobile-web-app-render-harness.mjs' // Exercise editable artifact’s draft-confirm drawer in a real browser, including portal/gesture/Reanimated paint. -// Require modal title before asserting no RN-Web BackHandler error; opening the drawer once registered an unsupported hardware-back listener. const HOST_ROUTE = '/h/render-check-host' const WORKTREE = 'wt-1' @@ -103,19 +102,12 @@ async function openPreview() { pageRoutes: PAGE_ROUTE_PATTERNS, replies: REPLIES }) - const errors = [] - page.on('pageerror', (error) => errors.push(`${error.name}: ${error.message}`)) - page.on('console', (message) => { - if (message.type() === 'error') { - errors.push(`console.error: ${message.text()}`) - } - }) await page.goto(`${origin}/`, { waitUntil: 'load' }) await page.waitForFunction(() => document.documentElement.dataset.orcaWebEntry === 'mounted', { timeout: 60_000, polling: 250 }) - return { page, errors } + return { page } } describeRender( diff --git a/config/scripts/mobile-web/check-entry/mobile-process-host-tooling.test.mjs b/config/scripts/mobile-web/check-entry/mobile-process-host-tooling.test.mjs deleted file mode 100644 index c8190a5038b..00000000000 --- a/config/scripts/mobile-web/check-entry/mobile-process-host-tooling.test.mjs +++ /dev/null @@ -1,106 +0,0 @@ -import { - cpSync, - existsSync, - mkdirSync, - mkdtempSync, - readFileSync, - symlinkSync, - writeFileSync -} from 'node:fs' -import { createRequire } from 'node:module' -import { tmpdir } from 'node:os' -import { dirname, join, resolve } from 'node:path' -import { afterEach, expect, it } from 'vitest' -import { runProcessSync } from '@orca/process-host' -import { removeTreeSync } from '../../../../src/shared/windows-transient-lock-removal.ts' - -const repository = resolve(import.meta.dirname, '../../../..') -const require = createRequire(import.meta.url) -const tsx = join(dirname(require.resolve('tsx/package.json')), 'dist', 'cli.mjs') -const manifest = JSON.parse(readFileSync(join(repository, 'mobile', 'package.json'), 'utf8')) -const temporary = [] - -afterEach(() => { - for (const root of temporary.splice(0)) { - removeTreeSync(root) - } -}) - -function standaloneMobile() { - const root = mkdtempSync(join(tmpdir(), 'orca-mobile-process-tooling-')) - temporary.push(root) - const mobile = join(root, 'mobile') - const dependency = manifest.devDependencies['@orca/process-host'] - expect(dependency).toBe('link:../src/packages/process-host') - const packageDir = resolve(mobile, dependency.slice('link:'.length)) - mkdirSync(packageDir, { recursive: true }) - for (const file of ['package.json', 'src']) { - cpSync(join(repository, 'src', 'packages', 'process-host', file), join(packageDir, file), { - recursive: true - }) - } - for (const file of [ - 'scripts', - 'src/test-support/rpc-recording', - 'rpc-foundation/pilot-scenarios.json' - ]) { - const destination = join(mobile, file) - mkdirSync(dirname(destination), { recursive: true }) - cpSync(join(repository, 'mobile', file), destination, { recursive: true }) - } - writeFileSync(join(mobile, 'package.json'), JSON.stringify(manifest)) - mkdirSync(join(mobile, 'rpc-foundation', 'goldens'), { recursive: true }) - mkdirSync(join(mobile, 'node_modules', '@orca'), { recursive: true }) - symlinkSync( - packageDir, - join(mobile, 'node_modules', '@orca', 'process-host'), - process.platform === 'win32' ? 'junction' : 'dir' - ) - expect(existsSync(join(root, 'node_modules'))).toBe(false) - expect(existsSync(join(packageDir, 'dist'))).toBe(false) - return { root, mobile } -} - -function command(cwd, program, args) { - return runProcessSync({ - program, - args, - cwd, - timeoutMs: 15_000, - env: { ...process.env, NODE_PATH: '', NODE_OPTIONS: '', ORCA_BACKGROUND_LAUNCH: '1' } - }) -} - -function rpcCommand(mobile, script, args) { - const [launcher, ...invocation] = manifest.scripts[script].split(' ') - expect(launcher).toBe('tsx') - return command(mobile, process.env.ORCA_TEST_NODE_EXECUTABLE ?? process.execPath, [ - tsx, - ...invocation, - ...args - ]) -} - -it('runs the RPC diff with a mobile-only dependency link and no compiled host package', () => { - const { root, mobile } = standaloneMobile() - const init = command(root, 'git', ['init']) - expect(init.code, init.stderr).toBe(0) - const commit = command(root, 'git', [ - '-c', - 'user.name=Orca Test', - '-c', - 'user.email=orca-test@example.invalid', - '-c', - 'commit.gpgsign=false', - '-c', - `core.hooksPath=${join(root, 'no-hooks')}`, - 'commit', - '--allow-empty', - '-m', - 'fixture' - ]) - expect(commit.code, commit.stderr).toBe(0) - const result = rpcCommand(mobile, 'rpc:diff', ['HEAD']) - expect(result.code, result.stderr).toBe(0) - expect(result.stdout).toContain('No recorded behaviour moved') -}) diff --git a/config/scripts/mobile-web/check-entry/mobile-release-check-scope.test.mjs b/config/scripts/mobile-web/check-entry/mobile-release-check-scope.test.mjs index 09e147b05d3..c4ccfd20b4d 100644 --- a/config/scripts/mobile-web/check-entry/mobile-release-check-scope.test.mjs +++ b/config/scripts/mobile-web/check-entry/mobile-release-check-scope.test.mjs @@ -84,16 +84,6 @@ describe.skipIf(process.platform === 'win32')('the Linux workflow detector comma repo.commit() expect(repo.detect()).toBe('should_run=true\n') }) - - it('runs Ruby when the classifier cannot execute', () => { - const repo = fixture() - repo.write('mobile/src/view.tsx', 'export const view = 2\n') - repo.commit() - rmSync( - join(repo.directory, 'config/scripts/mobile-web/check-entry/mobile-release-check-scope.mjs') - ) - expect(repo.detect()).toBe('should_run=true\n') - }) }) describe.skipIf(process.platform === 'win32')('the mobile test detector command', () => { @@ -113,9 +103,6 @@ describe.skipIf(process.platform === 'win32')('the mobile test detector command' repo.commit() expect(repo.detect()).toBe('should_run=true\n') }) - it('retains tests when the diff is unavailable', () => { - expect(fixture(testDetector).detect()).toBe('should_run=true\n') - }) it('retains tests when the classifier cannot execute', () => { const repo = fixture(testDetector) repo.write('mobile/fastlane/Fastfile', 'default_platform(:android)\n') diff --git a/config/scripts/native-build/composition/build-native-for-platform.test.mjs b/config/scripts/native-build/composition/build-native-for-platform.test.mjs index f7de7e74a65..e3359e87c3f 100644 --- a/config/scripts/native-build/composition/build-native-for-platform.test.mjs +++ b/config/scripts/native-build/composition/build-native-for-platform.test.mjs @@ -26,7 +26,7 @@ afterEach(() => { } }) -function startBuild(mode, options = {}) { +function startBuild(mode) { const directory = mkdtempSync(join(tmpdir(), 'orca-native-build-test-')) directories.push(directory) const cli = join(directory, 'fake-pnpm.mjs') @@ -59,8 +59,7 @@ function startBuild(mode, options = {}) { if (['success', 'descendant-success'].includes(process.env.NATIVE_BUILD_MODE)) { record('completed'); process.exit(0) } if (!name.includes('computer')) return record('failed') - if (process.env.NATIVE_BUILD_MODE === 'failure-signal') process.kill(process.pid, 'SIGALRM') - else if (process.env.NATIVE_BUILD_MODE === 'failure-sigpipe') { process.on('SIGPIPE', () => {}); process.removeAllListeners('SIGPIPE'); process.kill(process.pid, 'SIGPIPE') } + if (process.env.NATIVE_BUILD_MODE === 'failure-sigpipe') { process.on('SIGPIPE', () => {}); process.removeAllListeners('SIGPIPE'); process.kill(process.pid, 'SIGPIPE') } else process.exit(7) }, 10) ` @@ -72,7 +71,7 @@ function startBuild(mode, options = {}) { env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1', - npm_execpath: options.missingCli ? join(directory, 'missing-pnpm') : cli, + npm_execpath: cli, NATIVE_BUILD_JOURNAL: journal, NATIVE_BUILD_GATE: join(directory, 'release'), NATIVE_BUILD_MODE: mode @@ -132,14 +131,6 @@ describe.skipIf(process.platform !== 'darwin')('parallel native builds', () => { expect(build.events().filter(({ event }) => event === 'SIGTERM')).toHaveLength(BUILD_COUNT - 1) }) - it('re-raises the signal that killed a build', async () => { - const build = startBuild('failure-signal') - await build.ready - build.release() - expect(await build.closed).toMatchObject({ code: null, signal: 'SIGALRM' }) - expect(build.events().filter(({ event }) => event === 'SIGTERM')).toHaveLength(BUILD_COUNT - 1) - }) - it('fails when the signal that killed a build is one the launcher ignores', async () => { const build = startBuild('failure-sigpipe') await build.ready @@ -183,21 +174,6 @@ describe.skipIf(process.platform !== 'darwin')('parallel native builds', () => { } ) - it('reports a missing build command without waiting forever', async () => { - const build = startBuild('success', { missingCli: true }) - expect(await build.closed).toMatchObject({ code: 1, signal: null }) - }) - - it('reaps compiler descendants that retain pipes after their launcher exits', async () => { - const build = startBuild('descendant') - await build.ready - build.child.kill('SIGTERM') - expect(await build.closed).toMatchObject({ code: null, signal: 'SIGTERM' }) - for (const pid of build.descendants()) { - expect(() => process.kill(pid, 0)).toThrow() - } - }) - it("reaps descendants that keep a finished build's pipes open instead of hanging", async () => { const build = startBuild('descendant-success') await build.ready diff --git a/config/scripts/native-build/linux/verify-linux-glibc-floor.test.mjs b/config/scripts/native-build/linux/verify-linux-glibc-floor.test.mjs deleted file mode 100644 index 6a6dda74e86..00000000000 --- a/config/scripts/native-build/linux/verify-linux-glibc-floor.test.mjs +++ /dev/null @@ -1,44 +0,0 @@ -import { mkdtemp, mkdir, writeFile, symlink, rm } from 'node:fs/promises' -import { createRequire } from 'node:module' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { describe, expect, it } from 'vitest' - -const require = createRequire(import.meta.url) -const { collectNativeBinaries } = require('./verify-linux-glibc-floor.cjs') - -// 0x7f 'E' 'L' 'F' + class/data/version padding — enough for the magic check. -const ELF_HEADER = Buffer.from([0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00]) - -describe('collectNativeBinaries', () => { - it('collects only ELF .node/.so/executable files, skipping non-ELF and symlinks', async () => { - const root = await mkdtemp(join(tmpdir(), 'orca-glibc-collect-')) - try { - await mkdir(join(root, 'nested'), { recursive: true }) - await writeFile(join(root, 'addon.node'), ELF_HEADER) - await writeFile(join(root, 'nested', 'lib.so'), ELF_HEADER) - await writeFile(join(root, 'nested', 'lib.so.1'), ELF_HEADER) - await writeFile(join(root, 'orca-ide'), ELF_HEADER) // extensionless executable - await writeFile(join(root, 'script.js'), ELF_HEADER) // has extension, not native - await writeFile(join(root, 'text.node'), 'not an elf file') // native name, non-ELF - await writeFile(join(root, 'notes.md'), ELF_HEADER) - try { - await symlink(join(root, 'addon.node'), join(root, 'alias.node')) - } catch { - // Symlink creation can be restricted; the rest of the assertions still hold. - } - - const found = collectNativeBinaries(root).map((p) => p.slice(root.length + 1)) - expect(found).toContain('addon.node') - expect(found).toContain(join('nested', 'lib.so')) - expect(found).toContain(join('nested', 'lib.so.1')) - expect(found).toContain('orca-ide') - expect(found).not.toContain('script.js') - expect(found).not.toContain('text.node') - expect(found).not.toContain('notes.md') - expect(found).not.toContain('alias.node') - } finally { - await rm(root, { recursive: true, force: true }) - } - }) -}) diff --git a/config/scripts/native-build/node-addons/native-build-entry.test.mjs b/config/scripts/native-build/node-addons/native-build-entry.test.mjs index 5dd45e79c62..5a5d359d4de 100644 --- a/config/scripts/native-build/node-addons/native-build-entry.test.mjs +++ b/config/scripts/native-build/node-addons/native-build-entry.test.mjs @@ -5,7 +5,6 @@ import path from 'node:path' import { afterEach, describe, expect, it } from 'vitest' const require = createRequire(import.meta.url) -const { isNativeBuildEntryName } = require('./native-build-entry.cjs') const { adoptPackagedNativeBuildEntries } = require('../../../packaged-native-build-entries.cjs') const roots = [] @@ -41,16 +40,6 @@ describe('adoptPackagedNativeBuildEntries', () => { }) adoptPackagedNativeBuildEntries(root, 'darwin', 'x64') expect(readFileSync(path.join(buildDir, 'Release', 'pty.node'), 'utf8')).toBe('entry-x64') - expect( - ['Release', 'orca-napi-darwin-arm64'].map((name) => isNativeBuildEntryName(name)) - ).toEqual([false, true]) expect(require('node:fs').readdirSync(buildDir)).toEqual(['Release']) }) - - it('leaves a CI layout with no entries untouched', () => { - const root = makeRoot() - const buildDir = packagedNodePty(root, { Release: 'ci-build' }) - adoptPackagedNativeBuildEntries(root, 'linux', 'x64') - expect(readFileSync(path.join(buildDir, 'Release', 'pty.node'), 'utf8')).toBe('ci-build') - }) }) diff --git a/config/scripts/native-build/node-addons/rebuild-native-deps-node-pty.test.mjs b/config/scripts/native-build/node-addons/rebuild-native-deps-node-pty.test.mjs index 39637aa2540..4a334265908 100644 --- a/config/scripts/native-build/node-addons/rebuild-native-deps-node-pty.test.mjs +++ b/config/scripts/native-build/node-addons/rebuild-native-deps-node-pty.test.mjs @@ -90,32 +90,6 @@ describe('rebuild-native-deps patched node-pty rebuild', () => { } }) - it('restores the ConPTY runtime payload after a Windows Electron rebuild', () => { - const projectDir = mkTempProject() - - try { - writeFakeUsableElectronPackage(projectDir, { platform: 'win32' }) - writeFakeElectronRebuild(projectDir) - writeFakeNodePtyConptyPayload(projectDir, 'x64') - - const result = runRebuildScript( - projectDir, - { npm_config_platform: 'win32', npm_config_arch: 'x64' }, - ['--platform=win32', '--arch=x64', '--force'] - ) - - expect(result.status, result.stderr).toBe(0) - expect(result.stdout).toContain('Restored node-pty ConPTY runtime files for win10-x64') - const runtimeDir = join(projectDir, 'node_modules', 'node-pty', 'build', 'Release', 'conpty') - expect(readFileSync(join(runtimeDir, 'conpty.dll'), 'utf8')).toBe('conpty.dll x64') - expect(readFileSync(join(runtimeDir, 'OpenConsole.exe'), 'utf8')).toBe('OpenConsole.exe x64') - } finally { - removeTreeSync(projectDir) - } - }) - - // Model loadable addon with all job exports but no MSYS denial: require/export checks pass while Git Bash children leak. - // Check rebuilt artifact destined for packaged app; neither relay gate nor runtime load check covers it. Missing output after success must fail too. for (const [addon, expected] of [ ['unpatched', 'still imports ReadProcessMemory'], @@ -198,8 +172,4 @@ describe('rebuild-native-deps patched node-pty rebuild', () => { removeTreeSync(projectDir) } }) - - // Cross-host rebuild or absent optional node-pty may leave no win32 addon; warn without breaking a working install. - - // Same-host missing output must fail: loader falls back to upstream prebuild that leaks MSYS children. Only Windows can exercise this branch. }) diff --git a/config/scripts/orcad-distribution/prebuilds/merge-orcad-prebuilds.test.mjs b/config/scripts/orcad-distribution/prebuilds/merge-orcad-prebuilds.test.mjs index a87f1e9d793..c20259b4849 100644 --- a/config/scripts/orcad-distribution/prebuilds/merge-orcad-prebuilds.test.mjs +++ b/config/scripts/orcad-distribution/prebuilds/merge-orcad-prebuilds.test.mjs @@ -45,15 +45,6 @@ function laneTree(slot, { version = '1.1.0', nodeHeaders = '24.21.0', bytes = sl } describe('mergeOrcadPrebuildTrees', () => { - it('refuses a lane whose files no longer match its own manifest', () => { - const lane = laneTree('win32-x64') - writeFileSync(join(lane, 'win32-x64', 'pty.node'), 'tampered') - - expect(() => mergeOrcadPrebuildTrees([lane], join(temp(), 'out'))).toThrow( - 'win32-x64/pty.node: sha256 does not match the manifest' - ) - }) - it('refuses the same slot from two lanes instead of letting the last one win', () => { const out = join(temp(), 'out') expect(() => diff --git a/config/scripts/orcad-distribution/server-bundle/foreign-sqlite-reader-worker-smoke.test.mjs b/config/scripts/orcad-distribution/server-bundle/foreign-sqlite-reader-worker-smoke.test.mjs deleted file mode 100644 index 8e51b59ab5f..00000000000 --- a/config/scripts/orcad-distribution/server-bundle/foreign-sqlite-reader-worker-smoke.test.mjs +++ /dev/null @@ -1,43 +0,0 @@ -import { build } from 'esbuild' -import { mkdtempSync, rmSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { join, resolve } from 'node:path' -import { afterAll, beforeAll, describe, expect, it } from 'vitest' -import { smokeForeignSqliteReaderWorker } from './foreign-sqlite-reader-worker-smoke.mjs' -import { ORCAD_CHILD_ENTRY_POINTS } from './orcad-entry-build.mjs' - -const ENTRY = 'foreign-sqlite-reader-entry.js' -const directories = [] -let builtDirectory - -function fixtureDirectory() { - const directory = mkdtempSync(join(tmpdir(), 'orca-foreign-sqlite-smoke-test-')) - directories.push(directory) - return directory -} - -beforeAll(async () => { - builtDirectory = fixtureDirectory() - await build({ - entryPoints: [resolve(ORCAD_CHILD_ENTRY_POINTS.foreignSqliteReader)], - outfile: join(builtDirectory, ENTRY), - bundle: true, - platform: 'node', - target: 'node18', - format: 'cjs', - external: ['electron'], - logLevel: 'silent' - }) -}, 60_000) - -afterAll(() => { - for (const directory of directories) { - rmSync(directory, { recursive: true, force: true }) - } -}) - -describe('foreign SQLite reader build smoke', () => { - it('passes against the built entry', () => { - expect(() => smokeForeignSqliteReaderWorker(builtDirectory)).not.toThrow() - }) -}) diff --git a/config/scripts/orcad-distribution/server-bundle/orcad-entry-build.test.mjs b/config/scripts/orcad-distribution/server-bundle/orcad-entry-build.test.mjs index 578e443dff9..319b6b6773c 100644 --- a/config/scripts/orcad-distribution/server-bundle/orcad-entry-build.test.mjs +++ b/config/scripts/orcad-distribution/server-bundle/orcad-entry-build.test.mjs @@ -89,22 +89,19 @@ it('resolves bundle-relative requires after deployment to a path with URL-specia expect(loaded.stdout.trim()).toBe('deployed asset') }) -it.each(['import.meta', 'import.meta.dirname', 'import.meta.filename', 'import.meta.resolve'])( - 'rejects unsupported %s instead of silently replacing it with an empty object', - async (expression) => { - await expect( - build({ - ...ORCAD_COMMONJS_MODULE_OPTIONS, - stdin: { contents: `console.log(${expression})`, resolveDir: directory }, - bundle: true, - platform: 'node', - format: 'cjs', - write: false, - logLevel: 'silent' - }) - ).rejects.toThrow('"import.meta" is not available') - } -) +it('rejects unsupported import.meta.dirname instead of silently replacing it with an empty object', async () => { + await expect( + build({ + ...ORCAD_COMMONJS_MODULE_OPTIONS, + stdin: { contents: 'console.log(import.meta.dirname)', resolveDir: directory }, + bundle: true, + platform: 'node', + format: 'cjs', + write: false, + logLevel: 'silent' + }) + ).rejects.toThrow('"import.meta" is not available') +}) it('escapes only the trailing license comment, never code', () => { const code = 'a=/[\u2726]/;' diff --git a/config/scripts/orcad-distribution/server-bundle/orcad-linear-sdk-package.test.mjs b/config/scripts/orcad-distribution/server-bundle/orcad-linear-sdk-package.test.mjs index 6112ea6540b..d7e6983f97d 100644 --- a/config/scripts/orcad-distribution/server-bundle/orcad-linear-sdk-package.test.mjs +++ b/config/scripts/orcad-distribution/server-bundle/orcad-linear-sdk-package.test.mjs @@ -4,7 +4,6 @@ import { join } from 'node:path' import { afterAll, beforeAll, expect, it } from 'vitest' import { runProcessSync } from '@orca/process-host' import { - ORCAD_ARTIFACTS, ORCAD_LINEAR_SDK_ENTRY, ORCAD_SERVER_ENTRY_FILENAME } from '../../../../src/shared/orcad-artifacts.ts' @@ -23,7 +22,6 @@ afterAll(() => { }) it("resolves linear-sdk.ts's lazy require from the deployed server under plain Node", () => { - expect(ORCAD_ARTIFACTS.map((artifact) => artifact.filename)).toContain(ORCAD_LINEAR_SDK_ENTRY) const server = join(deployment, ORCAD_SERVER_ENTRY_FILENAME) writeFileSync( server, diff --git a/config/scripts/orcad-distribution/server-bundle/orcad-mobile-web-bundle.test.mjs b/config/scripts/orcad-distribution/server-bundle/orcad-mobile-web-bundle.test.mjs deleted file mode 100644 index 4607ab7e923..00000000000 --- a/config/scripts/orcad-distribution/server-bundle/orcad-mobile-web-bundle.test.mjs +++ /dev/null @@ -1,43 +0,0 @@ -import { mkdtempSync, readFileSync, rmSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterEach, beforeEach, expect, it, vi } from 'vitest' -import { ORCAD_MOBILE_WEB_MANIFEST } from '../../../../src/shared/orcad-artifacts.ts' -import { writeMobileWebBundleFixtureTree } from '../../build/mobile-web-app/mobile-web-bundle-fixture-tree.mjs' -import { - readOrcadMobileWebAssetFilenames, - stageOrcadMobileWebBundle -} from './orcad-mobile-web-bundle.mjs' - -let scratch -let bundleDir -let outputDir - -beforeEach(() => { - scratch = mkdtempSync(join(tmpdir(), 'orcad-mobile-web-')) - bundleDir = join(scratch, 'mobile-web') - outputDir = join(scratch, 'orcad') - vi.spyOn(console, 'log').mockImplementation(() => {}) -}) - -afterEach(() => { - vi.restoreAllMocks() - vi.unstubAllEnvs() - rmSync(scratch, { recursive: true, force: true }) -}) - -it('stages the manifest and every asset it lists, byte for byte', async () => { - const { manifest } = await writeMobileWebBundleFixtureTree({ outDir: bundleDir }) - stageOrcadMobileWebBundle(outputDir, bundleDir) - - const staged = readOrcadMobileWebAssetFilenames(outputDir) - expect(staged).toHaveLength(manifest.assets.length) - for (const asset of manifest.assets) { - expect(readFileSync(join(outputDir, 'out', 'mobile-web', asset.path))).toEqual( - readFileSync(join(bundleDir, asset.path)) - ) - } - expect(readFileSync(join(outputDir, ORCAD_MOBILE_WEB_MANIFEST))).toEqual( - readFileSync(join(bundleDir, 'manifest.json')) - ) -}) diff --git a/config/scripts/performance/measurement/benchmark-artifact-comparison.test.mjs b/config/scripts/performance/measurement/benchmark-artifact-comparison.test.mjs index 3abb570f748..dc83dc90144 100644 --- a/config/scripts/performance/measurement/benchmark-artifact-comparison.test.mjs +++ b/config/scripts/performance/measurement/benchmark-artifact-comparison.test.mjs @@ -1,5 +1,5 @@ import { spawnSync } from 'node:child_process' -import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' @@ -37,26 +37,6 @@ afterEach(() => { }) describe('benchmark artifact comparison', () => { - it('rejects invalid benchmark artifacts before comparing partial metrics', () => { - const dir = makeTempDir() - const baselinePath = writeArtifact(dir, 'split-invalid.json', { - label: 'invalid split', - status: 'failed', - valid: false, - headlineMs: { shortcutToFocusP50: 0 } - }) - const candidatePath = writeArtifact(dir, 'split-valid.json', { - label: 'valid split', - status: 'passed', - valid: true, - headlineMs: { shortcutToFocusP50: 10 } - }) - - expect(() => comparePaths(baselinePath, candidatePath)).toThrow( - 'split-invalid.json: benchmark artifact is marked invalid' - ) - }) - it('aggregates duplicate Playwright scenario metrics before comparison', () => { const dir = makeTempDir() const baselinePath = writeArtifact(dir, 'baseline-playwright-duplicates.json', { @@ -181,47 +161,6 @@ describe('benchmark artifact comparison', () => { }) }) - it('writes Markdown and JSON reports from the CLI while printing Markdown', () => { - const dir = makeTempDir() - const baselinePath = writeArtifact(dir, 'baseline.json', { - label: 'baseline', - summaryMedianMs: { totalToDidFinishLoad: 100 } - }) - const candidatePath = writeArtifact(dir, 'candidate.json', { - label: 'candidate', - summaryMedianMs: { totalToDidFinishLoad: 40 } - }) - const markdownPath = join(dir, 'nested', 'comparison.md') - const jsonPath = join(dir, 'nested', 'comparison.json') - - const result = spawnSync( - process.execPath, - [ - scriptPath, - '--baseline', - baselinePath, - '--candidate', - candidatePath, - '--title', - 'Test Compare', - '--output', - markdownPath, - '--json-output', - jsonPath - ], - { cwd: process.cwd(), encoding: 'utf8' } - ) - - expect(result.status).toBe(0) - expect(result.stdout).toContain('| Metric | Baseline | Candidate | Delta | Delta % | Result |') - expect(result.stdout).toContain( - '| totalToDidFinishLoad | 100.0ms | 40.0ms | -60.0ms | -60.0% | improved |' - ) - expect(existsSync(markdownPath)).toBe(true) - expect(existsSync(jsonPath)).toBe(true) - expect(JSON.parse(readFileSync(jsonPath, 'utf8')).schemaVersion).toBe(1) - }) - it('redacts absolute input paths from generated reports', () => { const dir = makeTempDir() const baselinePath = writeArtifact(dir, 'absolute-baseline.json', { diff --git a/config/scripts/performance/mobile-projection/mobile-source-control-collation-benchmark.mjs b/config/scripts/performance/mobile-projection/mobile-source-control-collation-benchmark.mjs index 68c2ea699a3..4c3c71ea332 100644 --- a/config/scripts/performance/mobile-projection/mobile-source-control-collation-benchmark.mjs +++ b/config/scripts/performance/mobile-projection/mobile-source-control-collation-benchmark.mjs @@ -4,6 +4,8 @@ import { readFileSync } from 'node:fs' import { dirname, resolve } from 'node:path' import { performance } from 'node:perf_hooks' import { build } from 'esbuild' +import { runProcessSync } from '@orca/process-host' +import { describeProcessFailure } from '../../tool-invocation/process-failure-message.mjs' import { buildCounterbalancedSchedule } from '../measurement/counterbalanced-benchmark-schedule.mjs' const baseline = process.argv[2] @@ -20,7 +22,31 @@ async function load(file, contents, name) { format: 'esm', write: false, logLevel: 'silent', - tsconfigRaw: {} + tsconfigRaw: {}, + plugins: [ + { + name: 'historical-source-control-owners', + setup(plugin) { + plugin.onResolve( + { filter: /^\.\.\/(?:mobile-git-status|git-status-reply-schema)$/ }, + (args) => { + if ( + file !== 'mobile/src/source-control/review/mobile-diff-review-queue.ts' || + args.importer !== '' || + args.resolveDir !== dirname(resolve(file)) + ) { + return + } + const target = + args.path === '../mobile-git-status' + ? 'mobile/src/source-control/changes/mobile-git-status.ts' + : 'mobile/src/source-control/git/git-status-reply-schema.ts' + return { path: resolve(target) } + } + ) + } + } + ] }) return ( await import( @@ -34,18 +60,44 @@ const paths = execFileSync('git', ['ls-files', '-z'], { maxBuffer: 16 * 1024 * 1 .split('\0') .filter(Boolean) const results = [] -for (const [file, name] of [ - ['mobile/src/source-control/mobile-git-status.ts', 'buildMobileSourceControlSections'], - ['mobile/src/source-control/mobile-branch-compare.ts', 'buildMobileBranchCompareSection'], - ['mobile/src/source-control/review/mobile-diff-review-queue.ts', 'buildMobileDiffReviewQueue'] +for (const { baselineLegacy, current, name } of [ + { + baselineLegacy: 'mobile/src/source-control/mobile-git-status.ts', + current: 'mobile/src/source-control/changes/mobile-git-status.ts', + name: 'buildMobileSourceControlSections' + }, + { + baselineLegacy: 'mobile/src/source-control/mobile-branch-compare.ts', + current: 'mobile/src/source-control/branches/mobile-branch-compare.ts', + name: 'buildMobileBranchCompareSection' + }, + { + baselineLegacy: 'mobile/src/source-control/review/mobile-diff-review-queue.ts', + current: 'mobile/src/source-control/review/mobile-diff-review-queue.ts', + name: 'buildMobileDiffReviewQueue' + } ]) { + const historicalFile = [...new Set([current, baselineLegacy])].find((file) => { + const probe = runProcessSync({ + program: 'git', + args: ['cat-file', '-t', `${baseline}:${file}`], + cwd: process.cwd(), + timeoutMs: 30_000, + maxOutputBytes: 2 * 1024 * 1024 + }) + if (probe.timedOut || probe.outputTruncated || ![0, 128].includes(probe.code)) { + throw new Error(describeProcessFailure(probe)) + } + return probe.code === 0 && probe.stdout.trim() === 'blob' + }) + assert(historicalFile, `Missing baseline entry: ${baselineLegacy}`) const arms = { before: await load( - file, - execFileSync('git', ['show', `${baseline}:${file}`], { encoding: 'utf8' }), + historicalFile, + execFileSync('git', ['show', `${baseline}:${historicalFile}`], { encoding: 'utf8' }), name ), - after: await load(file, readFileSync(file, 'utf8'), name) + after: await load(current, readFileSync(current, 'utf8'), name) } for (const count of [0, 1, 17, 63, 1000]) { const step = Math.max(1, Math.floor(paths.length / Math.max(1, count))) diff --git a/config/scripts/performance/terminal-input/mobile-backspace-benchmark.mjs b/config/scripts/performance/terminal-input/mobile-backspace-benchmark.mjs index c5256e4fee2..11c7a7e2616 100644 --- a/config/scripts/performance/terminal-input/mobile-backspace-benchmark.mjs +++ b/config/scripts/performance/terminal-input/mobile-backspace-benchmark.mjs @@ -6,8 +6,8 @@ import { build } from 'esbuild' import { buildCounterbalancedSchedule } from '../measurement/counterbalanced-benchmark-schedule.mjs' import { summarizeBenchmarkSamples } from '../measurement/benchmark-sample-summary.mjs' -// git show :mobile/src/terminal/terminal-live-text-commit.ts | node config/scripts/mobile-backspace-benchmark.mjs -const target = resolve('mobile/src/terminal/terminal-live-text-commit.ts') +// git show :mobile/src/terminal/live-input/terminal-live-text-commit.ts | node config/scripts/mobile-backspace-benchmark.mjs +const target = resolve('mobile/src/terminal/live-input/terminal-live-text-commit.ts') async function load(source) { const result = await build({ stdin: { contents: source, loader: 'ts', resolveDir: dirname(target) }, diff --git a/config/scripts/performance/terminal-reporting/check-terminal-perf-report-budgets.test.mjs b/config/scripts/performance/terminal-reporting/check-terminal-perf-report-budgets.test.mjs deleted file mode 100644 index ca9bafeb674..00000000000 --- a/config/scripts/performance/terminal-reporting/check-terminal-perf-report-budgets.test.mjs +++ /dev/null @@ -1,79 +0,0 @@ -import { spawnSync } from 'node:child_process' -import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterEach, describe, expect, it } from 'vitest' - -const scriptPath = - 'config/scripts/performance/terminal-reporting/check-terminal-perf-report-budgets.mjs' -const tempDirs = [] - -function writeReport(annotationDescription, annotationType = 'opencode-test') { - const dir = mkdtempSync(join(tmpdir(), 'orca-terminal-perf-report-')) - tempDirs.push(dir) - const reportPath = join(dir, 'report.json') - writeFileSync( - reportPath, - JSON.stringify({ - suites: [ - { - specs: [ - { - tests: [ - { - annotations: [ - { - type: annotationType, - description: annotationDescription - } - ] - } - ] - } - ] - } - ] - }) - ) - return reportPath -} - -function runChecker(reportPath) { - return spawnSync(process.execPath, [scriptPath, reportPath], { - cwd: process.cwd(), - encoding: 'utf8' - }) -} - -afterEach(() => { - while (tempDirs.length > 0) { - rmSync(tempDirs.pop(), { force: true, recursive: true }) - } -}) - -describe('check-terminal-perf-report-budgets', () => { - // Redraw scenarios must not inherit the unloaded drift ceiling. - - // Historical outliers must not become the new latency baseline. - - it('fails malformed metric values instead of treating them as absent', () => { - const reportPath = writeReport('panes=1 median=999 worst=abcms rendererQueuedChars=wat') - - const result = runChecker(reportPath) - - expect(result.status).toBe(1) - expect(result.stderr).toContain('median value "999" is malformed') - expect(result.stderr).toContain('worst value "abcms" is malformed') - expect(result.stderr).toContain('rendererQueuedChars value "wat" is malformed') - expect(result.stderr).toContain('no recognized budget metrics found') - }) - - it('ignores non-OpenCode annotations but fails when no perf rows remain', () => { - const reportPath = writeReport('median=999.0ms', 'browser-unrelated') - - const result = runChecker(reportPath) - - expect(result.status).toBe(1) - expect(result.stderr).toContain('No OpenCode terminal perf annotations found.') - }) -}) diff --git a/config/scripts/performance/windows-security/windows-security-reuse-benchmark.test.mjs b/config/scripts/performance/windows-security/windows-security-reuse-benchmark.test.mjs index d2e3971fa31..0320fa8f03f 100644 --- a/config/scripts/performance/windows-security/windows-security-reuse-benchmark.test.mjs +++ b/config/scripts/performance/windows-security/windows-security-reuse-benchmark.test.mjs @@ -1,16 +1,8 @@ import { createHash } from 'node:crypto' -import { - mkdirSync, - mkdtempSync, - readFileSync, - statSync, - symlinkSync, - writeFileSync, - rmSync -} from 'node:fs' +import { mkdirSync, mkdtempSync, readFileSync, statSync, writeFileSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' -import { afterEach, expect, it, vi } from 'vitest' +import { afterEach, expect, it } from 'vitest' import { peImage } from '../../native-build/windows/windows-pe-image-fixture.mjs' import { withInvalidSecurityCandidate } from './benchmark-windows-security-reuse.mjs' @@ -28,47 +20,17 @@ function fixture(arch = 'x64') { return { root, candidate } } -it.each(['success', 'rejection'])( - 'restores exact candidate bytes and permissions after %s', - async (outcome) => { - const f = fixture() - const original = readFileSync(f.candidate) - const before = statSync(f.candidate) - const action = async () => { - expect(readFileSync(f.candidate)).not.toEqual(original) - if (outcome === 'rejection') { - throw new Error('failed sample') - } - return 'proof' - } - const operation = withInvalidSecurityCandidate(f.candidate, f.root, digest(original), action) - if (outcome === 'rejection') { - await expect(operation).rejects.toThrow('failed sample') - } else { - expect(await operation).toBe('proof') - } - expect(readFileSync(f.candidate)).toEqual(original) - expect(statSync(f.candidate).mode).toBe(before.mode) - expect(statSync(f.candidate).mtimeMs).toBeCloseTo(before.mtimeMs, 0) - } -) - -it('refuses a changed candidate, symlink and preexisting backup before invalidating', async () => { +it('restores exact candidate bytes and permissions after a rejected sample', async () => { const f = fixture() - const bytes = readFileSync(f.candidate) - const action = vi.fn() - await expect( - withInvalidSecurityCandidate(f.candidate, f.root, 'wrong hash', action) - ).rejects.toThrow() - writeFileSync(join(f.root, 'prepared-security-backup.node'), 'occupied') - await expect( - withInvalidSecurityCandidate(f.candidate, f.root, digest(bytes), action) - ).rejects.toThrow() - const link = join(f.root, 'link.node') - symlinkSync(dirname(f.candidate), link, 'junction') - await expect(withInvalidSecurityCandidate(link, f.root, digest(bytes), action)).rejects.toThrow( - 'regular file' - ) - expect(readFileSync(f.candidate)).toEqual(bytes) - expect(action).not.toHaveBeenCalled() + const original = readFileSync(f.candidate) + const before = statSync(f.candidate) + const action = async () => { + expect(readFileSync(f.candidate)).not.toEqual(original) + throw new Error('failed sample') + } + const operation = withInvalidSecurityCandidate(f.candidate, f.root, digest(original), action) + await expect(operation).rejects.toThrow('failed sample') + expect(readFileSync(f.candidate)).toEqual(original) + expect(statSync(f.candidate).mode).toBe(before.mode) + expect(statSync(f.candidate).mtimeMs).toBeCloseTo(before.mtimeMs, 0) }) diff --git a/config/scripts/plugin-authoring/content-validation/validate-plugin-files.test.mjs b/config/scripts/plugin-authoring/content-validation/validate-plugin-files.test.mjs deleted file mode 100644 index 18ce2787ee4..00000000000 --- a/config/scripts/plugin-authoring/content-validation/validate-plugin-files.test.mjs +++ /dev/null @@ -1,39 +0,0 @@ -import { mkdtemp, rm } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import path from 'node:path' -import process from 'node:process' -import { runProcess } from '@orca/process-host' -import { afterEach, describe, expect, it } from 'vitest' -import { createPluginFromTemplate } from '../../../../src/cli/plugins/authoring/plugin-create' - -const SCRIPT = path.join(import.meta.dirname, './validate-plugin-files.mjs') -const roots = [] - -async function tempRoot() { - const root = await mkdtemp(path.join(tmpdir(), 'orca-validate-plugin-files-')) - roots.push(root) - return root -} - -afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))) -}) - -function validate(...targets) { - return runProcess({ - program: process.execPath, - args: [SCRIPT, ...targets], - timeoutMs: 60_000 - }) -} - -describe('validate-plugin-files', () => { - it('fails with exit 1 when a file the manifest names is missing', async () => { - const dir = path.join(await tempRoot(), 'panel') - await createPluginFromTemplate({ dir, template: 'panel', publisher: 'acme' }) - await rm(path.join(dir, 'panel', 'index.html')) - const result = await validate(dir) - expect(result.code).toBe(1) - expect(result.stderr).toContain('panel/index.html') - }, 60_000) -}) diff --git a/config/scripts/plugin-authoring/rpc-catalog/generate-rpc-params-catalog.mjs b/config/scripts/plugin-authoring/rpc-catalog/generate-rpc-params-catalog.mjs index a1b05beffd5..b5f4fbb5c8b 100644 --- a/config/scripts/plugin-authoring/rpc-catalog/generate-rpc-params-catalog.mjs +++ b/config/scripts/plugin-authoring/rpc-catalog/generate-rpc-params-catalog.mjs @@ -32,16 +32,25 @@ function scratchDir(prefix) { const posix = (value) => value.split(path.sep).join('/') const repoPath = (absolute) => posix(path.relative(REPO_ROOT, absolute)) +function isProductionModule(file) { + const value = posix(file) + return ( + !/(?:^|\/)(?:test-support|testing|__tests__|fixtures)(?:\/|$)/.test(value) && + !/\.(?:test|spec|test-fixture|fixture)\.[cm]?tsx?$/.test(value) && + !/\.d\.[cm]?ts$/.test(value) + ) +} + function indexableModules() { // Exclude tests or catalog’s CJS bundle requires Vitest and throws. const modules = new Set( - globSync('*.ts', { cwd: CONTRACT_DIR }) - .filter((name) => !name.endsWith('.test.ts')) + globSync('**/*.ts', { cwd: CONTRACT_DIR }) + .filter(isProductionModule) .map((name) => path.join(CONTRACT_DIR, name)) ) modules.delete(OUTPUT_PATH) for (const file of globSync('**/*.ts', { cwd: RPC_DIR })) { - if (file.endsWith('.test.ts')) { + if (!isProductionModule(file)) { continue } const source = readFileSync(path.join(RPC_DIR, file), 'utf8') @@ -50,6 +59,7 @@ function indexableModules() { // Exclude generator output even when params import its types; stale catalog bundling would break regeneration precisely when needed. if ( resolved !== OUTPUT_PATH && + isProductionModule(resolved) && resolved.startsWith(`${SHARED_DIR}${path.sep}`) && existsSync(resolved) ) { diff --git a/config/scripts/preflight/changed-checks/check-changed-code-quality.mjs b/config/scripts/preflight/changed-checks/check-changed-code-quality.mjs index 8cd2d6cf97d..2052fc083a1 100644 --- a/config/scripts/preflight/changed-checks/check-changed-code-quality.mjs +++ b/config/scripts/preflight/changed-checks/check-changed-code-quality.mjs @@ -76,8 +76,8 @@ const SUPPRESSED_REACT_DOCTOR_DIAGNOSTICS = new Map([ [ 'react-doctor(no-adjust-state-on-prop-change)', new Set([ - 'src/renderer/src/components/task-page/github/use-task-page-github-issue-draft.ts', - 'src/renderer/src/components/task-page/jira/use-task-page-jira-creation-state.ts' + 'src/renderer/src/components/task-page/github/issue-creation/use-task-page-github-issue-draft.ts', + 'src/renderer/src/components/task-page/jira/creation/use-task-page-jira-creation-state.ts' ]) ], [ diff --git a/config/scripts/preflight/changed-checks/check-changed-code-quality.test.mjs b/config/scripts/preflight/changed-checks/check-changed-code-quality.test.mjs index f6b7806fd7f..a431458a26d 100644 --- a/config/scripts/preflight/changed-checks/check-changed-code-quality.test.mjs +++ b/config/scripts/preflight/changed-checks/check-changed-code-quality.test.mjs @@ -3,11 +3,7 @@ import { tmpdir } from 'node:os' import path from 'node:path' import { describe, expect, it } from 'vitest' import { runProcessSync } from '@orca/process-host' -import { - collectBaseLineBlocks, - collectAddedLineRanges, - isUnloadedPluginDirectiveUnusedWarning -} from './check-changed-code-quality.mjs' +import { collectBaseLineBlocks, collectAddedLineRanges } from './check-changed-code-quality.mjs' describe('changed-code quality line matching', () => { it.each(['edit', 'rename'])('treats bracketed filenames literally after %s', (change) => { @@ -104,34 +100,3 @@ describe('moved-code exemption', () => { } }) }) - -describe('secondary-scan directive exemption', () => { - it('exempts type-aware and import-plugin directives from the root scan alone', () => { - const root = mkdtempSync(path.join(tmpdir(), 'changed-code-quality-type-aware-')) - try { - writeFileSync( - path.join(root, 'a.ts'), - [ - '// oxlint-disable-next-line typescript/return-await -- catch must not cover this', - '// oxlint-disable-next-line typescript/return-await-extra -- unknown rule', - '// oxlint-disable-next-line no-console -- root rule', - '// oxlint-disable-next-line import/no-cycle -- singleton hub' - ].join('\n') - ) - const unused = (line) => ({ - message: 'Unused oxlint-disable directive (no problems were reported).', - filename: 'a.ts', - labels: [{ span: { line } }] - }) - const exempt = (line, scan) => - isUnloadedPluginDirectiveUnusedWarning(unused(line), root, scan) - expect(exempt(1, 'code quality')).toBe(true) - expect(exempt(1, 'type-aware code quality')).toBe(false) - expect(exempt(2, 'code quality')).toBe(false) - expect(exempt(3, 'code quality')).toBe(false) - expect(exempt(4, 'code quality')).toBe(true) - } finally { - rmSync(root, { recursive: true, force: true }) - } - }) -}) diff --git a/config/scripts/release/javascript-artifacts/release-javascript-artifact.test.mjs b/config/scripts/release/javascript-artifacts/release-javascript-artifact.test.mjs index 107fd7a1a4f..1f3dec92aa7 100644 --- a/config/scripts/release/javascript-artifacts/release-javascript-artifact.test.mjs +++ b/config/scripts/release/javascript-artifacts/release-javascript-artifact.test.mjs @@ -110,18 +110,6 @@ describe('release JavaScript configuration', () => { }) describe('release JavaScript transfer', () => { - it('restores development bundles and rejects them for an official build', () => { - const options = fixture() - const devConfiguration = { ...configuration, identity: 'dev', telemetryKeySha256: null } - packReleaseJavascript({ ...options, configuration: devConfiguration }) - expect(() => restoreReleaseJavascript(options)).toThrow( - 'JavaScript build configuration differs' - ) - expect( - restoreReleaseJavascript({ ...options, configuration: devConfiguration }).configuration - ).toEqual(devConfiguration) - }) - it('rejects a damaged archive before replacing the current build', () => { const options = fixture() packReleaseJavascript(options) diff --git a/config/scripts/release/workflow-contracts/qualified-orcad-template.test.mjs b/config/scripts/release/workflow-contracts/qualified-orcad-template.test.mjs index d433ee9d563..9f773650d92 100644 --- a/config/scripts/release/workflow-contracts/qualified-orcad-template.test.mjs +++ b/config/scripts/release/workflow-contracts/qualified-orcad-template.test.mjs @@ -8,41 +8,28 @@ import { writeOrcadTemplateTestFixture } from '../../orcad-distribution/template const workflow = parse(readFileSync('.github/workflows/qualified-orcad-template.yml', 'utf8')) -it.each(['valid', 'corrupt', 'dark'])( - 'validates restored bytes and requalifies an unsafe template: %s', - async (kind) => { - const root = mkdtempSync(join(tmpdir(), 'qualified-template-restore-')) - const output = join(root, 'output') - try { - symlinkSync( - resolve('config'), - join(root, 'config'), - process.platform === 'win32' ? 'junction' : 'dir' - ) - const template = await writeOrcadTemplateTestFixture(join(root, 'out')) - if (kind === 'corrupt') { - writeFileSync(join(template, 'out/mobile-web/index.html'), 'mutated') - } else if (kind === 'dark') { - writeFileSync(join(template, 'ORCA_LAYOUT_OWNER_MAIN'), 'dark build') - } - const validate = workflow.jobs.cache.steps.find((step) => step.id === 'validate') - const result = runProcessSync({ - program: 'bash', - args: ['-e', '-c', validate.run], - cwd: root, - env: { ...process.env, GITHUB_OUTPUT: output } - }) - expect(result.code, result.stderr).toBe(0) - if (kind === 'valid') { - expect(readFileSync(output, 'utf8').trim()).toBe('valid=true') - expect(existsSync(template)).toBe(true) - } else { - expect(existsSync(output)).toBe(false) - expect(existsSync(template)).toBe(false) - expect(result.stdout).toContain('qualifying a fresh build') - } - } finally { - rmSync(root, { recursive: true, force: true }) - } +it('removes corrupted cached bytes without marking them reusable', async () => { + const root = mkdtempSync(join(tmpdir(), 'qualified-template-restore-')) + const output = join(root, 'output') + try { + symlinkSync( + resolve('config'), + join(root, 'config'), + process.platform === 'win32' ? 'junction' : 'dir' + ) + const template = await writeOrcadTemplateTestFixture(join(root, 'out')) + writeFileSync(join(template, 'out/mobile-web/index.html'), 'mutated') + const validate = workflow.jobs.cache.steps.find((step) => step.id === 'validate') + const result = runProcessSync({ + program: 'bash', + args: ['-e', '-c', validate.run], + cwd: root, + env: { ...process.env, GITHUB_OUTPUT: output } + }) + expect(result.code, result.stderr).toBe(0) + expect(existsSync(output)).toBe(false) + expect(existsSync(template)).toBe(false) + } finally { + rmSync(root, { recursive: true, force: true }) } -) +}) diff --git a/config/scripts/release/workflow-contracts/release-mac-build-workflow-dispatch.test.mjs b/config/scripts/release/workflow-contracts/release-mac-build-workflow-dispatch.test.mjs index 417074d1e11..5ce36b1091c 100644 --- a/config/scripts/release/workflow-contracts/release-mac-build-workflow-dispatch.test.mjs +++ b/config/scripts/release/workflow-contracts/release-mac-build-workflow-dispatch.test.mjs @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from 'vitest' +import { describe, expect, it } from 'vitest' import { executeReleaseMacBuildMode } from '../operations/run-release-mac-build-workflow.mjs' const splitEnvironment = { @@ -28,16 +28,8 @@ const splitRun = { } describe('release mac build split dispatch and verification', () => { - it.each([ - ['id', 124], - ['head_sha', 'b'.repeat(40)], - ['head_branch', 'other'], - ['run_attempt', 2], - ['event', 'pull_request'], - ['path', '.github/workflows/other.yml'], - ['display_title', 'Mac release build v1.2.4 (777)'] - ])('rejects a changed %s before inspecting success', async (field, value) => { - const { fetch } = createGitHubFetch([jsonResponse(200, { ...splitRun, [field]: value })]) + it('rejects a completed run from another commit', async () => { + const fetch = async () => jsonResponse(200, { ...splitRun, head_sha: 'b'.repeat(40) }) await expect( executeReleaseMacBuildMode('wait', splitEnvironment, { fetch, @@ -47,30 +39,6 @@ describe('release mac build split dispatch and verification', () => { }) }) -function createGitHubFetch(responses) { - const requests = [] - const fetch = vi.fn(async (rawUrl, init) => { - const url = new URL(rawUrl) - const response = responses.shift() - - if (response == null) { - throw new Error(`Unexpected request: ${init.method} ${rawUrl}`) - } - - requests.push({ - body: init.body == null ? undefined : JSON.parse(init.body), - method: init.method, - headers: init.headers, - path: url.pathname, - query: url.searchParams - }) - - return response - }) - - return { fetch, requests } -} - function jsonResponse(status, body) { return { ok: status >= 200 && status < 300, diff --git a/config/scripts/runtime-verification/headless-detector/headless-detector-compiler-cache.test.mjs b/config/scripts/runtime-verification/headless-detector/headless-detector-compiler-cache.test.mjs deleted file mode 100644 index 82b1144a8d8..00000000000 --- a/config/scripts/runtime-verification/headless-detector/headless-detector-compiler-cache.test.mjs +++ /dev/null @@ -1,73 +0,0 @@ -import { - appendFileSync, - existsSync, - mkdirSync, - mkdtempSync, - readFileSync, - rmSync, - symlinkSync, - writeFileSync -} from 'node:fs' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterEach, expect, it } from 'vitest' -import { - activateCompilerCache, - compilerCacheIdentity, - packCompilerCache -} from './headless-detector-compiler-cache.mjs' - -const temporary = [] -afterEach(() => { - for (const dir of temporary.splice(0)) { - rmSync(dir, { recursive: true, force: true }) - } -}) -function fixture() { - const directory = mkdtempSync(join(tmpdir(), 'headless-compiler-cache-')) - temporary.push(directory) - const root = join(directory, 'checkout') - mkdirSync(root) - writeFileSync(join(root, 'package.json'), '{"type":"module"}') - const identity = compilerCacheIdentity({ policyHash: 'policy', cacheRoot: directory }) - packCompilerCache({ identity }) - return { root, identity } -} - -it.each(['modified', 'missing', 'extra', 'symlink', 'malformed'])( - 'falls back on %s cache contents before loading code', - async (kind) => { - const { root, identity } = fixture() - const compiler = join(identity.path, 'node_modules', 'esbuild', 'lib', 'main.js') - if (kind === 'modified') { - appendFileSync(compiler, '\nthrow Error("must not load")') - } - if (kind === 'missing') { - rmSync(compiler) - } - if (kind === 'extra') { - writeFileSync(join(identity.path, 'unexpected'), 'extra') - } - if (kind === 'symlink') { - rmSync(compiler) - symlinkSync(join(root, 'package.json'), compiler) - } - if (kind === 'malformed') { - writeFileSync(join(identity.path, 'manifest.json'), '{') - } - expect((await activateCompilerCache({ root, identity })).available).toBe(false) - expect(existsSync(join(root, 'node_modules'))).toBe(false) - } -) - -it('leaves existing dependencies alone and falls back on an absent archive', async () => { - const { root, identity } = fixture() - mkdirSync(join(root, 'node_modules')) - writeFileSync(join(root, 'node_modules', 'retained'), 'retained') - expect((await activateCompilerCache({ root, identity })).available).toBe(false) - expect(readFileSync(join(root, 'node_modules', 'retained'), 'utf8')).toBe('retained') - rmSync(join(root, 'node_modules'), { recursive: true }) - rmSync(identity.path, { recursive: true }) - expect((await activateCompilerCache({ root, identity })).available).toBe(false) - expect(existsSync(join(root, 'node_modules'))).toBe(false) -}) diff --git a/config/scripts/runtime-verification/skills-cli/verify-skills-cli-runtime.test.mjs b/config/scripts/runtime-verification/skills-cli/verify-skills-cli-runtime.test.mjs index 60980d25ff1..a34d97f1c42 100644 --- a/config/scripts/runtime-verification/skills-cli/verify-skills-cli-runtime.test.mjs +++ b/config/scripts/runtime-verification/skills-cli/verify-skills-cli-runtime.test.mjs @@ -31,29 +31,6 @@ describe('skills CLI runtime closure', () => { } }) - it('rejects bare imports resolved outside the artifact', async () => { - const root = await mkdtemp(join(tmpdir(), 'orca-skills-cli-closure-')) - try { - const artifactRoot = join(root, 'artifact') - const outDir = join(artifactRoot, 'out') - const externalPackageDir = join(root, 'node_modules', 'external-package') - await mkdir(externalPackageDir, { recursive: true }) - await writeSkillsCliFixture(outDir, "require('external-package')\n") - await writeFile( - join(externalPackageDir, 'package.json'), - JSON.stringify({ main: 'index.js' }), - 'utf8' - ) - await writeFile(join(externalPackageDir, 'index.js'), '', 'utf8') - - expect(() => collectRuntimeClosure(outDir, artifactRoot)).toThrow( - /external-package.*resolved outside/s - ) - } finally { - await rm(root, { recursive: true, force: true }) - } - }) - it("resolves a linked package's imports from its store slot, as Node does", async () => { const root = await mkdtemp(join(tmpdir(), 'orca-skills-cli-closure-')) try { diff --git a/config/scripts/static-analysis/oxlint-contracts/app-store-performance-plugin.test.mjs b/config/scripts/static-analysis/oxlint-contracts/app-store-performance-plugin.test.mjs index 5576704bba0..ce751af1651 100644 --- a/config/scripts/static-analysis/oxlint-contracts/app-store-performance-plugin.test.mjs +++ b/config/scripts/static-analysis/oxlint-contracts/app-store-performance-plugin.test.mjs @@ -20,40 +20,6 @@ function lintSources(sources) { } const sources = { - // The subscription and post-render memo from the typing-lag regression. - historicalHotMap: ` - import { useAppStore } from '@/store' -export function useStructuredAgentSessionHostCapabilityState( - target: RuntimeClientTarget | null, - capability: RuntimeCapability -): StructuredAgentSessionHostCapabilityState { - const environmentId = target?.kind === 'environment' ? target.environmentId : null - const statuses = useAppStore((state) => state.runtimeStatusByEnvironmentId) - const remote = useMemo((): StructuredAgentSessionHostCapabilityState => { - if (environmentId === null) { - return 'unknown' - } - const entry = statuses.get(environmentId) - const status = isRuntimeHostContactRevoked(entry) ? null : lastVerifiedRuntimeStatus(entry) - return status - ? status.capabilities?.includes(capability) - ? 'supported' - : 'unsupported' - : 'unknown' - }, [statuses, environmentId, capability]) - const local = useSyncExternalStore( - subscribeLocalRuntimeCapabilityObservation, - () => localCapabilityState(capability), - () => 'unknown' as const - ) - useEffect(() => { - if (target?.kind === 'local') { - void ensureLocalRuntimeCapabilities() - } - }, [target?.kind]) - return !target ? 'unknown' : environmentId === null ? local : remote -} - `, hotMapReturns: ` import { useAppStore as useStore } from '@/store' import { useRuntimeStore } from '@/store/runtime' @@ -82,59 +48,6 @@ export function useStructuredAgentSessionHostCapabilityState( function ShadowedHook(useAppStore) { return useAppStore(s => s.runtimeStatusByEnvironmentId) } const ShadowedParameter = () => useAppStore(s => { { const s = other; return s.runtimeStatusByEnvironmentId } }) `, - fixedHostCapability: ` - import { useAppStore } from '@/store' -export function useStructuredAgentSessionOwnPermissionState( - target: RuntimeClientTarget | null -): StructuredAgentSessionHostCapabilityState { - const capability = AGENT_SESSION_PERMISSION_OWN_RUNTIME_CAPABILITY - const environmentId = target?.kind === 'environment' ? target.environmentId : null - // Select the verdict, not the map: unrelated host flips must not rerender every chat. - const remote = useAppStore((state): StructuredAgentSessionHostCapabilityState => { - if (environmentId === null) { - return 'unknown' - } - const entry = state.runtimeStatusByEnvironmentId.get(environmentId) - const status = isRuntimeHostContactRevoked(entry) ? null : lastVerifiedRuntimeStatus(entry) - return status - ? status.capabilities?.includes(capability) - ? 'supported' - : 'unsupported' - : 'unknown' - }) - const local = useSyncExternalStore( - subscribeLocalRuntimeCapabilityObservation, - () => localCapabilityState(capability), - () => 'unknown' as const - ) - useEffect(() => { - if (target?.kind === 'local') { - void ensureLocalRuntimeCapabilities() - } - }, [target?.kind]) - return !target ? 'unknown' : environmentId === null ? local : remote -} - - `, - parkingProjection: ` - import { useAppStore } from '@/store' -export function selectPairedRuntimeParkingEnvironmentIdsFromState(state: { - runtimeStatusByEnvironmentId: PairedRuntimeParkingCapabilityStatuses -}): ReadonlySet { - return selectPairedRuntimeParkingEnvironmentIds(state.runtimeStatusByEnvironmentId) -} - - const Parking = () => useAppStore(selectPairedRuntimeParkingEnvironmentIdsFromState) - `, - globalHotMapConsumer: ` - import { useAppStore } from '@/store' - // oxlint-disable-next-line app-store-performance/no-whole-hot-map-selector -- Fleet picker needs every host's current status. - const Fleet = () => useAppStore(s => s.runtimeStatusByEnvironmentId) - `, - escapedImport: String.raw` - import { use\u0041ppStore as useStore } from '@/store' - export const WholeStore = () => useStore() - `, subscriptions: ` import { useAppStore as useStore } from '@/store' const WholeStore = () => useStore() @@ -146,14 +59,6 @@ export function selectPairedRuntimeParkingEnvironmentIdsFromState(state: { return state.items }) `, - focusedSelectors: ` - import { useAppStore } from '@/store' - import { useShallow as shallow } from 'zustand/react/shallow' - const selectActive = (state) => state.active - const Focused = () => useAppStore(selectActive) - const Cached = () => useAppStore((state) => state.cachedProjection) - const Shallow = () => useAppStore(shallow((state) => ({ active: state.active }))) - `, namedSelectors: ` import { useAppStore } from '@/store' const EarlyFresh = () => useAppStore(selectFreshRows) @@ -161,22 +66,6 @@ export function selectPairedRuntimeParkingEnvironmentIdsFromState(state: { const Stable = () => useAppStore(selectActiveId) const selectActiveId = (state) => state.activeId `, - importedSelector: ` - import { useAppStore } from '@/store' - import { selectRows } from './selectors' - const Other = () => { - const selectRows = (state) => state.rows.map((row) => row.id) - return selectRows - } - const Imported = () => useAppStore(selectRows) - `, - siblingStores: ` - import { usePluginPanelsStore } from '@/store/plugin-panels' - import { useSyncExternalStore } from 'react' - const WholePanels = () => usePluginPanelsStore() - const FreshPanels = () => usePluginPanelsStore((state) => ({ open: state.open })) - const External = () => useSyncExternalStore(subscribe, () => ({ open: true })) - `, nestedShallow: ` import { useAppStore } from '@/store' import { useShallow } from 'zustand/react/shallow' @@ -233,22 +122,14 @@ beforeAll(() => { }) describe('app store performance Oxlint plugin', () => { - it.each([ - ['historicalHotMap', 1], - ['hotMapReturns', 9] - ])('reports whole hot-map results in %s', (name, count) => { - expect(diagnosticsBySource.get(sources[name]).map((diagnostic) => diagnostic.code)).toEqual( - Array(count).fill('app-store-performance(no-whole-hot-map-selector)') - ) + it('reports whole hot-map returns through local bindings', () => { + expect( + diagnosticsBySource.get(sources.hotMapReturns).map((diagnostic) => diagnostic.code) + ).toEqual(Array(9).fill('app-store-performance(no-whole-hot-map-selector)')) }) - it.each([ - 'focusedHotMapReads', - 'fixedHostCapability', - 'parkingProjection', - 'globalHotMapConsumer' - ])('allows stable projections and justified global consumers in %s', (name) => { - expect(diagnosticsBySource.get(sources[name])).toEqual([]) + it('allows stable projections of hot-map fields', () => { + expect(diagnosticsBySource.get(sources.focusedHotMapReads)).toEqual([]) }) it.each(['lexicalBindings', 'shadowedImports'])('respects lexical bindings in %s', (name) => { diff --git a/config/scripts/static-analysis/oxlint-contracts/ime-enter-guard-plugin.test.mjs b/config/scripts/static-analysis/oxlint-contracts/ime-enter-guard-plugin.test.mjs index 654a42e7076..76440924965 100644 --- a/config/scripts/static-analysis/oxlint-contracts/ime-enter-guard-plugin.test.mjs +++ b/config/scripts/static-analysis/oxlint-contracts/ime-enter-guard-plugin.test.mjs @@ -18,14 +18,6 @@ const violations = [ 'inline Enter on a raw input', "export const X = () => { if (e.key === 'Enter') go() }} />" ], - [ - 'Enter on a raw textarea', - "export const X = () =>