mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
[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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+10
@@ -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
|
||||
|
||||
@@ -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,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'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user