ci: reuse prepared Windows security addon in PR packages (#29352)

* ci: reuse prepared Windows security addon in PR packages

* ci: prepare bounded Windows security reuse qualification

* ci: ignore owned Windows benchmark receipts during source checks

* test: inject Windows security qualification dependencies

* test(mobile): register chat view mocks in test roots
This commit is contained in:
Neil
2026-10-15 21:05:34 -07:00
committed by GitHub
parent c386cdd202
commit f152101010
21 changed files with 1599 additions and 95 deletions
+3
View File
@@ -151,6 +151,7 @@ jobs:
ref: ${{ inputs.ref }}
persist-credentials: false
- uses: ./.github/actions/install-node-dependencies
id: dependencies
with:
native-runtime: ${{ runner.os == 'Windows' && 'node' || 'none' }}
cache-pnpm-store: ${{ runner.os != 'Windows' }}
@@ -191,6 +192,8 @@ jobs:
with:
arches: ${{ (inputs.build_template && matrix.os == 'windows-2022') && 'x64 arm64' || (runner.arch == 'ARM64' && 'arm64' || 'x64') }}
- run: pnpm build:orcad
env:
ORCA_REUSE_PREPARED_WINDOWS_SECURITY: ${{ github.event_name == 'pull_request' && runner.os == 'Windows' && steps.dependencies.outcome == 'success' }}
- wait: bun-orcad
- run: pnpm test:node-server --artifact ${{ runner.os == 'Linux' && '--cross-runtime' || '' }}
env:
+2
View File
@@ -161,6 +161,8 @@ jobs:
arches: ${{ matrix.arch }}
- name: Build the win32 template
shell: bash
env:
ORCA_REUSE_PREPARED_WINDOWS_SECURITY: ${{ github.event_name == 'pull_request' && steps.dependencies.outcome == 'success' }}
run: |
pnpm build:orcad-prebuilds --require-slots "win32-${{ matrix.arch }}"
node config/scripts/build-orcad-template.mjs --targets "win32-${{ matrix.arch }}"
@@ -3,6 +3,11 @@ name: Windows native preparation comparison
on:
workflow_dispatch:
inputs:
comparison:
description: Bounded comparison to run on the existing Windows slots.
type: choice
default: native-preparation
options: [native-preparation, windows-security]
source_sha:
description: Exact commit containing this benchmark and the Windows compiler candidate.
required: true
@@ -49,11 +54,19 @@ jobs:
cache-pnpm-store: 'false'
cache-pnpm-verification: 'false'
- name: Compare isolated cold and same-build warm preparation
if: inputs.comparison != 'windows-security'
shell: bash
env:
SOURCE_SHA: ${{ inputs.source_sha }}
BENCHMARK_RUNNER: ${{ matrix.os }}
run: node config/scripts/benchmark-windows-native-preparation.mjs
- name: Compare warm Windows security builds and qualify fallback
if: inputs.comparison == 'windows-security'
shell: bash
env:
SOURCE_SHA: ${{ inputs.source_sha }}
BENCHMARK_RUNNER: ${{ matrix.os }}
run: node config/scripts/benchmark-windows-security-reuse.mjs
- uses: actions/upload-artifact@v7
if: always()
with:
+1
View File
@@ -176,5 +176,6 @@ ci-shards/
# Paired CI timing reports and temporary baseline plugin.
.build/preflight-benchmark/
/.build/windows-native-benchmark/
.local/static-analysis-research/
@@ -122,7 +122,7 @@ function digestFiles(root, paths) {
)
}
function treeDigest(root) {
export function treeDigest(root) {
const digest = createHash('sha256')
const files = []
const visit = (path) => {
@@ -0,0 +1,439 @@
import assert from 'node:assert/strict'
import {
chmodSync,
copyFileSync,
existsSync,
lstatSync,
mkdirSync,
mkdtempSync,
readFileSync,
realpathSync,
rmSync,
utimesSync,
writeFileSync
} from 'node:fs'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { performance } from 'node:perf_hooks'
import { pathToFileURL } from 'node:url'
import { runProcess, runProcessSync } from '@orca/process-host'
import { NODE_RUNTIME_ASSETS, NODE_RUNTIME_PIN } from '../../src/shared/node-runtime-pin.ts'
import {
buildWindowsSecurityAddon,
probeWindowsSecurityAddon
} from './build-windows-security-addon.mjs'
import { assertNoHeaderDownloads, treeDigest } from './benchmark-windows-native-preparation.mjs'
import { ensurePinnedNodeExecutable, preparePinnedNodeDir } from './pinned-node-downloads.mjs'
import { sha256Of } from './orcad-prebuild-slot-contents.mjs'
import { hash } from './windows-native-benchmark-observer.mjs'
const ROOT = resolve(import.meta.dirname, '../..')
const { PE_MACHINE, readPeMachine } = createRequire(import.meta.url)('./windows-pe-machine.cjs')
const invalidCandidate = Buffer.from('orca-windows-security-qualification-invalid')
const preparedPath = (root) =>
join(root, 'native/windows-security/build/Release/orca_windows_security.node')
export function assertSecurityComparisonHost({
platform = process.platform,
arch = process.arch,
version = process.versions.node,
electron = process.versions.electron,
sourceSha = process.env.SOURCE_SHA,
runner = process.env.BENCHMARK_RUNNER
} = {}) {
assert.equal(platform, 'win32', 'Security comparison requires Windows')
assert(!electron, 'Security comparison requires plain Node')
assert.equal(
{ 'windows-2022': 'x64', 'windows-11-arm': 'arm64' }[runner],
arch,
'Runner architecture mismatch'
)
assert(['x64', 'arm64'].includes(arch), 'Unsupported architecture')
assert.equal(version, NODE_RUNTIME_PIN.version, 'Build Node differs from pin')
assert(/^[0-9a-f]{40}$/.test(sourceSha ?? ''), 'SOURCE_SHA must be exact')
}
export function assertSecurityCommand(result) {
assert(
result.code === 0 && !result.signal && !result.timedOut && !result.outputTruncated,
'Command failed or produced incomplete output'
)
}
export function probeSecurityArtifact(
path,
runtimePath,
arch,
run = runProcessSync,
runtimeAssets = NODE_RUNTIME_ASSETS
) {
const machine = readPeMachine(path)
assert.equal(machine, PE_MACHINE[arch], 'Security artifact architecture mismatch')
const sha256 = sha256Of(path)
assert.equal(
sha256Of(runtimePath),
runtimeAssets[`win32-${arch}`]?.executableSha256,
'Runtime hash mismatch'
)
const result = probeWindowsSecurityAddon(runtimePath, resolve(path), arch, run)
assertSecurityCommand(result)
assert.equal(result.stdout, 'windows-security-ready', 'Security operation failed')
assert.equal(sha256Of(path), sha256, 'Security artifact changed during probe')
return { sha256, machine, currentUserSid: 'passed' }
}
export async function withInvalidSecurityCandidate(candidate, backupDir, expectedHash, action) {
const stat = lstatSync(candidate)
assert(stat.isFile() && !stat.isSymbolicLink(), 'Candidate must be an owned regular file')
assert.equal(sha256Of(candidate), expectedHash, 'Prepared candidate changed before backup')
const backup = join(backupDir, 'prepared-security-backup.node')
writeFileSync(backup, readFileSync(candidate), { flag: 'wx', mode: 0o600 })
assert.equal(sha256Of(backup), expectedHash, 'Candidate backup mismatch')
try {
writeFileSync(candidate, invalidCandidate)
assert.equal(sha256Of(candidate), hash(invalidCandidate), 'Invalidation failed')
return await action()
} finally {
copyFileSync(backup, candidate)
chmodSync(candidate, stat.mode)
assert.equal(sha256Of(candidate), expectedHash, 'Candidate restoration failed')
utimesSync(candidate, stat.atimeMs / 1000, stat.mtimeMs / 1000)
}
}
export function runSecurityComparisonSample({
root = ROOT,
variant,
arch = process.arch,
runtimePath,
output,
receiptFile,
run = runProcessSync,
runtimeAssets = NODE_RUNTIME_ASSETS
}) {
assert(['control', 'candidate', 'fallback'].includes(variant), 'Unknown sample')
const receipt = { variant, qualification: 'incomplete', stage: 'initial-validation', calls: [] }
const save = () => writeFileSync(receiptFile, `${JSON.stringify(receipt, null, 2)}\n`)
const observed = (spec) => {
const phase = spec.args?.includes('rebuild')
? 'compiler'
: spec.args?.includes('--check-only')
? 'preparation-probe'
: 'security-probe'
const started = performance.now()
const call = { phase, complete: false }
receipt.stage = phase
receipt.calls.push(call)
try {
const result = run({
...spec,
stdio: 'pipe',
timeoutMs: spec.timeoutMs ?? 480_000,
maxOutputBytes: 64 * 1024 * 1024
})
Object.assign(call, {
code: result.code,
signal: result.signal,
timedOut: result.timedOut,
outputTruncated: result.outputTruncated,
stdoutSha256: hash(result.stdout),
stderrSha256: hash(result.stderr)
})
assertSecurityCommand(result)
assertNoHeaderDownloads(result)
call.complete = true
return result
} finally {
call.durationMs = performance.now() - started
save()
}
}
try {
assert.equal(arch, process.arch, 'Sample architecture differs from host')
assert.equal(
sha256Of(runtimePath),
runtimeAssets[`win32-${arch}`]?.executableSha256,
'Runtime hash mismatch'
)
receipt.stage = 'build'
const start = performance.now()
buildWindowsSecurityAddon(arch, output, {
root,
run: observed,
enabled: variant !== 'control',
runtimePath,
runtimeAssets
})
receipt.buildIncludingReuseProbesMs = performance.now() - start
receipt.staged = probeSecurityArtifact(output, runtimePath, arch, observed, runtimeAssets)
receipt.compilerInvocationCount = receipt.calls.filter(
(call) => call.phase === 'compiler'
).length
receipt.stage = 'validate-observations'
receipt.compilerCountMeaning =
'Actual node-gyp rebuild subprocess calls; each uses the builder fresh work directory.'
assert.equal(
receipt.compilerInvocationCount,
variant === 'candidate' ? 0 : 1,
'Unexpected compiler invocation count'
)
assert(
receipt.calls.every((call) => call.complete),
'A rejected/incomplete command was hidden by fallback'
)
receipt.qualification = 'passed'
return receipt
} catch {
receipt.qualification = 'failed'
receipt.failureStage = receipt.stage
throw new Error('Windows security sample did not qualify')
} finally {
receipt.compilerInvocationCount = receipt.calls.filter(
(call) => call.phase === 'compiler'
).length
save()
}
}
export async function benchmarkWindowsSecurityReuse(
root = ROOT,
{
run = runProcess,
runSync = runProcessSync,
ensureRuntime = ensurePinnedNodeExecutable,
prepareHeaders = preparePinnedNodeDir,
runtimeAssets = NODE_RUNTIME_ASSETS,
hashFile = sha256Of
} = {}
) {
assertSecurityComparisonHost()
const arch = process.arch
const target = `win32-${arch}`
const output = join(root, '.build/windows-native-benchmark/security')
mkdirSync(output, { recursive: true })
const temporary = mkdtempSync(
join(process.env.RUNNER_TEMP || tmpdir(), 'orca-security-comparison-')
)
const report = {
schemaVersion: 1,
qualification: 'incomplete',
startedAt: new Date().toISOString(),
phases: [],
samples: [],
scope:
'One same-run warm control/candidate pair and one invalid-candidate fallback; no CI occupancy or cache-hit-rate claim.',
sourceSha: process.env.SOURCE_SHA,
arch,
runner: process.env.BENCHMARK_RUNNER,
runId: process.env.GITHUB_RUN_ID,
runAttempt: process.env.GITHUB_RUN_ATTEMPT,
workflowSha: process.env.GITHUB_WORKFLOW_SHA,
preparation:
'Source-built in this run; no native cache restore. Headers and runtime preparation precede all measured samples.'
}
const save = () =>
writeFileSync(join(output, 'receipt.json'), `${JSON.stringify(report, null, 2)}\n`)
let environment = { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' }
const command = async (label, program, args) => {
report.stage = label
const start = performance.now()
const result = await run({
program,
args,
cwd: root,
env: environment,
timeoutMs: 1_200_000,
maxOutputBytes: 64 * 1024 * 1024,
terminationBarrier: true
})
report.phases.push({
label,
durationMs: performance.now() - start,
code: result.code,
signal: result.signal,
timedOut: result.timedOut,
outputTruncated: result.outputTruncated,
stdoutSha256: hash(result.stdout),
stderrSha256: hash(result.stderr)
})
save()
assertSecurityCommand(result)
return result
}
const sourceCheck = async () => {
const head = await command('source-head', 'git', ['rev-parse', 'HEAD'])
assert.equal(head.stdout.trim(), process.env.SOURCE_SHA, 'Source commit mismatch')
await command('source-clean', 'git', ['diff', '--exit-code', 'HEAD', '--'])
const untracked = await command('source-untracked', 'git', [
'ls-files',
'--others',
'--exclude-standard'
])
assert.equal(untracked.stdout, '', 'Untracked source prevents exact comparison')
}
try {
await sourceCheck()
report.stage = 'verify-build-node'
assert.equal(
hashFile(process.execPath),
runtimeAssets[target].executableSha256,
'Build Node hash mismatch'
)
const sourceFiles = (
await command('security-source-files', 'git', ['ls-files', '--', 'native/windows-security'])
).stdout
.trim()
.split(/\r?\n/)
const identityFiles = [
...sourceFiles,
'pnpm-lock.yaml',
'pnpm-workspace.yaml',
'package.json',
'.github/actions/prepare-native-runtime/action.yml',
'src/shared/node-runtime-pin.ts',
'config/scripts/build-windows-security-addon.mjs',
'config/scripts/ensure-native-runtime.mjs',
'config/scripts/benchmark-windows-security-reuse.mjs'
]
report.sourceHashes = Object.fromEntries(
identityFiles.map((path) => [path, sha256Of(join(root, path))])
)
report.sourceInputsSha256 = hash(JSON.stringify(report.sourceHashes))
report.tree = (await command('source-tree', 'git', ['rev-parse', 'HEAD^{tree}'])).stdout.trim()
// This mode owns the candidate because its preparation creates it in a fresh checkout.
const candidate = preparedPath(root)
report.stage = 'verify-fresh-checkout'
assert(!existsSync(candidate), 'Security comparison requires a fresh unprepared checkout')
report.stage = 'prepare-pinned-runtime-and-headers'
const preparationStart = performance.now()
const runtimePath = await ensureRuntime({ target })
const nodeDir = await prepareHeaders({ target, workDir: join(temporary, 'headers') })
environment = {
...environment,
npm_config_nodedir: nodeDir,
npm_package_config_node_gyp_nodedir: nodeDir
}
report.downloadAndHeaderPreparationMs = performance.now() - preparationStart
report.runtime = {
version: NODE_RUNTIME_PIN.version,
sha256: sha256Of(runtimePath),
buildNodeSha256: hashFile(process.execPath)
}
report.headers = treeDigest(nodeDir)
await command('prepare-node-artifacts', process.execPath, [
join(root, 'config/scripts/initialize-windows-native-toolchain.mjs')
])
await command('verify-prepared-node', process.execPath, [
join(root, 'config/scripts/ensure-native-runtime.mjs'),
'--check-only'
])
report.stage = 'verify-prepared-candidate-ownership'
assert.equal(
realpathSync(candidate).toLowerCase(),
preparedPath(realpathSync(root)).toLowerCase(),
'Candidate escapes owned checkout'
)
assert(
lstatSync(candidate).isFile() && !lstatSync(candidate).isSymbolicLink(),
'Prepared candidate must be regular'
)
report.prepared = probeSecurityArtifact(candidate, runtimePath, arch, runSync, runtimeAssets)
const sample = async (variant) => {
await sourceCheck()
const receiptFile = join(output, `${variant}.json`)
const result = await command(`${variant}-full-build-and-probe`, process.execPath, [
import.meta.filename,
`--sample=${variant}`,
runtimePath,
join(output, `${variant}.node`),
receiptFile
])
assertNoHeaderDownloads(result)
const receipt = JSON.parse(readFileSync(receiptFile, 'utf8'))
assert.equal(receipt.qualification, 'passed', 'Sample did not qualify')
assert.deepEqual(
treeDigest(nodeDir),
report.headers,
'Measured build changed prepared headers'
)
if (variant === 'candidate') {
assert.equal(
receipt.staged.sha256,
report.prepared.sha256,
'Reuse did not copy prepared bytes'
)
}
report.samples.push({ ...receipt, fullCommandMs: report.phases.at(-1).durationMs })
save()
await sourceCheck()
}
// Warm compiler discovery and local build inputs before measuring the pair.
await command('warm-compiler-before-measurement', process.execPath, [
join(root, 'config/scripts/build-windows-security-addon.mjs'),
`--arch=${arch}`
])
await sample('control')
await sample('candidate')
assert.equal(
sha256Of(candidate),
report.prepared.sha256,
'Prepared candidate changed during pair'
)
report.stage = 'invalidate-owned-candidate'
await withInvalidSecurityCandidate(candidate, output, report.prepared.sha256, async () => {
report.invalidCandidateSha256 = sha256Of(candidate)
await sample('fallback')
})
report.restoredPreparedSha256 = sha256Of(candidate)
assert.equal(
report.restoredPreparedSha256,
report.prepared.sha256,
'Prepared bytes were not restored'
)
await sourceCheck()
const [control, candidateSample] = report.samples
report.comparison = {
controlMs: control.fullCommandMs,
candidateMs: candidateSample.fullCommandMs,
candidateMinusControlMs: candidateSample.fullCommandMs - control.fullCommandMs,
limitation:
'One fixed-order same-run pair per architecture; descriptive only, not a significance or job-time estimate.'
}
report.qualification = 'passed'
} catch {
report.qualification = 'failed'
report.failureStage = report.stage
throw new Error('Windows security comparison did not qualify')
} finally {
report.finishedAt = new Date().toISOString()
report.restored = report.prepared
? existsSync(preparedPath(root)) && sha256Of(preparedPath(root)) === report.prepared.sha256
: null
save()
rmSync(temporary, { recursive: true, force: true })
}
return report
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
try {
const variant = process.argv
.find((arg) => arg.startsWith('--sample='))
?.slice('--sample='.length)
if (variant) {
assertSecurityComparisonHost()
runSecurityComparisonSample({
variant,
runtimePath: process.argv[3],
output: process.argv[4],
receiptFile: process.argv[5]
})
} else {
await benchmarkWindowsSecurityReuse()
}
} catch {
console.error('[windows-security-comparison] failed; inspect the redacted receipt')
process.exitCode = 1
}
}
+4 -1
View File
@@ -165,7 +165,10 @@ stageNodePty(BUILD_TARGET)
await stageParcelWatcher(BUILD_TARGET)
stageOrcadWindowsProcessTree(ROOT, OUT_DIR, BUILD_TARGET)
stageOrcadWindowsCliLauncher(ROOT, OUT_DIR, BUILD_TARGET)
stageOrcadWindowsSecurity(ROOT, OUT_DIR, BUILD_TARGET)
stageOrcadWindowsSecurity(ROOT, OUT_DIR, BUILD_TARGET, undefined, undefined, {
enabled: process.env.ORCA_REUSE_PREPARED_WINDOWS_SECURITY === 'true',
runtimePath: nodeRuntimePath
})
const emojiDatasetOutput = join(OUT_DIR, ORCAD_EMOJI_SHORTCODE_DATASET)
mkdirSync(dirname(emojiDatasetOutput), { recursive: true })
copyFileSync(
+102 -9
View File
@@ -12,9 +12,11 @@
*/
import { copyFileSync, cpSync, existsSync, mkdirSync, rmSync } from 'node:fs'
import { createRequire } from 'node:module'
import { join, resolve } from 'node:path'
import { isAbsolute, join, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { runProcessSync } from '@orca/process-host'
import { NODE_RUNTIME_ASSETS } from '../../src/shared/node-runtime-pin.ts'
import { sha256Of } from './orcad-prebuild-slot-contents.mjs'
import { ORCAD_WINDOWS_SECURITY_FILENAME } from '../../src/shared/orcad-artifacts.ts'
const { PE_MACHINE, describePeMachine, readPeMachine } = createRequire(import.meta.url)(
@@ -23,13 +25,96 @@ const { PE_MACHINE, describePeMachine, readPeMachine } = createRequire(import.me
const ROOT = resolve(import.meta.dirname, '..', '..')
const SUPPORTED_ARCHES = ['x64', 'arm64']
const PACKAGE_DIR = join(ROOT, 'native', 'windows-security')
export function windowsSecurityAddonOutput(root, arch) {
return join(root, '.build', 'windows-security', arch, ORCAD_WINDOWS_SECURITY_FILENAME)
}
export function buildWindowsSecurityAddon(arch, output = windowsSecurityAddonOutput(ROOT, arch)) {
export function probeWindowsSecurityAddon(
runtimePath,
candidate,
arch,
run = runProcessSync,
cwd = ROOT
) {
return run({
program: runtimePath,
args: [
'-e',
`if (process.platform !== 'win32' || process.arch !== ${JSON.stringify(arch)} || process.versions.electron) process.exit(1);` +
`const sid = require(${JSON.stringify(candidate)}).currentUserSid();` +
`if (typeof sid !== 'string' || !/^S-1-\\d+(?:-\\d+)+$/.test(sid)) process.exit(2);` +
`process.stdout.write('windows-security-ready');`
],
cwd,
timeoutMs: 30_000
})
}
export function reusePreparedWindowsSecurityAddon(
arch,
output,
{
enabled = false,
runtimePath,
root = ROOT,
platform = process.platform,
hostArch = process.arch,
runtimeAssets = NODE_RUNTIME_ASSETS,
run = runProcessSync
} = {}
) {
if (!enabled || platform !== 'win32' || arch !== hostArch || !runtimePath) {
return false
}
const candidate = resolve(
root,
'native/windows-security/build/Release/orca_windows_security.node'
)
try {
if (
readPeMachine(candidate) !== PE_MACHINE[arch] ||
!isAbsolute(runtimePath) ||
sha256Of(runtimePath) !== runtimeAssets[`win32-${arch}`]?.executableSha256
) {
return false
}
const prepared = run({
program: process.execPath,
args: [
join(root, 'config/scripts/ensure-native-runtime.mjs'),
'--runtime=node',
'--check-only'
],
cwd: root,
timeoutMs: 30_000
})
if (prepared.code !== 0 || prepared.timedOut || prepared.outputTruncated) {
return false
}
const probe = probeWindowsSecurityAddon(runtimePath, candidate, arch, run, root)
if (
probe.code !== 0 ||
probe.timedOut ||
probe.outputTruncated ||
probe.stdout !== 'windows-security-ready'
) {
return false
}
} catch {
return false
}
mkdirSync(resolve(output, '..'), { recursive: true })
copyFileSync(candidate, output)
console.log(`[windows-security] reused prepared ${arch} -> ${output}`)
return true
}
export function buildWindowsSecurityAddon(
arch,
output = windowsSecurityAddonOutput(ROOT, arch),
{ root = ROOT, run = runProcessSync, ...options } = {}
) {
if (!SUPPORTED_ARCHES.includes(arch)) {
throw new Error(`--arch must be one of ${SUPPORTED_ARCHES.join(', ')}; got ${arch}`)
}
@@ -38,16 +123,19 @@ export function buildWindowsSecurityAddon(arch, output = windowsSecurityAddonOut
`The Windows security addon only builds on Windows; running on ${process.platform}.`
)
}
if (reusePreparedWindowsSecurityAddon(arch, output, { ...options, root, run })) {
return output
}
// Inside the repo so binding.gyp's `require('node-addon-api')` resolves from root node_modules.
const workDir = join(ROOT, '.build', 'windows-security', arch, 'gyp')
const workDir = join(root, '.build', 'windows-security', arch, 'gyp')
rmSync(workDir, { recursive: true, force: true })
mkdirSync(workDir, { recursive: true })
copyFileSync(join(PACKAGE_DIR, 'binding.gyp'), join(workDir, 'binding.gyp'))
cpSync(join(PACKAGE_DIR, 'src'), join(workDir, 'src'), { recursive: true })
const result = runProcessSync({
copyFileSync(join(root, 'native/windows-security/binding.gyp'), join(workDir, 'binding.gyp'))
cpSync(join(root, 'native/windows-security/src'), join(workDir, 'src'), { recursive: true })
const result = run({
program: process.execPath,
args: [
join(ROOT, 'node_modules', 'node-gyp', 'bin', 'node-gyp.js'),
join(root, 'node_modules', 'node-gyp', 'bin', 'node-gyp.js'),
'rebuild',
`--arch=${arch}`
],
@@ -77,7 +165,12 @@ if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
try {
const arch =
process.argv.find((arg) => arg.startsWith('--arch='))?.slice('--arch='.length) ?? process.arch
buildWindowsSecurityAddon(arch)
buildWindowsSecurityAddon(arch, undefined, {
enabled: process.argv.includes('--reuse-prepared-runtime'),
runtimePath: process.argv
.find((arg) => arg.startsWith('--node-runtime='))
?.slice('--node-runtime='.length)
})
} catch (error) {
console.error(`[windows-security] ${error instanceof Error ? error.message : String(error)}`)
process.exit(1)
@@ -0,0 +1,245 @@
import { createHash } from 'node:crypto'
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, isAbsolute, join } from 'node:path'
import { runInNewContext } from 'node:vm'
import { afterEach, expect, it, vi } from 'vitest'
import {
buildWindowsSecurityAddon,
reusePreparedWindowsSecurityAddon
} from './build-windows-security-addon.mjs'
import { peImage } from './windows-pe-image-fixture.mjs'
const roots = []
afterEach(() => {
vi.unstubAllGlobals()
roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true }))
})
const success = { code: 0, timedOut: false, outputTruncated: false, stdout: '' }
function fixture(arch = 'x64') {
const root = mkdtempSync(join(tmpdir(), 'windows-security-build-'))
roots.push(root)
const candidate = join(root, 'native/windows-security/build/Release/orca_windows_security.node')
mkdirSync(dirname(candidate), { recursive: true })
writeFileSync(candidate, peImage({ arch }))
mkdirSync(join(root, 'native/windows-security/src'))
writeFileSync(join(root, 'native/windows-security/binding.gyp'), '{}')
writeFileSync(join(root, 'native/windows-security/src/addon.cc'), 'source')
const runtimePath = join(root, 'pinned node.exe')
writeFileSync(runtimePath, 'pinned Node fixture')
const executableSha256 = createHash('sha256').update('pinned Node fixture').digest('hex')
const runtimeAssets = { [`win32-${arch}`]: { executableSha256 } }
const output = join(root, 'output/windows-security.node')
const run = vi.fn((invocation) => {
if (invocation.args.includes('rebuild')) {
const built = join(invocation.cwd, 'build/Release/orca_windows_security.node')
mkdirSync(dirname(built), { recursive: true })
writeFileSync(
built,
peImage({ arch: invocation.args.find((arg) => arg.startsWith('--arch=')).slice(7) })
)
return success
}
return {
...success,
stdout: invocation.program === runtimePath ? 'windows-security-ready' : ''
}
})
return {
candidate,
output,
options: {
enabled: true,
root,
runtimePath,
platform: 'win32',
hostArch: arch,
runtimeAssets,
run
}
}
}
it.each(['x64', 'arm64'])('reuses the exact prepared %s candidate under pinned Node', (arch) => {
const { candidate, output, options } = fixture(arch)
expect(reusePreparedWindowsSecurityAddon(arch, output, options)).toBe(true)
expect(readFileSync(output)).toEqual(readFileSync(candidate))
const [preparation, probe] = options.run.mock.calls.map(([invocation]) => invocation)
expect(preparation).toEqual({
program: process.execPath,
args: [
join(options.root, 'config/scripts/ensure-native-runtime.mjs'),
'--runtime=node',
'--check-only'
],
cwd: options.root,
timeoutMs: 30_000
})
expect(probe.program).toBe(options.runtimePath)
expect(isAbsolute(probe.program)).toBe(true)
expect(probe.timeoutMs).toBe(30_000)
const requireAddon = vi.fn(() => ({ currentUserSid: () => 'S-1-5-21-123-456-789-1001' }))
const write = vi.fn()
runInNewContext(probe.args[1], {
process: { platform: 'win32', arch, versions: {}, stdout: { write } },
require: requireAddon
})
expect(requireAddon).toHaveBeenCalledWith(candidate)
expect(isAbsolute(candidate)).toBe(true)
expect(write).toHaveBeenCalledWith('windows-security-ready')
})
it.each([
{ enabled: false },
{ enabled: undefined },
{ platform: 'darwin' },
{ platform: 'linux' },
{ hostArch: 'arm64' },
{ runtimePath: undefined },
{ runtimePath: 'node.exe' }
])('declines reuse without same-host opt-in and pinned runtime: %j', (overrides) => {
const { output, options } = fixture()
expect(reusePreparedWindowsSecurityAddon('x64', output, { ...options, ...overrides })).toBe(false)
expect(options.run).not.toHaveBeenCalled()
})
it('declines missing, corrupt, wrong-architecture candidates and an unverified runtime', () => {
const { candidate, output, options } = fixture()
expect(
reusePreparedWindowsSecurityAddon('x64', output, {
...options,
runtimeAssets: undefined
})
).toBe(false)
expect(options.run).not.toHaveBeenCalled()
for (const bytes of [Buffer.from('corrupt'), peImage({ arch: 'arm64' })]) {
writeFileSync(candidate, bytes)
expect(reusePreparedWindowsSecurityAddon('x64', output, options)).toBe(false)
}
rmSync(candidate)
expect(reusePreparedWindowsSecurityAddon('x64', output, options)).toBe(false)
writeFileSync(candidate, peImage())
writeFileSync(options.runtimePath, 'different Node')
expect(reusePreparedWindowsSecurityAddon('x64', output, options)).toBe(false)
expect(options.run).not.toHaveBeenCalled()
})
it.each([
{ code: 1 },
{ code: null },
{ timedOut: true },
{ outputTruncated: true },
{ stdout: '' },
{ stdout: 'S-1-5-21-123' },
{ stdout: 'windows-security-ready\nextra output' }
])('declines failed loading or invalid probe results: %j', (result) => {
const { output, options } = fixture()
options.run
.mockReturnValueOnce(success)
.mockReturnValueOnce({ ...success, stdout: 'windows-security-ready', ...result })
expect(reusePreparedWindowsSecurityAddon('x64', output, options)).toBe(false)
})
it.each([{ code: 1 }, { timedOut: true }, { outputTruncated: true }])(
'does not load the candidate after failed Node preparation: %j',
(result) => {
const { output, options } = fixture()
options.run.mockReturnValueOnce({ ...success, ...result })
expect(reusePreparedWindowsSecurityAddon('x64', output, options)).toBe(false)
expect(options.run).toHaveBeenCalledOnce()
}
)
it('declines probe spawn exceptions', () => {
const { output, options } = fixture()
options.run.mockImplementation(() => {
throw new Error('spawn failed')
})
expect(reusePreparedWindowsSecurityAddon('x64', output, options)).toBe(false)
})
it.each(['', undefined, 123, 'invalid', 'S-1-no-sid'])(
'the probe rejects invalid currentUserSid results: %j',
(sid) => {
const { output, options } = fixture()
reusePreparedWindowsSecurityAddon('x64', output, options)
const probe = options.run.mock.calls[1][0]
const write = vi.fn()
expect(() =>
runInNewContext(probe.args[1], {
process: {
platform: 'win32',
arch: 'x64',
versions: {},
stdout: { write },
exit: (code) => {
throw new Error(`exit ${code}`)
}
},
require: () => ({ currentUserSid: () => sid })
})
).toThrow('exit 2')
expect(write).not.toHaveBeenCalled()
}
)
it.each([
'non-opt-in',
'cold',
'corrupt',
'wrong architecture',
'cross compilation',
'load failure',
'valid'
])('the builder retains real compilation unless reuse succeeds: %s', (scenario) => {
const { candidate, output, options } = fixture()
vi.stubGlobal('process', { ...process, platform: 'win32', arch: 'x64' })
if (scenario === 'non-opt-in') {
options.enabled = false
}
if (scenario === 'cold') {
rmSync(candidate)
}
if (scenario === 'corrupt') {
writeFileSync(candidate, 'bad')
}
if (scenario === 'wrong architecture') {
writeFileSync(candidate, peImage({ arch: 'arm64' }))
}
if (scenario === 'load failure') {
options.run.mockReturnValueOnce(success).mockReturnValueOnce({ ...success, code: 1 })
}
const arch = scenario === 'cross compilation' ? 'arm64' : 'x64'
expect(buildWindowsSecurityAddon(arch, output, options)).toBe(output)
expect(
options.run.mock.calls.filter(([invocation]) => invocation.args.includes('rebuild'))
).toHaveLength(scenario === 'valid' ? 0 : 1)
})
it('standalone compilation rejects unsupported architectures and non-Windows hosts', () => {
const { output, options } = fixture()
vi.stubGlobal('process', { ...process, platform: 'linux' })
expect(() => buildWindowsSecurityAddon('ia32', output, options)).toThrow('--arch must be')
expect(() => buildWindowsSecurityAddon('x64', output, options)).toThrow('only builds on Windows')
expect(options.run).not.toHaveBeenCalled()
})
it.each(['failed compiler', 'missing output', 'wrong machine'])(
'the cold path fails closed on %s',
(failure) => {
const { output, options } = fixture()
vi.stubGlobal('process', { ...process, platform: 'win32', arch: 'x64' })
options.enabled = false
options.run.mockImplementation((invocation) => {
if (failure === 'wrong machine') {
const built = join(invocation.cwd, 'build/Release/orca_windows_security.node')
mkdirSync(dirname(built), { recursive: true })
writeFileSync(built, peImage({ arch: 'arm64' }))
}
return { ...success, code: failure === 'failed compiler' ? 1 : 0 }
})
expect(() => buildWindowsSecurityAddon('x64', output, options)).toThrow()
expect(() => readFileSync(output)).toThrow()
}
)
+16 -5
View File
@@ -18,7 +18,8 @@ export function stageOrcadWindowsSecurity(
outputDir,
target,
host = { platform: process.platform },
compile = compileOrcadWindowsSecurity
compile = compileOrcadWindowsSecurity,
options = {}
) {
if (!target.startsWith('win32-')) {
return
@@ -32,7 +33,7 @@ export function stageOrcadWindowsSecurity(
`node config/scripts/build-windows-security-addon.mjs --arch=${arch}`
)
}
compile(root, arch)
compile(root, arch, options)
}
const machine = readPeMachine(source)
if (machine !== PE_MACHINE[arch]) {
@@ -41,10 +42,20 @@ export function stageOrcadWindowsSecurity(
copyFileSync(source, join(outputDir, ORCAD_WINDOWS_SECURITY_FILENAME))
}
function compileOrcadWindowsSecurity(root, arch) {
const result = runProcessSync({
function compileOrcadWindowsSecurity(
root,
arch,
{ enabled = false, runtimePath, run = runProcessSync } = {}
) {
const result = run({
program: process.execPath,
args: [join(root, 'config/scripts/build-windows-security-addon.mjs'), `--arch=${arch}`],
args: [
join(root, 'config/scripts/build-windows-security-addon.mjs'),
`--arch=${arch}`,
...(enabled && runtimePath
? ['--reuse-prepared-runtime', `--node-runtime=${runtimePath}`]
: [])
],
cwd: root,
stdio: 'inherit',
timeoutMs: null
+83 -1
View File
@@ -59,7 +59,7 @@ it('fails the build off Windows when the addon was never built, and compiles it
const compile = vi.fn((compileRoot, arch) => write(arch, 0x8664))
stageOrcadWindowsSecurity(root, output, 'win32-x64', { platform: 'win32' }, compile)
expect(compile).toHaveBeenCalledWith(root, 'x64')
expect(compile).toHaveBeenCalledWith(root, 'x64', {})
expect(existsSync(join(output, 'windows-security.node'))).toBe(true)
})
@@ -101,3 +101,85 @@ it.each([
expect(step['continue-on-error']).toBeUndefined()
}
})
it('forwards the opt-in and pinned runtime only when compilation is needed', () => {
const { root, output, write } = fixture()
const options = { enabled: true, runtimePath: join(root, 'pinned node.exe') }
const compile = vi.fn((_root, arch) => write(arch, 0x8664))
stageOrcadWindowsSecurity(root, output, 'win32-x64', { platform: 'win32' }, compile, options)
expect(compile).toHaveBeenCalledWith(root, 'x64', options)
compile.mockClear()
stageOrcadWindowsSecurity(root, output, 'win32-x64', { platform: 'win32' }, compile, options)
expect(compile).not.toHaveBeenCalled()
})
it.each(['ssh-windows-hosts', 'node-server-tests'])(
'opts in only in prepared Windows PR packaging in %s',
(name) => {
const workflow = parse(readFileSync(`.github/workflows/${name}.yml`, 'utf8'))
const job = workflow.jobs[name === 'ssh-windows-hosts' ? 'hosts' : 'persistence']
const preparation = job.steps.find((step) => step.id === 'dependencies')
const build = job.steps.find((step) => step.env?.ORCA_REUSE_PREPARED_WINDOWS_SECURITY)
expect(preparation.uses).toBe('./.github/actions/install-node-dependencies')
expect(preparation.with['native-runtime']).toContain('node')
expect(job.steps.indexOf(preparation)).toBeLessThan(job.steps.indexOf(build))
expect(preparation['continue-on-error']).toBeUndefined()
expect(build['continue-on-error']).toBeUndefined()
expect(build.run).toContain(
name === 'ssh-windows-hosts' ? 'build-orcad-template.mjs' : 'pnpm build:orcad'
)
for (const event of ['pull_request', 'push', 'workflow_dispatch', 'schedule']) {
for (const outcome of ['success', 'failure', 'skipped', undefined]) {
for (const os of ['Windows', 'Linux', 'macOS']) {
const enabled = runInNewContext(
build.env.ORCA_REUSE_PREPARED_WINDOWS_SECURITY.slice(3, -2),
{
github: { event_name: event },
runner: { os },
steps: { dependencies: { outcome } }
}
)
expect(enabled).toBe(
event === 'pull_request' &&
outcome === 'success' &&
(name === 'ssh-windows-hosts' || os === 'Windows')
)
}
}
}
}
)
it.each([false, true])('passes an explicit CLI opt-in only when enabled: %s', (enabled) => {
const { root, output, write } = fixture()
const runtimePath = join(root, 'pinned node.exe')
const run = vi.fn(() => {
write('x64', 0x8664)
return { code: 0 }
})
stageOrcadWindowsSecurity(root, output, 'win32-x64', { platform: 'win32' }, undefined, {
enabled,
runtimePath,
run
})
expect(run).toHaveBeenLastCalledWith({
program: process.execPath,
args: [
join(root, 'config/scripts/build-windows-security-addon.mjs'),
'--arch=x64',
...(enabled ? ['--reuse-prepared-runtime', `--node-runtime=${runtimePath}`] : [])
],
cwd: root,
stdio: 'inherit',
timeoutMs: null
})
})
it('does not stage an addon when the child builder fails', () => {
const { root, output } = fixture()
const run = vi.fn(() => ({ code: 1 }))
expect(() =>
stageOrcadWindowsSecurity(root, output, 'win32-x64', { platform: 'win32' }, undefined, { run })
).toThrow('failed with exit 1')
expect(existsSync(join(output, 'windows-security.node'))).toBe(false)
})
@@ -1,5 +1,5 @@
import assert from 'node:assert/strict'
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
@@ -210,3 +210,45 @@ test('manual workflow freezes source and limits Windows occupancy without changi
assert.equal(workflow.jobs.compare.steps.at(-1).if, 'always()')
assert(!Object.hasOwn(workflow.jobs, 'verify'))
})
test('source validation allows owned workflow receipts but rejects unrelated untracked source', async () => {
const directory = mkdtempSync(join(tmpdir(), 'orca-native-output-ignore-'))
const git = async (args) => {
const result = await runProcess({ program: 'git', args, cwd: directory, timeoutMs: 10_000 })
assert.equal(result.code, 0)
assert(!result.timedOut && !result.outputTruncated)
return result.stdout
}
try {
await git(['init', '--quiet'])
writeFileSync(
join(directory, '.gitignore'),
readFileSync(join(import.meta.dirname, '../../.gitignore'))
)
await git(['add', '.gitignore'])
const output = join(directory, '.build/windows-native-benchmark')
mkdirSync(join(output, 'security'), { recursive: true })
writeFileSync(
join(output, 'job-start.json'),
JSON.stringify({ startedAt: new Date().toISOString() })
)
writeFileSync(
join(output, 'security/receipt.json'),
JSON.stringify({ qualification: 'incomplete' })
)
const untracked = ['ls-files', '--others', '--exclude-standard']
assert.equal(await git(untracked), '')
mkdirSync(join(directory, 'config/scripts'), { recursive: true })
writeFileSync(
join(directory, 'config/scripts/untracked-source.mjs'),
'export const source = true'
)
writeFileSync(join(directory, '.build/unrelated-source.mjs'), 'export const source = true')
assert.deepEqual((await git(untracked)).trim().split('\n').sort(), [
'.build/unrelated-source.mjs',
'config/scripts/untracked-source.mjs'
])
} finally {
rmSync(directory, { recursive: true, force: true })
}
})
@@ -0,0 +1,399 @@
import { createHash } from 'node:crypto'
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
statSync,
symlinkSync,
writeFileSync,
rmSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { runInNewContext } from 'node:vm'
import { afterEach, expect, it, vi } from 'vitest'
import { parse } from 'yaml'
import { sha256Of } from './orcad-prebuild-slot-contents.mjs'
import { peImage } from './windows-pe-image-fixture.mjs'
import {
assertSecurityComparisonHost,
assertSecurityCommand,
benchmarkWindowsSecurityReuse,
probeSecurityArtifact,
runSecurityComparisonSample,
withInvalidSecurityCandidate
} from './benchmark-windows-security-reuse.mjs'
const directories = []
const sourceSha = 'a'.repeat(40)
const sidFixture = 'S-1-5-21-123456-234567-345678-1001'
const success = {
code: 0,
signal: null,
timedOut: false,
outputTruncated: false,
stdout: '',
stderr: ''
}
afterEach(() => {
vi.unstubAllGlobals()
vi.unstubAllEnvs()
vi.resetAllMocks()
directories.splice(0).forEach((path) => rmSync(path, { recursive: true, force: true }))
})
const digest = (bytes) => createHash('sha256').update(bytes).digest('hex')
function fixture(arch = 'x64') {
const root = mkdtempSync(join(tmpdir(), 'orca-security-qualification-'))
directories.push(root)
const candidate = join(root, 'native/windows-security/build/Release/orca_windows_security.node')
mkdirSync(dirname(candidate), { recursive: true })
writeFileSync(candidate, peImage({ arch }))
mkdirSync(join(root, 'native/windows-security/src'))
writeFileSync(join(root, 'native/windows-security/binding.gyp'), '{}')
const runtimePath = join(root, 'pinned node.exe')
writeFileSync(runtimePath, 'pinned fixture')
const output = join(root, 'staged.node')
const receiptFile = join(root, 'sample.json')
const run = vi.fn((spec) => {
if (spec.args.includes('rebuild')) {
const built = join(spec.cwd, 'build/Release/orca_windows_security.node')
mkdirSync(dirname(built), { recursive: true })
writeFileSync(built, peImage({ arch }))
}
let stdout = ''
if (spec.args[0] === '-e') {
runInNewContext(spec.args[1], {
process: {
platform: 'win32',
arch,
versions: {},
stdout: {
write: (value) => {
stdout += value
}
},
exit: () => {
throw new Error('invalid probe')
}
},
require: (path) => {
expect(existsSync(path)).toBe(true)
return { currentUserSid: () => sidFixture }
}
})
}
return { ...success, stdout }
})
const runtimeAssets = { [`win32-${arch}`]: { executableSha256: digest('pinned fixture') } }
return { root, arch, candidate, runtimePath, output, receiptFile, run, runtimeAssets }
}
function windows(arch = 'x64') {
vi.stubGlobal('process', {
...process,
platform: 'win32',
arch,
versions: { ...process.versions, node: '24.21.0' }
})
vi.stubEnv('SOURCE_SHA', sourceSha)
vi.stubEnv('BENCHMARK_RUNNER', arch === 'x64' ? 'windows-2022' : 'windows-11-arm')
}
it.each(['x64', 'arm64'])('accepts only matching Windows %s source and Node identity', (arch) => {
windows(arch)
expect(() => assertSecurityComparisonHost()).not.toThrow()
for (const mismatch of [
{ platform: 'linux' },
{ platform: 'darwin' },
{ arch: 'ia32' },
{ runner: 'unknown' },
{ version: '24.20.0' },
{ sourceSha: 'main' },
{ electron: '43.7.8' }
]) {
expect(() => assertSecurityComparisonHost(mismatch)).toThrow()
}
})
it.each([
{ code: 1 },
{ code: null },
{ signal: 'SIGTERM' },
{ timedOut: true },
{ outputTruncated: true }
])('refuses incomplete commands: %j', (failure) => {
expect(() => assertSecurityCommand({ ...success, ...failure })).toThrow()
})
it.each(['control', 'candidate', 'fallback'])(
'records observed compiler calls and private operation proof for %s',
async (variant) => {
const f = fixture()
windows()
const act = () => runSecurityComparisonSample({ ...f, variant })
const receipt =
variant === 'fallback'
? await withInvalidSecurityCandidate(f.candidate, f.root, sha256Of(f.candidate), act)
: act()
expect(receipt.qualification).toBe('passed')
expect(receipt.compilerInvocationCount).toBe(variant === 'candidate' ? 0 : 1)
expect(receipt.calls.filter((call) => call.phase === 'preparation-probe')).toHaveLength(
variant === 'candidate' ? 1 : 0
)
expect(receipt.staged.currentUserSid).toBe('passed')
expect(receipt.staged.sha256).toBe(sha256Of(f.output))
expect(readFileSync(f.receiptFile, 'utf8')).not.toContain(sidFixture)
for (const [spec] of f.run.mock.calls) {
expect(spec.stdio).toBe('pipe')
expect(spec.timeoutMs).toBeGreaterThan(0)
}
}
)
it.each(['timeout', 'truncated', 'download', 'spawn'])(
'cannot hide a %s in the reuse probe behind successful fallback',
(failure) => {
const f = fixture()
windows()
const original = f.run.getMockImplementation()
f.run.mockImplementationOnce((spec) => {
if (failure === 'spawn') {
throw new Error(sidFixture)
}
if (failure === 'download') {
return { ...success, stderr: 'gyp http GET https://nodejs.org/headers' }
}
return {
...original(spec),
timedOut: failure === 'timeout',
outputTruncated: failure === 'truncated'
}
})
expect(() => runSecurityComparisonSample({ ...f, variant: 'candidate' })).toThrow()
const receipt = JSON.parse(readFileSync(f.receiptFile, 'utf8'))
expect(receipt.qualification).not.toBe('passed')
expect(receipt.calls.some((call) => !call.complete)).toBe(true)
expect(JSON.stringify(receipt)).not.toContain(sidFixture)
}
)
it('rejects wrong PE/runtime identity and a changed artifact during probing', () => {
const f = fixture()
expect(() => probeSecurityArtifact(f.candidate, f.runtimePath, 'x64', f.run)).toThrow(
'Runtime hash'
)
expect(f.run).not.toHaveBeenCalled()
expect(() =>
probeSecurityArtifact(f.candidate, f.runtimePath, 'arm64', f.run, f.runtimeAssets)
).toThrow('architecture')
writeFileSync(f.runtimePath, 'wrong runtime')
expect(() =>
probeSecurityArtifact(f.candidate, f.runtimePath, 'x64', f.run, f.runtimeAssets)
).toThrow('Runtime hash')
expect(f.run).not.toHaveBeenCalled()
writeFileSync(f.runtimePath, 'pinned fixture')
f.run.mockImplementationOnce(() => {
writeFileSync(f.candidate, 'changed')
return { ...success, stdout: 'windows-security-ready' }
})
expect(() =>
probeSecurityArtifact(f.candidate, f.runtimePath, 'x64', f.run, f.runtimeAssets)
).toThrow('changed during probe')
})
it.each(['success', 'rejection'])(
'restores exact candidate bytes and permissions after %s',
async (outcome) => {
const f = fixture()
const original = readFileSync(f.candidate)
const before = statSync(f.candidate)
const action = async () => {
expect(readFileSync(f.candidate)).not.toEqual(original)
if (outcome === 'rejection') {
throw new Error('failed sample')
}
return 'proof'
}
const operation = withInvalidSecurityCandidate(f.candidate, f.root, digest(original), action)
if (outcome === 'rejection') {
await expect(operation).rejects.toThrow('failed sample')
} else {
expect(await operation).toBe('proof')
}
expect(readFileSync(f.candidate)).toEqual(original)
expect(statSync(f.candidate).mode).toBe(before.mode)
expect(statSync(f.candidate).mtimeMs).toBeCloseTo(before.mtimeMs, 0)
}
)
it('refuses a changed candidate, symlink and preexisting backup before invalidating', async () => {
const f = fixture()
const bytes = readFileSync(f.candidate)
const action = vi.fn()
await expect(
withInvalidSecurityCandidate(f.candidate, f.root, 'wrong hash', action)
).rejects.toThrow()
writeFileSync(join(f.root, 'prepared-security-backup.node'), 'occupied')
await expect(
withInvalidSecurityCandidate(f.candidate, f.root, digest(bytes), action)
).rejects.toThrow()
const link = join(f.root, 'link.node')
symlinkSync(dirname(f.candidate), link, 'junction')
await expect(withInvalidSecurityCandidate(link, f.root, digest(bytes), action)).rejects.toThrow(
'regular file'
)
expect(readFileSync(f.candidate)).toEqual(bytes)
expect(action).not.toHaveBeenCalled()
})
it('keeps the existing manual default and runs the security mode on only its two existing slots', () => {
const workflow = parse(
readFileSync('.github/workflows/windows-native-preparation-benchmark.yml', 'utf8')
)
expect(Object.keys(workflow.on)).toEqual(['workflow_dispatch'])
expect(workflow.on.workflow_dispatch.inputs.comparison).toEqual({
description: expect.any(String),
type: 'choice',
default: 'native-preparation',
options: ['native-preparation', 'windows-security']
})
expect(Object.keys(workflow.jobs)).toEqual(['compare'])
expect(workflow.jobs.compare.strategy.matrix.os).toEqual(['windows-2022', 'windows-11-arm'])
expect(workflow.jobs.compare.strategy['max-parallel']).toBe(2)
const steps = workflow.jobs.compare.steps
const native = steps.find(
(step) => step.run === 'node config/scripts/benchmark-windows-native-preparation.mjs'
)
const security = steps.find(
(step) => step.run === 'node config/scripts/benchmark-windows-security-reuse.mjs'
)
for (const choice of [undefined, '', 'native-preparation', 'windows-security']) {
expect(runInNewContext(native.if, { inputs: { comparison: choice } })).toBe(
choice !== 'windows-security'
)
expect(runInNewContext(security.if, { inputs: { comparison: choice } })).toBe(
choice === 'windows-security'
)
}
expect(steps.at(-1).if).toBe('always()')
expect(workflow.env.ORCA_BACKGROUND_LAUNCH).toBe('1')
})
function hostedFixture(arch = 'x64', failure) {
const f = fixture(arch)
windows(arch)
rmSync(f.candidate)
for (const path of [
'pnpm-lock.yaml',
'pnpm-workspace.yaml',
'package.json',
'.github/actions/prepare-native-runtime/action.yml',
'src/shared/node-runtime-pin.ts',
'config/scripts/build-windows-security-addon.mjs',
'config/scripts/ensure-native-runtime.mjs',
'config/scripts/benchmark-windows-security-reuse.mjs'
]) {
mkdirSync(dirname(join(f.root, path)), { recursive: true })
writeFileSync(join(f.root, path), 'source fixture')
}
const ensureRuntime = vi.fn(async () => f.runtimePath)
const prepareHeaders = vi.fn(async ({ workDir }) => {
mkdirSync(workDir, { recursive: true })
writeFileSync(join(workDir, 'header'), 'verified header fixture')
return workDir
})
const run = vi.fn(async (spec) => {
if (spec.args.includes('HEAD') && spec.args.includes('rev-parse')) {
return { ...success, stdout: sourceSha }
}
if (spec.args.includes('HEAD^{tree}')) {
return { ...success, stdout: 'b'.repeat(40) }
}
if (spec.args.includes('native/windows-security')) {
return { ...success, stdout: 'native/windows-security/binding.gyp\n' }
}
if (spec.args.some((arg) => arg.endsWith('initialize-windows-native-toolchain.mjs'))) {
writeFileSync(f.candidate, peImage({ arch }))
}
const variant = spec.args.find((arg) => arg.startsWith('--sample='))?.slice(9)
if (variant) {
if (failure === variant) {
return { ...success, code: 1, timedOut: true, stderr: sidFixture }
}
runSecurityComparisonSample({
...f,
variant,
output: spec.args[3],
receiptFile: spec.args[4]
})
}
return success
})
f.ports = {
run,
runSync: f.run,
ensureRuntime,
prepareHeaders,
runtimeAssets: f.runtimeAssets,
hashFile: (path) => (path === process.execPath ? digest('pinned fixture') : sha256Of(path))
}
return f
}
it.each(['x64', 'arm64'])(
'qualifies exactly one same-source %s pair and fallback after separate preparation',
async (arch) => {
const f = hostedFixture(arch)
const report = await benchmarkWindowsSecurityReuse(f.root, f.ports)
expect(report.qualification).toBe('passed')
expect(
report.samples.map((sample) => [sample.variant, sample.compilerInvocationCount])
).toEqual([
['control', 1],
['candidate', 0],
['fallback', 1]
])
expect(report.restored).toBe(true)
expect(report.restoredPreparedSha256).toBe(report.prepared.sha256)
expect(sha256Of(f.candidate)).toBe(report.prepared.sha256)
expect(report.runtime.sha256).toBe(digest('pinned fixture'))
expect(
report.samples.every((sample) => sample.fullCommandMs >= sample.buildIncludingReuseProbesMs)
).toBe(true)
const phases = report.phases.map((phase) => phase.label)
expect(phases.indexOf('verify-prepared-node')).toBeLessThan(
phases.indexOf('control-full-build-and-probe')
)
expect(phases.indexOf('warm-compiler-before-measurement')).toBeLessThan(
phases.indexOf('control-full-build-and-probe')
)
expect(report.sourceSha).toBe(sourceSha)
expect(report.sourceHashes['native/windows-security/binding.gyp']).toBe(
sha256Of(join(f.root, 'native/windows-security/binding.gyp'))
)
expect(JSON.stringify(report)).not.toContain(sidFixture)
}
)
it('restores the owned candidate and leaves a failed redacted receipt when fallback times out', async () => {
const f = hostedFixture('x64', 'fallback')
await expect(benchmarkWindowsSecurityReuse(f.root, f.ports)).rejects.toThrow('did not qualify')
const report = JSON.parse(
readFileSync(join(f.root, '.build/windows-native-benchmark/security/receipt.json'), 'utf8')
)
expect(report.qualification).toBe('failed')
expect(report.failureStage).toBe('fallback-full-build-and-probe')
expect(report.restored).toBe(true)
expect(sha256Of(f.candidate)).toBe(report.prepared.sha256)
expect(
report.phases.find((phase) => phase.label === 'fallback-full-build-and-probe').timedOut
).toBe(true)
expect(JSON.stringify(report)).not.toContain(sidFixture)
})
it('refuses another checkout commit before preparing artifacts', async () => {
const f = hostedFixture()
f.ports.run.mockResolvedValueOnce({ ...success, stdout: 'c'.repeat(40) })
await expect(benchmarkWindowsSecurityReuse(f.root, f.ports)).rejects.toThrow('did not qualify')
expect(f.ports.prepareHeaders).not.toHaveBeenCalled()
expect(existsSync(f.candidate)).toBe(false)
})
@@ -7,6 +7,37 @@ import {
update
} from './MobileNativeChatView.harness.test-fixture'
vi.mock('react-native', async () => {
const { reactNativeStub } = await import('./MobileNativeChatView.render-stubs.test-fixture')
return reactNativeStub()
})
vi.mock('react-native-safe-area-context', () => ({
useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 })
}))
vi.mock('react-native-gesture-handler', async () => {
const { gestureHandlerStub } = await import('./MobileNativeChatView.render-stubs.test-fixture')
return gestureHandlerStub()
})
vi.mock('lucide-react-native', () => ({
ArrowDown: 'ArrowDown',
ChevronsDownUp: 'ChevronsDownUp',
ChevronsUpDown: 'ChevronsUpDown',
Square: 'Square'
}))
vi.mock('./MobileNativeChatMessage', () => ({ MobileNativeChatMessage: 'ChatMessage' }))
vi.mock('./MobileNativeChatLiveLine', () => ({ MobileNativeChatLiveLine: 'LiveStatus' }))
vi.mock('./MobileNativeChatAsk', () => ({ MobileNativeChatAsk: 'ChatAsk' }))
vi.mock('./MobileNativeChatPermission', () => ({ MobileNativeChatPermission: 'ChatPermission' }))
vi.mock('./MobileNativeChatQuestion', () => ({ MobileNativeChatQuestion: 'ChatQuestion' }))
vi.mock('../components/ActionSheetModal', () => ({ ActionSheetModal: 'ActionSheetModal' }))
vi.mock('./MobileAgentWorkingIndicator', () => ({
MobileAgentWorkingIndicator: 'WorkingIndicator'
}))
vi.mock('./MobileNativeChatComposer', async () => {
const { composerStub } = await import('./MobileNativeChatView.render-stubs.test-fixture')
return composerStub()
})
describe('MobileNativeChatView: composer lock and prompt cards', () => {
installChatViewHooks()
@@ -12,6 +12,37 @@ import {
update
} from './MobileNativeChatView.harness.test-fixture'
vi.mock('react-native', async () => {
const { reactNativeStub } = await import('./MobileNativeChatView.render-stubs.test-fixture')
return reactNativeStub()
})
vi.mock('react-native-safe-area-context', () => ({
useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 })
}))
vi.mock('react-native-gesture-handler', async () => {
const { gestureHandlerStub } = await import('./MobileNativeChatView.render-stubs.test-fixture')
return gestureHandlerStub()
})
vi.mock('lucide-react-native', () => ({
ArrowDown: 'ArrowDown',
ChevronsDownUp: 'ChevronsDownUp',
ChevronsUpDown: 'ChevronsUpDown',
Square: 'Square'
}))
vi.mock('./MobileNativeChatMessage', () => ({ MobileNativeChatMessage: 'ChatMessage' }))
vi.mock('./MobileNativeChatLiveLine', () => ({ MobileNativeChatLiveLine: 'LiveStatus' }))
vi.mock('./MobileNativeChatAsk', () => ({ MobileNativeChatAsk: 'ChatAsk' }))
vi.mock('./MobileNativeChatPermission', () => ({ MobileNativeChatPermission: 'ChatPermission' }))
vi.mock('./MobileNativeChatQuestion', () => ({ MobileNativeChatQuestion: 'ChatQuestion' }))
vi.mock('../components/ActionSheetModal', () => ({ ActionSheetModal: 'ActionSheetModal' }))
vi.mock('./MobileAgentWorkingIndicator', () => ({
MobileAgentWorkingIndicator: 'WorkingIndicator'
}))
vi.mock('./MobileNativeChatComposer', async () => {
const { composerStub } = await import('./MobileNativeChatView.render-stubs.test-fixture')
return composerStub()
})
describe('MobileNativeChatView: send and read failure banners', () => {
installChatViewHooks()
@@ -3,80 +3,12 @@ import { act, create, type ReactTestInstance, type ReactTestRenderer } from 'rea
import { afterEach, beforeEach, expect, vi } from 'vitest'
import type { NativeChatMessage } from '../../../src/shared/native-chat-types'
import { MobileNativeChatView } from './MobileNativeChatView'
const scrollToEnd = vi.hoisted(() => vi.fn())
const scrollToOffset = vi.hoisted(() => vi.fn())
const platform = vi.hoisted(() => ({ OS: 'ios' }))
const pageScroller = vi.hoisted(() => ({ scrollHeight: 0 }))
vi.mock('react-native', async () => {
const React = await import('react')
return {
ActivityIndicator: 'ActivityIndicator',
FlatList: React.forwardRef((props, ref) => {
const handle = { scrollToEnd, scrollToOffset, getScrollableNode: () => pageScroller }
React.useImperativeHandle(ref, () => handle, [])
return React.createElement('FlatList', props)
}),
Platform: platform,
Pressable: 'Pressable',
StyleSheet: { create: (styles: unknown) => styles, hairlineWidth: 1 },
Text: 'Text',
View: 'View'
}
})
vi.mock('react-native-safe-area-context', () => ({
useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 })
}))
vi.mock('react-native-gesture-handler', () => {
const chain = {
runOnJS: () => chain,
onStart: () => chain,
onUpdate: () => chain
}
return {
Gesture: { Simultaneous: () => ({}), Native: () => ({}), Pinch: () => chain },
GestureDetector: 'GestureDetector',
GestureHandlerRootView: 'GestureHandlerRootView'
}
})
vi.mock('lucide-react-native', () => ({
ArrowDown: 'ArrowDown',
ChevronsDownUp: 'ChevronsDownUp',
ChevronsUpDown: 'ChevronsUpDown',
Square: 'Square'
}))
vi.mock('./MobileNativeChatMessage', () => ({ MobileNativeChatMessage: 'ChatMessage' }))
vi.mock('./MobileNativeChatLiveLine', () => ({ MobileNativeChatLiveLine: 'LiveStatus' }))
vi.mock('./MobileNativeChatAsk', () => ({ MobileNativeChatAsk: 'ChatAsk' }))
vi.mock('./MobileNativeChatPermission', () => ({ MobileNativeChatPermission: 'ChatPermission' }))
vi.mock('./MobileNativeChatQuestion', () => ({ MobileNativeChatQuestion: 'ChatQuestion' }))
vi.mock('../components/ActionSheetModal', () => ({ ActionSheetModal: 'ActionSheetModal' }))
vi.mock('./MobileAgentWorkingIndicator', () => ({
MobileAgentWorkingIndicator: 'WorkingIndicator'
}))
// Stand-in composer: exposes the view's `handleSend` through a pressable, which is
// the only composer behaviour these banner tests exercise.
vi.mock('./MobileNativeChatComposer', async () => {
const React = await import('react')
return {
MobileNativeChatComposer: (props: {
onSend: (text: string) => Promise<boolean>
disabled?: boolean
placeholder?: string
}) =>
React.createElement('Composer', {
...props,
accessibilityLabel: 'Send message',
onPress: () => props.onSend('hi')
})
}
})
import {
scrollToEnd,
scrollToOffset,
platform,
pageScroller
} from './MobileNativeChatView.render-stubs.test-fixture'
export type Overrides = {
messages?: Parameters<typeof MobileNativeChatView>[0]['messages']
@@ -222,5 +154,4 @@ export async function scrollAwayFromTail(): Promise<void> {
})
}
// Vitest rejects exporting vi.hoisted bindings directly, so they are exported by name here.
export { scrollToEnd, scrollToOffset, platform, pageScroller }
@@ -13,6 +13,37 @@ import {
update
} from './MobileNativeChatView.harness.test-fixture'
vi.mock('react-native', async () => {
const { reactNativeStub } = await import('./MobileNativeChatView.render-stubs.test-fixture')
return reactNativeStub()
})
vi.mock('react-native-safe-area-context', () => ({
useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 })
}))
vi.mock('react-native-gesture-handler', async () => {
const { gestureHandlerStub } = await import('./MobileNativeChatView.render-stubs.test-fixture')
return gestureHandlerStub()
})
vi.mock('lucide-react-native', () => ({
ArrowDown: 'ArrowDown',
ChevronsDownUp: 'ChevronsDownUp',
ChevronsUpDown: 'ChevronsUpDown',
Square: 'Square'
}))
vi.mock('./MobileNativeChatMessage', () => ({ MobileNativeChatMessage: 'ChatMessage' }))
vi.mock('./MobileNativeChatLiveLine', () => ({ MobileNativeChatLiveLine: 'LiveStatus' }))
vi.mock('./MobileNativeChatAsk', () => ({ MobileNativeChatAsk: 'ChatAsk' }))
vi.mock('./MobileNativeChatPermission', () => ({ MobileNativeChatPermission: 'ChatPermission' }))
vi.mock('./MobileNativeChatQuestion', () => ({ MobileNativeChatQuestion: 'ChatQuestion' }))
vi.mock('../components/ActionSheetModal', () => ({ ActionSheetModal: 'ActionSheetModal' }))
vi.mock('./MobileAgentWorkingIndicator', () => ({
MobileAgentWorkingIndicator: 'WorkingIndicator'
}))
vi.mock('./MobileNativeChatComposer', async () => {
const { composerStub } = await import('./MobileNativeChatView.render-stubs.test-fixture')
return composerStub()
})
describe('MobileNativeChatView: history paging and anchoring', () => {
installChatViewHooks()
@@ -0,0 +1,54 @@
import { vi } from 'vitest'
const scrollToEnd = vi.hoisted(() => vi.fn())
const scrollToOffset = vi.hoisted(() => vi.fn())
const platform = vi.hoisted(() => ({ OS: 'ios' }))
const pageScroller = vi.hoisted(() => ({ scrollHeight: 0 }))
export async function reactNativeStub() {
const React = await import('react')
return {
ActivityIndicator: 'ActivityIndicator',
FlatList: React.forwardRef((props, ref) => {
const handle = { scrollToEnd, scrollToOffset, getScrollableNode: () => pageScroller }
React.useImperativeHandle(ref, () => handle, [])
return React.createElement('FlatList', props)
}),
Platform: platform,
Pressable: 'Pressable',
StyleSheet: { create: (styles: unknown) => styles, hairlineWidth: 1 },
Text: 'Text',
View: 'View'
}
}
export function gestureHandlerStub() {
const chain = {
runOnJS: () => chain,
onStart: () => chain,
onUpdate: () => chain
}
return {
Gesture: { Simultaneous: () => ({}), Native: () => ({}), Pinch: () => chain },
GestureDetector: 'GestureDetector',
GestureHandlerRootView: 'GestureHandlerRootView'
}
}
export async function composerStub() {
const React = await import('react')
return {
MobileNativeChatComposer: (props: {
onSend: (text: string) => Promise<boolean>
disabled?: boolean
placeholder?: string
}) =>
React.createElement('Composer', {
...props,
accessibilityLabel: 'Send message',
onPress: () => props.onSend('hi')
})
}
}
export { scrollToEnd, scrollToOffset, platform, pageScroller }
@@ -1,4 +1,4 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import {
installChatViewHooks,
assistantTurn,
@@ -8,6 +8,37 @@ import {
update
} from './MobileNativeChatView.harness.test-fixture'
vi.mock('react-native', async () => {
const { reactNativeStub } = await import('./MobileNativeChatView.render-stubs.test-fixture')
return reactNativeStub()
})
vi.mock('react-native-safe-area-context', () => ({
useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 })
}))
vi.mock('react-native-gesture-handler', async () => {
const { gestureHandlerStub } = await import('./MobileNativeChatView.render-stubs.test-fixture')
return gestureHandlerStub()
})
vi.mock('lucide-react-native', () => ({
ArrowDown: 'ArrowDown',
ChevronsDownUp: 'ChevronsDownUp',
ChevronsUpDown: 'ChevronsUpDown',
Square: 'Square'
}))
vi.mock('./MobileNativeChatMessage', () => ({ MobileNativeChatMessage: 'ChatMessage' }))
vi.mock('./MobileNativeChatLiveLine', () => ({ MobileNativeChatLiveLine: 'LiveStatus' }))
vi.mock('./MobileNativeChatAsk', () => ({ MobileNativeChatAsk: 'ChatAsk' }))
vi.mock('./MobileNativeChatPermission', () => ({ MobileNativeChatPermission: 'ChatPermission' }))
vi.mock('./MobileNativeChatQuestion', () => ({ MobileNativeChatQuestion: 'ChatQuestion' }))
vi.mock('../components/ActionSheetModal', () => ({ ActionSheetModal: 'ActionSheetModal' }))
vi.mock('./MobileAgentWorkingIndicator', () => ({
MobileAgentWorkingIndicator: 'WorkingIndicator'
}))
vi.mock('./MobileNativeChatComposer', async () => {
const { composerStub } = await import('./MobileNativeChatView.render-stubs.test-fixture')
return composerStub()
})
describe('MobileNativeChatView: rows and the streaming bubble', () => {
installChatViewHooks()
@@ -1,4 +1,4 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import {
installChatViewHooks,
hostType,
@@ -7,6 +7,37 @@ import {
update
} from './MobileNativeChatView.harness.test-fixture'
vi.mock('react-native', async () => {
const { reactNativeStub } = await import('./MobileNativeChatView.render-stubs.test-fixture')
return reactNativeStub()
})
vi.mock('react-native-safe-area-context', () => ({
useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 })
}))
vi.mock('react-native-gesture-handler', async () => {
const { gestureHandlerStub } = await import('./MobileNativeChatView.render-stubs.test-fixture')
return gestureHandlerStub()
})
vi.mock('lucide-react-native', () => ({
ArrowDown: 'ArrowDown',
ChevronsDownUp: 'ChevronsDownUp',
ChevronsUpDown: 'ChevronsUpDown',
Square: 'Square'
}))
vi.mock('./MobileNativeChatMessage', () => ({ MobileNativeChatMessage: 'ChatMessage' }))
vi.mock('./MobileNativeChatLiveLine', () => ({ MobileNativeChatLiveLine: 'LiveStatus' }))
vi.mock('./MobileNativeChatAsk', () => ({ MobileNativeChatAsk: 'ChatAsk' }))
vi.mock('./MobileNativeChatPermission', () => ({ MobileNativeChatPermission: 'ChatPermission' }))
vi.mock('./MobileNativeChatQuestion', () => ({ MobileNativeChatQuestion: 'ChatQuestion' }))
vi.mock('../components/ActionSheetModal', () => ({ ActionSheetModal: 'ActionSheetModal' }))
vi.mock('./MobileAgentWorkingIndicator', () => ({
MobileAgentWorkingIndicator: 'WorkingIndicator'
}))
vi.mock('./MobileNativeChatComposer', async () => {
const { composerStub } = await import('./MobileNativeChatView.render-stubs.test-fixture')
return composerStub()
})
describe('MobileNativeChatView: Stop control', () => {
installChatViewHooks()
@@ -13,6 +13,37 @@ import {
update
} from './MobileNativeChatView.harness.test-fixture'
vi.mock('react-native', async () => {
const { reactNativeStub } = await import('./MobileNativeChatView.render-stubs.test-fixture')
return reactNativeStub()
})
vi.mock('react-native-safe-area-context', () => ({
useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 })
}))
vi.mock('react-native-gesture-handler', async () => {
const { gestureHandlerStub } = await import('./MobileNativeChatView.render-stubs.test-fixture')
return gestureHandlerStub()
})
vi.mock('lucide-react-native', () => ({
ArrowDown: 'ArrowDown',
ChevronsDownUp: 'ChevronsDownUp',
ChevronsUpDown: 'ChevronsUpDown',
Square: 'Square'
}))
vi.mock('./MobileNativeChatMessage', () => ({ MobileNativeChatMessage: 'ChatMessage' }))
vi.mock('./MobileNativeChatLiveLine', () => ({ MobileNativeChatLiveLine: 'LiveStatus' }))
vi.mock('./MobileNativeChatAsk', () => ({ MobileNativeChatAsk: 'ChatAsk' }))
vi.mock('./MobileNativeChatPermission', () => ({ MobileNativeChatPermission: 'ChatPermission' }))
vi.mock('./MobileNativeChatQuestion', () => ({ MobileNativeChatQuestion: 'ChatQuestion' }))
vi.mock('../components/ActionSheetModal', () => ({ ActionSheetModal: 'ActionSheetModal' }))
vi.mock('./MobileAgentWorkingIndicator', () => ({
MobileAgentWorkingIndicator: 'WorkingIndicator'
}))
vi.mock('./MobileNativeChatComposer', async () => {
const { composerStub } = await import('./MobileNativeChatView.render-stubs.test-fixture')
return composerStub()
})
describe('MobileNativeChatView: tail-follow', () => {
installChatViewHooks()