[CI] Reduce redundant test work and import existing helpers directly (#28365)

* [CI] Reduce redundant test work and import existing helpers directly

* [CI] Preserve provider guards and pin the mobile browser test core

* [CI] Fix CI test setup and verify retained contracts

* [CI] Compare native media chunks without boxed byte arrays

* [CI] Detect shifted native media reads with position-sensitive bytes
This commit is contained in:
Neil
2026-10-13 20:25:00 -07:00
committed by GitHub
parent abe2e8118d
commit 13a75d3939
35 changed files with 225 additions and 767 deletions
+1 -1
View File
@@ -903,7 +903,7 @@ jobs:
- name: Install WebKit for the drawer check
id: webkit
background: true
run: pnpm exec playwright install --with-deps webkit
run: pnpm exec playwright-core install --with-deps webkit
# The entry lives in mobile/ so one React resolves; without this every RN import is nothing.
- uses: ./.github/actions/install-mobile-dependencies
@@ -11,19 +11,15 @@ import {
import {
MOBILE_WEB_APP_NATIVE_PARITY_STYLE,
MOBILE_WEB_APP_ROOT_RESET,
MOBILE_WEB_APP_SHIMS,
bundleMobileWebApp,
buildMobileWebAppBundle,
entryStaticClosure,
mobileWebAppBuildOptions,
renameOutputsByContent,
routeChunkNames
} from './build-mobile-web-app-bundle.mjs'
import {
MOBILE_WEB_APP_ROUTE_ROOT,
ROUTE_SOURCE_LOADERS,
collectMobileWebAppRouteKeys,
collectMobileWebAppRoutes
collectMobileWebAppRouteKeys
} from './mobile-web-app-route-manifest.mjs'
import {
MOBILE_WEB_APP_BUNDLE_MAX_ENTRY_BYTES,
@@ -42,7 +38,6 @@ import {
BINARY_SOURCE_EXTENSIONS,
assertNoCarriageReturnsInSource
} from './mobile-web-source-line-endings.mjs'
import { spelledCountsAgainstTables } from './spelled-count-census.mjs'
import {
hashedAsset,
readDesktopVersion,
@@ -87,34 +82,6 @@ describe('the CRLF pin', () => {
})
describeBundling('the app bundle', () => {
it('isolates read-only fixture consumers from mutations in another assertion', async () => {
const first = await readAppBundle()
const original = first.script[0]
first.script[0] ^= 255
first.chunks.length = 0
first.routeKeys.length = 0
const next = await readAppBundle()
expect(next.script[0]).toBe(original)
expect(next.chunks.length).toBeGreaterThan(0)
expect(next.routeKeys.length).toBeGreaterThan(0)
const written = await readWrittenBundle()
written.manifest.assets.length = 0
expect((await readWrittenBundle()).manifest.assets.length).toBeGreaterThan(0)
const originalByte = written.files[0].bytes[0]
written.files[0].bytes[0] ^= 255
expect((await readWrittenBundle()).files[0].bytes[0]).toBe(originalByte)
await withScratch(async (scratch) => {
const firstDir = join(scratch, 'first')
await copyWrittenBundle(firstDir)
await writeFile(join(firstDir, 'manifest.json'), 'corrupted')
const secondDir = join(scratch, 'second')
const second = await copyWrittenBundle(secondDir)
for (const { file, bytes } of second.files) {
expect((await readFile(join(secondDir, file))).equals(bytes), file).toBe(true)
}
})
}, 120_000)
it('resolves react-native to react-native-web and leaves no require.context', async () => {
const sources = allScriptSource(await readAppBundle())
for (const source of sources) {
@@ -307,47 +274,6 @@ describeBundling('the app bundle', () => {
}
}, 120_000)
it('asks esbuild for the split the budgets assume', async () => {
const options = mobileWebAppBuildOptions(await collectMobileWebAppRoutes(appDir))
// Each of these is load-bearing for a budget below: esm and splitting are what make a route a
// chunk, and the metafile is the only thing that says which imports are static.
expect(options.format).toBe('esm')
expect(options.splitting).toBe(true)
expect(options.chunkNames).toBe('[hash]')
expect(options.metafile).toBe(true)
})
it('reads a route source the same way the export guard does', async () => {
const options = mobileWebAppBuildOptions(await collectMobileWebAppRoutes(appDir))
// The guard parses each route on its own, outside this build. Sharing the table is what stops
// a loader the bundle relies on from being missing there and reported as a syntax error.
for (const [extension, loader] of Object.entries(ROUTE_SOURCE_LOADERS)) {
expect(options.loader[extension], extension).toBe(loader)
}
})
it('applies every shim it names', async () => {
const options = mobileWebAppBuildOptions(await collectMobileWebAppRoutes(appDir))
for (const shim of MOBILE_WEB_APP_SHIMS) {
expect(shim.appliesTo(options), `${shim.name} is named but not applied`).toBe(true)
}
})
it('fails the named shim, not the whole build, when its option goes missing', async () => {
const options = mobileWebAppBuildOptions(await collectMobileWebAppRoutes(appDir))
// Each shim reads an option of its own (two read `banner.js`), so stripping every option
// leaves none applying. Without that, the list could name a shim the build stopped applying.
const stripped = {
...options,
alias: {},
loader: {},
define: {},
banner: {},
plugins: []
}
expect(MOBILE_WEB_APP_SHIMS.filter((shim) => shim.appliesTo(stripped))).toEqual([])
})
it('ships no haptic that reaches for the DOM', async () => {
// expo-haptics' web build fakes an iOS haptic by appending a hidden
// `<label><input type="checkbox" switch>` to document.head, clicking it, and removing it —
@@ -479,41 +405,6 @@ describe('the Phase C budget', () => {
120_000
)
it('says which node may be statically imported, and does not promise a route may', async () => {
const source = await readFile(
join(projectDir, 'config', 'scripts', 'verify-mobile-web-app-bundle.mjs'),
'utf8'
)
// The bound reads like a per-route escape hatch and is not one: of the fourteen routes in the
// tree, session breaks it outright at 3.32 MiB and five more spend most of it, so the hatch is
// one route away from unusable rather than free. What keeps it survivable is that expo-router
// wants a synchronous export off layout nodes only, so the note has to name the layout and the
// export that drives it.
const doc = source.slice(
0,
source.indexOf('export const MOBILE_WEB_APP_BUNDLE_MAX_ENTRY_BYTES')
)
const note = doc.slice(doc.lastIndexOf('/**'))
expect(note).toContain('h/_layout.tsx')
expect(note).toContain('unstable_settings')
})
/** The one count above that must follow the tree, spelled so the census reads it. The sweep is
* a row per route plus `_layout.tsx` and `h/_layout.tsx`, which are no screens, so it is that
* length less two. */
it('spells the route count off the sweep it is a count of', async () => {
const source = await readFile(
join(projectDir, 'config', 'scripts', 'build-mobile-web-app-bundle.test.mjs'),
'utf8'
)
const rows = [
{ precedes: 'routes in the tree', counted: MOBILE_WEB_APP_BUNDLE_SCRIPT_SWEEP.length - 2 }
]
for (const { precedes, spelled, counts } of spelledCountsAgainstTables(source, rows)) {
expect(spelled, precedes).toEqual(counts)
}
})
it('budgets what loads first well under what the whole page weighs', () => {
// The point of the split: the entry budget is the one a route must not grow, and it is a
// fraction of the total the bundle is still allowed to weigh.
@@ -163,30 +163,6 @@ describe('Windows CLI launcher', () => {
}
})
itCrossHost('never hands the child an explicit environment map', () => {
// Why: setting any entry on the child's environment makes the spawn build its own
// block from a case-insensitive map, which collapses an inherited PATH and Path
// into one entry and killed the CLI (stablyai/orca#12046). Mutating this process
// and leaving the map untouched passes the block through verbatim.
const source = readFileSync(
join(projectRoot, 'native', 'windows-cli-launcher', 'src', 'main.rs'),
'utf8'
)
const code = source.replace(/^\s*\/\/.*$/gm, '')
expect(code).not.toMatch(/\.envs?\(/u)
expect(code).not.toContain('env_clear')
expect(code).toContain('env::set_var')
})
itCrossHost('never reintroduces a managed launcher alongside the native one', () => {
// Why: the MSIL image is what vendors flagged (stablyai/orca#23383). A stray .cs
// left in the crate would compile back into the shape the rewrite removed.
expect(
existsSync(join(projectRoot, 'native', 'windows-cli-launcher', 'OrcaCliLauncher.cs'))
).toBe(false)
})
itWindows('preserves a multiline argument from PowerShell through the native launcher', () => {
const appRoot = mkdtempSync(join(tmpdir(), 'orca cli launcher '))
try {
@@ -7,13 +7,6 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { writeMobileWebBundleFixtureTree } from './mobile-web-bundle-fixture-tree.mjs'
const REPO_ROOT = join(import.meta.dirname, '..', '..')
const SRC_MAIN_DIR = join(REPO_ROOT, 'src', 'main')
const MAIN_BUILD_ENTRY_POINTS_PATH = join(
REPO_ROOT,
'config',
'build-plugins',
'main-build-entry-points.ts'
)
const require = createRequire(import.meta.url)
const electronBuilderConfig = require('../electron-builder.config.cjs')
@@ -393,11 +386,6 @@ describe('electron-builder config', () => {
it('unpacks the replaceable WSL transcript filesystem process entry', async () => {
const entryFilename = 'wsl-transcript-fs-process-entry.js'
expect(electronBuilderConfig.asarUnpack).toContain(`out/main/${entryFilename}`)
const entryPoints = await readFile(MAIN_BUILD_ENTRY_POINTS_PATH, 'utf8')
expect(entryPoints).toMatch(
new RegExp(`'${entryFilename.replace(/\.js$/, '')}':\\s*resolve\\(`)
)
})
// Why: the CLI runs this entry under ELECTRON_RUN_AS_NODE, which cannot read
@@ -422,34 +410,6 @@ describe('electron-builder config', () => {
).toBe(true)
})
// Why: the scanner service is forked with ELECTRON_RUN_AS_NODE, so asar is
// invisible to it and a packed worker entry fails closed — dropping every
// OpenCode session in packaged builds while dev stays green. Three legs must
// agree on the filename, so all three are read rather than hardcoded.
it('unpacks the foreign SQLite reader entry the scanner service runs OpenCode reads on', async () => {
const entryPathSource = await readFile(
join(SRC_MAIN_DIR, 'foreign-sqlite-readers', 'foreign-sqlite-reader-entry-path.ts'),
'utf8'
)
const entryFilename = entryPathSource.match(
/FOREIGN_SQLITE_READER_ENTRY_FILENAME = '([^']+)'/
)?.[1]
expect(entryFilename).toBeDefined()
expect(electronBuilderConfig.asarUnpack).toContain(`out/main/${entryFilename}`)
// Why: the emitted path comes from the rollup input key under
// entryFileNames '[name].js', not from the source filename — renaming the
// key alone would leave the other two legs agreeing on a file that no
// longer exists.
const viteConfig = await readFile(join(REPO_ROOT, 'electron.vite.config.ts'), 'utf8')
expect(viteConfig).toContain("entryFileNames: '[name].js'")
const entryPoints = await readFile(MAIN_BUILD_ENTRY_POINTS_PATH, 'utf8')
expect(entryPoints).toMatch(
new RegExp(`'${entryFilename.replace(/\.js$/, '')}':\\s*resolve\\(`)
)
})
it('keeps the worker-thread hang watchdog inside app.asar', () => {
expect(electronBuilderConfig.asarUnpack).not.toContain(
'out/main/main-thread-hang-watchdog-entry.js'
@@ -602,18 +562,6 @@ describe('electron-builder config', () => {
)
expect(unrecoverable).toEqual([])
})
it('keeps the pinned FpmTarget overwrite for configured deb and rpm artifacts', async () => {
const source = await readFile(
require.resolve('app-builder-lib/out/targets/FpmTarget'),
'utf8'
)
expect(source).toContain('path.join(resourceDir, "package-type"), target')
for (const target of RECOVERABLE_TARGETS) {
expect(electronBuilderConfig[target]).toBeDefined()
}
})
})
})
@@ -19,6 +19,8 @@ import {
import { createRequire } from 'node:module'
import { electronViteConfig } from '../../electron.vite.config'
import { BOOTSTRAP_FATAL_EXIT_GUARD_KEY } from '../../src/main/startup/bootstrap-fatal-exit-guard'
import { FOREIGN_SQLITE_READER_ENTRY_FILENAME } from '../../src/main/foreign-sqlite-readers/foreign-sqlite-reader-entry-path'
import { ORCAD_CHILD_ENTRY_POINTS } from './orcad-entry-build.mjs'
type BootstrapProcessMock = EventEmitter & {
env: Record<string, string>
@@ -70,7 +72,9 @@ function failBootstrapWithBanner(options: {
return processMock
}
const electronBuilderConfig = createRequire(import.meta.url)('../electron-builder.config.cjs')
const require = createRequire(import.meta.url)
const electronBuilderConfig = require('../electron-builder.config.cjs')
const { FileMatcher } = require('app-builder-lib/out/fileMatcher')
describe('Electron Vite output contract', () => {
it("minifies main and renderer with rolldown's in-process minifier", () => {
@@ -90,13 +94,38 @@ describe('Electron Vite output contract', () => {
it('keeps main-process and plain-Node entries at stable CommonJS paths', () => {
const output = electronViteConfig.main?.build?.rollupOptions?.output
if (!output || Array.isArray(output)) {
throw new Error('Expected one main-process output')
if (!output || Array.isArray(output) || typeof output.entryFileNames !== 'string') {
throw new Error('Expected one main-process output with a filename template')
}
expect(output.format).toBe('cjs')
expect(output.entryFileNames).toBe('[name].js')
expect(output.chunkFileNames).toBe('chunks/[name]-[hash].js')
const build = electronViteConfig.main?.build
const input = build?.rollupOptions?.input
if (!input || typeof input !== 'object' || Array.isArray(input)) {
throw new Error('Expected named main-process inputs')
}
const entryName = nodePath.parse(FOREIGN_SQLITE_READER_ENTRY_FILENAME).name
expect(input[entryName]).toBe(nodePath.resolve(ORCAD_CHILD_ENTRY_POINTS.foreignSqliteReader))
const outputFilename = output.entryFileNames.replace('[name]', entryName)
expect(outputFilename).toBe(FOREIGN_SQLITE_READER_ENTRY_FILENAME)
const appRoot = join(tmpdir(), 'orca-sqlite-unpack-app')
const matcher = new FileMatcher(
appRoot,
join(tmpdir(), 'orca-sqlite-unpack-destination'),
(value: string) => value,
electronBuilderConfig.asarUnpack
)
const relativeOutDir = nodePath.relative(
nodePath.resolve('.'),
nodePath.resolve(build?.outDir ?? join('out', 'main'))
)
const outputPath = join(appRoot, relativeOutDir, outputFilename)
expect(matcher.createFilter()(outputPath, { isDirectory: () => false })).toBe(true)
})
it('keeps CLI main imports unpacked at stable paths', () => {
@@ -47,17 +47,6 @@ const GUIDE_REFERENCE_PATHS = Object.entries(GUIDE_REFERENCES).flatMap(([guide,
references.map((reference) => [guide, reference])
)
async function readPerWorkspaceEnvCorpus() {
const guideRoot = path.join(projectDir, 'skill-guides')
const files = [
path.join(guideRoot, 'orca-per-workspace-env.md'),
...GUIDE_REFERENCES['orca-per-workspace-env'].map((reference) =>
path.join(guideRoot, 'orca-per-workspace-env', 'references', reference)
)
]
return (await Promise.all(files.map((file) => readFile(file, 'utf8')))).join('\n')
}
async function createFixture() {
const root = await mkdtemp(path.join(tmpdir(), 'orca-bundled-skill-guides-'))
temporaryDirectories.push(root)
@@ -97,62 +86,45 @@ describe('bundled skill guide generator', () => {
}
})
it('uses the exported recipe id variable in per-workspace environment examples', async () => {
// The guide is a kernel plus conditional references, so the env-var contract is asserted over
// the whole corpus while the name-building recipe is pinned in the file that now carries it.
const corpus = await readPerWorkspaceEnvCorpus()
const vercelReference = await readFile(
path.join(
projectDir,
'skill-guides',
'orca-per-workspace-env',
'references',
'provider-vercel.md'
),
'utf8'
)
expect(corpus).toContain('ORCA_RECIPE_ID')
expect(corpus).not.toContain('ORCA_VM_RECIPE_ID')
expect(vercelReference).toContain('recipe_id="${recipe_id//./-}"')
expect(vercelReference).toContain('max_recipe_id_length=$((128 - ${#instance_id} - 6))')
expect(vercelReference).toContain(
'name="orca-${recipe_id:0:max_recipe_id_length}-${instance_id}"'
)
})
it.skipIf(process.platform === 'win32')(
'resolves snapshot cleanup through Orca user-data precedence',
'resolves the documented cleanup path from custom, XDG, and home settings',
async () => {
const source = await readFile(
path.join(projectDir, 'skill-guides', 'orca-per-workspace-env.md'),
'utf8'
)
const assignment =
'orca_user_data_path="${ORCA_USER_DATA_PATH:-${XDG_CONFIG_HOME:-$HOME/.config}/orca}"'
expect(source).toContain(assignment)
const renderPath = async (env) =>
(
await execFileAsync(
'bash',
['-u', '-c', `${assignment}; printf '%s' "$orca_user_data_path"`],
{
env
}
)
).stdout
const assignments = [...source.matchAll(/`(orca_user_data_path=[^`\r\n]+)`/gu)].map(
(match) => match[1]
)
expect(assignments).toHaveLength(1)
const script = `${assignments[0]}\nprintf '%s' "$orca_user_data_path"`
const root = await mkdtemp(path.join(tmpdir(), 'orca-guide-cleanup-precedence-'))
temporaryDirectories.push(root)
const home = path.join(root, 'isolated home')
const xdg = path.join(root, 'xdg config')
const custom = path.join(root, 'custom runtime data')
const homeDefault = path.join(home, '.config', 'orca')
const xdgDefault = path.join(xdg, 'orca')
const cases = [
[{}, homeDefault],
[{ XDG_CONFIG_HOME: xdg }, xdgDefault],
[{ ORCA_USER_DATA_PATH: custom, XDG_CONFIG_HOME: xdg }, custom],
[{ ORCA_USER_DATA_PATH: '', XDG_CONFIG_HOME: xdg }, xdgDefault],
[{ XDG_CONFIG_HOME: '' }, homeDefault],
[{ ORCA_USER_DATA_PATH: '', XDG_CONFIG_HOME: '' }, homeDefault]
]
await expect(renderPath({ HOME: '/home/orca' })).resolves.toBe('/home/orca/.config/orca')
await expect(
renderPath({ HOME: '/home/orca', XDG_CONFIG_HOME: '/srv/config' })
).resolves.toBe('/srv/config/orca')
await expect(
renderPath({
HOME: '/home/orca',
XDG_CONFIG_HOME: '/srv/config',
ORCA_USER_DATA_PATH: '/var/lib/orca-custom'
})
).resolves.toBe('/var/lib/orca-custom')
for (const [environment, expected] of cases) {
const { stdout, stderr } = await execFileAsync(
'bash',
['--noprofile', '--norc', '-u', '-c', script],
{
env: { HOME: home, ORCA_BACKGROUND_LAUNCH: '1', ...environment }
}
)
expect(stdout).toBe(expected)
expect(stderr).toBe('')
}
}
)
@@ -258,31 +230,6 @@ describe('bundled skill guide generator', () => {
}
})
it('keeps CLI guide examples safe across shells and Linux command names', async () => {
for (const name of ['orca-cli', 'computer-use', 'orca-emulator', 'orca-emulator-android']) {
const source = await readFile(path.join(projectDir, 'skill-guides', `${name}.md`), 'utf8')
expect(source).toMatch(/^ORCA .+--json$/mu)
// Why: bare command lines can launch GNOME Orca, while shell variables make
// the same guide unusable from PowerShell and cmd.exe.
expect(source).not.toMatch(/^orca /mu)
expect(source).not.toMatch(/\$ORCA(?:_|\b)/u)
}
})
// Why: `skills get` already ran on a resolved executable, so guide bodies point back at the
// stub's resolution instead of carrying another copy of the ladder the stubs own.
it('points every guide at the executable the stub resolved', async () => {
// orchestration.md is rewritten to this contract by its own PR (#16904).
for (const name of CANONICAL_GUIDE_NAMES.filter((name) => name !== 'orchestration')) {
const source = await readFile(path.join(projectDir, 'skill-guides', `${name}.md`), 'utf8')
expect(source.replace(/\s+/gu, ' '), name).toContain(
'the executable you resolved in the stub'
)
}
})
it('builds deterministic artifacts and verifies the checked-in outputs', async () => {
const first = await buildArtifacts(projectDir)
const second = await buildArtifacts(projectDir)
@@ -396,30 +343,11 @@ describe('bundled skill guide generator', () => {
const blocks = await readSharedStubBlocks(projectDir)
expect([...blocks.keys()]).toEqual(['resolver', 'no-guessing'])
// Why: the guide copies of this warning had each dropped one half. #7904 is the incident
// where bare `orca` started the screen reader talking on a user's Ubuntu box.
expect(blocks.get('resolver').text).toContain('(`/usr/bin/orca`)')
expect(blocks.get('resolver').text).toContain("starts speech on the user's machine")
for (const name of STUB_TOPICS) {
const projection = await readFile(path.join(projectDir, 'skills', name, 'SKILL.md'), 'utf8')
for (const [id, block] of blocks) {
expect(projection.split(block.text), `${name}/${id}`).toHaveLength(2)
}
// The `ORCA` placeholder rule is stated once, in the fragment, never restated.
expect(projection.split('is a placeholder for the executable'), name).toHaveLength(2)
}
})
// G2, second half: the ladder is pre-resolution guidance and belongs only to the stub —
// every path that delivers a guide body has already resolved an executable. Guides keep
// the `ORCA` placeholder rule. Red until the guide bodies drop their ladders; retiring
// those also retires the ORCA_CLI_COMMAND/orca-dev/orca-ide assertions in
// 'keeps CLI guide examples safe across shells and Linux command names' above, which
// pin the opposite contract.
it('keeps the CLI resolver ladder out of every guide body', async () => {
for (const name of CANONICAL_GUIDE_NAMES) {
const source = await readFile(path.join(projectDir, 'skill-guides', `${name}.md`), 'utf8')
expect(source, name).not.toContain('ORCA_CLI_COMMAND')
}
})
@@ -66,12 +66,6 @@ function routesDeclaring(token) {
).map((route) => route.pathname)
}
describe('the grant token this census is written against', () => {
it('is the one the shell declares', () => {
expect(externalNavigationGrantToken()).toBe('externalNavigation')
})
})
describeClosure(
'the routes that render an HTML artifact',
() => {
@@ -7,8 +7,6 @@
* route that imports the seam without declaring `haptics` is a page whose taps go quiet, because
* grants are resolved once from the route the shell opened.
*
* The scan has a control rather than an empty list: the same walk over the native sibling finds the
* same five functions and no posting site, which is what says it can tell the two apart.
*/
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
@@ -152,12 +150,6 @@ describe('the seam reader', () => {
})
})
/**
* The two siblings measured against each other, which is what makes "all five post" a number.
*
* The native file is the control: same five names, same walk, no posting site. Without it an empty
* result and a file the scan could not read would report the same thing.
*/
describe('the two haptics siblings', () => {
it('posts every kind the notify admits from the web sibling, and nothing more', () => {
const kinds = bridgeHapticsKinds(read(HAPTICS_KINDS_MODULE), HAPTICS_KINDS_MODULE)
@@ -166,17 +158,6 @@ describe('the two haptics siblings', () => {
expect([...posted].sort()).toEqual([...kinds].sort())
})
it('finds five functions in the native sibling and no posting site at all', () => {
const sites = hapticsTriggerSites(read(HAPTICS_NATIVE), HAPTICS_NATIVE)
expect(sites).toHaveLength(5)
expect(sites.filter((site) => site.kind !== null)).toEqual([])
})
it('exports the same five names from both, which is what makes one a substitution', () => {
const names = (file) => hapticsTriggerSites(read(file), file).map((site) => site.name)
expect(names(HAPTICS_SEAM)).toEqual(names(HAPTICS_NATIVE))
})
/**
* The third direction, which no type in the app states.
*
@@ -6,7 +6,7 @@
* reddened nothing. Each row below gets its own named case, and each case's control is the same
* rule driven over the entry that route would have had with the grant struck out.
*/
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
@@ -18,7 +18,6 @@ import {
pageRouteModulesCoverTheManifest
} from './mobile-web-app-page-route-modules.mjs'
import { MOBILE_WEB_PAGE_ROUTES } from './mobile-web-page-routes.mjs'
import { spelledCountsAgainstTables } from './spelled-count-census.mjs'
import {
PAGE_GRANT_CALL_SITES,
createGrantCallSiteReader,
@@ -47,82 +46,10 @@ function closureOf(pathname) {
return held
}
const scriptsDir = import.meta.dirname
const sessionGrants = MOBILE_WEB_PAGE_ROUTES.filter((route) => route.pathname === SESSION).flatMap(
(route) => route.grants
)
const pinnedHere = PAGE_GRANT_CALL_SITES.flatMap((row) => row.grants)
const pinnedHereForSession = pinnedHere.filter((grant) => sessionGrants.includes(grant))
const sessionOptionalGrants = MOBILE_WEB_PAGE_ROUTES.filter(
(route) => route.pathname === SESSION
).flatMap((route) => route.optionalGrants ?? [])
const pinnedElsewhere = sessionGrants.filter((grant) => !pinnedHere.includes(grant))
const censusedElsewhere = [...sessionGrants, ...sessionOptionalGrants].filter(
(grant) => !pinnedHere.includes(grant)
)
/**
* The split both this file and its census state in prose, counted off the two tables instead.
*
* The rows here pin some of the session route's grants and named censuses pin the rest; the
* sentences that say how many were written when a fourteenth grant existed and did not move when
* #22072 removed it.
*
* Two counts, and which one a row takes is what its sentence is about. Exactly one row takes the
* intersection: the `.mjs` sentence for how many of the session route's grants these rows cover,
* which a row pinning a grant no route declares -- the shell can serve a verb before a screen asks
* for it -- would otherwise make the census demand a comment overstate the session route's list.
*
* Of the rows that could take either, every other one counts the rows here, the title about
* reaching them through the session route included. That title names the session route but is not
* a claim about it: the assertion under it compares `grantsNeeded` with every row's grants, so it
* has to move when the rows move and not when the route does. A census holding it at the
* intersection would have kept the title below the assertion it heads. The remaining rows count
* neither: they read the route's own list, or the grants on it that no row pins.
*
* The rows read the whole file, titles included: a count in a JSDoc and the same count in an `it`
* title go stale together, and pinning only the first leaves a green suite describing itself
* wrongly to whoever reads the run.
*/
const SPELLED_COUNTS = {
'mobile-web-app-page-grant-call-sites.mjs': [
{ precedes: 'grants', counted: sessionGrants.length },
{ precedes: 'audio grants', counted: sessionGrants.filter(isAudio).length },
{ precedes: 'rows pin', counted: PAGE_GRANT_CALL_SITES.length },
{ precedes: 'of the session', counted: pinnedHereForSession.length },
{ precedes: 'have censuses of their own', counted: pinnedElsewhere.length },
// One more than the row above: C8.1's optional grant is censused elsewhere as well, and the
// sentence here counts what has a census rather than what the route requires.
{ precedes: 'are not repeated here', counted: censusedElsewhere.length }
],
'mobile-web-app-page-grant-call-sites.test.mjs': [
// Both the header's "eight grants this file pins" and the title's "eight grants".
{ precedes: 'grants', counted: pinnedHere.length },
{ precedes: 'rows covering', counted: PAGE_GRANT_CALL_SITES.length },
{ precedes: 'audio grants', counted: sessionGrants.filter(isAudio).length },
{ precedes: 'did not', counted: pinnedHere.length },
{ precedes: 'through the session route', counted: pinnedHere.length }
]
}
function isAudio(grant) {
return grant.startsWith('native.audio.')
}
describe('the call-site reader', () => {
const navigate = PAGE_GRANT_CALL_SITES[0]
const storage = PAGE_GRANT_CALL_SITES[1]
it('spells the split off the two tables, in this file and in the one it reads', async () => {
for (const [name, rows] of Object.entries(SPELLED_COUNTS)) {
const source = await readFile(join(scriptsDir, name), 'utf8')
for (const { precedes, spelled, counts } of spelledCountsAgainstTables(source, rows)) {
expect(spelled, `${name}: ${precedes}`).toEqual(counts)
}
}
})
it('reuses parsed references across rows while observing source and row changes', async () => {
const directory = await mkdtemp(join(tmpdir(), 'orca-grant-reader-'))
const file = join(directory, 'route.tsx')
@@ -193,29 +120,6 @@ describe('the call-site reader', () => {
moduleReachesGrantRow("import AsyncStorage from './other-storage'\n", 'a.ts', storage)
).toBe(false)
})
it('names six rows covering eight grants, none of them a grant another census owns', () => {
const grants = PAGE_GRANT_CALL_SITES.flatMap((row) => row.grants)
expect(PAGE_GRANT_CALL_SITES).toHaveLength(6)
expect(grants).toEqual([
'navigate',
'storage',
'externalLink',
'native.clipboard.write',
'native.clipboard.read',
'native.media.pick',
'native.media.read',
'native.media.release'
])
for (const owned of [
'haptics',
'screencastBinary',
'externalNavigation',
'native.audio.start'
]) {
expect(grants).not.toContain(owned)
}
})
})
describeClosure(
@@ -55,12 +55,6 @@ function screencastGrantCallers(closure) {
})
}
describe('the grant token this census is written against', () => {
it('is the one the shell declares', () => {
expect(screencastGrantToken()).toBe('screencastBinary')
})
})
describeClosure(
'the routes that mount the browser pane',
() => {
@@ -18,14 +18,6 @@ const ANSWERED = [
describeClosure(
'the session route web component closure',
() => {
it('includes all four web components in the session route closure', async () => {
const closure = await mobileWebAppRouteClosure(SESSION)
expect(closure.local.length).toBeGreaterThan(500)
for (const file of ANSWERED) {
expect(closure.local, file).toContain(file)
}
})
it('resolves all four web components and excludes their native files', async () => {
const closure = await mobileWebAppRouteClosure(SESSION)
for (const file of ANSWERED) {
@@ -92,31 +92,42 @@ describeClosure(
expect(local).toContain(ENGINE_CSS_MODULE)
}, 180_000)
it('would be reported if a document module imported it', async () => {
// Planted in a scratch tree rather than under src/terminal/document, so nothing else in the
// repository ever walks the plant and no other census has to know it exists.
const scratch = await mkdtemp(join(tmpdir(), 'orca-c75-engine-closure-'))
try {
const planted = join(scratch, 'src', 'terminal', 'document')
await mkdir(planted, { recursive: true })
await writeFile(
join(planted, 'planted.ts'),
"import { XTERM_ENGINE_JS } from '../terminal-webview-engine.generated'\n" +
'export const planted = XTERM_ENGINE_JS.length\n'
)
await mkdir(join(scratch, 'src', 'terminal'), { recursive: true })
await writeFile(
join(scratch, 'src', 'terminal', 'terminal-webview-engine.generated.ts'),
"export const XTERM_ENGINE_JS = 'planted'\n"
)
const { local } = await mobileWebAppModuleClosure(['./src/terminal/document/planted'], {
absWorkingDir: scratch
})
expect(local).toContain(ENGINE_MODULE)
} finally {
await rm(scratch, { recursive: true, force: true })
}
}, 180_000)
it.each(['bare', 'relative', 'absolute'])(
'would be reported if a document module imported it (%s entry)',
async (entryKind) => {
// Planted in a scratch tree rather than under src/terminal/document, so nothing else in the
// repository ever walks the plant and no other census has to know it exists.
const scratch = await mkdtemp(join(tmpdir(), 'orca-c75-engine-closure-'))
try {
const planted = join(scratch, 'src', 'terminal', 'document')
await mkdir(planted, { recursive: true })
await writeFile(
join(planted, 'planted.ts'),
"import { XTERM_ENGINE_JS } from '../terminal-webview-engine.generated'\n" +
'export const planted = XTERM_ENGINE_JS.length\n'
)
await mkdir(join(scratch, 'src', 'terminal'), { recursive: true })
await writeFile(
join(scratch, 'src', 'terminal', 'terminal-webview-engine.generated.ts'),
"export const XTERM_ENGINE_JS = 'planted'\n"
)
const entry = 'src/terminal/document/planted'
const entryPoint =
entryKind === 'absolute'
? join(scratch, 'src', 'terminal', 'document', 'planted')
: entryKind === 'relative'
? `./${entry}`
: entry
const { local } = await mobileWebAppModuleClosure([entryPoint], {
absWorkingDir: scratch
})
expect(local).toContain(ENGINE_MODULE)
} finally {
await rm(scratch, { recursive: true, force: true })
}
},
180_000
)
},
600_000
)
@@ -24,7 +24,6 @@ import {
formatCheckFailure,
normalizePnpmDiff,
pnpmDiffEnvironment,
selectPatchEntries,
sourceHunks,
splitPatchEntries
} from './xterm-patch-text.mjs'
@@ -436,22 +435,6 @@ describe('check-mode reporting', () => {
// These run without network or a build, so ordinary `pnpm test` catches the two
// desyncs that would otherwise only surface in the heavy xterm_patch_sync job.
describe('committed xterm patch artifacts', () => {
it('carries the font-weight probe into every WebGL runtime copy', async () => {
const manifest = JSON.parse(await readFile(MANIFEST_PATH, 'utf8'))
const webgl = manifest.packages.find((entry) => entry.name === '@xterm/addon-webgl')
const patch = await readFile(path.join(REPO_ROOT, webgl.patch), 'utf8')
// The ESM and CJS bundles mangle locals differently, so anchor on the global the
// renderer diagnostics read; a copy missing it reports a font mismatch as a repaint bug.
for (const file of ['src/TextureAtlas.ts', 'lib/addon-webgl.js', 'lib/addon-webgl.mjs']) {
const stanza = selectPatchEntries(patch, (candidate) => candidate === file)
expect(stanza, file).not.toBe('')
expect(stanza, file).toContain('__orcaAtlasFontProbe')
}
// The probe must compare the rasterized weight against the requested one; comparing
// against a literal '400' would call every non-default weight a mismatch.
expect(sourceHunks(patch)).toContain('actual === desired')
})
it('records the lockfile hash pnpm derives from the patch file', async () => {
const manifest = JSON.parse(await readFile(MANIFEST_PATH, 'utf8'))
const lockfile = await readFile(path.join(REPO_ROOT, 'pnpm-lock.yaml'), 'utf8')
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'
import { BRIDGE_MAX_MESSAGE_BYTES } from './bridge/bridge-caps'
import { readBridgeHostMessage, type BridgeHostMessage } from './bridge/bridge-envelope'
import {
BRIDGE_ACK_INTERVAL_BYTES,
BRIDGE_ACK_INTERVAL_FRAMES
@@ -114,11 +115,40 @@ function replay(chunks: readonly string[], options: { method?: string } = {}): R
let unreadBytes = 0
let drainCreditBytes = 0
const events: { frame: BridgeHostMessage; bytes: number }[] = []
let postedCursor = 0
let largestFrameBytes = 0
let ended = false
// A page decodes each posted frame once, rather than replaying its whole history on every read.
const readPosted = (): void => {
for (const json of bridge.posted.slice(postedCursor)) {
const read = readBridgeHostMessage(json)
if (!read.ok) {
throw new Error(`the page would refuse this frame: ${read.refusal}`)
}
postedCursor += 1
const frame = read.message
const bytes = JSON.stringify(frame).length
largestFrameBytes = Math.max(largestFrameBytes, bytes)
if (frame.type === 'event' && frame.id === ID) {
events.push({ frame, bytes })
}
if (frame.type === 'end' && frame.id === ID) {
ended = true
}
}
}
/** One frame off the page's queue, or false when it has caught up. This is the drain. */
const readOne = (): boolean => {
const events = bridge.frames().filter((frame) => frame.type === 'event' && frame.id === ID)
const frame = events[readFrames]
if (frame === undefined || frame.type !== 'event') {
readPosted()
const event = events[readFrames]
if (event === undefined) {
return false
}
const { frame, bytes } = event
if (frame.type !== 'event') {
return false
}
readFrames += 1
@@ -135,7 +165,7 @@ function replay(chunks: readonly string[], options: { method?: string } = {}): R
}
drainCreditBytes -= applied
unreadFrames += 1
unreadBytes += JSON.stringify(frame).length
unreadBytes += bytes
if (unreadFrames >= BRIDGE_ACK_INTERVAL_FRAMES || unreadBytes >= BRIDGE_ACK_INTERVAL_BYTES) {
unreadFrames = 0
unreadBytes = 0
@@ -159,12 +189,12 @@ function replay(chunks: readonly string[], options: { method?: string } = {}): R
while (readOne()) {
// The page reads; the interval acks inside `readOne` are the only ones it sends.
}
const events = bridge.frames().filter((frame) => frame.type === 'event' && frame.id === ID)
readPosted()
return {
delivered,
largestFrameBytes: Math.max(0, ...bridge.frames().map((frame) => JSON.stringify(frame).length)),
largestFrameBytes,
frames: events.length,
ended: bridge.frames().some((frame) => frame.type === 'end' && frame.id === ID)
ended
}
}
+16 -6
View File
@@ -1,4 +1,5 @@
/** The device half of the media verbs: the picker it runs, the bytes it moves, the file it deletes. */
import { Buffer } from 'node:buffer'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { splitBridgeReply } from '../mobile-web-shell/bridge/bridge-reply-chunking'
import {
@@ -52,7 +53,16 @@ function fakeFile(bytes: Uint8Array): NativeMediaFile {
}
function bytesOf(length: number): Uint8Array {
return Uint8Array.from({ length }, (_, index) => index % 251)
// A fixed stream exposes shifted offsets and reordered chunks without a short repeating pattern.
let state = 0x9e3779b9
const bytes = new Uint8Array(length)
for (let index = 0; index < length; index += 1) {
state ^= state << 13
state ^= state >>> 17
state ^= state << 5
bytes[index] = state >>> 24
}
return bytes
}
type Harness = {
@@ -365,7 +375,7 @@ describe('reading chunks', () => {
const total = 1_000_000
const { probe, handle } = await staged(total)
const source = bytesOf(total)
const collected: number[] = []
const collected = new Uint8Array(total)
let offset = 0
let reads = 0
for (;;) {
@@ -378,16 +388,16 @@ describe('reading chunks', () => {
)
reads += 1
const bytes = atob(chunk.base64)
for (let index = 0; index < bytes.length; index += 1) {
collected.push(bytes.codePointAt(index) ?? 0)
}
collected.set(Buffer.from(bytes, 'latin1'), offset)
offset += bytes.length
expect(chunk.eof).toBe(offset === total)
if (chunk.eof) {
break
}
}
expect(reads).toBe(Math.ceil(total / BRIDGE_MEDIA_READ_MAX_BYTES))
expect(collected).toEqual([...source])
expect(offset).toBe(total)
expect(Buffer.compare(collected, source)).toBe(0)
})
it('answers exactly the bytes a short read asked for, at the offset it asked for', async () => {
@@ -1,8 +1,9 @@
import { Buffer } from 'node:buffer'
import { describe, expect, it } from 'vitest'
import { MobileImageBase64Accumulator } from './mobile-image-base64-accumulator'
function decodeBase64(data: string): Uint8Array {
return Uint8Array.from(atob(data), (character) => character.charCodeAt(0))
return Buffer.from(atob(data), 'latin1')
}
describe('MobileImageBase64Accumulator', () => {
@@ -24,6 +25,6 @@ describe('MobileImageBase64Accumulator', () => {
accumulator.append(bytes.subarray(0, 123_457))
accumulator.append(bytes.subarray(123_457))
expect(decodeBase64(accumulator.finish())).toEqual(bytes)
expect(Buffer.compare(decodeBase64(accumulator.finish()), bytes)).toBe(0)
})
})
@@ -1,3 +1,4 @@
import { Buffer } from 'node:buffer'
import { describe, expect, it, vi } from 'vitest'
import { decodeBase64Bytes, encodeBase64Bytes } from './base64-byte-codec'
@@ -42,7 +43,9 @@ describe('base64 byte codec', () => {
const bytes = backing.subarray(3, length + 3)
const encoded = encodeBase64Bytes(bytes)
expect(encoded).toBe(Buffer.from(bytes).toString('base64'))
expect(decodeBase64Bytes(encoded)).toEqual(bytes)
const decoded = decodeBase64Bytes(encoded)
expect(decoded.constructor).toBe(bytes.constructor)
expect(Buffer.compare(decoded, bytes)).toBe(0)
}
)
+1
View File
@@ -297,6 +297,7 @@
"oxlint-plugin-react-doctor": "0.9.14",
"oxlint-tsgolint": "7.0.2003",
"pdfjs-dist": "^6.3.289",
"playwright-core": "1.64.0",
"pngjs": "^7.0.0",
"radix-ui": "^1.6.7",
"react": "~19.2.8",
+10
View File
@@ -524,6 +524,9 @@ importers:
pdfjs-dist:
specifier: ^6.3.289
version: 6.3.289
playwright-core:
specifier: 1.64.0
version: 1.64.0
pngjs:
specifier: ^7.0.0
version: 7.0.0
@@ -6758,6 +6761,11 @@ packages:
engines: {node: '>=20'}
hasBin: true
playwright-core@1.64.0:
resolution: {integrity: sha512-T9r+MZkTECl2+oUcZ26YgZzTrYidTsIhYYkJix+6iDOymOb1FB2RLLSOxRGnZGv5DStt+aduIVbt+E8WygXYpg==}
engines: {node: '>=20'}
hasBin: true
playwright@1.63.0:
resolution: {integrity: sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==}
engines: {node: '>=20'}
@@ -14330,6 +14338,8 @@ snapshots:
playwright-core@1.63.0: {}
playwright-core@1.64.0: {}
playwright@1.63.0:
dependencies:
playwright-core: 1.63.0
-4
View File
@@ -96,10 +96,6 @@ describe('duplicate command keys', () => {
const routes = buildHandlerRoutes([group('alpha', ['a']), group('beta', ['b'])])
expect([...routes.keys()]).toEqual(['a', 'b'])
})
it('holds for the live manifest', () => {
expect(() => buildHandlerRoutes(HANDLER_GROUPS)).not.toThrow()
})
})
describe('dispatch errors', () => {
@@ -217,26 +217,6 @@ it('creates the directory the index lives in', async () => {
}
})
it('rebuilds a newer index rather than reading a schema it does not know', async () => {
const path = await tempDatabasePath()
const newer = openSessionSearchDatabase(path)
newer.prepare("INSERT INTO files(path,byte_offset,mtime_ms) VALUES ('a',1,1)").run()
newer
.prepare("UPDATE meta SET value = ? WHERE key = 'schema_version'")
.run(String(SESSION_SEARCH_SCHEMA_VERSION + 1))
newer.close()
const rebuilt = openSessionSearchDatabase(path)
try {
expect(schemaVersion(rebuilt)).toBe(String(SESSION_SEARCH_SCHEMA_VERSION))
expect(rebuilt.prepare('SELECT COUNT(*) AS c FROM files').get()).toEqual({
c: 0
})
} finally {
rebuilt.close()
}
})
it('rebuilds when meta exists but its version row is gone', async () => {
const path = await tempDatabasePath()
const damaged = openSessionSearchDatabase(path)
@@ -4,17 +4,12 @@ import type {
AgentJournalItemBody,
AgentJournalItemIdentity
} from '../../shared/agent-session-journal-types'
import { deriveToolInputPreview } from '../../shared/agent-hook-listener/tool-input-preview'
import type { StructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink'
import { ClaudeChildWorkDecoder } from './claude-child-work-decoder'
import { claudeChildOperation, drainClaudeChildWork } from './claude-child-work-evidence'
import { ClaudePromptRegistry } from './claude-prompt-registry'
import { createClaudeJournalTranslator } from './claude-structured-journal-translation'
import {
claudeToolResults,
claudeToolUses,
readClaudeMessageEnvelope
} from './claude-structured-item-translation'
import { claudeToolResults, readClaudeMessageEnvelope } from './claude-structured-item-translation'
function frame(content: unknown, parentToolUseId: string | null): Record<string, unknown> {
return {
@@ -26,36 +21,6 @@ function frame(content: unknown, parentToolUseId: string | null): Record<string,
}
}
function previousChildOperation(
message: Record<string, unknown>,
activityOf: Parameters<typeof claudeChildOperation>[1],
observedAt: number
): ReturnType<typeof claudeChildOperation> {
const envelope = activityOf ? readClaudeMessageEnvelope(message) : null
const parentRef = envelope?.parentToolUseId
if (!envelope || !parentRef || !activityOf) {
return []
}
const toolTraffic =
claudeToolUses(envelope).length > 0 ||
claudeToolResults(envelope).some((result) => result.toolUseId !== parentRef)
if (!toolTraffic) {
return []
}
const { agentId, openTool } = activityOf(parentRef)
const input = openTool ? deriveToolInputPreview(openTool.name, openTool.input) : undefined
return [
{
type: 'operation',
observedAt,
childId: agentId,
operation: openTool
? { toolName: openTool.name, ...(input ? { input } : {}), basis: 'open', observedAt }
: null
}
]
}
afterEach(() => vi.restoreAllMocks())
describe('Claude child operation output retention', () => {
@@ -126,6 +91,22 @@ describe('Claude child operation output retention', () => {
}
})
it('does not read child activity for a missing or invalid message envelope', () => {
const activityOf = vi.fn(() => ({ agentId: 'child', openTool: null }))
const message = frame([{ type: 'tool_result', tool_use_id: 'child' }], 'parent')
expect(claudeChildOperation({}, activityOf, 1_000)).toEqual([])
expect(claudeChildOperation({ ...message, type: 'system' }, activityOf, 1_000)).toEqual([])
expect(claudeChildOperation({ ...message, uuid: '' }, activityOf, 1_000)).toEqual([])
expect(activityOf).not.toHaveBeenCalled()
})
it('emits no child operation without an activity reader', () => {
const message = frame([{ type: 'tool_result', tool_use_id: 'child' }], 'parent')
expect(claudeChildOperation(message, undefined, 1_000)).toEqual([])
})
it('keeps canonical result admission and metadata ownership on explicit expected IDs', () => {
const cases: { part: unknown; expectedId: string | null }[] = [
{ part: null, expectedId: null },
@@ -157,63 +138,4 @@ describe('Claude child operation output retention', () => {
)
}
})
it('preserves operation ownership and traffic decisions for mixed provider envelopes', () => {
const parts: unknown[] = [
null,
false,
[],
{},
{ type: 'tool_result' },
{ type: 'tool_result', tool_use_id: '' },
{ type: 'tool_result', tool_use_id: 4 },
{ type: 'tool_result', tool_use_id: 'parent' },
{ type: 'tool_result', tool_use_id: 'child' },
{ type: 'tool_result', tool_use_id: ' ' },
{ type: 'text', tool_use_id: 'child', text: 'message' },
{ type: 'tool_use', id: 'open', name: 'Bash', input: { command: 'pwd' } }
]
const outputs = [undefined, null, '', 'text', 7, {}, ['a', 'b'], [{ type: 'text', text: '😀' }]]
const activities: Parameters<typeof claudeChildOperation>[1][] = [
undefined,
() => ({ agentId: 'agent', openTool: null }),
() => ({
agentId: 'agent',
openTool: { id: 'open', name: 'Bash', input: { command: 'pwd' } }
})
]
for (const part of parts) {
for (const output of outputs) {
const result =
typeof part === 'object' && part !== null && !Array.isArray(part)
? { ...part, content: output }
: part
for (const parent of ['parent', 'child', '', ' ', null]) {
for (const activity of activities) {
const message = frame([result], parent)
expect(claudeChildOperation(message, activity, 1_000)).toEqual(
previousChildOperation(message, activity, 1_000)
)
}
}
}
}
const parentResult = { type: 'tool_result', tool_use_id: 'parent', content: 'spawned' }
const childResult = { type: 'tool_result', tool_use_id: 'child', content: ['first', 'second'] }
for (const content of [
null,
'text',
[],
parts,
[parentResult, childResult],
[childResult, parentResult],
[{ type: 'tool_result', tool_use_id: '' }, parentResult, childResult]
]) {
const message = frame(content, 'parent')
expect(claudeChildOperation(message, activities[2], 1_000)).toEqual(
previousChildOperation(message, activities[2], 1_000)
)
expect(claudeChildOperation({ ...message, type: 'system' }, activities[2], 1_000)).toEqual([])
}
})
})
@@ -7,7 +7,6 @@ import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { structuredAgentSessionSendBody } from '../../shared/structured-agent-session-send-mutation'
import { structuredAgentSessionPayloadFingerprint } from '../../shared/structured-agent-session-mutation'
import { computeAgentSessionPayloadFingerprint } from '../../shared/agent-session-mutation-envelope'
import { reconcileSubmissions } from '../native-chat/agent-session-journal/journal-submission-reconciler'
import {
claudeProviderHistoryWindowFromJsonl,
@@ -92,22 +91,6 @@ describe('claudeProviderHistoryWindowFromJsonl', () => {
expect(window?.items.map((item) => item.providerItemId)).toEqual(['u-1'])
})
it('pins the renderer and host fingerprint functions to the same digest', () => {
// The renderer computes a send's fingerprint with one, the host admission gate
// validates it with the other, and the window matches with the host's. A
// divergence would refuse every send long before it reached here — but it
// would also silently turn every reconciliation into `not_delivered`.
const input = {
method: 'agentSession.send',
sessionId: ORCA_SESSION,
fields: { body: structuredAgentSessionSendBody('ship it', []) }
}
expect(structuredAgentSessionPayloadFingerprint(input)).toBe(
computeAgentSessionPayloadFingerprint(input)
)
})
it('fingerprints a prompt after the anchor exactly as the send that produced it', () => {
const contents = jsonl(
[ANCHOR, prompt('u-1', 'anchor', [{ type: 'text', text: 'ship it' }])],
@@ -1,10 +1,6 @@
import { describe, expect, it } from 'vitest'
import type { AgentJournalItemIdentity } from '../../shared/agent-session-journal-types'
import {
createCodexDispatchEchoes,
readCodexDispatchEcho,
MAX_CODEX_PENDING_DISPATCH_ECHOES
} from './codex-structured-dispatch-echo'
import { createCodexDispatchEchoes, readCodexDispatchEcho } from './codex-structured-dispatch-echo'
const CODEX_IDENTITY: AgentJournalItemIdentity = {
provider: 'codex',
@@ -14,29 +10,6 @@ const CODEX_IDENTITY: AgentJournalItemIdentity = {
}
describe('codex dispatch echoes', () => {
it('settles by client message id rather than arrival order', () => {
const echoes = createCodexDispatchEchoes()
echoes.arm('client-1')
echoes.arm('client-2')
// Codex coalesces both sends into one turn, and the second can be echoed
// first. Queue position would settle the wrong submission here.
expect(echoes.settle('client-2')).toBe(true)
expect(echoes.settle('client-1')).toBe(true)
expect(echoes.size).toBe(0)
})
it('reads each submission instant by client message id', () => {
const echoes = createCodexDispatchEchoes()
echoes.arm('stale-unknown', 100)
echoes.arm('later-turn', 200)
expect(echoes.requestOrigin('later-turn')).toEqual({ requestedAt: 200, sequence: 1 })
expect(echoes.requestOrigin('stale-unknown')).toEqual({ requestedAt: 100, sequence: 0 })
expect(echoes.requestOrigin('never-armed')).toBeNull()
expect(echoes.latestSequence()).toBe(1)
})
it('keeps one causal sequence when an unconfirmed send retries', () => {
const echoes = createCodexDispatchEchoes()
echoes.arm('client-1', 100)
@@ -45,65 +18,9 @@ describe('codex dispatch echoes', () => {
expect(echoes.requestOrigin('client-1')).toEqual({ requestedAt: 100, sequence: 0 })
expect(echoes.latestSequence()).toBe(0)
})
it('refuses an echo this session never armed', () => {
const echoes = createCodexDispatchEchoes()
echoes.arm('client-1')
expect(echoes.settle('client-from-history')).toBe(false)
expect(echoes.size).toBe(1)
})
it('settles a send exactly once', () => {
const echoes = createCodexDispatchEchoes()
echoes.arm('client-1')
expect(echoes.settle('client-1')).toBe(true)
expect(echoes.settle('client-1')).toBe(false)
})
it('drops a send whose write never reached the provider', () => {
const echoes = createCodexDispatchEchoes()
echoes.arm('client-1')
echoes.disarm('client-1')
expect(echoes.settle('client-1')).toBe(false)
})
it('clears every armed send', () => {
const echoes = createCodexDispatchEchoes()
echoes.arm('client-1')
echoes.arm('client-2')
echoes.clear()
expect(echoes.size).toBe(0)
expect(echoes.settle('client-1')).toBe(false)
})
it('refuses new correlations at capacity without dropping an older send', () => {
const echoes = createCodexDispatchEchoes()
for (let index = 0; index < MAX_CODEX_PENDING_DISPATCH_ECHOES; index += 1) {
expect(echoes.arm(`client-${index}`)).toBe(true)
}
expect(echoes.arm(`client-${MAX_CODEX_PENDING_DISPATCH_ECHOES}`)).toBe(false)
expect(echoes.size).toBe(MAX_CODEX_PENDING_DISPATCH_ECHOES)
expect(echoes.settle('client-0')).toBe(true)
expect(echoes.settle(`client-${MAX_CODEX_PENDING_DISPATCH_ECHOES}`)).toBe(false)
})
})
describe('readCodexDispatchEcho', () => {
it('reads the client message id off a user message', () => {
expect(
readCodexDispatchEcho(
{ type: 'userMessage', id: 'item-1', clientId: 'client-1' },
CODEX_IDENTITY
)
).toEqual({ clientMessageId: 'client-1', providerIdentity: CODEX_IDENTITY })
})
it('ignores an item that is not a user message', () => {
expect(
readCodexDispatchEcho(
@@ -113,10 +30,6 @@ describe('readCodexDispatchEcho', () => {
).toBeNull()
})
it('ignores a user message Codex did not correlate', () => {
expect(readCodexDispatchEcho({ type: 'userMessage', id: 'item-1' }, CODEX_IDENTITY)).toBeNull()
})
it('ignores an item with no durable Codex identity', () => {
expect(
readCodexDispatchEcho(
+1 -2
View File
@@ -1,7 +1,6 @@
import { describe, expect, it } from 'vitest'
import { applyGhHostToArgs } from './runner'
import { collectGhArgvHostSignals } from './command-runner/gh-host-args'
import { applyGhHostToArgs, collectGhArgvHostSignals } from './command-runner/gh-host-args'
describe('applyGhHostToArgs', () => {
it('returns args unchanged when no host is given', () => {
@@ -15,7 +15,7 @@ vi.mock('./runner', () => ({
translateWslOutputPaths: translateWslOutputPathsMock
}))
import { resolveWorktreeAddTimeoutMs, WORKTREE_ADD_TIMEOUT_MS } from './worktree'
import { resolveWorktreeAddTimeoutMs, WORKTREE_ADD_TIMEOUT_MS } from './worktree-operation-options'
import { registerWorktreeSuiteHooks } from './worktree-test-harness'
registerWorktreeSuiteHooks()
@@ -3,6 +3,7 @@ import { rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type * as WindowsAcl from '../../shared/secure-path-windows-acl'
import {
ORCAD_CANCEL_MANAGED_STOP_FLAG,
OrcadManagedStopCancellationSchema,
@@ -19,8 +20,26 @@ import { completeOrcadManagedStop } from './orcad-managed-stop-completion'
import { runOrcadManagedStopCancelCommand } from './orcad-managed-stop-command'
import { installOrcadStopRequestListeners } from './orcad-stop-request-listener'
const hardening = vi.hoisted(() => ({ pending: new Set<Promise<void>>() }))
vi.mock('../../shared/secure-path-windows-acl', async (importOriginal) => {
const acl = await importOriginal<typeof WindowsAcl>()
return {
...acl,
restrictWindowsDirectory: (dirPath: string): Promise<boolean> => {
const restricted = acl.restrictWindowsDirectory(dirPath)
const pending = restricted.then(() => undefined)
hardening.pending.add(pending)
void pending.then(() => hardening.pending.delete(pending))
return restricted
}
}
})
const roots: string[] = []
afterEach(async () => {
// ACL updates must finish before their temporary directories disappear.
await Promise.all(hardening.pending)
vi.restoreAllMocks()
for (const root of roots.splice(0)) {
await rm(root, { recursive: true, force: true, maxRetries: 8, retryDelay: 100 })
@@ -4,9 +4,9 @@ import { getDefaultUIState } from '../../../shared/constants'
import type { ChangelogData, UpdateStatus } from '../../../shared/update-status-types'
import { createUISlice } from '../store/slices/ui'
import type { AppState } from '../store/types'
import { isHttp2ProtocolError } from './UpdateCard'
import {
getUpdateCardAriaLabel,
isHttp2ProtocolError,
isUpdateCardVisible
} from './maintenance/update-card/update-card-visibility'
@@ -5,7 +5,7 @@ import { createRoot, type Root } from 'react-dom/client'
import { renderToStaticMarkup } from 'react-dom/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { ContextualTourId } from '../../../../shared/contextual-tours'
import { getContextualTourCleanupOutcome } from './ContextualTourOverlay'
import { getContextualTourCleanupOutcome } from './contextual-tour-overlay-measurement'
import {
ContextualTourOverlaySurface,
handleContextualTourGlobalKeyDown,
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'
import { getProjectPickerBrowseHost, parseProjectInput } from './ProjectPicker'
import { parseProjectInput } from './project-picker-input'
import { getProjectPickerBrowseHost } from './project-picker-runtime'
describe('ProjectPicker project input', () => {
it('preserves the host and custom port from a GHES project URL', () => {
@@ -1,43 +0,0 @@
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
const css = readFileSync(
resolve('src/renderer/src/components/native-chat/native-chat-appearance.css'),
'utf8'
)
const rootRule = /\.native-chat-appearance \{([^}]*)\}/.exec(css)?.[1] ?? ''
function declared(name: string): string {
return new RegExp(`${name}:\\s*([^;]+);`).exec(rootRule)?.[1] ?? ''
}
describe('chat type scale', () => {
it('sizes the reading step from the text size setting', () => {
expect(declared('--text-sm')).toContain('var(--chat-font-size')
})
// Labels and controls follow the chat's own size, which only differs from --text-sm when matching a terminal.
it('sizes labels and controls from the bounded chat size, falling back to the reading step', () => {
expect(declared('--chat-ui-size')).toContain('var(--chat-ui-font-size')
expect(declared('--chat-ui-size')).toContain('var(--text-sm)')
})
// A step that stops deriving from the chat size stays fixed while the setting moves the rest.
it.each(['--text-xs', '--text-2xs', '--text-3xs'])('scales %s with the chat size', (step) => {
expect(declared(step)).toContain('var(--chat-ui-size)')
})
// Labels never outgrow the message text when a small terminal size is matched.
it.each(['--text-xs', '--text-2xs'])('caps %s at the reading step', (step) => {
expect(declared(step)).toMatch(/^min\(var\(--text-sm\),/)
})
it('scales spacing and icons with the chat size', () => {
expect(declared('--spacing')).toContain('var(--chat-ui-size)')
})
it('sizes code from the code text size setting', () => {
expect(declared('--text-chat-code')).toContain('var(--chat-code-font-size')
})
})
@@ -21,7 +21,7 @@ vi.mock('@/lib/worktree-activation', () => ({
activateAndRevealWorktree: activateAndRevealWorktreeMock
}))
import { getLocalWorkspacePortSections } from './PortsPanel'
import { getLocalWorkspacePortSections } from './local-workspace-port-sections'
import {
getPortOpenBrowserTooltipLabel,
getPortSystemBrowserHint,
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { shouldBeginWorktreeRename } from './WorktreeCard'
import { shouldBeginWorktreeRename } from './worktree-card-model'
describe('shouldBeginWorktreeRename', () => {
it('matches unscoped legacy rename requests by worktree id', () => {
@@ -13,7 +13,7 @@ import {
planWorkspaceStatusAssignment,
selectMenuScopedMap,
shouldRevealWorktreeDeveloperMenu
} from './WorktreeContextMenu'
} from './worktree-context-menu-policy'
import { getDeleteStateForWorktreeHost } from './worktree-delete-state-host-match'
import type { WorktreeLineage } from '../../../../shared/worktree/lineage-types'
import type { WorkspaceStatusDefinition, Worktree } from '../../../../shared/worktree/types'
@@ -7,11 +7,13 @@ import type {
} from '../../../../shared/managed-account-types'
import {
buildClaudeStatusSwitchGroups,
resolveClaudeStatusAccountState
} from './status-bar-claude-accounts'
import {
buildCodexStatusSwitchGroups,
getStatusBarPreferredWslDistro,
resolveClaudeStatusAccountState,
resolveCodexStatusAccountState
} from './StatusBar'
} from './status-bar-codex-accounts'
import { getStatusBarPreferredWslDistro } from './status-bar-runtime-targets'
const hostLabel = navigator.userAgent.includes('Windows') ? 'Windows' : 'This device'