From ced9a3d85f7262e66e5b95034ad4e2391006996c Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Sun, 25 Oct 2026 04:32:05 -0700 Subject: [PATCH] test: remove mock-only cases and repair audit fixtures (#33509) * test: remove mock-only cases and repair audit fixtures * test: use checked upload fetch mocks * test: assert upload MIME on actual multipart wire bytes --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> --- .../golden-value-pool-serialization.test.ts | 30 ---- .../recorded-request-params.test.ts | 2 +- .../rpc-recording/recording-runner.test.ts | 24 +-- .../mobile-relay-credential-bundle.test.ts | 68 ++++++-- .../mobile-relay-credential-rotation.test.ts | 94 +++++++++-- ...mobile-relay-renewal-compatibility.test.ts | 156 ++++++++---------- .../paired-e2ee-native-codec.test.ts | 40 +++-- .../agent-prompt-submission-runtime.test.ts | 142 +--------------- .../skills/skill-cloud-direct-upload.test.ts | 60 +++++-- .../skill-cloud-grant-installation.test.ts | 152 +++++++++-------- .../skills/skill-cloud-install-target.test.ts | 3 + src/main/skills/skill-cloud-service.test.ts | 54 ++++-- src/shared/native-chat-tool-identity.test.ts | 14 +- .../native-chat-tool-line-identity.test.ts | 14 +- .../native-chat-tool-line-label.test.ts | 2 +- 15 files changed, 408 insertions(+), 447 deletions(-) delete mode 100644 mobile/src/test-support/rpc-recording/golden-value-pool-serialization.test.ts diff --git a/mobile/src/test-support/rpc-recording/golden-value-pool-serialization.test.ts b/mobile/src/test-support/rpc-recording/golden-value-pool-serialization.test.ts deleted file mode 100644 index 36199c84161..00000000000 --- a/mobile/src/test-support/rpc-recording/golden-value-pool-serialization.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { expect, it, vi } from 'vitest' -import { internRecording, resolveRecording } from './golden-value-pool' - -it('serializes each pooled value once for its hash and collision check', () => { - const recording = { - scenario: 'serialization', - checkpoints: [ - { - id: 'one', - observation: { - sender: [], - payloads: [], - settlements: {}, - state: { value: 'one' }, - effects: [] - } - } - ] - } - const stringify = vi.spyOn(JSON, 'stringify') - try { - const interned = internRecording(recording) - const calls = stringify.mock.calls.length - stringify.mockRestore() - expect(calls).toBe(2) - expect(resolveRecording(interned.values, interned.recording)).toEqual(recording) - } finally { - stringify.mockRestore() - } -}) diff --git a/mobile/src/test-support/rpc-recording/recorded-request-params.test.ts b/mobile/src/test-support/rpc-recording/recorded-request-params.test.ts index a7a33d8d204..d95dddaa72c 100644 --- a/mobile/src/test-support/rpc-recording/recorded-request-params.test.ts +++ b/mobile/src/test-support/rpc-recording/recorded-request-params.test.ts @@ -130,7 +130,7 @@ function contractProblems( describe('recorded requests against the host params contract', () => { it('sends only methods and params the host dispatcher accepts', () => { const requests = wireRequests() - expect(requests.length).toBeGreaterThan(100) + expect(requests.length).toBeGreaterThan(0) expect(contractProblems(requests, STRIPPED_KEY_INVENTORY)).toEqual([]) // Resolving the full golden corpus can exceed the default timeout on loaded CI workers. }, 60_000) diff --git a/mobile/src/test-support/rpc-recording/recording-runner.test.ts b/mobile/src/test-support/rpc-recording/recording-runner.test.ts index f2c86b39778..2f8406ce539 100644 --- a/mobile/src/test-support/rpc-recording/recording-runner.test.ts +++ b/mobile/src/test-support/rpc-recording/recording-runner.test.ts @@ -1,18 +1,9 @@ -import { - existsSync, - mkdirSync, - mkdtempSync, - readFileSync, - readdirSync, - rmSync, - writeFileSync -} from 'node:fs' +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' -import { join, resolve } from 'node:path' +import { join } from 'node:path' import { operationModuleLoader } from './operation-module-loader' import { describe, expect, it } from 'vitest' import { captureArguments, captureError, captureValue } from './recording-values' -import { RECORDING_DRIVERS } from './recording-drivers' import { ScriptedRpcTransport } from './scripted-rpc-transport' import { vitestRecordingScheduler } from './vitest-recording-scheduler' import { @@ -452,17 +443,6 @@ describe('recording boundaries', () => { }) }) - it('keeps every named driver real and unimported by the engine', () => { - const directory = resolve(import.meta.dirname) - const missing = RECORDING_DRIVERS.filter((driver) => !existsSync(join(directory, driver))) - const imported = readdirSync(directory) - .filter((file) => file.endsWith('.ts')) - .filter((file) => - /(?:from|import\()\s*'\.[^']*\.test'/.test(readFileSync(join(directory, file), 'utf8')) - ) - expect({ missing, imported }).toEqual({ missing: [], imported: [] }) - }) - it('stamps a write ordinal that moves when a subscribe is reordered against a send', async () => { const subscribeFirst = await payloadsFrom((client) => { client.subscribe(CLIENT_EVENTS, null, () => {}) diff --git a/mobile/src/transport/mobile-relay-credential-bundle.test.ts b/mobile/src/transport/mobile-relay-credential-bundle.test.ts index 185d5410980..828f3b89f04 100644 --- a/mobile/src/transport/mobile-relay-credential-bundle.test.ts +++ b/mobile/src/transport/mobile-relay-credential-bundle.test.ts @@ -64,21 +64,21 @@ const journal = { } satisfies MobileRelayPairingJournal describe('mobile relay credential bundle', () => { - let stored: string | null + const stored = new Map() beforeEach(() => { vi.clearAllMocks() resetPairingKeychainForTests() platform.OS = 'ios' - stored = null + stored.clear() asyncStorage.getItem.mockResolvedValue(null) asyncStorage.setItem.mockResolvedValue(undefined) - secureStore.getItemAsync.mockImplementation(async () => stored) - secureStore.setItemAsync.mockImplementation(async (_key: string, value: string) => { - stored = value + secureStore.getItemAsync.mockImplementation(async (key: string) => stored.get(key) ?? null) + secureStore.setItemAsync.mockImplementation(async (key: string, value: string) => { + stored.set(key, value) }) - secureStore.deleteItemAsync.mockImplementation(async () => { - stored = null + secureStore.deleteItemAsync.mockImplementation(async (key: string) => { + stored.delete(key) }) }) @@ -95,33 +95,67 @@ describe('mobile relay credential bundle', () => { }) await writeMobileRelayCredentialBundle(bundle) - await expect(readMobileRelayCredentialBundle('host-1')).resolves.toEqual(bundle) - expect(stored).toContain(journal.secrets.pendingResumeToken) - expect(stored).not.toContain(journal.secrets.inviteToken) + const expected = { + v: 1, + hostId: 'host-1', + deviceToken: 'device-token', + current: { + token: 'D'.repeat(43), + hash: 'B'.repeat(43), + version: 3, + expiresAt: 50_000 + } + } + expect(bundle).toEqual(expected) + await expect(readMobileRelayCredentialBundle('host-1')).resolves.toEqual(expected) + expect(stored.get('orca.mobile-relay.credentials.host-1')).toBe(JSON.stringify(expected)) }) it('rejects a result from another request or authorization mode', () => { + const installed = { + v: 1 as const, + reqId: 'install-1', + authorizationMode: 'authenticated-direct' as const, + currentVersion: 1, + resumeExpiresAt: 50_000 + } expect(() => promotePairingJournalCredential({ journal, - installed: { - v: 1, - reqId: 'other-request', - authorizationMode: 'relay-basis', - currentVersion: 1, - resumeExpiresAt: 50_000 - } + installed: { ...installed, reqId: 'other-request' } + }) + ).toThrow(/does not match/) + expect(() => + promotePairingJournalCredential({ + journal, + installed: { ...installed, authorizationMode: 'relay-basis' } }) ).toThrow(/does not match/) }) it('deletes the namespaced bundle and never enables it on web', async () => { + const bundle = promotePairingJournalCredential({ + journal, + installed: { + v: 1, + reqId: 'install-1', + authorizationMode: 'authenticated-direct', + currentVersion: 1, + resumeExpiresAt: 50_000 + } + }) + await writeMobileRelayCredentialBundle(bundle) await deleteMobileRelayCredentialBundle('host-1') + await expect(readMobileRelayCredentialBundle('host-1')).resolves.toBeNull() expect(secureStore.deleteItemAsync).toHaveBeenCalledWith( 'orca.mobile-relay.credentials.host-1', expect.any(Object) ) + secureStore.deleteItemAsync.mockClear() platform.OS = 'web' + await deleteMobileRelayCredentialBundle('host-1') + expect(secureStore.deleteItemAsync).not.toHaveBeenCalled() + await expect(writeMobileRelayCredentialBundle(bundle)).rejects.toThrow(/native secret store/) await expect(readMobileRelayCredentialBundle('host-1')).rejects.toThrow(/native secret store/) }) }) diff --git a/mobile/src/transport/mobile-relay-credential-rotation.test.ts b/mobile/src/transport/mobile-relay-credential-rotation.test.ts index 2870a3fe50f..8ba1ecc1553 100644 --- a/mobile/src/transport/mobile-relay-credential-rotation.test.ts +++ b/mobile/src/transport/mobile-relay-credential-rotation.test.ts @@ -49,9 +49,16 @@ function install(reqId: string) { } } -function fakeClient(responses: RpcResponse[]): RpcClient { +function fakeClient(responses: RpcResponse[], beforeRequest = () => {}): RpcClient { return { - sendRequest: vi.fn(async () => responses.shift()!), + sendRequest: vi.fn(async () => { + beforeRequest() + const response = responses.shift() + if (!response) { + throw new Error('unexpected credential request') + } + return response + }), subscribe: vi.fn(() => () => {}), updateTerminalSubscriptionViewport: vi.fn(), getState: () => 'connected', @@ -66,16 +73,26 @@ function fakeClient(responses: RpcResponse[]): RpcClient { describe('mobile relay credential rotation', () => { it('persists pending material before install and promotes only committed status', async () => { const installed = install('rotate-CAgICAgICAgICAgICAgICA') - const client = fakeClient([ - success({ v: 1, relay, installStatus: { v: 1, reqId: installed.reqId, state: 'not-found' } }), - success(installed), - success({ - v: 1, - relay, - installStatus: { v: 1, reqId: installed.reqId, state: 'committed', result: installed } - }) - ]) const writes: MobileRelayCredentialBundle[] = [] + const client = fakeClient( + [ + success({ + v: 1, + relay, + installStatus: { v: 1, reqId: installed.reqId, state: 'not-found' } + }), + success(installed), + success({ + v: 1, + relay, + installStatus: { v: 1, reqId: installed.reqId, state: 'committed', result: installed } + }) + ], + () => { + expect(writes).toHaveLength(1) + expect(writes[0]?.pending?.reqId).toBe(installed.reqId) + } + ) const result = await rotateMobileRelayCredential({ client, @@ -98,7 +115,20 @@ describe('mobile relay credential rotation', () => { current: { version: 3, expiresAt: 100_000 }, grace: { version: 2, expiresAt: 70_000 } }) - expect(result.bundle.pending).toBeUndefined() + expect(result.bundle).toEqual({ + v: 1, + hostId: 'host-1', + deviceToken: 'device-token', + current: { + token: 'BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc', + hash: '3Ev4DHdHPRMPoN6GukAY_pi7IUAF5qWJHRK6kURvnoE', + version: 3, + expiresAt: 100_000 + }, + grace: { ...bundle.current, expiresAt: 70_000 }, + pending: undefined + }) + expect(writes[1]).toEqual(result.bundle) }) it('repairs legacy decoded-byte hashes before the normal rotation window', () => { @@ -113,7 +143,30 @@ describe('mobile relay credential rotation', () => { } expect(mobileRelayCredentialNeedsRotation(valid, now)).toBe(false) - expect(mobileRelayCredentialNeedsRotation(bundle, now)).toBe(true) + expect( + mobileRelayCredentialNeedsRotation( + { + ...valid, + current: { ...valid.current, expiresAt: now + 604_800_000 } + }, + now + ) + ).toBe(true) + expect( + mobileRelayCredentialNeedsRotation( + { + ...valid, + current: { ...valid.current, expiresAt: now + 604_800_001 } + }, + now + ) + ).toBe(false) + expect( + mobileRelayCredentialNeedsRotation( + { ...valid, current: { ...valid.current, hash: bundle.current.hash } }, + now + ) + ).toBe(true) expect( mobileRelayCredentialNeedsRotation( { ...valid, pending: { token: 'C'.repeat(43), hash: 'D'.repeat(43), reqId: 'pending' } }, @@ -137,13 +190,24 @@ describe('mobile relay credential rotation', () => { ]) const writeBundle = vi.fn(async () => {}) - await rotateMobileRelayCredential({ client, bundle: pendingBundle, writeBundle }) + const result = await rotateMobileRelayCredential({ client, bundle: pendingBundle, writeBundle }) expect(client.sendRequest).toHaveBeenCalledOnce() expect(client.sendRequest).toHaveBeenCalledWith('pairing.getEndpoints', { installReqId: 'rotate-existing' }) - expect(writeBundle).toHaveBeenCalledOnce() + expect(writeBundle).toHaveBeenCalledExactlyOnceWith({ + ...bundle, + current: { token: 'C'.repeat(43), hash: 'D'.repeat(43), version: 3, expiresAt: 100_000 }, + grace: { ...bundle.current, expiresAt: 70_000 }, + pending: undefined + }) + expect(result.bundle.current).toEqual({ + token: 'C'.repeat(43), + hash: 'D'.repeat(43), + version: 3, + expiresAt: 100_000 + }) }) it('applies only authoritative current or grace confirmation expiries', () => { diff --git a/mobile/src/transport/mobile-relay-renewal-compatibility.test.ts b/mobile/src/transport/mobile-relay-renewal-compatibility.test.ts index 309964c1002..928f062e5d1 100644 --- a/mobile/src/transport/mobile-relay-renewal-compatibility.test.ts +++ b/mobile/src/transport/mobile-relay-renewal-compatibility.test.ts @@ -1,49 +1,19 @@ -import { describe, expect, it, vi } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import type { DeviceResumeConfirmed } from '../../../src/shared/mobile-relay-credential-contract' import type { MobileRelayCredentialBundle } from './mobile-relay-credential-bundle' import { hashMobileRelayCredential } from './mobile-relay-credential-hash' -import { - mobileRelayCredentialNeedsRotation, - persistResumeConfirmation -} from './mobile-relay-credential-rotation' +import { persistResumeConfirmation } from './mobile-relay-credential-rotation' import { RelayLeaseRotationTimer } from './mobile-relay-lease-rotation-timer' vi.mock('react-native', () => ({ Platform: { OS: 'ios' } })) vi.mock('expo-secure-store', () => ({ WHEN_UNLOCKED_THIS_DEVICE_ONLY: 'when-unlocked' })) vi.mock('expo-crypto', () => ({ getRandomBytes: (length: number) => new Uint8Array(length) })) -// Memory cells renew at most daily; current-version confirmations must still say renewed so shipped phones retain lease timers. -const DAY = 24 * 60 * 60 * 1000 -const RESUME_TTL_MS = 30 * DAY -const RENEWAL_INTERVAL_MS = DAY const TOKEN = 'A'.repeat(43) +const INITIAL_EXPIRY = 2_592_001_000 -// The memory cell's answer (cloud/apps/relay/src/credential-store.ts confirmMemoryResume). -function memoryCellConfirm( - stored: { expiresAt: number; renewals: number }, - now: number, - answerNotDue: 'renewed' | 'unchanged' -): DeviceResumeConfirmed { - const due = stored.expiresAt < now + RESUME_TTL_MS - RENEWAL_INTERVAL_MS - if (due) { - stored.expiresAt = now + RESUME_TTL_MS - stored.renewals += 1 - } +function credentialBundle(): MobileRelayCredentialBundle { return { - v: 1, - reqId: `confirm-${now}`, - currentVersion: 2, - acceptedAs: 'current', - renewed: due || answerNotDue === 'renewed', - resumeExpiresAt: stored.expiresAt - } -} - -// A phone whose only path is the relay: the lease timer's re-resume is its only renewal. -async function relayOnlyPhone(days: number, answerNotDue: 'renewed' | 'unchanged') { - let now = Date.parse('2026-10-19T00:00:00Z') - const stored = { expiresAt: now + RESUME_TTL_MS, renewals: 0 } - let bundle: MobileRelayCredentialBundle = { v: 1, hostId: 'host-1', deviceToken: 'device-token', @@ -51,63 +21,77 @@ async function relayOnlyPhone(days: number, answerNotDue: 'renewed' | 'unchanged token: TOKEN, hash: hashMobileRelayCredential(TOKEN), version: 2, - expiresAt: stored.expiresAt + expiresAt: INITIAL_EXPIRY } } - const slot: { delayMs: number | null } = { delayMs: null } - const timer = new RelayLeaseRotationTimer( - { - now: () => now, - setTimer: (_callback, delayMs) => { - slot.delayMs = delayMs - return setTimeout(() => {}, 0) - }, - clearTimer: () => { - slot.delayMs = null - } - }, - () => {} - ) - const resume = async () => { - const confirmation = memoryCellConfirm(stored, now, answerNotDue) - const persisted = await persistResumeConfirmation({ - session: { getResumeConfirmation: () => confirmation, getResumeExpiresAt: () => null }, - bundle, - usedCredentialVersion: 2, - writeBundle: async () => {} - }) - bundle = persisted.bundle - timer.scheduleFromLease(persisted.leaseExpiry) - } - await resume() - let resumes = 1 - while (slot.delayMs !== null && now < Date.parse('2026-10-19T00:00:00Z') + days * DAY) { - now += slot.delayMs - timer.clear() - await resume() - resumes += 1 - expect(bundle.current.expiresAt).toBeGreaterThan(now) - expect(mobileRelayCredentialNeedsRotation(bundle, now)).toBe(false) - } - return { now, bundle, stored, resumes, timerArmed: timer.pending } } -describe('relay renewal under the step-6 memory cell rule', () => { - it('keeps a relay-only phone valid for 31 days, with its lease timer armed after every confirm', async () => { - const phone = await relayOnlyPhone(31, 'renewed') - expect(phone.timerArmed).toBe(true) - expect(phone.resumes).toBeGreaterThanOrEqual(31 * 4) - expect(phone.stored.renewals).toBeLessThanOrEqual(32) - expect(phone.bundle.current.expiresAt).toBe(phone.stored.expiresAt) - expect(phone.bundle.current.expiresAt - phone.now).toBeGreaterThan( - RESUME_TTL_MS - 2 * RENEWAL_INTERVAL_MS - ) +async function confirm(confirmation: DeviceResumeConfirmed) { + const writeBundle = vi.fn(async (_bundle: MobileRelayCredentialBundle) => {}) + const persisted = await persistResumeConfirmation({ + session: { getResumeConfirmation: () => confirmation, getResumeExpiresAt: () => 9_999 }, + bundle: credentialBundle(), + usedCredentialVersion: 2, + writeBundle + }) + const onRotate = vi.fn() + const timer = new RelayLeaseRotationTimer( + { now: Date.now, setTimer: setTimeout, clearTimer: clearTimeout }, + onRotate + ) + timer.scheduleFromLease(persisted.leaseExpiry) + return { ...persisted, writeBundle, timer, onRotate } +} + +afterEach(() => { + vi.useRealTimers() +}) + +describe('mobile relay resume confirmation scheduling', () => { + it('persists a current confirmation and fires its six-hour re-resume timer', async () => { + vi.useFakeTimers() + vi.setSystemTime(1_000) + const phone = await confirm({ + v: 1, + reqId: 'confirm-current', + currentVersion: 2, + acceptedAs: 'current', + renewed: true, + resumeExpiresAt: 2_678_401_000 + }) + + expect(phone.bundle.current.expiresAt).toBe(2_678_401_000) + expect(phone.writeBundle).toHaveBeenCalledExactlyOnceWith({ + ...credentialBundle(), + current: { ...credentialBundle().current, expiresAt: 2_678_401_000 } + }) + expect(phone.leaseExpiry).toBe(2_678_401_000) + expect(phone.timer.pending).toBe(true) + await vi.advanceTimersByTimeAsync(21_599_999) + expect(phone.onRotate).not.toHaveBeenCalled() + await vi.advanceTimersByTimeAsync(1) + expect(phone.onRotate).toHaveBeenCalledOnce() + expect(phone.timer.pending).toBe(false) }) - it('would strand that phone if a not-due confirm answered renewed:false', async () => { - // An unchanged confirmation would stop this shipped phone’s lease timer immediately. - const phone = await relayOnlyPhone(31, 'unchanged') - expect(phone.resumes).toBe(1) - expect(phone.timerArmed).toBe(false) + it('keeps an unchanged confirmation durable without arming a re-resume timer', async () => { + vi.useFakeTimers() + vi.setSystemTime(1_000) + const phone = await confirm({ + v: 1, + reqId: 'confirm-unchanged', + currentVersion: 2, + acceptedAs: 'current', + renewed: false, + resumeExpiresAt: INITIAL_EXPIRY + }) + + expect(phone.bundle).toEqual(credentialBundle()) + expect(phone.writeBundle).toHaveBeenCalledExactlyOnceWith(credentialBundle()) + expect(phone.leaseExpiry).toBeNull() + expect(phone.timer.pending).toBe(false) + expect(vi.getTimerCount()).toBe(0) + await vi.advanceTimersByTimeAsync(2_678_400_000) + expect(phone.onRotate).not.toHaveBeenCalled() }) }) diff --git a/mobile/src/transport/paired-e2ee-native-codec.test.ts b/mobile/src/transport/paired-e2ee-native-codec.test.ts index e052aad1458..14e0fb34f77 100644 --- a/mobile/src/transport/paired-e2ee-native-codec.test.ts +++ b/mobile/src/transport/paired-e2ee-native-codec.test.ts @@ -167,25 +167,27 @@ describe('the native paired E2EE codec', () => { }) it('opens and refuses exactly the frames the JS paths do, never advancing on a refusal', async () => { - const { primitives: nativePrimitives, native } = await load(true) - const { primitives: fallbackPrimitives, native: absent } = await load(false) - expect(native).not.toBeNull() - expect(absent).toBeNull() - const originalPrimitives: PairedE2EEPrimitives = { - ...fallbackPrimitives, - openSecretbox: undefined, - decodeCanonicalBase64: undefined, - decodeUtf8: undefined - } - const sessions = [nativePrimitives, fallbackPrimitives, originalPrimitives].map((primitives) => - pairedSession(primitives) - ) - const wire = inboundWire(sessions[0]!.schedule) + const wire = inboundWire(pairedSession((await load(true)).primitives).schedule) expect(wire.filter((entry) => entry.plaintext === null).length).toBeGreaterThan(50) - for (const { session } of sessions) { + for (const mode of ['native', 'fallback', 'original']) { + const { primitives, native } = await load(mode === 'native') + expect(native === null).toBe(mode !== 'native') + const originalPrimitives: PairedE2EEPrimitives = { + ...primitives, + openSecretbox: undefined, + decodeCanonicalBase64: undefined, + decodeUtf8: undefined + } + const { session } = pairedSession(mode === 'original' ? originalPrimitives : primitives) + secretboxOpenCalls.mockClear() expect(wire.map(({ frame }) => session.openText(frame))).toEqual( wire.map(({ plaintext }) => plaintext) ) + if (mode === 'native') { + expect(secretboxOpenCalls).toHaveBeenCalled() + } else { + expect(secretboxOpenCalls).not.toHaveBeenCalled() + } } }) @@ -221,10 +223,11 @@ describe('the native paired E2EE codec', () => { // UTF-8 fallback must preserve an already-opened frame whose counter advanced. it('falls back to TextDecoder when native UTF-8 throws', async () => { - const { primitives } = await load(true) - bindings!.codec.decodeUtf8 = () => { + const { primitives, native } = await load(true) + expect(native).not.toBeNull() + const decodeUtf8 = vi.spyOn(bindings!.codec, 'decodeUtf8').mockImplementation(() => { throw new Error('native UTF-8 failed') - } + }) const { session, schedule } = pairedSession(primitives) const frames = ['{"a":1}', 'é✓🙂'].map((payload, counter) => b64( @@ -239,6 +242,7 @@ describe('the native paired E2EE codec', () => { ) ) expect(frames.map((frame) => session.openText(frame))).toEqual(['{"a":1}', 'é✓🙂']) + expect(decodeUtf8).toHaveBeenCalledTimes(2) }) // libsodium sizes its output as the box length minus the MAC, which underflows and crashes. diff --git a/src/main/runtime/agent-prompt-submission-runtime.test.ts b/src/main/runtime/agent-prompt-submission-runtime.test.ts index c38f78b4ed4..f1114023486 100644 --- a/src/main/runtime/agent-prompt-submission-runtime.test.ts +++ b/src/main/runtime/agent-prompt-submission-runtime.test.ts @@ -8,8 +8,6 @@ import { OrcaRuntimeService } from './orca-runtime' import { TUI_IDLE_QUIESCENCE_MS } from './orca-runtime-postlude' import { makeStore } from './runtime-rpc-worktree-store-fixtures' import { settledWriteStub } from '../providers/settled-pty-write-stub' -import type { RuntimeTerminalWriter } from './runtime-terminal-writer' -import type { RuntimePtyController } from './runtime-pty-controller-contract' const createPromptRuntime = createAgentPromptSubmissionRuntime @@ -590,75 +588,12 @@ function inputDispatchRuntime() { return result } -describe('terminal input dispatch through the current runtime', () => { - it.each(['pty', 'leaf'])('uses the runtime writer override in the %s lane', async (lane) => { - const target = inputDispatchRuntime() - if (lane === 'leaf') { - Object.defineProperties(target, { - getLivePtyForHandle: { value: () => null }, - getLiveLeafForHandle: { value: () => ({ leaf: { writable: true, ptyId: PTY } }) }, - isLeafPtyProvenAbsent: { value: async () => false } - }) - } - const writer = vi.fn(function ( - this: OrcaRuntimeService, - ...args: Parameters - ) { - expect(this).toBe(target) - expect(args).toHaveLength(4) - expect(args[0]).toBe(PTY) - return Promise.resolve() - }) - Object.defineProperty(target, 'writeTerminalAction', { value: writer }) - - await expect( - target.sendTerminal(HANDLE, { text: 'hello' }, { inputKind: 'driving' }) - ).resolves.toMatchObject({ accepted: true, bytesWritten: 5 }) - expect(writer).toHaveBeenCalledExactlyOnceWith(PTY, { text: 'hello' }, 'hello', { - inputKind: 'driving' - }) - }) -}) - -describe('input original queue, callback and private method contracts', () => { +describe('agent prompt submission queue', () => { afterEach(() => { vi.restoreAllMocks() vi.useRealTimers() }) - it('keeps the private retry lookup on the Root receiver before any delay', async () => { - const target = inputDispatchRuntime() - const read = vi.fn(function (this: OrcaRuntimeService, ...args: unknown[]) { - expect(this).toBe(target) - expect(args).toEqual([PTY]) - return null - }) - Object.defineProperty(target, 'getPtyAgent', { value: read }) - await expect( - target['resubmitHeldAgentPrompt']( - PTY, - 1, - { inputKind: 'driving' }, - { - baseline: { - generation: 1, - permissionSequence: 0, - workingSequence: 0, - explicitWorkingStartedAt: null, - outputSequence: 0, - status: null - }, - readActivity: () => { - throw new Error('no read before the agent lookup') - }, - prompt: undefined, - heldBudgetMs: 0 - } - ) - ).resolves.toBe(0) - expect(read).toHaveBeenCalledOnce() - }) - it('serializes rejected submissions, isolates generations and keeps a replacement tail', async () => { const target = inputDispatchRuntime() const gate = Promise.withResolvers() @@ -689,79 +624,4 @@ describe('input original queue, callback and private method contracts', () => { expect(target['agentPromptSubmissionTailByPtyId'].get(key)).toBe(replacement) expect(target['agentPromptSubmissionTailByPtyId'].has(`${PTY}\u00002`)).toBe(false) }) - - it('keeps hook presence callback receivers and preserves invalidation failure', async () => { - const target = inputDispatchRuntime() - const check = vi.fn(async function (this: OrcaRuntimeService, ...args: unknown[]) { - expect(this).toBe(target) - expect(args).toHaveLength(1) - return 'live' as const - }) - Object.defineProperty(target, 'checkHookAgentPresenceFn', { - configurable: true, - writable: true, - value: check - }) - Object.defineProperty(target, 'collectAgentStatusPaneKeysForPty', { - configurable: true, - value: () => new Set(['pane-first', 'pane-second']) - }) - await expect(target['recheckHookAgentPresenceForPty'](PTY)).resolves.toBe('live') - expect(check.mock.calls).toEqual([['pane-first'], ['pane-second']]) - Object.defineProperty(target, 'collectAgentStatusPaneKeysForPty', { - value: () => { - Object.defineProperty(target, 'checkHookAgentPresenceFn', { value: null }) - return new Set(['pane-first']) - } - }) - await expect(target['recheckHookAgentPresenceForPty'](PTY)).rejects.toBeInstanceOf(TypeError) - expect(check).toHaveBeenCalledTimes(2) - }) - - it.each([ - { recover: false, hasHandle: false }, - { recover: true, hasHandle: false }, - { recover: true, hasHandle: true } - ])('keeps captured recovery handle semantics for %j', async ({ recover, hasHandle }) => { - const target = inputDispatchRuntime() - const pty = target['ptysById'].get(PTY) - if (!pty) { - throw new Error('registered fixture PTY missing') - } - pty.lastOscTitleAt = 0 - pty.lastAgentStatusObservedLive = true - if (!hasHandle) { - target['handleByPtyId'].delete(PTY) - } - const foreground = Promise.withResolvers<{ - controller: RuntimePtyController | null - available: boolean - process: string | null - }>() - const reads = vi.fn(() => foreground.promise) - const explicit = vi.fn(() => ({ status: 'idle' as const, updatedAt: 0, stateStartedAt: 0 })) - Object.defineProperties(target, { - readPtyForegroundProcessFromController: { value: reads }, - getFreshExplicitAgentStatusForPty: { value: explicit }, - recordTerminalSideEffectFact: { value: vi.fn() } - }) - target['confirmLegacyPtyAgentExit'](PTY, recover, false) - target['handleByPtyId'].set(PTY, 'late-handle') - foreground.resolve({ controller: target['ptyController'], available: false, process: null }) - await Promise.resolve() - if (recover && !hasHandle) { - expect(reads).not.toHaveBeenCalled() - expect(explicit).not.toHaveBeenCalled() - } else { - expect(reads).toHaveBeenCalledOnce() - expect(explicit.mock.calls).toEqual( - recover - ? [ - [HANDLE, PTY], - [HANDLE, PTY] - ] - : [] - ) - } - }) }) diff --git a/src/main/skills/skill-cloud-direct-upload.test.ts b/src/main/skills/skill-cloud-direct-upload.test.ts index f065a05d1b3..809fb185a06 100644 --- a/src/main/skills/skill-cloud-direct-upload.test.ts +++ b/src/main/skills/skill-cloud-direct-upload.test.ts @@ -87,18 +87,15 @@ describe('uploadSkillPackageToSignedPolicy', () => { const archive = Buffer.from('private-package-bytes') await writeFile(archivePath, archive) let uploaded = Buffer.alloc(0) - const fetcher = vi.fn(async (_url: URL | RequestInfo, init?: RequestInit) => { - const chunks: Buffer[] = [] - const body = init?.body as unknown as AsyncIterable - for await (const chunk of body) { - chunks.push(Buffer.from(chunk)) - } - uploaded = Buffer.concat(chunks) + let contentType = '' + const fetcher = vi.fn(async (_url, init) => { + uploaded = Buffer.from(await new Response(init?.body).arrayBuffer()) + contentType = new Headers(init?.headers).get('content-type') ?? '' expect(Number(init?.headers && new Headers(init.headers).get('content-length'))).toBe( uploaded.length ) return new Response(null, { status: 204 }) - }) as typeof fetch + }) const progress = vi.fn() await uploadSkillPackageToSignedPolicy({ @@ -112,9 +109,28 @@ describe('uploadSkillPackageToSignedPolicy', () => { onProgress: progress }) - expect(uploaded.includes(archive)).toBe(true) - expect(uploaded.toString('utf8')).toContain('opaque-policy') - expect(progress).toHaveBeenLastCalledWith(archive.length) + const form = await new Response(uploaded, { + headers: { 'content-type': contentType } + }).formData() + expect(form.get('key')).toBe('uploads/private/package.tar.gz') + expect(form.get('policy')).toBe('opaque-policy') + const file = form.get('file') + expect(file).toBeInstanceOf(File) + if (!(file instanceof File)) { + throw new Error('Expected uploaded package file') + } + expect(Buffer.from(await file.arrayBuffer())).toEqual(archive) + expect(file.name).toBe('package.tar.gz') + expect(uploaded.toString('utf8')).toContain( + 'Content-Disposition: form-data; name="file"; filename="package.tar.gz"\r\nContent-Type: application/vnd.orca.skill+tar+gzip\r\n\r\n' + ) + const sent = progress.mock.calls.map(([bytes]) => bytes) + expect(sent).not.toHaveLength(0) + for (const [index, bytes] of sent.entries()) { + expect(bytes).toBeGreaterThanOrEqual(index === 0 ? 0 : sent[index - 1]) + expect(bytes).toBeLessThanOrEqual(archive.length) + } + expect(sent.at(-1)).toBe(archive.length) }) it('rejects insecure destinations and source drift before upload', async () => { @@ -150,16 +166,22 @@ describe('uploadSkillPackageToSignedPolicy', () => { }) it('bounds a stalled signed upload even without a caller-owned signal', async () => { + vi.useFakeTimers() const root = await mkdtemp(join(tmpdir(), 'orca-skill-cloud-upload-')) roots.push(root) const archivePath = join(root, 'package.tar.gz') await writeFile(archivePath, 'bytes') - const fetcher = vi.fn( - async (_url: URL | RequestInfo, init?: RequestInit) => + let markStarted!: () => void + const started = new Promise((resolve) => { + markStarted = resolve + }) + const fetcher = vi.fn( + async (_url, init) => await new Promise((_resolve, reject) => { init?.signal?.addEventListener('abort', () => reject(init.signal?.reason), { once: true }) + markStarted() }) - ) as typeof fetch + ) const upload = uploadSkillPackageToSignedPolicy({ policy: { url: 'https://storage.googleapis.com/upload', fields: {} }, @@ -168,9 +190,11 @@ describe('uploadSkillPackageToSignedPolicy', () => { fetcher, timeoutMs: 25 }) - const startedAt = Date.now() - - await expect(upload).rejects.toThrow('skill-cloud-upload-timeout') - expect(Date.now() - startedAt).toBeLessThan(1_000) + const expectation = expect(upload).rejects.toThrow('skill-cloud-upload-timeout') + await started + await vi.advanceTimersByTimeAsync(24) + expect(fetcher.mock.calls[0]?.[1]?.signal?.aborted).toBe(false) + await vi.advanceTimersByTimeAsync(1) + await expectation }) }) diff --git a/src/main/skills/skill-cloud-grant-installation.test.ts b/src/main/skills/skill-cloud-grant-installation.test.ts index 63eb7bc798b..b77a8c5c3a4 100644 --- a/src/main/skills/skill-cloud-grant-installation.test.ts +++ b/src/main/skills/skill-cloud-grant-installation.test.ts @@ -1,5 +1,14 @@ +import { createHash } from 'node:crypto' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { SkillCloudDownloadGrant } from '../../shared/skill-cloud-contract' +import { + computeSkillBundleDigest, + parseSkillBundleManifest +} from '../../shared/skill-bundle-manifest' +import { + computeSkillPackageDigest, + parseSkillPackageManifest +} from '../../shared/skill-package-manifest' import type { OrcaRuntimeService } from '../runtime/orca-runtime' const mocks = vi.hoisted(() => ({ @@ -24,7 +33,20 @@ import { installSkillCloudGrant } from './skill-cloud-grant-installation' -const grant = { +const markdown = '# Private skill' +const fileDigest = createHash('sha256').update(markdown).digest('hex') +const files = [ + { + path: 'SKILL.md', + size: Buffer.byteLength(markdown), + executable: false, + classification: 'text' as const, + sha256: fileDigest, + identitySha256: fileDigest + } +] +const packageDigest = computeSkillPackageDigest(files) +const grant: SkillCloudDownloadGrant = { grant: { url: 'https://storage.googleapis.com/bucket/package.tar.gz', expiresAt: '2099-01-01T00:00:00.000Z' @@ -33,11 +55,53 @@ const grant = { packageId: 'package-1', versionId: 'version-1', name: 'private-skill', - packageDigest: 'a'.repeat(64), + description: '', + releaseNotes: '', + createdAt: '2026-08-11T00:00:00.000Z', + packageDigest, archiveSha256: 'b'.repeat(64), - compressedBytes: 12 + compressedBytes: 12, + manifest: parseSkillPackageManifest({ + schemaVersion: 1, + packageId: 'package-1', + versionId: 'version-1', + name: 'private-skill', + description: '', + createdAt: '2026-08-11T00:00:00.000Z', + packageDigest, + files + }) } -} as unknown as SkillCloudDownloadGrant +} + +function bundleDownloadGrant(ids: string[]): SkillCloudDownloadGrant { + const skills = ids.map((id, index) => ({ + id, + name: `skill-${String(index).padStart(3, '0')}`, + description: '', + digest: packageDigest, + files + })) + const manifest = parseSkillBundleManifest({ + schemaVersion: 1, + packageId: 'package-1', + versionId: 'version-1', + bundleName: 'private-bundle', + description: '', + createdAt: '2026-08-11T00:00:00.000Z', + bundleDigest: computeSkillBundleDigest(skills), + skills + }) + return { + ...grant, + version: { + ...grant.version, + name: 'private-bundle', + packageDigest: manifest.bundleDigest, + manifest + } + } +} describe('installSkillCloudGrant', () => { beforeEach(() => { @@ -52,7 +116,7 @@ describe('installSkillCloudGrant', () => { operationId: 'operation-1', status: 'installed', name: 'private-skill', - packageDigest: 'a'.repeat(64), + packageDigest, placements: [] } mocks.getRuntimeEnvironmentStatus.mockResolvedValue({ @@ -79,36 +143,12 @@ describe('installSkillCloudGrant', () => { }) it('installs selected bundle skills through a capable remote runtime', async () => { - const bundleGrant = { - ...grant, - version: { - ...grant.version, - name: 'private-bundle', - manifest: { - schemaVersion: 1, - packageId: 'package-1', - versionId: 'version-1', - bundleName: 'private-bundle', - description: '', - createdAt: '2026-08-11T00:00:00.000Z', - bundleDigest: 'c'.repeat(64), - skills: [ - { - id: 'skill-1', - name: 'private-skill', - description: '', - digest: 'a'.repeat(64), - files: [] - } - ] - } - } - } as unknown as SkillCloudDownloadGrant + const bundleGrant = bundleDownloadGrant(['skill-1']) const result = { operationId: 'operation-1', packageId: 'package-1', versionId: 'version-1', - bundleDigest: 'c'.repeat(64), + bundleDigest: bundleGrant.version.packageDigest, status: 'complete', skills: [] } @@ -134,18 +174,8 @@ describe('installSkillCloudGrant', () => { }) }) -// Set membership must retain manifest order and duplicates in user-visible failure reports. -it('reports selected manifest entries in manifest order, duplicates and all', async () => { - const skills = ['b', 'a', 'dupe', 'dupe', 'unselected'].map((id) => ({ - id, - name: `name-${id}`, - digest: 'a'.repeat(64), - files: [] - })) - const bundleGrant = { - ...grant, - version: { ...grant.version, manifest: { skills, bundleDigest: 'c'.repeat(64) } } - } as unknown as SkillCloudDownloadGrant +it('reports only selected skills in manifest order', async () => { + const bundleGrant = bundleDownloadGrant(['b', 'a', 'unselected']) const runtime = { installSharedSkillBundleRequest: vi .fn() @@ -153,33 +183,13 @@ it('reports selected manifest entries in manifest order, duplicates and all', as } as unknown as OrcaRuntimeService const result = await installSkillBundleCloudGrant(runtime, bundleGrant, { operationId: 'op', - // Repeated/unknown selection ids must not change manifest membership. - selectedSkillIds: ['dupe', 'a', 'a', 'b', 'never-in-manifest'], + selectedSkillIds: ['a', 'b'], destination: { scope: 'global' } }) expect(result.status).toBe('ok') if (result.status === 'ok') { - expect(result.value.skills.map((skill) => skill.skillId)).toEqual(['b', 'a', 'dupe', 'dupe']) - } -}) - -it('reports no skills when nothing was selected', async () => { - const skills = [{ id: 'a', name: 'a', digest: 'a'.repeat(64), files: [] }] - const bundleGrant = { - ...grant, - version: { ...grant.version, manifest: { skills, bundleDigest: 'c'.repeat(64) } } - } as unknown as SkillCloudDownloadGrant - const runtime = { - installSharedSkillBundleRequest: vi.fn().mockRejectedValue(new Error('skill-install-cancelled')) - } as unknown as OrcaRuntimeService - const result = await installSkillBundleCloudGrant(runtime, bundleGrant, { - operationId: 'op', - selectedSkillIds: [], - destination: { scope: 'global' } - }) - expect(result.status).toBe('ok') - if (result.status === 'ok') { - expect(result.value.skills).toEqual([]) + expect(result.value.skills.map((skill) => skill.skillId)).toEqual(['b', 'a']) + expect(result.value.status).toBe('failed') } }) @@ -187,7 +197,7 @@ it.each(['skill-install-cancelled', 'skill-install-filesystem-failed'])( 'indexes selected IDs when reporting %s', async (code) => { let reads = 0 - const ids = Array.from({ length: 1000 }, (_, index) => `skill-${index}`) + const ids = Array.from({ length: 512 }, (_, index) => `skill-${index}`) const selectedSkillIds = new Proxy(ids, { get(target, key, receiver) { if (typeof key === 'string' && /^\d+$/.test(key)) { @@ -197,11 +207,7 @@ it.each(['skill-install-cancelled', 'skill-install-filesystem-failed'])( return Reflect.get(target, key, receiver) } }) - const skills = ids.map((id) => ({ id, name: id, digest: 'a'.repeat(64), files: [] })) - const bundleGrant = { - ...grant, - version: { ...grant.version, manifest: { skills, bundleDigest: 'c'.repeat(64) } } - } as unknown as SkillCloudDownloadGrant + const bundleGrant = bundleDownloadGrant(ids) const runtime = { installSharedSkillBundleRequest: vi.fn().mockRejectedValue(new Error(code)) } as unknown as OrcaRuntimeService @@ -215,6 +221,6 @@ it.each(['skill-install-cancelled', 'skill-install-filesystem-failed'])( expect(result.value.skills.map((skill) => skill.skillId)).toEqual(ids) expect(result.value.status).toBe(code.includes('cancelled') ? 'cancelled' : 'failed') } - expect(reads).toBeLessThanOrEqual(2000) + expect(reads).toBeLessThanOrEqual(2 * ids.length) } ) diff --git a/src/main/skills/skill-cloud-install-target.test.ts b/src/main/skills/skill-cloud-install-target.test.ts index e0b26edbe84..4a27a35456a 100644 --- a/src/main/skills/skill-cloud-install-target.test.ts +++ b/src/main/skills/skill-cloud-install-target.test.ts @@ -24,6 +24,9 @@ describe('classifySkillCloudInstallTarget', () => { }) it('keeps native and WSL installs in the local grant lane', async () => { + await expect( + classifySkillCloudInstallTarget(runtime(false), { destination: { scope: 'global' } }) + ).resolves.toBe('local') await expect( classifySkillCloudInstallTarget(runtime(false), { destination: { scope: 'global', executionTarget: { kind: 'wsl', distro: 'Ubuntu' } } diff --git a/src/main/skills/skill-cloud-service.test.ts b/src/main/skills/skill-cloud-service.test.ts index 43d575a34ed..676d3b96c16 100644 --- a/src/main/skills/skill-cloud-service.test.ts +++ b/src/main/skills/skill-cloud-service.test.ts @@ -1,8 +1,13 @@ +import { createHash } from 'node:crypto' import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import type { SkillCloudVersion } from '../../shared/skill-cloud-contract' +import { + computeSkillPackageDigest, + parseSkillPackageManifest +} from '../../shared/skill-package-manifest' import { SkillCloudService } from './skill-cloud-service' const { packaged } = vi.hoisted(() => ({ packaged: { value: false } })) @@ -32,26 +37,38 @@ function userDataPath(): string { } function publishedVersion(archiveSha256: string, compressedBytes: number): SkillCloudVersion { + const digest = createHash('sha256').update('# Retry skill').digest('hex') + const files = [ + { + path: 'SKILL.md', + size: Buffer.byteLength('# Retry skill'), + executable: false, + classification: 'text' as const, + sha256: digest, + identitySha256: digest + } + ] + const packageDigest = computeSkillPackageDigest(files) return { packageId: 'pkg_retry', versionId: 'ver_retry', name: 'retry-skill', description: 'Retry publication', - packageDigest: 'a'.repeat(64), + packageDigest, archiveSha256, compressedBytes, createdAt: '2026-08-12T12:00:00.000Z', releaseNotes: 'retry', - manifest: { + manifest: parseSkillPackageManifest({ schemaVersion: 1, packageId: 'pkg_retry', versionId: 'ver_retry', name: 'retry-skill', description: 'Retry publication', createdAt: '2026-08-12T12:00:00.000Z', - packageDigest: 'a'.repeat(64), - files: [] - } + packageDigest, + files + }) } } @@ -69,24 +86,31 @@ function publishRequest(archivePath: string, archiveSha256: string, compressedBy describe('SkillCloudService bearer links', () => { it('resolves and grants downloads without an Orca session', async () => { const requests: RequestInit[] = [] + const version = publishedVersion('a'.repeat(64), 12) + const grant = { + grant: { url: 'https://storage.test/package', expiresAt: '2026-08-11T00:05:00Z' }, + version + } + const share = { id: 'share_1', version } vi.stubGlobal( 'fetch', vi.fn(async (input: URL | RequestInfo, init?: RequestInit) => { requests.push(init ?? {}) return String(input).endsWith('/download-grants') - ? Response.json({ - grant: { url: 'https://storage.test/package', expiresAt: '2026-08-11T00:05:00Z' }, - version: { versionId: 'ver_1' } - }) - : Response.json({ share: { id: 'share_1', version: { versionId: 'ver_1' } } }) + ? Response.json(grant) + : Response.json({ share }) }) ) const service = new SkillCloudService('/unused') const options = { apiUrl: 'http://127.0.0.1:8787' } - await expect(service.resolveShare('share_1', options)).resolves.toMatchObject({ status: 'ok' }) - await expect(service.createDownloadGrant('share_1', options)).resolves.toMatchObject({ - status: 'ok' + await expect(service.resolveShare('share_1', options)).resolves.toEqual({ + status: 'ok', + value: share + }) + await expect(service.createDownloadGrant('share_1', options)).resolves.toEqual({ + status: 'ok', + value: grant }) expect(requests).toHaveLength(2) @@ -129,7 +153,7 @@ describe('SkillCloudService publication retries', () => { const root = userDataPath() const archivePath = join(root, 'package.tar.gz') const archive = Buffer.from('skill archive') - const archiveSha256 = 'b'.repeat(64) + const archiveSha256 = createHash('sha256').update(archive).digest('hex') const version = publishedVersion(archiveSha256, archive.byteLength) writeFileSync(archivePath, archive) let createAttempts = 0 @@ -182,7 +206,7 @@ describe('SkillCloudService publication retries', () => { const root = userDataPath() const archivePath = join(root, 'package.tar.gz') const archive = Buffer.from('skill archive') - const archiveSha256 = 'c'.repeat(64) + const archiveSha256 = createHash('sha256').update(archive).digest('hex') const version = publishedVersion(archiveSha256, archive.byteLength) writeFileSync(archivePath, archive) let createAttempts = 0 diff --git a/src/shared/native-chat-tool-identity.test.ts b/src/shared/native-chat-tool-identity.test.ts index 7bdb72f5594..eb91be22f19 100644 --- a/src/shared/native-chat-tool-identity.test.ts +++ b/src/shared/native-chat-tool-identity.test.ts @@ -8,14 +8,12 @@ import { } from './native-chat-tool-identity' describe('MCP tool identity', () => { - it.each(['mcp__linear__list_issues'])( - 'splits %s for display without changing the identifier', - (name) => { - const raw = name - expect(mcpToolIdentity(name)).toEqual({ server: 'Linear', tool: 'list issues' }) - expect(name).toBe(raw) - } - ) + it('splits a qualified MCP name into display labels', () => { + expect(mcpToolIdentity('mcp__linear__list_issues')).toEqual({ + server: 'Linear', + tool: 'list issues' + }) + }) it.each([ 'linear/list_issues', 'linear.list_issues', diff --git a/src/shared/native-chat-tool-line-identity.test.ts b/src/shared/native-chat-tool-line-identity.test.ts index 8d8c3a5465f..614c26d4cf9 100644 --- a/src/shared/native-chat-tool-line-identity.test.ts +++ b/src/shared/native-chat-tool-line-identity.test.ts @@ -5,12 +5,22 @@ import { nativeChatToolLineIdentity } from './native-chat-tool-line-identity' it('keys an anonymous mobile row by a bounded summary, so a refetched copy keeps it', () => { const input = { command: 'ls', - toJSON: () => { - throw new Error('Input must stay unread.') + a: 1, + b: 2, + c: 3, + d: 4, + e: 5, + f: 6, + g: 7, + get unread() { + throw new Error('Input beyond the preview budget must stay unread.') } } const block: NativeChatToolCallBlock = { type: 'tool-call', name: 'Bash', input } const original = nativeChatToolLineIdentity(block, new Map(), true) + expect(original).toBe( + 'tool-call:Bash:{"command":"ls","a":1,"b":2,"c":3,"d":4,"e":5,"f":6,"g":7,"…":"…"}:0' + ) expect(nativeChatToolLineIdentity({ ...block }, new Map(), true)).toBe(original) const seen = new Map() expect(nativeChatToolLineIdentity(block, seen, true)).not.toBe( diff --git a/src/shared/native-chat-tool-line-label.test.ts b/src/shared/native-chat-tool-line-label.test.ts index bfc1aed6e58..d21de3d6dec 100644 --- a/src/shared/native-chat-tool-line-label.test.ts +++ b/src/shared/native-chat-tool-line-label.test.ts @@ -20,7 +20,7 @@ describe('nativeChatToolLineLabel', () => { expect(label.filePath).toBe('2 files') }) - it('links a read or an edit, never a listed folder', () => { + it('links a read, never a listed folder', () => { const read = nativeChatToolLineLabel({ type: 'tool-call', name: 'Read',