diff --git a/config/scripts/pty-batch-flush-benchmark.mjs b/config/scripts/pty-batch-flush-benchmark.mjs index f8ad0d63fa0..388a055421c 100644 --- a/config/scripts/pty-batch-flush-benchmark.mjs +++ b/config/scripts/pty-batch-flush-benchmark.mjs @@ -428,9 +428,10 @@ async function runBenchmark() { totalDrain: boundedTotalSummary, inputTimerDelay: boundedInputTimerSummary }, - runtimeIngress: { + syntheticIngressModel: { + scope: 'Local algorithm model; does not execute the shipped PTY runtime', legacyRepeatedScans: legacyRuntimeIngressSummary, - currentFastPath: currentRuntimeIngressSummary, + modeledFastPath: currentRuntimeIngressSummary, estimatedReduction: legacyRuntimeIngressSummary.max / Math.max(currentRuntimeIngressSummary.max, Number.EPSILON) @@ -457,8 +458,8 @@ async function runBenchmark() { `bounded max first slice: ${formatMs(boundedFirstSliceSummary.max)}`, `bounded max any slice: ${formatMs(boundedMaxSliceSummary.max)}`, `bounded max total drain: ${formatMs(boundedTotalSummary.max)}`, - `runtime ingress legacy max: ${formatMs(legacyRuntimeIngressSummary.max)}`, - `runtime ingress current max: ${formatMs(currentRuntimeIngressSummary.max)}`, + `synthetic ingress legacy max: ${formatMs(legacyRuntimeIngressSummary.max)}`, + `synthetic ingress fast-path max: ${formatMs(currentRuntimeIngressSummary.max)}`, legacyInputTimerSummary ? `legacy max input timer delay: ${formatMs(legacyInputTimerSummary.max)}` : null, diff --git a/config/scripts/pty-transcript-secret-scan.test.mjs b/config/scripts/pty-transcript-secret-scan.test.mjs index 2d3cd894da0..ba91edf6aea 100644 --- a/config/scripts/pty-transcript-secret-scan.test.mjs +++ b/config/scripts/pty-transcript-secret-scan.test.mjs @@ -55,8 +55,9 @@ describe('pty transcript secret scan', () => { }) it('claims a span once, so a token inside an email is not double-reported', () => { - const findings = scanTranscriptForSecrets('longlivedaccountname@corp.internal') - expect(findings).toHaveLength(1) + const email = 'longlivedaccountnamefortycharacterslongenough@corp.internal' + const findings = scanTranscriptForSecrets(email) + expect(findings).toMatchObject([{ kind: 'email', match: email, index: 0 }]) }) it('passes a fixture that is already scrubbed, so "prove it is gone" can succeed', () => { @@ -84,7 +85,7 @@ describe('redaction', () => { it('leaves the rest of the screen byte-for-byte untouched', () => { const text = 'line one\nuser@corp.io\nline three' - expect(redactTranscript(text).text.split('\n')[2]).toBe('line three') + expect(redactTranscript(text).text).toBe('line one\nXXXXXXXXXXXX\nline three') }) }) @@ -96,7 +97,7 @@ describe('committed transcripts', () => { it.each(transcripts)('%s carries no account identifier or credential', (name) => { const findings = scanTranscriptForSecrets(readFileSync(join(fixtureDir, name), 'utf8')) - expect(formatFindings(name, findings)).toContain('clean') + expect(findings, formatFindings(name, findings)).toEqual([]) }) }) diff --git a/config/scripts/publish-complete-draft-releases.test.mjs b/config/scripts/publish-complete-draft-releases.test.mjs index bc61c58c7e3..854e6e03d78 100644 --- a/config/scripts/publish-complete-draft-releases.test.mjs +++ b/config/scripts/publish-complete-draft-releases.test.mjs @@ -9,6 +9,7 @@ import { publishCompleteDraftReleases, writeGithubOutputs } from './publish-complete-draft-releases.mjs' +import { getRequiredReleaseAssetNames } from './verify-release-required-assets.mjs' function git(cwd, args) { return execFileSync('git', args, { @@ -18,13 +19,13 @@ function git(cwd, args) { }).trim() } -function withGitRepo(run) { +async function withGitRepo(run) { const dir = mkdtempSync(join(tmpdir(), 'orca-draft-release-')) try { git(dir, ['init', '--initial-branch=main']) git(dir, ['config', 'user.name', 'Test Bot']) git(dir, ['config', 'user.email', 'test@example.com']) - run(dir) + return await run(dir) } finally { rmSync(dir, { recursive: true, force: true }) } @@ -74,12 +75,26 @@ describe('isReleaseCutDraft', () => { author: { login: 'github-actions[bot]' } }) ).toBe(false) + expect( + isReleaseCutDraft({ + draft: false, + tag_name: 'v1.4.2-rc.7', + author: { login: 'github-actions[bot]' } + }) + ).toBe(false) + expect( + isReleaseCutDraft({ + draft: true, + tag_name: 'v1.4.2-rc.7', + author: { login: 'release-maintainer' } + }) + ).toBe(false) }) }) describe('isTagBuiltFromCurrentRef', () => { - it('accepts a tag on the current release commit', () => { - withGitRepo((repo) => { + it('accepts a tag on the current release commit', async () => { + await withGitRepo((repo) => { commit(repo, 'initial') commit(repo, 'release: v1.4.36-rc.6') git(repo, ['tag', 'v1.4.36-rc.6']) @@ -88,8 +103,8 @@ describe('isTagBuiltFromCurrentRef', () => { }) }) - it('accepts a tag whose release commit is built from the current ref', () => { - withGitRepo((repo) => { + it('accepts a tag whose release commit is built from the current ref', async () => { + await withGitRepo((repo) => { commit(repo, 'initial') const source = git(repo, ['rev-parse', 'HEAD']) commit(repo, 'release: v1.4.36-rc.6') @@ -100,8 +115,8 @@ describe('isTagBuiltFromCurrentRef', () => { }) }) - it('rejects a stale tag when the current ref has moved on', () => { - withGitRepo((repo) => { + it('rejects a stale tag when the current ref has moved on', async () => { + await withGitRepo((repo) => { commit(repo, 'initial') commit(repo, 'release: v1.4.36-rc.6') git(repo, ['tag', 'v1.4.36-rc.6']) @@ -114,91 +129,114 @@ describe('isTagBuiltFromCurrentRef', () => { describe('publishCompleteDraftReleases', () => { it('publishes complete release-cut drafts and skips incomplete ones', async () => { - const fetchImpl = vi - .fn() - .mockResolvedValueOnce( + await withGitRepo(async (repo) => { + commit(repo, 'release source') + git(repo, ['tag', 'v1.4.2-rc.7']) + git(repo, ['tag', 'v1.4.2-rc.8']) + const releases = [7, 8].map((id) => ({ + id, + draft: true, + tag_name: `v1.4.2-rc.${id}`, + created_at: `2026-05-15T0${id}:31:19Z`, + author: { login: 'github-actions[bot]' }, + assets: getRequiredReleaseAssetNames(`v1.4.2-rc.${id}`) + .filter((name) => id === 7 || name !== 'orca-windows-setup.exe') + .map((name, index) => ({ id: id * 100 + index, name, state: 'uploaded', size: 1 })) + })) + const manifestUrls = new Map( + releases.flatMap(({ assets }) => + assets + .filter(({ name }) => name.startsWith('latest')) + .map(({ id, name }) => [ + `https://api.github.com/repos/stablyai/orca/releases/assets/${id}`, + name === 'latest.yml' ? 'path: orca-windows-setup.exe\n' : '' + ]) + ) + ) + const fetchImpl = vi.fn(async (url, options) => { + if (url === 'https://api.github.com/repos/stablyai/orca/releases?per_page=100') { + return jsonResponse(releases) + } + if (manifestUrls.has(url) && !options.method) { + return jsonResponse(manifestUrls.get(url)) + } + if ( + url === 'https://api.github.com/repos/stablyai/orca/releases/7' && + options.method === 'PATCH' + ) { + return jsonResponse({ tag_name: 'v1.4.2-rc.7', draft: false }) + } + throw new Error(`Unexpected release request: ${options.method ?? 'GET'} ${url}`) + }) + const fetch = vi.spyOn(globalThis, 'fetch').mockImplementation(fetchImpl) + try { + const result = await publishCompleteDraftReleases({ + repo: 'stablyai/orca', + token: 'token', + fetchImpl, + isDraftBuiltFromCurrentRef: ({ tag }) => isTagBuiltFromCurrentRef(tag, { cwd: repo }), + log: vi.fn() + }) + expect(result).toEqual({ + published: ['v1.4.2-rc.7'], + skipped: [ + { tag: 'v1.4.2-rc.8', reason: 'Release v1.4.2-rc.8 is missing required assets.' } + ] + }) + expect(fetchImpl.mock.calls.filter(([, options]) => options.method === 'PATCH')).toEqual([ + [ + 'https://api.github.com/repos/stablyai/orca/releases/7', + expect.objectContaining({ + method: 'PATCH', + body: JSON.stringify({ draft: false, prerelease: true }) + }) + ] + ]) + } finally { + fetch.mockRestore() + } + }) + }) + + it('skips stale complete drafts before publishing', async () => { + await withGitRepo(async (repo) => { + commit(repo, 'old release source') + git(repo, ['tag', 'v1.4.2-rc.7']) + commit(repo, 'current release source') + const fetchImpl = vi.fn().mockResolvedValueOnce( jsonResponse([ { id: 7, draft: true, tag_name: 'v1.4.2-rc.7', created_at: '2026-05-15T07:31:19Z', - author: { login: 'github-actions[bot]' } - }, - { - id: 8, - draft: true, - tag_name: 'v1.4.2-rc.8', - created_at: '2026-05-15T10:51:57Z', - author: { login: 'github-actions[bot]' } + author: { login: 'github-actions[bot]' }, + assets: getRequiredReleaseAssetNames('v1.4.2-rc.7').map((name) => ({ + name, + state: 'uploaded', + size: 1 + })) } ]) ) - .mockResolvedValueOnce(jsonResponse({ tag_name: 'v1.4.2-rc.7', draft: false })) - const verifyReleaseAssets = vi.fn(async ({ tag }) => { - if (tag === 'v1.4.2-rc.8') { - throw new Error('Release v1.4.2-rc.8 is missing required assets.') + const fetch = vi.spyOn(globalThis, 'fetch').mockImplementation(fetchImpl) + try { + const result = await publishCompleteDraftReleases({ + repo: 'stablyai/orca', + token: 'token', + fetchImpl, + isDraftBuiltFromCurrentRef: ({ tag }) => isTagBuiltFromCurrentRef(tag, { cwd: repo }), + log: vi.fn() + }) + expect(result).toEqual({ + published: [], + skipped: [{ tag: 'v1.4.2-rc.7', reason: 'tag is not built from the current release ref' }] + }) + expect(fetchImpl).toHaveBeenCalledTimes(1) + } finally { + fetch.mockRestore() } }) - const log = vi.fn() - - const result = await publishCompleteDraftReleases({ - repo: 'stablyai/orca', - token: 'token', - fetchImpl, - verifyReleaseAssets, - isDraftBuiltFromCurrentRef: vi.fn(async () => true), - log - }) - - expect(result).toEqual({ - published: ['v1.4.2-rc.7'], - skipped: [ - { - tag: 'v1.4.2-rc.8', - reason: 'Release v1.4.2-rc.8 is missing required assets.' - } - ] - }) - expect(fetchImpl).toHaveBeenLastCalledWith( - 'https://api.github.com/repos/stablyai/orca/releases/7', - expect.objectContaining({ - method: 'PATCH', - body: JSON.stringify({ draft: false, prerelease: true }) - }) - ) - }) - - it('skips stale complete drafts before publishing', async () => { - const fetchImpl = vi.fn().mockResolvedValueOnce( - jsonResponse([ - { - id: 7, - draft: true, - tag_name: 'v1.4.2-rc.7', - created_at: '2026-05-15T07:31:19Z', - author: { login: 'github-actions[bot]' } - } - ]) - ) - const verifyReleaseAssets = vi.fn() - const log = vi.fn() - - const result = await publishCompleteDraftReleases({ - repo: 'stablyai/orca', - token: 'token', - fetchImpl, - verifyReleaseAssets, - isDraftBuiltFromCurrentRef: vi.fn(async () => false), - log - }) - - expect(result).toEqual({ - published: [], - skipped: [{ tag: 'v1.4.2-rc.7', reason: 'tag is not built from the current release ref' }] - }) - expect(verifyReleaseAssets).not.toHaveBeenCalled() - expect(fetchImpl).toHaveBeenCalledTimes(1) }) }) diff --git a/config/scripts/quick-open-exclusion-benchmark.mjs b/config/scripts/quick-open-exclusion-benchmark.mjs index 399302c5a2b..987518f0518 100644 --- a/config/scripts/quick-open-exclusion-benchmark.mjs +++ b/config/scripts/quick-open-exclusion-benchmark.mjs @@ -25,6 +25,11 @@ function before(relPath, prefixes) { } return false } +for (const predicate of [before, after]) { + assert.equal(predicate('packages/app', ['packages/app']), true) + assert.equal(predicate('packages/app/file.ts', ['packages/app']), true) + assert.equal(predicate('packages/app2/file.ts', ['packages/app']), false) +} const files = Array.from( { length: 100000 }, (_, index) => `src/components/group-${index % 100}/file-${index}.tsx` diff --git a/config/scripts/ratchet-baseline.test.mjs b/config/scripts/ratchet-baseline.test.mjs index 922c4e51968..758b429d97a 100644 --- a/config/scripts/ratchet-baseline.test.mjs +++ b/config/scripts/ratchet-baseline.test.mjs @@ -137,7 +137,9 @@ describe('count baseline', () => { ['src/a.ts', 1], ['src/c.ts', 0] ]) - expect(parseCountBaseline(formatCountBaseline(counts, ['# header']))).toEqual( + const formatted = formatCountBaseline(counts, ['# header']) + expect(formatted).toBe('# header\n1 src/a.ts\n2 src/b.ts\n') + expect(parseCountBaseline(formatted)).toEqual( new Map([ ['src/a.ts', 1], ['src/b.ts', 2] diff --git a/src/main/agent-hooks/wsl-hook-relay-launch.test.ts b/src/main/agent-hooks/wsl-hook-relay-launch.test.ts index 8b23e41edd2..bbe9a0fba22 100644 --- a/src/main/agent-hooks/wsl-hook-relay-launch.test.ts +++ b/src/main/agent-hooks/wsl-hook-relay-launch.test.ts @@ -1,4 +1,8 @@ import { describe, expect, it, vi } from 'vitest' +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { resetWslInteropSpawnDirectoryCache } from '../wsl-interop-spawn-directory' const { spawnMock } = vi.hoisted(() => ({ spawnMock: vi.fn((..._args: unknown[]) => ({ pid: 1 })) @@ -9,14 +13,30 @@ vi.mock('node:child_process', () => ({ spawn: spawnMock })) import { spawnWslRelayProcess } from './wsl-hook-relay-launch' describe('spawnWslRelayProcess', () => { - it('names an explicit Windows directory rather than inheriting one', () => { - spawnWslRelayProcess('Ubuntu', {}, '1.2.3') + it('launches from the system directory instead of the current worktree', () => { + const systemRoot = mkdtempSync(join(tmpdir(), 'wsl-relay-launch-')) + const env = { ORCA_AGENT_HOOK_PORT: '1234' } + vi.stubEnv('SystemRoot', systemRoot) + resetWslInteropSpawnDirectoryCache() + try { + spawnWslRelayProcess('Ubuntu', env, '1.2.3') - // A stable Windows cwd prevents deleted WSL worktrees from breaking future CreateProcessW calls. - expect(spawnMock).toHaveBeenCalledWith( - 'wsl.exe', - expect.arrayContaining(['-d', 'Ubuntu', '--exec']), - expect.objectContaining({ cwd: expect.any(String) }) - ) + expect(spawnMock).toHaveBeenCalledExactlyOnceWith( + 'wsl.exe', + [ + '-d', + 'Ubuntu', + '--exec', + 'sh', + '-c', + 'exec sh "$HOME/.orca-wsl/hook-relay/1.2.3/launch.sh"' + ], + { env, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true, cwd: systemRoot } + ) + } finally { + vi.unstubAllEnvs() + resetWslInteropSpawnDirectoryCache() + rmSync(systemRoot, { recursive: true, force: true }) + } }) }) diff --git a/src/main/agent-hooks/wsl-hook-relay-reattach.test.ts b/src/main/agent-hooks/wsl-hook-relay-reattach.test.ts index 8e9e425a05c..e9d69fe5c45 100644 --- a/src/main/agent-hooks/wsl-hook-relay-reattach.test.ts +++ b/src/main/agent-hooks/wsl-hook-relay-reattach.test.ts @@ -3,13 +3,14 @@ import { describe, expect, it, vi } from 'vitest' import { ensureWslHookRelayForReattach } from './wsl-hook-relay-reattach' describe('ensureWslHookRelayForReattach', () => { - it('refreshes the surviving session distro after a local reattach', () => { + it('refreshes exact surviving distros across local reattachments', () => { const ensure = vi.fn() - ensureWslHookRelayForReattach({ isReattach: true, wslDistro: 'Ubuntu-24.04' }, null, ensure) + for (const wslDistro of ['Ubuntu-24.04', 'Ubuntu', 'Debian']) { + ensureWslHookRelayForReattach({ isReattach: true, wslDistro }, null, ensure) + } - expect(ensure).toHaveBeenCalledOnce() - expect(ensure).toHaveBeenCalledWith('Ubuntu-24.04') + expect(ensure.mock.calls).toEqual([['Ubuntu-24.04'], ['Ubuntu'], ['Debian']]) }) it.each([ @@ -25,13 +26,4 @@ describe('ensureWslHookRelayForReattach', () => { expect(ensure).not.toHaveBeenCalled() }) - - it('preserves exact distro ownership across multiple local reattachments', () => { - const ensure = vi.fn() - - ensureWslHookRelayForReattach({ isReattach: true, wslDistro: 'Ubuntu' }, null, ensure) - ensureWslHookRelayForReattach({ isReattach: true, wslDistro: 'Debian' }, null, ensure) - - expect(ensure.mock.calls).toEqual([['Ubuntu'], ['Debian']]) - }) }) diff --git a/src/main/browser/browser-grab-payload.test.ts b/src/main/browser/browser-grab-payload.test.ts index 575d67375a9..1a4d6db2109 100644 --- a/src/main/browser/browser-grab-payload.test.ts +++ b/src/main/browser/browser-grab-payload.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' import { clampGrabPayload } from './browser-grab-payload' -function makeRawPayload(overrides: Record = {}): Record { +function makeRawPayload(overrides: Record = {}) { return { page: { sanitizedUrl: 'https://example.com/page?access_token=secret#hash', @@ -66,7 +66,7 @@ describe('clampGrabPayload', () => { const payload = clampGrabPayload( makeRawPayload({ target: { - ...(makeRawPayload().target as Record), + ...makeRawPayload().target, cssClasses: 'primary access_token=secret' } }) @@ -79,7 +79,7 @@ describe('clampGrabPayload', () => { const payload = clampGrabPayload( makeRawPayload({ target: { - ...(makeRawPayload().target as Record), + ...makeRawPayload().target, elementPath: 'main > button[aria-label="access_token=secret"]', fullPath: 'body > main > button#client_secret', reactComponents: ' ', @@ -110,7 +110,7 @@ describe('clampGrabPayload', () => { const payload = clampGrabPayload( makeRawPayload({ target: { - ...(makeRawPayload().target as Record), + ...makeRawPayload().target, nearbyElements: Array.from({ length: 10 }, (_, index) => `item-${index}`), reactComponents: ''.repeat(200), sourceFile: 'src/Button.tsx:12:4' @@ -118,8 +118,15 @@ describe('clampGrabPayload', () => { }) ) - expect(payload?.target.nearbyElements).toHaveLength(6) - expect(payload?.target.reactComponents?.length).toBeLessThanOrEqual(512) + expect(payload?.target.nearbyElements).toEqual([ + 'item-0', + 'item-1', + 'item-2', + 'item-3', + 'item-4', + 'item-5' + ]) + expect(payload?.target.reactComponents).toBe(`${''.repeat(100)} (truncated)`) expect(payload?.target.sourceFile).toBe('src/Button.tsx:12:4') }) @@ -127,11 +134,11 @@ describe('clampGrabPayload', () => { const payload = clampGrabPayload( makeRawPayload({ page: { - ...(makeRawPayload().page as Record), + ...makeRawPayload().page, sanitizedUrl: 'javascript:alert(1)' }, target: { - ...(makeRawPayload().target as Record), + ...makeRawPayload().target, attributes: { href: 'javascript:alert(1)', src: 'data:text/html,', @@ -148,4 +155,60 @@ describe('clampGrabPayload', () => { expect(payload?.target.attributes.action).toBe('') expect(payload?.target.attributes.title).toBe('Safe label') }) + + it('keeps safe and ARIA attributes while stripping unsafe names', () => { + const payload = clampGrabPayload( + makeRawPayload({ + target: { + ...makeRawPayload().target, + attributes: { + ID: 'checkout', + class: 'source-code stateful auth token', + role: 'button', + type: 'button', + 'aria-label': 'Submit', + 'aria-labelledby': 'checkout-label', + 'aria-hidden': 'false', + onclick: 'alert(1)', + style: 'display:none', + 'data-secret': 'hidden', + 'notaria-label': 'unexpected' + } + } + }) + ) + + expect(payload?.target.attributes).toEqual({ + id: 'checkout', + class: 'source-code stateful auth token', + role: 'button', + type: 'button', + 'aria-label': 'Submit', + 'aria-labelledby': 'checkout-label', + 'aria-hidden': 'false' + }) + }) + + it.each([ + 'access_token=credential', + 'API_KEY=credential', + 'password=credential', + 'secret=credential', + 'session_id=credential', + 'csrf=credential' + ])('redacts %s in allowed attribute values', (value) => { + const payload = clampGrabPayload( + makeRawPayload({ + target: { + ...makeRawPayload().target, + attributes: { title: value, 'aria-label': value } + } + }) + ) + + expect(payload?.target.attributes).toEqual({ + title: '[redacted]', + 'aria-label': '[redacted]' + }) + }) }) diff --git a/src/shared/browser-client-host-protocol.test.ts b/src/shared/browser-client-host-protocol.test.ts index 71e4d22be38..908b3d9c5f5 100644 --- a/src/shared/browser-client-host-protocol.test.ts +++ b/src/shared/browser-client-host-protocol.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from 'vitest' -import { z } from 'zod' import { REMOTE_RUNTIME_MAX_SUBSCRIPTION_PARAM_BYTES } from './remote-runtime-memory-limits' import { BROWSER_CLIENT_HOST_PAGE_INVENTORY_MAX_BYTES, @@ -238,41 +237,6 @@ describe('browser client-host control protocol', () => { ).toThrow('Browser page inventory identity exceeds its JSON byte budget') }) - it('keeps old attach and ready decoders compatible with optional inventory fields', () => { - const legacyAttach = z.object({ - authorityRuntimeId: z.string(), - browserHostClientId: z.string(), - hostCapabilities: z.array(z.string()), - pageCommandProtocolVersion: z.literal(1).optional() - }) - const legacyReady = z.object({ - type: z.literal('ready'), - authorityEpoch: z.string(), - browserHostGeneration: z.number(), - pageCommandProtocolVersion: z.literal(1).optional() - }) - - expect( - legacyAttach.parse({ - authorityRuntimeId: 'runtime-a', - browserHostClientId: 'host-a', - hostCapabilities: ['webview'], - pageInventoryProtocolVersion: 1, - pageInventory: [], - leaseReconnectProtocolVersion: 1 - }) - ).not.toHaveProperty('pageInventory') - expect( - legacyReady.parse({ - type: 'ready', - authorityEpoch: 'epoch-a', - browserHostGeneration: 2, - pageInventoryProtocolVersion: 1, - leaseReconnectProtocolVersion: 1 - }) - ).not.toHaveProperty('pageInventoryProtocolVersion') - }) - it('binds create-page commands and results to exact bounded authority', () => { const authority = { authorityRuntimeId: 'runtime-a', @@ -359,24 +323,35 @@ describe('browser client-host control protocol', () => { ).toThrow() }) - it('requires every lease and execution-host fence on tunnel attach', () => { + it.each([ + { scope: 'attach', field: 'authorityRuntimeId' }, + { scope: 'attach', field: 'authorityEpoch' }, + { scope: 'attach', field: 'browserHostClientId' }, + { scope: 'attach', field: 'browserHostGeneration' }, + { scope: 'attach', field: 'executionHost' }, + { scope: 'executionHost', field: 'kind' }, + { scope: 'executionHost', field: 'runtimeId' }, + { scope: 'executionHost', field: 'revision' } + ])('requires $field in $scope on tunnel attach', ({ scope, field }) => { + const attach = { + authorityRuntimeId: 'runtime-a', + authorityEpoch: 'epoch-a', + browserHostClientId: 'host-a', + browserHostGeneration: 1, + executionHost: { kind: 'native', runtimeId: 'runtime-a', revision: 1 } + } + expect(BrowserNetworkTunnelAttachParams.parse(attach)).toEqual(attach) + + const incomplete = Object.fromEntries( + Object.entries(scope === 'attach' ? attach : attach.executionHost).filter( + ([key]) => key !== field + ) + ) expect(() => - BrowserNetworkTunnelAttachParams.parse({ - authorityRuntimeId: 'runtime-a', - browserHostClientId: 'host-a', - browserHostGeneration: 1, - executionHost: { kind: 'native', runtimeId: 'runtime-a', revision: 1 } - }) + BrowserNetworkTunnelAttachParams.parse( + scope === 'attach' ? incomplete : { ...attach, executionHost: incomplete } + ) ).toThrow() - expect( - BrowserNetworkTunnelAttachParams.parse({ - authorityRuntimeId: 'runtime-a', - authorityEpoch: 'epoch-a', - browserHostClientId: 'host-a', - browserHostGeneration: 1, - executionHost: { kind: 'native', runtimeId: 'runtime-a', revision: 1 } - }) - ).toMatchObject({ authorityEpoch: 'epoch-a', browserHostGeneration: 1 }) }) it('decodes an exact SSH provider authority without changing native v1 attaches', () => { diff --git a/src/shared/browser-client-host-reconciliation-protocol.test.ts b/src/shared/browser-client-host-reconciliation-protocol.test.ts index 9ce314ee9b2..86eb9b375cc 100644 --- a/src/shared/browser-client-host-reconciliation-protocol.test.ts +++ b/src/shared/browser-client-host-reconciliation-protocol.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from 'vitest' -import { z } from 'zod' import { BROWSER_CLIENT_HOST_PAGE_RECONCILIATION_PROTOCOL_VERSION, BrowserClientHostAttachParams, @@ -77,43 +76,6 @@ describe('browser client-host reconciliation protocol', () => { } }) - it('keeps optional negotiation fields invisible to old attach and ready decoders', () => { - const legacyAttach = z.object({ - authorityRuntimeId: z.string(), - browserHostClientId: z.string(), - hostCapabilities: z.array(z.string()), - pageCommandProtocolVersion: z.literal(1).optional() - }) - const legacyReady = z.object({ - type: z.literal('ready'), - authorityEpoch: z.string(), - browserHostGeneration: z.number(), - pageCommandProtocolVersion: z.literal(1).optional() - }) - - expect( - legacyAttach.parse({ - authorityRuntimeId: 'runtime-a', - browserHostClientId: 'host-a', - hostCapabilities: ['webview'], - pageCommandProtocolVersion: 1, - pageInventoryProtocolVersion: 1, - pageInventory: [], - pageReconciliationProtocolVersion: 1 - }) - ).not.toHaveProperty('pageReconciliationProtocolVersion') - expect( - legacyReady.parse({ - type: 'ready', - authorityEpoch: 'epoch-new', - browserHostGeneration: 4, - pageCommandProtocolVersion: 1, - pageInventoryProtocolVersion: 1, - pageReconciliationProtocolVersion: 1 - }) - ).not.toHaveProperty('pageReconciliationProtocolVersion') - }) - it('decodes exact reclaim, close, and restore commands only after negotiation', () => { const previousAuthority = { authorityRuntimeId: 'runtime-a', @@ -180,16 +142,19 @@ describe('browser client-host reconciliation protocol', () => { ).toThrow() }) - it('leaves legacy create and navigate commands unchanged', () => { - const legacyCommand = { - ...command({ - type: 'createPage', - browserProfileId: 'profile-a', - executionHostKey: 'native:runtime-a:1' - }) - } - delete (legacyCommand as { pageReconciliationProtocolVersion?: number }) - .pageReconciliationProtocolVersion + it.each([ + { + type: 'createPage', + browserProfileId: 'profile-a', + executionHostKey: 'native:runtime-a:1' + }, + { type: 'navigate', url: 'https://remote.internal/' } + ])('leaves legacy $type commands unchanged', (legacyPageCommand) => { + const legacyCommand = Object.fromEntries( + Object.entries(command(legacyPageCommand)).filter( + ([key]) => key !== 'pageReconciliationProtocolVersion' + ) + ) expect(BrowserClientHostCommandEvent.parse(legacyCommand)).toEqual(legacyCommand) }) diff --git a/src/shared/browser-client-hosting-eligibility.test.ts b/src/shared/browser-client-hosting-eligibility.test.ts index bc7a24519b1..023c98415eb 100644 --- a/src/shared/browser-client-hosting-eligibility.test.ts +++ b/src/shared/browser-client-hosting-eligibility.test.ts @@ -1,15 +1,22 @@ import { describe, expect, it } from 'vitest' import { - BROWSER_CLIENT_HOSTING_RUNTIME_CAPABILITIES, expectsBrowserClientHosting, runtimeAdvertisesBrowserClientHosting } from './browser-client-hosting-eligibility' +const CLIENT_HOST_CAPABILITIES = [ + 'browser.clientHost.v1', + 'browser.clientHost.pageMetadata.v1', + 'browser.clientHost.automation.v1', + 'network.browserTunnel.v1', + 'network.browserTunnel.executionHosts.v1' +] + const ELIGIBLE = { enabled: true, preference: 'auto' as const, deviceScope: 'desktop', - capabilities: [...BROWSER_CLIENT_HOSTING_RUNTIME_CAPABILITIES, 'browser.screencast.v1'] + capabilities: [...CLIENT_HOST_CAPABILITIES, 'browser.screencast.v1'] } describe('expectsBrowserClientHosting', () => { @@ -30,9 +37,7 @@ describe('expectsBrowserClientHosting', () => { expect(expectsBrowserClientHosting({ ...ELIGIBLE, ...input })).toBe(false) }) - // Why each capability gets its own case: the set is the contract between this client's guest - // renderer and the runtime, and dropping any one of them leaves a host that cannot serve it. - it.each(BROWSER_CLIENT_HOSTING_RUNTIME_CAPABILITIES)('refuses without %s', (missing) => { + it.each(CLIENT_HOST_CAPABILITIES)('refuses without %s', (missing) => { const capabilities = ELIGIBLE.capabilities.filter((capability) => capability !== missing) expect(runtimeAdvertisesBrowserClientHosting(capabilities)).toBe(false) diff --git a/src/shared/browser-grab-types.test.ts b/src/shared/browser-grab-types.test.ts deleted file mode 100644 index 12d26556074..00000000000 --- a/src/shared/browser-grab-types.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { - GRAB_SAFE_ATTRIBUTE_NAMES, - GRAB_SECRET_PATTERNS, - isAriaAttribute -} from './browser-grab-types' - -describe('browser-grab-types', () => { - describe('isAriaAttribute', () => { - it('returns true for aria- prefixed attributes', () => { - expect(isAriaAttribute('aria-label')).toBe(true) - expect(isAriaAttribute('aria-labelledby')).toBe(true) - expect(isAriaAttribute('aria-hidden')).toBe(true) - }) - - it('returns false for non-aria attributes', () => { - expect(isAriaAttribute('class')).toBe(false) - expect(isAriaAttribute('id')).toBe(false) - expect(isAriaAttribute('notaria-label')).toBe(false) - }) - }) - - describe('GRAB_SAFE_ATTRIBUTE_NAMES', () => { - it('includes core safe attributes', () => { - expect(GRAB_SAFE_ATTRIBUTE_NAMES.has('id')).toBe(true) - expect(GRAB_SAFE_ATTRIBUTE_NAMES.has('class')).toBe(true) - expect(GRAB_SAFE_ATTRIBUTE_NAMES.has('role')).toBe(true) - expect(GRAB_SAFE_ATTRIBUTE_NAMES.has('type')).toBe(true) - }) - - it('does not include unsafe attributes', () => { - expect(GRAB_SAFE_ATTRIBUTE_NAMES.has('onclick')).toBe(false) - expect(GRAB_SAFE_ATTRIBUTE_NAMES.has('style')).toBe(false) - expect(GRAB_SAFE_ATTRIBUTE_NAMES.has('data-secret')).toBe(false) - }) - }) - - describe('GRAB_SECRET_PATTERNS', () => { - it('includes precise secret patterns', () => { - expect(GRAB_SECRET_PATTERNS).toContain('access_token') - expect(GRAB_SECRET_PATTERNS).toContain('api_key') - expect(GRAB_SECRET_PATTERNS).toContain('password') - expect(GRAB_SECRET_PATTERNS).toContain('secret') - expect(GRAB_SECRET_PATTERNS).toContain('session_id') - expect(GRAB_SECRET_PATTERNS).toContain('csrf') - }) - - it('does not include overly broad patterns', () => { - // Why: 'code' and 'state' match normal CSS classes like 'source-code' - // and 'stateful', causing false positive redactions on most sites. - expect(GRAB_SECRET_PATTERNS).not.toContain('code') - expect(GRAB_SECRET_PATTERNS).not.toContain('state') - expect(GRAB_SECRET_PATTERNS).not.toContain('auth') - expect(GRAB_SECRET_PATTERNS).not.toContain('token') - }) - }) -}) diff --git a/src/shared/browser-network-tunnel-stream-framing.test.ts b/src/shared/browser-network-tunnel-stream-framing.test.ts index c4efcbca83a..eeda648baaf 100644 --- a/src/shared/browser-network-tunnel-stream-framing.test.ts +++ b/src/shared/browser-network-tunnel-stream-framing.test.ts @@ -27,19 +27,20 @@ describe('browser network tunnel stream framing', () => { expect(errors).toEqual([]) }) - it('fails closed on oversized or empty frames', () => { - const oversizedError = vi.fn() - const oversized = new Uint8Array(4) - new DataView(oversized.buffer).setUint32(0, 2 * 1024 * 1024, false) - const oversizedDecoder = new BrowserNetworkTunnelStreamFrameDecoder(() => {}, oversizedError) - const emptyError = vi.fn() - const emptyDecoder = new BrowserNetworkTunnelStreamFrameDecoder(() => {}, emptyError) + it.each([0, 2 * 1024 * 1024])('stays closed after rejecting a %i-byte frame', (length) => { + const onFrame = vi.fn() + const onError = vi.fn() + const header = new Uint8Array(4) + new DataView(header.buffer).setUint32(0, length, false) + const decoder = new BrowserNetworkTunnelStreamFrameDecoder(onFrame, onError) - oversizedDecoder.feed(oversized) - emptyDecoder.feed(new Uint8Array([0, 0, 0, 0])) + decoder.feed(header) + decoder.feed(encodeBrowserNetworkTunnelStreamFrame(new Uint8Array([1]))) - expect(oversizedError).toHaveBeenCalledOnce() - expect(emptyError).toHaveBeenCalledOnce() + expect(onFrame).not.toHaveBeenCalled() + expect(onError).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ message: 'browser_tunnel_stream_frame_invalid' }) + ) }) it('fails the stream when a frame consumer rejects input', () => { diff --git a/src/shared/browser-screencast-protocol.test.ts b/src/shared/browser-screencast-protocol.test.ts index a2a9bec083f..151b670a4b1 100644 --- a/src/shared/browser-screencast-protocol.test.ts +++ b/src/shared/browser-screencast-protocol.test.ts @@ -76,16 +76,36 @@ describe('browser screencast binary protocol', () => { expect(decodeBrowserScreencastFrame(encoded)).toBeNull() }) - it('rejects oversized metadata before decoding it', () => { - const encoded = new Uint8Array(16 + BROWSER_SCREENCAST_MAX_METADATA_BYTES + 1) - const view = new DataView(encoded.buffer) - encoded[0] = 0x62 - encoded[1] = 1 - encoded[2] = BrowserScreencastOpcode.Frame - encoded[3] = 1 - view.setUint32(8, BROWSER_SCREENCAST_MAX_METADATA_BYTES + 1, true) + it('rejects oversized valid metadata before decoding it', () => { + const metadata = { deviceWidth: 1280, padding: '' } + metadata.padding = 'x'.repeat( + BROWSER_SCREENCAST_MAX_METADATA_BYTES - + new TextEncoder().encode(JSON.stringify(metadata)).byteLength + ) + const withinBudget = encodeBrowserScreencastFrame({ + opcode: BrowserScreencastOpcode.Frame, + seq: 1, + format: 'jpeg', + metadata, + image: new Uint8Array([1]) + }) + expect(decodeBrowserScreencastFrame(withinBudget)?.metadata).toEqual({ deviceWidth: 1280 }) - expect(decodeBrowserScreencastFrame(encoded)).toBeNull() + const oversizedMetadata = { ...metadata, padding: `${metadata.padding}x` } + const oversized = encodeBrowserScreencastFrame({ + opcode: BrowserScreencastOpcode.Frame, + seq: 1, + format: 'jpeg', + metadata: oversizedMetadata, + image: new Uint8Array([1]) + }) + const decode = vi.spyOn(TextDecoder.prototype, 'decode') + try { + expect(decodeBrowserScreencastFrame(oversized)).toBeNull() + expect(decode).not.toHaveBeenCalled() + } finally { + decode.mockRestore() + } }) it('rejects excessive metadata nesting before JSON.parse', () => {