mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
test: replace self-fulfilling checks with production boundary assertions (#33006)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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([])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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']])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { clampGrabPayload } from './browser-grab-payload'
|
||||
|
||||
function makeRawPayload(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
||||
function makeRawPayload(overrides: Record<string, unknown> = {}) {
|
||||
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<string, unknown>),
|
||||
...makeRawPayload().target,
|
||||
cssClasses: 'primary access_token=secret'
|
||||
}
|
||||
})
|
||||
@@ -79,7 +79,7 @@ describe('clampGrabPayload', () => {
|
||||
const payload = clampGrabPayload(
|
||||
makeRawPayload({
|
||||
target: {
|
||||
...(makeRawPayload().target as Record<string, unknown>),
|
||||
...makeRawPayload().target,
|
||||
elementPath: 'main > button[aria-label="access_token=secret"]',
|
||||
fullPath: 'body > main > button#client_secret',
|
||||
reactComponents: '<App> <PasswordSecretPanel>',
|
||||
@@ -110,7 +110,7 @@ describe('clampGrabPayload', () => {
|
||||
const payload = clampGrabPayload(
|
||||
makeRawPayload({
|
||||
target: {
|
||||
...(makeRawPayload().target as Record<string, unknown>),
|
||||
...makeRawPayload().target,
|
||||
nearbyElements: Array.from({ length: 10 }, (_, index) => `item-${index}`),
|
||||
reactComponents: '<App>'.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(`${'<App>'.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<string, unknown>),
|
||||
...makeRawPayload().page,
|
||||
sanitizedUrl: 'javascript:alert(1)'
|
||||
},
|
||||
target: {
|
||||
...(makeRawPayload().target as Record<string, unknown>),
|
||||
...makeRawPayload().target,
|
||||
attributes: {
|
||||
href: 'javascript:alert(1)',
|
||||
src: 'data:text/html,<script>alert(1)</script>',
|
||||
@@ -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]'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user