Merge remote-tracking branch 'origin/main' into brennanb2025/chat-recorded-outcomes-from-journal

# Conflicts:
#	mobile/src/session/MobileNativeChatMessage.test.ts
This commit is contained in:
Brennan Benson
2026-10-04 00:33:03 -07:00
2287 changed files with 111895 additions and 16819 deletions
@@ -6,6 +6,10 @@ inputs:
description: Restore or save the pnpm download store; verification and native caches are independent.
required: false
default: 'true'
cache-pnpm-store-lookup-only:
description: Auto uses measured hosted Node 24 root installs; true forces lookup, false retains archive restoration.
required: false
default: auto
cache-pnpm-verification:
description: Restore pnpm's policy-checked lockfile verification record.
required: false
@@ -32,6 +36,9 @@ inputs:
default: 'false'
outputs:
pnpm-store-cache-hit:
description: Whether the requested download store matched an existing cache.
value: ${{ steps.pnpm-store-lookup.outputs.cache-hit || steps.pnpm-store-restore.outputs.cache-hit || steps.requested-node.outputs.cache-hit || steps.default-node.outputs.cache-hit }}
verification-cache-hit:
description: Whether pnpm's verification record was restored.
value: ${{ steps.verification-cache.outputs.cache-hit }}
@@ -60,6 +67,30 @@ outputs:
runs:
using: composite
steps:
- name: Resolve pnpm store mode
id: pnpm-store-mode
if: >-
github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' &&
(inputs.cache-pnpm-store-lookup-only == 'true' ||
(inputs.cache-pnpm-store-lookup-only == 'auto' &&
inputs.cache-dependency-path == 'pnpm-lock.yaml' &&
runner.environment == 'github-hosted' && job.container.id == '' &&
(runner.os == 'Linux' || runner.os == 'macOS' || runner.os == 'Windows') &&
(runner.arch == 'X64' || runner.arch == 'ARM64') &&
(inputs.node-version == '' || inputs.node-version == '24')))
shell: bash
env:
LOOKUP_REQUEST: ${{ inputs.cache-pnpm-store-lookup-only }}
run: |
lookup_only=true
case "$LOOKUP_REQUEST" in
[aA][uU][tT][oO])
# Hosted runners have Node for this manifest-only check before toolchain setup.
lookup_only="$(node -p 'const p = require("./package.json"); p.engines?.node === "24" && typeof p.packageManager === "string" && p.packageManager.split("+")[0] === "pnpm@12.8.1"')"
;;
esac
printf 'lookup-only=%s\n' "$lookup_only" >> "$GITHUB_OUTPUT"
# setup-node needs pnpm on PATH to locate and restore its store.
- name: Setup pnpm
uses: pnpm/setup@v2
@@ -73,7 +104,7 @@ runs:
uses: actions/setup-node@v6
with:
node-version-file: package.json
cache: ${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && 'pnpm' || '' }}
cache: ${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && steps.pnpm-store-mode.outputs.lookup-only != 'true' && 'pnpm' || '' }}
cache-dependency-path: ${{ inputs.cache-dependency-path }}
package-manager-cache: false
@@ -83,7 +114,7 @@ runs:
uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node-version }}
cache: ${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && 'pnpm' || '' }}
cache: ${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && steps.pnpm-store-mode.outputs.lookup-only != 'true' && 'pnpm' || '' }}
cache-dependency-path: ${{ inputs.cache-dependency-path }}
package-manager-cache: false
@@ -92,26 +123,33 @@ runs:
id: pnpm-store
if: >-
github.event_name == 'pull_request' && inputs.cache-pnpm-store != 'false' &&
!(runner.os == 'Linux' && (runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml') &&
!((runner.os == 'Linux' || runner.os == 'macOS') && (runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml') &&
(runner.os != 'Windows' ||
!(runner.arch == 'X64' && contains(inputs.cache-dependency-path, 'mobile/pnpm-lock.yaml')) &&
!((runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml'))
!((runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml')) ||
(github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' &&
steps.pnpm-store-mode.outputs.lookup-only == 'true')
shell: bash
env:
LOCKFILE_HASH: ${{ hashFiles(inputs.cache-dependency-path) }}
STORE_LOOKUP_ONLY: ${{ steps.pnpm-store-mode.outputs.lookup-only == 'true' }}
run: |
test -n "$LOCKFILE_HASH"
cache_path="$(pnpm store path --silent)"
test -n "$cache_path"
printf 'path=%s\n' "$cache_path" >> "$GITHUB_OUTPUT"
printf 'arch=%s\n' "$(node -p 'require("node:os").arch()')" >> "$GITHUB_OUTPUT"
if [ "$STORE_LOOKUP_ONLY" = 'true' ]; then
printf 'ORCA_PNPM_STORE_CACHE_PATH=%s\n' "$cache_path" >> "$GITHUB_ENV"
fi
# Match setup-node's key and path so existing default-branch stores remain reusable.
# Direct downloads beat store restoration for the measured Linux and Windows installs.
# Direct downloads beat store restoration for the measured Linux, macOS and Windows installs.
- name: Restore pnpm download store without saving
id: pnpm-store-restore
if: >-
github.event_name == 'pull_request' && inputs.cache-pnpm-store != 'false' &&
!(runner.os == 'Linux' && (runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml') &&
!((runner.os == 'Linux' || runner.os == 'macOS') && (runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml') &&
(runner.os != 'Windows' ||
!(runner.arch == 'X64' && contains(inputs.cache-dependency-path, 'mobile/pnpm-lock.yaml')) &&
!((runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml'))
@@ -120,6 +158,17 @@ runs:
path: ${{ steps.pnpm-store.outputs.path }}
key: node-cache-${{ runner.os }}-${{ steps.pnpm-store.outputs.arch }}-pnpm-${{ hashFiles(inputs.cache-dependency-path) }}
# Producers can refresh access and publish misses without downloading existing archives.
- name: Keep pnpm download store without restoring
id: pnpm-store-lookup
if: steps.pnpm-store-mode.outputs.lookup-only == 'true'
uses: actions/cache@v5
with:
# Twice-nested composite cleanup loses internal step outputs.
path: ${{ env.ORCA_PNPM_STORE_CACHE_PATH }}
key: node-cache-${{ runner.os }}-${{ steps.pnpm-store.outputs.arch }}-pnpm-${{ hashFiles(inputs.cache-dependency-path) }}
lookup-only: true
- name: Restore pnpm verification record
id: verification-cache
uses: ./.github/actions/restore-pnpm-verification
@@ -10,7 +10,7 @@ runs:
uses: actions/cache@v5
with:
path: ~/.cache/orca-git-compat/git-2.25.5
key: git-compat-baseline-${{ runner.os }}-${{ runner.arch }}-2.25.5
key: git-compat-baseline-${{ runner.os }}-${{ runner.arch }}-2.25.5-submodule
# Finish the CPU-heavy build before any timed compatibility lanes start.
- name: Build the baseline Git binary
@@ -18,7 +18,9 @@ runs:
run: |
archive="$RUNNER_TEMP/git-2.25.5.tar.gz"
source="$HOME/.cache/orca-git-compat/git-2.25.5"
if [ -x "$source/git" ]; then
if [ -x "$source/git" ] && [ -x "$source/git-submodule" ] \
&& [ -f "$source/git-sh-setup" ] && [ -f "$source/git-sh-i18n" ] \
&& [ -f "$source/git-parse-remote" ] && [ -x "$source/git-sh-i18n--envsubst" ]; then
exit 0
fi
curl -fsSL https://www.kernel.org/pub/software/scm/git/git-2.25.5.tar.gz -o "$archive"
@@ -27,6 +29,7 @@ runs:
mkdir -p "$source"
tar -xzf "$archive" -C "$source" --strip-components=1
make -C "$source" -j"$(nproc)" \
NO_GETTEXT=YesPlease NO_TCLTK=YesPlease NO_PYTHON=YesPlease git
NO_GETTEXT=YesPlease NO_TCLTK=YesPlease NO_PYTHON=YesPlease \
git git-submodule git-sh-setup git-sh-i18n git-parse-remote git-sh-i18n--envsubst
# Object files are no longer needed after linking the cached binary.
find "$source" -name '*.o' -delete
@@ -0,0 +1,45 @@
name: Prepare headless detector compiler
description: Reuse the policy-checked compiler from main; callers install normally on a miss.
inputs:
seed:
description: Pack an already installed compiler instead of activating a cached compiler.
default: 'false'
outputs:
available:
description: Whether the cached compiler was validated and activated.
value: ${{ steps.activate.outputs.available }}
runs:
using: composite
steps:
- id: identity
shell: bash
env:
COMPILER_POLICY_HASH: ${{ hashFiles('package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml', '.npmrc', '.pnpmfile.cjs', 'config/patches/**', '.github/actions/install-node-dependencies/**', '.github/actions/restore-pnpm-verification/**', 'config/scripts/headless-detector-compiler-cache.mjs', '.github/actions/prepare-headless-compiler/action.yml') }}
run: node config/scripts/headless-detector-compiler-cache.mjs identity
- id: cache
uses: actions/cache/restore@v5
continue-on-error: true
with:
path: ${{ steps.identity.outputs.path }}
key: ${{ steps.identity.outputs.key }}
- id: activate
if: inputs.seed != 'true' && steps.cache.outputs.cache-hit == 'true'
shell: bash
env:
COMPILER_CACHE_KEY: ${{ steps.identity.outputs.key }}
COMPILER_CACHE_PATH: ${{ steps.identity.outputs.path }}
run: node config/scripts/headless-detector-compiler-cache.mjs activate
- name: Pack installed compiler
if: inputs.seed == 'true' && steps.cache.outputs.cache-hit != 'true'
shell: bash
env:
COMPILER_CACHE_KEY: ${{ steps.identity.outputs.key }}
COMPILER_CACHE_PATH: ${{ steps.identity.outputs.path }}
run: node config/scripts/headless-detector-compiler-cache.mjs pack
- name: Save compiler only from main
if: inputs.seed == 'true' && steps.cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
uses: actions/cache/save@v5
continue-on-error: true
with:
path: ${{ steps.identity.outputs.path }}
key: ${{ steps.identity.outputs.key }}
+12 -2
View File
@@ -13,6 +13,8 @@ on:
- '.github/actions/restore-pnpm-verification/**'
- '.github/actions/prepare-native-runtime/**'
- '.github/actions/prepare-git-compatibility/**'
- '.github/actions/prepare-headless-compiler/**'
- 'config/scripts/headless-detector-compiler-cache*'
- '.github/actions/prepare-linux-package-fixture/**'
- 'config/docker/headless-serve-shutdown/**'
- 'config/docker/cli-launch-contract/**'
@@ -35,13 +37,14 @@ on:
- 'src/shared/zip-extractor-command.ts'
- 'config/scripts/shared-electron-dist-cache.mjs'
- 'config/scripts/space-sharing-copy.mjs'
- 'config/patches/node-pty@1.1.0.patch'
- 'config/patches/@vscode__windows-process-tree@0.8.0.patch'
- 'config/patches/**'
- 'native/windows-registry/**'
pull_request:
paths:
- '.github/workflows/ci-cache-warmup.yml'
- '.github/actions/prepare-git-compatibility/**'
- '.github/actions/prepare-headless-compiler/**'
- 'config/scripts/headless-detector-compiler-cache*'
- '.github/actions/prepare-linux-package-fixture/**'
- 'config/docker/headless-serve-shutdown/**'
- 'config/docker/cli-launch-contract/**'
@@ -73,6 +76,11 @@ jobs:
native-runtime: node
node-version: '24'
cache-electron-package: 'true'
cache-pnpm-store-lookup-only: 'true'
- uses: ./.github/actions/prepare-headless-compiler
with:
seed: 'true'
- name: Populate shared Electron archive
run: node config/scripts/install-electron-package-binary.mjs
@@ -106,6 +114,7 @@ jobs:
native-runtime: node
node-version: '24'
cache-electron-package: 'true'
cache-pnpm-store-lookup-only: 'true'
- name: Populate shared Electron archive
run: node config/scripts/install-electron-package-binary.mjs
- name: Verify native cache is usable
@@ -127,6 +136,7 @@ jobs:
- uses: ./.github/actions/install-node-dependencies
with:
native-runtime: node
cache-pnpm-store-lookup-only: 'true'
- name: Verify native cache is usable
run: node config/scripts/ensure-native-runtime.mjs --check-only
+49
View File
@@ -0,0 +1,49 @@
name: macOS updater regression tests
on:
pull_request:
paths:
- '.github/workflows/macos-updater-tests.yml'
- 'src/main/macos-update-running-instances*'
- 'src/main/updater*'
- 'src/main/updater/**'
- 'src/main/startup/main-process-quit*'
- 'src/main/window/main-window-state-lifecycle*'
- 'src/main/window/dashboard-popout-window*'
- 'src/shared/child-process/**'
- 'src/shared/update-status-types.ts'
- 'pnpm-lock.yaml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: macos-updater-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
updater:
runs-on: macos-15
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: ./.github/actions/install-node-dependencies
with:
native-runtime: node
- name: Exercise native application registry and update shutdown
env:
ORCA_BACKGROUND_LAUNCH: '1'
run: >-
pnpm exec vitest run --config config/vitest.config.ts
src/main/macos-update-running-instances.test.ts
src/main/macos-update-running-instances.integration.test.ts
src/main/updater.mac-install.test.ts
src/main/updater.headless-serve-install.test.ts
src/main/updater-mac-quit-guard.test.ts
src/main/startup/desktop-startup-ordering.test.ts
src/main/startup/main-process-quit-update-veto.test.ts
src/main/window/main-window-state-lifecycle.test.ts
src/main/window/dashboard-popout-window.test.ts
+5 -5
View File
@@ -114,20 +114,20 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
# Both compilers are read-only; finish them before starting the test workers.
# Call installed tools so pnpm's dependency refresh cannot race between checks.
- name: Typecheck
id: production-types
background: true
run: pnpm typecheck
run: node node_modules/typescript/bin/tsc --noEmit
- wait: production-types
# Why a ratchet and not the raw typecheck: mobile/tsconfig.json excludes test files, so until
# tsconfig.test.json existed nothing checked them, and at introduction 127 of the 632 had
# drifted. This fails when a test file that checks today stops checking, when a test leaves
# the program, and on @ts-nocheck; the baseline may only shrink.
- name: Typecheck tests (ratchet)
run: pnpm run check:tests-typecheck
- wait: production-types
run: node scripts/check-tests-typecheck-ratchet.mjs
# This includes the bridged replay of the whole recording corpus, which used to be a second
# step of its own behind RPC_FOUNDATION_BRIDGE=1. A gate nobody can forget to set is the point:
+33 -20
View File
@@ -17,6 +17,7 @@ on:
- '.pnpmfile.cjs'
- '.github/actions/install-node-dependencies/**'
- '.github/actions/restore-pnpm-verification/**'
- '.github/actions/prepare-headless-compiler/**'
- '.github/actions/prepare-native-runtime/**'
- '.github/actions/prepare-orcad-prebuilds/**'
- '.github/workflows/node-server-tests.yml'
@@ -37,6 +38,7 @@ on:
- '.pnpmfile.cjs'
- '.github/actions/install-node-dependencies/**'
- '.github/actions/restore-pnpm-verification/**'
- '.github/actions/prepare-headless-compiler/**'
- '.github/actions/prepare-native-runtime/**'
- '.github/actions/prepare-orcad-prebuilds/**'
- '.github/workflows/node-server-tests.yml'
@@ -112,8 +114,12 @@ jobs:
echo 'should_run=true' >> "$GITHUB_OUTPUT"
fi
- uses: ./.github/actions/install-node-dependencies
- uses: ./.github/actions/prepare-headless-compiler
id: compiler
if: steps.scope.outputs.graph_required == 'true'
continue-on-error: true
- uses: ./.github/actions/install-node-dependencies
if: steps.scope.outputs.graph_required == 'true' && steps.compiler.outputs.available != 'true'
- name: Check the headless import graph
id: graph
if: steps.scope.outputs.graph_required == 'true'
@@ -155,6 +161,28 @@ jobs:
with:
native-runtime: ${{ runner.os == 'Windows' && 'node' || 'none' }}
cache-pnpm-store: ${{ runner.os != 'Windows' }}
cache-pnpm-store-lookup-only: 'true'
# Design D7 upgrade and rollback: the last Bun orcad, built from a main commit that shipped
# it, beside this checkout's Node slot; the live-terminal hand-over skips once PROTOCOL_VERSION
# moves past the Bun daemon's. Its build uses this checkout's installed dependencies.
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
if: runner.os == 'Linux'
with:
bun-version: 1.4.2
- name: Build the last Bun orcad for the cross-runtime tests
id: bun-orcad
background: true
shell: bash
env:
BUN_ORCAD_COMMIT: f4092c06d639ee13ad446261dcabc78b27a21fbc
run: |
if [ "$RUNNER_OS" != Linux ]; then exit 0; fi
git fetch --no-tags --depth=1 origin "$BUN_ORCAD_COMMIT"
git worktree add --detach "$RUNNER_TEMP/bun-orcad-source" "$BUN_ORCAD_COMMIT"
ln -s "$GITHUB_WORKSPACE/node_modules" "$RUNNER_TEMP/bun-orcad-source/node_modules"
node "$RUNNER_TEMP/bun-orcad-source/config/scripts/build-orcad-bun.mjs" --out-dir "$RUNNER_TEMP/bun-orcad"
echo "slot=$RUNNER_TEMP/bun-orcad" >> "$GITHUB_OUTPUT"
echo "executable=$(command -v bun)" >> "$GITHUB_OUTPUT"
# Linux release slots come from the floor and Alpine lanes; this slot serves local tests.
- uses: ./.github/actions/prepare-orcad-prebuilds
id: orcad-prebuild
@@ -165,26 +193,11 @@ jobs:
(github.ref == 'refs/heads/main' && contains(fromJSON('["push","schedule","workflow_dispatch"]'), github.event_name))) }}
restore-windows-cache: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }}
- run: pnpm build:orcad
# Design D7 upgrade and rollback: the last Bun orcad, built from a main commit that shipped
# it, beside this checkout's Node slot; the live-terminal hand-over skips once PROTOCOL_VERSION
# moves past the Bun daemon's. Same lockfile, so its build reuses this checkout's node_modules.
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
if: runner.os == 'Linux'
with:
bun-version: 1.4.2
- name: Build the last Bun orcad for the cross-runtime tests
if: runner.os == 'Linux'
shell: bash
env:
BUN_ORCAD_COMMIT: f4092c06d639ee13ad446261dcabc78b27a21fbc
run: |
git fetch --no-tags --depth=1 origin "$BUN_ORCAD_COMMIT"
git worktree add --detach "$RUNNER_TEMP/bun-orcad-source" "$BUN_ORCAD_COMMIT"
ln -s "$GITHUB_WORKSPACE/node_modules" "$RUNNER_TEMP/bun-orcad-source/node_modules"
node "$RUNNER_TEMP/bun-orcad-source/config/scripts/build-orcad-bun.mjs" --out-dir "$RUNNER_TEMP/bun-orcad"
echo "ORCA_BUN_ORCAD_SLOT=$RUNNER_TEMP/bun-orcad" >> "$GITHUB_ENV"
echo "BUN_EXECUTABLE=$(command -v bun)" >> "$GITHUB_ENV"
- wait: bun-orcad
- run: pnpm test:node-server --artifact ${{ runner.os == 'Linux' && '--cross-runtime' || '' }}
env:
ORCA_BUN_ORCAD_SLOT: ${{ steps.bun-orcad.outputs.slot }}
BUN_EXECUTABLE: ${{ steps.bun-orcad.outputs.executable }}
# Only a Windows runner compiles it; arm64 cross-compiles here, as release-cut does for the relay.
# Before the Node 18 check below: the build script imports TypeScript, which Node 18 cannot load.
- name: Build the Windows process-table addons for the desktop template
+15 -3
View File
@@ -415,6 +415,8 @@ jobs:
- uses: ./.github/actions/prepare-git-compatibility
- name: Verify Git binary compatibility matrix
env:
ORCA_BACKGROUND_LAUNCH: '1'
run: |
specs=(
"alpine/git:edge-2.38.1|2.38.1"
@@ -429,9 +431,13 @@ jobs:
pids=()
(
ORCA_GIT_COMPAT_BINARY="$HOME/.cache/orca-git-compat/git-2.25.5/git" \
GIT_EXEC_PATH="$HOME/.cache/orca-git-compat/git-2.25.5" \
ORCA_GIT_COMPAT_VERSION="2.25.5" \
pnpm exec vitest run --config config/vitest.config.ts \
src/shared/git-binary-compatibility.test.ts
src/shared/git-binary-compatibility.test.ts \
src/main/git/worktree-safety-real-git.test.ts \
src/main/git/worktree-rebase-update-refs-real-git.test.ts \
src/relay/git-review-draft-binary-compatibility.test.ts
) &
pids+=("$!")
@@ -441,7 +447,10 @@ jobs:
version="${spec#*|}"
ORCA_GIT_COMPAT_IMAGE="$image" ORCA_GIT_COMPAT_VERSION="$version" \
pnpm exec vitest run --config config/vitest.config.ts \
src/shared/git-binary-compatibility.test.ts
src/shared/git-binary-compatibility.test.ts \
src/main/git/worktree-safety-real-git.test.ts \
src/main/git/worktree-rebase-update-refs-real-git.test.ts \
src/relay/git-review-draft-binary-compatibility.test.ts
) &
pids+=("$!")
done
@@ -702,7 +711,7 @@ jobs:
# after the last shard. Deliberately absent from verify's needs for the same reason.
unit_selection_evidence:
needs: [test]
if: ${{ !cancelled() && needs.test.result == 'success' }}
if: ${{ !cancelled() && (needs.test.result == 'success' || needs.test.result == 'failure') }}
uses: ./.github/workflows/unit-selection-evidence.yml
# Why a separate job: the test needs a real Chrome, and the sharded `test` matrix
@@ -1128,6 +1137,7 @@ jobs:
src/shared/child-process/windows-cmd-shim-resolution.test.ts
src/shared/child-process/windows-cmd-shim-resolution.win32.test.ts
src/main/agent-hooks/windows-hook-payload-delivery.test.ts
src/main/jcode/hook-gate-script.test.ts
src/main/agent-hooks/windows-direct-cmd-hook-command.test.ts
src/main/codex/windows-hook-command.test.ts
src/main/codex/windows-hook-upgrade.test.ts
@@ -1158,6 +1168,8 @@ jobs:
src/main/runtime/unreadable-secret-store-preservation.win32.test.ts
src/main/ipc/pty-codex-account-attribution.test.ts
src/main/ipc/pty-spawn-env-codex-resume-provenance.test.ts
src/main/ipc/preflight-provider-command-selection.test.ts
src/main/ipc/preflight-runnable-local-cli.test.ts
src/relay/windows-port-scan.win32.test.ts
src/main/ssh/ssh-relay-upload-stage-windows-identity.test.ts
src/main/ssh/remote-node-runtime-store-windows.test.ts
+37 -2
View File
@@ -68,25 +68,60 @@ jobs:
- name: Install native build tools and xvfb
run: sudo apt-get update && sudo apt-get install -y build-essential python3 xvfb zsh
- name: Select dependency preparation
id: install-mode
shell: bash
env:
RUNNER_KIND: ${{ runner.environment }}
JOB_CONTAINER: ${{ job.container.id }}
run: |
node <<'NODE'
const fs = require('node:fs')
const manifest = JSON.parse(fs.readFileSync('package.json', 'utf8'))
const actionPath = '.github/actions/install-node-dependencies/action.yml'
const action = fs.existsSync(actionPath) ? fs.readFileSync(actionPath, 'utf8') : ''
const inputs = action.split(/^inputs:[ \t]*\r?$/m)[1]?.split(/^\S/m)[0] ?? ''
const shared = process.env.RUNNER_KIND === 'github-hosted' && !process.env.JOB_CONTAINER &&
process.env.RUNNER_OS === 'Linux' && process.env.RUNNER_ARCH === 'X64' &&
manifest.engines?.node === '24' && typeof manifest.packageManager === 'string' &&
manifest.packageManager.split('+')[0] === 'pnpm@12.8.1' &&
manifest.scripts?.postinstall === 'node config/scripts/rebuild-native-deps.mjs' &&
/^ native-runtime:/m.test(inputs) && /^ cache-pnpm-store-lookup-only:/m.test(inputs) &&
fs.existsSync('.github/actions/prepare-native-runtime/action.yml') &&
fs.existsSync('config/scripts/ensure-native-runtime.mjs')
fs.appendFileSync(process.env.GITHUB_OUTPUT, `shared=${shared}\n`)
NODE
- name: Prepare current dependencies
if: steps.install-mode.outputs.shared == 'true'
uses: ./.github/actions/install-node-dependencies
with:
native-runtime: electron
cache-electron-package: 'true'
cache-pnpm-store-lookup-only: 'true'
- name: Setup pnpm
if: steps.install-mode.outputs.shared != 'true'
uses: pnpm/setup@v2
with:
install: false
- name: Setup Node.js
if: steps.install-mode.outputs.shared != 'true'
uses: actions/setup-node@v6
with:
node-version-file: package.json
cache: pnpm
# Why: this scheduled/manual workflow uses the same native install path as
# PR and E2E CI, which needs pnpm to bypass its bundled gyp_main.py.
# Historical refs can lack the shared action; retain their original install path.
- name: Use external node-gyp to avoid pnpm's bundled copy
if: steps.install-mode.outputs.shared != 'true'
run: |
npm install -g node-gyp@11.5.0
echo "npm_config_node_gyp=$(npm root -g)/node-gyp/bin/node-gyp.js" >> "$GITHUB_ENV"
- name: Install dependencies
if: steps.install-mode.outputs.shared != 'true'
run: pnpm install --frozen-lockfile
- name: Build Electron app for terminal perf
+1
View File
@@ -128,6 +128,7 @@ docs/**
!docs/reference/git-compatibility.md
!docs/reference/headless-linux-server.md
!docs/reference/ime-regression-checklist.md
!docs/reference/jcode-hook-events.md
!docs/reference/linux-glibc-compatibility.md
!docs/reference/macos-press-and-hold.md
!docs/reference/orcad-operations.md
+2 -2
View File
@@ -36,7 +36,7 @@
Monitor and steer your agents from your phone — get notified when an agent finishes and send follow-ups from anywhere.
[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK 0.0.52](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.52/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
</td>
<td width="50%">
@@ -235,7 +235,7 @@ yay -S stably-orca-bin
Pair with your desktop app to monitor and steer your agents from your phone.
- **iOS:** [Download on the App Store](https://apps.apple.com/us/app/orca-ide/id6766130217)
- **Android:** [Download APK 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [Install guide](https://www.onorca.dev/docs/android-apk)
- **Android:** [Download APK 0.0.52](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.52/app-release.apk) · [Install guide](https://www.onorca.dev/docs/android-apk)
---
@@ -0,0 +1,244 @@
import { createHash } from 'node:crypto'
import { afterEach, expect, it, vi } from 'vitest'
import { DurablePushStore } from './durable-push-store.js'
import { buildPushDelivery } from './push-delivery-message.js'
import type { PushDatabase } from './push-database.js'
import {
cleanupDurablePushFixtures,
fixture,
notification
} from './durable-push-store.test-fixture.js'
type QueryCall = { sql: string; params?: unknown[] }
afterEach(async () => {
vi.restoreAllMocks()
await cleanupDurablePushFixtures()
})
function traceDatabase(
database: PushDatabase,
calls: QueryCall[],
errors: unknown[]
): PushDatabase {
return {
dialect: database.dialect,
query: async (sql, params) => {
calls.push({ sql, params })
try {
return await database.query(sql, params)
} catch (error) {
errors.push(error)
throw error
}
},
transaction: (run) => database.transaction((tx) => run(traceDatabase(tx, calls, errors))),
lockQuotaScope: (key) => database.lockQuotaScope(key),
tryLockScope: (key) => database.tryLockScope(key),
tryLockSharedScope: (key) => database.tryLockSharedScope(key),
close: () => database.close()
}
}
function payloadHash(value: unknown): string {
return createHash('sha256').update(JSON.stringify(value)).digest('hex')
}
it('parses each leased row once with exact complete payload, serialized key order and SQL sequence', async () => {
const { db, store, clock } = await fixture()
const input = {
...notification(1),
body: 'Unicode: 🐋\ud800',
extra: { first: [null, false, 3], next: { z: 'last', a: 'first' } }
}
await store.accept('host', 'phone', input)
const [row] = await db.query('SELECT * FROM push_delivery_batches')
if (!row) {
throw new Error('Missing delivery row')
}
const payload = String(row.payload_json)
const calls: QueryCall[] = []
const errors: unknown[] = []
const owner = new DurablePushStore(traceDatabase(db, calls, errors), clock)
const parse = vi.spyOn(JSON, 'parse')
const delivery = await owner.claim()
expect(delivery).toEqual({
id: row.batch_id,
registrationId: 'phone',
hostFingerprint: 'host',
notification: input,
expiresAt: 1_300_000,
lease: expect.any(String),
attempts: 1
})
expect(JSON.stringify(delivery?.notification)).toBe(payload)
expect(payloadHash(delivery?.notification)).toBe(payloadHash(input))
if (!delivery) {
throw new Error('Missing delivery')
}
const published = buildPushDelivery(delivery)
const expected = buildPushDelivery({ ...delivery, notification: input })
expect(JSON.stringify(published)).toBe(JSON.stringify(expected))
expect(payloadHash(published)).toBe(payloadHash(expected))
expect(calls.map(({ sql }) => sql.replace(/\s+/g, ' ').trim())).toEqual([
`SELECT * FROM push_delivery_batches WHERE state = 'pending' AND lease_until <= ? AND expires_at > ? AND due_at <= ? AND due_at > ? AND NOT EXISTS (SELECT 1 FROM push_delivery_batches busy WHERE busy.registration_id = push_delivery_batches.registration_id AND busy.state = 'pending' AND busy.lease_until > 0 AND busy.lease_until > ?) ORDER BY due_at, created_at, batch_id LIMIT 1${db.dialect === 'postgres' ? ' FOR UPDATE SKIP LOCKED' : ''}`,
"SELECT (SELECT batch_id FROM push_delivery_batches WHERE registration_id = ? AND state = 'pending' AND expires_at > ? AND due_at > ? ORDER BY due_at, created_at, batch_id LIMIT 1) AS head, EXISTS (SELECT 1 FROM push_delivery_batches WHERE registration_id = ? AND state = 'pending' AND lease_until > 0 AND lease_until > ?) AS busy",
'SELECT notification_seq FROM push_dismissed_events WHERE host_fingerprint = ? AND notification_epoch = ? AND notification_id = ?',
'UPDATE push_delivery_batches SET lease_token = ?, lease_until = ?, attempts = attempts + 1 WHERE batch_id = ?'
])
expect(calls[1]?.params).toEqual(['phone', clock(), clock() - 300_000, 'phone', clock()])
expect(calls[2]?.params).toEqual(['host', 'epoch', 'notification-1'])
expect(calls[3]?.params).toEqual([delivery?.lease, clock() + 30_000, row.batch_id])
expect(errors).toEqual([])
expect(parse.mock.calls.filter(([value]) => value === payload)).toHaveLength(1)
})
it('keeps concurrent device claims separate and returns fresh payload objects', async () => {
const { store } = await fixture()
const input = notification(1)
await store.accept('host', 'phone-a', input)
await store.accept('host', 'phone-b', input)
const payload = JSON.stringify(input)
const parse = vi.spyOn(JSON, 'parse')
const claims = await Promise.all(Array.from({ length: 4 }, () => store.claim()))
const delivered = claims.filter((claim) => claim !== null)
expect(delivered).toHaveLength(2)
expect(delivered.map((claim) => claim.registrationId).sort()).toEqual(['phone-a', 'phone-b'])
expect(delivered.every((claim) => JSON.stringify(claim.notification) === payload)).toBe(true)
expect(delivered[0]?.notification).not.toBe(delivered[1]?.notification)
expect(parse.mock.calls.filter(([value]) => value === payload)).toHaveLength(2)
})
it('reads changed retry bytes and a later writer update without carrying a parsed result across calls', async () => {
const { db, store, advance } = await fixture()
await store.accept('host', 'phone', notification(1))
const first = await store.claim()
if (!first) {
throw new Error('Missing first delivery')
}
first.notification.body = 'provider changed this retry'
await store.finish(first, 1000)
advance(1000)
const retriedPayload = JSON.stringify(first.notification)
const parse = vi.spyOn(JSON, 'parse')
const retry = await store.claim()
expect(retry).toEqual({ ...first, lease: expect.any(String), attempts: 2 })
expect(retry?.lease).not.toBe(first.lease)
expect(retry?.notification).not.toBe(first.notification)
expect(JSON.stringify(retry?.notification)).toBe(retriedPayload)
expect(payloadHash(retry?.notification)).toBe(payloadHash(first.notification))
const retryParses = parse.mock.calls.filter(([value]) => value === retriedPayload).length
if (!retry) {
throw new Error('Missing retry delivery')
}
await store.finish(retry, 1000)
const changed = { ...notification(1), body: 'fresh database row', title: 'Changed' }
const changedPayload = JSON.stringify(changed)
await db.query('UPDATE push_delivery_batches SET payload_json = ? WHERE batch_id = ?', [
changedPayload,
first.id
])
advance(1000)
const fresh = await store.claim()
expect(fresh).toEqual({ ...retry, notification: changed, lease: expect.any(String), attempts: 3 })
expect(JSON.stringify(fresh?.notification)).toBe(changedPayload)
expect(payloadHash(fresh?.notification)).toBe(payloadHash(changed))
expect(retry.notification.body).toBe('provider changed this retry')
expect(retryParses).toBe(1)
expect(parse.mock.calls.filter(([value]) => value === changedPayload)).toHaveLength(1)
})
it('keeps dismissed alerts on the original single-parse delete path without leasing', async () => {
const { db, store, clock } = await fixture()
const input = notification(1)
await store.accept('host', 'phone', input)
await db.query(
'INSERT INTO push_dismissed_events(host_fingerprint, notification_epoch, notification_id, notification_seq, created_at) VALUES (?, ?, ?, ?, ?)',
['host', 'epoch', input.notificationId, 1, clock()]
)
const calls: QueryCall[] = []
const parse = vi.spyOn(JSON, 'parse')
expect(await new DurablePushStore(traceDatabase(db, calls, []), clock).claim()).toBeNull()
expect(await store.pendingCount('phone')).toBe(0)
expect(calls.at(-1)?.sql).toBe('DELETE FROM push_delivery_batches WHERE batch_id = ?')
expect(calls.some(({ sql }) => sql.startsWith('UPDATE'))).toBe(false)
expect(parse.mock.calls.filter(([value]) => value === JSON.stringify(input))).toHaveLength(1)
})
it('preserves the existing trust boundary for an object missing notification fields', async () => {
const { db, store } = await fixture()
await store.accept('host', 'phone', notification(1))
await db.query('UPDATE push_delivery_batches SET payload_json = ?', ['{}'])
const parse = vi.spyOn(JSON, 'parse')
const delivery = await store.claim()
expect(delivery?.notification).toEqual({})
expect(JSON.stringify(delivery?.notification)).toBe('{}')
expect(parse.mock.calls.filter(([value]) => value === '{}')).toHaveLength(1)
})
it.each(['not JSON', 'undefined', 'null', '[]'])(
'preserves invalid payload rejection and rolls back the lease for %s',
async (payload) => {
const { db, store } = await fixture()
await store.accept('host', 'phone', notification(1))
await db.query('UPDATE push_delivery_batches SET payload_json = ?', [payload])
const [before] = await db.query('SELECT * FROM push_delivery_batches')
const parse = vi.spyOn(JSON, 'parse')
let caught: unknown
try {
await store.claim()
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Error)
const index = parse.mock.calls.findIndex(([value]) => value === payload)
expect(index).toBeGreaterThanOrEqual(0)
if (payload === 'not JSON' || payload === 'undefined') {
expect(caught).toBe(parse.mock.results[index]?.value)
expect(caught).toBeInstanceOf(SyntaxError)
} else {
expect(caught).toMatchObject({ message: 'invalid_push_delivery_payload' })
}
expect(await db.query('SELECT * FROM push_delivery_batches')).toEqual([before])
expect(parse.mock.calls.filter(([value]) => value === payload)).toHaveLength(1)
}
)
it('preserves the exact database UPDATE error and retries with a fresh payload after rollback', async () => {
const { db, store, clock } = await fixture()
await store.accept('host', 'phone', notification(1))
const [before] = await db.query('SELECT * FROM push_delivery_batches')
if (!before) {
throw new Error('Missing delivery row')
}
const originalQuery = db.query.bind(db)
const errors: unknown[] = []
// SQLite raises a native error in the real transaction; PostgreSQL uses its real constraint.
await originalQuery(
db.dialect === 'sqlite'
? "CREATE TRIGGER deny_lease BEFORE UPDATE ON push_delivery_batches BEGIN SELECT RAISE(FAIL, 'deny_lease'); END"
: 'ALTER TABLE push_delivery_batches ADD CONSTRAINT deny_lease CHECK (lease_until = 0)'
)
const owner = new DurablePushStore(traceDatabase(db, [], errors), clock)
const parse = vi.spyOn(JSON, 'parse')
let caught: unknown
try {
await owner.claim()
} catch (error) {
caught = error
}
expect(errors).toHaveLength(1)
expect(caught).toBe(errors[0])
expect(await originalQuery('SELECT * FROM push_delivery_batches')).toEqual([before])
expect(parse.mock.calls.filter(([value]) => value === String(before.payload_json))).toHaveLength(
1
)
await originalQuery(
db.dialect === 'sqlite'
? 'DROP TRIGGER deny_lease'
: 'ALTER TABLE push_delivery_batches DROP CONSTRAINT deny_lease'
)
const fresh = await owner.claim()
expect(fresh?.notification).toEqual(notification(1))
expect(fresh?.attempts).toBe(1)
})
+3 -3
View File
@@ -153,15 +153,15 @@ export class DurablePushStore {
'UPDATE push_delivery_batches SET lease_token = ?, lease_until = ?, attempts = attempts + 1 WHERE batch_id = ?',
[lease, now + DELIVERY_LEASE_MS, row.batch_id]
)
return this.delivery(row, lease)
return this.delivery(row, lease, notification)
}
private delivery(row: SqlRow, lease: string): QueuedPushDelivery {
private delivery(row: SqlRow, lease: string, notification: PushNotification): QueuedPushDelivery {
return {
id: String(row.batch_id),
registrationId: String(row.registration_id),
hostFingerprint: String(row.host_fingerprint),
notification: parsePushDeliveryPayload(String(row.payload_json)),
notification,
expiresAt: Number(row.expires_at),
lease,
attempts: Number(row.attempts) + 1
@@ -0,0 +1,366 @@
import { createHash, createHmac } from 'node:crypto'
import { EventEmitter } from 'node:events'
import {
buildHostProofMacInput,
HostChallengeSchema,
HOST_CHALLENGE_PLAINTEXT_DOMAIN,
RELAY_CLOSE_CODE
} from '@orca-cloud/relay-contract'
import nacl from 'tweetnacl'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type WebSocket from 'ws'
import { RelayAssignmentStore } from './assignment-store.js'
import { loadRelayConfig } from './config.js'
import { RelayCredentialStore } from './credential-store.js'
import type { RelayDatabase } from './database.js'
import { HostSessionRegistry } from './host-session-registry.js'
import type { RelayTokenClaims } from './relay-token-verifier.js'
import { ProcessQueuedByteBudget } from './splice-forwarder.js'
class ProofSocket extends EventEmitter {
readonly OPEN = 1
readonly CLOSING = 2
readonly CLOSED = 3
readyState = this.OPEN
readonly send = vi.fn<(frame: string) => void>()
readonly close = vi.fn((code?: number, reason?: string) => {
this.readyState = this.CLOSED
this.emit('close', code, Buffer.from(reason ?? ''))
})
peerClose(): void {
this.readyState = this.CLOSED
this.emit('close', 1000, Buffer.alloc(0))
}
registrySocket(): WebSocket {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This fake implements the registry's send, state, close and EventEmitter surface; no actual networking is invoked.
return this as unknown as WebSocket
}
}
function fixture() {
const database: RelayDatabase = {
query: vi.fn(async () => []),
queryLocked: vi.fn(async () => []),
transaction: (operation) => operation(database),
close: async () => undefined
}
const config = loadRelayConfig({
ORCA_RELAY_PUBLIC_URL: 'http://127.0.0.1',
ORCA_RELAY_CELL_URL: 'http://127.0.0.1',
ORCA_RELAY_AUTH_ISSUER: 'https://auth.example.test',
ORCA_RELAY_JWKS_URL: 'https://auth.example.test/jwks',
ORCA_RELAY_ASSIGNMENT_SIGNING_KEY: 'synthetic-assignment-key-for-test-only',
ORCA_RELAY_ROLE: 'cell',
ORCA_RELAY_ADMIN_AUDIENCE: 'https://auth.example.test/admin',
ORCA_RELAY_DEPLOY_SERVICE_ACCOUNT: 'deploy@example.test',
ORCA_RELAY_CELL_CONNECTION_HARD_CAP: '600',
ORCA_RELAY_CELL_CONNECTION_UNOBSERVED_BOUND: '60'
})
const assignments = new RelayAssignmentStore(database)
const verify = vi.spyOn(assignments, 'verifyCellAssignment').mockResolvedValue(true)
const activate = vi.spyOn(assignments, 'activateControl').mockResolvedValue('control:1')
vi.spyOn(assignments, 'markMigrationTargetRegistered').mockResolvedValue(true)
const recordAuth = vi.fn()
const registry = new HostSessionRegistry(
config,
async () => null,
new RelayCredentialStore(database),
assignments,
new ProcessQueuedByteBudget(),
{
recordAuth,
recordForwardedBytes: vi.fn(),
recordHttp: vi.fn(),
recordReconnect: vi.fn(),
recordSql: vi.fn()
}
)
const keyPair = nacl.box.keyPair()
const identity = {
sub: 'user-proof',
prof: 'profile-proof',
relayHostId: createHash('sha256').update(keyPair.publicKey).digest('base64url').slice(0, 16),
purpose: 'host-control',
exp: Math.floor(Date.now() / 1000) + 3600
} satisfies RelayTokenClaims
const hello = JSON.stringify({
type: 'host-hello',
v: 1,
relayHostId: identity.relayHostId,
assignmentEpoch: 1,
hostPublicKeyB64: Buffer.from(keyPair.publicKey).toString('base64'),
appVersion: 'test'
})
return { registry, verify, activate, recordAuth, database, identity, keyPair, hello }
}
async function openProof(h: ReturnType<typeof fixture>, socket = new ProofSocket()) {
h.registry.acceptControl(socket.registrySocket(), h.identity)
socket.emit('message', Buffer.from(h.hello), false)
await vi.advanceTimersByTimeAsync(0)
expect(h.verify).toHaveBeenCalled()
expect(socket.send).toHaveBeenCalledOnce()
return socket
}
function answerProof(socket: ProofSocket, keyPair: nacl.BoxKeyPair): void {
const frame = socket.send.mock.calls[0]?.[0]
if (frame === undefined) {
throw new Error('missing challenge')
}
const parsed: unknown = JSON.parse(frame)
if (parsed === null || typeof parsed !== 'object' || !('type' in parsed)) {
throw new Error('invalid challenge frame')
}
const { type, ...fields } = parsed
expect(type).toBe('host-challenge')
const challenge = HostChallengeSchema.parse(fields)
const plaintext = nacl.box.open(
Buffer.from(challenge.ciphertextB64, 'base64'),
Buffer.from(challenge.nonceB64, 'base64'),
Buffer.from(challenge.relayEphemeralPublicKeyB64, 'base64'),
keyPair.secretKey
)
if (plaintext === null) {
throw new Error('challenge did not decrypt')
}
const domain = new TextEncoder().encode(`${HOST_CHALLENGE_PLAINTEXT_DOMAIN}\0`)
expect(plaintext.subarray(0, domain.length)).toEqual(domain)
const transcriptLength = new DataView(
plaintext.buffer,
plaintext.byteOffset + domain.length,
4
).getUint32(0, false)
const transcriptStart = domain.length + 4
const transcript = plaintext.subarray(transcriptStart, transcriptStart + transcriptLength)
const secret = plaintext.subarray(transcriptStart + transcriptLength)
const proofB64 = createHmac('sha256', secret)
.update(buildHostProofMacInput(transcript))
.digest('base64')
socket.emit(
'message',
Buffer.from(
JSON.stringify({ type: 'host-challenge-ack', challengeId: challenge.challengeId, proofB64 })
),
false
)
}
beforeEach(() => vi.useFakeTimers())
afterEach(() => {
vi.clearAllTimers()
vi.useRealTimers()
vi.restoreAllMocks()
})
describe('host control proof cleanup', () => {
it('allocates no hello stage for an already closed peer', () => {
const h = fixture()
const socket = new ProofSocket()
socket.peerClose()
h.registry.acceptControl(socket.registrySocket(), h.identity)
expect(vi.getTimerCount()).toBe(0)
expect(socket.listenerCount('message')).toBe(0)
expect(socket.listenerCount('close')).toBe(0)
expect(h.verify).not.toHaveBeenCalled()
})
it('releases the host hello timer and listeners when its peer closes early', () => {
const h = fixture()
const socket = new ProofSocket()
h.registry.acceptControl(socket.registrySocket(), h.identity)
expect(vi.getTimerCount()).toBe(1)
expect(socket.listenerCount('message')).toBe(1)
socket.peerClose()
expect({
timers: vi.getTimerCount(),
message: socket.listenerCount('message'),
close: socket.listenerCount('close')
}).toEqual({ timers: 0, message: 0, close: 0 })
vi.advanceTimersByTime(2000)
expect(socket.close).not.toHaveBeenCalled()
expect(h.verify).not.toHaveBeenCalled()
expect(h.database.query).not.toHaveBeenCalled()
})
it('preserves the exact hello deadline and refusal while releasing its message listener', () => {
const h = fixture()
const socket = new ProofSocket()
h.registry.acceptControl(socket.registrySocket(), h.identity)
vi.advanceTimersByTime(1999)
expect(socket.close).not.toHaveBeenCalled()
vi.advanceTimersByTime(1)
expect(socket.close).toHaveBeenCalledExactlyOnceWith(
RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL,
'host hello timeout'
)
expect(socket.listenerCount('message')).toBe(0)
expect(vi.getTimerCount()).toBe(0)
})
it('releases the challenge timer and listeners when its peer closes before proof', async () => {
const h = fixture()
const socket = await openProof(h)
expect(vi.getTimerCount()).toBe(1)
expect(socket.listenerCount('message')).toBe(1)
socket.peerClose()
expect({
timers: vi.getTimerCount(),
message: socket.listenerCount('message'),
close: socket.listenerCount('close')
}).toEqual({ timers: 0, message: 0, close: 0 })
await vi.advanceTimersByTimeAsync(10_000)
expect(socket.close).not.toHaveBeenCalled()
expect(h.activate).not.toHaveBeenCalled()
expect(h.database.query).not.toHaveBeenCalled()
})
it('preserves the exact proof deadline and refusal with no leftover listener', async () => {
const h = fixture()
const socket = await openProof(h)
await vi.advanceTimersByTimeAsync(9999)
expect(socket.close).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(1)
expect(socket.close).toHaveBeenCalledExactlyOnceWith(
RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL,
'host proof timeout'
)
expect(socket.listenerCount('message')).toBe(0)
expect(h.activate).not.toHaveBeenCalled()
expect(h.recordAuth).not.toHaveBeenCalled()
})
it('does no challenge crypto, send, timer or registration after a closed peer finishes verification', async () => {
const h = fixture()
let finish!: (valid: boolean) => void
h.verify.mockReturnValueOnce(
new Promise<boolean>((resolve) => {
finish = resolve
})
)
const generateKey = vi.spyOn(nacl.box, 'keyPair')
const socket = new ProofSocket()
h.registry.acceptControl(socket.registrySocket(), h.identity)
socket.emit('message', Buffer.from(h.hello), false)
expect(h.verify).toHaveBeenCalledOnce()
socket.peerClose()
finish(true)
await vi.advanceTimersByTimeAsync(0)
expect(socket.send).not.toHaveBeenCalled()
expect(generateKey).not.toHaveBeenCalled()
expect(vi.getTimerCount()).toBe(0)
expect(socket.listenerCount('message')).toBe(0)
expect(h.activate).not.toHaveBeenCalled()
expect(h.database.query).not.toHaveBeenCalled()
expect(
h.registry.get({ userId: h.identity.sub, relayHostId: h.identity.relayHostId })
).toBeNull()
})
it.each([false, true])('preserves invalid first-frame refusal (binary=%s)', (binary) => {
const h = fixture()
const socket = new ProofSocket()
h.registry.acceptControl(socket.registrySocket(), h.identity)
socket.emit('message', Buffer.from('{}'), binary)
expect(socket.close).toHaveBeenCalledExactlyOnceWith(
RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL,
binary ? 'host hello must be text' : 'invalid host hello'
)
expect(vi.getTimerCount()).toBe(0)
expect(socket.listenerCount('close')).toBe(0)
expect(h.activate).not.toHaveBeenCalled()
})
it.each([false, true])(
'preserves invalid proof authentication failure (binary=%s)',
async (binary) => {
const h = fixture()
const socket = await openProof(h)
socket.emit('message', Buffer.from('{}'), binary)
expect(socket.close).toHaveBeenCalledExactlyOnceWith(
RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL,
'invalid host proof'
)
expect(h.recordAuth).toHaveBeenCalledExactlyOnceWith(false)
expect(vi.getTimerCount()).toBe(0)
expect(socket.listenerCount('close')).toBe(0)
expect(h.activate).not.toHaveBeenCalled()
}
)
it('allocates no proof wait when sending the challenge closes its peer', async () => {
const h = fixture()
const socket = new ProofSocket()
socket.send.mockImplementation(() => socket.peerClose())
await openProof(h, socket)
expect(vi.getTimerCount()).toBe(0)
expect(socket.listenerCount('message')).toBe(0)
expect(socket.listenerCount('close')).toBe(0)
expect(h.activate).not.toHaveBeenCalled()
})
it('keeps the existing diagnostic and refusal when challenge send throws', async () => {
const h = fixture()
const socket = new ProofSocket()
socket.send.mockImplementation(() => {
throw new Error('synthetic send failure')
})
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
await openProof(h, socket)
expect(socket.close).toHaveBeenCalledExactlyOnceWith(
RELAY_CLOSE_CODE.LIMIT_EXCEEDED,
'relay temporarily unavailable'
)
expect(warn).toHaveBeenCalledExactlyOnceWith(
'[orca-relay] host hello proof failed: synthetic send failure'
)
expect(vi.getTimerCount()).toBe(0)
expect(socket.listenerCount('close')).toBe(0)
})
it('contains assignment lookup rejection with its existing close and diagnostic', async () => {
const h = fixture()
h.verify.mockRejectedValueOnce(new Error('synthetic lookup failure'))
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
const socket = new ProofSocket()
h.registry.acceptControl(socket.registrySocket(), h.identity)
socket.emit('message', Buffer.from(h.hello), false)
await vi.advanceTimersByTimeAsync(0)
expect(socket.close).toHaveBeenCalledExactlyOnceWith(
RELAY_CLOSE_CODE.LIMIT_EXCEEDED,
'relay temporarily unavailable'
)
expect(warn).toHaveBeenCalledExactlyOnceWith(
'[orca-relay] host hello proof failed: synthetic lookup failure'
)
expect(vi.getTimerCount()).toBe(0)
expect(socket.listenerCount('close')).toBe(0)
})
it('keeps a newer same-host peer live when the old proof peer closes', async () => {
const h = fixture()
const oldPeer = await openProof(h)
const replacement = await openProof(h)
oldPeer.peerClose()
expect(vi.getTimerCount()).toBe(1)
answerProof(replacement, h.keyPair)
await vi.advanceTimersByTimeAsync(0)
expect(h.activate).toHaveBeenCalledOnce()
expect(h.recordAuth).toHaveBeenCalledExactlyOnceWith(true)
expect(
h.registry.get({ userId: h.identity.sub, relayHostId: h.identity.relayHostId })?.socket
).toBe(replacement)
expect(replacement.send).toHaveBeenCalledTimes(2)
expect(replacement.listenerCount('message')).toBe(1)
expect(replacement.listenerCount('close')).toBe(2)
expect(vi.getTimerCount()).toBe(1)
await vi.advanceTimersByTimeAsync(10_000)
expect(oldPeer.close).not.toHaveBeenCalled()
expect(replacement.close).not.toHaveBeenCalled()
h.registry.drain(0)
await vi.advanceTimersByTimeAsync(0)
expect(vi.getTimerCount()).toBe(0)
})
})
+27 -11
View File
@@ -160,6 +160,30 @@ function send(socket: WebSocket, type: string, message: object): void {
socket.send(JSON.stringify({ type, ...message }))
}
function readControlFrame(
socket: WebSocket,
timeoutMs: number,
timeoutReason: string,
receive: (raw: RawData, isBinary: boolean) => void
): void {
if (socket.readyState !== socket.OPEN) return
const timer = setTimeout(() => {
finish()
socket.close(RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL, timeoutReason)
}, timeoutMs)
function finish(): void {
clearTimeout(timer)
socket.off('message', onMessage)
socket.off('close', finish)
}
function onMessage(raw: RawData, isBinary: boolean): void {
finish()
receive(raw, isBinary)
}
socket.once('message', onMessage)
socket.once('close', finish)
}
// Hosts abandon connects after 15s; waiting much longer than that behind a
// stalled predecessor only accumulates doomed sockets.
const ACTIVATION_QUEUE_WAIT_MS = 30_000
@@ -794,12 +818,7 @@ export class HostSessionRegistry {
socket.close(RELAY_CLOSE_CODE.DRAINING, 'relay draining')
return
}
let firstFrameTimer: ReturnType<typeof setTimeout> | null = setTimeout(() => {
socket.close(RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL, 'host hello timeout')
}, 2_000)
socket.once('message', (raw, isBinary) => {
if (firstFrameTimer) clearTimeout(firstFrameTimer)
firstFrameTimer = null
readControlFrame(socket, 2_000, 'host hello timeout', (raw, isBinary) => {
if (isBinary) {
socket.close(RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL, 'host hello must be text')
return
@@ -991,6 +1010,7 @@ export class HostSessionRegistry {
socket.close(RELAY_CLOSE_CODE.WRONG_CELL, 'wrong assignment epoch')
return
}
if (socket.readyState !== socket.OPEN) return
const key = this.key(identity.sub, identity.relayHostId)
const existing = this.sessions.get(key)
@@ -1035,11 +1055,7 @@ export class HostSessionRegistry {
ciphertextB64: Buffer.from(ciphertext).toString('base64'),
expiresAt
})
const proofTimer = setTimeout(() => {
socket.close(RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL, 'host proof timeout')
}, 10_000)
socket.once('message', (raw, isBinary) => {
clearTimeout(proofTimer)
readControlFrame(socket, 10_000, 'host proof timeout', (raw, isBinary) => {
const ack = isBinary
? null
: HostChallengeAckSchema.safeParse(payload(raw, 'host-challenge-ack'))
+6
View File
@@ -0,0 +1,6 @@
import { resolve } from 'node:path'
// UMD relative requires cannot survive a self-contained bundle.
export const JSONC_PARSER_ESM_ALIAS = {
'jsonc-parser': resolve(import.meta.dirname, '../../node_modules/jsonc-parser/lib/esm/main.js')
}
+23 -4
View File
@@ -19,6 +19,7 @@ type OutputChunk = Rollup.OutputChunk
// The CLI loads these paths after electron-vite replaces out/main.
export const CLI_MAIN_ENTRY_NAMES = [
'agent-hooks/managed-agent-hook-controls',
'gitlab/project-ref-parser',
'orca-profiles/profile-index-store',
'claude-accounts/keychain',
...[
@@ -56,7 +57,6 @@ const WORKER_THREAD_ENTRY_NAMES = [
'stt-worker',
'warp-theme-parser-worker',
'foreign-sqlite-reader-entry',
'session-scanner-worker-entry',
'main-thread-hang-watchdog-entry',
'port-scan-command-worker-entry',
'usage-scan-worker-entry',
@@ -122,10 +122,15 @@ function assertNoElectronRequire(
entryName: string,
entry: OutputChunk,
byFileName: Map<string, OutputChunk>,
electronFreeChunkCode: Map<OutputChunk, string>,
runtime: EntryRuntime = 'plain-Node process'
): void {
for (const chunk of collectReachableChunks(entry, byFileName)) {
if (ELECTRON_REQUIRE_RE.test(chunk.code)) {
const code = chunk.code
if (electronFreeChunkCode.get(chunk) === code) {
continue
}
if (ELECTRON_REQUIRE_RE.test(code)) {
throw new Error(
`[plain-node-entry-guard] "${entryName}" reaches chunk "${chunk.fileName}" that ` +
`requires electron. "${entryName}" runs as a ${runtime}, where ` +
@@ -133,6 +138,7 @@ function assertNoElectronRequire(
`v1.4.129-rc.1 daemon outage). Keep electron imports out of its module graph.`
)
}
electronFreeChunkCode.set(chunk, code)
}
}
@@ -272,17 +278,30 @@ export function createPlainNodeEntryGuardPlugin(
}
}
const electronFreeChunkCode = new Map<OutputChunk, string>()
for (const entryName of PLAIN_NODE_ENTRY_NAMES) {
const entry = entryByName.get(entryName)
if (entry) {
assertNoElectronRequire(entryName, entry, byFileName, 'plain-Node process')
assertNoElectronRequire(
entryName,
entry,
byFileName,
electronFreeChunkCode,
'plain-Node process'
)
}
}
for (const entryName of WORKER_THREAD_ENTRY_NAMES) {
const entry = entryByName.get(entryName)
if (entry) {
assertNoElectronRequire(entryName, entry, byFileName, 'worker thread')
assertNoElectronRequire(
entryName,
entry,
byFileName,
electronFreeChunkCode,
'worker thread'
)
}
}
+21 -10
View File
@@ -164,9 +164,10 @@ const rpmElectronRuntimeDependencies = [
]
// Why mirrored, not imported: this config is CJS loaded by electron-builder outside the TS build.
// Keep in sync with isMarkdownDocumentName() in src/main/ipc/markdown-documents.ts and with
// Keep in sync with isOsOpenedDocumentName() in src/main/startup/os-opened-documents.ts and with
// config/nsis/orca-installer-hooks.nsh, which registers the same set on Windows.
const MARKDOWN_FILE_EXTENSIONS = ['md', 'markdown', 'mdx']
const TABULAR_FILE_EXTENSIONS = ['csv', 'tsv']
// Why: the config must load on a host-only install without resolving unused Windows addons.
// This is load-time tolerance only; beforePack enforces that the target's natives are installed.
@@ -302,6 +303,7 @@ module.exports = {
'out/main/cursor/**',
'out/main/droid/**',
'out/main/gemini/**',
'out/main/gitlab/project-ref-parser.js',
'out/main/grok/**',
'out/main/hermes/**',
'out/main/orca-profiles/profile-index-store.js',
@@ -509,16 +511,25 @@ module.exports = {
include: resolve(__dirname, 'nsis', 'orca-installer-hooks.nsh')
},
mac: {
// Why rank Alternate: Orca joins Finder's "Open With" list for Markdown without claiming
// Why rank Alternate: Orca joins Finder's "Open With" list without claiming
// LSHandlerRank ownership, so whichever editor the user already prefers stays the default.
// Why one entry per extension: app-builder-lib globs `*.${ext}`, which an array would break.
fileAssociations: MARKDOWN_FILE_EXTENSIONS.map((ext) => ({
ext,
name: 'Markdown Document',
description: 'Markdown Document',
role: 'Editor',
rank: 'Alternate'
})),
fileAssociations: [
...MARKDOWN_FILE_EXTENSIONS.map((ext) => ({
ext,
name: 'Markdown Document',
description: 'Markdown Document',
role: 'Editor',
rank: 'Alternate'
})),
...TABULAR_FILE_EXTENSIONS.map((ext) => ({
ext,
name: `${ext.toUpperCase()} Document`,
description: `${ext.toUpperCase()} Document`,
role: 'Editor',
rank: 'Alternate'
}))
],
icon: 'resources/build/icon.icns',
entitlements: 'resources/build/entitlements.mac.plist',
entitlementsInherit: 'resources/build/entitlements.mac.plist',
@@ -611,7 +622,7 @@ module.exports = {
// override. A desktop entry's MimeType only adds a handler - mimeapps.list still owns the
// default. .mdx is deliberately absent: Ubuntu 24.04's mime database maps it to
// application/x-genesis-32x-rom, so claiming it here would need a glob override.
mimeTypes: ['text/markdown'],
mimeTypes: ['text/markdown', 'text/csv', 'text/tab-separated-values'],
// Why: Ubuntu desktop ships GNOME Orca as the `orca` package and /usr/bin/orca.
// The Linux installer should not claim those system package/file names.
executableName: 'orca-ide',
-1
View File
@@ -10,7 +10,6 @@
"src/main/speech/stt-worker.ts",
"src/main/warp-themes/warp-theme-parser-worker.ts",
"src/main/foreign-sqlite-readers/foreign-sqlite-reader-entry.ts",
"src/main/ai-vault/session-scanner-worker-entry.ts",
"src/main/ports/port-scan-command-worker-entry.ts",
"src/main/ipc/parcel-watcher-process-entry.ts",
"src/main/hang-watchdog/main-thread-hang-watchdog-entry.ts",
+27 -17
View File
@@ -6,7 +6,7 @@
!include "${__FILEDIR__}\orca-process-check.nsh"
; ---------------------------------------------------------------------------
; Markdown "Open with Orca" (issue #10138)
; Markdown and CSV/TSV "Open with Orca" (issues #10138, #23225)
;
; Why hand-rolled instead of electron-builder's `fileAssociations` on Windows:
; app-builder-lib emits !insertmacro APP_ASSOCIATE, whose first line is
@@ -21,29 +21,36 @@
; exactly where the user left it. Never add a `Software\Classes\.<ext>` default
; value here.
;
; MARKDOWN_PROGID must stay in sync with the extension list handled by
; isMarkdownDocumentName() in src/main/ipc/markdown-documents.ts.
; Keep the extension list in sync with isOsOpenedDocumentName().
; ---------------------------------------------------------------------------
!define MARKDOWN_PROGID "Orca.Markdown"
!define TABULAR_PROGID "Orca.Tabular"
!macro ORCA_REGISTER_MARKDOWN_OPEN_WITH EXT
WriteRegNone SHELL_CONTEXT "Software\Classes\${EXT}\OpenWithProgids" "${MARKDOWN_PROGID}"
!macro ORCA_REGISTER_DOCUMENT_OPEN_WITH EXT PROGID
WriteRegNone SHELL_CONTEXT "Software\Classes\${EXT}\OpenWithProgids" "${PROGID}"
WriteRegStr SHELL_CONTEXT "Software\Classes\Applications\${APP_EXECUTABLE_FILENAME}\SupportedTypes" "${EXT}" ""
!macroend
!macro ORCA_UNREGISTER_MARKDOWN_OPEN_WITH EXT
DeleteRegValue SHELL_CONTEXT "Software\Classes\${EXT}\OpenWithProgids" "${MARKDOWN_PROGID}"
!macro ORCA_UNREGISTER_DOCUMENT_OPEN_WITH EXT PROGID
DeleteRegValue SHELL_CONTEXT "Software\Classes\${EXT}\OpenWithProgids" "${PROGID}"
DeleteRegValue SHELL_CONTEXT "Software\Classes\Applications\${APP_EXECUTABLE_FILENAME}\SupportedTypes" "${EXT}"
!macroend
!macro ORCA_REGISTER_DOCUMENT_PROGID PROGID NAME
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROGID}" "" "${NAME}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROGID}\DefaultIcon" "" "$appExe,0"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROGID}\shell\open" "" "Open with ${PRODUCT_NAME}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROGID}\shell\open\command" "" '"$appExe" "%1"'
!macroend
!macro customInstall
WriteRegStr SHELL_CONTEXT "Software\Classes\${MARKDOWN_PROGID}" "" "Markdown Document"
WriteRegStr SHELL_CONTEXT "Software\Classes\${MARKDOWN_PROGID}\DefaultIcon" "" "$appExe,0"
WriteRegStr SHELL_CONTEXT "Software\Classes\${MARKDOWN_PROGID}\shell\open" "" "Open with ${PRODUCT_NAME}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${MARKDOWN_PROGID}\shell\open\command" "" '"$appExe" "%1"'
!insertmacro ORCA_REGISTER_MARKDOWN_OPEN_WITH ".md"
!insertmacro ORCA_REGISTER_MARKDOWN_OPEN_WITH ".markdown"
!insertmacro ORCA_REGISTER_MARKDOWN_OPEN_WITH ".mdx"
!insertmacro ORCA_REGISTER_DOCUMENT_PROGID "${MARKDOWN_PROGID}" "Markdown Document"
!insertmacro ORCA_REGISTER_DOCUMENT_PROGID "${TABULAR_PROGID}" "Tabular Document"
!insertmacro ORCA_REGISTER_DOCUMENT_OPEN_WITH ".md" "${MARKDOWN_PROGID}"
!insertmacro ORCA_REGISTER_DOCUMENT_OPEN_WITH ".markdown" "${MARKDOWN_PROGID}"
!insertmacro ORCA_REGISTER_DOCUMENT_OPEN_WITH ".mdx" "${MARKDOWN_PROGID}"
!insertmacro ORCA_REGISTER_DOCUMENT_OPEN_WITH ".csv" "${TABULAR_PROGID}"
!insertmacro ORCA_REGISTER_DOCUMENT_OPEN_WITH ".tsv" "${TABULAR_PROGID}"
; Why: Explorer caches the association list until told otherwise.
System::Call "shell32::SHChangeNotify(i,i,i,i) (0x08000000, 0x1000, 0, 0)"
!macroend
@@ -100,8 +107,11 @@
; Why outside the ${isUpdated} guard: customInstall rewrites these on every update, so
; dropping them during uninstallOldVersion is correct and keeps the pair symmetric.
DeleteRegKey SHELL_CONTEXT "Software\Classes\${MARKDOWN_PROGID}"
!insertmacro ORCA_UNREGISTER_MARKDOWN_OPEN_WITH ".md"
!insertmacro ORCA_UNREGISTER_MARKDOWN_OPEN_WITH ".markdown"
!insertmacro ORCA_UNREGISTER_MARKDOWN_OPEN_WITH ".mdx"
DeleteRegKey SHELL_CONTEXT "Software\Classes\${TABULAR_PROGID}"
!insertmacro ORCA_UNREGISTER_DOCUMENT_OPEN_WITH ".md" "${MARKDOWN_PROGID}"
!insertmacro ORCA_UNREGISTER_DOCUMENT_OPEN_WITH ".markdown" "${MARKDOWN_PROGID}"
!insertmacro ORCA_UNREGISTER_DOCUMENT_OPEN_WITH ".mdx" "${MARKDOWN_PROGID}"
!insertmacro ORCA_UNREGISTER_DOCUMENT_OPEN_WITH ".csv" "${TABULAR_PROGID}"
!insertmacro ORCA_UNREGISTER_DOCUMENT_OPEN_WITH ".tsv" "${TABULAR_PROGID}"
System::Call "shell32::SHChangeNotify(i,i,i,i) (0x08000000, 0x1000, 0, 0)"
!macroend
+4 -2
View File
@@ -25,8 +25,6 @@ const PACKAGED_RUNTIME_PACKAGE_ROOTS = [
'node-pty',
'posthog-node',
'proper-lockfile',
// serve-sim (for CLI JS entry + closure + state/middleware + to make packaged require('serve-sim') + its internal relatives work; mirrors other runtime JS like ws/yaml/zod. Natives/dylibs still via extraResources + the node_modules/serve-sim copy in resources from builder. Client if added too.
'serve-sim',
'qrcode',
'ssh2',
'tweetnacl',
@@ -34,6 +32,9 @@ const PACKAGED_RUNTIME_PACKAGE_ROOTS = [
'yaml',
'zod'
]
// Why macOS only: serve-sim drives the iOS Simulator, and its native addon is a Mach-O that
// Windows signing rejects as a PE file.
const DARWIN_PACKAGED_RUNTIME_PACKAGE_ROOTS = ['serve-sim']
const WINDOWS_PACKAGED_RUNTIME_PACKAGE_ROOTS = [
'@vscode/windows-process-tree',
'@orca/windows-registry'
@@ -180,6 +181,7 @@ function collectPackagedRuntimePackages(electronPlatformName = process.platform)
// Why: cross-builds must select native dependencies from the artifact target, not the build host.
const packageRoots = [
...PACKAGED_RUNTIME_PACKAGE_ROOTS,
...(electronPlatformName === 'darwin' ? DARWIN_PACKAGED_RUNTIME_PACKAGE_ROOTS : []),
...(electronPlatformName === 'win32' ? WINDOWS_PACKAGED_RUNTIME_PACKAGE_ROOTS : [])
]
for (const packageName of packageRoots) {
@@ -1,119 +0,0 @@
diff --git a/dev/lib/edit-map.js b/dev/lib/edit-map.js
index 983d0556e2713dc92982faa6a09b1093ef4d02db..818e702f5de8a1a8b1d35b25b5f6a0568989b152 100644
--- a/dev/lib/edit-map.js
+++ b/dev/lib/edit-map.js
@@ -34,6 +34,7 @@ export class EditMap {
* @type {Array<Change>}
*/
this.map = []
+ this.indexByOffset = new Map()
}
/**
@@ -124,6 +125,7 @@ export class EditMap {
// Truncate everything.
this.map.length = 0
+ this.indexByOffset.clear()
}
}
@@ -137,32 +139,16 @@ export class EditMap {
* @returns {undefined}
*/
function addImplementation(editMap, at, remove, add) {
- let index = 0
-
- /* c8 ignore next 3 -- `resolve` is never called without tables, so without edits. */
- if (remove === 0 && add.length === 0) {
+ if (remove === 0 && add.length === 0) return
+ const existing = editMap.indexByOffset.get(at)
+ if (existing) {
+ existing[1] += remove
+ existing[2].push(...add)
return
}
-
- while (index < editMap.map.length) {
- if (editMap.map[index][0] === at) {
- editMap.map[index][1] += remove
-
- // To do: before not used by tables, use when moving to micromark.
- // if (before) {
- // add.push(...editMap.map[index][2])
- // editMap.map[index][2] = add
- // } else {
- editMap.map[index][2].push(...add)
- // }
-
- return
- }
-
- index += 1
- }
-
- editMap.map.push([at, remove, add])
+ const change = [at, remove, add]
+ editMap.map.push(change)
+ editMap.indexByOffset.set(at, change)
}
// /**
diff --git a/lib/edit-map.js b/lib/edit-map.js
index ecc8bce784d29a48e0869be1c4a2dd5ccf1d3d01..8cb2282f2669f978f1ae60ab23f65f1e6523864a 100644
--- a/lib/edit-map.js
+++ b/lib/edit-map.js
@@ -34,6 +34,7 @@ export class EditMap {
* @type {Array<Change>}
*/
this.map = [];
+ this.indexByOffset = new Map();
}
/**
@@ -117,6 +118,7 @@ export class EditMap {
// Truncate everything.
this.map.length = 0;
+ this.indexByOffset.clear();
}
}
@@ -130,29 +132,16 @@ export class EditMap {
* @returns {undefined}
*/
function addImplementation(editMap, at, remove, add) {
- let index = 0;
-
- /* c8 ignore next 3 -- `resolve` is never called without tables, so without edits. */
- if (remove === 0 && add.length === 0) {
+ if (remove === 0 && add.length === 0) return;
+ const existing = editMap.indexByOffset.get(at);
+ if (existing) {
+ existing[1] += remove;
+ existing[2].push(...add);
return;
}
- while (index < editMap.map.length) {
- if (editMap.map[index][0] === at) {
- editMap.map[index][1] += remove;
-
- // To do: before not used by tables, use when moving to micromark.
- // if (before) {
- // add.push(...editMap.map[index][2])
- // editMap.map[index][2] = add
- // } else {
- editMap.map[index][2].push(...add);
- // }
-
- return;
- }
- index += 1;
- }
- editMap.map.push([at, remove, add]);
+ const change = [at, remove, add];
+ editMap.map.push(change);
+ editMap.indexByOffset.set(at, change);
}
// /**
+98 -14
View File
@@ -12107,10 +12107,10 @@
"https://github.com/stablyai/orca/pull/12778"
],
"invariant": "Typing, focus, terminal switch, workspace switch, visibility resume, resize, render, per-pane liveness, and tab-title synchronization must not call global pty:listSessions or aiVault.listSessions; they must use targeted APIs or cached provider-owned state.",
"oracle": "The current executable slice asserts targeted visibility/first-input liveness, resize re-assertion after visibility resume, light tab/active-state resume, SSH/remote skip behavior, and a closed Resource Manager budget of one readiness seed plus one coalesced inventory read only for unknown spawn IDs. AI Vault title sync deterministically accepts only resolveSessionTitles, batches at most 64 exact identities, serializes worker work, routes requests to the transcript-owning local/SSH/runtime host, and proves zero broad scans for unsupported hosts. The full hot-path oracle still needs instrumentation around raw focus, split focus, workspace switch, render ticks, and high-session PTY fixtures.",
"oracle": "The current executable slice asserts targeted visibility/first-input liveness, resize re-assertion after visibility resume, light tab/active-state resume, SSH/remote skip behavior, and a closed Resource Manager budget of one readiness seed plus one coalesced inventory read only for unknown spawn IDs. AI Vault title sync deterministically accepts only resolveSessionTitles, batches at most 64 exact identities, bounds scanner-service calls at sixteen, routes requests to the transcript-owning local/SSH/runtime host, and proves zero broad scans for unsupported hosts. The full hot-path oracle still needs instrumentation around raw focus, split focus, workspace switch, render ticks, and high-session PTY fixtures.",
"commands": [
"pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/pty-startup-barrier-and-listing.test.ts src/renderer/src/components/status-bar/use-resource-session-inventory.test.tsx src/renderer/src/components/status-bar/resource-session-inventory.test.ts src/renderer/src/components/status-bar/ResourceUsageStatusSegment.session-polling.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/lib/ai-vault-tab-title-sync.test.ts src/main/ai-vault/session-scanner-worker-client.test.ts src/main/ai-vault/session-title-file-reader.test.ts src/main/ai-vault/session-parse-cache-persistence.test.ts src/main/ipc/ai-vault.test.ts src/main/runtime/rpc/methods/ai-vault.test.ts src/relay/ai-vault-handler.test.ts"
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/lib/ai-vault-tab-title-sync.test.ts src/main/ai-vault/session-scanner-service-client.test.ts src/main/ai-vault/session-title-file-reader.test.ts src/main/ai-vault/session-parse-cache-persistence.test.ts src/main/ipc/ai-vault.test.ts src/main/runtime/rpc/methods/ai-vault.test.ts src/relay/ai-vault-handler.test.ts"
],
"testFiles": [
"src/main/ipc/pty-startup-barrier-and-listing.test.ts",
@@ -12118,7 +12118,7 @@
"src/renderer/src/components/status-bar/resource-session-inventory.test.ts",
"src/renderer/src/components/status-bar/ResourceUsageStatusSegment.session-polling.test.ts",
"src/renderer/src/lib/ai-vault-tab-title-sync.test.ts",
"src/main/ai-vault/session-scanner-worker-client.test.ts",
"src/main/ai-vault/session-scanner-service-client.test.ts",
"src/main/ai-vault/session-title-file-reader.test.ts",
"src/main/ai-vault/session-parse-cache-persistence.test.ts",
"src/main/ipc/ai-vault.test.ts",
@@ -12168,12 +12168,12 @@
]
},
{
"file": "src/main/ai-vault/session-scanner-worker-client.test.ts",
"file": "src/main/ai-vault/session-scanner-service-client.test.ts",
"assertions": [
"full scans and exact-title reads share one serial FIFO worker",
"active cancellation stays serialized and queued work remains bounded",
"worker faults restart queued work and idle time preserves incremental parse state",
"worker disposal rejects retained work and terminates the worker"
"the service waits for ready and runs the cache and interactive lanes independently",
"active and queued calls are bounded together at sixteen",
"cancellation reaches active work and kills a service that ignores it",
"service faults restart queued work under a restart circuit that a forced refresh reopens"
]
},
{
@@ -12202,13 +12202,13 @@
"summary": "4 files and 358 tests passed, covering readiness seed/recovery, zero interval polling, bounded unknown-spawn reconciliation, concurrent provider starts, exit fencing, cleanup, and out-of-order refresh fencing."
},
{
"date": "2026-08-09",
"date": "2026-10-02",
"runner": "local",
"platform": "macos",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/lib/ai-vault-tab-title-sync.test.ts src/main/ai-vault/session-scanner-worker-client.test.ts src/main/ai-vault/session-title-file-reader.test.ts src/main/ai-vault/session-parse-cache-persistence.test.ts src/main/ipc/ai-vault.test.ts src/main/runtime/rpc/methods/ai-vault.test.ts src/relay/ai-vault-handler.test.ts",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/lib/ai-vault-tab-title-sync.test.ts src/main/ai-vault/session-scanner-service-client.test.ts src/main/ai-vault/session-title-file-reader.test.ts src/main/ai-vault/session-parse-cache-persistence.test.ts src/main/ipc/ai-vault.test.ts src/main/runtime/rpc/methods/ai-vault.test.ts src/relay/ai-vault-handler.test.ts",
"result": "passed",
"durationSeconds": 3.1,
"summary": "The focused run passed 112 tests across 7 files, proving exact-title-only renderer requests, provider-isolated batching, persistent serial worker lifecycle and fault recovery, exact transcript identity, host routing, mixed-version degradation, and zero broad-scan fallback."
"durationSeconds": 5.3,
"summary": "The focused run passed 138 tests across 7 files, proving exact-title-only renderer requests, provider-isolated batching, per-lane scanner-service lifecycle and fault recovery, exact transcript identity, host routing, mixed-version degradation, and zero broad-scan fallback."
}
],
"runtimeBudget": {
@@ -12221,11 +12221,11 @@
},
"redGreenEvidence": {
"status": "partial",
"evidence": "Tests assert visibility resume prefers targeted hasPty over listSessions, first input after visibility resume calls targeted hasPty once, resize re-assertion after visibility resume uses getSize/resize without listSessions, light tab switches and visible active-state resume avoid listSessions/hasPty/getSize fanout, and the closed Resource Manager performs one readiness seed while known reattach signals and steady time perform no additional reads. For the #12778 regression, title sync no longer receives a listSessions dependency at all: it sends at most 64 exact identities per batch to one serial worker or transcript-owning remote host, and old hosts degrade without broad fallback. Needs broader raw focus/workspace-switch/render/high-session PTY count coverage before promotion."
"evidence": "Tests assert visibility resume prefers targeted hasPty over listSessions, first input after visibility resume calls targeted hasPty once, resize re-assertion after visibility resume uses getSize/resize without listSessions, light tab switches and visible active-state resume avoid listSessions/hasPty/getSize fanout, and the closed Resource Manager performs one readiness seed while known reattach signals and steady time perform no additional reads. For the #12778 regression, title sync no longer receives a listSessions dependency at all: it sends at most 64 exact identities per batch to the local scanner service or transcript-owning remote host, and old hosts degrade without broad fallback. Needs broader raw focus/workspace-switch/render/high-session PTY count coverage before promotion."
},
"performanceBudget": {
"required": true,
"evidence": "This gate is the performance budget for global session listing in hot paths. AI Vault title sync permits zero global scans, at most 64 exact identities per request, one active worker operation, 16 queued operations, four concurrent transcript parses inside the worker, a 4,096-title index, and no worktree-path-triggered refresh. The worker emits the aiVault.scan.worker span with duration and session count for full scans."
"evidence": "This gate is the performance budget for global session listing in hot paths. AI Vault title sync permits zero global scans, at most 64 exact identities per request, one active scanner-service call per lane (cache, interactive), 16 active plus queued calls, four concurrent transcript parses inside the service, a 4,096-title index, and no worktree-path-triggered refresh. The service emits the aiVault.scan.service span with duration and session count for full scans."
},
"promotionCriteria": [
"Add deterministic call-count instrumentation.",
@@ -21411,6 +21411,90 @@
"A tombstone that exhausts its retries stays on disk until the next startup, unchanged from before."
],
"demotionRule": "Keep experimental or demote if the reused listing strands a displaced root, crosses the admission cap, rearms an exhausted retry through another root, hands one tombstone to removal twice, or touches a recreated live history path."
},
{
"id": "terminal-performance.consumed-side-effect-retention",
"title": "Terminal side-effect queues release successfully applied and evicted effects",
"maturity": "experimental",
"protection": "partial",
"owner": "terminal-runtime",
"layer": "renderer-unit",
"surfaces": ["terminal output side effects", "renderer memory census"],
"platforms": ["macos", "linux", "windows"],
"providers": ["local", "daemon", "ssh", "remote-runtime"],
"coveredPlatforms": ["macos"],
"coveredProviders": ["local"],
"coverageNotes": "Provider-independent queue and mocked IPC output contracts run on macOS. Remote-runtime uses this processor but has no live session run. Native mobile uses another processor and is unaffected; host ownership, ACKs, wire, paths, folder/git identity, PTY lifecycle and output bytes are unchanged. Linux, Windows, WSL and live remote execution are gaps.",
"motivatingLinks": [
"https://github.com/stablyai/orca/blob/main/src/renderer/src/components/terminal-pane/pty-output-side-effect-queue.ts"
],
"invariant": "Release consumed title/payload objects after successful apply or overflow carry, preserving callback order, 64-effect drains, the 512-effect pending cap, bell and payload carry, empty-tail coalescing, reentrant clear/flush/enqueue, thrown-apply behavior and output delivery.",
"oracle": "Forced GC collects all 64 applied effects from a 100-effect bounded drain while the remaining 36 stay alive and deliver in order; it also collects the first evicted effect in a 513-effect burst while all 512 survivors remain alive. Clearing during apply immediately releases all 99 other pending effects, as the original queue did. Census reports 36 retained objects after the bounded drain. Explicit reentrant and error cases produce the same observations against the original queue.",
"commands": [
"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/renderer/src/components/terminal-pane/pty-output-side-effect-queue-retention.test.ts src/renderer/src/components/terminal-pane/pty-output-side-effect-queue-reentrancy.test.ts src/renderer/src/components/terminal-pane/pty-side-effect-pending-census.test.ts src/renderer/src/components/terminal-pane/pty-transport-output-side-effects.test.ts src/renderer/src/components/terminal-pane/pty-transport-eager-buffer-replay.test.ts"
],
"testFiles": [
"src/renderer/src/components/terminal-pane/pty-output-side-effect-queue-retention.test.ts",
"src/renderer/src/components/terminal-pane/pty-output-side-effect-queue-reentrancy.test.ts",
"src/renderer/src/components/terminal-pane/pty-side-effect-pending-census.test.ts",
"src/renderer/src/components/terminal-pane/pty-transport-output-side-effects.test.ts",
"src/renderer/src/components/terminal-pane/pty-transport-eager-buffer-replay.test.ts"
],
"assertionRefs": [
{
"file": "src/renderer/src/components/terminal-pane/pty-output-side-effect-queue-retention.test.ts",
"assertions": [
"releases applied effects while preserving every pending effect and its delivery order",
"releases an evicted effect before the compaction threshold",
"releases every pending effect immediately when clear is called during apply"
]
},
{
"file": "src/renderer/src/components/terminal-pane/pty-output-side-effect-queue-reentrancy.test.ts",
"assertions": [
"keeps empty-tail coalescing observable during the apply callback",
"delivers the same effect requeued after clear without releasing its new slot",
"preserves nested flush order and effects enqueued after the inner compaction",
"preserves thrown apply errors and their existing empty-tail coalescing"
]
}
],
"evidenceRuns": [
{
"date": "2026-10-01",
"runner": "local",
"platform": "macos",
"command": "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/renderer/src/components/terminal-pane/pty-output-side-effect-queue-retention.test.ts src/renderer/src/components/terminal-pane/pty-output-side-effect-queue-reentrancy.test.ts src/renderer/src/components/terminal-pane/pty-side-effect-pending-census.test.ts src/renderer/src/components/terminal-pane/pty-transport-output-side-effects.test.ts src/renderer/src/components/terminal-pane/pty-transport-eager-buffer-replay.test.ts",
"result": "passed",
"durationSeconds": 1.66,
"summary": "46 tests across five files passed. Four reentrant/error cases also pass against an isolated original-queue copy; stock and candidate both release 99 pending effects when clear runs during apply."
}
],
"runtimeBudget": {
"p95Seconds": 30,
"scope": "Focused renderer queue/output tests, including forced GC; p95 not established."
},
"flakeHistory": {
"status": "not-started",
"evidence": "Local author and independent review validation; no CI soak. Reference-lifetime tests require the test runner's existing --expose-gc."
},
"redGreenEvidence": {
"status": "complete",
"evidence": "Original queue fails both WeakRef collection assertions: all 64 applied effects and the evicted effect remain reachable. Clearing only consumed slots passes while all pending effects stay live. Baseline and candidate both pass all four reentrant/error observations."
},
"performanceBudget": {
"required": true,
"evidence": "Retained objects fall from 100 to 36 after one bounded drain and from 513 to 512 after one overflow eviction before compaction. Release is constant work per consumed effect; no changed drain limit, timer, polling, batching, cache, transport call or subprocess. Existing compaction cadence remains; clear truncates then replaces its backing array to preserve immediate release and protect reentrant same-object requeue."
},
"knownGaps": [
"No real renderer heap-byte or input-latency measurement; references and complete delivery are the deterministic oracle.",
"No live Linux, Windows, WSL, SSH or paired-runtime session run; these use provider-independent queue code.",
"Thrown apply callbacks keep their consumed reference until the original compaction boundary to preserve exception/coalescing behavior."
],
"promotionCriteria": [
"Collect CI soak with zero unexplained GC flakes and retain callback-order, overflow-carry and reentrancy assertions."
],
"demotionRule": "Keep experimental; investigate delivery, coalescing, error-path or pending-reference regressions without weakening the retention or fidelity oracle."
}
]
}
@@ -363,15 +363,12 @@ export function mobileWebAppBuildOptions(routes) {
*/
export function entryStaticClosure(metafile, entryOutputPath) {
const reached = new Set([entryOutputPath])
const queue = [entryOutputPath]
while (queue.length > 0) {
const current = queue.shift()
for (const current of reached) {
for (const imported of metafile.outputs[current]?.imports ?? []) {
if (imported.kind !== 'import-statement' || reached.has(imported.path)) {
continue
}
reached.add(imported.path)
queue.push(imported.path)
}
}
return reached
+17
View File
@@ -23,10 +23,12 @@ import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import process from 'node:process'
import { smokeProfileStateWorkers } from './profile-state-worker-smoke.mjs'
import { smokeForeignSqliteReaderWorker } from './foreign-sqlite-reader-worker-smoke.mjs'
import { materializeWatcherPackage } from './orcad-watcher-package.mjs'
import { stageOrcadWindowsProcessTree } from './orcad-windows-process-tree.mjs'
import {
ORCAD_EMOJI_SHORTCODE_DATASET,
ORCAD_FOREIGN_SQLITE_READER_ENTRY,
ORCAD_NODE_PTY_DIR,
ORCAD_NODE_PTY_JS_ARTIFACTS,
ORCAD_NODE_RUNTIME_MARKER_FILENAME,
@@ -56,6 +58,10 @@ const WATCHER_OUT_FILE = join(OUT_DIR, 'parcel-watcher-process-entry.js')
// orcad restart would SIGKILL every running terminal.
const DAEMON_ENTRY = join(ROOT, ORCAD_CHILD_ENTRY_POINTS.daemon)
const DAEMON_OUT_FILE = join(OUT_DIR, 'daemon-entry.js')
// Why beside orcad.js: the hook server's OpenCode binder and the OpenCode history scanner
// start this worker from the module dir, since orcad has no Electron resources tree.
const FOREIGN_SQLITE_READER_ENTRY = join(ROOT, ORCAD_CHILD_ENTRY_POINTS.foreignSqliteReader)
const FOREIGN_SQLITE_READER_OUT_FILE = join(OUT_DIR, ORCAD_FOREIGN_SQLITE_READER_ENTRY)
const OUT_FILE = join(OUT_DIR, 'orcad.js')
const BUILD_TARGET = process.env.ORCAD_BUILD_TARGET
if (!BUILD_TARGET) {
@@ -205,6 +211,7 @@ function buildForkedChild(entryPoint, outfile) {
const childResults = await Promise.all([
buildForkedChild(WATCHER_ENTRY, WATCHER_OUT_FILE),
buildForkedChild(DAEMON_ENTRY, DAEMON_OUT_FILE),
buildForkedChild(FOREIGN_SQLITE_READER_ENTRY, FOREIGN_SQLITE_READER_OUT_FILE),
...['writer', 'backup'].map((role) =>
buildForkedChild(
join(ROOT, ORCAD_CHILD_ENTRY_POINTS[role]),
@@ -340,6 +347,16 @@ try {
process.exitCode = 1
}
try {
smokeForeignSqliteReaderWorker(OUT_DIR)
if (nodeRuntimePath) {
smokeForeignSqliteReaderWorker(OUT_DIR, { runtimePath: nodeRuntimePath })
}
} catch (error) {
console.error('[build-orcad] foreign SQLite reader worker check failed:', error)
process.exitCode = 1
}
// Why a content hash and not ORCAD_VERSION alone: the remote install directory is keyed on
// this string, so two different builds carrying one version would share a directory — and an
// already-`.install-complete` dir is never re-uploaded. The deploy would silently run stale
+4 -2
View File
@@ -9,6 +9,7 @@
* gracefully degraded.
*/
import { build } from 'esbuild'
import { JSONC_PARSER_ESM_ALIAS } from '../build-plugins/jsonc-parser-esm.ts'
import { createHash } from 'node:crypto'
import {
copyFileSync,
@@ -60,7 +61,6 @@ const MANAGED_HOOK_RUNTIME_ENTRY = join(
'agent-hooks',
'managed-hook-runtime.ts'
)
const JSONC_PARSER_ESM_ENTRY = join(ROOT, 'node_modules', 'jsonc-parser', 'lib', 'esm', 'main.js')
const NODE_PTY_CONSOLE_LIST_PATCH_FILENAME = 'node-pty-1.1.0-console-list-agent-patch.cjs'
const NODE_PTY_CONSOLE_LIST_PATCH_SOURCE = join(
ROOT,
@@ -102,6 +102,7 @@ const RELAY_VERSION = '0.1.0'
async function buildRelayBundles(outDir) {
await build({
entryPoints: [RELAY_ENTRY],
alias: JSONC_PARSER_ESM_ALIAS,
bundle: true,
platform: 'node',
target: 'node18',
@@ -186,7 +187,7 @@ async function buildRelayBundles(outDir) {
outfile: join(outDir, 'managed-hook-runtime.js'),
// Why: jsonc-parser's default UMD build keeps relative dynamic requires
// that break after bundling; its ESM entry is equivalent and self-contained.
alias: { 'jsonc-parser': JSONC_PARSER_ESM_ENTRY },
alias: JSONC_PARSER_ESM_ALIAS,
sourcemap: false,
minify: true,
define: {
@@ -293,6 +294,7 @@ for (const platform of RELAY_BUILD_PLATFORMS) {
mkdirSync(outDir, { recursive: true })
await build({
entryPoints: [wslHookEntry],
alias: JSONC_PARSER_ESM_ALIAS,
bundle: true,
platform: 'node',
target: 'node18',
+55 -13
View File
@@ -13,6 +13,9 @@ const CASTING_DISABLE_PATTERN =
/\/[/*]\s*(?:oxlint|eslint)-disable(?:-next-line|-line)?\s[^\n]*typescript\/consistent-type-assertions/
const ANTI_SLOP_DISABLE_PATTERN =
/\/[/*]\s*(?:oxlint|eslint)-disable(?:-next-line|-line)?\s[^\n]*\banti-slop\//
const REACT_DOCTOR_DISABLE_PATTERN =
/^\s*\/[/*]\s*(?:oxlint|eslint)-disable(?:-next-line|-line)?\s+react-doctor\/[\w-]+(?:\s*,\s*react-doctor\/[\w-]+)*\s*(?:--(?:(?!\*\/).)*)?(?:\*\/)?\s*$/
const EXPLICIT_DISABLE_RULE_PATTERN = /(?:-disable(?:-next-line|-line)?\s+|^)[\w-]+(?:\/[\w-]+)?/
export const OXLINT_SCANS = [
{
// Why: no --config, so Oxlint keeps discovering nested configs. Pinning the root
@@ -41,7 +44,12 @@ export const OXLINT_SCANS = [
},
{
label: 'React Doctor',
args: ['--config', 'config/oxlint-react-doctor.json']
args: [
'--config',
'config/oxlint-react-doctor.json',
'--report-unused-disable-directives-severity',
'warn'
]
},
{
// Why changed-lines only: the renderer carries ~4.7k pre-existing restyle/raw-color
@@ -250,6 +258,16 @@ export function collectBaseLineBlocks(root, comparisonBase, files = null) {
}
export function isMovedCode(highlightedLines, baseBlocks) {
return createMovedCodeMatcher(baseBlocks)(highlightedLines)
}
export function createMovedCodeMatcher(baseBlocks) {
// Base-revision blocks stay fixed for the gate run; normalize each visited block once.
const normalizedBlocks = new Map()
return (highlightedLines) => matchMovedCode(highlightedLines, baseBlocks, normalizedBlocks)
}
function matchMovedCode(highlightedLines, baseBlocks, normalizedBlocks) {
const needle = highlightedLines.map(normalizeSourceLine).filter((line) => line !== '')
if (needle.length === 0) {
return false
@@ -262,8 +280,12 @@ export function isMovedCode(highlightedLines, baseBlocks) {
// and nearly all of it must be present. Genuinely new code shares neither the
// anchor nor the ordering, so it stays reported.
const MIN_COVERAGE = 0.9
return baseBlocks.some((rawHaystack) => {
const haystack = rawHaystack.map(normalizeSourceLine).filter((line) => line !== '')
return baseBlocks.some((block) => {
let haystack = normalizedBlocks.get(block)
if (!haystack) {
haystack = block.map(normalizeSourceLine).filter((line) => line !== '')
normalizedBlocks.set(block, haystack)
}
for (let start = 0; start < haystack.length; start += 1) {
if (haystack[start] !== needle[0]) {
continue
@@ -301,7 +323,8 @@ export function diagnosticTouchesAddedLines(
diagnostic,
rangesByFile,
root = process.cwd(),
baseBlocks = []
baseBlocks = [],
movedCodeMatcher = isMovedCode
) {
const file = normalizedDiagnosticPath(root, diagnostic.filename)
const ranges = rangesByFile.get(file)
@@ -313,7 +336,7 @@ export function diagnosticTouchesAddedLines(
if (lineRange === null || !overlapsAddedLines(lineRange.start, lineRange.end, ranges)) {
return false
}
return !isMovedCode(
return !movedCodeMatcher(
diagnosticHighlightedLines(root, diagnostic.filename, label.span),
baseBlocks
)
@@ -348,16 +371,34 @@ export function isCastingDirectiveUnusedWarning(diagnostic, root) {
)
}
// Why: the anti-slop rules live in a JS plugin that only config/oxlint-anti-slop.json loads, so
// the root scan never sees those rule names and reports every anti-slop suppression as unused.
// `audit:anti-slop` is the scan that enforces them.
export function isAntiSlopDirectiveUnusedWarning(diagnostic, root) {
// Unloaded plugin directives are checked by their owning scan.
export function isUnloadedPluginDirectiveUnusedWarning(diagnostic, root, scanLabel) {
if (!/^Unused (?:oxlint|eslint)-disable/.test(diagnostic.message ?? '')) {
return false
}
if (scanLabel === 'React Doctor') {
const labels = diagnostic.labels ?? []
return (
labels.length > 0 &&
labels.every(({ span }) => {
if (span.offset === undefined || span.length === undefined) {
return false
}
const file = path.isAbsolute(diagnostic.filename)
? diagnostic.filename
: path.join(root, diagnostic.filename)
// Oxlint spans use UTF-8 byte offsets, including before non-ASCII comments.
const directive = readFileSync(file)
.subarray(span.offset, span.offset + span.length)
.toString('utf8')
const rules = directive.split('--')[0]
return EXPLICIT_DISABLE_RULE_PATTERN.test(rules) && !/\breact-doctor\//.test(rules)
})
)
}
return (diagnostic.labels ?? []).some((label) =>
diagnosticHighlightedLines(root, diagnostic.filename, label.span).some((line) =>
ANTI_SLOP_DISABLE_PATTERN.test(line)
diagnosticHighlightedLines(root, diagnostic.filename, label.span).some(
(line) => ANTI_SLOP_DISABLE_PATTERN.test(line) || REACT_DOCTOR_DISABLE_PATTERN.test(line)
)
)
}
@@ -429,6 +470,7 @@ export function main(
}
const baseBlocks = collectBaseLineBlocks(root, comparisonBase)
const movedCodeMatcher = createMovedCodeMatcher(baseBlocks)
let failures = 0
for (const scan of OXLINT_SCANS) {
@@ -436,8 +478,8 @@ export function main(
(diagnostic) =>
!isSuppressedDiagnostic(diagnostic, root) &&
!isCastingDirectiveUnusedWarning(diagnostic, root) &&
!isAntiSlopDirectiveUnusedWarning(diagnostic, root) &&
diagnosticTouchesAddedLines(diagnostic, rangesByFile, root, baseBlocks)
!isUnloadedPluginDirectiveUnusedWarning(diagnostic, root, scan.label) &&
diagnosticTouchesAddedLines(diagnostic, rangesByFile, root, baseBlocks, movedCodeMatcher)
)
for (const diagnostic of diagnostics) {
printDiagnostic(diagnostic, root)
@@ -1,10 +1,12 @@
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import { describe, expect, it } from 'vitest'
import { runProcessSync } from '../../src/shared/child-process/run-process'
import { resolveOxlintInvocation } from './oxlint-cli-invocation.mjs'
import {
OXLINT_SCANS,
diagnosticTouchesAddedLines,
isAntiSlopDirectiveUnusedWarning,
isUnloadedPluginDirectiveUnusedWarning,
isMovedCode,
isRootCodeQualityPath,
overlapsAddedLines,
@@ -124,7 +126,7 @@ describe('moved-code exemption', () => {
})
})
describe('anti-slop directive unused warning', () => {
describe('unloaded plugin directive unused warning', () => {
const root = path.resolve(import.meta.dirname, '..', '..')
// Assembled so no line here is itself a directive the gate would scan.
const directive = (rule) => `/* oxlint-disable ${rule} -- reason */`
@@ -146,21 +148,149 @@ describe('anti-slop directive unused warning', () => {
it('exempts a suppression the root scan cannot resolve', () => {
withFixture(directive('anti-slop/no-module-mocking'), (diagnostic) => {
expect(isAntiSlopDirectiveUnusedWarning(diagnostic, root)).toBe(true)
expect(isUnloadedPluginDirectiveUnusedWarning(diagnostic, root, 'code quality')).toBe(true)
})
})
it('still reports an unused directive for a rule the root scan does load', () => {
withFixture(directive('unicorn/no-array-reduce'), (diagnostic) => {
expect(isAntiSlopDirectiveUnusedWarning(diagnostic, root)).toBe(false)
expect(isUnloadedPluginDirectiveUnusedWarning(diagnostic, root, 'code quality')).toBe(false)
})
})
it('ignores diagnostics that are not unused-directive warnings', () => {
withFixture(directive('anti-slop/no-module-mocking'), (diagnostic) => {
expect(
isAntiSlopDirectiveUnusedWarning({ ...diagnostic, message: 'Unexpected any.' }, root)
isUnloadedPluginDirectiveUnusedWarning(
{ ...diagnostic, message: 'Unexpected any.' },
root,
'code quality'
)
).toBe(false)
})
})
function scanFixture(label, file) {
const scan = OXLINT_SCANS.find((candidate) => candidate.label === label)
if (!scan) {
throw new Error(`Missing ${label} scan`)
}
const { command, prefixArgs } = resolveOxlintInvocation(root)
const result = runProcessSync({
program: command,
args: [...prefixArgs, ...scan.args, '--format', 'json', file],
cwd: root,
timeoutMs: 30_000,
maxOutputBytes: 4 * 1024 * 1024
})
return JSON.parse(result.stdout).diagnostics
}
it('accepts a used Doctor directive only through its loaded scan', () => {
const source = [
"import { useEffect, useState } from 'react'",
directive('react-doctor/no-derived-state-effect'),
'export function Title({ title }: { title: string }) {',
" const [value, setValue] = useState('')",
' useEffect(() => { setValue(title) }, [title])',
' return value',
'}'
].join('\n')
withFixture(source, ({ filename }) => {
const normal = scanFixture('code quality', filename)
const unused = normal.find((diagnostic) => diagnostic.message.startsWith('Unused '))
expect(unused).toBeDefined()
expect(isUnloadedPluginDirectiveUnusedWarning(unused, root, 'code quality')).toBe(true)
expect(scanFixture('React Doctor', filename)).toEqual([])
})
})
it('keeps an unused Doctor directive failing in its loaded scan', () => {
withFixture(directive('react-doctor/no-derived-state-effect'), ({ filename }) => {
const diagnostics = scanFixture('React Doctor', filename)
expect(diagnostics).toHaveLength(1)
expect(diagnostics[0].message).toMatch(/^Unused /)
expect(isUnloadedPluginDirectiveUnusedWarning(diagnostics[0], root, 'React Doctor')).toBe(
false
)
})
})
it('does not hide unused native rules in a mixed directive', () => {
withFixture(
directive('react-doctor/no-derived-state-effect, unicorn/no-array-reduce'),
(diagnostic) => {
expect(isUnloadedPluginDirectiveUnusedWarning(diagnostic, root, 'code quality')).toBe(false)
}
)
})
it('recognizes a standalone directive containing only Doctor rules', () => {
withFixture(
directive(
'react-doctor/no-derived-state-effect, react-doctor/no-adjust-state-on-prop-change'
),
(diagnostic) => {
expect(isUnloadedPluginDirectiveUnusedWarning(diagnostic, root, 'code quality')).toBe(true)
}
)
})
it('keeps adjacent native directive warnings visible', () => {
const doctor = directive('react-doctor/no-derived-state-effect')
const native = directive('unicorn/no-array-reduce')
for (const source of [`${doctor} ${native}`, `${native} ${doctor}`]) {
withFixture(source, ({ filename }) => {
const diagnostic = scanFixture('code quality', filename).find((candidate) =>
candidate.labels.some((label) => label.span.offset === source.indexOf(native))
)
expect(diagnostic).toBeDefined()
expect(diagnostic.message).toMatch(/^Unused /)
expect(isUnloadedPluginDirectiveUnusedWarning(diagnostic, root, 'code quality')).toBe(false)
})
}
})
it('leaves used native directives to the scan that loads them', () => {
withFixture(
[
'export const banner = "λ"',
directive('typescript/no-explicit-any'),
'export const answer: any = 42'
].join('\n'),
({ filename }) => {
expect(scanFixture('code quality', filename)).toEqual([])
const diagnostics = scanFixture('React Doctor', filename)
expect(diagnostics).toHaveLength(1)
expect(isUnloadedPluginDirectiveUnusedWarning(diagnostics[0], root, 'React Doctor')).toBe(
true
)
}
)
})
it('does not exempt unused Doctor rules together with unloaded native rules', () => {
withFixture(
directive('react-doctor/no-derived-state-effect, typescript/no-explicit-any'),
({ filename }) => {
const diagnostics = scanFixture('React Doctor', filename)
expect(diagnostics).toHaveLength(1)
expect(isUnloadedPluginDirectiveUnusedWarning(diagnostics[0], root, 'React Doctor')).toBe(
false
)
}
)
})
it('keeps blanket unused directives visible in the Doctor scan', () => {
for (const source of [directive(''), '// oxlint-disable-next-line -- reason']) {
withFixture(source, ({ filename }) => {
const diagnostics = scanFixture('React Doctor', filename)
expect(diagnostics).toHaveLength(1)
expect(isUnloadedPluginDirectiveUnusedWarning(diagnostics[0], root, 'React Doctor')).toBe(
false
)
})
}
})
})
@@ -5,6 +5,7 @@ import { describe, expect, it } from 'vitest'
const pr = parse(readFileSync('.github/workflows/pr.yml', 'utf8'))
const mobile = parse(readFileSync('.github/workflows/mobile.yml', 'utf8'))
const cloud = parse(readFileSync('.github/workflows/cloud-verify.yml', 'utf8'))
const headless = parse(readFileSync('.github/workflows/node-server-tests.yml', 'utf8'))
function assertJoinedBefore(steps, id, consumer) {
const start = steps.findIndex((step) => step.id === id)
@@ -25,7 +26,8 @@ describe('CI background step barriers', () => {
pr.jobs.package,
pr.jobs.shell_contracts,
mobile.jobs.verify,
cloud.jobs.security
cloud.jobs.security,
headless.jobs.persistence
]) {
const pending = new Set()
for (const step of job.steps) {
@@ -56,6 +58,35 @@ describe('CI background step barriers', () => {
)
})
it('joins the Linux Bun build before requiring both headless runtime artifacts', () => {
const steps = headless.jobs.persistence.steps
const consumer = (step) => step.run?.startsWith('pnpm test:node-server --artifact ')
assertJoinedBefore(steps, 'bun-orcad', consumer)
const start = steps.findIndex((step) => step.id === 'bun-orcad')
const join = steps.findIndex((step) => step.wait === 'bun-orcad')
const install = steps.findIndex((step) => step.uses?.endsWith('/install-node-dependencies'))
const setup = steps.findIndex((step) => step.uses?.startsWith('oven-sh/setup-bun@'))
expect(install).toBeGreaterThanOrEqual(0)
expect(setup).toBeGreaterThanOrEqual(0)
expect(install).toBeLessThan(setup)
expect(setup).toBeLessThan(start)
expect(steps[setup].if).toBe("runner.os == 'Linux'")
expect(steps[start].if).toBeUndefined()
expect(steps[start].run).toContain('if [ "$RUNNER_OS" != Linux ]; then exit 0; fi')
for (const build of [
steps.findIndex((step) => step.uses?.endsWith('/prepare-orcad-prebuilds')),
steps.findIndex((step) => step.run === 'pnpm build:orcad')
]) {
expect(build).toBeGreaterThan(start)
expect(build).toBeLessThan(join)
expect(steps[build].background).toBeUndefined()
}
const test = steps.find(consumer)
expect(test.run).toContain("${{ runner.os == 'Linux' && '--cross-runtime' || '' }}")
expect(test.env.ORCA_BUN_ORCAD_SLOT).toBe('${{ steps.bun-orcad.outputs.slot }}')
expect(test.env.BUN_EXECUTABLE).toBe('${{ steps.bun-orcad.outputs.executable }}')
})
it('finishes native import-cycle analysis before mobile installation changes resolution', () => {
const steps = pr.jobs.static_analysis.steps
assertJoinedBefore(steps, 'native-code-quality', (step) =>
@@ -66,13 +97,13 @@ describe('CI background step barriers', () => {
expect(steps.findIndex((step) => step.id === 'changed-code-quality')).toBeGreaterThan(install)
})
it('finishes both mobile typechecks before allocating test workers', () => {
it('serializes mobile pnpm entrypoints before allocating test workers', () => {
const steps = mobile.jobs.verify.steps
assertJoinedBefore(steps, 'production-types', (step) => step.name === 'Test')
const ratchet = steps.findIndex((step) => step.name === 'Typecheck tests (ratchet)')
const join = steps.findIndex((step) => step.wait === 'production-types')
expect(steps[ratchet].background).toBeUndefined()
expect(ratchet).toBeLessThan(join)
expect(ratchet).toBeGreaterThan(join)
})
it('waits for WebKit and the bundle before any browser tests', () => {
@@ -42,6 +42,7 @@ it('populates shared Electron archives on both Linux architectures without chang
)
expect(install.with['native-runtime']).toBe('node')
expect(install.with['cache-electron-package']).toBe('true')
expect(install.with['cache-pnpm-store-lookup-only']).toBe('true')
const populate = steps.find((step) => step.name === 'Populate shared Electron archive')
expect(populate.run).toBe('node config/scripts/install-electron-package-binary.mjs')
expect(steps.indexOf(populate)).toBeGreaterThan(steps.indexOf(install))
@@ -97,6 +98,9 @@ it('warms and probes both Windows images with the persistence job runtime', () =
const install = job.steps.find(
(step) => step.uses === './.github/actions/install-node-dependencies'
)
expect(install.with).toEqual({ 'native-runtime': 'node' })
expect(install.with).toEqual({
'native-runtime': 'node',
'cache-pnpm-store-lookup-only': 'true'
})
expect(job.steps.at(-1).run).toBe('node config/scripts/ensure-native-runtime.mjs --check-only')
})
@@ -12,7 +12,7 @@ describe('CI dependency download caches', () => {
expect(action.inputs['cache-dependency-path'].default).toBe('pnpm-lock.yaml')
for (const step of action.runs.steps.filter((step) => step.uses === 'actions/setup-node@v6')) {
expect(step.with.cache).toBe(
"${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && 'pnpm' || '' }}"
"${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && steps.pnpm-store-mode.outputs.lookup-only != 'true' && 'pnpm' || '' }}"
)
expect(step.with['cache-dependency-path']).toBe('${{ inputs.cache-dependency-path }}')
expect(step.with['package-manager-cache']).toBe(false)
@@ -32,15 +32,17 @@ describe('CI dependency download caches', () => {
])
})
it('restores PR stores except measured Windows and Linux installs, without a post-job save', () => {
it('restores PR stores except measured Windows, Linux and macOS installs, without a post-job save', () => {
const resolve = action.runs.steps.find((step) => step.id === 'pnpm-store')
const restore = action.runs.steps.find(
(step) => step.name === 'Restore pnpm download store without saving'
)
expect(restore.if).toBe(
"github.event_name == 'pull_request' && inputs.cache-pnpm-store != 'false' && !((runner.os == 'Linux' || runner.os == 'macOS') && (runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml') && (runner.os != 'Windows' || !(runner.arch == 'X64' && contains(inputs.cache-dependency-path, 'mobile/pnpm-lock.yaml')) && !((runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml'))"
)
expect(resolve.if).toBe(
"github.event_name == 'pull_request' && inputs.cache-pnpm-store != 'false' && !(runner.os == 'Linux' && (runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml') && (runner.os != 'Windows' || !(runner.arch == 'X64' && contains(inputs.cache-dependency-path, 'mobile/pnpm-lock.yaml')) && !((runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml'))"
`${restore.if} || (github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && steps.pnpm-store-mode.outputs.lookup-only == 'true')`
)
expect(restore.if).toBe(resolve.if)
expect(restore.uses).toBe('actions/cache/restore@v5')
expect(restore.with.path).toBe('${{ steps.pnpm-store.outputs.path }}')
expect(restore.with.key).toBe(
@@ -67,6 +69,25 @@ describe('CI dependency download caches', () => {
])
})
it('keeps producer lookup optional and compatible with the existing store archive', () => {
const lookup = action.runs.steps.find((step) => step.id === 'pnpm-store-lookup')
const restore = action.runs.steps.find((step) => step.id === 'pnpm-store-restore')
expect(action.inputs['cache-pnpm-store-lookup-only'].default).toBe('auto')
expect(lookup.uses).toBe('actions/cache@v5')
expect(lookup.if).toBe("steps.pnpm-store-mode.outputs.lookup-only == 'true'")
expect(lookup.with).toEqual({
path: '${{ env.ORCA_PNPM_STORE_CACHE_PATH }}',
key: restore.with.key,
'lookup-only': true
})
expect(action.runs.steps.indexOf(lookup)).toBeLessThan(
action.runs.steps.findIndex((step) => step.name === 'Install dependencies')
)
expect(action.outputs['pnpm-store-cache-hit'].value).toBe(
'${{ steps.pnpm-store-lookup.outputs.cache-hit || steps.pnpm-store-restore.outputs.cache-hit || steps.requested-node.outputs.cache-hit || steps.default-node.outputs.cache-hit }}'
)
})
it.each([
['Windows x64 mixed PR', 'pull_request', 'Windows', 'X64', true, false, ''],
['Windows ARM64 mixed PR', 'pull_request', 'Windows', 'ARM64', true, true, ''],
@@ -124,21 +145,75 @@ describe('CI dependency download caches', () => {
['Linux x64 custom PR', 'pull_request', 'Linux', 'X64', 'cloud/pnpm-lock.yaml', true, ''],
['Linux x64 root-only push', 'push', 'Linux', 'X64', false, false, 'pnpm'],
['Linux ARM64 root-only manual', 'workflow_dispatch', 'Linux', 'ARM64', false, false, 'pnpm'],
['macOS x64 root-only PR', 'pull_request', 'macOS', 'X64', false, true, ''],
['macOS ARM64 root-only PR', 'pull_request', 'macOS', 'ARM64', false, true, ''],
['macOS x64 root-only PR', 'pull_request', 'macOS', 'X64', false, false, ''],
['macOS ARM64 root-only PR', 'pull_request', 'macOS', 'ARM64', false, false, ''],
['macOS x86 root-only PR', 'pull_request', 'macOS', 'X86', false, true, ''],
['macOS x64 mixed PR', 'pull_request', 'macOS', 'X64', true, true, ''],
['macOS ARM64 mixed PR', 'pull_request', 'macOS', 'ARM64', true, true, ''],
['macOS ARM64 custom PR', 'pull_request', 'macOS', 'ARM64', 'cloud/pnpm-lock.yaml', true, ''],
['macOS ARM64 opted-out PR', 'pull_request', 'macOS', 'ARM64', true, false, '', 'false'],
['macOS x64 root-only push', 'push', 'macOS', 'X64', false, false, 'pnpm'],
['macOS ARM64 root-only manual', 'workflow_dispatch', 'macOS', 'ARM64', false, false, 'pnpm'],
['Linux x64 mixed PR', 'pull_request', 'Linux', 'X64', true, true, ''],
['Linux ARM64 mixed PR', 'pull_request', 'Linux', 'ARM64', true, true, ''],
['macOS ARM64 mixed PR', 'pull_request', 'macOS', 'ARM64', true, true, ''],
['Windows x64 mixed push', 'push', 'Windows', 'X64', true, false, 'pnpm'],
['Windows x64 mixed manual run', 'workflow_dispatch', 'Windows', 'X64', true, false, 'pnpm']
['Windows x64 mixed manual run', 'workflow_dispatch', 'Windows', 'X64', true, false, 'pnpm'],
['Windows x64 lookup producer', 'push', 'Windows', 'X64', false, false, '', 'true', 'true'],
[
'Windows ARM64 lookup producer',
'schedule',
'Windows',
'ARM64',
false,
false,
'',
'true',
'true'
],
['macOS ARM64 lookup producer', 'push', 'macOS', 'ARM64', false, false, '', 'true', 'true'],
[
'Linux x64 lookup producer',
'workflow_dispatch',
'Linux',
'X64',
false,
false,
'',
'true',
'true'
],
['Opted-out lookup producer', 'push', 'Windows', 'ARM64', false, false, '', 'false', 'true'],
[
'macOS root PR lookup flag',
'pull_request',
'macOS',
'ARM64',
false,
false,
'',
'true',
'true'
],
['macOS mixed PR lookup flag', 'pull_request', 'macOS', 'ARM64', true, true, '', 'true', 'true']
])(
'%s keeps its scoped store policy',
(_name, event, os, arch, mixed, restore, cache, storeCache = 'true') => {
(_name, event, os, arch, mixed, restore, cache, storeCache = 'true', lookupOnly = 'false') => {
const context = {
github: { event_name: event },
runner: { os, arch },
steps: {
'pnpm-store-mode': {
outputs: {
'lookup-only':
event !== 'pull_request' && storeCache !== 'false' && lookupOnly === 'true'
? 'true'
: ''
}
}
},
inputs: {
'cache-pnpm-store': storeCache,
'cache-pnpm-store-lookup-only': lookupOnly,
'cache-dependency-path':
typeof mixed === 'string'
? mixed
@@ -151,6 +226,14 @@ describe('CI dependency download caches', () => {
const evaluate = (expression) =>
runInNewContext(
expression
.replaceAll(
'steps.pnpm-store-mode.outputs.lookup-only',
'steps["pnpm-store-mode"].outputs["lookup-only"]'
)
.replaceAll(
'inputs.cache-pnpm-store-lookup-only',
'inputs["cache-pnpm-store-lookup-only"]'
)
.replaceAll('inputs.cache-dependency-path', 'inputs["cache-dependency-path"]')
.replaceAll('inputs.cache-pnpm-store', 'inputs["cache-pnpm-store"]'),
context
@@ -159,8 +242,17 @@ describe('CI dependency download caches', () => {
(step) =>
step.id === 'pnpm-store' || step.name === 'Restore pnpm download store without saving'
)) {
expect(evaluate(step.if)).toBe(restore)
expect(evaluate(step.if)).toBe(
restore ||
(step.id === 'pnpm-store' &&
event !== 'pull_request' &&
storeCache !== 'false' &&
lookupOnly === 'true')
)
}
expect(evaluate(action.runs.steps.find((step) => step.id === 'pnpm-store-lookup').if)).toBe(
event !== 'pull_request' && storeCache !== 'false' && lookupOnly === 'true'
)
for (const step of action.runs.steps.filter(
(step) => step.uses === 'actions/setup-node@v6'
)) {
@@ -185,6 +277,14 @@ describe('CI dependency download caches', () => {
expect(persistence.with['cache-pnpm-store']).toBe("${{ runner.os != 'Windows' }}")
expect(ssh.with['cache-pnpm-store']).toBe('false')
expect(warmer.with['cache-pnpm-store']).toBeUndefined()
expect(warmer.with['cache-pnpm-store-lookup-only']).toBe('true')
expect(persistence.with['cache-pnpm-store-lookup-only']).toBe('true')
for (const name of ['warm', 'warm-linux-arm']) {
const install = workflow('ci-cache-warmup').jobs[name].steps.find((step) =>
step.uses?.includes('install-node-dependencies')
)
expect(install.with['cache-pnpm-store-lookup-only']).toBe('true')
}
})
it('restores Windows packaging downloads from the release cache without a PR upload', () => {
+150
View File
@@ -0,0 +1,150 @@
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { runInNewContext } from 'node:vm'
import { parse } from 'yaml'
import { describe, expect, it } from 'vitest'
import { runProcessSync } from './script-child-process.mjs'
const action = parse(readFileSync('.github/actions/install-node-dependencies/action.yml', 'utf8'))
const mode = action.runs.steps.find((step) => step.id === 'pnpm-store-mode')
const defaultContext = {
github: { event_name: 'push' },
runner: { os: 'Linux', arch: 'X64', environment: 'github-hosted' },
job: { container: { id: '' } },
inputs: {
'cache-pnpm-store': 'true',
'cache-pnpm-store-lookup-only': 'auto',
'cache-dependency-path': 'pnpm-lock.yaml',
'node-version': ''
}
}
const expression = mode.if.replaceAll(/inputs\.([\w-]+)/g, 'inputs["$1"]')
function eligible(changes) {
const context = structuredClone(defaultContext)
for (const [name, fields] of Object.entries(changes)) {
Object.assign(context[name], fields)
}
return runInNewContext(expression, context)
}
function resolveMode(request, node, manager) {
const directory = mkdtempSync(join(tmpdir(), 'orca-store-mode-'))
const output = join(directory, 'output')
try {
writeFileSync(
join(directory, 'package.json'),
JSON.stringify({ engines: { node }, packageManager: manager })
)
const result = runProcessSync({
program: 'bash',
args: ['-e', '-o', 'pipefail', '-c', mode.run],
cwd: directory,
env: { ...process.env, LOOKUP_REQUEST: request, GITHUB_OUTPUT: output }
})
expect(result.code, result.stderr || result.stdout).toBe(0)
return readFileSync(output, 'utf8')
} finally {
rmSync(directory, { recursive: true, force: true })
}
}
describe('automatic pnpm store mode', () => {
it.each([
['auto', '24', 'pnpm@12.8.1', '', true],
['auto', '25', 'pnpm@12.8.1', 'pnpm', false],
['auto', '24', 'pnpm@13.0.0', 'pnpm', false],
['true', '25', 'pnpm@13.0.0', '', true]
])(
'routes resolved %s mode for Node %s / %s into both cache steps',
(request, node, manager, cache, lookup) => {
const context = structuredClone(defaultContext)
context.inputs['cache-pnpm-store-lookup-only'] = request
const resolved = resolveMode(request, node, manager).split('=')[1].trim()
const evaluate = (value) =>
runInNewContext(
value
.replaceAll(/inputs\.([\w-]+)/g, 'inputs["$1"]')
.replaceAll(
'steps.pnpm-store-mode.outputs.lookup-only',
'steps["pnpm-store-mode"].outputs["lookup-only"]'
),
{ ...context, steps: { 'pnpm-store-mode': { outputs: { 'lookup-only': resolved } } } }
)
const nodeSetup = action.runs.steps.find((step) => step.id === 'default-node')
expect(evaluate(nodeSetup.with.cache.slice(3, -2))).toBe(cache)
expect(evaluate(action.runs.steps.find((step) => step.id === 'pnpm-store-lookup').if)).toBe(
lookup
)
}
)
it.each(
['Linux', 'Windows', 'macOS'].flatMap((os) => ['X64', 'ARM64'].map((arch) => [os, arch]))
)('qualifies the measured %s/%s hosted root context', (os, arch) => {
expect(eligible({ runner: { os, arch } })).toBe(true)
})
it.each([
['PR', { github: { event_name: 'pull_request' } }],
['opted-out store', { inputs: { 'cache-pnpm-store': 'false' } }],
['opted-out lookup', { inputs: { 'cache-pnpm-store-lookup-only': 'false' } }],
['unknown request', { inputs: { 'cache-pnpm-store-lookup-only': 'other' } }],
[
'mixed lockfiles',
{ inputs: { 'cache-dependency-path': 'pnpm-lock.yaml\nmobile/pnpm-lock.yaml' } }
],
['custom lockfile', { inputs: { 'cache-dependency-path': 'cloud/pnpm-lock.yaml' } }],
['Node 25', { inputs: { 'node-version': '25' } }],
['job container', { job: { container: { id: 'container-id' } } }],
['self-hosted runner', { runner: { environment: 'self-hosted' } }],
['unknown host kind', { runner: { environment: '' } }],
['unmeasured architecture', { runner: { arch: 'X86' } }],
['unmeasured OS', { runner: { os: 'other' } }]
])('retains the legacy policy for %s', (_name, changes) => {
expect(eligible(changes)).toBe(false)
})
it('allows an explicit request to preserve the existing force-lookup contract', () => {
expect(
eligible({
inputs: {
'cache-pnpm-store-lookup-only': 'true',
'node-version': '25',
'cache-dependency-path': 'custom-lock.yaml'
},
runner: { environment: 'self-hosted' },
job: { container: { id: 'container-id' } }
})
).toBe(true)
expect(
eligible({
github: { event_name: 'pull_request' },
inputs: { 'cache-pnpm-store-lookup-only': 'true' }
})
).toBe(false)
})
it.each([
['24', 'pnpm@12.8.1', 'true'],
['24', 'pnpm@12.8.1+sha512.fixture', 'true'],
['25', 'pnpm@12.8.1', 'false'],
['24.x', 'pnpm@12.8.1', 'false'],
['24', 'pnpm@12.8.2', 'false'],
['24', 'pnpm@12.8.10', 'false'],
['24', undefined, 'false'],
[undefined, 'pnpm@12.8.1', 'false'],
['24', 12, 'false']
])('checks manifest Node %s and manager %s before choosing lookup', (node, manager, expected) => {
expect(resolveMode('auto', node, manager)).toBe(`lookup-only=${expected}\n`)
})
it('checks uppercase auto requests consistently with GitHub expression comparisons', () => {
expect(resolveMode('AUTO', '25', 'pnpm@12.8.1')).toBe('lookup-only=false\n')
})
it('does not constrain an explicit request to the automatic manifest profile', () => {
expect(resolveMode('true', '25', 'pnpm@13.0.0')).toBe('lookup-only=true\n')
})
})
-2
View File
@@ -626,7 +626,6 @@
"src/main/ai-vault/session-scanner-agent-root-overrides.test.ts": 1121,
"src/main/ai-vault/session-scanner-antigravity-parser.test.ts": 72,
"src/main/ai-vault/session-scanner-antigravity-source.test.ts": 330,
"src/main/ai-vault/session-scanner-background.test.ts": 354,
"src/main/ai-vault/session-scanner-claude-cwd-drift.test.ts": 245,
"src/main/ai-vault/session-scanner-claude-subagent-prune.test.ts": 170,
"src/main/ai-vault/session-scanner-claude-subagents.test.ts": 137,
@@ -706,7 +705,6 @@
"src/main/ai-vault/session-scanner-timeline.test.ts": 75,
"src/main/ai-vault/session-scanner-unlimited-dedup.test.ts": 3562,
"src/main/ai-vault/session-scanner-values.test.ts": 92,
"src/main/ai-vault/session-scanner-worker-client.test.ts": 48,
"src/main/ai-vault/session-scanner.test.ts": 343,
"src/main/ai-vault/session-sidecar-stat.test.ts": 25,
"src/main/ai-vault/session-title-file-reader-wsl-stall.test.ts": 173,
+8 -3
View File
@@ -42,9 +42,14 @@ export function buildUnitDependencyGraph(sources) {
if (path === null) {
continue
}
const resolved = EXTENSIONS.map((extension) => path + extension).find((candidate) =>
sources.has(candidate)
)
let resolved
for (const extension of EXTENSIONS) {
const candidate = path + extension
if (sources.has(candidate)) {
resolved = candidate
break
}
}
if (!resolved) {
opaque.add(file)
continue
@@ -0,0 +1,59 @@
import { describe, expect, it, vi } from 'vitest'
import { buildUnitDependencyGraph } from './ci-unit-dependency-graph.mjs'
describe('unit graph import resolution', () => {
it('avoids allocating an extension-candidate array for every resolved import', () => {
const consumers = Array.from({ length: 1000 }, (_, index) => `src/consumer-${index}.ts`)
const sources = new Map([
['src/leaf', 'export const value = 1'],
...consumers.map((file) => [file, "import './leaf'"])
])
const originalMap = Array.prototype.map
let extensionArrays = 0
const spy = vi.spyOn(Array.prototype, 'map').mockImplementation(function (...args) {
if (this.length === 10 && this[0] === '' && this[1] === '.ts' && this[9] === '/index.js') {
extensionArrays += 1
}
return originalMap.apply(this, args)
})
try {
const graph = buildUnitDependencyGraph(sources)
spy.mockRestore()
expect([...graph.reverse]).toEqual([['src/leaf', new Set(consumers)]])
expect(graph.opaque).toEqual(new Set())
expect(extensionArrays).toBe(0)
} finally {
spy.mockRestore()
}
})
it('keeps first-match precedence across literal paths, extensions and index files', () => {
const sources = new Map([
['src/leaf', ''],
['src/leaf.ts', ''],
['src/leaf.tsx', ''],
['src/component.tsx', ''],
['src/component.js', ''],
['src/folder/index.ts', ''],
['src/folder/index.tsx', ''],
['src/config.json', '{}'],
['src/renderer/src/view.tsx', ''],
['src/use.ts', "import './leaf'; import './component'; import './folder'; import './config'"],
['src/aliases.ts', "import '@renderer/view'; import '@/view'; import 'external-package'"],
['src/missing.ts', "import './missing-file'"],
['src/dynamic.ts', 'import(variablePath)'],
['config/owner.mjs', "import '../src/leaf'"],
['tests/owner.ts', "import '../src/leaf'"]
])
expect(buildUnitDependencyGraph(sources)).toEqual({
reverse: new Map([
['src/leaf', new Set(['src/use.ts', 'config/owner.mjs', 'tests/owner.ts'])],
['src/component.tsx', new Set(['src/use.ts'])],
['src/folder/index.ts', new Set(['src/use.ts'])],
['src/config.json', new Set(['src/use.ts'])],
['src/renderer/src/view.tsx', new Set(['src/aliases.ts'])]
]),
opaque: new Set(['src/missing.ts', 'src/dynamic.ts', 'config/owner.mjs', 'tests/owner.ts'])
})
})
})
+2 -1
View File
@@ -16,13 +16,14 @@ export default class TimingSequencer extends BaseSequencer {
'utf8'
)
)
const discovered = new Set(plan.files)
if (
plan.version !== 1 ||
!plan.sourceSha ||
plan.sourceSha !== process.env.ORCA_SHARD_SOURCE_SHA ||
JSON.stringify([...plan.files].sort()) !== JSON.stringify(specs.map(key).sort()) ||
!Array.isArray(plan.executionFiles) ||
plan.executionFiles.some((file) => !plan.files.includes(file))
plan.executionFiles.some((file) => !discovered.has(file))
) {
throw new Error('Selection provenance or discovery differs')
}
+76 -29
View File
@@ -6,39 +6,86 @@ import TimingSequencer from './ci-unit-sequencer.mjs'
let root
afterEach(() => {
vi.restoreAllMocks()
vi.unstubAllEnvs()
if (root) {
rmSync(root, { recursive: true, force: true })
}
})
it.each(['valid', 'stale', 'missing-file', 'missing-artifact'])(
'preserves complete shard coverage with %s planning evidence',
async (kind) => {
root = mkdtempSync(join(tmpdir(), 'unit-sequencer-'))
const files = ['src/a.test.ts', 'src/b.test.ts', 'src/c.test.ts', 'src/d.test.ts']
const plan = {
version: 1,
sourceSha: kind === 'stale' ? 'old' : 'current',
files: kind === 'missing-file' ? files.slice(1) : files,
executionFiles: files.slice(0, 2)
}
const planPath = join(root, 'selection.json')
if (kind !== 'missing-artifact') {
writeFileSync(planPath, JSON.stringify(plan))
}
vi.stubEnv('ORCA_UNIT_SELECTION_PLAN', planPath)
vi.stubEnv('ORCA_SHARD_SOURCE_SHA', 'current')
vi.stubEnv('ORCA_SHARD_MANIFEST', join(root, 'assignment.json'))
const assigned = []
for (const index of [1, 2]) {
const sequencer = new TimingSequencer({ config: { root, shard: { index, count: 2 } } })
const specs = files.map((file) => ({ moduleId: join(root, file) }))
assigned.push(...(await sequencer.shard(specs)).map((spec) => spec.moduleId))
}
expect(assigned.sort()).toEqual(
(kind === 'valid' ? files.slice(0, 2) : files).map((file) => join(root, file)).sort()
)
expect(new Set(assigned).size).toBe(assigned.length)
it.each([
'valid',
'stale',
'missing-file',
'missing-artifact',
'outside-selection',
'empty-selection'
])('preserves complete shard coverage with %s planning evidence', async (kind) => {
root = mkdtempSync(join(tmpdir(), 'unit-sequencer-'))
const files = ['src/a.test.ts', 'src/b.test.ts', 'src/c.test.ts', 'src/d.test.ts']
const plan = {
version: 1,
sourceSha: kind === 'stale' ? 'old' : 'current',
files: kind === 'missing-file' ? files.slice(1) : files,
executionFiles:
kind === 'outside-selection'
? ['src/unknown.test.ts']
: kind === 'empty-selection'
? []
: files.slice(0, 2)
}
)
const planPath = join(root, 'selection.json')
if (kind !== 'missing-artifact') {
writeFileSync(planPath, JSON.stringify(plan))
}
vi.stubEnv('ORCA_UNIT_SELECTION_PLAN', planPath)
vi.stubEnv('ORCA_SHARD_SOURCE_SHA', 'current')
vi.stubEnv('ORCA_SHARD_MANIFEST', join(root, 'assignment.json'))
const assigned = []
for (const index of [1, 2]) {
const sequencer = new TimingSequencer({ config: { root, shard: { index, count: 2 } } })
const specs = files.map((file) => ({ moduleId: join(root, file) }))
assigned.push(...(await sequencer.shard(specs)).map((spec) => spec.moduleId))
}
expect(assigned.sort()).toEqual(
(kind === 'valid' ? files.slice(0, 2) : files).map((file) => join(root, file)).sort()
)
expect(new Set(assigned).size).toBe(assigned.length)
})
it('validates a large selection without scanning the discovered array for each file', async () => {
root = mkdtempSync(join(tmpdir(), 'unit-sequencer-scale-'))
const files = Array.from({ length: 1600 }, (_, index) => `src/scale-${index}.test.ts`)
const executionFiles = files.slice(800)
const planPath = join(root, 'selection.json')
writeFileSync(
planPath,
JSON.stringify({ version: 1, sourceSha: 'current', files, executionFiles })
)
vi.stubEnv('ORCA_UNIT_SELECTION_PLAN', planPath)
vi.stubEnv('ORCA_SHARD_SOURCE_SHA', 'current')
vi.stubEnv('ORCA_SHARD_MANIFEST', join(root, 'assignment.json'))
const sequencer = new TimingSequencer({ config: { root, shard: { index: 1, count: 1 } } })
const specs = files.map((file) => ({ moduleId: join(root, file) }))
const includes = Array.prototype.includes
let discoveredArrayScans = 0
const scan = vi
.spyOn(Array.prototype, 'includes')
.mockImplementation(function (value, fromIndex) {
if (
this.length === files.length &&
this[0] === files[0] &&
typeof value === 'string' &&
value.startsWith('src/scale-')
) {
discoveredArrayScans += 1
}
return includes.call(this, value, fromIndex)
})
const selected = await sequencer.shard(specs)
scan.mockRestore()
expect(selected.map((spec) => spec.moduleId).sort()).toEqual(
executionFiles.map((file) => join(root, file)).sort()
)
expect(discoveredArrayScans).toBe(0)
})
@@ -242,6 +242,23 @@ describe('electron-builder config', () => {
])
})
// Why: serve-sim's addon is a Mach-O, and Windows signing rejects every *.node that is not PE.
it('keeps serve-sim out of the Windows and Linux runtime closures', () => {
const {
PACKAGED_RUNTIME_PACKAGE_ROOTS,
createPackagedRuntimeNodeModuleResources
} = require('../packaged-runtime-node-modules.cjs')
expect(PACKAGED_RUNTIME_PACKAGE_ROOTS).not.toContain('serve-sim')
const serveSimTarget = join('node_modules', 'serve-sim')
expect(createPackagedRuntimeNodeModuleResources('linux').map((r) => r.to)).not.toContain(
serveSimTarget
)
expect(electronBuilderConfig.linux.extraResources.map((r) => r.to)).not.toContain(
serveSimTarget
)
expect(electronBuilderConfig.win.extraResources.map((r) => r.to)).not.toContain(serveSimTarget)
})
// Why: the Windows CLI shim is delivered only via extraResources to
// resources/bin/orca.cmd (beside the native resources/bin/orca.exe). If the
// source tree is also packed into app.asar it gets extracted by
@@ -8,6 +8,8 @@ const require = createRequire(import.meta.url)
const electronBuilderConfig = require('../electron-builder.config.cjs')
const MARKDOWN_EXTENSIONS = ['md', 'markdown', 'mdx']
const TABULAR_EXTENSIONS = ['csv', 'tsv']
const DOCUMENT_EXTENSIONS = [...MARKDOWN_EXTENSIONS, ...TABULAR_EXTENSIONS]
// The exact shape app-builder-lib's APP_ASSOCIATE emits: a write to the DEFAULT ("")
// value of Software\Classes\.<ext>. Additive `WriteRegNone ...\OpenWithProgids` must not
@@ -23,23 +25,23 @@ const stripNsisCommentLines = (source) =>
const readInstallerHooks = () => readFile(electronBuilderConfig.nsis.include, 'utf8')
describe('electron-builder markdown file associations', () => {
describe('electron-builder document file associations', () => {
// Why: any top-level (or `win.`) fileAssociations entry makes app-builder-lib's NSIS
// packager emit `!insertmacro APP_ASSOCIATE`, whose first line writes that DEFAULT value
// — silently taking .md from whichever editor owns it, for every existing user on their
// next UPDATE, with APP_UNASSOCIATE never restoring it. `rank: 'Alternate'` cannot
// prevent this; it is LSHandlerRank and applies to macOS only. So the mac block must
// stay under `mac.` — hoisting it up "to share it with Windows" is what this test blocks.
it('never claims the Windows default markdown handler', () => {
it('never claims the Windows default document handler', () => {
expect(electronBuilderConfig.fileAssociations).toBeUndefined()
expect(electronBuilderConfig.win?.fileAssociations).toBeUndefined()
})
it('joins the macOS Open With list for every markdown extension without owning it', () => {
it('joins the macOS Open With list for every supported extension without owning it', () => {
const associations = electronBuilderConfig.mac.fileAssociations
// One entry per extension: an array `ext` would break the Linux packager's `*.${ext}` glob.
expect([...associations].map((association) => association.ext).sort()).toEqual(
[...MARKDOWN_EXTENSIONS].sort()
[...DOCUMENT_EXTENSIONS].sort()
)
for (const association of associations) {
expect(association).toMatchObject({ role: 'Editor', rank: 'Alternate' })
@@ -54,6 +56,14 @@ describe('electron-builder markdown file associations', () => {
expect(electronBuilderConfig.linux.fileAssociations).toBeUndefined()
})
it('adds CSV and TSV handlers to the Linux desktop entry', () => {
expect(electronBuilderConfig.linux.mimeTypes).toEqual([
'text/markdown',
'text/csv',
'text/tab-separated-values'
])
})
it('points the single NSIS include at the installer hooks file on disk', () => {
const includePath = electronBuilderConfig.nsis.include
expect(existsSync(includePath)).toBe(true)
@@ -84,7 +94,7 @@ describe('electron-builder markdown file associations', () => {
expect(stripped).toMatch(DEFAULT_HANDLER_WRITE)
})
it('registers Windows markdown Open With additively, never as the default', async () => {
it('registers Windows document Open With additively, never as the default', async () => {
const hooks = await readInstallerHooks()
expect(stripNsisCommentLines(hooks)).not.toMatch(DEFAULT_HANDLER_WRITE)
@@ -92,11 +102,18 @@ describe('electron-builder markdown file associations', () => {
expect(hooks).toMatch(
/WriteRegNone\s+SHELL_CONTEXT\s+"Software\\Classes\\\$\{EXT\}\\OpenWithProgids"/
)
expect(hooks).toMatch(/!macro\s+ORCA_REGISTER_MARKDOWN_OPEN_WITH\s+EXT/)
expect(hooks).toMatch(/!macro\s+ORCA_REGISTER_DOCUMENT_OPEN_WITH\s+EXT\s+PROGID/)
for (const ext of MARKDOWN_EXTENSIONS) {
expect(hooks).toContain(`ORCA_REGISTER_MARKDOWN_OPEN_WITH ".${ext}"`)
expect(hooks).toContain(`ORCA_UNREGISTER_MARKDOWN_OPEN_WITH ".${ext}"`)
expect(hooks).toContain(`ORCA_REGISTER_DOCUMENT_OPEN_WITH ".${ext}" "\${MARKDOWN_PROGID}"`)
expect(hooks).toContain(`ORCA_UNREGISTER_DOCUMENT_OPEN_WITH ".${ext}" "\${MARKDOWN_PROGID}"`)
}
for (const ext of TABULAR_EXTENSIONS) {
expect(hooks).toContain(`ORCA_REGISTER_DOCUMENT_OPEN_WITH ".${ext}" "\${TABULAR_PROGID}"`)
expect(hooks).toContain(`ORCA_UNREGISTER_DOCUMENT_OPEN_WITH ".${ext}" "\${TABULAR_PROGID}"`)
}
expect(hooks).toContain('!define TABULAR_PROGID "Orca.Tabular"')
expect(hooks).toContain('ORCA_REGISTER_DOCUMENT_PROGID "${TABULAR_PROGID}" "Tabular Document"')
expect(hooks).toContain('DeleteRegKey SHELL_CONTEXT "Software\\Classes\\${TABULAR_PROGID}"')
expect(hooks).toMatch(/!macro\s+customInstall\b/)
expect(hooks).toMatch(/!macro\s+customUnInstall\b/)
})
@@ -99,13 +99,14 @@ describe('Electron Vite output contract', () => {
expect(output.chunkFileNames).toBe('chunks/[name]-[hash].js')
})
it('keeps offline profile-state CLI imports unpacked at stable paths', () => {
it('keeps CLI main imports unpacked at stable paths', () => {
const input = electronViteConfig.main?.build?.rollupOptions?.input
if (!input || typeof input !== 'object' || Array.isArray(input)) {
throw new Error('Expected named main-process inputs')
}
for (const name of [
'gitlab/project-ref-parser',
'orca-profiles/profile-index-store',
'persistence/profile-state/profile-state-access',
'persistence/profile-state/profile-state-active-location',
@@ -121,6 +122,7 @@ describe('Electron Vite output contract', () => {
]) {
expect(input).toHaveProperty(name)
}
expect(electronBuilderConfig.asarUnpack).toContain('out/main/gitlab/project-ref-parser.js')
expect(electronBuilderConfig.asarUnpack).toContain('out/main/persistence/profile-state/**')
expect(electronBuilderConfig.asarUnpack).toContain(
'out/main/orca-profiles/profile-index-store.js'
@@ -144,8 +146,11 @@ describe('Electron Vite output contract', () => {
expect(external('@xterm/addon-serialize', undefined, false)).toBe(false)
expect(external('tldts', undefined, false)).toBe(false)
expect(external('zod', undefined, false)).toBe(false)
expect(external('smol-toml', undefined, false)).toBe(false)
expect(external('smol-toml/package.json', undefined, false)).toBe(false)
expect(electronViteConfig.main?.build?.externalizeDeps?.exclude).toContain('tldts')
expect(electronViteConfig.main?.build?.externalizeDeps?.exclude).toContain('zod')
expect(electronViteConfig.main?.build?.externalizeDeps?.exclude).toContain('smol-toml')
})
it('bundles validation dependencies used by the sandboxed preload', () => {
+6 -3
View File
@@ -9,10 +9,12 @@ import {
ensureWindowsProcessTreeCommandLinePatch,
inspectWindowsProcessTreeAddon,
nodeGypRebuildInvocation,
nodeGypRebuildTimeoutMs,
stageWindowsProcessTreeNodeAddonApiHeaders,
windowsProcessTreeAddonPath
} from './windows-process-tree-gyp-rebuild.mjs'
import { describeProcessFailure, runProcessSync } from './script-child-process.mjs'
import { disableMsbuildFileTrackingOnWindows } from './msbuild-file-tracking.mjs'
const require = createRequire(import.meta.url)
const { assertNodePtyJobOwnership, nodePtyAddonPath } = require('./node-pty-job-ownership.cjs')
@@ -404,6 +406,7 @@ function rebuildNodeRuntimeModules(moduleNames) {
console.warn(`[native-runtime] Rebuilding ${moduleName} with node-gyp.`)
// pnpm exec inside an installed addon cannot discover the root build tool.
runNodeGyp(
moduleName,
nodeGypRebuildInvocation(
process.arch,
moduleDir,
@@ -416,18 +419,18 @@ function rebuildNodeRuntimeModules(moduleNames) {
}
}
function runNodeGyp({ args, cwd }) {
function runNodeGyp(moduleName, { args, cwd }) {
const env =
process.platform === 'linux'
? { ...process.env, CXXFLAGS: `${process.env.CXXFLAGS ?? ''} -std=gnu++2a`.trim() }
: process.env
: disableMsbuildFileTrackingOnWindows({ ...process.env })
const result = runProcessSync({
program: process.execPath,
args,
cwd,
env,
stdio: 'inherit',
timeoutMs: 300_000
timeoutMs: nodeGypRebuildTimeoutMs(moduleName)
})
if (result.code !== 0) {
console.error(
+33 -7
View File
@@ -20,7 +20,10 @@ import { resolveCliCommand } from '../../src/shared/node-cli-command-resolution.
import { removeTreeSync } from '../../src/shared/windows-transient-lock-removal.ts'
import { resolvePnpmCliInvocation } from './pnpm-cli-invocation.mjs'
import { copyScriptWithLocalModules } from './script-module-dependencies.mjs'
import { nodeGypRebuildInvocation } from './windows-process-tree-gyp-rebuild.mjs'
import {
nodeGypRebuildInvocation,
nodeGypRebuildTimeoutMs
} from './windows-process-tree-gyp-rebuild.mjs'
const sourceScriptPath = fileURLToPath(new URL('./ensure-native-runtime.mjs', import.meta.url))
// The import walk sees `from './x.mjs'` only, so the createRequire'd CJS
@@ -60,6 +63,8 @@ describe('ensure-native-runtime', () => {
expect(result.stderr).toContain('node-gyp stderr complete\n')
const log = readFileSync(logPath, 'utf8')
expect(log).toContain(`node-gyp rebuild --arch=${process.arch}\n`)
expect(log).toContain(`node-gyp timeout=${nodeGypRebuildTimeoutMs('node-pty')}\n`)
expect(log).toContain(`trackFileAccess=${process.platform === 'win32' ? 'false' : ''}\n`)
expect(log).toContain(join('node_modules', 'node-pty'))
if (process.platform === 'linux') {
expect(log).toMatch(/^cxxflags=(?:.*\s)?-std=gnu\+\+2a$/m)
@@ -73,9 +78,14 @@ describe('ensure-native-runtime', () => {
}
})
it.skipIf(process.platform !== 'win32')(
'rebuilds other failed Windows addons with patched node-pty',
() => {
it.skipIf(process.platform !== 'win32').each([
{ trackingEnv: {}, tracking: 'false' },
{ trackingEnv: { TrackFileAccess: 'true' }, tracking: 'true' },
{ trackingEnv: { trackfileaccess: 'true' }, tracking: 'true' },
{ trackingEnv: { tRaCkFiLeAcCeSs: 'false' }, tracking: 'false' }
])(
'rebuilds other failed Windows addons with patched node-pty and tracking=$tracking',
({ trackingEnv, tracking }) => {
const projectDir = mkTempProject()
try {
@@ -90,6 +100,7 @@ describe('ensure-native-runtime', () => {
cwd: projectDir,
encoding: 'utf8',
env: envForNativeFixture(projectDir, {
...trackingEnv,
ORCA_NATIVE_TEST_LOG: logPath,
ORCA_NATIVE_TEST_MARKER: markerPath
})
@@ -100,6 +111,9 @@ describe('ensure-native-runtime', () => {
expect(
log.split('\n').filter((line) => line === `node-gyp rebuild --arch=${process.arch}`)
).toHaveLength(2)
expect(
log.split('\n').filter((line) => line === `trackFileAccess=${tracking}`)
).toHaveLength(2)
expect(log).toContain(join('node_modules', 'node-pty'))
expect(log).toContain(join('node_modules', '@orca', 'windows-registry'))
} finally {
@@ -265,7 +279,14 @@ function mkTempProject() {
copyScriptWithLocalModules(sourceScriptPath, join(projectDir, 'config', 'scripts'))
writeFileSync(
join(projectDir, 'config', 'scripts', 'script-child-process.mjs'),
`export { describeProcessFailure, runProcessSync } from ${JSON.stringify(new URL('./script-child-process.mjs', import.meta.url).href)}\n`
`import { appendFileSync } from 'node:fs'
import { describeProcessFailure, runProcessSync as run } from ${JSON.stringify(new URL('./script-child-process.mjs', import.meta.url).href)}
export { describeProcessFailure }
export function runProcessSync(options) {
appendFileSync(process.env.ORCA_NATIVE_TEST_LOG, \`node-gyp timeout=\${options.timeoutMs}\\n\`)
return run(options)
}
`
)
for (const name of REQUIRED_CJS_SIBLINGS) {
copyFileSync(
@@ -277,8 +298,12 @@ function mkTempProject() {
}
function envForNativeFixture(projectDir, extraEnv) {
// An inherited tracking preference would mask the Windows default under test.
const inherited = Object.fromEntries(
Object.entries(process.env).filter(([key]) => key.toLowerCase() !== 'trackfileaccess')
)
return {
...process.env,
...inherited,
...extraEnv,
npm_config_node_gyp: join(projectDir, 'node_modules', 'node-gyp', 'bin', 'node-gyp.js')
}
@@ -323,7 +348,7 @@ exports.loadNativeModule = function loadNativeModule(nativeName) {
writeFakeWindowsRegistry(projectDir, { requiresMarker: windowsRegistryRequiresMarker })
if (process.platform === 'win32') {
const buildDir = join(nodePtyDir, 'build', 'Release')
mkdirSync(buildDir, { recursive: true })
writePatchedNodePtyBuildArtifacts(projectDir)
writeFileSync(join(buildDir, 'conpty.node'), Buffer.from('msys-2.0.dll', 'utf16le'))
}
}
@@ -419,6 +444,7 @@ const { appendFileSync, writeFileSync, writeSync } = require('node:fs')
appendFileSync(process.env.ORCA_NATIVE_TEST_LOG, \`node-gyp \${process.argv.slice(2).join(' ')}\\n\`)
appendFileSync(process.env.ORCA_NATIVE_TEST_LOG, \`cwd=\${process.cwd()}\\n\`)
appendFileSync(process.env.ORCA_NATIVE_TEST_LOG, \`cxxflags=\${process.env.CXXFLAGS || ''}\\n\`)
appendFileSync(process.env.ORCA_NATIVE_TEST_LOG, \`trackFileAccess=\${process.env.TrackFileAccess ?? ''}\\n\`)
if (process.env.ORCA_NATIVE_TEST_VERBOSE_OUTPUT_BYTES) {
const output = Buffer.alloc(Number(process.env.ORCA_NATIVE_TEST_VERBOSE_OUTPUT_BYTES), 'x')
for (let offset = 0; offset < output.length;) {
@@ -0,0 +1,80 @@
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { describeProcessFailure, runProcessSync } from './script-child-process.mjs'
import { ORCAD_FOREIGN_SQLITE_READER_ENTRY } from '../../src/shared/orcad-artifacts.ts'
// Why a child process: the read must run under the runtime orcad ships, which may not be
// the Node running the build. The verdict is the exit code, never matched output.
const PROBE = `
const { Worker } = require('node:worker_threads')
const { DatabaseSync } = process.getBuiltinModule('node:sqlite')
const [entry, dbPath, missingPath] = process.argv.slice(2)
const db = new DatabaseSync(dbPath)
db.exec('CREATE TABLE session (id TEXT PRIMARY KEY, directory TEXT NOT NULL, time_created INTEGER NOT NULL, parent_id TEXT)')
db.prepare('INSERT INTO session VALUES (?, ?, ?, ?)').run('ses_smoke', '/smoke', 100, null)
db.close()
const steps = [
{
request: { id: 1, kind: 'openCodeBinderSessions', dbPath, cursor: { ms: 0, id: '' } },
expected: [{ id: 'ses_smoke', directory: '/smoke', createdAtMs: 100, parentId: null }]
},
{ request: { id: 2, kind: 'cursorProfile', dbPath: missingPath }, expected: { status: 'missing' } },
// The OpenCode history scanner's kinds share this entry.
{ request: { id: 3, kind: 'list', dbPaths: [], limit: null }, expected: { candidates: [], issues: [] } }
]
const worker = new Worker(entry, { execArgv: [] })
const fail = (code, message) => {
console.error(message)
process.exit(code)
}
setTimeout(() => fail(3, 'foreign SQLite reader worker did not answer'), 20000).unref()
worker.on('error', (error) => fail(4, String(error && error.stack || error)))
worker.on('exit', (code) => fail(5, 'foreign SQLite reader worker exited with ' + code))
let step = 0
worker.on('message', (response) => {
const { request, expected } = steps[step]
if (!response || response.id !== request.id || response.ok !== true ||
JSON.stringify(response.value) !== JSON.stringify(expected)) {
fail(6, request.kind + ' answered ' + JSON.stringify(response))
}
step += 1
if (step === steps.length) {
process.exit(0)
}
worker.postMessage(steps[step].request)
})
worker.postMessage(steps[0].request)
`
/**
* Load the built foreign SQLite reader entry and run real reads through it.
* @param outDir - orcad output directory holding the entry.
* @param options.runtimePath - Node to run under; the build's own Node when omitted.
*/
export function smokeForeignSqliteReaderWorker(outDir, { runtimePath, timeoutMs = 30_000 } = {}) {
const directory = mkdtempSync(join(tmpdir(), 'orca-foreign-sqlite-smoke-'))
try {
const probe = join(directory, 'probe.cjs')
writeFileSync(probe, PROBE)
const result = runProcessSync({
program: runtimePath ?? process.execPath,
args: [
probe,
resolve(outDir, ORCAD_FOREIGN_SQLITE_READER_ENTRY),
join(directory, 'opencode.db'),
join(directory, 'missing.vscdb')
],
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' },
timeoutMs,
maxOutputBytes: 64 * 1024
})
if (result.code !== 0 || result.timedOut) {
throw new Error(
`Foreign SQLite reader worker smoke failed: ${describeProcessFailure(result)}`
)
}
} finally {
rmSync(directory, { recursive: true, force: true })
}
}
@@ -0,0 +1,65 @@
import { build } from 'esbuild'
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { smokeForeignSqliteReaderWorker } from './foreign-sqlite-reader-worker-smoke.mjs'
import { ORCAD_CHILD_ENTRY_POINTS } from './orcad-entry-build.mjs'
const ENTRY = 'foreign-sqlite-reader-entry.js'
const directories = []
let builtDirectory
function fixtureDirectory() {
const directory = mkdtempSync(join(tmpdir(), 'orca-foreign-sqlite-smoke-test-'))
directories.push(directory)
return directory
}
beforeAll(async () => {
builtDirectory = fixtureDirectory()
await build({
entryPoints: [resolve(ORCAD_CHILD_ENTRY_POINTS.foreignSqliteReader)],
outfile: join(builtDirectory, ENTRY),
bundle: true,
platform: 'node',
target: 'node18',
format: 'cjs',
external: ['electron'],
logLevel: 'silent'
})
}, 60_000)
afterAll(() => {
for (const directory of directories) {
rmSync(directory, { recursive: true, force: true })
}
})
describe('foreign SQLite reader build smoke', () => {
it('passes against the built entry', () => {
expect(() => smokeForeignSqliteReaderWorker(builtDirectory)).not.toThrow()
})
it('fails when the entry is missing', () => {
expect(() => smokeForeignSqliteReaderWorker(fixtureDirectory())).toThrow('smoke failed')
})
it('fails when the worker answers without reading', () => {
const directory = fixtureDirectory()
writeFileSync(
join(directory, ENTRY),
`const { parentPort } = require('node:worker_threads')
parentPort.on('message', ({ id }) => parentPort.postMessage({ id, ok: true, value: [] }))`
)
expect(() => smokeForeignSqliteReaderWorker(directory)).toThrow('smoke failed')
})
it('runs in the orcad build under both runtimes', () => {
const source = readFileSync(resolve('config/scripts/build-orcad.mjs'), 'utf8')
expect(source).toContain('smokeForeignSqliteReaderWorker(OUT_DIR)')
expect(source).toContain(
'smokeForeignSqliteReaderWorker(OUT_DIR, { runtimePath: nodeRuntimePath })'
)
})
})
@@ -10,6 +10,12 @@ export const associationsPath = fileURLToPath(
new URL('../../src/renderer/src/lib/monaco-language-associations.json', import.meta.url)
)
// Monaco omits common Ruby task, template and configuration files.
const rubyAssociations = {
extensions: ['.rake', '.ru', '.jbuilder', '.thor'],
filenames: ['Guardfile', 'Capfile', 'Podfile', 'Brewfile', 'Vagrantfile']
}
// Read registration metadata without importing Monaco or executing its grammar loaders.
export function readMonacoAssociations() {
const entry = ts.createSourceFile(
@@ -62,6 +68,12 @@ export function readMonacoAssociations() {
if (!metadata.id) {
throw new Error(`Missing language id in ${file}`)
}
if (metadata.id === 'ruby') {
metadata.extensions = [
...new Set([...metadata.extensions, ...rubyAssociations.extensions])
]
metadata.filenames = [...new Set([...metadata.filenames, ...rubyAssociations.filenames])]
}
registrations.push(metadata)
}
ts.forEachChild(node, visit)
@@ -3,10 +3,36 @@ import { describe, expect, it } from 'vitest'
import { associationsPath, readMonacoAssociations } from './generate-monaco-associations.mjs'
describe('Monaco filename associations', () => {
it('matches every registration shipped by the installed editor entry point', () => {
it('matches the installed editor registrations and curated Orca associations', () => {
expect(
JSON.parse(readFileSync(associationsPath, 'utf8')),
'Run node config/scripts/generate-monaco-associations.mjs after upgrading Monaco'
'Run node config/scripts/generate-monaco-associations.mjs after changing associations or Monaco'
).toEqual(readMonacoAssociations())
})
it('keeps built-in Ruby aliases alongside the curated Ruby associations', () => {
expect(readMonacoAssociations().find((language) => language.id === 'ruby')).toEqual({
id: 'ruby',
extensions: expect.arrayContaining([
'.rb',
'.rbx',
'.rjs',
'.gemspec',
'.pp',
'.rake',
'.ru',
'.jbuilder',
'.thor'
]),
filenames: expect.arrayContaining([
'rakefile',
'Gemfile',
'Guardfile',
'Capfile',
'Podfile',
'Brewfile',
'Vagrantfile'
])
})
})
})
@@ -16,10 +16,14 @@ describe('Git binary compatibility PR gate', () => {
const run = stepNamed('Verify Git binary compatibility matrix')?.run
expect(run).toContain('ORCA_GIT_COMPAT_BINARY="$HOME/.cache/orca-git-compat/git-2.25.5/git"')
expect(run).toContain('GIT_EXEC_PATH="$HOME/.cache/orca-git-compat/git-2.25.5"')
expect(run).toContain('alpine/git:edge-2.38.1|2.38.1')
expect(run).toContain('alpine/git:v2.49.1|2.49.1')
expect(run).toContain('ORCA_GIT_COMPAT_IMAGE="$image"')
expect(run).toContain('src/shared/git-binary-compatibility.test.ts')
expect(run).toContain('src/main/git/worktree-safety-real-git.test.ts')
expect(run).toContain('src/main/git/worktree-rebase-update-refs-real-git.test.ts')
expect(run).toContain('src/relay/git-review-draft-binary-compatibility.test.ts')
expect(run).toContain('pids+=("$!")')
expect(run).toContain('wait "$pid" || status=1')
})
@@ -30,10 +34,17 @@ describe('Git binary compatibility PR gate', () => {
expect(run).toContain('git-2.25.5.tar.gz')
// Why asserted: the sha256 check only runs on the build path, so a cached binary
// must come from a key that pins the same version the tarball line declares.
expect(run).toContain('if [ -x "$source/git" ]; then')
expect(run).toContain('[ -x "$source/git" ] && [ -x "$source/git-submodule" ]')
expect(run).toContain('[ -f "$source/git-sh-setup" ] && [ -f "$source/git-sh-i18n" ]')
expect(run).toContain(
'[ -f "$source/git-parse-remote" ] && [ -x "$source/git-sh-i18n--envsubst" ]'
)
expect(run).toContain('41662c52fc16fec4963bfc41075e71f8ead6b5e386797eb6f9a1111ff95a8ddf')
expect(run).toContain('-j"$(nproc)"')
expect(run).toContain('NO_GETTEXT=YesPlease NO_TCLTK=YesPlease NO_PYTHON=YesPlease git')
expect(run).toContain('NO_GETTEXT=YesPlease NO_TCLTK=YesPlease NO_PYTHON=YesPlease')
expect(run).toContain(
'git git-submodule git-sh-setup git-sh-i18n git-parse-remote git-sh-i18n--envsubst'
)
expect(run).toContain('sha256sum --check')
expect(run).toContain('find "$source" -name \'*.o\' -delete')
// The cached path and the build path must be the same directory or the guard
@@ -61,7 +72,7 @@ describe('Git binary compatibility PR gate', () => {
expect(steps[matrixIndex].run).not.toContain('make -C')
expect(baselineSteps[cacheIndex].with.path).toBe(BASELINE_DIR)
expect(baselineSteps[cacheIndex].with.key).toBe(
'git-compat-baseline-${{ runner.os }}-${{ runner.arch }}-2.25.5'
'git-compat-baseline-${{ runner.os }}-${{ runner.arch }}-2.25.5-submodule'
)
})
@@ -0,0 +1,440 @@
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { createElement } from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import type * as ReactModule from 'react'
import type * as GhUtils from '../../src/main/github/gh-utils'
import type * as IssueMetadata from '../../src/renderer/src/hooks/useIssueMetadata'
import type { GitHubWorkItem } from '../../src/shared/github/work-item-types'
import type { GitHubOwnerRepo } from '../../src/shared/github/pull-request-types'
import type { TaskSourceContext } from '../../src/shared/task-source-context'
import type { Repo } from '../../src/shared/repo-types'
const fixture = vi.hoisted(() => {
const state: {
loads: { key: string | null; load: () => Promise<unknown[]> }[]
requests: { args: string[]; host?: string }[]
gh: ReturnType<typeof vi.fn>
apiUpdate: ReturnType<typeof vi.fn>
preference: 'origin' | 'upstream'
} = { loads: [], requests: [], gh: vi.fn(), apiUpdate: vi.fn(), preference: 'upstream' }
return state
})
vi.mock('react', async (original) => ({
...(await original<typeof ReactModule>()),
useState: (initial: unknown) => [typeof initial === 'function' ? initial() : initial, vi.fn()],
useMemo: <T>(value: () => T) => value(),
useCallback: <T>(value: T) => value,
useRef: <T>(initial: T) => ({ current: initial }),
useEffect: vi.fn()
}))
vi.mock('zustand/react/shallow', () => ({ useShallow: <T>(value: T) => value }))
vi.mock('@/store', () => ({
useAppStore: Object.assign(
(selector: (state: unknown) => unknown) =>
selector({ patchWorkItem: vi.fn(), patchProjectRowContent: vi.fn() }),
{ getState: () => ({ recordFeatureInteraction: vi.fn() }) }
)
}))
vi.mock('@/lib/repo-runtime-owner', () => ({
getSettingsForRepoRuntimeOwner: () => ({ activeRuntimeEnvironmentId: null })
}))
vi.mock('@/i18n/i18n', () => ({ translate: (_key: string, fallback: string) => fallback }))
vi.mock('@/components/github/github-duplicate-issue-candidates', () => ({
useGitHubDuplicateIssueCandidates: () => []
}))
vi.mock('@/components/github/github-work-item-comment-mutations', () => ({
notifyWorkItemDetailsMutation: vi.fn()
}))
vi.mock('@/hooks/useIssueMetadata', async (original) => ({
...(await original<typeof IssueMetadata>()),
useImmediateMutation: () => ({ isPending: () => false, run: vi.fn() })
}))
vi.mock('@/hooks/useMetadataListRequest', () => ({
useMetadataListRequest: <T>(args: { cacheKey: string | null; load: () => Promise<T[]> }) => {
fixture.loads.push({ key: args.cacheKey, load: args.load })
return { data: [], loading: false, error: null }
}
}))
vi.mock('../../src/main/github/gh-utils', async (original) => ({
...(await original<typeof GhUtils>()),
ghExecFileAsync: fixture.gh,
acquire: vi.fn(),
release: vi.fn(),
getOwnerRepoForRemote: async (_path: string, remote: string) => ({
owner: remote === 'upstream' ? 'upstream-owner' : 'fork-owner',
repo: 'widgets'
})
}))
vi.mock('../../src/main/git/remote-name-listing', () => ({
shouldProbeGitRemote: async () => true
}))
import { GHEditSection } from '../../src/renderer/src/components/github-item-dialog/edit-item-fields/gh-edit-section'
import {
runGHEditLabelToggle,
runGHEditStateChange
} from '../../src/renderer/src/components/github-item-dialog/edit-item-fields/gh-edit-section-mutations'
import { findTaskPageDialogWorkItem } from '../../src/renderer/src/components/task-page-cache-selectors'
import { getTaskPageRepoSourceContext } from '../../src/renderer/src/components/task-page-source-context'
import { workItemsCacheKey } from '../../src/renderer/src/store/github/cache-identity'
import { createTestStore } from '../../src/renderer/src/store/slices/github-slice-test-harness'
import { getTaskSourceCacheScope } from '../../src/shared/task-source-context'
import { listLabels, listAssignableUsers } from '../../src/main/github/issue-field-options'
import { useRepoLabels, useRepoAssignees } from '../../src/renderer/src/hooks/useIssueMetadata'
import { updateIssue } from '../../src/main/github/issue-update'
import { materializeTaskPageItemList } from '../../src/renderer/src/components/task-page-github-work-item-mutations'
import {
resetTaskPageGitHubMutationRegistryForTests,
setTaskPageGitHubMutationQueryKey
} from '../../src/renderer/src/components/task-page-github-work-item-mutation-registry'
function renderEditSection(props: Parameters<typeof GHEditSection>[0]): void {
renderToStaticMarkup(createElement(GHEditSection, props))
}
const registeredRepo: Repo = {
id: 'repo-1',
path: join(tmpdir(), 'orca-opened-issue-repository-fixture'),
displayName: 'widgets',
badgeColor: 'primary',
addedAt: 1,
upstream: { owner: 'upstream-owner', repo: 'widgets', host: 'github.com' }
}
function sourceFor(preference: 'origin' | 'upstream'): TaskSourceContext {
const source = getTaskPageRepoSourceContext(
{ ...registeredRepo, issueSourcePreference: preference },
'github'
)
if (!source) {
throw new Error('Registered fixture must produce a source context')
}
return source
}
const sourceContext = sourceFor('origin')
const fork: GitHubWorkItem = {
id: 'issue:5',
type: 'issue',
number: 5,
title: 'FORK title',
state: 'open',
url: 'https://github.com/fork-owner/widgets/issues/5',
labels: [],
updatedAt: '',
author: null,
repoId: 'repo-1'
}
const upstream: GitHubWorkItem = {
...fork,
title: 'UPSTREAM title',
url: 'https://github.com/upstream-owner/widgets/issues/5'
}
const issueRepo = { owner: 'fork-owner', repo: 'widgets', host: 'github.com' }
type MetadataArgs = { repoPath: string; ownerRepo?: GitHubOwnerRepo }
beforeEach(() => {
fixture.preference = 'upstream'
fixture.loads = []
fixture.requests = []
resetTaskPageGitHubMutationRegistryForTests()
setTaskPageGitHubMutationQueryKey('current-upstream-list')
fixture.gh.mockReset()
fixture.gh.mockImplementation(async (args: string[], options: { host?: string }) => {
fixture.requests.push({ args, host: options.host })
return { stdout: '', stderr: '' }
})
fixture.apiUpdate = vi.fn((args: Parameters<typeof window.api.gh.updateIssue>[0]) =>
updateIssue(
args.repoPath,
args.number,
args.updates,
null,
{},
fixture.preference,
args.ownerRepo
)
)
vi.stubGlobal('window', {
api: {
gh: {
updateIssue: fixture.apiUpdate,
listLabels: (args: MetadataArgs) =>
listLabels(args.repoPath, fixture.preference, null, {}, args.ownerRepo),
listAssignableUsers: (args: MetadataArgs) =>
listAssignableUsers(args.repoPath, fixture.preference, null, {}, args.ownerRepo)
}
}
})
})
afterEach(() => {
resetTaskPageGitHubMutationRegistryForTests()
vi.unstubAllGlobals()
})
it.each([
{ openedItem: fork, listItem: upstream, preference: 'upstream' },
{ openedItem: upstream, listItem: fork, preference: 'origin' },
{ openedItem: fork, listItem: fork, preference: 'origin' }
] as const)(
'scopes $openedItem.title labels under $preference to its canonical list row',
async ({ openedItem, listItem, preference }) => {
fixture.preference = preference
expect(sourceFor('upstream')).toEqual(sourceContext)
const store = createTestStore()
const key = workItemsCacheKey(
registeredRepo.id,
36,
'',
getTaskSourceCacheScope(sourceFor('upstream'))
)
store.setState({
workItemsCache: { [key]: { data: [listItem], fetchedAt: Date.now() } }
})
const opened =
findTaskPageDialogWorkItem(store.getState().workItemsCache, {
id: openedItem.id,
repoId: openedItem.repoId,
url: openedItem.url
}) ?? openedItem
expect(opened.url).toBe(openedItem.url)
const target = { ...issueRepo, owner: openedItem === fork ? 'fork-owner' : 'upstream-owner' }
let mutation: Promise<unknown> = Promise.resolve()
runGHEditLabelToggle({
itemId: opened.id,
itemNumber: opened.number,
itemRepoId: opened.repoId,
repoPath: registeredRepo.path,
sourceContext,
projectOrigin: undefined,
issueRepo: target,
label: 'fork-only-label',
localLabels: [],
run: async (_key, options) => {
options.onOptimistic?.()
mutation = options.mutate()
await mutation
options.onSuccess?.()
return true
},
onLabelsChange: vi.fn(),
patchWorkItem: store.getState().patchWorkItem,
patchProjectRowIfNeeded: vi.fn(),
onMutated: vi.fn()
})
await mutation
expect(fixture.requests[0].args).toContain(`${target.owner}/widgets`)
expect(fixture.apiUpdate).toHaveBeenCalledWith(expect.objectContaining({ ownerRepo: target }))
expect(store.getState().workItemsCache[key]?.data?.[0].labels).toEqual(
listItem.url === openedItem.url ? ['fork-only-label'] : []
)
}
)
it.each([
{ openedItem: fork, owner: 'fork-owner', preference: 'origin' },
{ openedItem: fork, owner: 'fork-owner', preference: 'upstream' },
{ openedItem: upstream, owner: 'upstream-owner', preference: 'origin' },
{ openedItem: upstream, owner: 'upstream-owner', preference: 'upstream' }
] as const)(
'loads $owner picker candidates while preference=$preference',
async ({ preference, openedItem, owner }) => {
fixture.preference = preference
renderEditSection({
item: openedItem,
repoPath: registeredRepo.path,
repoId: fork.repoId,
sourceContext,
projectOrigin: undefined,
localState: 'open',
localLabels: [],
assignees: [],
onStateChange: vi.fn(),
onLabelsChange: vi.fn(),
onMutated: vi.fn(),
onUse: vi.fn()
})
for (const request of fixture.loads.filter((load) => load.key !== null)) {
await request.load()
}
expect(
fixture.requests.map((request) => request.args.find((arg) => arg.startsWith('repos/')))
).toEqual([`repos/${owner}/widgets/labels`, `repos/${owner}/widgets/assignees?per_page=100`])
}
)
it.each([
{ openedItem: fork, listItem: fork },
{ openedItem: fork, listItem: upstream },
{ openedItem: upstream, listItem: fork },
{ openedItem: upstream, listItem: upstream }
])(
'a $openedItem.title close only controls its own row while search lags (list=$listItem.title)',
async ({ openedItem, listItem }) => {
const target = { ...issueRepo, owner: openedItem === fork ? 'fork-owner' : 'upstream-owner' }
let pending = Promise.resolve()
runGHEditStateChange({
newState: 'closed',
localState: 'open',
itemId: fork.id,
itemNumber: fork.number,
itemRepoId: fork.repoId,
repoPath: registeredRepo.path,
sourceContext,
projectOrigin: undefined,
issueRepo: target,
run: (_key, options) => {
pending = (async () => {
options.onOptimistic?.()
await options.mutate()
options.onSuccess?.()
})()
return pending
},
onStateChange: vi.fn(),
patchWorkItem: vi.fn(),
patchProjectRowIfNeeded: vi.fn(),
onMutated: vi.fn()
})
await pending
expect(fixture.apiUpdate).toHaveBeenCalledWith(expect.objectContaining({ ownerRepo: target }))
const displayed = materializeTaskPageItemList({
networkItems: [listItem],
previousItems: [listItem],
queryKey: 'current-upstream-list'
})
expect(displayed[0]?.state).toBe(listItem.url === openedItem.url ? 'closed' : 'open')
}
)
it('keeps ordinary metadata caches distinct by canonical repository and host', () => {
const identities = [
issueRepo,
{ ...issueRepo, owner: 'upstream-owner' },
{ ...issueRepo, host: 'ghe.example:8443' }
]
for (const ownerRepo of identities) {
useRepoLabels(registeredRepo.path, registeredRepo.id, { ownerRepo })
useRepoAssignees(registeredRepo.path, registeredRepo.id, { ownerRepo })
}
expect(new Set(fixture.loads.filter((_, i) => i % 2 === 0).map((load) => load.key)).size).toBe(3)
expect(new Set(fixture.loads.filter((_, i) => i % 2 === 1).map((load) => load.key)).size).toBe(3)
})
it('keeps metadata requests without an explicit target compatible', async () => {
useRepoLabels(registeredRepo.path, registeredRepo.id)
useRepoAssignees(registeredRepo.path, registeredRepo.id)
for (const request of fixture.loads) {
await request.load()
}
expect(fixture.loads.map((load) => load.key)).toEqual([registeredRepo.id, registeredRepo.id])
expect(
fixture.requests.map((request) => request.args.find((arg) => arg.startsWith('repos/')))
).toEqual([
'repos/upstream-owner/widgets/labels',
'repos/upstream-owner/widgets/assignees?per_page=100'
])
})
it('keeps Project row metadata on the existing slug route', async () => {
const labels = vi.fn().mockResolvedValue({ ok: true, labels: [] })
const users = vi.fn().mockResolvedValue({ ok: true, users: [] })
vi.stubGlobal('window', {
api: { gh: { listLabelsBySlug: labels, listAssignableUsersBySlug: users } }
})
renderEditSection({
item: fork,
repoPath: registeredRepo.path,
repoId: fork.repoId,
sourceContext,
projectOrigin: {
owner: 'project-owner',
repo: 'outside',
host: 'ghe.example',
number: fork.number,
type: 'issue',
projectId: 'project-1',
projectItemId: 'row-1',
cacheKey: 'project-key'
},
localState: 'open',
localLabels: [],
assignees: [],
onStateChange: vi.fn(),
onLabelsChange: vi.fn(),
onMutated: vi.fn(),
onUse: vi.fn()
})
for (const request of fixture.loads.filter((load) => load.key !== null)) {
await request.load()
}
expect(labels).toHaveBeenCalledWith({
owner: 'project-owner',
repo: 'outside',
host: 'ghe.example'
})
expect(users).toHaveBeenCalledWith({
owner: 'project-owner',
repo: 'outside',
host: 'ghe.example'
})
expect(fixture.requests).toEqual([])
})
it('rolls back a rejected fork label without changing the upstream row', async () => {
const store = createTestStore()
const key = workItemsCacheKey(registeredRepo.id, 36, '', getTaskSourceCacheScope(sourceContext))
store.setState({ workItemsCache: { [key]: { data: [upstream, fork], fetchedAt: 1 } } })
fixture.gh.mockRejectedValueOnce(new Error('Fixture rejects label'))
let pending = Promise.resolve(false)
const observed: string[][][] = []
runGHEditLabelToggle({
itemId: fork.id,
itemNumber: fork.number,
itemRepoId: fork.repoId,
repoPath: registeredRepo.path,
sourceContext,
projectOrigin: undefined,
issueRepo,
label: 'fork-only-label',
localLabels: [],
run: (_key, options) => {
pending = (async () => {
options.onOptimistic?.()
observed.push(store.getState().workItemsCache[key]?.data?.map((row) => row.labels) ?? [])
try {
await options.mutate()
return true
} catch {
options.onRevert?.()
observed.push(store.getState().workItemsCache[key]?.data?.map((row) => row.labels) ?? [])
return false
}
})()
return pending
},
onLabelsChange: vi.fn(),
patchWorkItem: store.getState().patchWorkItem,
patchProjectRowIfNeeded: vi.fn(),
onMutated: vi.fn()
})
expect(await pending).toBe(false)
expect(observed).toEqual([
[[], ['fork-only-label']],
[[], []]
])
})
it('does not fall back to upstream metadata for an invalid explicit repository', async () => {
const invalid = { owner: '../escape', repo: 'widgets', host: 'github.com' }
await expect(listLabels(registeredRepo.path, 'upstream', null, {}, invalid)).resolves.toEqual([])
await expect(
listAssignableUsers(registeredRepo.path, 'upstream', null, {}, invalid)
).resolves.toEqual([])
expect(fixture.requests).toEqual([])
})
@@ -0,0 +1,204 @@
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import type * as GhUtils from '../../src/main/github/gh-utils'
import type * as ReactModule from 'react'
import type { GitHubOwnerRepo } from '../../src/shared/github/pull-request-types'
import type { GitHubWorkItem } from '../../src/shared/github/work-item-types'
import type { GitHubIssueUpdate } from '../../src/shared/issue-mutation-types'
const fixture = vi.hoisted(() => {
const state: {
callbacks: unknown[]
mutation: Promise<unknown> | null
gh: ReturnType<typeof vi.fn>
apiUpdate: ReturnType<typeof vi.fn>
patch: ReturnType<typeof vi.fn>
preference: 'origin' | 'upstream'
localGitOptions: { wslDistro?: string }
requests: { args: string[]; host?: string; cwd?: string; wslDistro?: string }[]
} = {
callbacks: [],
mutation: null,
gh: vi.fn(),
apiUpdate: vi.fn(),
patch: vi.fn(),
preference: 'origin',
localGitOptions: {},
requests: []
}
return state
})
vi.mock('react', async (original) => ({
...(await original<typeof ReactModule>()),
useState: (value: unknown) => [typeof value === 'function' ? value() : value, vi.fn()],
useMemo: <T>(getValue: () => T) => getValue(),
useCallback: <T>(callback: T) => {
fixture.callbacks.push(callback)
return callback
}
}))
vi.mock('zustand/react/shallow', () => ({ useShallow: <T>(selector: T) => selector }))
vi.mock('@/store', () => ({
useAppStore: Object.assign(
(selector: (state: unknown) => unknown) =>
selector({
patchWorkItem: fixture.patch,
patchProjectRowContent: fixture.patch,
repos: [],
settings: {}
}),
{ getState: () => ({ recordFeatureInteraction: vi.fn() }) }
)
}))
vi.mock('@/lib/repo-runtime-owner', () => ({
getSettingsForRepoRuntimeOwner: () => ({ activeRuntimeEnvironmentId: null })
}))
vi.mock('@/components/ui/popover', () => ({
Popover: vi.fn(),
PopoverContent: vi.fn(),
PopoverTrigger: vi.fn()
}))
vi.mock('@/hooks/useIssueMetadata', () => ({
useRepoAssignees: () => ({ data: [], loading: false, error: null }),
useImmediateMutation: () => ({
isPending: () => false,
run: (_key: string, spec: { mutate: () => Promise<unknown> }) => {
fixture.mutation = spec.mutate()
}
})
}))
vi.mock('@/hooks/useGitHubSlugMetadata', () => ({
useRepoAssigneesBySlug: () => ({
data: [{ login: 'octo', name: null, avatarUrl: '' }],
loading: false,
error: null
})
}))
vi.mock('@/i18n/i18n', () => ({ translate: (_key: string, fallback: string) => fallback }))
vi.mock('@/components/github/work-item-state-presentation', () => ({ ReviewerAvatar: vi.fn() }))
vi.mock('../../src/main/github/gh-utils', async (original) => ({
...(await original<typeof GhUtils>()),
ghExecFileAsync: fixture.gh,
acquire: vi.fn(),
release: vi.fn(),
getOwnerRepoForRemote: async (_path: string, remote: string) => ({
owner: remote === 'upstream' ? 'upstream-owner' : 'fork-owner',
repo: 'widgets'
})
}))
vi.mock('../../src/main/git/remote-name-listing', () => ({
shouldProbeGitRemote: async () => true
}))
import { PRAssigneesPanel } from '../../src/renderer/src/components/github/PRAssigneesPanel'
import { updateIssue } from '../../src/main/github/issue-update'
import { _resetOriginGitHubApiRepositoryCache } from '../../src/main/github/github-api-repository'
const repoPath = join(tmpdir(), 'orca-pr-assignee-repository-fixture')
beforeEach(() => {
fixture.callbacks = []
fixture.mutation = null
fixture.requests = []
fixture.localGitOptions = {}
fixture.gh.mockReset()
fixture.gh.mockImplementation(
async (
args: string[],
options: {
host?: string
cwd?: string
wslDistro?: string
}
) => {
fixture.requests.push({
args,
host: options.host,
cwd: options.cwd,
wslDistro: options.wslDistro
})
return { stdout: '', stderr: '' }
}
)
_resetOriginGitHubApiRepositoryCache()
fixture.apiUpdate = vi.fn(
(args: {
repoPath: string
number: number
updates: GitHubIssueUpdate
ownerRepo?: GitHubOwnerRepo
}) =>
updateIssue(
args.repoPath,
args.number,
args.updates,
null,
fixture.localGitOptions,
fixture.preference,
args.ownerRepo
)
)
vi.stubGlobal('window', { api: { gh: { updateIssue: fixture.apiUpdate } } })
})
afterEach(() => vi.unstubAllGlobals())
it.each([
{ owner: 'upstream-owner', preference: 'origin', assigned: false, legacy: false },
{ owner: 'upstream-owner', preference: 'origin', assigned: true, legacy: false },
{ owner: 'fork-owner', preference: 'upstream', assigned: false, legacy: false },
{ owner: 'fork-owner', preference: 'upstream', assigned: true, legacy: false },
{ owner: 'upstream-owner', preference: 'origin', assigned: false, legacy: true }
] as const)(
'keeps $owner PR assignees under $preference (remove=$assigned, legacy=$legacy)',
async ({ owner, preference, assigned, legacy }) => {
fixture.preference = preference
fixture.localGitOptions = owner === 'fork-owner' ? { wslDistro: 'Ubuntu' } : {}
const item: GitHubWorkItem = {
id: 'pr:5',
type: 'pr',
number: 5,
title: 'Opened PR',
state: 'open',
url: `https://github.com/${owner}/widgets/pull/5`,
prRepo: legacy ? undefined : { owner, repo: 'widgets', host: 'github.com' },
labels: [],
updatedAt: '',
author: null,
repoId: 'repo-1',
assignees: assigned ? [{ login: 'octo', name: null, avatarUrl: '' }] : []
}
PRAssigneesPanel({ item, repoPath, projectOrigin: undefined, onMutated: vi.fn() })
const toggleAssignee = fixture.callbacks.at(-1)
if (typeof toggleAssignee !== 'function') {
throw new Error('PR panel did not create an assignee handler')
}
toggleAssignee('octo')
await fixture.mutation
expect(fixture.requests).toEqual([
{
args: [
'issue',
'edit',
'5',
'--repo',
`${owner}/widgets`,
assigned ? '--remove-assignee' : '--add-assignee',
'octo'
],
host: 'github.com',
cwd: repoPath,
wslDistro: fixture.localGitOptions.wslDistro
}
])
expect(fixture.apiUpdate).toHaveBeenCalledWith(
expect.objectContaining({
repoPath,
repoId: item.repoId,
ownerRepo: { owner, repo: 'widgets', host: 'github.com' }
})
)
}
)
@@ -90,9 +90,10 @@ export async function sampleProductionPerformance(boundary, options) {
heartbeatCount += 1
boundary.sendHeartbeat()
}, options.heartbeatIntervalMs)
const cpuBefore = combinedCpuTimeMs(boundary.pids)
loopDelay.enable()
let cpuBefore
try {
cpuBefore = combinedCpuTimeMs(boundary.pids)
loopDelay.enable()
await options.sleep(options.sampleMs)
} finally {
loopDelay.disable()
@@ -1,9 +1,15 @@
import { describe, expect, it } from 'vitest'
import childProcess from 'node:child_process'
import { syncBuiltinESMExports } from 'node:module'
import perfHooks from 'node:perf_hooks'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
parsePhysicalFootprintBytes,
parseProcessCpuTimeMs
parseProcessCpuTimeMs,
sampleProductionPerformance
} from './hang-watchdog-process-metrics.mjs'
const observations = { events: [], readCpu: null, histogram: null }
describe('hang watchdog process metrics', () => {
it('uses the de-duplicated summary for multiple processes', () => {
const output = `
@@ -43,3 +49,203 @@ Electron [101]: 64-bit Footprint: 5000000 B (16384 bytes per page)
expect(parseProcessCpuTimeMs('-1:00')).toBeNull()
})
})
function cpuObservation(pid) {
return ['cpu', 'ps', ['-o', 'time=', '-p', String(pid)], { encoding: 'utf8' }]
}
describe('production watchdog sample lifetime', () => {
beforeEach(() => {
vi.useFakeTimers()
observations.events = []
let enabled = false
observations.histogram = {
enable() {
observations.events.push(['enable'])
const changed = !enabled
enabled = true
return changed
},
disable() {
observations.events.push(['disable'])
const changed = enabled
enabled = false
return changed
},
percentile: (value) => {
observations.events.push(['percentile', value])
return value === 95 ? 1_900_000 : 3_100_000
},
max: 5_000_000
}
vi.spyOn(childProcess, 'execFileSync').mockImplementation((command, args, options) => {
observations.events.push(['cpu', command, args, options])
return observations.readCpu()
})
vi.spyOn(perfHooks, 'monitorEventLoopDelay').mockImplementation((options) => {
observations.events.push(['monitor', options])
return observations.histogram
})
syncBuiltinESMExports()
})
afterEach(() => {
vi.clearAllTimers()
vi.restoreAllMocks()
syncBuiltinESMExports()
vi.useRealTimers()
})
it.each([1, 2])('clears the heartbeat when initial CPU observation %s throws', async (failAt) => {
const error = new Error('PID observation failed')
let reads = 0
observations.readCpu = () => {
if (++reads === failAt) {
throw error
}
return '0:01.20'
}
const sendHeartbeat = vi.fn()
const sleep = vi.fn()
await expect(
sampleProductionPerformance(
{ pids: [101, 102], sendHeartbeat },
{ heartbeatIntervalMs: 2_000, sampleMs: 30_000, sleep }
)
).rejects.toBe(error)
expect(reads).toBe(failAt)
expect(sleep).not.toHaveBeenCalled()
expect(observations.events.filter(([kind]) => kind !== 'disable')).toEqual([
['monitor', { resolution: 10 }],
...[101, 102].slice(0, failAt).map(cpuObservation)
])
expect(vi.getTimerCount()).toBe(0)
await vi.advanceTimersByTimeAsync(4_000)
expect(sendHeartbeat).not.toHaveBeenCalled()
})
it('preserves an invalid CPU diagnostic while releasing the heartbeat', async () => {
observations.readCpu = () => 'invalid CPU time'
const sendHeartbeat = vi.fn()
const sleep = vi.fn()
await expect(
sampleProductionPerformance(
{ pids: [101], sendHeartbeat },
{ heartbeatIntervalMs: 2_000, sampleMs: 30_000, sleep }
)
).rejects.toThrow('Could not read CPU time for PID 101')
expect(sleep).not.toHaveBeenCalled()
expect(observations.events.filter(([kind]) => kind !== 'disable')).toEqual([
['monitor', { resolution: 10 }],
cpuObservation(101)
])
expect(vi.getTimerCount()).toBe(0)
await vi.advanceTimersByTimeAsync(4_000)
expect(sendHeartbeat).not.toHaveBeenCalled()
})
it('repeated failed owners leave no timers or later sends', async () => {
const error = new Error('sample CPU failure')
observations.readCpu = () => {
throw error
}
const sendHeartbeat = vi.fn()
const sleep = vi.fn()
for (let index = 0; index < 64; index++) {
await expect(
sampleProductionPerformance(
{ pids: [101], sendHeartbeat },
{ heartbeatIntervalMs: 2_000, sampleMs: 30_000, sleep }
)
).rejects.toBe(error)
}
expect(sleep).not.toHaveBeenCalled()
expect(observations.events.filter(([kind]) => kind !== 'disable')).toEqual(
Array.from({ length: 64 }, () => [
['monitor', { resolution: 10 }],
cpuObservation(101)
]).flat()
)
expect(vi.getTimerCount()).toBe(0)
await vi.advanceTimersByTimeAsync(4_000)
expect(sendHeartbeat).not.toHaveBeenCalled()
})
async function runSample({ values, sleepError } = {}) {
const cpuValues = values ?? ['0:01.20', '0:02.30', '0:01.25', '0:02.35']
let index = 0
observations.readCpu = () => {
const value = cpuValues[index++]
if (value instanceof Error) {
throw value
}
return value
}
const sendHeartbeat = vi.fn(() => observations.events.push(['heartbeat']))
const sleep = async (ms) => {
observations.events.push(['sleep', ms])
await vi.advanceTimersByTimeAsync(ms)
if (sleepError) {
throw sleepError
}
}
return sampleProductionPerformance(
{ pids: [101, 102], sendHeartbeat },
{ heartbeatIntervalMs: 2_000, sampleMs: 6_000, sleep }
)
}
const completedSampleEvents = [
['monitor', { resolution: 10 }],
cpuObservation(101),
cpuObservation(102),
['enable'],
['sleep', 6_000],
['heartbeat'],
['heartbeat'],
['heartbeat'],
['disable']
]
it('keeps complete live results, observation order and heartbeat pacing', async () => {
expect(await runSample()).toEqual({
cpuMs: 100,
heartbeatCount: 3,
eventLoopDelayP95Ms: 1.9,
eventLoopDelayP99Ms: 3.1,
eventLoopDelayMaxMs: 5
})
expect(observations.events).toEqual([
...completedSampleEvents,
cpuObservation(101),
cpuObservation(102),
['percentile', 95],
['percentile', 99]
])
expect(vi.getTimerCount()).toBe(0)
})
it('keeps a sample sleep rejection and its existing cleanup', async () => {
const error = new Error('sample sleep rejected')
await expect(runSample({ sleepError: error })).rejects.toBe(error)
expect(observations.events).toEqual(completedSampleEvents)
expect(vi.getTimerCount()).toBe(0)
})
it('keeps a final CPU observation failure after cleanup', async () => {
const error = new Error('final CPU read failed')
await expect(runSample({ values: ['0:01.20', '0:02.30', error] })).rejects.toBe(error)
expect(observations.events).toEqual([...completedSampleEvents, cpuObservation(101)])
expect(vi.getTimerCount()).toBe(0)
})
it('keeps the zero floor when the CPU total decreases', async () => {
expect(await runSample({ values: ['0:02.20', '0:03.30', '0:01.25', '0:02.35'] })).toEqual({
cpuMs: 0,
heartbeatCount: 3,
eventLoopDelayP95Ms: 1.9,
eventLoopDelayP99Ms: 3.1,
eventLoopDelayMaxMs: 5
})
expect(vi.getTimerCount()).toBe(0)
})
})
@@ -0,0 +1,160 @@
import { createHash } from 'node:crypto'
import {
appendFileSync,
cpSync,
existsSync,
lstatSync,
mkdirSync,
readFileSync,
readdirSync,
rmSync,
symlinkSync,
writeFileSync
} from 'node:fs'
import { createRequire } from 'node:module'
import { dirname, join, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
const ROOT = resolve(import.meta.dirname, '../..')
function inventory(directory, prefix = '') {
if (lstatSync(directory).isSymbolicLink()) {
throw new Error('Compiler cache directory is a symlink')
}
return readdirSync(directory)
.flatMap((name) => {
const path = join(directory, name)
const file = `${prefix}${name}`
const stat = lstatSync(path)
if (stat.isSymbolicLink()) {
throw new Error('Compiler cache contains a symlink')
}
if (stat.isDirectory()) {
return inventory(path, `${file}/`)
}
if (!stat.isFile()) {
throw new Error('Compiler cache contains a special file')
}
return [{ file, sha256: createHash('sha256').update(readFileSync(path)).digest('hex') }]
})
.sort((a, b) => a.file.localeCompare(b.file))
}
export function compilerCacheIdentity({
policyHash = process.env.COMPILER_POLICY_HASH,
cacheRoot = process.env.RUNNER_TEMP,
platform = process.platform,
arch = process.arch,
node = process.version
} = {}) {
if (!policyHash || !cacheRoot) {
throw new Error('Compiler cache requires policy hash and cache root')
}
return {
key: `headless-compiler-v1-${platform}-${arch}-${node}-${policyHash}`,
path: join(cacheRoot, 'headless-detector-compiler')
}
}
export function packCompilerCache({ root = ROOT, identity = compilerCacheIdentity() } = {}) {
const require = createRequire(join(root, 'package.json'))
const esbuildDir = dirname(require.resolve('esbuild/package.json'))
const nativeName = `@esbuild/${process.platform}-${process.arch}`
const nativeDir = dirname(require.resolve(`${nativeName}/package.json`, { paths: [esbuildDir] }))
rmSync(identity.path, { recursive: true, force: true })
mkdirSync(join(identity.path, 'node_modules', '@esbuild'), { recursive: true })
cpSync(esbuildDir, join(identity.path, 'node_modules', 'esbuild'), {
recursive: true,
dereference: true
})
cpSync(nativeDir, join(identity.path, 'node_modules', nativeName), {
recursive: true,
dereference: true
})
writeFileSync(
join(identity.path, 'manifest.json'),
JSON.stringify({
key: identity.key,
node: process.version,
version: require('esbuild').version,
files: inventory(identity.path)
})
)
}
export async function activateCompilerCache({
root = ROOT,
identity = compilerCacheIdentity()
} = {}) {
const dependencies = join(root, 'node_modules')
let created = false
try {
if (existsSync(dependencies)) {
throw new Error('Compiler activation requires an empty dependency tree')
}
const files = inventory(identity.path).filter((row) => row.file !== 'manifest.json')
const manifest = JSON.parse(readFileSync(join(identity.path, 'manifest.json'), 'utf8'))
if (
manifest.key !== identity.key ||
manifest.node !== process.version ||
JSON.stringify(files) !== JSON.stringify(manifest.files)
) {
throw new Error('Compiler cache identity or contents differ')
}
mkdirSync(join(dependencies, '@esbuild'), { recursive: true })
created = true
for (const name of ['esbuild', `@esbuild/${process.platform}-${process.arch}`]) {
symlinkSync(join(identity.path, 'node_modules', name), join(dependencies, name), 'dir')
}
const require = createRequire(join(root, 'package.json'))
const esbuild = require('esbuild')
if (esbuild.version !== manifest.version) {
throw new Error('Compiler API version differs')
}
await esbuild.build({
stdin: { contents: 'export const value = 1' },
write: false,
logLevel: 'silent'
})
return { available: true }
} catch (error) {
if (created) {
rmSync(dependencies, { recursive: true, force: true })
}
return { available: false, reason: String(error) }
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const phase = process.argv[2]
const identity =
phase === 'identity'
? compilerCacheIdentity()
: {
key: process.env.COMPILER_CACHE_KEY,
path: process.env.COMPILER_CACHE_PATH
}
if (!identity.key || !identity.path) {
throw new Error('Compiler cache identity required')
}
const output = (values) => {
for (const [name, value] of Object.entries(values)) {
appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`)
}
}
if (phase === 'identity') {
output(identity)
} else if (phase === 'pack') {
packCompilerCache({ identity })
} else if (phase === 'activate') {
const result = await activateCompilerCache({ identity })
output({ available: result.available })
console.log(
result.available
? 'Validated headless compiler cache'
: `Use normal dependency install: ${result.reason}`
)
} else {
throw new Error('Expected identity, pack, or activate')
}
}
@@ -0,0 +1,204 @@
import {
appendFileSync,
cpSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
readdirSync,
rmSync,
symlinkSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, expect, it } from 'vitest'
import { parse } from 'yaml'
import {
activateCompilerCache,
compilerCacheIdentity,
packCompilerCache
} from './headless-detector-compiler-cache.mjs'
import { collectNodeServerInputs } from './node-server-change-scope.mjs'
import { runProcessSync } from './script-child-process.mjs'
const temporary = []
afterEach(() => {
for (const dir of temporary.splice(0)) {
rmSync(dir, { recursive: true, force: true })
}
})
function fixture() {
const directory = mkdtempSync(join(tmpdir(), 'headless-compiler-cache-'))
temporary.push(directory)
const root = join(directory, 'checkout')
mkdirSync(root)
writeFileSync(join(root, 'package.json'), '{"type":"module"}')
const identity = compilerCacheIdentity({ policyHash: 'policy', cacheRoot: directory })
packCompilerCache({ identity })
return { root, identity }
}
function changeManifest(identity, update) {
const path = join(identity.path, 'manifest.json')
const manifest = JSON.parse(readFileSync(path, 'utf8'))
update(manifest)
writeFileSync(path, JSON.stringify(manifest))
}
it('separates policy, Node, platform and architecture identities with the same archive path', () => {
const options = {
policyHash: 'policy',
cacheRoot: '/cache',
platform: 'linux',
arch: 'x64',
node: 'v24.21.0'
}
const original = compilerCacheIdentity(options)
for (const override of [
{ policyHash: 'changed' },
{ node: 'v24.22.0' },
{ platform: 'darwin' },
{ arch: 'arm64' }
]) {
const other = compilerCacheIdentity({ ...options, ...override })
expect(other.key).not.toBe(original.key)
expect(other.path).toBe(original.path)
}
})
it('activates only the actual compiler packages and preserves import graph behavior', async () => {
const { root, identity } = fixture()
expect(await activateCompilerCache({ root, identity })).toEqual({ available: true })
expect(readdirSync(join(root, 'node_modules')).sort()).toEqual(['@esbuild', 'esbuild'])
for (const name of [
'node-server-change-scope',
'node-server-test-paths',
'node-server-qualification',
'orcad-entry-build'
]) {
mkdirSync(join(root, 'config', 'scripts'), { recursive: true })
cpSync(
new URL(`./${name}.mjs`, import.meta.url),
join(root, 'config', 'scripts', `${name}.mjs`)
)
}
writeFileSync(
join(root, 'entry.ts'),
"import './first'; export * from './exports'; import('./dynamic'); require('./required'); import 'external-package'; import './native.node'"
)
for (const name of ['first', 'exports', 'dynamic', 'required']) {
writeFileSync(join(root, `${name}.ts`), 'export const value = 1')
}
writeFileSync(
join(root, 'probe.mjs'),
"import { collectNodeServerInputs } from './config/scripts/node-server-change-scope.mjs'; console.log(JSON.stringify([...(await collectNodeServerInputs({ root: process.cwd(), entryPoints: ['entry.ts'] }))].sort()))"
)
const baseline = [...(await collectNodeServerInputs({ root, entryPoints: ['entry.ts'] }))].sort()
const candidate = runProcessSync({
program: process.execPath,
args: ['probe.mjs'],
cwd: root,
timeoutMs: 10_000
})
expect(candidate.code, candidate.stderr).toBe(0)
expect(JSON.parse(candidate.stdout.trim())).toEqual(baseline)
}, 20_000)
it.each(['key', 'node', 'version', 'files'])(
'falls back on invalid manifest %s and cleans partial activation',
async (field) => {
const { root, identity } = fixture()
changeManifest(identity, (manifest) => {
manifest[field] = 'wrong'
})
expect((await activateCompilerCache({ root, identity })).available).toBe(false)
expect(existsSync(join(root, 'node_modules'))).toBe(false)
}
)
it.each(['modified', 'missing', 'extra', 'symlink', 'malformed'])(
'falls back on %s cache contents before loading code',
async (kind) => {
const { root, identity } = fixture()
const compiler = join(identity.path, 'node_modules', 'esbuild', 'lib', 'main.js')
if (kind === 'modified') {
appendFileSync(compiler, '\nthrow Error("must not load")')
}
if (kind === 'missing') {
rmSync(compiler)
}
if (kind === 'extra') {
writeFileSync(join(identity.path, 'unexpected'), 'extra')
}
if (kind === 'symlink') {
rmSync(compiler)
symlinkSync(join(root, 'package.json'), compiler)
}
if (kind === 'malformed') {
writeFileSync(join(identity.path, 'manifest.json'), '{')
}
expect((await activateCompilerCache({ root, identity })).available).toBe(false)
expect(existsSync(join(root, 'node_modules'))).toBe(false)
}
)
it('leaves existing dependencies alone and falls back on an absent archive', async () => {
const { root, identity } = fixture()
rmSync(identity.path, { recursive: true })
expect((await activateCompilerCache({ root, identity })).available).toBe(false)
mkdirSync(join(root, 'node_modules'))
writeFileSync(join(root, 'node_modules', 'retained'), 'retained')
expect((await activateCompilerCache({ root, identity })).available).toBe(false)
expect(readFileSync(join(root, 'node_modules', 'retained'), 'utf8')).toBe('retained')
})
it('uses exact optional restores, seeds only main and retains full dependency fallback', () => {
const action = parse(readFileSync('.github/actions/prepare-headless-compiler/action.yml', 'utf8'))
const steps = action.runs.steps
const restore = steps.find((step) => step.id === 'cache')
expect(restore.uses).toBe('actions/cache/restore@v5')
expect(restore['continue-on-error']).toBe(true)
expect(restore.with['restore-keys']).toBeUndefined()
const save = steps.find((step) => step.uses === 'actions/cache/save@v5')
expect(save.if).toContain("github.ref == 'refs/heads/main'")
expect(save.if).toContain("github.event_name != 'pull_request'")
expect(save['continue-on-error']).toBe(true)
expect(save.with).toEqual(restore.with)
const workflow = parse(readFileSync('.github/workflows/node-server-tests.yml', 'utf8'))
const detector = workflow.jobs.changes.steps
const cached = detector.find((step) => step.id === 'compiler')
expect(cached.if).toBe("steps.scope.outputs.graph_required == 'true'")
expect(cached['continue-on-error']).toBe(true)
expect(
detector.find((step) => step.uses === './.github/actions/install-node-dependencies').if
).toBe(
"steps.scope.outputs.graph_required == 'true' && steps.compiler.outputs.available != 'true'"
)
for (const event of ['push', 'pull_request']) {
expect(workflow.on[event].paths).toContain('.github/actions/prepare-headless-compiler/**')
}
const warmer = parse(readFileSync('.github/workflows/ci-cache-warmup.yml', 'utf8'))
const warmSteps = warmer.jobs.warm.steps
expect(
warmSteps.findIndex((step) => step.uses === './.github/actions/prepare-headless-compiler')
).toBeGreaterThan(
warmSteps.findIndex((step) => step.uses === './.github/actions/install-node-dependencies')
)
for (const event of ['push', 'pull_request']) {
expect(warmer.on[event].paths).toContain('.github/actions/prepare-headless-compiler/**')
}
const inputs = [
...steps.find((step) => step.id === 'identity').env.COMPILER_POLICY_HASH.matchAll(/'([^']+)'/g)
].map((match) => match[1])
for (const input of inputs) {
expect(
warmer.on.push.paths.some(
(pattern) =>
pattern === input ||
(pattern.endsWith('/**') && input.startsWith(pattern.slice(0, -2))) ||
(pattern.endsWith('*') && input.startsWith(pattern.slice(0, -1)))
),
input
).toBe(true)
}
})
@@ -1,4 +1,12 @@
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import {
chmodSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { delimiter, join } from 'node:path'
import { spawnSync } from 'node:child_process'
@@ -56,28 +64,38 @@ function executeInstallScript(fixture) {
}
describe('install-node-dependencies action', () => {
it.each(['/home/runner/pnpm store/v11', 'C:\\Users\\runner\\pnpm store\\v11'])(
'preserves setup-node store path %s and lowercase architecture',
(storePath) => {
const fixture = createFixture()
const output = join(fixture.root, 'github-output')
try {
const result = run('bash', ['-e', '-o', 'pipefail', '-c', storeScript], {
env: {
...process.env,
GITHUB_OUTPUT: output,
LOCKFILE_HASH: 'lockfile-digest',
PNPM_TEST_STORE_PATH: storePath,
PATH: `${fixture.bin}${delimiter}${process.env.PATH}`
}
})
expect(result.status, result.stderr || result.stdout).toBe(0)
expect(readFileSync(output, 'utf8')).toBe(`path=${storePath}\narch=${process.arch}\n`)
} finally {
rmSync(fixture.root, { recursive: true, force: true })
it.each([
['/home/runner/pnpm store/v11', 'true'],
['/home/runner/pnpm store/v11', 'false'],
['C:\\Users\\runner\\pnpm store\\v11', 'true'],
['C:\\Users\\runner\\pnpm store\\v11', 'false']
])('preserves setup-node store path %s with producer lookup %s', (storePath, lookupOnly) => {
const fixture = createFixture()
const output = join(fixture.root, 'github-output')
const environment = join(fixture.root, 'github-env')
try {
const result = run('bash', ['-e', '-o', 'pipefail', '-c', storeScript], {
env: {
...process.env,
GITHUB_OUTPUT: output,
GITHUB_ENV: environment,
STORE_LOOKUP_ONLY: lookupOnly,
LOCKFILE_HASH: 'lockfile-digest',
PNPM_TEST_STORE_PATH: storePath,
PATH: `${fixture.bin}${delimiter}${process.env.PATH}`
}
})
expect(result.status, result.stderr || result.stdout).toBe(0)
expect(readFileSync(output, 'utf8')).toBe(`path=${storePath}\narch=${process.arch}\n`)
if (lookupOnly === 'true') {
expect(readFileSync(environment, 'utf8')).toBe(`ORCA_PNPM_STORE_CACHE_PATH=${storePath}\n`)
} else {
expect(existsSync(environment)).toBe(false)
}
} finally {
rmSync(fixture.root, { recursive: true, force: true })
}
)
})
it.each([
['', 'store'],
@@ -52,6 +52,7 @@ export const NEVER_TRANSLATE_VALUES = new Set([
'Goose',
'Grok',
'Hermes',
'Jcode',
'Jira',
'Kilocode',
'Kimi',
@@ -79,6 +80,7 @@ export const NEVER_TRANSLATE_VALUES = new Set([
'markdown',
'gh',
'idle',
'jcode',
'anthropic',
'Discord',
'WSL',
@@ -0,0 +1,106 @@
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { resolveConfig } from 'electron-vite'
import { build } from 'vite'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { runProcess } from '../../src/shared/child-process/run-process'
const projectDir = resolve(import.meta.dirname, '../..')
const require = createRequire(import.meta.url)
let outputDir: string
beforeAll(async () => {
outputDir = mkdtempSync(join(tmpdir(), 'orca-account-runtime-'))
const resolved = await resolveConfig(
{ configFile: join(projectDir, 'electron.vite.config.ts') },
'build',
'production'
)
const main = resolved.config?.main
if (!main?.build) {
throw new Error('Expected main-process build config')
}
await build({
...main,
logLevel: 'silent',
build: {
...main.build,
outDir: join(outputDir, 'bundle'),
sourcemap: false,
rollupOptions: {
...main.build.rollupOptions,
input: join(projectDir, 'src/main/managed-data-accounts/credential-capture.ts'),
output: { format: 'cjs', entryFileNames: 'credential-capture.cjs' }
}
}
})
mkdirSync(join(outputDir, 'source', 'devin'), { recursive: true })
writeFileSync(
join(outputDir, 'source', 'devin', 'credentials.toml'),
'windsurf_api_key = "offline-account-runtime-fixture"\n'
)
})
afterAll(() => {
if (outputDir) {
rmSync(outputDir, { recursive: true, force: true })
}
})
describe('managed account credentials in the production main bundle', () => {
it.each([
{ name: 'Node', program: process.execPath },
{ name: 'Electron', program: require('electron') }
])(
'captures Devin credentials under $name outside the dependency install',
async ({ name, program }) => {
const environment: Record<string, string | undefined> = {
...process.env,
ORCA_BACKGROUND_LAUNCH: '1',
ELECTRON_RUN_AS_NODE: '1'
}
for (const key of [
'HOME',
'XDG_CONFIG_HOME',
'XDG_DATA_HOME',
'XDG_STATE_HOME',
'XDG_CACHE_HOME'
]) {
const directory = join(outputDir, name, key)
mkdirSync(directory, { recursive: true })
environment[key] = directory
}
const script = `
const assert = require('node:assert/strict')
const { captureDataAccountCredentials } = require(process.argv[1])
captureDataAccountCredentials('devin', process.argv[2], process.argv[3])
.then((integrations) => {
assert.deepEqual(integrations, ['devin'])
console.log('Private credentials captured')
}).catch((error) => { console.error(error); process.exitCode = 1 })
`
const destination = join(outputDir, name, 'captured')
const result = await runProcess({
program,
args: [
'-e',
script,
join(outputDir, 'bundle', 'credential-capture.cjs'),
join(outputDir, 'source'),
destination
],
cwd: outputDir,
env: environment,
timeoutMs: 20000
})
expect(result.code, result.stderr).toBe(0)
expect(result.timedOut).toBe(false)
expect(result.stdout.trim()).toBe('Private credentials captured')
expect(readFileSync(join(destination, 'devin', 'credentials.toml'), 'utf8')).toContain(
'offline-account-runtime-fixture'
)
}
)
})
@@ -1,83 +0,0 @@
import { createRequire } from 'node:module'
import path from 'node:path'
import { pathToFileURL } from 'node:url'
import { describe, expect, it } from 'vitest'
import { unified } from 'unified'
import remarkParse from 'remark-parse'
import remarkGfm from 'remark-gfm'
const require = createRequire(import.meta.url)
const resolvedRoot = path.dirname(require.resolve('micromark-extension-gfm-table'))
const root = path.basename(resolvedRoot) === 'dev' ? path.dirname(resolvedRoot) : resolvedRoot
for (const directory of ['lib', 'dev/lib']) {
const { EditMap } = await import(pathToFileURL(path.join(root, directory, 'edit-map.js')).href)
describe(`table edit map ${directory}`, () => {
it('merges repeated offsets, sorts edits, and resets for reuse', () => {
const edits = new EditMap()
edits.add(3, 1, ['c'])
edits.add(1, 1, ['a'])
edits.add(3, 1, ['d'])
edits.add(0, 0, [])
const events = [0, 1, 2, 3, 4, 5]
edits.consume(events)
expect(events).toEqual([0, 'a', 2, 'c', 'd', 5])
edits.add(1, 1, ['new'])
edits.consume(events)
expect(events).toEqual([0, 'new', 2, 'c', 'd', 5])
})
it('does not scan prior offsets when adding thousands of distinct edits', () => {
const edits = new EditMap()
edits.add(0, 1, ['first'])
let reads = 0
const first = edits.map[0]
Object.defineProperty(edits.map, '0', {
configurable: true,
get() {
reads += 1
return first
}
})
for (let offset = 1; offset < 5000; offset += 1) {
edits.add(offset, 1, [offset])
}
expect(reads).toBe(0)
expect(edits.map).toHaveLength(5000)
})
it('preserves complete parsed trees and source positions against the previous merge algorithm', () => {
const parser = unified().use(remarkParse).use(remarkGfm)
const sources = [
'| a | b |\n| :- | -: |\n| x | y |\n',
'> | a | b |\n> | --- | --- |\n> | **bold** | [link][r] |\n\n[r]: https://example.com',
'- item\n\n | a | b |\n | --- | --- |\n | x | y |',
'| escaped \\| pipe | `code` |\n| --- | --- |\n| ~~del~~ | 😀 |\n',
`Before\n\n${'| a | b |\n| --- | --- |\n| x | y |\n\n'.repeat(200)}`
]
const patched = sources.map((source) => parser.parse(source))
const originalAdd = EditMap.prototype.add
let stockCalls = 0
try {
EditMap.prototype.add = function (at, remove, add) {
stockCalls += 1
if (remove === 0 && add.length === 0) {
return
}
const change = this.map.find((entry) => entry[0] === at)
if (change) {
change[1] += remove
change[2].push(...add)
} else {
this.map.push([at, remove, add])
}
}
expect(sources.map((source) => parser.parse(source))).toEqual(patched)
if (directory === 'dev/lib') {
expect(stockCalls).toBeGreaterThan(0)
}
} finally {
EditMap.prototype.add = originalAdd
}
})
})
}
@@ -0,0 +1,120 @@
import { expect, it } from 'vitest'
import { entryStaticClosure } from './build-mobile-web-app-bundle.mjs'
it.each([1, 12, 128, 1000])(
'keeps the complete %s-output closure without shifting a second queue',
(count) => {
const paths = Array.from({ length: count }, (_value, index) => `dist/chunk-${index}.js`)
const metafile = {
outputs: Object.fromEntries(
paths.map((path, index) => [
path,
{
bytes: index + 1,
imports:
index === 0
? paths.slice(1).map((child) => ({ kind: 'import-statement', path: child }))
: []
}
])
)
}
const before = structuredClone(metafile)
const originalShift = Array.prototype.shift
let reached
let shifts = 0
Array.prototype.shift = function () {
shifts++
return originalShift.call(this)
}
try {
reached = entryStaticClosure(metafile, paths[0])
} finally {
Array.prototype.shift = originalShift
}
expect([...reached]).toEqual(paths)
expect(metafile).toEqual(before)
expect([...reached].reduce((total, path) => total + metafile.outputs[path].bytes, 0)).toBe(
(count * (count + 1)) / 2
)
const fresh = entryStaticClosure(metafile, paths[0])
expect(fresh).not.toBe(reached)
reached.add('mutated-result')
expect([...fresh]).toEqual(paths)
expect(shifts).toBe(0)
}
)
it('keeps breadth-first order, duplicates, cycles, missing chunks and dynamic boundaries', () => {
const metafile = {
outputs: {
entry: {
imports: [
{ kind: 'import-statement', path: 'one' },
{ kind: 'dynamic-import', path: 'deferred' },
{ kind: 'import-statement', path: 'two' },
{ kind: 'import-statement', path: 'one' }
]
},
one: { imports: [{ kind: 'import-statement', path: 'three' }] },
two: {
imports: [
{ kind: 'import-statement', path: 'entry' },
{ kind: 'import-statement', path: 'three' },
{ kind: 'import-statement', path: 'missing' }
]
},
three: { imports: [] },
deferred: { imports: [{ kind: 'import-statement', path: 'deferred-child' }] }
}
}
expect([...entryStaticClosure(metafile, 'entry')]).toEqual([
'entry',
'one',
'two',
'three',
'missing'
])
metafile.outputs.one.imports.push({ kind: 'import-statement', path: 'fresh-😀' })
expect([...entryStaticClosure(metafile, 'entry')]).toEqual([
'entry',
'one',
'two',
'three',
'fresh-😀',
'missing'
])
expect([...entryStaticClosure({ outputs: {} }, 'unknown')]).toEqual(['unknown'])
})
it('keeps a later output error after earlier discoveries in the same order', () => {
const error = new Error('later-output')
const reads = []
const outputs = {
get entry() {
reads.push('entry')
return {
imports: [
{ kind: 'import-statement', path: 'one' },
{ kind: 'import-statement', path: 'two' }
]
}
},
get one() {
reads.push('one')
return { imports: [{ kind: 'import-statement', path: 'three' }] }
},
get two() {
reads.push('two')
throw error
}
}
let caught
try {
entryStaticClosure({ outputs }, 'entry')
} catch (failure) {
caught = failure
}
expect(caught).toBe(error)
expect(reads).toEqual(['entry', 'one', 'two'])
})
@@ -120,9 +120,10 @@ const BUNDLER_CACHE_PATHS = ['metro-cache', '.expo', 'node_modules/.cache']
const REVIEWED_COMPUTED_PATHS = [
'${{ steps.electron-package-cache.outputs.cache-root }}',
'${{ steps.pnpm-store.outputs.path }}',
'${{ env.ORCA_PNPM_STORE_CACHE_PATH }}',
// Only pnpm's lockfile-verified.jsonl record, never Metro transforms.
'${{ steps.verification-cache.outputs.path }}',
"${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && 'pnpm' || '' }} store"
"${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && steps.pnpm-store-mode.outputs.lookup-only != 'true' && 'pnpm' || '' }} store"
]
/** Every step a workflow runs, descending into the repository's own composite actions. */
@@ -0,0 +1,42 @@
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
import { parse } from 'yaml'
const workflow = parse(readFileSync('.github/workflows/mobile.yml', 'utf8'))
const packageJson = JSON.parse(readFileSync('mobile/package.json', 'utf8'))
const job = workflow.jobs.verify
const steps = job.steps
describe('mobile verification command ownership', () => {
it('runs the declared checks through installed tools without a script-time install', () => {
const production = steps.find((step) => step.name === 'Typecheck')
const tests = steps.find((step) => step.name === 'Typecheck tests (ratchet)')
expect(packageJson.scripts.typecheck).toMatch(/^tsc\b/)
expect(production.run).toBe(`node node_modules/typescript/bin/${packageJson.scripts.typecheck}`)
expect(tests.run).toBe(packageJson.scripts['check:tests-typecheck'])
expect(tests.run).toMatch(/^node\s/)
expect(job.defaults.run['working-directory']).toBe('mobile')
for (const name of ['typecheck', 'check:tests-typecheck']) {
expect(packageJson.scripts[`pre${name}`]).toBeUndefined()
expect(packageJson.scripts[`post${name}`]).toBeUndefined()
}
})
it('finishes installation and joins production types before checking test types', () => {
const installIndex = steps.findIndex((step) => step.name === 'Install dependencies')
const productionIndex = steps.findIndex((step) => step.name === 'Typecheck')
const ratchetIndex = steps.findIndex((step) => step.name === 'Typecheck tests (ratchet)')
const waitIndex = steps.findIndex((step) => step.wait === steps[productionIndex].id)
const testIndex = steps.findIndex((step) => step.name === 'Test')
expect(steps[installIndex].run).toBe('pnpm install --frozen-lockfile')
expect(steps[installIndex].background ?? false).toBe(false)
expect(installIndex).toBeLessThan(productionIndex)
expect(steps[productionIndex].background).toBe(true)
expect(productionIndex).toBeLessThan(ratchetIndex)
expect(waitIndex).toBeGreaterThan(productionIndex)
expect(waitIndex).toBeLessThan(ratchetIndex)
expect(ratchetIndex).toBeLessThan(testIndex)
})
})
@@ -611,7 +611,13 @@ for (const engine of ['chromium', 'webkit']) {
await frame?.click('#fraglink', { timeout: 2000 })
}
const shown = await open(browser(), { signal: ctx.signal, extra: tall, act: tapFragment })
const shown = await open(browser(), {
signal: ctx.signal,
extra: tall,
act: tapFragment,
reportAfterAct: 'frame-src'
})
expect(shown.actError).toBeNull()
// The precondition the whole case rests on: the base URL is the embedder's, which is what
// makes a fragment resolve off-document here.
expect(shown.inside?.baseUri ?? shown.mountedSrcDoc).toBeTruthy()
@@ -4,6 +4,8 @@
import { recordRequestsTo } from './mobile-web-app-preview-request-log.mjs'
import { watchImageEvidence } from './mobile-web-app-preview-image-evidence.mjs'
import { artifact } from './mobile-web-app-preview-artifact-fixture.mjs'
import { pollReportsUntil } from './mobile-web-app-preview-csp-reports.mjs'
import { describePreviewFrame, untilAborted } from './mobile-web-app-preview-frame-diagnosis.mjs'
import {
previewFrame,
settleAfterMount,
@@ -42,6 +44,7 @@ export async function openPreviewArm(
assets,
doctype,
reportReady = null,
reportAfterAct = null,
/** What the shell told this page it may do. Defaults to the session route's own list, so an arm
* that does not mention it measures the shipped screen (C8.1). */
grants = null,
@@ -199,6 +202,15 @@ export async function openPreviewArm(
// write, and the bounded wait says so rather than leaving a bare timeout.
actError
})
// A refusal caused by the tap arrives after mount readiness.
if (reportAfterAct && !actError) {
await untilAborted(
pollReportsUntil(cspReports, nonce, reportAfterAct, signal),
signal,
async () =>
`the policy reported no ${String(reportAfterAct)} refusal after the action: ${arm} | ${await describePreviewFrame(page, previewFrame(page), browserVersion)}`
)
}
const result = await readPreviewArm({
page,
clip,
@@ -0,0 +1,164 @@
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import * as quality from './check-changed-code-quality.mjs'
const processCalls = vi.hoisted(() => ({ execFileSync: vi.fn(), spawnSync: vi.fn() }))
// oxlint-disable-next-line anti-slop/no-module-mocking -- Test-only external process boundary; actual main executes.
vi.mock('node:child_process', () => processCalls)
// oxlint-disable-next-line anti-slop/no-module-mocking -- Resolve only the external executable used by this fixture.
vi.mock('./oxlint-cli-invocation.mjs', () => ({
resolveOxlintInvocation: () => ({ command: 'fixture-oxlint', prefixArgs: [] })
}))
afterEach(() => {
vi.restoreAllMocks()
vi.unstubAllEnvs()
})
const createMatcher = (blocks) =>
quality.createMovedCodeMatcher?.(blocks) ?? ((lines) => quality.isMovedCode(lines, blocks))
describe('moved-code base normalization budget', () => {
it('normalizes fixed base lines once across repeated changed-line diagnostics', () => {
const root = mkdtempSync(join(tmpdir(), 'orca-moved-code-budget-'))
const file = join(root, 'fixture.mjs')
const source = 'brandNewCall()\n'
writeFileSync(file, source)
const blocks = [Array.from({ length: 5000 }, (_, index) => ` base_line_${index}() `)]
const matcher = createMatcher(blocks)
const ranges = new Map([['fixture.mjs', [{ start: 1, end: 1 }]]])
const diagnostic = {
filename: file,
labels: [{ span: { line: 1, offset: 0, length: source.length - 1 } }]
}
const original = String.prototype.replace
let normalizedBaseLines = 0
const spy = vi.spyOn(String.prototype, 'replace').mockImplementation(function (...args) {
if (String(this).startsWith(' base_line_')) {
normalizedBaseLines += 1
}
return original.apply(this, args)
})
try {
const results = Array.from({ length: 100 }, () =>
quality.diagnosticTouchesAddedLines(diagnostic, ranges, root, blocks, matcher)
)
spy.mockRestore()
expect(results).toEqual(Array(100).fill(true))
expect(normalizedBaseLines).toBe(5000)
} finally {
spy.mockRestore()
rmSync(root, { recursive: true, force: true })
}
})
it('keeps the existing exemption decisions across mixed repeated highlights', () => {
const body = Array.from({ length: 20 }, (_, index) => `line${index}()`)
const blocks = [[' a() ', '', '\tb()'], body, ['first()', 'last()']]
const matcher = createMatcher(blocks)
const observations = [
{ lines: ['a()', ' ', 'b()'], moved: true },
{ lines: ['', ' '], moved: false },
{ lines: ['brandNewCall()', 'a()'], moved: false },
{ lines: ['first()', 'brandNewCall()'], moved: false },
{ lines: [...body.slice(0, 19), 'newDep,', body[19]], moved: true },
{
lines: ['line0()', ...Array.from({ length: 18 }, (_, index) => `fresh${index}()`)],
moved: false
},
{ lines: ['b()', 'a()'], moved: false }
]
for (let repeat = 0; repeat < 10; repeat += 1) {
for (const { lines, moved } of observations) {
expect(matcher(lines)).toBe(moved)
expect(quality.isMovedCode(lines, blocks)).toBe(moved)
}
}
})
it('does not normalize unvisited blocks and recomputes for the next invocation', () => {
const unused = [' base_line_unused() ']
const blocks = [['first()'], unused]
const matcher = createMatcher(blocks)
const original = String.prototype.replace
let unusedReads = 0
const spy = vi.spyOn(String.prototype, 'replace').mockImplementation(function (...args) {
if (String(this).startsWith(' base_line_')) {
unusedReads += 1
}
return original.apply(this, args)
})
try {
expect(matcher([])).toBe(false)
expect(matcher(['first()'])).toBe(true)
expect(unusedReads).toBe(0)
} finally {
spy.mockRestore()
}
blocks[0][0] = 'changed()'
const nextMatcher = createMatcher(blocks)
expect(nextMatcher(['changed()'])).toBe(true)
expect(nextMatcher(['first()'])).toBe(false)
expect(quality.isMovedCode(['changed()'], blocks)).toBe(true)
})
it('shares the matcher across diagnostics and scans in the actual main entrypoint', () => {
const root = mkdtempSync(join(tmpdir(), 'orca-moved-code-main-'))
const file = join(root, 'fixture.mjs')
writeFileSync(file, 'brandNewCall()\n')
vi.stubEnv('GITHUB_EVENT_NAME', '')
processCalls.execFileSync.mockImplementation((_command, args) => {
if (args[0] === 'rev-list') {
return 'head parent\n'
}
if (args[0] === 'merge-base') {
return 'baseline\n'
}
if (args[0] === 'ls-files') {
return ''
}
if (args.includes('--name-only')) {
return 'fixture.mjs\0'
}
if (args.includes('--unified=0')) {
return '@@ -0,0 +1 @@\n+brandNewCall()\n'
}
throw new Error(`Unexpected Git arguments: ${args.join(' ')}`)
})
const baseline = Array.from({ length: 1000 }, (_, index) => `base_line_${index}()`)
const diagnostics = Array.from({ length: 10 }, () => ({
filename: file,
message: 'New code finding',
labels: [{ span: { line: 1, offset: 0, length: 14 } }]
}))
processCalls.spawnSync.mockImplementation((command, args) => {
if (command === 'git') {
return { status: 0, stdout: args[0] === 'show' ? baseline.join('\n') : '' }
}
return { status: 1, stdout: JSON.stringify({ diagnostics }), stderr: '' }
})
const error = vi.spyOn(console, 'error').mockImplementation(() => {})
vi.spyOn(console, 'log').mockImplementation(() => {})
const replace = String.prototype.replace
let normalizedBaseLines = 0
vi.spyOn(String.prototype, 'replace').mockImplementation(function (...args) {
if (String(this).startsWith('base_line_')) {
normalizedBaseLines += 1
}
return replace.apply(this, args)
})
try {
expect(quality.main(root, 'baseline')).toBe(1)
const scans = processCalls.spawnSync.mock.calls.filter(([command]) => command !== 'git')
expect(scans).toHaveLength(quality.OXLINT_SCANS.length)
expect(error.mock.calls.filter(([message]) => message.startsWith('::error '))).toHaveLength(
diagnostics.length * quality.OXLINT_SCANS.length
)
expect(normalizedBaseLines).toBe(2000)
} finally {
rmSync(root, { recursive: true, force: true })
}
})
})
+14
View File
@@ -0,0 +1,14 @@
// FileTracker's long-path-unsafe .tlog files serve incremental builds; these rebuilds are forced.
export function disableMsbuildFileTrackingOnWindows(
env = process.env,
platform = process.platform
) {
// Windows environment keys are case-insensitive, including caller overrides in copied objects.
if (
platform === 'win32' &&
!Object.keys(env).some((key) => key.toLowerCase() === 'trackfileaccess')
) {
env.TrackFileAccess = 'false'
}
return env
}
@@ -0,0 +1,28 @@
import { describe, expect, it } from 'vitest'
import { disableMsbuildFileTrackingOnWindows } from './msbuild-file-tracking.mjs'
describe('disableMsbuildFileTrackingOnWindows', () => {
it('turns tracking off on Windows when the caller left it unset', () => {
expect(disableMsbuildFileTrackingOnWindows({ PATH: 'x' }, 'win32')).toEqual({
PATH: 'x',
TrackFileAccess: 'false'
})
})
it.each(['TrackFileAccess', 'trackfileaccess', 'TRACKFILEACCESS', 'tRaCkFiLeAcCeSs'])(
'preserves explicit %s values without adding a duplicate key',
(key) => {
for (const value of ['true', 'false', '']) {
const env = { [key]: value }
expect(disableMsbuildFileTrackingOnWindows(env, 'win32')).toBe(env)
expect(env).toEqual({ [key]: value })
}
}
)
it.each(['linux', 'darwin'])('leaves %s hosts alone', (platform) => {
const env = { PATH: 'x' }
expect(disableMsbuildFileTrackingOnWindows(env, platform)).toBe(env)
expect(env).toEqual({ PATH: 'x' })
})
})
@@ -31,12 +31,14 @@ const ALWAYS_FILES = new Set([
'.github/workflows/node-server-tests.yml',
'config/scripts/node-server-change-scope.mjs',
'config/scripts/node-server-change-scope.test.mjs',
'config/scripts/headless-detector-compiler-cache.mjs',
'config/scripts/node-server-qualification.mjs',
'config/scripts/node-server-qualification.test.mjs'
])
const ALWAYS_PREFIXES = [
'.github/actions/install-node-dependencies/',
'.github/actions/restore-pnpm-verification/',
'.github/actions/prepare-headless-compiler/',
'.github/actions/prepare-native-runtime/',
'.github/actions/prepare-orcad-prebuilds/',
// These areas also contain worker paths and fixtures opened without an import.
@@ -86,6 +86,8 @@ it.each([
'native/windows-registry/src/addon.cc',
'.github/actions/install-node-dependencies/action.yml',
'.github/actions/restore-pnpm-verification/action.yml',
'.github/actions/prepare-headless-compiler/action.yml',
'config/scripts/headless-detector-compiler-cache.mjs',
'.github/actions/prepare-native-runtime/action.yml',
'.github/actions/prepare-orcad-prebuilds/action.yml',
'.github/workflows/node-server-tests.yml',
@@ -174,9 +176,19 @@ describe('the actual Bun build and profile-test dependency graph', () => {
inputs = await collectNodeServerInputs()
}, 60_000)
it('tracks the shared close probe without pulling in its mocked renderer adapter', () => {
expect(inputs.has('src/shared/pty-running-work-probe.ts')).toBe(true)
expect(inputs.has('src/shared/pty-running-work-probe.test.ts')).toBe(true)
expect(inputs.has('src/renderer/src/components/terminal/pty-running-work-probe.ts')).toBe(false)
expect(inputs.has('src/renderer/src/runtime/runtime-terminal-inspection.ts')).toBe(false)
expect([...inputs].some((file) => file.startsWith('src/renderer/'))).toBe(false)
})
it.each([
'config/scripts/ci-shard-timings.json',
'config/scripts/mobile-web-app-terminal-render.test.mjs',
'src/renderer/src/components/terminal/pty-running-work-probe.ts',
'src/renderer/src/runtime/runtime-terminal-inspection.ts',
'src/main/ssh/ssh-relay-upload-stage-commands.test.ts',
'src/main/menu/register-app-menu.ts'
])('skips unrelated work: %s', async (file) => {
@@ -186,6 +198,8 @@ describe('the actual Bun build and profile-test dependency graph', () => {
it.each([
...Object.values(ORCAD_CHILD_ENTRY_POINTS),
'src/shared/keybindings/definitions-core-1.ts',
'src/shared/pty-running-work-probe.ts',
'src/shared/pty-running-work-probe.test.ts',
'src/main/runtime/orca-runtime.ts',
'src/main/windows/windows-process-table.ts',
'src/main/worker-thread-entry-path.ts',
@@ -290,14 +304,29 @@ it('runs the Bun and Node cross-runtime tests on Linux against pinned inputs', (
expect(setupBun.with['bun-version']).toBe('1.4.2')
const build = steps.find((step) => String(step.run).includes('build-orcad-bun.mjs'))
expect(build.env.BUN_ORCAD_COMMIT).toMatch(/^[0-9a-f]{40}$/)
expect(build.run).toContain('ORCA_BUN_ORCAD_SLOT=')
expect(build.run).toContain('BUN_EXECUTABLE=')
for (const step of [setupBun, build]) {
expect(step.if).toBe("runner.os == 'Linux'")
}
expect(steps.map((step) => step.run).join('\n')).toContain(
expect(setupBun.if).toBe("runner.os == 'Linux'")
expect(build.id).toBe('bun-orcad')
expect(build.background).toBe(true)
expect(build.if).toBeUndefined()
expect(build['continue-on-error']).toBeUndefined()
expect(build.run).toMatch(/^if \[ "\$RUNNER_OS" != Linux \]; then exit 0; fi\n/)
expect(build.run).toContain('echo "slot=$RUNNER_TEMP/bun-orcad" >> "$GITHUB_OUTPUT"')
expect(build.run).toContain('echo "executable=$(command -v bun)" >> "$GITHUB_OUTPUT"')
expect(build.run).not.toContain('GITHUB_ENV')
const join = steps.findIndex((step) => step.wait === build.id)
expect(join).toBeGreaterThan(steps.indexOf(build))
expect(steps[join].if).toBeUndefined()
expect(steps[join]['continue-on-error']).toBeUndefined()
const consumer = steps.find((step) => step.run?.startsWith('pnpm test:node-server --artifact '))
expect(steps.indexOf(consumer)).toBeGreaterThan(join)
expect(consumer.run).toBe(
"pnpm test:node-server --artifact ${{ runner.os == 'Linux' && '--cross-runtime' || '' }}"
)
expect(consumer.if).toBeUndefined()
expect(consumer.env).toEqual({
ORCA_BUN_ORCAD_SLOT: '${{ steps.bun-orcad.outputs.slot }}',
BUN_EXECUTABLE: '${{ steps.bun-orcad.outputs.executable }}'
})
const alpine = workflow.jobs.linux_musl.steps.find((step) =>
String(step.run).includes('docker run')
)
@@ -21,6 +21,7 @@ export function nodeServerTestPaths({ artifact = false, crossRuntime = false } =
...(artifact
? [
'tests/e2e/daemon-running-work-probe.unit.test.ts',
'src/shared/pty-running-work-probe.test.ts',
'src/main/orcad/orcad-packaged-node-pty.integration.test.ts',
'src/main/providers/agent-foreground-process-git-bash.win32.test.ts',
'src/main/orcad/orcad-node-launcher.integration.test.ts',
+2 -1
View File
@@ -8,7 +8,8 @@ export const ORCAD_CHILD_ENTRY_POINTS = {
watcher: 'src/main/ipc/parcel-watcher-process-entry.ts',
daemon: 'src/main/daemon/daemon-entry.ts',
writer: 'src/main/persistence/profile-state/profile-state-writer-worker-entry.ts',
backup: 'src/main/persistence/profile-state/profile-state-backup-worker-entry.ts'
backup: 'src/main/persistence/profile-state/profile-state-backup-worker-entry.ts',
foreignSqliteReader: 'src/main/foreign-sqlite-readers/foreign-sqlite-reader-entry.ts'
}
export const ORCAD_EXTERNAL_MODULES = ['electron', 'node-pty', '@parcel/watcher', 'fsevents']
+97 -4
View File
@@ -2,7 +2,7 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import type { Plugin, Rollup } from 'vite'
import { afterEach, describe, expect, it } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
CLI_MAIN_ENTRY_NAMES,
createPlainNodeEntryGuardPlugin,
@@ -160,14 +160,20 @@ describe('guarded entry names', () => {
// hand-written "must stay electron-free" comments, and the port-scan worker sits
// one import away from a client that deliberately does require electron.
describe('CLI and worker thread entry guard', () => {
function runEntryWriteBundle(plugin: Plugin, bundle: Rollup.OutputBundle): void {
function runEntryWriteBundle(
plugin: Plugin,
bundle: Rollup.OutputBundle,
watchMode = false
): void {
const hook = plugin.writeBundle
if (typeof hook !== 'function') {
throw new Error('Expected writeBundle hook')
}
hook.call(
{ meta: { watchMode: false } } as never,
{ dir: createOutputDir() } as Rollup.NormalizedOutputOptions,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This hook reads only meta.watchMode from its context.
{ meta: { watchMode } } as never,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This hook reads only dir from the output options.
{ dir: outputDir ?? createOutputDir() } as Rollup.NormalizedOutputOptions,
bundle
)
}
@@ -184,6 +190,93 @@ describe('CLI and worker thread entry guard', () => {
} as Rollup.OutputChunk
}
function countElectronRequireScans(run: () => void): number {
const pattern = /require\(\s*["'`]electron(?:\/[^"'`]+)?["'`]\s*\)/
const originalTest = RegExp.prototype.test
let scans = 0
const spy = vi.spyOn(RegExp.prototype, 'test').mockImplementation(function (
this: RegExp,
value: string
) {
if (this.source === pattern.source) {
scans += 1
}
return originalTest.call(this, value)
})
try {
run()
} finally {
spy.mockRestore()
}
return scans
}
it('scans shared code once across every guarded entry', () => {
const shared = entryChunk('shared', 'require("node:fs")')
shared.isEntry = false
const bundle: Rollup.OutputBundle = { [shared.fileName]: shared }
for (const name of GUARDED_ENTRY_NAMES) {
const entry = entryChunk(name, `require("./shared.js"); // ${name}`, [shared.fileName])
bundle[entry.fileName] = entry
}
const snapshot = structuredClone(bundle)
const scans = countElectronRequireScans(() => {
runEntryWriteBundle(createPlainNodeEntryGuardPlugin(), bundle)
})
expect(bundle).toEqual(snapshot)
expect(scans).toBe(GUARDED_ENTRY_NAMES.length + 1)
})
it('does not retain a successful scan across output bundles', () => {
const plugin = createPlainNodeEntryGuardPlugin()
const entry = entryChunk('stt-worker', 'require("node:fs")')
const bundle: Rollup.OutputBundle = { [entry.fileName]: entry }
const scans = countElectronRequireScans(() => {
runEntryWriteBundle(plugin, bundle)
runEntryWriteBundle(plugin, bundle)
})
expect(scans).toBe(2)
entry.code = 'require("electron/main")'
expect(() => runEntryWriteBundle(plugin, bundle)).toThrow(
'[plain-node-entry-guard] "stt-worker" reaches chunk "stt-worker.js" that requires electron. '
)
})
it('still reads changed code on the same chunk within one bundle scan', () => {
const first = entryChunk('stt-worker', 'require("node:fs")')
const second = entryChunk('warp-theme-parser-worker', 'require("node:fs")')
const shared = entryChunk('shared', '')
shared.isEntry = false
let reads = 0
Object.defineProperty(shared, 'code', {
get: () => (++reads === 1 ? 'require("node:fs")' : 'require("electron")')
})
first.imports = [shared.fileName]
second.dynamicImports = [shared.fileName]
const bundle: Rollup.OutputBundle = {
[first.fileName]: first,
[second.fileName]: second,
[shared.fileName]: shared
}
expect(() => runEntryWriteBundle(createPlainNodeEntryGuardPlugin(), bundle)).toThrow(
'[plain-node-entry-guard] "warp-theme-parser-worker" reaches chunk "shared.js"'
)
expect(reads).toBe(2)
})
it('keeps watch mode free of entry scanning', () => {
const entry = entryChunk('stt-worker', 'require("electron")')
const bundle: Rollup.OutputBundle = { [entry.fileName]: entry }
const scans = countElectronRequireScans(() => {
runEntryWriteBundle(createPlainNodeEntryGuardPlugin(), bundle, true)
})
expect(scans).toBe(0)
})
it.each(CLI_MAIN_ENTRY_NAMES)('rejects direct and transitive Electron imports in %s', (name) => {
const plugin = createPlainNodeEntryGuardPlugin()
const entry = entryChunk(name, 'require("electron")')
@@ -0,0 +1,39 @@
import { describe, expect, it } from 'vitest'
import { classifyPrJobs } from './pr-code-change-scope.mjs'
// #24901 changed the send builders and orchestration code, and this job skipped. It runs only for
// code a suite executes: loading a module the dispatcher registers is not coverage.
describe('cross-version wire routing for the send path', () => {
it.each([
'src/shared/agent-session-wire-refusals.ts',
'src/shared/structured-agent-session-mutation.ts',
'src/shared/structured-agent-session-send-mutation.ts',
'src/shared/structured-agent-session-outbox.ts',
'src/main/runtime/rpc/core.ts',
'src/main/runtime/rpc/errors.ts',
'src/main/runtime/rpc/rpc-streaming-dispatcher.ts',
'src/main/runtime/rpc/orchestration-contract-fence.ts',
'src/main/runtime/rpc/orchestration-session-caller.ts',
'src/main/runtime/rpc/orchestration-legacy-compatibility.ts',
'src/main/runtime/rpc/orchestration-mutation-executor.ts',
'src/shared/orchestration-rpc-contract.ts',
'src/main/runtime/orchestration/db/schema/migrate.ts'
])('runs the cross-version suites when %s changes', (file) => {
expect(classifyPrJobs([file])).toMatchObject({ should_run: true, 'cross-version-wire': true })
})
it.each([
'src/shared/structured-agent-session-outbox-admission.ts',
'src/shared/structured-agent-session-outbox-delivery.ts',
'src/shared/structured-agent-session-outbox-stop-withdrawal.ts',
'src/shared/structured-agent-session-composer.ts',
'src/shared/structured-agent-session-reducer.ts',
'src/main/runtime/orchestration/send-agent-turn.ts',
'src/main/runtime/orchestration/orchestration-caller-identity.ts',
'src/main/runtime/rpc/orchestration-legacy-mail.ts',
'src/main/runtime/rpc/methods/orchestration.ts',
'src/main/runtime/rpc/methods/orchestration/runs/dispatch-methods.ts'
])('leaves them off for %s, which no cross-version suite executes', (file) => {
expect(classifyPrJobs([file])).toMatchObject({ should_run: true, 'cross-version-wire': false })
})
})
+17
View File
@@ -165,6 +165,11 @@ const CROSS_VERSION_WIRE_PREFIXES = [
'src/shared/rpc-contract/agent-launch-params',
'src/shared/agent-session-wire',
'src/shared/agent-session-mutation-envelope',
// The send a client builds (the agent-session suite sends it to the release host) and the
// fingerprint the host's ledger and journal re-derive.
'src/shared/structured-agent-session-mutation.ts',
'src/shared/structured-agent-session-send-mutation.ts',
'src/shared/structured-agent-session-outbox.ts',
'src/shared/agent-session-record',
'src/shared/agent-session-journal-',
'src/main/ai-vault/structured-session-ownership.ts',
@@ -174,6 +179,15 @@ const CROSS_VERSION_WIRE_PREFIXES = [
'src/main/runtime/agent-session-recovery-capsule',
'src/shared/agent-session-resume-marker',
'src/main/runtime/rpc/dispatcher',
// Run on every request the suites dispatch, whatever its method.
'src/main/runtime/rpc/core.ts',
'src/main/runtime/rpc/errors.ts',
'src/main/runtime/rpc/rpc-streaming-dispatcher.ts',
'src/main/runtime/rpc/orchestration-contract-fence.ts',
'src/main/runtime/rpc/orchestration-session-caller.ts',
'src/main/runtime/rpc/orchestration-legacy-compatibility.ts',
'src/main/runtime/rpc/orchestration-mutation-executor.ts',
'src/shared/orchestration-rpc-contract.ts',
'src/main/runtime/rpc/methods/agent-launch',
'src/main/runtime/rpc/methods/ai-vault.ts',
'src/main/runtime/rpc/methods/browser-tab-create-schema',
@@ -326,6 +340,7 @@ const WINDOWS_PACKAGE_TESTS = [
'src/shared/child-process/windows-cmd-shim-resolution.test.ts',
'src/shared/child-process/windows-cmd-shim-resolution.win32.test.ts',
'src/main/agent-hooks/windows-hook-payload-delivery.test.ts',
'src/main/jcode/hook-gate-script.test.ts',
'src/main/agent-hooks/windows-direct-cmd-hook-command.test.ts',
'src/main/codex/windows-hook-command.test.ts',
'src/main/codex/windows-hook-upgrade.test.ts',
@@ -356,6 +371,8 @@ const WINDOWS_PACKAGE_TESTS = [
'src/main/runtime/unreadable-secret-store-preservation.win32.test.ts',
'src/main/ipc/pty-codex-account-attribution.test.ts',
'src/main/ipc/pty-spawn-env-codex-resume-provenance.test.ts',
'src/main/ipc/preflight-provider-command-selection.test.ts',
'src/main/ipc/preflight-runnable-local-cli.test.ts',
'src/relay/windows-port-scan.win32.test.ts',
'src/main/ssh/ssh-relay-upload-stage-windows-identity.test.ts',
'src/main/ssh/remote-node-runtime-store-windows.test.ts'
+5 -3
View File
@@ -1,4 +1,5 @@
import { DEDICATED_E2E_SPECS } from './ci-e2e-job-selection.mjs'
import { linuxInstallPackageList } from './pr-e2e-linux-packages.test-fixture.mjs'
import { existsSync, readdirSync, readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { parse as parseJsonc } from 'jsonc-parser'
@@ -20,6 +21,7 @@ import {
const projectDir = resolve(import.meta.dirname, '../..')
const prWorkflow = parseYaml(readFileSync(join(projectDir, '.github/workflows/pr.yml'), 'utf8'))
const e2eWorkflow = parseYaml(readFileSync(join(projectDir, '.github/workflows/e2e.yml'), 'utf8'))
const reliabilityManifest = parseJsonc(
readFileSync(join(projectDir, 'config/reliability-gates.jsonc'), 'utf8')
)
@@ -218,7 +220,7 @@ describe('PR E2E gate contract', () => {
const installStep = e2eWorkflow.jobs[jobName].steps.find((step) =>
step.name.startsWith('Install native build')
)
expect(installStep.env.ORCA_E2E_APT_PACKAGES.split(/\s+/), jobName).toContain('zsh')
expect(linuxInstallPackageList(installStep, jobName), jobName).toMatch(/(^|\s)zsh(\s|$)/)
}
})
@@ -305,10 +307,10 @@ describe('PR E2E gate contract', () => {
// Why: this lane can now pay a Docker image build plus serial SSH specs.
expect(e2eWorkflow.jobs['changed-e2e']['timeout-minutes']).toBeGreaterThanOrEqual(45)
const changedInstall = e2eWorkflow.jobs['changed-e2e'].steps.find((step) =>
const install = e2eWorkflow.jobs['changed-e2e'].steps.find((step) =>
step.name.startsWith('Install native build')
)
expect(changedInstall.env.ORCA_E2E_APT_PACKAGES.split(/\s+/)).toContain('openssh-client')
expect(linuxInstallPackageList(install, 'changed-e2e')).toMatch(/(^|\s)openssh-client(\s|$)/)
})
it('routes direct-SSH workspace and tab restore from its unnamed source seams', () => {
@@ -0,0 +1,10 @@
import { expect } from 'vitest'
export function linuxInstallPackageList(step, jobName) {
const packages = step.env?.ORCA_E2E_APT_PACKAGES
if (packages !== undefined) {
expect(step.run, jobName).toContain('read -r -a packages <<< "$ORCA_E2E_APT_PACKAGES"')
expect(step.run, jobName).toContain('sudo apt-get install -y "${packages[@]}"')
}
return packages ?? step.run
}
@@ -1,4 +1,5 @@
import { existsSync, globSync, readFileSync } from 'node:fs'
import { runInNewContext } from 'node:vm'
import { parse } from 'yaml'
import { describe, expect, it } from 'vitest'
import { UNIT_EXCLUDE } from './ci-unit-files.mjs'
@@ -290,7 +291,7 @@ describe('PR workflow parallelism', () => {
expect(steps[pnpmIndex].with.version).toBeUndefined()
expect(steps[pnpmIndex].with.install).toBe(false)
const saveOutsidePrs =
"${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && 'pnpm' || '' }}"
"${{ github.event_name != 'pull_request' && inputs.cache-pnpm-store != 'false' && steps.pnpm-store-mode.outputs.lookup-only != 'true' && 'pnpm' || '' }}"
expect(steps[nodeIndex].with.cache).toBe(saveOutsidePrs)
expect(steps[nodeIndex].if).toBe("inputs.node-version == ''")
expect(steps[requestedNodeIndex].if).toBe("inputs.node-version != ''")
@@ -305,7 +306,7 @@ describe('PR workflow parallelism', () => {
)
expect(steps[restoreIndex].uses).toBe('actions/cache/restore@v5')
expect(steps[restoreIndex].if).toBe(
"github.event_name == 'pull_request' && inputs.cache-pnpm-store != 'false' && !(runner.os == 'Linux' && (runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml') && (runner.os != 'Windows' || !(runner.arch == 'X64' && contains(inputs.cache-dependency-path, 'mobile/pnpm-lock.yaml')) && !((runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml'))"
"github.event_name == 'pull_request' && inputs.cache-pnpm-store != 'false' && !((runner.os == 'Linux' || runner.os == 'macOS') && (runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml') && (runner.os != 'Windows' || !(runner.arch == 'X64' && contains(inputs.cache-dependency-path, 'mobile/pnpm-lock.yaml')) && !((runner.arch == 'X64' || runner.arch == 'ARM64') && inputs.cache-dependency-path == 'pnpm-lock.yaml'))"
)
})
@@ -515,6 +516,21 @@ describe('PR workflow parallelism', () => {
const evidence = workflow.jobs.unit_selection_evidence
expect(evidence.uses).toBe('./.github/workflows/unit-selection-evidence.yml')
expect(evidence.needs).toEqual(['test'])
for (const [result, cancelled, expected] of [
['success', false, true],
['failure', false, true],
['skipped', false, false],
['cancelled', false, false],
['success', true, false],
['failure', true, false]
]) {
expect(
runInNewContext(evidence.if.slice(3, -2), {
cancelled: () => cancelled,
needs: { test: { result } }
})
).toBe(expected)
}
expect(workflow.jobs.verify.needs).not.toContain('unit_selection_evidence')
expect(unitTestWorkflow.jobs.selection_evidence).toBeUndefined()
const evidenceWorkflow = parse(
@@ -22,6 +22,51 @@ import {
} from './rebuild-native-deps-test-fixtures.mjs'
describe('rebuild-native-deps patched node-pty rebuild', () => {
it.skipIf(process.platform !== 'win32')(
'passes the Windows tracking default and explicit overrides to forced Electron rebuilds',
() => {
const projectDir = mkTempProject()
try {
const rebuildLogPath = join(projectDir, 'electron-rebuild.log')
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' })
writeFakeLoadableNodePty(projectDir)
writeFakeWindowsProcessTree(projectDir)
writeFakeNodePtyConptyPayload(projectDir, process.arch)
const env = {
ORCA_REBUILD_TEST_LOG: rebuildLogPath,
npm_config_platform: 'win32',
npm_config_arch: process.arch
}
for (const override of [
{},
{ TrackFileAccess: 'true' },
{ trackfileaccess: 'true' },
{ tRaCkFiLeAcCeSs: 'false' }
]) {
const result = runRebuildScript(projectDir, { ...env, ...override })
expect(result.status, result.stderr).toBe(0)
}
const calls = readFileSync(rebuildLogPath, 'utf8')
.trim()
.split('\n')
.map((line) => JSON.parse(line))
expect(calls.map((call) => call.trackFileAccess)).toEqual([
'false',
'true',
'true',
'false'
])
expect(calls.every((call) => call.force)).toBe(true)
} finally {
removeTreeSync(projectDir)
}
}
)
it.skipIf(process.platform !== 'win32')(
'repairs a missing ConPTY runtime before probing without recompiling node-pty',
() => {
@@ -110,7 +110,7 @@ export function writeWindowsProcessTreePatchFile(projectDir) {
export function mkTempProject() {
const projectDir = mkdtempSync(join(tmpdir(), 'orca-rebuild-native-deps-'))
mkdirSync(join(projectDir, 'config', 'scripts'), { recursive: true })
copyFileSync(sourceScriptPath, join(projectDir, 'config', 'scripts', 'rebuild-native-deps.mjs'))
copyScriptWithLocalModules(sourceScriptPath, join(projectDir, 'config', 'scripts'))
copyScriptWithLocalModules(sourceInstallScriptPath, join(projectDir, 'config', 'scripts'))
copyScriptWithLocalModules(sourceNodePtyJobOwnershipPath, join(projectDir, 'config', 'scripts'))
copyFileSync(
@@ -134,7 +134,8 @@ export function runRebuildScript(projectDir, extraEnv = {}, args = []) {
for (const key of Object.keys(env)) {
if (
key.toLowerCase() === 'orca_strict_electron_install' ||
key.toLowerCase() === 'npm_lifecycle_event'
key.toLowerCase() === 'npm_lifecycle_event' ||
key.toLowerCase() === 'trackfileaccess'
) {
delete env[key]
}
@@ -286,6 +287,7 @@ export async function rebuild(options) {${emitAddon}
electronVersion: options.electronVersion,
force: options.force,
ignoreModules: options.ignoreModules,
trackFileAccess: process.env.TrackFileAccess ?? null,
onlyModules: options.onlyModules,
platform: options.platform
}) + '\\n'
+2
View File
@@ -26,6 +26,7 @@ import {
stageWindowsProcessTreeNodeAddonApiHeaders,
windowsProcessTreeAddonPath
} from './windows-process-tree-gyp-rebuild.mjs'
import { disableMsbuildFileTrackingOnWindows } from './msbuild-file-tracking.mjs'
import {
copyFileSync,
existsSync,
@@ -162,6 +163,7 @@ try {
console.warn('[rebuild] Repaired the un-applied windows-process-tree command-line patch.')
}
}
disableMsbuildFileTrackingOnWindows()
await rebuild({
buildPath: projectDir,
electronVersion,
@@ -198,7 +198,7 @@ function resolveLaunch(userDataDir) {
label: `electron (${serveEntry})`,
command: override ?? 'npx',
args: override ? serveArgs : ['electron', ...serveArgs],
env: {}
env: { ORCA_DEV_USER_DATA_PATH: userDataDir }
}
}
@@ -0,0 +1,110 @@
import { describe, expect, it } from 'vitest'
import { selectLatestStableReleaseTag } from './stable-release-tags.mjs'
function expectedLatest(tags) {
let latest = null
let latestParts = []
for (const tag of tags) {
const match = /^v(\d+)\.(\d+)\.(\d+)$/.exec(tag)
if (!match) {
continue
}
const parts = match.slice(1).map((part) => {
const value = Number.parseInt(part, 10)
return Number.isFinite(value) ? value : 0
})
let comparison = 0
for (let index = 0; index < 3 && comparison === 0; index++) {
comparison = parts[index] - (latestParts[index] ?? 0)
}
if (latest === null || comparison >= 0) {
latest = tag
latestParts = parts
}
}
return latest
}
function measurePartMaps(tags) {
const nativeMap = Array.prototype.map
let partMaps = 0
Array.prototype.map = function (callback, thisArg) {
partMaps++
return nativeMap.call(this, callback, thisArg)
}
let result
try {
result = selectLatestStableReleaseTag(tags)
} finally {
Array.prototype.map = nativeMap
}
return { result, partMaps }
}
describe('stable release tag selection work', () => {
it.each([0, 1, 12, 128, 1000])('bounds numeric parsing for %i Git tag strings', (count) => {
const tags = Array.from(
{ length: count },
(_, index) => `v1.${(index * 37) % 17}.${(index * 101) % (count + 1)}`
)
const input = [...tags]
const expected = expectedLatest(tags)
const measured = measurePartMaps(Object.freeze(tags))
expect(measured.result).toBe(expected)
expect(tags).toEqual(input)
// Each unchanged comparator converts the two triples through four maps.
expect(measured.partMaps).toBeLessThanOrEqual(4 * Math.max(0, count - 1))
})
it('preserves tie spelling, numeric fallback and invalid-tag admission', () => {
const cases = [
{ tags: [], expected: null },
{ tags: ['nightly', 'mobile-v1.2.3', 'v1.2.3-rc.1'], expected: null },
{ tags: ['v0001.4.003', 'v1.04.3'], expected: 'v1.04.3' },
{ tags: ['v1.04.3', 'v0001.4.003'], expected: 'v0001.4.003' },
{ tags: ['v1.2.3', 'v1.2.3\n'], expected: 'v1.2.3' },
{ tags: ['v1.2.3\r\n', 'v0.0.1'], expected: 'v0.0.1' },
{ tags: [`v${'9'.repeat(400)}.1.2`, 'v0.1.2'], expected: 'v0.1.2' },
{
tags: ['v9007199254740993.1.0', 'v9007199254740992.1.0'],
expected: 'v9007199254740992.1.0'
}
]
for (const { tags, expected } of cases) {
const input = [...tags]
expect(selectLatestStableReleaseTag(Object.freeze(tags))).toBe(expected)
expect(tags).toEqual(input)
}
const sparse = []
sparse.length = 12
sparse[3] = 'v1.2.3'
sparse[8] = 'v2.0.0'
expect(selectLatestStableReleaseTag(Object.freeze(sparse))).toBe('v2.0.0')
})
it('selects the same latest spelling across ordinary version namespaces and repeated calls', () => {
let randomState = 673151
const next = () => {
randomState = (randomState * 1664525 + 1013904223) >>> 0
return randomState
}
for (let seed = 0; seed < 256; seed++) {
const tags = []
for (let index = 0, count = next() % 129; index < count; index++) {
const triple = [next() % 13, next() % 17, next() % 257]
const prefix = next() % 8 === 0 ? 'mobile-v' : 'v'
const suffix = next() % 9 === 0 ? '-rc.1' : ''
tags.push(`${prefix}${triple.join('.')}${suffix}`)
if (index % 11 === 0) {
tags.push(tags.at(-1))
}
}
const input = [...tags]
const expected = expectedLatest(tags)
expect(selectLatestStableReleaseTag(tags)).toBe(expected)
expect(tags).toEqual(input)
tags.push('v99.99.99')
expect(selectLatestStableReleaseTag(tags)).toBe('v99.99.99')
}
})
})
+10 -6
View File
@@ -21,10 +21,14 @@ export function compareReleaseTags(a, b) {
/** @param {string[]} tags @returns {string | null} */
export function selectLatestStableReleaseTag(tags) {
return (
tags
.filter((tag) => STABLE_DESKTOP_RELEASE_TAG.test(tag))
.sort(compareReleaseTags)
.at(-1) ?? null
)
let latest = null
for (const tag of tags) {
if (
STABLE_DESKTOP_RELEASE_TAG.test(tag) &&
(latest === null || compareReleaseTags(latest, tag) <= 0)
) {
latest = tag
}
}
return latest
}
@@ -0,0 +1,116 @@
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { runInNewContext } from 'node:vm'
import { parse } from 'yaml'
import { expect, it } from 'vitest'
import { runProcessSync } from './script-child-process.mjs'
const workflow = parse(readFileSync('.github/workflows/terminal-perf.yml', 'utf8'))
const steps = workflow.jobs['terminal-perf'].steps
const selector = steps.find((step) => step.id === 'install-mode')
const script = selector.run.trim().match(/^node <<'NODE'\n([\s\S]*)\nNODE$/)[1]
const supportedAction = readFileSync('.github/actions/install-node-dependencies/action.yml', 'utf8')
const supportedManifest = {
engines: { node: '24' },
packageManager: 'pnpm@12.8.1',
scripts: { postinstall: 'node config/scripts/rebuild-native-deps.mjs' }
}
function select(options = {}) {
const directory = mkdtempSync(join(tmpdir(), 'orca-terminal-preparation-'))
const output = join(directory, 'output')
try {
writeFileSync(
join(directory, 'package.json'),
JSON.stringify(options.manifest ?? supportedManifest)
)
for (const [file, content] of [
['.github/actions/install-node-dependencies/action.yml', options.action ?? supportedAction],
['.github/actions/prepare-native-runtime/action.yml', 'runs: {}'],
['config/scripts/ensure-native-runtime.mjs', '']
]) {
if (options.missing === file) {
continue
}
const path = join(directory, file)
mkdirSync(dirname(path), { recursive: true })
writeFileSync(path, content)
}
const result = runProcessSync({
program: process.execPath,
args: ['-e', script],
cwd: directory,
env: {
...process.env,
GITHUB_OUTPUT: output,
RUNNER_KIND: options.kind ?? 'github-hosted',
JOB_CONTAINER: options.container ?? '',
RUNNER_OS: options.os ?? 'Linux',
RUNNER_ARCH: options.arch ?? 'X64'
}
})
expect(result.code, result.stderr || result.stdout).toBe(0)
return readFileSync(output, 'utf8').trim()
} finally {
rmSync(directory, { recursive: true, force: true })
}
}
it('selects the measured current root profile with the actual installer metadata', () => {
expect(select()).toBe('shared=true')
expect(
select({ manifest: { ...supportedManifest, packageManager: 'pnpm@12.8.1+sha512.fixture' } })
).toBe('shared=true')
})
it.each([
['historical Node', { manifest: { ...supportedManifest, engines: { node: '22' } } }],
['historical pnpm', { manifest: { ...supportedManifest, packageManager: 'pnpm@10.0.0' } }],
['unmeasured pnpm', { manifest: { ...supportedManifest, packageManager: 'pnpm@12.8.10' } }],
['missing toolchain', { manifest: {} }],
[
'extra lifecycle work',
{ manifest: { ...supportedManifest, scripts: { postinstall: 'generate' } } }
],
['self-hosted runner', { kind: 'self-hosted' }],
['job container', { container: 'container-id' }],
['another OS', { os: 'Windows' }],
['another architecture', { arch: 'ARM64' }],
['missing installer', { missing: '.github/actions/install-node-dependencies/action.yml' }],
['missing native action', { missing: '.github/actions/prepare-native-runtime/action.yml' }],
['missing runtime script', { missing: 'config/scripts/ensure-native-runtime.mjs' }],
['old installer interface', { action: 'inputs:\n native-runtime: {}\nruns: {}\n' }],
[
'output-only names',
{ action: 'outputs:\n native-runtime: {}\n cache-pnpm-store-lookup-only: {}\n' }
]
])('retains the original install for %s', (_name, options) => {
expect(select(options)).toBe('shared=false')
})
it.each(['true', 'false', ''])('routes mode %s to one complete preparation path', (shared) => {
const enabled = (step) =>
runInNewContext(step.if.replaceAll('steps.install-mode.outputs.shared', 'shared'), { shared })
const current = steps.find((step) => step.name === 'Prepare current dependencies')
const legacy = steps.filter((step) =>
[
'Setup pnpm',
'Setup Node.js',
"Use external node-gyp to avoid pnpm's bundled copy",
'Install dependencies'
].includes(step.name)
)
expect(legacy).toHaveLength(4)
expect(enabled(current)).toBe(shared === 'true')
expect(legacy.every((step) => enabled(step) === (shared !== 'true'))).toBe(true)
expect(current.with).toEqual({
'native-runtime': 'electron',
'cache-electron-package': 'true',
'cache-pnpm-store-lookup-only': 'true'
})
expect(legacy.at(-1).run).toBe('pnpm install --frozen-lockfile')
expect(steps.find((step) => step.name === 'Run terminal scale perf report gate').run).toContain(
'pnpm run test:e2e:terminal-perf:scale:report'
)
})
@@ -4,7 +4,10 @@ import { writeFile } from 'node:fs/promises'
import { homedir, tmpdir, userInfo } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { takeRealAgentHomeWriteViolations } from './vitest-real-agent-home-write-guard'
import {
clearInheritedAgentStateEnv,
takeRealAgentHomeWriteViolations
} from './vitest-real-agent-home-write-guard'
// Why never-created paths: each sits under a missing folder or is a forced no-op removal, so even
// with the guard off (the ablation) nothing lands in the real home.
@@ -17,6 +20,29 @@ afterEach(() => {
})
describe('vitest real-agent-home write guard', () => {
it.each([undefined, '', '0', 'true', '1'])(
'keeps only the explicitly opted-in Claude profile for %s',
(value) => {
vi.stubEnv('ORCA_REAL_CLAUDE_CLI_TEST', value)
vi.stubEnv('CLAUDE_CONFIG_DIR', '/tmp/explicit-claude-profile')
vi.stubEnv('CODEX_HOME', '/tmp/inherited-codex-profile')
vi.stubEnv('ORCA_USER_DATA_PATH', '/tmp/inherited-orca-state')
vi.stubEnv('ORCA_CODEX_LAUNCH_PREFLIGHT', '/tmp/inherited-live-cli')
clearInheritedAgentStateEnv()
expect(process.env.CLAUDE_CONFIG_DIR).toBe(
value === '1' ? '/tmp/explicit-claude-profile' : undefined
)
expect(process.env.CODEX_HOME).toBeUndefined()
expect(process.env.ORCA_USER_DATA_PATH).toBeUndefined()
expect(process.env.ORCA_CODEX_LAUNCH_PREFLIGHT).toBeUndefined()
expect(() => rmSync(missingRealFolder('.claude'), { force: true })).toThrow(
/real-agent-home guard/
)
}
)
it('refuses a named-import sync write under the real ~/.codex', () => {
const target = join(missingRealFolder('.codex'), 'config.toml')
expect(() => writeFileSync(target, '[projects."/tmp/x"]\n')).toThrow(/real-agent-home guard/)
@@ -188,12 +188,18 @@ declare global {
}
const state = (globalThis.orcaRealAgentHomeWriteGuard ??= install())
// Why after install: the guard keeps the inherited paths as roots; tests that need one set their own.
const inheritedEnvToUnset = realAgentSuiteOptedIn()
? []
: [...INHERITED_STATE_ENV, ...INHERITED_LIVE_CLI_ENV]
for (const name of inheritedEnvToUnset) {
delete process.env[name]
export function clearInheritedAgentStateEnv(): void {
const inheritedEnvToUnset = realAgentSuiteOptedIn()
? []
: [...INHERITED_STATE_ENV, ...INHERITED_LIVE_CLI_ENV]
for (const name of inheritedEnvToUnset) {
if (name === 'CLAUDE_CONFIG_DIR' && process.env.ORCA_REAL_CLAUDE_CLI_TEST === '1') {
continue
}
delete process.env[name]
}
}
clearInheritedAgentStateEnv()
/** Drains recorded violations; only the guard's own self-test should need this. */
export function takeRealAgentHomeWriteViolations(): string[] {
@@ -65,6 +65,16 @@ export function nodeGypRebuildInvocation(
}
}
export function nodeGypRebuildTimeoutMs(
moduleName,
{ platform = process.platform, arch = process.arch, ci = process.env.CI } = {}
) {
// Cold headers and toolchain discovery consumed over four minutes on Windows ARM CI.
return moduleName === 'node-pty' && platform === 'win32' && arch === 'arm64' && ci === 'true'
? 600_000
: 300_000
}
/** The binary the addon actually loads. */
export function windowsProcessTreeAddonPath(packageDir = WINDOWS_PROCESS_TREE_PACKAGE_DIR) {
return join(packageDir, 'build', 'Release', 'windows_process_tree.node')
@@ -16,6 +16,7 @@ import {
assertWindowsProcessTreeRuntimeCreationTime,
inspectWindowsProcessTreeAddon,
nodeGypRebuildInvocation,
nodeGypRebuildTimeoutMs,
stageWindowsProcessTreeNodeAddonApiHeaders,
WINDOWS_PROCESS_TREE_NODE_ADDON_API_HEADERS,
WINDOWS_PROCESS_TREE_PACKAGE_DIR
@@ -54,6 +55,33 @@ describe('windows-process-tree node-gyp rebuild', () => {
})
})
it('allows cold setup and compilation only for node-pty on a Windows ARM CI host', () => {
expect(
nodeGypRebuildTimeoutMs('node-pty', { platform: 'win32', arch: 'arm64', ci: 'true' })
).toBe(600_000)
})
it.each([
{ moduleName: 'node-pty', platform: 'win32', arch: 'x64', ci: 'true' },
{ moduleName: 'node-pty', platform: 'linux', arch: 'arm64', ci: 'true' },
{ moduleName: 'node-pty', platform: 'darwin', arch: 'arm64', ci: 'true' },
{ moduleName: 'node-pty', platform: 'win32', arch: 'arm64', ci: '' },
{ moduleName: 'node-pty', platform: 'win32', arch: 'arm64', ci: 'false' },
{ moduleName: 'node-pty', platform: 'win32', arch: 'arm64', ci: '1' },
{ moduleName: '@orca/windows-registry', platform: 'win32', arch: 'arm64', ci: 'true' },
{ moduleName: '@vscode/windows-process-tree', platform: 'win32', arch: 'arm64', ci: 'true' }
])('keeps the five-minute bound for $moduleName on $platform/$arch with CI=$ci', (host) => {
expect(nodeGypRebuildTimeoutMs(host.moduleName, host)).toBe(300_000)
})
it('uses the execution host rather than an ARM cross-compilation target', () => {
const { args } = nodeGypRebuildInvocation('arm64', import.meta.dirname)
expect(args).toContain('--arch=arm64')
expect(
nodeGypRebuildTimeoutMs('node-pty', { platform: 'win32', arch: 'x64', ci: 'true' })
).toBe(300_000)
})
it('copies node-addon-api headers into the patched include dir', () => {
const packageDir = mkdtempSync(join(tmpdir(), 'orca-windows-process-tree-headers-'))
try {
@@ -0,0 +1,191 @@
import { createHash } from 'node:crypto'
import { errorMonitor } from 'node:events'
import { readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { redactTranscript } from './pty-transcript-secret-scan.mjs'
const MAX_RECORDS = 32
const MAX_EVENTS = 256
const ESC = String.fromCharCode(27)
const CONTROL_SEQUENCE = new RegExp(`${ESC}(?:\\[[0-?]*[ -/]*[@-~]|[@-_])`, 'g')
export function sanitizeStressText(text) {
const source = String(text ?? '')
const controls = []
let sourceCursor = 0
let scanIndex = 0
const scanText = source.replace(CONTROL_SEQUENCE, (sequence, index) => {
scanIndex += index - sourceCursor
sourceCursor = index + sequence.length
// OSC framing keeps title payloads separate from the adjacent rendered text.
if (sequence === `${ESC}]` || sequence === `${ESC}\\`) {
scanIndex += sequence.length
return sequence
}
controls.push({ sequence, index: scanIndex })
return ''
})
const firstPass = redactTranscript(scanText).text
// A local identity can mask a wider email finding in the first pass.
const sanitized = redactTranscript(firstPass).text
let result = ''
let cursor = 0
for (const { sequence, index } of controls) {
result += sanitized.slice(cursor, index) + sequence
cursor = index
}
return result + sanitized.slice(cursor)
}
export function loadedStressInputHashes(addonPath, resolveModule) {
const files = [
['conpty.node', addonPath],
['conpty.dll', join(dirname(addonPath), 'conpty', 'conpty.dll')],
['OpenConsole.exe', join(dirname(addonPath), 'conpty', 'OpenConsole.exe')]
]
const modules = [
'utils.js',
'windowsTerminal.js',
'windowsPtyAgent.js',
'windowsConoutConnection.js',
'worker/conoutSocketWorker.js'
]
return [...files, ...modules.map((name) => [name])].map(([name, path]) => {
try {
const bytes = readFileSync(path ?? resolveModule(`node-pty/lib/${name}`))
return { name, bytes: bytes.length, sha256: createHash('sha256').update(bytes).digest('hex') }
} catch (error) {
return { name, unavailable: error.code ?? 'unknown' }
}
})
}
function socketState(socket) {
if (!socket) {
return null
}
return {
connecting: socket.connecting === true,
destroyed: socket.destroyed === true,
readable: socket.readable === true,
writable: socket.writable === true
}
}
export function createStressObserver(report) {
const started = performance.now()
const records = []
let events = 0
let omittedEvents = 0
let omittedRecords = 0
function state(record, context) {
const { proc } = record
const agent = proc._agent
return {
...context,
shellPid: proc.pid,
ptyId: proc._pty,
terminalReady: proc._isReady === true,
exitCallbackObserved: record.exited === true,
closeRequested: record.closed === true,
nativeExitCode: Number.isInteger(agent?.exitCode) ? agent.exitCode : null,
deferredOperations: Array.isArray(proc._deferreds) ? proc._deferreds.length : null,
inputSocket: socketState(agent?._inSocket),
outputSocket: socketState(proc._socket),
conoutWorkerThreadId: agent?._conoutSocketWorker?._worker?.threadId ?? null
}
}
function emit(phase, details) {
if (events >= MAX_EVENTS) {
omittedEvents += 1
return
}
events += 1
report(phase, { elapsedMs: Math.round(performance.now() - started), ...details })
}
function watch(record, context) {
if (records.length >= MAX_RECORDS) {
// Keep the warmup survivor alongside the newest terminals.
const oldestRecent = records[0].context.round === -1 && records[0].context.slot === -1 ? 1 : 0
records.splice(oldestRecent, 1)
omittedRecords += 1
}
records.push({ record, context })
const { proc } = record
const snapshot = () => state(record, context)
emit('spawn-returned', snapshot())
let firstData = true
proc.onData((chunk) => {
if (firstData) {
firstData = false
emit('first-data', { ...snapshot(), bytes: Buffer.byteLength(chunk) })
}
})
proc.onExit((event) => emit('pty-exit-callback', { ...snapshot(), exitCode: event.exitCode }))
for (const [name, socket] of [
['input', proc._agent?._inSocket],
['output', proc._socket]
]) {
if (socket) {
socket.on(errorMonitor, (error) =>
emit('pipe-error', {
...snapshot(),
pipe: name,
code: error.code ?? null,
message: sanitizeStressText(String(error.message))
})
)
for (const event of ['connect', 'ready_datapipe', 'end', 'close']) {
socket.on(event, () => emit(`pipe-${event}`, { ...snapshot(), pipe: name }))
}
}
}
const worker = proc._agent?._conoutSocketWorker?._worker
if (worker) {
worker.on('online', () => emit('conout-worker-online', snapshot()))
worker.on('message', (message) => {
if (message === 1) {
emit('conout-worker-ready', snapshot())
}
})
worker.on('exit', (code) => emit('conout-worker-exit', { ...snapshot(), code }))
worker.on(errorMonitor, (error) =>
emit('conout-worker-error', { ...snapshot(), message: sanitizeStressText(error.message) })
)
}
const agent = proc._agent
if (typeof agent?._$onProcessExit === 'function') {
const original = agent._$onProcessExit
// Observe the existing callback without changing its receiver, arguments, or result.
agent._$onProcessExit = function (...args) {
emit('native-exit-callback', { ...snapshot(), exitCode: args[0] })
return original.call(this, ...args)
}
}
}
function pending(phase) {
report(phase, {
elapsedMs: Math.round(performance.now() - started),
observerEvents: events,
omittedEvents,
omittedRecords,
records: records.map(({ record, context }) => ({
...state(record, context),
output: sanitizeStressText(record.output.slice(-2048))
}))
})
}
function checkpoint(phase, record) {
const entry = records.find((entry) => entry.record === record)
if (entry) {
emit(phase, state(entry.record, entry.context))
}
}
return { watch, pending, checkpoint }
}
@@ -0,0 +1,318 @@
import { EventEmitter, errorMonitor } from 'node:events'
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { hostname, tmpdir, userInfo } from 'node:os'
import { join } from 'node:path'
import { scanTranscriptForSecrets } from './pty-transcript-secret-scan.mjs'
import { afterEach, describe, expect, it } from 'vitest'
import {
createStressObserver,
loadedStressInputHashes,
sanitizeStressText
} from './windows-pty-table-stress-observer.mjs'
const directories = []
afterEach(() => {
for (const directory of directories.splice(0)) {
rmSync(directory, { recursive: true, force: true })
}
})
function terminal() {
const proc = new EventEmitter()
proc.pid = 321
proc._pty = 12
proc._isReady = false
proc._deferreds = [() => {}]
proc._socket = new EventEmitter()
proc._agent = {
_inSocket: new EventEmitter(),
_conoutSocketWorker: { _worker: new EventEmitter() },
_$onProcessExit(code) {
this.exitCode = code
return 'original-result'
}
}
proc.onData = (listener) => proc.on('ptyData', listener)
proc.onExit = (listener) => proc.on('ptyExit', listener)
return { proc, exited: false, closed: false, output: '' }
}
function observation(record = terminal()) {
const events = []
const observer = createStressObserver((phase, details) => events.push({ phase, ...details }))
observer.watch(record, { round: 0, slot: 1 })
return { observer, events, record }
}
describe('Windows PTY stress observer', () => {
it('distinguishes a silent deferred terminal, native exit, and the public exit callback', () => {
const { observer, events, record } = observation()
observer.pending('readiness-timeout-state')
expect(events.at(-1).records[0]).toMatchObject({
shellPid: 321,
terminalReady: false,
deferredOperations: 1,
nativeExitCode: null,
exitCallbackObserved: false
})
expect(record.proc._agent._$onProcessExit(9)).toBe('original-result')
observer.pending('exit-drain-state')
expect(events.at(-1).records[0]).toMatchObject({
nativeExitCode: 9,
exitCallbackObserved: false
})
record.exited = true
record.proc.emit('ptyExit', { exitCode: 9 })
expect(events.at(-1)).toMatchObject({ phase: 'pty-exit-callback', exitCallbackObserved: true })
})
it('preserves native callback receiver, arguments, return value, and thrown errors', () => {
const record = terminal()
const calls = []
const failure = new Error('native callback failure')
record.proc._agent._$onProcessExit = function (...args) {
calls.push({ receiver: this, args })
if (args[0] === 1) {
throw failure
}
return 'unchanged'
}
observation(record)
expect(record.proc._agent._$onProcessExit(0, 'extra')).toBe('unchanged')
expect(calls).toEqual([{ receiver: record.proc._agent, args: [0, 'extra'] }])
expect(() => record.proc._agent._$onProcessExit(1)).toThrow(failure)
expect(calls).toHaveLength(2)
})
it('observes worker and socket errors without consuming an unhandled error', () => {
const { events, record } = observation()
const output = record.proc._socket
expect(output.listenerCount('error')).toBe(0)
expect(output.listenerCount(errorMonitor)).toBe(1)
const failure = Object.assign(new Error('broken pipe'), { code: 'EPIPE' })
expect(() => output.emit('error', failure)).toThrow(failure)
expect(events.at(-1)).toMatchObject({ phase: 'pipe-error', pipe: 'output', code: 'EPIPE' })
const worker = record.proc._agent._conoutSocketWorker._worker
expect(worker.listenerCount('error')).toBe(0)
expect(() => worker.emit('error', failure)).toThrow(failure)
expect(events.at(-1)).toMatchObject({ phase: 'conout-worker-error' })
})
it('keeps first data separate from worker readiness and bounds repeated milestones', () => {
const { observer, events, record } = observation()
const worker = record.proc._agent._conoutSocketWorker._worker
worker.emit('message', 1)
expect(events.at(-1).phase).toBe('conout-worker-ready')
expect(events.some((event) => event.phase === 'first-data')).toBe(false)
record.proc.emit('ptyData', 'first')
record.proc.emit('ptyData', 'second')
expect(events.filter((event) => event.phase === 'first-data')).toHaveLength(1)
for (let index = 0; index < 1_000; index += 1) {
worker.emit('message', 1)
}
expect(events).toHaveLength(256)
observer.pending('exit-drain-state')
expect(events.at(-1).observerEvents).toBe(256)
expect(events.at(-1).omittedEvents).toBeGreaterThan(0)
})
it('bounds tracked terminals and redacts the assembled tail without changing raw input', () => {
const events = []
const observer = createStressObserver((phase, details) => events.push({ phase, ...details }))
const raw = '\u001b[31mprivate@sensitive.test\r\nBearer secret01234567890123456789'
const first = terminal()
first.output = raw
observer.watch(first, { round: -1, slot: -1 })
for (let index = 1; index < 40; index += 1) {
observer.watch(terminal(), { round: index, slot: 1 })
}
observer.pending('exit-drain-state')
const last = events.at(-1)
expect(last.records).toHaveLength(32)
expect(last.omittedRecords).toBe(8)
expect(last.records[0]).toMatchObject({ round: -1, slot: -1 })
expect(last.records.at(-1)).toMatchObject({ round: 39, slot: 1 })
expect(last.records[0].output).not.toContain('private@sensitive.test')
expect(last.records[0].output).not.toContain('secret01234567890123456789')
expect(last.records[0].output.length).toBe(raw.length)
expect(last.records[0].output).toContain('\u001b[31m')
expect(first.output).toBe(raw)
})
it('hashes actual loaded files and qualifies missing runtime companions', () => {
const directory = mkdtempSync(join(tmpdir(), 'pty-stress-inputs-'))
directories.push(directory)
const addon = join(directory, 'conpty.node')
writeFileSync(addon, 'actual-loaded-bytes')
const hashes = loadedStressInputHashes(addon, () => addon)
expect(hashes[0]).toMatchObject({ name: 'conpty.node', bytes: 19 })
expect(hashes[0].sha256).toMatch(/^[a-f0-9]{64}$/)
expect(hashes[1]).toEqual({ name: 'conpty.dll', unavailable: 'ENOENT' })
expect(hashes[2]).toEqual({ name: 'OpenConsole.exe', unavailable: 'ENOENT' })
expect(hashes.slice(3).every((hash) => hash.sha256 === hashes[0].sha256)).toBe(true)
const missing = loadedStressInputHashes(addon, () => {
throw Object.assign(new Error('missing module'), { code: 'MODULE_NOT_FOUND' })
})
expect(missing.slice(3).every((hash) => hash.unavailable === 'MODULE_NOT_FOUND')).toBe(true)
})
it('preserves OSC boundaries while redacting their title payload and adjacent CSI text', () => {
const esc = String.fromCharCode(27)
const raw = `${esc}]0;private@sensitive.test${esc}\\${esc}[31mprivate@sensitive.test`
const sanitized = sanitizeStressText(raw)
expect(sanitized).not.toContain('private@')
expect(sanitized).not.toContain('sensitive.test')
expect(sanitized).toContain(`${esc}]0;`)
expect(sanitized).toContain(`${esc}\\${esc}[31m`)
expect(sanitized.length).toBe(raw.length)
})
it.each([
['email domain', 'private@', 'sensitive.test', ['[31m']],
['email name', 'pri', 'vate@sensitive.test', ['[31m', '[1m']],
['vendor key', 'sk-secret01', '234567890abcdefghijkl', ['[31m', '[1m']],
['bearer token', 'Bearer secret01', '234567890abcdefghijkl', ['[31m', '[1m']]
])(
'redacts %s interrupted by adjacent controls without moving their bytes',
(_kind, before, after, fragments) => {
const esc = String.fromCharCode(27)
const controls = fragments.map((fragment) => esc + fragment).join('')
const raw = `${before}${controls}${after}`
const sanitized = sanitizeStressText(raw)
expect(sanitized).not.toContain(before)
expect(sanitized).not.toContain(after)
expect(sanitized.slice(before.length, before.length + controls.length)).toBe(controls)
expect(sanitized.length).toBe(raw.length)
}
)
it('scrubs an interrupted OSC title independently from the adjacent rendered address', () => {
const esc = String.fromCharCode(27)
const title = `private@${esc}[31msensitive.test`
const raw = `${esc}]0;${title}${esc}\\private@sensitive.test`
const sanitized = sanitizeStressText(raw)
expect(sanitized).not.toContain('private@')
expect(sanitized).not.toContain('sensitive.test')
expect(sanitized.slice(0, 4)).toBe(`${esc}]0;`)
expect(sanitized.slice(12, 17)).toBe(`${esc}[31m`)
expect(sanitized.slice(4 + title.length, 6 + title.length)).toBe(`${esc}\\`)
expect(sanitized.length).toBe(raw.length)
})
it('keeps the warmup survivor and newest terminals beyond eleven rounds', () => {
const events = []
const observer = createStressObserver((phase, details) => events.push({ phase, ...details }))
const survivor = terminal()
observer.watch(survivor, { round: -1, slot: -1 })
let last
for (let round = 0; round < 11; round += 1) {
for (let slot = 0; slot < 3; slot += 1) {
last = terminal()
last.proc.pid = 1000 + round * 3 + slot
observer.watch(last, { round, slot })
}
}
const agent = last.proc._agent
expect(agent._$onProcessExit(4, 'extra')).toBe('original-result')
expect(agent.exitCode).toBe(4)
expect(events.at(-1)).toMatchObject({ phase: 'native-exit-callback', round: 10, slot: 2 })
last.exited = true
last.proc.emit('ptyExit', { exitCode: 4 })
expect(events.at(-1)).toMatchObject({ phase: 'pty-exit-callback', round: 10, slot: 2 })
observer.pending('readiness-timeout-state')
const snapshot = events.at(-1)
expect(snapshot.omittedRecords).toBe(2)
expect(snapshot.records).toHaveLength(32)
expect(snapshot.records[0]).toMatchObject({ round: -1, slot: -1 })
expect(snapshot.records[1]).toMatchObject({ round: 0, slot: 2 })
expect(snapshot.records.at(-1)).toMatchObject({
round: 10,
slot: 2,
nativeExitCode: 4,
exitCallbackObserved: true
})
})
it('retains late terminal state after exhausting the 256-milestone budget', () => {
const { observer, events, record: survivor } = observation()
const worker = survivor.proc._agent._conoutSocketWorker._worker
for (let index = 0; index < 300; index += 1) {
worker.emit('message', 1)
}
let last
for (let index = 0; index < 40; index += 1) {
last = terminal()
last.proc.pid = 2000 + index
observer.watch(last, { round: index, slot: 2 })
}
expect(last.proc._agent._$onProcessExit(7)).toBe('original-result')
last.exited = true
last.proc.emit('ptyExit', { exitCode: 7 })
expect(events).toHaveLength(256)
observer.pending('exit-drain-timeout-state')
const snapshot = events.at(-1)
expect(snapshot.observerEvents).toBe(256)
expect(snapshot.omittedEvents).toBeGreaterThan(0)
expect(snapshot.omittedRecords).toBe(9)
expect(snapshot.records).toHaveLength(32)
expect(snapshot.records.at(-1)).toMatchObject({
shellPid: 2039,
nativeExitCode: 7,
exitCallbackObserved: true
})
})
it.each([
['username', 'plain'],
['username', 'csi'],
['username', 'osc'],
['hostname', 'plain'],
['hostname', 'csi'],
['hostname', 'osc']
])('scrubs the entire %s email in %s framing', (identity, framing) => {
const name = identity === 'username' ? userInfo().username : hostname()
const esc = String.fromCharCode(27)
const csi = `${esc}[31m`
const email = `${name}@privatecorp.test`
const raw =
framing === 'csi'
? `${name}${csi}@privatecorp.test`
: framing === 'osc'
? `${esc}]0;${email}${esc}\\`
: email
const sanitized = sanitizeStressText(raw)
expect(sanitized).not.toContain(name)
expect(sanitized).not.toContain('privatecorp.test')
expect(sanitized.length).toBe(raw.length)
const visible = sanitized
.replaceAll(csi, '')
.replaceAll(`${esc}]0;`, '')
.replaceAll(`${esc}\\`, '')
expect(scanTranscriptForSecrets(visible)).toEqual([])
if (framing === 'csi') {
expect(sanitized.slice(name.length, name.length + csi.length)).toBe(csi)
} else if (framing === 'osc') {
expect(sanitized.slice(0, 4)).toBe(`${esc}]0;`)
expect(sanitized.slice(-2)).toBe(`${esc}\\`)
}
})
it.each(['vendor', 'bearer'])(
'keeps %s priority when a credential contains the local username',
(kind) => {
const name = userInfo().username
const esc = String.fromCharCode(27)
const csi = `${esc}[31m`
const prefix = kind === 'vendor' ? 'sk-' : 'Bearer '
const raw = `${prefix}${name}${csi}01234567890123456789`
const sanitized = sanitizeStressText(raw)
expect(sanitized).not.toContain(name)
expect(sanitized).not.toContain('01234567890123456789')
expect(sanitized.length).toBe(raw.length)
expect(
sanitized.slice(prefix.length + name.length, prefix.length + name.length + csi.length)
).toBe(csi)
expect(scanTranscriptForSecrets(sanitized.replaceAll(csi, ''))).toEqual([])
}
)
})
+22 -7
View File
@@ -13,16 +13,20 @@ async function exerciseTable() {
assert.equal(process.platform, 'win32', 'This probe requires real Windows ConPTY')
const rounds = Number(process.env.ORCA_PTY_TABLE_STRESS_ROUNDS ?? 8)
assert.ok(Number.isInteger(rounds) && rounds > 0 && rounds <= 2000)
const { createStressObserver, loadedStressInputHashes, sanitizeStressText } =
await import('./windows-pty-table-stress-observer.mjs')
const observer = createStressObserver(report)
const pty = require('node-pty')
const nativePath = require.resolve('node-pty/lib/utils')
const loaded = require(nativePath).loadNativeModule('conpty')
const native = loaded.module
const addonPath = resolve(dirname(nativePath), loaded.dir, 'conpty.node')
report('native', {
addonPath,
addonPath: sanitizeStressText(addonPath),
sha256: createHash('sha256').update(readFileSync(addonPath)).digest('hex'),
node: process.version,
rounds
rounds,
inputs: loadedStressInputHashes(addonPath, require.resolve)
})
// Unlike production's fallback, this crash probe requires a host that permits nested jobs.
const hostJobAssigned = native.assignCurrentProcessToJob()
@@ -57,6 +61,7 @@ async function exerciseTable() {
resolveReady(true)
}
})
observer.watch(record, { round, slot })
spawned.push(record)
// Escaping one letter keeps echoed input from satisfying the output marker.
proc.write(`echo ${marker.replace('READY', 'REA^DY')}\r`)
@@ -74,7 +79,11 @@ async function exerciseTable() {
),
new Promise((_, reject) => {
timer = setTimeout(() => {
const transcripts = records.map(({ proc, output }) => ({ pid: proc.pid, output }))
observer.pending('readiness-timeout-state')
const transcripts = records.map(({ proc, output }) => ({
pid: proc.pid,
output: sanitizeStressText(output)
}))
reject(new Error(`PTY readiness timed out: ${JSON.stringify(transcripts)}`))
}, 15_000)
})
@@ -93,6 +102,7 @@ async function exerciseTable() {
report('kill', { round, slot, shellPid: record.proc.pid })
record.proc.kill()
record.closed = true
observer.checkpoint('kill-returned-state', record)
}
let failure
@@ -131,12 +141,15 @@ async function exerciseTable() {
await Promise.race([
Promise.all(spawned.map((record) => record.exit)),
new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error('PTY exit callbacks did not drain')), 15_000)
timer = setTimeout(() => {
observer.pending('exit-drain-timeout-state')
reject(new Error('PTY exit callbacks did not drain'))
}, 15_000)
})
])
} catch (error) {
if (failure) {
report('drain-error', { message: error.stack })
report('drain-error', { message: sanitizeStressText(error.stack) })
} else {
failure = { error }
}
@@ -147,10 +160,12 @@ async function exerciseTable() {
if (failure) {
throw failure.error
}
observer.pending('complete-state')
report('complete', { terminals: spawned.length })
}
exerciseTable().catch((error) => {
report('error', { message: error.stack })
exerciseTable().catch(async (error) => {
const { sanitizeStressText } = await import('./windows-pty-table-stress-observer.mjs')
report('error', { message: sanitizeStressText(error.stack) })
process.exitCode = 1
})
+7
View File
@@ -4,11 +4,13 @@
"../src/cli/**/*",
"../src/shared/**/*",
"../src/main/agent-state-file-reader.ts",
"../src/main/gitlab/project-ref-parser.ts",
"../src/main/agent-hooks/grok-replay-guard.ts",
"../src/main/claude/hook-script.ts",
"../src/main/claude/claude-hook-event-versions.ts",
"../src/main/claude/claude-managed-hook-events.ts",
"../src/main/qoder/hook-service.ts",
"../src/main/qwen-code/hook-service.ts",
"../src/main/codebuddy/hook-service.ts",
"../src/main/agent-hooks/hook-stdin-contract.ts",
"../src/main/agent-hooks/hook-post-command.ts",
@@ -103,6 +105,7 @@
"../src/main/amp/managed-plugin-install-status.ts",
"../src/main/antigravity/hook-events.ts",
"../src/main/antigravity/hook-script.ts",
"../src/main/antigravity/windows-hook-json-post.ts",
"../src/main/antigravity/hook-service.ts",
"../src/main/antigravity/hooks-json-bundle.ts",
"../src/main/claude/hook-settings.ts",
@@ -221,6 +224,9 @@
"../src/main/hermes/hook-service.ts",
"../src/main/git-bash.ts",
"../src/main/in-flight-run-dedupe.ts",
"../src/main/jcode/hook-settings.ts",
"../src/main/jcode/hook-config.ts",
"../src/main/jcode/hook-service.ts",
"../src/main/kimi/hook-service.ts",
"../src/main/kimi/kimi-hook-config-toml.ts",
"../src/main/dsh/dsh-home-patch.ts",
@@ -235,6 +241,7 @@
"../src/main/openclaude/hook-service.ts",
"../src/main/rolling-file-backup.ts",
"../src/main/startup/hydrate-shell-path.ts",
"../src/main/startup/shell-path-probe.ts",
"../src/main/startup/windows-shell-path-ownership.ts",
// Why: serve-electron-flag-parity.test.ts checks the Electron-side serve argv rewrite against this
// project's serve spec; the module has no imports, so listing it pulls in nothing else.
+4 -4
View File
@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="106" height="20" role="img" aria-label="downloads: 88m">
<title>downloads: 88m</title>
<svg xmlns="http://www.w3.org/2000/svg" width="106" height="20" role="img" aria-label="downloads: 91m">
<title>downloads: 91m</title>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
@@ -15,7 +15,7 @@
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
<text x="37" y="15" fill="#010101" fill-opacity=".3">downloads</text>
<text x="37" y="14">downloads</text>
<text x="90" y="15" fill="#010101" fill-opacity=".3">88m</text>
<text x="90" y="14">88m</text>
<text x="90" y="15" fill="#010101" fill-opacity=".3">91m</text>
<text x="90" y="14">91m</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 935 B

After

Width:  |  Height:  |  Size: 935 B

+2 -2
View File
@@ -36,7 +36,7 @@
Supervisa y dirige a tus agentes desde el teléfono — recibe una notificación cuando un agente termine y envía instrucciones de seguimiento desde cualquier lugar.
[App Store de iOS](https://apps.apple.com/us/app/orca-ide/id6766130217) · [APK para Android](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
[App Store de iOS](https://apps.apple.com/us/app/orca-ide/id6766130217) · [APK para Android](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.52/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
</td>
<td width="50%">
@@ -231,7 +231,7 @@ yay -S stably-orca-bin
Vincúlala con tu app de escritorio para supervisar y dirigir a tus agentes desde el teléfono.
- **iOS:** [Descargar desde App Store](https://apps.apple.com/us/app/orca-ide/id6766130217)
- **Android:** [Descargar el APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk)
- **Android:** [Descargar el APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.52/app-release.apk)
---
+2 -2
View File
@@ -40,7 +40,7 @@
Surveillez et pilotez vos agents depuis votre téléphone — soyez notifié quand un agent termine, et envoyez des instructions de suivi où que vous soyez.
[App Store iOS](https://apps.apple.com/us/app/orca-ide/id6766130217) · [APK Android 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
[App Store iOS](https://apps.apple.com/us/app/orca-ide/id6766130217) · [APK Android 0.0.52](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.52/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
</td>
<td width="50%">
@@ -239,7 +239,7 @@ yay -S stably-orca-bin
Associez-la à l'app de bureau pour surveiller et piloter vos agents depuis votre téléphone.
- **iOS :** [Télécharger sur l'App Store](https://apps.apple.com/us/app/orca-ide/id6766130217)
- **Android :** [Télécharger l'APK 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk)
- **Android :** [Télécharger l'APK 0.0.52](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.52/app-release.apk)
---
+2 -2
View File
@@ -36,7 +36,7 @@
スマートフォンからエージェントを監視・操作 — エージェントの完了を通知で受け取り、どこからでもフォローアップを送信できます。
[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [ドキュメント →](https://www.onorca.dev/docs/mobile)
[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.52/app-release.apk) · [ドキュメント →](https://www.onorca.dev/docs/mobile)
</td>
<td width="50%">
@@ -231,7 +231,7 @@ yay -S stably-orca-bin
デスクトップアプリとペアリングして、スマートフォンからエージェントを監視・操作できます。
- **iOS:** [App Store からダウンロード](https://apps.apple.com/us/app/orca-ide/id6766130217)
- **Android:** [APK をダウンロード](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk)
- **Android:** [APK をダウンロード](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.52/app-release.apk)
---
+2 -2
View File
@@ -36,7 +36,7 @@
휴대폰에서 에이전트를 모니터링하고 조종하세요 — 에이전트가 완료되면 알림을 받고 어디서든 후속 지시를 보낼 수 있습니다.
[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [문서 →](https://www.onorca.dev/docs/mobile)
[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK 0.0.52](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.52/app-release.apk) · [문서 →](https://www.onorca.dev/docs/mobile)
</td>
<td width="50%">
@@ -234,7 +234,7 @@ yay -S stably-orca-bin
데스크톱 앱과 페어링해 휴대폰에서 에이전트를 모니터링하고 조종하세요.
- **iOS:** [App Store에서 다운로드](https://apps.apple.com/us/app/orca-ide/id6766130217)
- **Android:** [APK 0.0.50 다운로드](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [설치 가이드](https://www.onorca.dev/docs/android-apk)
- **Android:** [APK 0.0.52 다운로드](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.52/app-release.apk) · [설치 가이드](https://www.onorca.dev/docs/android-apk)
---
+2 -2
View File
@@ -36,7 +36,7 @@
Monitore e conduza seus agentes pelo celular — receba uma notificação quando um agente terminar e envie instruções de acompanhamento de qualquer lugar.
[App Store para iOS](https://apps.apple.com/us/app/orca-ide/id6766130217) · [APK Android 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
[App Store para iOS](https://apps.apple.com/us/app/orca-ide/id6766130217) · [APK Android 0.0.52](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.52/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
</td>
<td width="50%">
@@ -234,7 +234,7 @@ yay -S stably-orca-bin
Conecte ao app desktop para monitorar e conduzir seus agentes pelo celular.
- **iOS:** [Baixar na App Store](https://apps.apple.com/us/app/orca-ide/id6766130217)
- **Android:** [Baixar APK 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk)
- **Android:** [Baixar APK 0.0.52](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.52/app-release.apk)
---
+2 -2
View File
@@ -36,7 +36,7 @@
用手机监控并指挥你的智能体 — 智能体完成时收到通知,随时随地发送后续指令。
[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [文档 →](https://www.onorca.dev/docs/mobile)
[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.52/app-release.apk) · [文档 →](https://www.onorca.dev/docs/mobile)
</td>
<td width="50%">
@@ -231,7 +231,7 @@ yay -S stably-orca-bin
与桌面应用配对,用手机监控并指挥你的智能体。
- **iOS:** [从 App Store 下载](https://apps.apple.com/us/app/orca-ide/id6766130217)
- **Android:** [下载 APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk)
- **Android:** [下载 APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.52/app-release.apk)
---
+11 -3
View File
@@ -288,7 +288,10 @@ Every lane, Codex included, combines through the fold. A child waiting on a
human is a fold input (`childWorkLiveness: 'waiting'`, derived from the child's
own `waiting` state; a child's `blocked` means it failed and stays live work)
and makes the row wait whatever the main agent is doing, unless the main agent
is itself asking. Only the Codex hook lane feeds that input today. Known
is itself asking. The Codex hook lane feeds it from its child transcripts, and
the structured lanes from child records, which read `waiting` for a Codex child
thread's approval or input flag and for a Claude subagent's open permission
request. Known
divergences, pinned by name in the parity table
(`src/shared/main-agent-status-parity.test.ts`) where they are reachable, so a
reader does not mistake them for drift:
@@ -299,8 +302,13 @@ reader does not mistake them for drift:
main agent event overwrites the slot, so the row stops reading `waiting`
while the child is still asking, and a second asking child replaces the
first.
- The structured lane has no per-child wait: a child's pending prompt makes
the session `attention`, which reads as the main agent's own `blocked`.
- In the structured lane a child's pending prompt also makes the session
`attention`, which reads as the main agent's own `blocked`: one needs-input
state whoever asked. A Claude subagent reads `waiting` only while the
journal holds its card pending: from after the card's row is written until
just before anyone closes it, so every publish that shows the child waiting
also shows the session's `attention`, and the row never reads `waiting` for
a Claude subagent's request.
- The Codex hook lane drops its roster on a root `Stop` when it tracks no
child transcripts, so a still-running or still-asking child stops holding
the row.
@@ -0,0 +1,90 @@
# Native Antigravity Accounts
Accounts reads the credential authority on the runtime that owns execution. A client chooses
an owning Orca runtime and a host/distro target before sending an operation; it never replaces
the client's Mac Keychain item for another host. The RPC capability is
`accounts.antigravity-native.v1`. Older paired hosts are refused before account mutations.
The RPC returns account summaries only, never credential JSON, access tokens or refresh tokens.
Displayed quota is tied to the subject and authentication method observed during its refresh;
an external identity change hides the previous account's quota without an automatic fetch.
## Supported authority
Normal macOS agy uses service `gemini`, account `antigravity`. Its go-keyring values use the
base64 or legacy hex wrapper. Orca passes writes through `security -i` stdin, validates bounded
output and reads the entire native value back. The command buffer limit is checked before
writing. A missing native item falls back to the CLI-specific
`~/.gemini/antigravity-cli/antigravity-oauth-token` file. The distinct legacy jetski fallback
is not imported.
The compiled CLI bypasses keyring storage when SSH/WSL environment detectors or WSL kernel
identity apply. A runtime running under that evidenced bypass reads/writes its own CLI file;
it does not contact the client keychain. The file must be private and regular. A macOS
`cache/antigravity-keyring-unavailable` marker makes authority uncertain: Orca refuses instead
of assuming that the keychain or file wins.
Native Windows Credential Manager, native Linux Secret Service, and operations directed from
Windows Orca to a selected WSL distro are explicitly unsupported pending verified adapters.
Windows file bypass is also refused until private ACL protection is verified.
Windows' `gemini:antigravity` raw blob and 2560-byte limit are different from the Mac wrapper;
Linux uses the login collection with `service=gemini`, `username=antigravity`. No dependency,
PowerShell compilation, credential-home flag, or cross-host fallback is invented here.
A separate SSH relay has no Accounts RPC; use a paired owning runtime that implements it.
## Identity and snapshots
A Google ID token supplies the normalized Google issuer and stable subject. The authentication
method also scopes identity. The label uses a verified email when available; email is never the
identity key. Account record IDs are random and survive token, expiry, refresh-token and email
rotation. Profiles without a stable subject can be displayed but cannot be saved for switching.
Snapshots preserve the exact native JSON, including fields that Orca does not interpret. The
host's vault under `userData/antigravity-accounts/vault` requires meaningful OS encryption and
private permissions. Weak or unavailable encryption is refused. Unreadable/corrupt ciphertext
is preserved; it is never treated as an empty vault. This does not migrate the experimental
candidate's incompatible array vault or token-hash IDs.
One host service serializes Add, Select, Remove, launch checks and refresh reconciliation.
It re-reads the vault after asynchronous native reads and captures external CLI refreshes into
the same stable account. Selection reconciles the outgoing snapshot, checks the expected native
bytes before writing, and checks native readback before publishing the selected ID. It avoids
writing an old snapshot over an already-active account. The current or selected account cannot
be removed; deletion checks the latest native value again before committing.
A selected account is checked before new Orca PTY launches, including desktop daemon and
headless runtime paths. An externally changed native identity blocks the launch and asks the
user to select again. Existing sessions can retain their original credentials in memory.
Shell commands typed manually into a running terminal are outside the Orca launch guard.
## Sign-in and concurrency limits
Sign-in uses the supported ordinary agy browser/code flow. Users run agy on the owning host;
to add a different account they use its `/logout` command, complete the next sign-in, then save
the actual resulting account in Orca. This implementation does not advertise an Orca-managed
login or invent an agy `login`/`--login` flag. Browser completion and a second real Google
account remain user-driven; tests do not sign out or change the developer's real native item.
Native keyring does not expose compare-and-swap. Orca's queue serializes its own calls, and
bounded before/after checks detect observed conflicts; another independently running agy or
Orca process can still write between the final check and the write or launch. A failed
verification may mean the native item changed but selection was not persisted. Refresh and
explicit selection resolve that state; automatic rollback could destroy a newer CLI refresh
and is deliberately avoided. The file backend has the same external-writer limit.
## Evidence and contributor credit
The foundation adapts the reviewed codec/macOS adapter from #21784 and account-service concepts
from #21797 (nwparker), with fresh identity, persistence, serialization and conflict handling.
The signed-in Accounts card and quota-error visibility acknowledge #19588 by @artile; quota
transport is reused from current main rather than its obsolete extraction code. Targeted
multi-account UI/target concepts acknowledge #23761 by @Tai-DT, replacing its placeholder login
and unused settings selection. The Accounts legacy-Gemini clarification acknowledges #21682
and the original relevant migration contribution by @siddqamar, as requested in #17345.
No stale development stack was cherry-picked.
Live proof uses a disposable Mac service/account item, a fully isolated hidden Electron home,
and synthetic accounts. A private task-only copy was also selected through the real service;
installed agy 1.2.14 consumed that verified file credential under its SSH bypass and returned
`command.name=usage`, `num_turns=0`, no conversation. The real native item remained unchanged.
This proves the Mac adapter mechanics and actual CLI file authority, not a second-account
native-keychain switch, native Windows/Linux switching, or WSL/SSH relay deployment.
+9
View File
@@ -45,6 +45,15 @@ PRs pay the extra stage latency. Existing per-PR cancellation remains in place.
Package assertions, native boundaries, SSH/folder coverage, cache warming and
slow-test assertions are retained.
The daemon running-work test imports the shared probe directly, with the daemon's
process inspector supplied as its callback. The renderer keeps its existing
adapter and forwarding tests. This removes a mocked renderer dependency from the
headless graph without changing the probe algorithm or skipping backend tests.
At validation, the graph fell from 6,018 inputs (1,070 renderer inputs) to 4,879
inputs (no renderer inputs), including nine added shared-probe cases. Renderer
adapter changes no longer qualify the headless matrix; shared probe and daemon
test changes still do. Future actual renderer imports remain discoverable.
## Unit selection rollout
PR planning runs alongside typechecking after their shared dependency setup; an

Some files were not shown because too many files have changed in this diff Show More