mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
Extract subprocess handling into @orca/process-host (#26963)
Extract subprocess execution and process contracts into @orca/process-host with explicit public entry points, independent checks, and one Node 18 compatible CommonJS runtime. Migrate application consumers and integrate package builds, dependency selection, import enforcement, and runtime resource staging. Preserve daemon startup ownership and profile isolation, and qualify development rebuild recovery and concurrent output publication. Remove unnecessary aliases, test forwarding exports, and a duplicate output scan. Validation: full typechecks, changed-code quality, package and artifact checks, native Windows builds/types plus 306 runtime tests, and independent native reviews. Matched full E2E controls on original main and unchanged reviewed head both pass all 17 applicable tests (nine skips). Earlier intermittent E2E failures are documented in the PR; no assertions were weakened. The final combined-tree audit preserves both branches with no import-boundary violations.
This commit is contained in:
@@ -209,6 +209,10 @@ runs:
|
||||
git -C "$GITHUB_WORKSPACE" diff --exit-code -- package.json pnpm-lock.yaml pnpm-workspace.yaml
|
||||
fi
|
||||
|
||||
- name: Build internal packages
|
||||
shell: bash
|
||||
run: pnpm run build:packages
|
||||
|
||||
# pnpm checks the cached record's policy and validity; never bypass verification.
|
||||
- name: Save pnpm verification record on main
|
||||
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' && steps.verification-cache.outputs.key != '' && steps.verification-cache.outputs.cache-hit != 'true'
|
||||
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
- 'src/main/startup/main-process-quit*'
|
||||
- 'src/main/window/main-window-state-lifecycle*'
|
||||
- 'src/main/window/dashboard-popout-window*'
|
||||
- 'src/shared/child-process/**'
|
||||
- 'src/packages/process-host/**'
|
||||
- 'src/shared/update-status-types.ts'
|
||||
- 'pnpm-lock.yaml'
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -12,6 +12,8 @@ on:
|
||||
# recordings and the host params check, so any shared edit can move a golden or break mobile's
|
||||
# typecheck. Why the root lockfile: those modules resolve their packages from the root install.
|
||||
- 'src/shared/**'
|
||||
# Mobile's Node RPC recording tools import this package.
|
||||
- 'src/packages/process-host/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
# Mobile launch contracts exercise the real host dispatcher and durable receipt store.
|
||||
- 'src/main/agent-launch/**'
|
||||
@@ -40,6 +42,7 @@ on:
|
||||
paths:
|
||||
- 'mobile/**'
|
||||
- 'src/shared/**'
|
||||
- 'src/packages/process-host/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'src/main/agent-launch/**'
|
||||
- 'src/main/runtime/rpc/**'
|
||||
|
||||
@@ -268,7 +268,9 @@ jobs:
|
||||
|
||||
- name: Check reliability gate manifest
|
||||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||||
run: pnpm run check:reliability-gates
|
||||
run: |
|
||||
pnpm run check:reliability-gates
|
||||
pnpm run check:process-host-imports
|
||||
|
||||
- name: Enforce dead design-system classes
|
||||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||||
@@ -1009,6 +1011,10 @@ jobs:
|
||||
- name: Smoke managed-hook companions
|
||||
run: node config/scripts/smoke-managed-hook-runtime-node18.mjs
|
||||
|
||||
# The process-host package declares engines >=18; load its staged public output on Node 18.
|
||||
- name: Smoke process-host package
|
||||
run: node config/scripts/smoke-process-host-node18.mjs
|
||||
|
||||
package:
|
||||
name: package
|
||||
needs: [code_paths, preflight]
|
||||
@@ -1252,9 +1258,9 @@ jobs:
|
||||
src/main/persistence/profile-state/profile-state-writer-stall.electron.test.ts
|
||||
src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts
|
||||
src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts
|
||||
src/shared/child-process/windows-command-line.win32.test.ts
|
||||
src/shared/child-process/windows-cmd-shim-resolution.test.ts
|
||||
src/shared/child-process/windows-cmd-shim-resolution.win32.test.ts
|
||||
src/shared/__tests__/process-host/windows-command-line.win32.test.ts
|
||||
src/shared/__tests__/process-host/windows-cmd-shim-resolution.test.ts
|
||||
src/shared/__tests__/process-host/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
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
- 'src/main/ipc/command-path-resolver*'
|
||||
- 'src/main/preflight/**'
|
||||
- 'src/main/startup/*shell*'
|
||||
- 'src/shared/child-process/**'
|
||||
- 'src/packages/process-host/**'
|
||||
- '.github/actions/install-node-dependencies/**'
|
||||
- '.github/workflows/preflight-cli-platforms.yml'
|
||||
workflow_dispatch:
|
||||
@@ -53,6 +53,6 @@ jobs:
|
||||
src/main/startup/hydrate-shell-path.windows.test.ts
|
||||
src/main/startup/login-shell-environment.test.ts
|
||||
src/main/runtime/rpc/methods/preflight.test.ts
|
||||
src/shared/child-process/process-tree-termination.test.ts
|
||||
src/shared/child-process/run-process.test.ts
|
||||
src/shared/child-process/run-process-termination-failure.test.ts
|
||||
src/packages/process-host/src/process-tree-termination.test.ts
|
||||
src/packages/process-host/src/run-process.test.ts
|
||||
src/packages/process-host/src/run-process-termination-failure.test.ts
|
||||
|
||||
@@ -18,6 +18,8 @@ on:
|
||||
- 'src/main/ssh/ssh-windows-host-*'
|
||||
- 'src/main/ssh/ssh-session-command-audit.ts'
|
||||
- 'src/relay/**'
|
||||
- 'src/packages/process-host/**'
|
||||
- 'pnpm-workspace.yaml'
|
||||
- 'src/shared/node-runtime-pin.ts'
|
||||
- 'src/shared/orcad-artifacts.ts'
|
||||
- 'config/scripts/build-orcad-*.mjs'
|
||||
|
||||
@@ -81,11 +81,15 @@ jobs:
|
||||
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 supportedPostinstalls = [
|
||||
'node config/scripts/rebuild-native-deps.mjs',
|
||||
'pnpm run build:packages && node config/scripts/rebuild-native-deps.mjs'
|
||||
]
|
||||
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' &&
|
||||
supportedPostinstalls.includes(manifest.scripts?.postinstall) &&
|
||||
/^ 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')
|
||||
|
||||
@@ -75,7 +75,7 @@ Orca targets macOS, Linux, and Windows. Keep all platform-dependent behavior beh
|
||||
- **File paths**: Use `path.join` or Electron/Node path utilities — never assume `/` or `\`.
|
||||
- **Windows terminal shells**: `--shell` picks the shell a terminal _is_; `--command` is typed into whatever shell the host spawned, so a shell choice routed through `command` silently becomes a child process.
|
||||
- **Windows setup scripts**: the setup/issue-command runner is a `.cmd` batch file unless the script starts with a `#!` line — never derive that from the user's terminal-shell preference, and never launch a `.cmd` runner with a bare `cmd.exe /c` from a Git Bash pane (MSYS rewrites the `/c`).
|
||||
- **Windows child processes**: start them through `runProcess`/`spawnProcess` in `src/shared/child-process/` — never `child_process` directly. It pins `windowsHide`, refuses `shell: true`, and encodes `.cmd`/`.bat` arguments so neither `CommandLineToArgvW` nor `cmd.exe` mangles them. Recognised npm/pnpm `.cmd` shims are resolved to their real target so the spawn skips `cmd.exe` entirely.
|
||||
- **Windows child processes**: start them through `runProcess`/`spawnProcess` from `@orca/process-host` — never `child_process` directly. It pins `windowsHide`, refuses `shell: true`, and encodes `.cmd`/`.bat` arguments so neither `CommandLineToArgvW` nor `cmd.exe` mangles them. Recognised npm/pnpm `.cmd` shims are resolved to their real target so the spawn skips `cmd.exe` entirely.
|
||||
- **Ripgrep**: Orca bundles `rg` for every platform, WSL, and SSH remotes. Spawn it through `spawnBundledRipgrep` (main) or `resolveRelayRipgrepCommand` (relay), never a bare `'rg'` — Windows resolves a bare name in the spawn cwd before PATH. Don't add git/readdir fallbacks locally; the relay's chain exists only for hosts an upload never reached.
|
||||
- **Windows process enumeration**: read the table through `src/main/windows/windows-process-table.ts`, never by forking `powershell.exe`.
|
||||
- **Windows MSYS/Git Bash panes**: their children break away from the per-PTY job unless it is created without `JOB_OBJECT_LIMIT_BREAKAWAY_OK`, and a `conpty.node` built before that fix passes every existing gate.
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import ts from 'typescript-api'
|
||||
import type { Plugin } from 'rolldown'
|
||||
import { createWorkspaceSourceResolver } from '../scripts/workspace-source-exports.mjs'
|
||||
|
||||
function configError(diagnostic: ts.Diagnostic): Error {
|
||||
return new Error(ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'))
|
||||
}
|
||||
|
||||
function processHostInputs(root: string): {
|
||||
directory: string
|
||||
files: string[]
|
||||
directories: ts.MapLike<ts.WatchDirectoryFlags>
|
||||
} {
|
||||
const source = createWorkspaceSourceResolver(root).resolve('@orca/process-host')
|
||||
if (!source) {
|
||||
throw new Error('The process-host workspace source export is missing')
|
||||
}
|
||||
const manifest = resolve(root, source.manifest)
|
||||
const directory = dirname(manifest)
|
||||
const config = join(directory, 'tsconfig.json')
|
||||
const parsed = ts.getParsedCommandLineOfConfigFile(config, undefined, {
|
||||
...ts.sys,
|
||||
onUnRecoverableConfigFileDiagnostic(diagnostic) {
|
||||
throw configError(diagnostic)
|
||||
}
|
||||
})
|
||||
if (!parsed || parsed.errors.length > 0) {
|
||||
throw parsed?.errors[0] ? configError(parsed.errors[0]) : new Error(`Cannot read ${config}`)
|
||||
}
|
||||
const configSource = parsed.options.configFile
|
||||
const inheritedConfigs =
|
||||
configSource && typeof configSource === 'object' && 'extendedSourceFiles' in configSource
|
||||
? configSource.extendedSourceFiles
|
||||
: []
|
||||
return {
|
||||
directory,
|
||||
files: [manifest, config, ...parsed.fileNames, ...(inheritedConfigs ?? [])],
|
||||
directories: parsed.wildcardDirectories ?? {}
|
||||
}
|
||||
}
|
||||
|
||||
export function createProcessHostDevRebuildPlugin(root = process.cwd()): Plugin {
|
||||
const directoryWatchers = new Map<string, { recursive: boolean; watcher: ts.FileWatcher }>()
|
||||
let signalDirectory: string | undefined
|
||||
let signalFile: string | undefined
|
||||
let signalVersion = 0
|
||||
return {
|
||||
name: 'orca-process-host-dev-rebuild',
|
||||
buildStart() {
|
||||
if (!this.meta.watchMode) {
|
||||
return
|
||||
}
|
||||
const inputs = processHostInputs(root)
|
||||
for (const file of inputs.files) {
|
||||
this.addWatchFile(file)
|
||||
}
|
||||
// The installed bundler invalidates exact files; source creation needs a private watch signal.
|
||||
if (!signalDirectory) {
|
||||
signalDirectory = mkdtempSync(join(tmpdir(), 'orca-process-host-watch-'))
|
||||
signalFile = join(signalDirectory, 'source-directory-change')
|
||||
writeFileSync(signalFile, String(signalVersion))
|
||||
}
|
||||
if (signalFile) {
|
||||
this.addWatchFile(signalFile)
|
||||
}
|
||||
for (const [directory, watched] of directoryWatchers) {
|
||||
if (inputs.directories[directory] === undefined) {
|
||||
watched.watcher.close()
|
||||
directoryWatchers.delete(directory)
|
||||
}
|
||||
}
|
||||
for (const [directory, flags] of Object.entries(inputs.directories)) {
|
||||
const recursive = flags === ts.WatchDirectoryFlags.Recursive
|
||||
const previous = directoryWatchers.get(directory)
|
||||
if (previous?.recursive === recursive) {
|
||||
continue
|
||||
}
|
||||
previous?.watcher.close()
|
||||
if (!ts.sys.watchDirectory) {
|
||||
throw new Error('The process-host source directory watcher is unavailable')
|
||||
}
|
||||
const watcher = ts.sys.watchDirectory(
|
||||
directory,
|
||||
() => {
|
||||
if (signalFile) {
|
||||
writeFileSync(signalFile, String(++signalVersion))
|
||||
}
|
||||
},
|
||||
recursive,
|
||||
{ watchDirectory: ts.WatchDirectoryKind.UseFsEvents }
|
||||
)
|
||||
directoryWatchers.set(directory, { recursive, watcher })
|
||||
}
|
||||
// The external package must finish emitting before electron-vite restarts the app. The
|
||||
// package's own build keeps its compiler and per-file atomic dist sync identical to build:packages.
|
||||
const result = runProcessSync({
|
||||
program: process.execPath,
|
||||
args: [
|
||||
createRequire(join(inputs.directory, 'package.json')).resolve('tsx/cli'),
|
||||
'--conditions=orca-source',
|
||||
join(inputs.directory, 'scripts', 'build-dist.mjs')
|
||||
],
|
||||
cwd: inputs.directory,
|
||||
timeoutMs: 60_000
|
||||
})
|
||||
if (result.code !== 0) {
|
||||
throw new Error(
|
||||
`[process-host-dev-rebuild] Package compilation failed: ${result.stderr || result.stdout || result.signal || result.code}`
|
||||
)
|
||||
}
|
||||
},
|
||||
closeWatcher() {
|
||||
signalFile = undefined
|
||||
for (const { watcher } of directoryWatchers.values()) {
|
||||
watcher.close()
|
||||
}
|
||||
directoryWatchers.clear()
|
||||
if (signalDirectory) {
|
||||
rmSync(signalDirectory, { recursive: true, force: true })
|
||||
signalDirectory = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ const {
|
||||
} = require('./scripts/verify-packaged-daemon-entry.cjs')
|
||||
const {
|
||||
assertPackagedNativeVariantsInstalled,
|
||||
assertProcessHostOutputBuilt,
|
||||
createPackagedRuntimeNodeModuleResources,
|
||||
prunePackagedRuntimeNodeModules,
|
||||
verifyPackagedMainRuntimeDeps
|
||||
@@ -343,6 +344,7 @@ module.exports = {
|
||||
beforePack: (context, mobileWebBundleDir = MOBILE_WEB_BUNDLE_DIR) => {
|
||||
assertPackagedNativeVariantsInstalled(context.electronPlatformName, context.arch)
|
||||
assertBundledRipgrepInstalled()
|
||||
assertProcessHostOutputBuilt()
|
||||
assertOrcadTemplateBuilt()
|
||||
assertMobileWebBundleBuilt(mobileWebBundleDir)
|
||||
},
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
; Clean up the relocated terminal daemon on a REAL uninstall.
|
||||
;
|
||||
; Why: the daemon host is deliberately copied OUT of the install dir into
|
||||
; %LOCALAPPDATA%\Orca\daemon-host so that app UPDATES cannot kill it —
|
||||
; %LOCALAPPDATA%\Orca\daemon-host-profiles so that app UPDATES cannot kill it —
|
||||
; electron-builder's kill sweep selects processes whose image path is under
|
||||
; $INSTDIR, and that relocation is what keeps terminals alive across updates.
|
||||
; The same design means a normal uninstall's process sweep and file removal both
|
||||
@@ -103,6 +103,7 @@
|
||||
; Give the OS a moment to release the image lock before removing the tree.
|
||||
Sleep 500
|
||||
RMDir /r "$LOCALAPPDATA\Orca\daemon-host"
|
||||
RMDir /r "$LOCALAPPDATA\Orca\daemon-host-profiles"
|
||||
${endIf}
|
||||
; Why outside the ${isUpdated} guard: customInstall rewrites these on every update, so
|
||||
; dropping them during uninstallOldVersion is correct and keeps the pair symmetric.
|
||||
|
||||
@@ -18,6 +18,7 @@ const PACKAGED_RUNTIME_PACKAGE_ROOTS = [
|
||||
'@anthropic-ai/claude-agent-sdk',
|
||||
'@electron-toolkit/utils',
|
||||
'@linear/sdk',
|
||||
'@orca/process-host',
|
||||
'@parcel/watcher',
|
||||
'electron-updater',
|
||||
'i18next',
|
||||
@@ -214,10 +215,55 @@ function collectPackagedRuntimePackages(electronPlatformName = process.platform)
|
||||
function createPackagedRuntimeNodeModuleResources(electronPlatformName = process.platform) {
|
||||
return collectPackagedRuntimePackages(electronPlatformName).map(([packageName, packageDir]) => ({
|
||||
from: packageDir,
|
||||
to: join('node_modules', ...packageName.split('/'))
|
||||
to: join('node_modules', ...packageName.split('/')),
|
||||
...(packageName === '@orca/process-host' ? { filter: ['package.json', 'dist/**/*'] } : {})
|
||||
}))
|
||||
}
|
||||
|
||||
function listProcessHostModules(directory, extension, prefix = '') {
|
||||
if (!existsSync(directory)) {
|
||||
return []
|
||||
}
|
||||
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
||||
const relativePath = `${prefix}${entry.name}`
|
||||
if (entry.isDirectory()) {
|
||||
return entry.name === '__fixtures__'
|
||||
? []
|
||||
: listProcessHostModules(join(directory, entry.name), extension, `${relativePath}/`)
|
||||
}
|
||||
return entry.name.endsWith(extension) && !entry.name.endsWith(`.test${extension}`)
|
||||
? [relativePath.slice(0, -extension.length)]
|
||||
: []
|
||||
})
|
||||
}
|
||||
|
||||
// Why: the resource filter copies whatever dist holds, so a missing, partial, or stale build
|
||||
// would ship a package main cannot load. Mirrors the package tsconfig include/exclude.
|
||||
function assertProcessHostOutputBuilt(packageDir = readPackage('@orca/process-host').packageDir) {
|
||||
const manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'))
|
||||
const publicTargets = [
|
||||
manifest.main,
|
||||
...Object.values(manifest.exports ?? {}).map((target) =>
|
||||
typeof target === 'string' ? target : target?.default
|
||||
)
|
||||
].filter((target) => typeof target === 'string')
|
||||
const sources = listProcessHostModules(join(packageDir, 'src'), '.ts')
|
||||
const sourceSet = new Set(sources)
|
||||
const missing = [
|
||||
...new Set([...publicTargets, ...sources.map((module) => `./dist/${module}.js`)])
|
||||
].filter((target) => !existsSync(join(packageDir, target)))
|
||||
const stale = listProcessHostModules(join(packageDir, 'dist'), '.js')
|
||||
.filter((module) => !sourceSet.has(module))
|
||||
.map((module) => `./dist/${module}.js`)
|
||||
if (missing.length > 0 || stale.length > 0) {
|
||||
throw new Error(
|
||||
`@orca/process-host output in ${packageDir} does not match its source ` +
|
||||
`(missing: ${missing.join(', ') || 'none'}; stale: ${stale.join(', ') || 'none'}). ` +
|
||||
'Run `pnpm build:packages` before packaging.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeAsarEntryPath(entry) {
|
||||
return entry.replace(/\\/g, '/').replace(/^\/+/, '')
|
||||
}
|
||||
@@ -617,6 +663,7 @@ function pruneMatchingFiles(directory, shouldPrune) {
|
||||
module.exports = {
|
||||
PACKAGED_RUNTIME_PACKAGE_ROOTS,
|
||||
assertPackagedNativeVariantsInstalled,
|
||||
assertProcessHostOutputBuilt,
|
||||
createPackagedRuntimeNodeModuleResources,
|
||||
findAsarEntry,
|
||||
isPackagedExternalSpecifier,
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
# Existing production source files importing child_process outside @orca/process-host.
|
||||
# Migration debt: this list may only shrink; new callers use the package public API.
|
||||
|
||||
src/cli/handlers/account.ts
|
||||
src/cli/handlers/core.ts
|
||||
src/cli/handlers/interactive-login-interruption.ts
|
||||
src/cli/handlers/skills.ts
|
||||
src/cli/runtime/launch.ts
|
||||
src/cli/runtime/serve-update-supervisor.ts
|
||||
src/main/agent-hooks/managed-hook-owner-identity.ts
|
||||
src/main/agent-hooks/managed-hook-runtime.ts
|
||||
src/main/agent-hooks/wsl-hook-relay-launch.ts
|
||||
src/main/agent-hooks/wsl-hook-relay-link.ts
|
||||
src/main/agent-hooks/wsl-hook-relay-manager.ts
|
||||
src/main/agent-hooks/wsl-hook-relay-sentinel.ts
|
||||
src/main/ai-vault/session-scanner-service-client-state.ts
|
||||
src/main/ai-vault/session-scanner-service-client.ts
|
||||
src/main/ai-vault/session-scanner-service-spawn.ts
|
||||
src/main/app-icon.ts
|
||||
src/main/automations/precheck-runner.ts
|
||||
src/main/browser/agent-browser-bridge-execution.ts
|
||||
src/main/browser/agent-browser-bridge-raw-process.ts
|
||||
src/main/browser/agent-browser-bridge-state.ts
|
||||
src/main/browser/agent-browser-bridge-types.ts
|
||||
src/main/browser/browser-route-egress-electron-launch.ts
|
||||
src/main/browser/browser-route-persisted-worker-electron-process.ts
|
||||
src/main/codex-accounts/legacy-wsl-runtime-auth-drain-recovery-script-harness.ts
|
||||
src/main/codex-accounts/legacy-wsl-runtime-auth-drain-script-harness.ts
|
||||
src/main/codex-accounts/service.ts
|
||||
src/main/codex/codex-app-server-session.ts
|
||||
src/main/codex/codex-wsl-hook-install-plan.ts
|
||||
src/main/computer/desktop-script-provider-bridge.ts
|
||||
src/main/computer/macos-computer-use-permission-status.ts
|
||||
src/main/computer/macos-computer-use-permissions.ts
|
||||
src/main/computer/macos-native-provider-transport.ts
|
||||
src/main/computer/sidecar-client.ts
|
||||
src/main/daemon/daemon-launched-child.ts
|
||||
src/main/daemon/daemon-process-identity-query.ts
|
||||
src/main/daemon/daemon-process-inspection.ts
|
||||
src/main/daemon/daemon-process-start-time.ts
|
||||
src/main/emulator/android/android-avd-boot.ts
|
||||
src/main/emulator/android/android-command-runner.ts
|
||||
src/main/emulator/android/scrcpy-stream-session.ts
|
||||
src/main/emulator/serve-sim-helper-processes.ts
|
||||
src/main/emulator/serve-sim-runtime-materializer.ts
|
||||
src/main/emulator/simctl-simulator-devices.ts
|
||||
src/main/emulator/simulator-app-visibility.ts
|
||||
src/main/external-editor-launch.ts
|
||||
src/main/git/command-runner/exec-file-capture.ts
|
||||
src/main/git/command-runner/git-exec-file.ts
|
||||
src/main/git/command-runner/git-spawn.ts
|
||||
src/main/git/command-runner/git-stream-stdout.ts
|
||||
src/main/git/command-runner/spawn-command-capture.ts
|
||||
src/main/git/command-runner/spawned-command-tree-kill.ts
|
||||
src/main/git/command-runner/wsl-aware-spawn.ts
|
||||
src/main/git/wsl-git-read-environment.ts
|
||||
src/main/github/gh-utils.ts
|
||||
src/main/hooks.ts
|
||||
src/main/ipc/developer-permissions.ts
|
||||
src/main/ipc/filesystem-list-files.ts
|
||||
src/main/ipc/filesystem-watcher-wsl.ts
|
||||
src/main/ipc/filesystem/filesystem-handler-context.ts
|
||||
src/main/ipc/local-network-connection-test.ts
|
||||
src/main/ipc/macos-keyboard-input-source.ts
|
||||
src/main/ipc/macos-keyboard-layout-snapshot.ts
|
||||
src/main/ipc/notification-authorization-status.ts
|
||||
src/main/ipc/parcel-watcher-cancellation-restart.ts
|
||||
src/main/ipc/parcel-watcher-cancellation-tracker.ts
|
||||
src/main/ipc/parcel-watcher-child-launch.ts
|
||||
src/main/ipc/parcel-watcher-child-messaging.ts
|
||||
src/main/ipc/parcel-watcher-child-recovery.ts
|
||||
src/main/ipc/parcel-watcher-child-termination.ts
|
||||
src/main/ipc/parcel-watcher-host-subscriptions.ts
|
||||
src/main/ipc/parcel-watcher-pending-cancellation.ts
|
||||
src/main/ipc/parcel-watcher-process-supervisor.ts
|
||||
src/main/ipc/parcel-watcher-supervisor-disposal.ts
|
||||
src/main/ipc/parcel-watcher-supervisor-message.ts
|
||||
src/main/ipc/parcel-watcher-supervisor-subscribe.ts
|
||||
src/main/ipc/preflight-command-exec.ts
|
||||
src/main/ipc/repos/repo-clone-lifecycle.ts
|
||||
src/main/ipc/worktree-apfs-clone.ts
|
||||
src/main/ipc/wsl-watcher-process-exit.ts
|
||||
src/main/local-builds/local-build-candidate.ts
|
||||
src/main/macos-keychain/generic-password.ts
|
||||
src/main/macos-system-sleep-assertion.ts
|
||||
src/main/macos-tcc-prompt-watch.ts
|
||||
src/main/memory/collector.ts
|
||||
src/main/native-chat/wsl-transcript-fs-process-slot.ts
|
||||
src/main/native-chat/wsl-transcript-fs-process-spawn.ts
|
||||
src/main/network/macos-system-resolver-health.ts
|
||||
src/main/network/macos-tailscale-dns-diagnostic.ts
|
||||
src/main/plugins/plugin-host-process.ts
|
||||
src/main/plugins/plugin-private-marketplace-ssh-shim.cjs
|
||||
src/main/ports/port-scan-command-execution.ts
|
||||
src/main/providers/macos-login-session-pty-probe.ts
|
||||
src/main/providers/process-cwd.ts
|
||||
src/main/providers/windows-console-attached-processes.ts
|
||||
src/main/pty-descendant-termination.ts
|
||||
src/main/pty/posix-pty-foreground-group.ts
|
||||
src/main/pty/windows-environment-path.ts
|
||||
src/main/rate-limits/gemini-cli-oauth-extractor.ts
|
||||
src/main/runtime/tls-certificate.ts
|
||||
src/main/runtime/windows-default-route-interfaces.ts
|
||||
src/main/runtime/windows-mobile-firewall.ts
|
||||
src/main/skills/skill-update-run.ts
|
||||
src/main/ssh/ssh-connection.ts
|
||||
src/main/ssh/ssh-proxy-command.ts
|
||||
src/main/ssh/ssh-remote-cli-host-passthrough.ts
|
||||
src/main/ssh/system-ssh-command.ts
|
||||
src/main/ssh/system-ssh-dynamic-forward-process.ts
|
||||
src/main/ssh/system-ssh-file-transfer.ts
|
||||
src/main/ssh/system-ssh-forward-process.ts
|
||||
src/main/ssh/system-ssh-operation-lifecycle.ts
|
||||
src/main/startup/ensure-virtual-display.ts
|
||||
src/main/startup/hydrate-shell-path.ts
|
||||
src/main/startup/windows-install-dir-acl-probe.ts
|
||||
src/main/startup/windows-user-data-acl.ts
|
||||
src/main/win32-utils.ts
|
||||
src/main/window/clipboard-ipc-handlers.ts
|
||||
src/main/windows-process-tree-kill.ts
|
||||
src/main/workspace-space-analysis.ts
|
||||
src/main/wsl-availability.ts
|
||||
src/main/wsl-unc-delete.ts
|
||||
src/main/wsl.ts
|
||||
src/main/zsh-startup-hook-pty-harness.ts
|
||||
src/relay/agent-exec-handler.ts
|
||||
src/relay/ai-vault-service-client-state.ts
|
||||
src/relay/ai-vault-service-client.ts
|
||||
src/relay/ai-vault-service-spawn.ts
|
||||
src/relay/external-automations-handler.ts
|
||||
src/relay/fs-handler-git-fallback.ts
|
||||
src/relay/fs-handler-list-files.ts
|
||||
src/relay/preflight-handler.ts
|
||||
src/relay/pty-shell-utils.ts
|
||||
src/relay/subprocess-tree-termination.ts
|
||||
src/relay/workspace-space-scan.ts
|
||||
src/shared/ephemeral-vm-recipe-process.ts
|
||||
src/shared/ephemeral-vm-recipe-runner.ts
|
||||
src/shared/fish-binary-requirement.ts
|
||||
src/shared/process-table-snapshot-reader.ts
|
||||
src/shared/pty-slave-line-discipline-echo.ts
|
||||
src/shared/shell-process-readiness.ts
|
||||
@@ -760,12 +760,12 @@
|
||||
"invariant": "Decoded stdout and stderr callback contents and order remain unchanged, final tails are the last capped run of raw output bytes with any leading partial UTF-8 sequence dropped, and success or failure releases capture storage without recapturing late output.",
|
||||
"oracle": "Compare byte-bounded tails against the old per-chunk model and an independent code-point model across compaction, growth, oversized chunks and mid-character splits. Feed split and malformed UTF-8 through actual stream decoding. Require at most 5 MiB moved through the capture for 4 MiB of output with zero encoding, collectible backing buffers after close and error, zero late capture work with preserved callbacks, one decode per stream, unchanged tails on forced cancellation or synchronous kill-close, and every capture limit clamped to a bounded byte count.",
|
||||
"commands": [
|
||||
"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/shared/ephemeral-vm-recipe-process.test.ts src/shared/ephemeral-vm-recipe-output-capture.test.ts src/shared/growing-byte-buffer.test.ts src/main/ephemeral-vm-recipe-runner.test.ts"
|
||||
"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/shared/ephemeral-vm-recipe-process.test.ts src/shared/ephemeral-vm-recipe-output-capture.test.ts src/packages/process-host/src/growing-byte-buffer.test.ts src/main/ephemeral-vm-recipe-runner.test.ts"
|
||||
],
|
||||
"testFiles": [
|
||||
"src/shared/ephemeral-vm-recipe-process.test.ts",
|
||||
"src/shared/ephemeral-vm-recipe-output-capture.test.ts",
|
||||
"src/shared/growing-byte-buffer.test.ts",
|
||||
"src/packages/process-host/src/growing-byte-buffer.test.ts",
|
||||
"src/main/ephemeral-vm-recipe-runner.test.ts"
|
||||
],
|
||||
"assertionRefs": [
|
||||
@@ -782,7 +782,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/shared/growing-byte-buffer.test.ts",
|
||||
"file": "src/packages/process-host/src/growing-byte-buffer.test.ts",
|
||||
"assertions": [
|
||||
"reads and writes correctly after the head offset has advanced",
|
||||
"keeps a bounded suffix stable across many small appends without unbounded growth"
|
||||
@@ -794,7 +794,7 @@
|
||||
"date": "2026-09-26",
|
||||
"runner": "local",
|
||||
"platform": "macos",
|
||||
"command": "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/shared/ephemeral-vm-recipe-process.test.ts src/shared/ephemeral-vm-recipe-output-capture.test.ts src/shared/growing-byte-buffer.test.ts src/main/ephemeral-vm-recipe-runner.test.ts",
|
||||
"command": "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/shared/ephemeral-vm-recipe-process.test.ts src/shared/ephemeral-vm-recipe-output-capture.test.ts src/packages/process-host/src/growing-byte-buffer.test.ts src/main/ephemeral-vm-recipe-runner.test.ts",
|
||||
"result": "passed",
|
||||
"durationSeconds": 0.48,
|
||||
"summary": "60 tests across four suites passed, including forced-GC lifetime, synchronous SIGKILL-close and the shared byte buffer's head-offset cases."
|
||||
|
||||
@@ -6,7 +6,7 @@ import { join } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
import { runProcess } from '../../src/shared/child-process/run-process'
|
||||
import { runProcess } from '@orca/process-host'
|
||||
|
||||
const workflow = parse(readFileSync('.github/workflows/adhoc-mac-build.yml', 'utf8'))
|
||||
const PINNED = '${{ needs.resolve-ref.outputs.sha }}'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { createRequire } from 'node:module'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { existsSync, realpathSync } from 'node:fs'
|
||||
import { delimiter, join, resolve } from 'node:path'
|
||||
|
||||
@@ -20,10 +20,6 @@ for (const entry of Object.values(entries)) {
|
||||
assert.ok(existsSync(entry), `Missing emitted CLI: ${entry}`)
|
||||
}
|
||||
|
||||
const { runProcessSync } = createRequire(import.meta.url)(
|
||||
join(resolve(afterDir), 'shared', 'child-process', 'run-process.js')
|
||||
)
|
||||
|
||||
const child = String.raw`
|
||||
const { performance } = require('node:perf_hooks')
|
||||
const { writeSync } = require('node:fs')
|
||||
|
||||
@@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { spawnProcess } from '../../src/shared/child-process/run-process'
|
||||
import { spawnProcess } from '@orca/process-host'
|
||||
|
||||
const buildScript = fileURLToPath(new URL('./build-native-for-platform.mjs', import.meta.url))
|
||||
const directories = []
|
||||
|
||||
@@ -12,7 +12,7 @@ import { orcadNodeRuntimeRelativePath } from '../../src/shared/orcad-artifacts.t
|
||||
import { ORCAD_PREBUILDS_DIR } from './build-orcad-prebuilds.mjs'
|
||||
import { findSlotProblems, readManifest } from './orcad-prebuild-slot-contents.mjs'
|
||||
import { ensurePinnedNodeExecutable } from './pinned-node-downloads.mjs'
|
||||
import { runProcessSync } from './script-child-process.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { currentTarget } from './server-build-target.mjs'
|
||||
|
||||
const root = resolve(import.meta.dirname, '../..')
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
findSlotProblems,
|
||||
readManifest
|
||||
} from './orcad-prebuild-slot-contents.mjs'
|
||||
import { runProcessSync } from './script-child-process.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { verifyPackagedOrcadTemplate } from './verify-packaged-orcad-template.cjs'
|
||||
|
||||
const root = resolve(import.meta.dirname, '../..')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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 { runProcessSync } from '@orca/process-host'
|
||||
import { resolveOxlintInvocation } from './oxlint-cli-invocation.mjs'
|
||||
import {
|
||||
OXLINT_SCANS,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { runProcessSync } from '../../src/shared/child-process/run-process'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { resolveOxlintInvocation } from './oxlint-cli-invocation.mjs'
|
||||
|
||||
import {
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { globSync, readFileSync } from 'node:fs'
|
||||
import { isBuiltin } from 'node:module'
|
||||
import path from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { collectModuleSpecifiers } from './static-module-specifiers.mjs'
|
||||
import { isTestOnlySourcePath } from './test-only-source-path.mjs'
|
||||
|
||||
const ROOT = path.resolve(import.meta.dirname, '..', '..')
|
||||
const PACKAGE_NAME = '@orca/process-host'
|
||||
const PACKAGE_DIRECTORY = 'src/packages/process-host'
|
||||
const PACKAGE_SOURCE = `${PACKAGE_DIRECTORY}/src/`
|
||||
const BASELINE_PATH = 'config/process-host-direct-import-baseline.txt'
|
||||
const SOURCE_PATTERNS = [
|
||||
'*.{ts,tsx,mts,cts,js,mjs,cjs}',
|
||||
...['src', 'config', 'tests', 'mobile/src', 'mobile/app', 'mobile/scripts'].map(
|
||||
(directory) => `${directory}/**/*.{ts,tsx,mts,cts,js,mjs,cjs}`
|
||||
)
|
||||
]
|
||||
|
||||
function normalized(file) {
|
||||
return file.replaceAll('\\', '/')
|
||||
}
|
||||
|
||||
function packageName(specifier) {
|
||||
return specifier.startsWith('@')
|
||||
? specifier.split('/').slice(0, 2).join('/')
|
||||
: specifier.split('/')[0]
|
||||
}
|
||||
|
||||
function publicEntry(specifier, manifest) {
|
||||
const subpath = specifier === PACKAGE_NAME ? '.' : `.${specifier.slice(PACKAGE_NAME.length)}`
|
||||
return Object.hasOwn(manifest.exports ?? {}, subpath)
|
||||
}
|
||||
|
||||
function packageRelativeImport(file, specifier) {
|
||||
if (!specifier.startsWith('.')) {
|
||||
return null
|
||||
}
|
||||
return path.posix.normalize(path.posix.join(path.posix.dirname(file), specifier))
|
||||
}
|
||||
|
||||
export function assessProcessHostImports(sources, manifest, baseline) {
|
||||
const direct = new Set()
|
||||
const violations = []
|
||||
const dependencies = new Set([
|
||||
...Object.keys(manifest.dependencies ?? {}),
|
||||
...Object.keys(manifest.optionalDependencies ?? {}),
|
||||
...Object.keys(manifest.peerDependencies ?? {})
|
||||
])
|
||||
for (const [file, contents] of sources) {
|
||||
const ownSource = file.startsWith(PACKAGE_SOURCE)
|
||||
for (const sourceSpecifier of collectModuleSpecifiers(file, contents)) {
|
||||
const specifier = normalized(sourceSpecifier)
|
||||
const local = packageRelativeImport(file, specifier)
|
||||
const selfImport = specifier === PACKAGE_NAME || specifier.startsWith(`${PACKAGE_NAME}/`)
|
||||
const testCorpusImport =
|
||||
isTestOnlySourcePath(file) && local?.startsWith(`${PACKAGE_SOURCE}__fixtures__/`)
|
||||
if (selfImport && !publicEntry(specifier, manifest)) {
|
||||
violations.push(`${file}: ${sourceSpecifier} is not a public process-host export`)
|
||||
}
|
||||
if (
|
||||
!ownSource &&
|
||||
!testCorpusImport &&
|
||||
(local === PACKAGE_DIRECTORY || local?.startsWith(`${PACKAGE_DIRECTORY}/`))
|
||||
) {
|
||||
violations.push(
|
||||
`${file}: import process-host through ${PACKAGE_NAME}, not ${sourceSpecifier}`
|
||||
)
|
||||
}
|
||||
if (ownSource) {
|
||||
if (local !== null && !local.startsWith(PACKAGE_SOURCE)) {
|
||||
violations.push(
|
||||
`${file}: process-host cannot import outside its source: ${sourceSpecifier}`
|
||||
)
|
||||
} else if (path.posix.isAbsolute(specifier) || path.win32.isAbsolute(specifier)) {
|
||||
violations.push(
|
||||
`${file}: process-host cannot import an absolute path: ${sourceSpecifier}`
|
||||
)
|
||||
} else if (
|
||||
local === null &&
|
||||
!selfImport &&
|
||||
!isBuiltin(specifier) &&
|
||||
!dependencies.has(packageName(specifier)) &&
|
||||
!(
|
||||
isTestOnlySourcePath(file) &&
|
||||
Object.hasOwn(manifest.devDependencies ?? {}, packageName(specifier))
|
||||
)
|
||||
) {
|
||||
violations.push(`${file}: undeclared process-host dependency: ${sourceSpecifier}`)
|
||||
}
|
||||
} else if (
|
||||
file.startsWith('src/') &&
|
||||
!isTestOnlySourcePath(file) &&
|
||||
(specifier === 'child_process' || specifier === 'node:child_process')
|
||||
) {
|
||||
direct.add(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
const current = [...direct].sort()
|
||||
const baselineSet = new Set(baseline)
|
||||
return {
|
||||
direct: current,
|
||||
added: current.filter((file) => !baselineSet.has(file)),
|
||||
stale: baseline.filter((file) => !direct.has(file)).sort(),
|
||||
violations: violations.sort()
|
||||
}
|
||||
}
|
||||
|
||||
export function readProcessHostBaseline(contents) {
|
||||
return contents
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line && !line.startsWith('#'))
|
||||
}
|
||||
|
||||
export function collectProcessHostSources(root = ROOT) {
|
||||
return new Map(
|
||||
globSync(SOURCE_PATTERNS, {
|
||||
cwd: root,
|
||||
exclude: ['**/node_modules/**', '**/.*/**', '**/dist/**', '**/out/**', '**/build/**']
|
||||
})
|
||||
.map(normalized)
|
||||
.sort()
|
||||
.map((file) => [file, readFileSync(path.join(root, file), 'utf8')])
|
||||
)
|
||||
}
|
||||
|
||||
export function main(root = ROOT) {
|
||||
const manifest = JSON.parse(
|
||||
readFileSync(path.join(root, PACKAGE_DIRECTORY, 'package.json'), 'utf8')
|
||||
)
|
||||
const baseline = readProcessHostBaseline(readFileSync(path.join(root, BASELINE_PATH), 'utf8'))
|
||||
const sources = collectProcessHostSources(root)
|
||||
if (
|
||||
![...sources.keys()].some(
|
||||
(file) => file.startsWith(PACKAGE_SOURCE) && !isTestOnlySourcePath(file)
|
||||
)
|
||||
) {
|
||||
throw new Error(`Missing process-host implementation under ${PACKAGE_SOURCE}`)
|
||||
}
|
||||
const result = assessProcessHostImports(sources, manifest, baseline)
|
||||
const failures = [
|
||||
...result.violations,
|
||||
...result.added.map((file) => `${file}: new direct child_process import; use ${PACKAGE_NAME}`),
|
||||
...result.stale.map((file) => `${file}: remove stale direct-import baseline entry`)
|
||||
]
|
||||
if (failures.length > 0) {
|
||||
console.error(`[process-host-imports]\n${failures.join('\n')}`)
|
||||
return 1
|
||||
}
|
||||
console.log(`[process-host-imports] ok; ${result.direct.length} legacy direct importers remain`)
|
||||
return 0
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
||||
process.exitCode = main()
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import path from 'node:path'
|
||||
import {
|
||||
assessProcessHostImports,
|
||||
collectProcessHostSources,
|
||||
main,
|
||||
readProcessHostBaseline
|
||||
} from './check-process-host-imports.mjs'
|
||||
import { collectModuleSpecifiers } from './static-module-specifiers.mjs'
|
||||
|
||||
const PACKAGE_SOURCE = 'src/packages/process-host/src/'
|
||||
const manifest = {
|
||||
exports: { '.': './src/run-process.ts', './fork-process': './src/fork-process.ts' },
|
||||
devDependencies: { vitest: '*' }
|
||||
}
|
||||
const roots = []
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
for (const root of roots.splice(0)) {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
function assess(files, baseline = [], packageManifest = manifest) {
|
||||
return assessProcessHostImports(new Map(Object.entries(files)), packageManifest, baseline)
|
||||
}
|
||||
|
||||
function fixture(files) {
|
||||
const root = mkdtempSync(path.join(tmpdir(), 'orca-process-host-imports-'))
|
||||
roots.push(root)
|
||||
for (const [file, source] of Object.entries(files)) {
|
||||
const target = path.join(root, file)
|
||||
mkdirSync(path.dirname(target), { recursive: true })
|
||||
writeFileSync(target, source)
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
describe('process-host import boundary', () => {
|
||||
it('reads actual imports and reexports while ignoring comments and embedded programs', () => {
|
||||
expect(
|
||||
collectModuleSpecifiers(
|
||||
'file.ts',
|
||||
`
|
||||
// import { spawn } from 'node:child_process'
|
||||
const script = "require('node:child_process')"
|
||||
import type { ProcessSpec } from '@orca/process-host/process-spec'
|
||||
export { forkProcess } from '@orca/process-host/fork-process'
|
||||
type Child = import('child_process').ChildProcess
|
||||
import legacy = require('legacy-package')
|
||||
import('dynamic-package')
|
||||
require('required-package')
|
||||
require.resolve('resolved-package')
|
||||
`
|
||||
)
|
||||
).toEqual([
|
||||
'@orca/process-host/process-spec',
|
||||
'@orca/process-host/fork-process',
|
||||
'child_process',
|
||||
'legacy-package',
|
||||
'dynamic-package',
|
||||
'required-package',
|
||||
'resolved-package'
|
||||
])
|
||||
})
|
||||
|
||||
it('fails closed on source it cannot parse', () => {
|
||||
expect(() => collectModuleSpecifiers('broken.ts', 'import {')).toThrow('Cannot parse broken.ts')
|
||||
})
|
||||
|
||||
it('allows Node implementation imports and public consumer entry points', () => {
|
||||
expect(
|
||||
assess({
|
||||
[`${PACKAGE_SOURCE}run-process.ts`]: "import { spawn } from 'node:child_process'",
|
||||
[`${PACKAGE_SOURCE}run-process.test.ts`]: "import { it } from 'vitest'",
|
||||
'src/main/consumer.ts': "import { spawnProcess } from '@orca/process-host'",
|
||||
'config/scripts/consumer.mjs':
|
||||
"import { forkProcess } from '@orca/process-host/fork-process'"
|
||||
})
|
||||
).toEqual({ direct: [], added: [], stale: [], violations: [] })
|
||||
})
|
||||
|
||||
it.each([
|
||||
['src/main/consumer.ts', '../packages/process-host'],
|
||||
['src/main/consumer.ts', '../packages/process-host/src/run-process'],
|
||||
['src/main/consumer.test.ts', '../packages/process-host/src/run-process'],
|
||||
['config/scripts/consumer.mjs', '../../src/packages/process-host/src/run-process'],
|
||||
['tests/e2e/consumer.ts', '../../src/packages/process-host/src/run-process'],
|
||||
['mobile/src/consumer.ts', '../../src/packages/process-host/src/run-process'],
|
||||
['src/main/consumer.ts', '@orca/process-host/src/run-process'],
|
||||
['src/main/consumer.ts', '@orca/process-host/private-process-state']
|
||||
])('rejects private package access in %s', (file, specifier) => {
|
||||
expect(assess({ [file]: `import '${specifier}'` }).violations).toHaveLength(1)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['src/main/consumer.ts', '..\\packages\\process-host'],
|
||||
['src/main/consumer.ts', '..\\packages\\process-host\\src\\run-process'],
|
||||
['src/main/consumer.ts', '..\\packages\\process-host\\dist\\run-process.js'],
|
||||
['config/scripts/consumer.mjs', '..\\..\\src\\packages\\process-host\\src\\run-process'],
|
||||
['src/main/consumer.ts', '@orca\\process-host\\src\\run-process'],
|
||||
['src/main/consumer.ts', '@orca/process-host\\private-process-state']
|
||||
])('rejects Windows private package access in %s through %s', (file, specifier) => {
|
||||
expect(assess({ [file]: `require(${JSON.stringify(specifier)})` }).violations).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('shares fixture corpora only with test-only importers, keeping implementation imports public', () => {
|
||||
const corpus = '../packages/process-host/src/__fixtures__/windows-argument-corpus'
|
||||
expect(assess({ 'src/main/consumer.test.ts': `import '${corpus}'` }).violations).toEqual([])
|
||||
expect(assess({ 'src/main/consumer.ts': `import '${corpus}'` }).violations).toHaveLength(1)
|
||||
expect(
|
||||
assess({
|
||||
'src/main/consumer.test.ts':
|
||||
"import '../packages/process-host/src/__fixtures__/../run-process'"
|
||||
}).violations
|
||||
).toHaveLength(1)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['../main/runtime', 'cannot import outside'],
|
||||
['../../../main/runtime', 'cannot import outside'],
|
||||
['/repo/src/main/runtime.ts', 'absolute path'],
|
||||
['@renderer/store', 'undeclared'],
|
||||
['@/store', 'undeclared'],
|
||||
['electron', 'undeclared'],
|
||||
['vitest', 'undeclared'],
|
||||
['undeclared-package/subpath', 'undeclared']
|
||||
])('rejects package dependence on %s', (specifier, reason) => {
|
||||
expect(
|
||||
assess({ [`${PACKAGE_SOURCE}runner.ts`]: `import '${specifier}'` }).violations[0]
|
||||
).toContain(reason)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['..\\..\\..\\main\\runtime', 'cannot import outside'],
|
||||
['.\\..\\..\\..\\main\\runtime', 'cannot import outside'],
|
||||
['C:\\repo\\src\\main\\runtime.ts', 'absolute path'],
|
||||
['\\\\server\\repo\\src\\main\\runtime.ts', 'absolute path'],
|
||||
['@renderer\\store', 'undeclared']
|
||||
])('rejects package dependence on Windows specifier %s', (specifier, reason) => {
|
||||
expect(
|
||||
assess({ [`${PACKAGE_SOURCE}runner.ts`]: `require(${JSON.stringify(specifier)})` })
|
||||
.violations[0]
|
||||
).toContain(reason)
|
||||
})
|
||||
|
||||
it('permits Windows relative imports within package source and test fixture corpora', () => {
|
||||
const corpus = '..\\packages\\process-host\\src\\__fixtures__\\windows-argument-corpus'
|
||||
const traversal = '..\\packages\\process-host\\src\\__fixtures__\\..\\run-process'
|
||||
expect(
|
||||
assess({
|
||||
[`${PACKAGE_SOURCE}runner.ts`]: `require(${JSON.stringify('.\\process-spec')})`,
|
||||
'src/main/consumer.test.ts': `require(${JSON.stringify(corpus)})`
|
||||
}).violations
|
||||
).toEqual([])
|
||||
expect(
|
||||
assess({
|
||||
'src/main/consumer.test.ts': `require(${JSON.stringify(traversal)})`
|
||||
}).violations
|
||||
).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('allows only declared runtime dependencies in production package source', () => {
|
||||
expect(
|
||||
assess(
|
||||
{ [`${PACKAGE_SOURCE}runner.ts`]: "import 'declared/subpath'; import './process-spec'" },
|
||||
[],
|
||||
{ ...manifest, dependencies: { declared: '*' } }
|
||||
).violations
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it('reports new and removed legacy direct importers without exempting type imports', () => {
|
||||
expect(
|
||||
assess(
|
||||
{
|
||||
'src/main/legacy.ts': "import { spawn } from 'node:child_process'",
|
||||
'src/relay/new.ts': "import type { ChildProcess } from 'child_process'",
|
||||
'src/main/consumer.test.ts': "import { spawn } from 'node:child_process'",
|
||||
'config/scripts/build.mjs': "import { spawn } from 'node:child_process'"
|
||||
},
|
||||
['src/main/legacy.ts', 'src/main/removed.ts']
|
||||
)
|
||||
).toEqual({
|
||||
direct: ['src/main/legacy.ts', 'src/relay/new.ts'],
|
||||
added: ['src/relay/new.ts'],
|
||||
stale: ['src/main/removed.ts'],
|
||||
violations: []
|
||||
})
|
||||
})
|
||||
|
||||
it('scans nested package, consumer, test, config, and mobile source without generated checkouts', () => {
|
||||
const files = [
|
||||
`${PACKAGE_SOURCE}nested/runner.ts`,
|
||||
'src/main/consumer.ts',
|
||||
'electron.vite.config.ts',
|
||||
'config/build-plugins/consumer.ts',
|
||||
'config/scripts/nested/build.mjs',
|
||||
'tests/e2e/consumer.ts',
|
||||
'mobile/src/consumer.ts',
|
||||
'mobile/app/consumer.tsx',
|
||||
'mobile/scripts/consumer.mts'
|
||||
]
|
||||
const ignored = [
|
||||
'tests/e2e/.cross-version-checkouts/src/runtime.ts',
|
||||
'src/packages/process-host/node_modules/dep/index.js'
|
||||
]
|
||||
const root = fixture(
|
||||
Object.fromEntries([...files, ...ignored].map((file) => [file, 'export {}']))
|
||||
)
|
||||
expect([...collectProcessHostSources(root).keys()]).toEqual(files.sort())
|
||||
})
|
||||
|
||||
it('checks the command-line path against the source tree and baseline', () => {
|
||||
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
const error = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
const files = {
|
||||
'src/packages/process-host/package.json': JSON.stringify(manifest),
|
||||
'config/process-host-direct-import-baseline.txt': 'src/main/legacy.ts\n',
|
||||
[`${PACKAGE_SOURCE}runner.ts`]: "import { spawn } from 'node:child_process'",
|
||||
'src/main/legacy.ts': "import { spawn } from 'node:child_process'"
|
||||
}
|
||||
expect(main(fixture(files))).toBe(0)
|
||||
expect(log).toHaveBeenCalledWith(expect.stringContaining('1 legacy direct importers remain'))
|
||||
expect(
|
||||
main(fixture({ ...files, 'src/main/new.ts': "import { spawn } from 'node:child_process'" }))
|
||||
).toBe(1)
|
||||
expect(error).toHaveBeenCalledWith(
|
||||
expect.stringContaining('src/main/new.ts: new direct child_process import')
|
||||
)
|
||||
})
|
||||
|
||||
it('fails loudly when the package implementation disappears', () => {
|
||||
const root = fixture({
|
||||
'src/packages/process-host/package.json': JSON.stringify(manifest),
|
||||
'config/process-host-direct-import-baseline.txt': ''
|
||||
})
|
||||
expect(() => main(root)).toThrow('Missing process-host implementation')
|
||||
})
|
||||
|
||||
it('reads a commented baseline without allowing formatting to invent entries', () => {
|
||||
expect(readProcessHostBaseline('# migration debt\n\n src/main/legacy.ts \n')).toEqual([
|
||||
'src/main/legacy.ts'
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -21,6 +21,7 @@ import path from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import process from 'node:process'
|
||||
import { isTestOnlyDirectoryName, isTestOnlySourcePath } from './test-only-source-path.mjs'
|
||||
import { createWorkspaceSourceResolver } from './workspace-source-exports.mjs'
|
||||
|
||||
// Why absolute, not cwd-relative: `pnpm lint` runs from the repo root but CI steps and
|
||||
// editors do not always, and a cwd-relative miss surfaced as an unhandled ENOENT stack
|
||||
@@ -47,6 +48,7 @@ export const STRUCTURED_CHAT_LANES = [
|
||||
{ directory: ['src', 'main', 'claude'] },
|
||||
{ directory: ['src', 'main', 'codex'] },
|
||||
{ directory: ['src', 'shared'] },
|
||||
{ directory: ['src', 'packages', 'process-host', 'src'] },
|
||||
{ directory: ['src', 'main', 'runtime'], basename: /^(?:structured-|agent-session-)/ },
|
||||
{ directory: ['src', 'main', 'provider-process'] },
|
||||
{ directory: ['src', 'main', 'acp'] },
|
||||
@@ -114,6 +116,16 @@ const externalNativeAddons = {
|
||||
|
||||
// Why `plugins`: lets a test add an Electron import to a real file in memory, never on disk.
|
||||
export async function collectElectronImporters(entryPoints, { plugins = [] } = {}) {
|
||||
const workspace = createWorkspaceSourceResolver(ROOT)
|
||||
const workspaceSources = {
|
||||
name: 'runtime-workspace-sources',
|
||||
setup(pluginBuild) {
|
||||
pluginBuild.onResolve({ filter: /^@orca\// }, ({ path: specifier }) => {
|
||||
const source = workspace.resolve(specifier)
|
||||
return source ? { path: source.path } : undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
const result = await build({
|
||||
// Export every entry through one bundle so shared dependencies are emitted once.
|
||||
stdin: {
|
||||
@@ -137,7 +149,7 @@ export async function collectElectronImporters(entryPoints, { plugins = [] } = {
|
||||
metafile: true,
|
||||
absWorkingDir: ROOT,
|
||||
logLevel: 'silent',
|
||||
plugins: [externalNativeAddons, ...plugins]
|
||||
plugins: [externalNativeAddons, workspaceSources, ...plugins]
|
||||
})
|
||||
const importers = new Set()
|
||||
for (const [file, info] of Object.entries(result.metafile.inputs)) {
|
||||
|
||||
@@ -41,7 +41,8 @@ describe('structured chat coverage', () => {
|
||||
'src/main/acp/acp-structured-session-adapter.ts': 'export {}',
|
||||
'src/main/jsonl-rpc/peer.ts': 'export {}',
|
||||
'src/main/pi/rpc-launch.ts': 'export {}',
|
||||
'src/shared/agent-session-record.ts': 'export {}'
|
||||
'src/shared/agent-session-record.ts': 'export {}',
|
||||
'src/packages/process-host/src/run-process.ts': 'export {}'
|
||||
}
|
||||
|
||||
it('covers whole lane directories, structured runtime files at any depth, and no test code', () => {
|
||||
@@ -53,6 +54,7 @@ describe('structured chat coverage', () => {
|
||||
'src/main/codex/codex-provider-timeline-identity.ts',
|
||||
'src/shared/nested/agent-session-account-home.ts',
|
||||
'src/shared/relay-runtime-self-test-report.ts',
|
||||
'src/packages/process-host/src/nested/process-state.ts',
|
||||
'src/main/runtime/agent-session-record.ts',
|
||||
'src/main/runtime/structured-agent-runtime-registrations.ts',
|
||||
'src/main/runtime/rpc/methods/structured-agent-session-agents.ts',
|
||||
@@ -75,6 +77,8 @@ describe('structured chat coverage', () => {
|
||||
'src/main/native-chat/test-support/reader.ts',
|
||||
'src/main/runtime/orca-runtime-tests/structured-agent-session-host.ts',
|
||||
'src/shared/types.d.ts',
|
||||
'src/packages/process-host/src/run-process.test.ts',
|
||||
'src/packages/process-host/src/__fixtures__/spawned-child.ts',
|
||||
'src/main/runtime/other.ts',
|
||||
'src/main/runtime/rpc/methods/browser.ts',
|
||||
'src/main/jsonl-rpc/peer.test.ts',
|
||||
@@ -142,6 +146,24 @@ describe('structured chat coverage', () => {
|
||||
}
|
||||
)
|
||||
|
||||
it('checks public workspace imports against source without requiring a built package', async () => {
|
||||
const root = fixture({ 'entry.ts': "import '@orca/process-host'" })
|
||||
const target = path.join(process.cwd(), 'src/packages/process-host/src/run-process.ts')
|
||||
const current = await collectElectronImporters([path.join(root, 'entry.ts')], {
|
||||
plugins: [
|
||||
{
|
||||
name: 'inject-process-host-electron-import',
|
||||
setup(pluginBuild) {
|
||||
pluginBuild.onLoad({ filter: /run-process\.ts$/ }, (args) =>
|
||||
args.path === target ? { contents: "import 'electron'", loader: 'ts' } : undefined
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
expect(current).toEqual(['src/packages/process-host/src/run-process.ts'])
|
||||
})
|
||||
|
||||
it('bounds shared dependency output without dropping any entry point', async () => {
|
||||
const entries = Array.from({ length: 40 }, (_, index) => `entry-${index}.ts`)
|
||||
const root = fixture({
|
||||
@@ -186,7 +208,7 @@ describe('the default entry points', () => {
|
||||
'src/main/runtime/runtime-rpc.ts',
|
||||
'src/main/orcad/main.ts'
|
||||
])
|
||||
for (const lane of [...lanes, 'src/shared/']) {
|
||||
for (const lane of [...lanes, 'src/shared/', 'src/packages/process-host/src/']) {
|
||||
expect(entries.some((file) => file.startsWith(lane))).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -32,6 +32,19 @@ describe('CI dependency download caches', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('builds internal packages before native preparation after a script-free install', () => {
|
||||
const steps = action.runs.steps
|
||||
const buildIndex = steps.findIndex((step) => step.name === 'Build internal packages')
|
||||
expect(buildIndex).toBeGreaterThan(
|
||||
steps.findIndex((step) => step.name === 'Install dependencies')
|
||||
)
|
||||
expect(buildIndex).toBeLessThan(
|
||||
steps.findIndex((step) => step.name === 'Prepare native runtime')
|
||||
)
|
||||
expect(steps[buildIndex].if).toBeUndefined()
|
||||
expect(steps[buildIndex].run).toBe('pnpm run build:packages')
|
||||
})
|
||||
|
||||
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(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { expect, it } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
import { runProcess } from '../../src/shared/child-process/run-process'
|
||||
import { runProcess } from '@orca/process-host'
|
||||
import {
|
||||
classifyE2eJobs,
|
||||
DEDICATED_E2E_SPECS,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { expect, it } from 'vitest'
|
||||
import { runProcess } from '../../src/shared/child-process/run-process'
|
||||
import { runProcess } from '@orca/process-host'
|
||||
import { planE2e, verifyE2eSelection } from './ci-e2e-shard-plan.mjs'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
@@ -4,7 +4,7 @@ import { join } from 'node:path'
|
||||
import { parse } from 'yaml'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { classifyPrJobs } from './pr-code-change-scope.mjs'
|
||||
import { runProcessSync } from './script-child-process.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
|
||||
const installerPath = '.github/actions/install-node-dependencies/action.yml'
|
||||
const nativePath = '.github/actions/prepare-native-runtime/action.yml'
|
||||
|
||||
@@ -10,11 +10,12 @@ import {
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { resolvePnpmCliInvocation } from './pnpm-cli-invocation.mjs'
|
||||
import { runProcessSync } from './script-child-process.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { workspacePackageManifests } from './workspace-source-exports.mjs'
|
||||
|
||||
const { values } = parseArgs({
|
||||
options: { samples: { type: 'string', default: '3' }, output: { type: 'string' } }
|
||||
@@ -27,7 +28,12 @@ const checkout = join(temporary, 'checkout')
|
||||
const store = join(temporary, 'store')
|
||||
const archive = join(temporary, 'store.tar.zst')
|
||||
const uncompressed = join(temporary, 'store.tar')
|
||||
const manifests = ['package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml']
|
||||
const manifests = [
|
||||
'package.json',
|
||||
'pnpm-lock.yaml',
|
||||
'pnpm-workspace.yaml',
|
||||
...workspacePackageManifests(repository)
|
||||
]
|
||||
const pnpm = resolvePnpmCliInvocation()
|
||||
const results = []
|
||||
|
||||
@@ -61,6 +67,15 @@ function install() {
|
||||
.milliseconds
|
||||
}
|
||||
|
||||
function resetModules() {
|
||||
const packageDirs = new Set(
|
||||
manifests.filter((file) => file.endsWith('package.json')).map(dirname)
|
||||
)
|
||||
for (const directory of packageDirs) {
|
||||
rmSync(join(checkout, directory, 'node_modules'), { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
function transferArchive(restore) {
|
||||
const start = performance.now()
|
||||
// Windows BSD tar needs a separate zstd process, as in the Actions cache toolkit.
|
||||
@@ -86,7 +101,7 @@ function transferArchive(restore) {
|
||||
|
||||
try {
|
||||
mkdirSync(checkout)
|
||||
for (const file of [...manifests, 'native/windows-registry/package.json', 'config/patches']) {
|
||||
for (const file of [...manifests, 'config/patches']) {
|
||||
const target = join(checkout, file)
|
||||
mkdirSync(resolve(target, '..'), { recursive: true })
|
||||
cpSync(join(repository, file), target, { recursive: true })
|
||||
@@ -99,7 +114,7 @@ try {
|
||||
for (let sample = 0; sample < samples; sample++) {
|
||||
const policies = sample % 2 === 0 ? ['save', 'restore-only'] : ['restore-only', 'save']
|
||||
for (const policy of policies) {
|
||||
rmSync(join(checkout, 'node_modules'), { recursive: true, force: true })
|
||||
resetModules()
|
||||
rmSync(store, { recursive: true, force: true })
|
||||
const installMs = install()
|
||||
assert.equal(digest(), sourceDigest, 'frozen install changed a manifest')
|
||||
@@ -123,7 +138,7 @@ try {
|
||||
}
|
||||
|
||||
// Both policies restore identical bytes on a hit; measure that common cost separately.
|
||||
rmSync(join(checkout, 'node_modules'), { recursive: true, force: true })
|
||||
resetModules()
|
||||
rmSync(store, { recursive: true, force: true })
|
||||
mkdirSync(store)
|
||||
const restoreMs = transferArchive(true)
|
||||
|
||||
@@ -4,7 +4,7 @@ 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'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
|
||||
const action = parse(readFileSync('.github/actions/install-node-dependencies/action.yml', 'utf8'))
|
||||
const mode = action.runs.steps.find((step) => step.id === 'pnpm-store-mode')
|
||||
|
||||
@@ -5,7 +5,7 @@ import { runInNewContext } from 'node:vm'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
import { classifyPrJobs, PR_CHECK_JOBS } from './pr-code-change-scope.mjs'
|
||||
import { runProcessSync } from './script-child-process.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
|
||||
const action = parse(readFileSync('.github/actions/install-node-dependencies/action.yml', 'utf8'))
|
||||
const steps = action.runs.steps
|
||||
|
||||
@@ -9077,18 +9077,17 @@
|
||||
"src/shared/check-job-log-byte-cap.test.ts": 77,
|
||||
"src/shared/check-job-log-retention.test.ts": 186,
|
||||
"src/shared/check-job-log-tail-slice.test.ts": 55,
|
||||
"src/shared/child-process/bounded-output-sink.test.ts": 48,
|
||||
"src/shared/child-process/child-process-import-boundary.test.ts": 1004,
|
||||
"src/shared/child-process/close-process-registry.test.ts": 40,
|
||||
"src/shared/child-process/posix-lsof-probe.test.ts": 4289,
|
||||
"src/shared/child-process/process-tree-termination.test.ts": 68,
|
||||
"src/shared/child-process/retryable-process-exit-proof.test.ts": 38,
|
||||
"src/shared/child-process/run-process-termination-failure.test.ts": 49,
|
||||
"src/shared/child-process/run-process.test.ts": 7245,
|
||||
"src/shared/child-process/windows-cmd-shim-resolution.test.ts": 57,
|
||||
"src/shared/child-process/windows-cmd-shim-resolution.win32.test.ts": 42,
|
||||
"src/shared/child-process/windows-command-line.test.ts": 55,
|
||||
"src/shared/child-process/windows-command-line.win32.test.ts": 39,
|
||||
"src/packages/process-host/src/bounded-output-sink.test.ts": 48,
|
||||
"src/packages/process-host/src/close-process-registry.test.ts": 40,
|
||||
"src/packages/process-host/src/posix-lsof-probe.test.ts": 4289,
|
||||
"src/packages/process-host/src/process-tree-termination.test.ts": 68,
|
||||
"src/packages/process-host/src/retryable-process-exit-proof.test.ts": 38,
|
||||
"src/packages/process-host/src/run-process-termination-failure.test.ts": 49,
|
||||
"src/packages/process-host/src/run-process.test.ts": 7245,
|
||||
"src/shared/__tests__/process-host/windows-cmd-shim-resolution.test.ts": 57,
|
||||
"src/shared/__tests__/process-host/windows-cmd-shim-resolution.win32.test.ts": 42,
|
||||
"src/packages/process-host/src/windows-command-line.test.ts": 55,
|
||||
"src/shared/__tests__/process-host/windows-command-line.win32.test.ts": 39,
|
||||
"src/shared/child-process/windows-console-visibility.test.ts": 3436,
|
||||
"src/shared/claimed-agent-pty-owner.test.ts": 94,
|
||||
"src/shared/claude-agent-teams-tmux-compat.test.ts": 57,
|
||||
@@ -9231,7 +9230,7 @@
|
||||
"src/shared/gitlab-projects.test.ts": 40,
|
||||
"src/shared/grok-model-list-probe.test.ts": 49,
|
||||
"src/shared/grok-session-paths.test.ts": 87,
|
||||
"src/shared/growing-byte-buffer.test.ts": 96,
|
||||
"src/packages/process-host/src/growing-byte-buffer.test.ts": 96,
|
||||
"src/shared/handled-wire-discriminant.test.ts": 36,
|
||||
"src/shared/harness-injected-user-turns.test.ts": 42,
|
||||
"src/shared/hermes-run-ref-retention.test.ts": 29,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { globSync, readFileSync } from 'node:fs'
|
||||
import { posix, join } from 'node:path'
|
||||
import ts from 'typescript-api'
|
||||
import { createWorkspaceSourceResolver } from './workspace-source-exports.mjs'
|
||||
|
||||
const EXTENSIONS = [
|
||||
'',
|
||||
@@ -30,15 +31,22 @@ function localPath(file, specifier) {
|
||||
return null
|
||||
}
|
||||
|
||||
export function buildUnitDependencyGraph(sources) {
|
||||
export function buildUnitDependencyGraph(sources, workspace = { resolve: () => null }) {
|
||||
const reverse = new Map()
|
||||
const opaque = new Set()
|
||||
function addConsumer(input, file) {
|
||||
if (!reverse.has(input)) {
|
||||
reverse.set(input, new Set())
|
||||
}
|
||||
reverse.get(input).add(file)
|
||||
}
|
||||
for (const [file, source] of sources) {
|
||||
if (INDIRECT_INPUT.test(source) || file.startsWith('config/') || file.startsWith('tests/')) {
|
||||
opaque.add(file)
|
||||
}
|
||||
for (const imported of ts.preProcessFile(source, true, true).importedFiles) {
|
||||
const path = localPath(file, imported.fileName)
|
||||
const workspaceSource = workspace.resolve(imported.fileName)
|
||||
const path = workspaceSource?.file ?? localPath(file, imported.fileName)
|
||||
if (path === null) {
|
||||
continue
|
||||
}
|
||||
@@ -54,10 +62,10 @@ export function buildUnitDependencyGraph(sources) {
|
||||
opaque.add(file)
|
||||
continue
|
||||
}
|
||||
if (!reverse.has(resolved)) {
|
||||
reverse.set(resolved, new Set())
|
||||
if (workspaceSource) {
|
||||
addConsumer(workspaceSource.manifest, file)
|
||||
}
|
||||
reverse.get(resolved).add(file)
|
||||
addConsumer(resolved, file)
|
||||
}
|
||||
}
|
||||
return { reverse, opaque }
|
||||
@@ -70,12 +78,13 @@ export function collectUnitDependencyGraph(root = process.cwd()) {
|
||||
'config/**/*.{ts,tsx,js,mjs,cjs,json}',
|
||||
'tests/**/*.{ts,tsx,js,mjs,cjs,json}'
|
||||
],
|
||||
{ cwd: root }
|
||||
{ cwd: root, exclude: ['**/node_modules/**', '**/dist/**'] }
|
||||
)
|
||||
const sources = new Map(
|
||||
files.map((file) => [file.replaceAll('\\', '/'), readFileSync(join(root, file), 'utf8')])
|
||||
)
|
||||
return { ...buildUnitDependencyGraph(sources), files: new Set(sources.keys()) }
|
||||
const workspace = createWorkspaceSourceResolver(root)
|
||||
return { ...buildUnitDependencyGraph(sources, workspace), files: new Set(sources.keys()) }
|
||||
}
|
||||
|
||||
export function unitConsumers(seeds, reverse) {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, expect, it } from 'vitest'
|
||||
import { collectUnitDependencyGraph } from './ci-unit-dependency-graph.mjs'
|
||||
import { selectUnitFiles } from './ci-unit-selection.mjs'
|
||||
|
||||
const temporary = []
|
||||
afterEach(() => {
|
||||
for (const root of temporary.splice(0)) {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
function fixture(extra = {}) {
|
||||
const root = mkdtempSync(join(tmpdir(), 'unit-workspace-graph-'))
|
||||
temporary.push(root)
|
||||
const manifest = (name, source) =>
|
||||
JSON.stringify({
|
||||
name,
|
||||
exports: {
|
||||
'.': { 'orca-source': source, types: './dist/index.d.ts', default: './dist/index.js' }
|
||||
}
|
||||
})
|
||||
const files = {
|
||||
'pnpm-workspace.yaml': 'packages:\n - src/packages/*\n',
|
||||
'src/packages/process-host/package.json': manifest(
|
||||
'@orca/process-host',
|
||||
'./src/run-process.ts'
|
||||
),
|
||||
'src/packages/process-host/src/run-process.ts':
|
||||
"import { value } from '@orca/byte-buffer'; export { value }; throw Error('source must not execute')",
|
||||
'src/packages/process-host/dist/index.js': 'throw Error("stale output")',
|
||||
'src/packages/byte-buffer/package.json': manifest('@orca/byte-buffer', './src/byte-buffer.ts'),
|
||||
'src/packages/byte-buffer/src/byte-buffer.ts': 'export const value = 1',
|
||||
'src/consumer.test.ts': "import '@orca/process-host'",
|
||||
'src/dynamic.test.ts': "import('@orca/process-host')",
|
||||
'src/require.test.ts': "require('@orca/process-host')",
|
||||
'src/type.test.ts': "import type { value } from '@orca/process-host'",
|
||||
'src/unrelated.test.ts': 'export const unrelated = true',
|
||||
...extra
|
||||
}
|
||||
for (const [file, source] of Object.entries(files)) {
|
||||
const path = join(root, file)
|
||||
mkdirSync(join(path, '..'), { recursive: true })
|
||||
writeFileSync(path, source)
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
it('tracks transitive public workspace source consumers including type and dynamic imports', () => {
|
||||
const graph = collectUnitDependencyGraph(fixture())
|
||||
const tests = [...graph.files].filter((file) => file.endsWith('.test.ts')).sort()
|
||||
expect(
|
||||
selectUnitFiles(tests, ['src/packages/byte-buffer/src/byte-buffer.ts'], graph)
|
||||
).toMatchObject({
|
||||
full: false,
|
||||
files: [
|
||||
'src/consumer.test.ts',
|
||||
'src/dynamic.test.ts',
|
||||
'src/require.test.ts',
|
||||
'src/type.test.ts'
|
||||
]
|
||||
})
|
||||
expect(graph.files.has('src/packages/process-host/dist/index.js')).toBe(false)
|
||||
})
|
||||
|
||||
it('tracks package manifests as inputs to all public consumers', () => {
|
||||
const graph = collectUnitDependencyGraph(fixture())
|
||||
const tests = [...graph.files].filter((file) => file.endsWith('.test.ts')).sort()
|
||||
for (const manifest of [
|
||||
'src/packages/process-host/package.json',
|
||||
'src/packages/byte-buffer/package.json'
|
||||
]) {
|
||||
const selection = selectUnitFiles(tests, [manifest], graph)
|
||||
expect(selection.full).toBe(false)
|
||||
expect(selection.files).toEqual(tests.filter((file) => file !== 'src/unrelated.test.ts'))
|
||||
}
|
||||
})
|
||||
|
||||
it.each(['@orca/unknown', '@orca/process-host/private'])(
|
||||
'does not silently drop unresolved internal imports: %s',
|
||||
(specifier) => {
|
||||
expect(() =>
|
||||
collectUnitDependencyGraph(fixture({ 'src/consumer.test.ts': `import '${specifier}'` }))
|
||||
).toThrow('No public orca-source export')
|
||||
}
|
||||
)
|
||||
@@ -1,6 +1,6 @@
|
||||
import { appendFileSync, readFileSync } from 'node:fs'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { runProcessSync } from './script-child-process.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { collectUnitDependencyGraph } from './ci-unit-dependency-graph.mjs'
|
||||
import { discoverUnitFiles } from './ci-unit-files.mjs'
|
||||
import { FULL_SHARD_COUNT, planUnitSelection } from './ci-unit-selection.mjs'
|
||||
|
||||
@@ -3,12 +3,16 @@ import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { expect, it } from 'vitest'
|
||||
import { runProcessSync } from './script-child-process.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { FULL_SHARD_COUNT } from './ci-unit-selection.mjs'
|
||||
|
||||
it.each([true, false])(
|
||||
'plans from a real Git diff, with parent evidence available: %s',
|
||||
(withParent) => {
|
||||
it.each([
|
||||
{ withParent: true, unresolvedPackage: false },
|
||||
{ withParent: false, unresolvedPackage: false },
|
||||
{ withParent: true, unresolvedPackage: true }
|
||||
])(
|
||||
'plans from a real Git diff, retaining full coverage for incomplete package evidence: %j',
|
||||
({ withParent, unresolvedPackage }) => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'orca-unit-plan-'))
|
||||
const git = (args) => {
|
||||
const result = runProcessSync({ program: 'git', args, cwd: root })
|
||||
@@ -18,7 +22,10 @@ it.each([true, false])(
|
||||
try {
|
||||
mkdirSync(join(root, 'src'))
|
||||
writeFileSync(join(root, 'src/value.ts'), 'export const value = 1')
|
||||
writeFileSync(join(root, 'src/consumer.test.ts'), "import './value'")
|
||||
writeFileSync(
|
||||
join(root, 'src/consumer.test.ts'),
|
||||
unresolvedPackage ? "import '@orca/missing-package'" : "import './value'"
|
||||
)
|
||||
writeFileSync(join(root, 'src/unrelated.test.ts'), 'export const unrelated = true')
|
||||
git(['init', '--quiet'])
|
||||
git(['add', 'src'])
|
||||
@@ -62,16 +69,19 @@ it.each([true, false])(
|
||||
expect(result.code, result.stderr).toBe(0)
|
||||
const plan = JSON.parse(readFileSync(join(root, 'ci-shards/unit-selection.json'), 'utf8'))
|
||||
expect(plan.sourceSha).toBe(sourceSha)
|
||||
expect(plan.mode).toBe(withParent ? 'selected' : 'shadow')
|
||||
const selected = withParent && !unresolvedPackage
|
||||
expect(plan.mode).toBe(selected ? 'selected' : 'shadow')
|
||||
expect(plan.executionFiles).toEqual(
|
||||
withParent ? ['src/consumer.test.ts'] : ['src/consumer.test.ts', 'src/unrelated.test.ts']
|
||||
selected ? ['src/consumer.test.ts'] : ['src/consumer.test.ts', 'src/unrelated.test.ts']
|
||||
)
|
||||
expect(plan.reason).toBe(
|
||||
withParent
|
||||
? 'Transitive imports plus indirect-input consumers'
|
||||
: 'Error: Changed paths unavailable'
|
||||
!withParent
|
||||
? 'Error: Changed paths unavailable'
|
||||
: unresolvedPackage
|
||||
? 'Error: No public orca-source export for @orca/missing-package'
|
||||
: 'Transitive imports plus indirect-input consumers'
|
||||
)
|
||||
expect(plan.shards).toHaveLength(withParent ? 1 : FULL_SHARD_COUNT)
|
||||
expect(plan.shards).toHaveLength(selected ? 1 : FULL_SHARD_COUNT)
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { join } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { expect, it } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
import { runProcessSync } from '../../src/shared/child-process/run-process'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
|
||||
const workflow = parse(
|
||||
readFileSync(new URL('../../.github/workflows/cloud-verify.yml', import.meta.url), 'utf8')
|
||||
|
||||
@@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'
|
||||
import { delimiter, join } from 'node:path'
|
||||
import { parse } from 'yaml'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { runProcessSync } from '../../src/shared/child-process/run-process'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
|
||||
const workflow = parse(readFileSync('.github/workflows/e2e.yml', 'utf8'))
|
||||
const build = workflow.jobs.build.steps.find((step) => step.id === 'e2e-cli')
|
||||
|
||||
@@ -142,6 +142,8 @@ describe('Electron Vite output contract', () => {
|
||||
expect(external('@parcel/watcher', undefined, false)).toBe(true)
|
||||
expect(external('electron', undefined, false)).toBe(true)
|
||||
expect(external('node:fs', undefined, false)).toBe(true)
|
||||
expect(external('@orca/process-host', undefined, false)).toBe(true)
|
||||
expect(external('@orca/process-host/spawn-observer', undefined, false)).toBe(true)
|
||||
expect(external('@xterm/headless', undefined, false)).toBe(false)
|
||||
expect(external('@xterm/addon-serialize', undefined, false)).toBe(false)
|
||||
expect(external('tldts', undefined, false)).toBe(false)
|
||||
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
stageWindowsProcessTreeNodeAddonApiHeaders,
|
||||
windowsProcessTreeAddonPath
|
||||
} from './windows-process-tree-gyp-rebuild.mjs'
|
||||
import { describeProcessFailure, runProcessSync } from './script-child-process.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { describeProcessFailure } from './process-failure-message.mjs'
|
||||
import { disableMsbuildFileTrackingOnWindows } from './msbuild-file-tracking.mjs'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
@@ -9,13 +9,12 @@ import {
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { createRequire } from 'node:module'
|
||||
import { isAbsolute, join, parse } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
DEFAULT_MAX_OUTPUT_BYTES,
|
||||
runProcessSync
|
||||
} from '../../src/shared/child-process/run-process.ts'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { DEFAULT_MAX_OUTPUT_BYTES } from '@orca/process-host/process-spec'
|
||||
import { resolveCliCommand } from '../../src/shared/node-cli-command-resolution.ts'
|
||||
import { removeTreeSync } from '../../src/shared/windows-transient-lock-removal.ts'
|
||||
import { resolvePnpmCliInvocation } from './pnpm-cli-invocation.mjs'
|
||||
@@ -277,11 +276,16 @@ function mkTempProject() {
|
||||
// Walked, not listed: the script imports windows-process-tree-gyp-rebuild.mjs, and a fixture
|
||||
// missing it fails every case with a module-resolution error instead of the defect under test.
|
||||
copyScriptWithLocalModules(sourceScriptPath, join(projectDir, 'config', 'scripts'))
|
||||
const processPackage = join(projectDir, 'node_modules', '@orca', 'process-host')
|
||||
mkdirSync(processPackage, { recursive: true })
|
||||
writeFileSync(
|
||||
join(projectDir, 'config', 'scripts', 'script-child-process.mjs'),
|
||||
join(processPackage, 'package.json'),
|
||||
JSON.stringify({ name: '@orca/process-host', type: 'module', exports: './index.mjs' })
|
||||
)
|
||||
writeFileSync(
|
||||
join(processPackage, 'index.mjs'),
|
||||
`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 }
|
||||
import { runProcessSync as run } from ${JSON.stringify(pathToFileURL(createRequire(import.meta.url).resolve('@orca/process-host')).href)}
|
||||
export function runProcessSync(options) {
|
||||
appendFileSync(process.env.ORCA_NATIVE_TEST_LOG, \`node-gyp timeout=\${options.timeoutMs}\\n\`)
|
||||
return run(options)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
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 { runProcessSync } from '@orca/process-host'
|
||||
import { describeProcessFailure } from './process-failure-message.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
|
||||
|
||||
@@ -61,6 +61,7 @@ export function packCompilerCache({ root = ROOT, identity = compilerCacheIdentit
|
||||
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] }))
|
||||
const yamlDir = dirname(require.resolve('yaml/package.json'))
|
||||
rmSync(identity.path, { recursive: true, force: true })
|
||||
mkdirSync(join(identity.path, 'node_modules', '@esbuild'), { recursive: true })
|
||||
cpSync(esbuildDir, join(identity.path, 'node_modules', 'esbuild'), {
|
||||
@@ -71,6 +72,10 @@ export function packCompilerCache({ root = ROOT, identity = compilerCacheIdentit
|
||||
recursive: true,
|
||||
dereference: true
|
||||
})
|
||||
cpSync(yamlDir, join(identity.path, 'node_modules', 'yaml'), {
|
||||
recursive: true,
|
||||
dereference: true
|
||||
})
|
||||
writeFileSync(
|
||||
join(identity.path, 'manifest.json'),
|
||||
JSON.stringify({
|
||||
@@ -103,7 +108,7 @@ export async function activateCompilerCache({
|
||||
}
|
||||
mkdirSync(join(dependencies, '@esbuild'), { recursive: true })
|
||||
created = true
|
||||
for (const name of ['esbuild', `@esbuild/${process.platform}-${process.arch}`]) {
|
||||
for (const name of ['esbuild', `@esbuild/${process.platform}-${process.arch}`, 'yaml']) {
|
||||
symlinkSync(join(identity.path, 'node_modules', name), join(dependencies, name), 'dir')
|
||||
}
|
||||
const require = createRequire(join(root, 'package.json'))
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
packCompilerCache
|
||||
} from './headless-detector-compiler-cache.mjs'
|
||||
import { collectNodeServerInputs } from './node-server-change-scope.mjs'
|
||||
import { runProcessSync } from './script-child-process.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
|
||||
const temporary = []
|
||||
afterEach(() => {
|
||||
@@ -66,15 +66,16 @@ it('separates policy, Node, platform and architecture identities with the same a
|
||||
}
|
||||
})
|
||||
|
||||
it('activates only the actual compiler packages and preserves import graph behavior', async () => {
|
||||
it('activates the compiler and workspace-policy parser 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'])
|
||||
expect(readdirSync(join(root, 'node_modules')).sort()).toEqual(['@esbuild', 'esbuild', 'yaml'])
|
||||
for (const name of [
|
||||
'node-server-change-scope',
|
||||
'node-server-test-paths',
|
||||
'node-server-qualification',
|
||||
'orcad-entry-build'
|
||||
'orcad-entry-build',
|
||||
'workspace-source-exports'
|
||||
]) {
|
||||
mkdirSync(join(root, 'config', 'scripts'), { recursive: true })
|
||||
cpSync(
|
||||
@@ -89,7 +90,20 @@ it('activates only the actual compiler packages and preserves import graph behav
|
||||
)
|
||||
writeFileSync(
|
||||
join(root, 'entry.ts'),
|
||||
"import './first'; export * from './exports'; import('./dynamic'); require('./required'); import 'external-package'; import './native.node'"
|
||||
"import './first'; export * from './exports'; import('./dynamic'); require('./required'); import 'external-package'; import './native.node'; import '@orca/process-host'"
|
||||
)
|
||||
writeFileSync(join(root, 'pnpm-workspace.yaml'), 'packages:\n - src/packages/*\n')
|
||||
mkdirSync(join(root, 'src/packages/process-host/src'), { recursive: true })
|
||||
writeFileSync(
|
||||
join(root, 'src/packages/process-host/package.json'),
|
||||
JSON.stringify({
|
||||
name: '@orca/process-host',
|
||||
exports: { '.': { 'orca-source': './src/run-process.ts', default: './dist/run-process.js' } }
|
||||
})
|
||||
)
|
||||
writeFileSync(
|
||||
join(root, 'src/packages/process-host/src/run-process.ts'),
|
||||
'export const value = 1'
|
||||
)
|
||||
for (const name of ['first', 'exports', 'dynamic', 'required']) {
|
||||
writeFileSync(join(root, `${name}.ts`), 'export const value = 1')
|
||||
@@ -99,6 +113,8 @@ it('activates only the actual compiler packages and preserves import graph behav
|
||||
"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()
|
||||
expect(baseline).toContain('src/packages/process-host/src/run-process.ts')
|
||||
expect(baseline).toContain('src/packages/process-host/package.json')
|
||||
const candidate = runProcessSync({
|
||||
program: process.execPath,
|
||||
args: ['probe.mjs'],
|
||||
|
||||
@@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
import { runProcess } from '../../src/shared/child-process/run-process'
|
||||
import { runProcess } from '@orca/process-host'
|
||||
|
||||
const workflow = parse(
|
||||
readFileSync(new URL('../../.github/workflows/hourly-mac-build.yml', import.meta.url), 'utf8')
|
||||
|
||||
@@ -4,7 +4,7 @@ import { join, resolve } from 'node:path'
|
||||
import { build } from 'vite'
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||||
import { electronViteConfig } from '../../electron.vite.config'
|
||||
import { runProcess } from '../../src/shared/child-process/run-process'
|
||||
import { runProcess } from '@orca/process-host'
|
||||
|
||||
const projectDir = resolve(import.meta.dirname, '../..')
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'
|
||||
import { delimiter, join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
import { runProcessSync } from '../../src/shared/child-process/run-process'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
|
||||
const action = parse(
|
||||
readFileSync('.github/actions/prepare-linux-package-fixture/action.yml', 'utf8')
|
||||
|
||||
@@ -5,7 +5,8 @@ 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'
|
||||
import { runProcess } from '@orca/process-host'
|
||||
import { stagePackagedProcessHost } from './packaged-process-host-fixture.mjs'
|
||||
|
||||
const projectDir = resolve(import.meta.dirname, '../..')
|
||||
const require = createRequire(import.meta.url)
|
||||
@@ -36,6 +37,7 @@ beforeAll(async () => {
|
||||
}
|
||||
}
|
||||
})
|
||||
await stagePackagedProcessHost(outputDir)
|
||||
mkdirSync(join(outputDir, 'source', 'devin'), { recursive: true })
|
||||
writeFileSync(
|
||||
join(outputDir, 'source', 'devin', 'credentials.toml'),
|
||||
@@ -59,6 +61,8 @@ describe('managed account credentials in the production main bundle', () => {
|
||||
const environment: Record<string, string | undefined> = {
|
||||
...process.env,
|
||||
ORCA_BACKGROUND_LAUNCH: '1',
|
||||
NODE_PATH: '',
|
||||
NODE_OPTIONS: '',
|
||||
ELECTRON_RUN_AS_NODE: '1'
|
||||
}
|
||||
for (const key of [
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import {
|
||||
cpSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
symlinkSync,
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { afterEach, expect, it } from 'vitest'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { removeTreeSync } from '../../src/shared/windows-transient-lock-removal.ts'
|
||||
|
||||
const repository = resolve(import.meta.dirname, '../..')
|
||||
const require = createRequire(import.meta.url)
|
||||
const tsx = join(dirname(require.resolve('tsx/package.json')), 'dist', 'cli.mjs')
|
||||
const manifest = JSON.parse(readFileSync(join(repository, 'mobile', 'package.json'), 'utf8'))
|
||||
const temporary = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of temporary.splice(0)) {
|
||||
removeTreeSync(root)
|
||||
}
|
||||
})
|
||||
|
||||
function standaloneMobile() {
|
||||
const root = mkdtempSync(join(tmpdir(), 'orca-mobile-process-tooling-'))
|
||||
temporary.push(root)
|
||||
const mobile = join(root, 'mobile')
|
||||
const dependency = manifest.devDependencies['@orca/process-host']
|
||||
expect(dependency).toBe('link:../src/packages/process-host')
|
||||
const packageDir = resolve(mobile, dependency.slice('link:'.length))
|
||||
mkdirSync(packageDir, { recursive: true })
|
||||
for (const file of ['package.json', 'src']) {
|
||||
cpSync(join(repository, 'src', 'packages', 'process-host', file), join(packageDir, file), {
|
||||
recursive: true
|
||||
})
|
||||
}
|
||||
for (const file of [
|
||||
'scripts',
|
||||
'src/test-support/rpc-recording',
|
||||
'rpc-foundation/pilot-scenarios.json'
|
||||
]) {
|
||||
const destination = join(mobile, file)
|
||||
mkdirSync(dirname(destination), { recursive: true })
|
||||
cpSync(join(repository, 'mobile', file), destination, { recursive: true })
|
||||
}
|
||||
writeFileSync(join(mobile, 'package.json'), JSON.stringify(manifest))
|
||||
mkdirSync(join(mobile, 'rpc-foundation', 'goldens'), { recursive: true })
|
||||
mkdirSync(join(mobile, 'node_modules', '@orca'), { recursive: true })
|
||||
symlinkSync(
|
||||
packageDir,
|
||||
join(mobile, 'node_modules', '@orca', 'process-host'),
|
||||
process.platform === 'win32' ? 'junction' : 'dir'
|
||||
)
|
||||
expect(existsSync(join(root, 'node_modules'))).toBe(false)
|
||||
expect(existsSync(join(packageDir, 'dist'))).toBe(false)
|
||||
return { root, mobile }
|
||||
}
|
||||
|
||||
function command(cwd, program, args) {
|
||||
return runProcessSync({
|
||||
program,
|
||||
args,
|
||||
cwd,
|
||||
timeoutMs: 15_000,
|
||||
env: { ...process.env, NODE_PATH: '', NODE_OPTIONS: '', ORCA_BACKGROUND_LAUNCH: '1' }
|
||||
})
|
||||
}
|
||||
|
||||
function rpcCommand(mobile, script, args) {
|
||||
const [launcher, ...invocation] = manifest.scripts[script].split(' ')
|
||||
expect(launcher).toBe('tsx')
|
||||
return command(mobile, process.env.ORCA_TEST_NODE_EXECUTABLE ?? process.execPath, [
|
||||
tsx,
|
||||
...invocation,
|
||||
...args
|
||||
])
|
||||
}
|
||||
|
||||
it('runs the RPC diff with a mobile-only dependency link and no compiled host package', () => {
|
||||
const { root, mobile } = standaloneMobile()
|
||||
const init = command(root, 'git', ['init'])
|
||||
expect(init.code, init.stderr).toBe(0)
|
||||
const commit = command(root, 'git', [
|
||||
'-c',
|
||||
'user.name=Orca Test',
|
||||
'-c',
|
||||
'user.email=orca-test@example.invalid',
|
||||
'-c',
|
||||
'commit.gpgsign=false',
|
||||
'-c',
|
||||
`core.hooksPath=${join(root, 'no-hooks')}`,
|
||||
'commit',
|
||||
'--allow-empty',
|
||||
'-m',
|
||||
'fixture'
|
||||
])
|
||||
expect(commit.code, commit.stderr).toBe(0)
|
||||
const result = rpcCommand(mobile, 'rpc:diff', ['HEAD'])
|
||||
expect(result.code, result.stderr).toBe(0)
|
||||
expect(result.stdout).toContain('No recorded behaviour moved')
|
||||
})
|
||||
|
||||
it('launches the recording runner with a mobile-only dependency link and no compiled host package', () => {
|
||||
const { mobile } = standaloneMobile()
|
||||
const vitest = join(mobile, 'node_modules', 'vitest')
|
||||
mkdirSync(vitest)
|
||||
writeFileSync(join(vitest, 'package.json'), '{"name":"vitest","version":"0.0.0"}')
|
||||
writeFileSync(
|
||||
join(vitest, 'vitest.mjs'),
|
||||
'process.stdout.write(JSON.stringify(process.argv.slice(2)))'
|
||||
)
|
||||
const { scenarios } = JSON.parse(
|
||||
readFileSync(join(mobile, 'rpc-foundation', 'pilot-scenarios.json'), 'utf8')
|
||||
)
|
||||
const result = rpcCommand(mobile, 'rpc:record', [scenarios[0].id])
|
||||
expect(result.code, result.stderr).toBe(0)
|
||||
expect(JSON.parse(result.stdout)).toEqual([
|
||||
'run',
|
||||
'src/test-support/rpc-recording/pilot-recordings.test.ts',
|
||||
'src/test-support/rpc-recording/family-recordings.test.ts',
|
||||
'-t',
|
||||
expect.any(String)
|
||||
])
|
||||
})
|
||||
@@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'
|
||||
import { dirname, join, matchesGlob, resolve } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
import { runProcessSync } from './script-child-process.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { shouldRunMobileReleaseChecks } from './mobile-release-check-scope.mjs'
|
||||
|
||||
const root = resolve(import.meta.dirname, '../..')
|
||||
|
||||
@@ -29,6 +29,7 @@ it.each([
|
||||
'pnpm-lock.yaml',
|
||||
'src/main/runtime/runtime-rpc.ts',
|
||||
'src/shared/protocol-version.ts',
|
||||
'src/packages/process-host/src/run-process.ts',
|
||||
'.github/workflows/mobile.yml',
|
||||
'.github/actions/install-node-dependencies/action.yml',
|
||||
'config/scripts/mobile-test-change-scope.mjs',
|
||||
@@ -43,14 +44,22 @@ it('retains tests on missing evidence and a move out of the source tree', () =>
|
||||
expect(shouldRunMobileTests(['mobile/src/deleted.ts', 'mobile/docs/moved.ts'])).toBe(true)
|
||||
})
|
||||
|
||||
it('runs Mobile Checks on any shared-module or root lockfile change, on a pull request and on main', () => {
|
||||
it('runs Mobile Checks for shared modules, process-host tooling and the root lockfile on PRs and main', () => {
|
||||
const workflow = parse(
|
||||
readFileSync(new URL('../../.github/workflows/mobile.yml', import.meta.url), 'utf8')
|
||||
)
|
||||
for (const paths of [workflow.on.pull_request.paths, workflow.on.push.paths]) {
|
||||
for (const file of ['src/shared/any/module.ts', 'pnpm-lock.yaml']) {
|
||||
for (const file of [
|
||||
'src/shared/any/module.ts',
|
||||
'src/packages/process-host/src/run-process.ts',
|
||||
'src/packages/process-host/package.json',
|
||||
'pnpm-lock.yaml'
|
||||
]) {
|
||||
expect(paths.some((pattern) => matchesGlob(file, pattern))).toBe(true)
|
||||
}
|
||||
expect(
|
||||
paths.some((pattern) => matchesGlob('src/packages/unrelated/src/index.ts', pattern))
|
||||
).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createServer } from 'node:https'
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { runProcess } from '../../src/shared/child-process/run-process'
|
||||
import { runProcess } from '@orca/process-host'
|
||||
|
||||
/**
|
||||
* A real TLS origin for the artifact's images, because interception could not measure them.
|
||||
|
||||
@@ -31,9 +31,15 @@ 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/workspace-source-exports.mjs',
|
||||
'config/scripts/workspace-source-exports.test.mjs',
|
||||
'config/scripts/headless-detector-compiler-cache.mjs',
|
||||
'config/scripts/node-server-qualification.mjs',
|
||||
'config/scripts/node-server-qualification.test.mjs'
|
||||
'config/scripts/node-server-qualification.test.mjs',
|
||||
// The source graph sees process-host source, not the build that emits the dist servers load.
|
||||
'src/packages/process-host/.gitignore',
|
||||
'src/packages/process-host/scripts/build-dist.mjs',
|
||||
'src/packages/process-host/tsconfig.json'
|
||||
])
|
||||
const ALWAYS_PREFIXES = [
|
||||
'.github/actions/install-node-dependencies/',
|
||||
@@ -72,8 +78,28 @@ export async function collectNodeServerInputs({ root = ROOT, entryPoints } = {})
|
||||
ORCAD_CHILD_ENTRY_POINTS,
|
||||
ORCAD_ENTRY_POINT,
|
||||
ORCAD_LAUNCHER_ENTRY_POINT
|
||||
},
|
||||
{ createWorkspaceSourceResolver }
|
||||
] = await Promise.all([
|
||||
import('esbuild'),
|
||||
import('./orcad-entry-build.mjs'),
|
||||
import('./workspace-source-exports.mjs')
|
||||
])
|
||||
const workspace = createWorkspaceSourceResolver(root)
|
||||
const manifests = new Set()
|
||||
const workspaceSources = {
|
||||
name: 'workspace-source-exports',
|
||||
setup(pluginBuild) {
|
||||
pluginBuild.onResolve({ filter: /^[^./]/ }, ({ path }) => {
|
||||
const source = workspace.resolve(path)
|
||||
if (!source) {
|
||||
return null
|
||||
}
|
||||
manifests.add(source.manifest)
|
||||
return { path: source.path }
|
||||
})
|
||||
}
|
||||
] = await Promise.all([import('esbuild'), import('./orcad-entry-build.mjs')])
|
||||
}
|
||||
const entries = entryPoints ?? [
|
||||
ORCAD_ENTRY_POINT,
|
||||
ORCAD_LAUNCHER_ENTRY_POINT,
|
||||
@@ -92,18 +118,19 @@ export async function collectNodeServerInputs({ root = ROOT, entryPoints } = {})
|
||||
splitting: true,
|
||||
packages: 'external',
|
||||
loader: { '.svg': 'empty', '.png': 'empty', '.webp': 'empty', '.css': 'empty' },
|
||||
plugins: [externalNativeAddons],
|
||||
plugins: [workspaceSources, externalNativeAddons],
|
||||
metafile: true,
|
||||
logLevel: 'silent'
|
||||
})
|
||||
if (result.warnings.length > 0) {
|
||||
throw new Error(result.warnings.map((warning) => warning.text).join('\n'))
|
||||
}
|
||||
return new Set(
|
||||
Object.keys(result.metafile.inputs).map((file) =>
|
||||
return new Set([
|
||||
...manifests,
|
||||
...Object.keys(result.metafile.inputs).map((file) =>
|
||||
file.replaceAll('\\', '/').replace(/\?.*$/, '')
|
||||
)
|
||||
)
|
||||
])
|
||||
}
|
||||
|
||||
function isSourceUnitTest(file) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { nodeServerTestPaths } from './node-server-test-paths.mjs'
|
||||
import { ORCAD_CHILD_ENTRY_POINTS } from './orcad-entry-build.mjs'
|
||||
import { NODE_RUNTIME_PIN } from '../../src/shared/node-runtime-pin.ts'
|
||||
import { runProcessSync } from './script-child-process.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { NODE_SERVER_RUNNERS } from './node-server-qualification.mjs'
|
||||
|
||||
const temporaryDirs = []
|
||||
@@ -63,6 +63,82 @@ it('runs the matrix when a dependency is deleted or graph analysis fails', async
|
||||
expect((await classifyNodeServerChanges([])).shouldRun).toBe(true)
|
||||
})
|
||||
|
||||
function workspaceTree(imported = '@orca/process-host') {
|
||||
const manifest = (name, source) =>
|
||||
JSON.stringify({
|
||||
name,
|
||||
exports: { '.': { 'orca-source': source, default: './dist/index.js' } }
|
||||
})
|
||||
return moduleTree({
|
||||
'entry.ts': `export * from '${imported}'`,
|
||||
'pnpm-workspace.yaml': 'packages:\n - src/packages/*\n',
|
||||
'src/packages/process-host/package.json': manifest(
|
||||
'@orca/process-host',
|
||||
'./src/run-process.ts'
|
||||
),
|
||||
'src/packages/process-host/src/run-process.ts':
|
||||
"import('@orca/byte-buffer'); require('./private'); export const value = 1",
|
||||
'src/packages/process-host/src/private.ts': 'module.exports = 2',
|
||||
'src/packages/process-host/dist/index.js': "require('./missing-stale-output')",
|
||||
'src/packages/byte-buffer/package.json': manifest('@orca/byte-buffer', './src/byte-buffer.ts'),
|
||||
'src/packages/byte-buffer/src/byte-buffer.ts': 'export const value = 3',
|
||||
'src/packages/unrelated/package.json': manifest('@orca/unrelated', './src/unrelated.ts'),
|
||||
'src/packages/unrelated/src/unrelated.ts': 'throw Error("unrelated")'
|
||||
})
|
||||
}
|
||||
|
||||
it('tracks workspace source and manifest closures without trusting dist or widening to unrelated packages', async () => {
|
||||
const root = workspaceTree()
|
||||
const inputs = await collectNodeServerInputs({ root, entryPoints: ['entry.ts'] })
|
||||
expect([...inputs].sort()).toEqual([
|
||||
'entry.ts',
|
||||
'src/packages/byte-buffer/package.json',
|
||||
'src/packages/byte-buffer/src/byte-buffer.ts',
|
||||
'src/packages/process-host/package.json',
|
||||
'src/packages/process-host/src/private.ts',
|
||||
'src/packages/process-host/src/run-process.ts'
|
||||
])
|
||||
for (const file of [
|
||||
'src/packages/process-host/package.json',
|
||||
'src/packages/process-host/src/private.ts',
|
||||
'src/packages/byte-buffer/src/byte-buffer.ts'
|
||||
]) {
|
||||
expect((await classifyNodeServerChanges([file], async () => inputs)).shouldRun).toBe(true)
|
||||
}
|
||||
expect(
|
||||
(
|
||||
await classifyNodeServerChanges(
|
||||
['src/packages/unrelated/src/unrelated.ts'],
|
||||
async () => inputs
|
||||
)
|
||||
).shouldRun
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it.each([
|
||||
'src/packages/process-host/.gitignore',
|
||||
'src/packages/process-host/scripts/build-dist.mjs',
|
||||
'src/packages/process-host/tsconfig.json'
|
||||
])('runs for a change to the process-host build input %s', async (file) => {
|
||||
const result = await classifyNodeServerChanges([file], async () => new Set())
|
||||
expect(result.shouldRun).toBe(true)
|
||||
expect(result.reason).toBe(`Build or CI input changed: ${file}`)
|
||||
})
|
||||
|
||||
it.each(['@orca/unknown', '@orca/process-host/private'])(
|
||||
'retains qualification for an unresolved internal package export: %s',
|
||||
async (specifier) => {
|
||||
const root = workspaceTree(specifier)
|
||||
const result = await classifyNodeServerChanges(
|
||||
['src/packages/process-host/src/run-process.ts'],
|
||||
() => collectNodeServerInputs({ root, entryPoints: ['entry.ts'] })
|
||||
)
|
||||
expect(result.shouldRun).toBe(true)
|
||||
expect(result.graphUnavailable).toBe(true)
|
||||
expect(result.reason).toContain('No public orca-source export')
|
||||
}
|
||||
)
|
||||
|
||||
it.each([
|
||||
['tests/e2e/daemon-running-work-probe.unit.test.ts'],
|
||||
['config/scripts/zip-extractor-command.test.mjs'],
|
||||
@@ -88,6 +164,8 @@ it.each([
|
||||
'.github/actions/restore-pnpm-verification/action.yml',
|
||||
'.github/actions/prepare-headless-compiler/action.yml',
|
||||
'config/scripts/headless-detector-compiler-cache.mjs',
|
||||
'config/scripts/workspace-source-exports.mjs',
|
||||
'config/scripts/workspace-source-exports.test.mjs',
|
||||
'.github/actions/prepare-native-runtime/action.yml',
|
||||
'.github/actions/prepare-orcad-prebuilds/action.yml',
|
||||
'.github/workflows/node-server-tests.yml',
|
||||
|
||||
@@ -24,7 +24,7 @@ const PLATFORM_PREFIXES = [
|
||||
'src/main/orcad/',
|
||||
'src/main/daemon/',
|
||||
'src/main/wsl/',
|
||||
'src/shared/child-process/'
|
||||
'src/packages/process-host/'
|
||||
]
|
||||
|
||||
const PLATFORM_FAMILIES = [
|
||||
|
||||
@@ -32,7 +32,9 @@ it.each([
|
||||
'.github/actions/prepare-orcad-prebuilds/action.yml',
|
||||
'src/main/ssh/ssh-provider.ts',
|
||||
'src/main/providers/local-pty-provider.ts',
|
||||
'src/shared/child-process/run-process.ts',
|
||||
'src/packages/process-host/src/run-process.ts',
|
||||
'src/packages/process-host/src/growing-byte-buffer.ts',
|
||||
'src/packages/process-host/package.json',
|
||||
'src/main/persistence/profile-state/store.ts',
|
||||
'src/main/sqlite/database.ts',
|
||||
'src/main/orcad/entry.ts',
|
||||
@@ -70,6 +72,16 @@ it.each([
|
||||
false
|
||||
],
|
||||
['src/main/wsl/runner.ts', ['ubuntu-22.04', 'windows-2022', 'windows-11-arm'], false],
|
||||
[
|
||||
'src/packages/process-host/src/windows-command-line.ts',
|
||||
['ubuntu-22.04', 'windows-2022', 'windows-11-arm'],
|
||||
false
|
||||
],
|
||||
[
|
||||
'src/shared/__tests__/process-host/windows-command-line.win32.test.ts',
|
||||
['ubuntu-22.04', 'windows-2022', 'windows-11-arm'],
|
||||
false
|
||||
],
|
||||
[
|
||||
'src/main/orcad/orcad-launcher.win32.test.ts',
|
||||
['ubuntu-22.04', 'windows-2022', 'windows-11-arm'],
|
||||
|
||||
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { runProcessSync } from '../../src/shared/child-process/run-process'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
|
||||
const hooks = readFileSync(new URL('../nsis/orca-installer-hooks.nsh', import.meta.url), 'utf8')
|
||||
const processCheck = readFileSync(
|
||||
|
||||
@@ -22,7 +22,7 @@ export function stageNodeAddonApi(sourceDir, stagedDir) {
|
||||
export async function nodeGypRebuild({ stagedDir, workDir, nodeDir, staticCxxRuntime }) {
|
||||
const compileGypi = join(workDir, 'prebuild-compile.gypi')
|
||||
writeFileSync(compileGypi, prebuildCompileGypi({ staticCxxRuntime }))
|
||||
const { runProcessSync } = await import('./script-child-process.mjs')
|
||||
const { runProcessSync } = await import('@orca/process-host')
|
||||
const result = runProcessSync({
|
||||
program: process.execPath,
|
||||
args: [
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
readManifest
|
||||
} from './orcad-prebuild-slot-contents.mjs'
|
||||
import { ensurePinnedNodeExecutable } from './pinned-node-downloads.mjs'
|
||||
import { runProcessSync } from './script-child-process.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
|
||||
@@ -31,17 +31,22 @@ export const WINDOWS_PREBUILD_CACHE_INPUTS = [
|
||||
'config/scripts/windows-pe-machine.cjs',
|
||||
'config/scripts/pinned-node-downloads.mjs',
|
||||
'config/scripts/update-node-runtime-pin.mjs',
|
||||
'config/scripts/script-child-process.mjs',
|
||||
'src/shared/child-process/run-process.ts',
|
||||
'src/shared/child-process/spawn-resolution.ts',
|
||||
'src/shared/child-process/process-tree-termination.ts',
|
||||
'src/shared/child-process/process-tree-kill-gate.ts',
|
||||
'src/shared/child-process/spawn-observer.ts',
|
||||
'src/shared/child-process/bounded-output-sink.ts',
|
||||
'src/shared/child-process/child-termination-reporter.ts',
|
||||
'src/shared/child-process/process-spec.ts',
|
||||
'src/shared/child-process/windows-command-line.ts',
|
||||
'src/shared/child-process/windows-cmd-shim-resolution.ts'
|
||||
'src/packages/process-host/.gitignore',
|
||||
'src/packages/process-host/package.json',
|
||||
'src/packages/process-host/scripts/build-dist.mjs',
|
||||
'src/packages/process-host/tsconfig.json',
|
||||
'src/packages/process-host/src/run-process.ts',
|
||||
'src/packages/process-host/src/spawn-resolution.ts',
|
||||
'src/packages/process-host/src/process-tree-termination.ts',
|
||||
'src/packages/process-host/src/process-tree-kill-gate.ts',
|
||||
'src/packages/process-host/src/spawn-observer.ts',
|
||||
'src/packages/process-host/src/bounded-output-sink.ts',
|
||||
'src/packages/process-host/src/growing-byte-buffer.ts',
|
||||
'src/packages/process-host/src/child-termination-reporter.ts',
|
||||
'src/packages/process-host/src/process-spec.ts',
|
||||
'src/packages/process-host/src/windows-command-line.ts',
|
||||
'src/packages/process-host/src/windows-cmd-shim-resolution.ts',
|
||||
'src/packages/process-host/src/windows-system-binary.ts'
|
||||
]
|
||||
|
||||
function windowsSlot(platform, arch) {
|
||||
|
||||
@@ -219,13 +219,23 @@ describe('Windows server prebuild cache inputs', () => {
|
||||
'process-tree-kill-gate',
|
||||
'spawn-observer',
|
||||
'bounded-output-sink',
|
||||
'growing-byte-buffer',
|
||||
'child-termination-reporter',
|
||||
'process-spec',
|
||||
'windows-command-line',
|
||||
'windows-cmd-shim-resolution'
|
||||
].map((name) => `src/shared/child-process/${name}.ts`)
|
||||
'windows-cmd-shim-resolution',
|
||||
'windows-system-binary'
|
||||
].map((name) => `src/packages/process-host/src/${name}.ts`)
|
||||
)
|
||||
)
|
||||
expect(WINDOWS_PREBUILD_CACHE_INPUTS).toEqual(
|
||||
expect.arrayContaining([
|
||||
'src/packages/process-host/.gitignore',
|
||||
'src/packages/process-host/package.json',
|
||||
'src/packages/process-host/scripts/build-dist.mjs',
|
||||
'src/packages/process-host/tsconfig.json'
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it.each(WINDOWS_PREBUILD_CACHE_INPUTS)('invalidates a changed %s input', (file) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { join } from 'node:path'
|
||||
import { runInNewContext } from 'node:vm'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
import { runProcessSync } from './script-child-process.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
|
||||
const workflow = parse(readFileSync('.github/workflows/node-server-tests.yml', 'utf8'))
|
||||
const steps = workflow.jobs.persistence.steps
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { mkdirSync, symlinkSync, writeFileSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { describeProcessFailure, runProcessSync } from './script-child-process.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { describeProcessFailure } from './process-failure-message.mjs'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { tmpdir } from 'node:os'
|
||||
import { join, relative } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { preparePrAppImageTools } from './package-linux-formats-appimage.mjs'
|
||||
import { runProcessSync } from './script-child-process.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const configuration = { toolsets: { appimage: '1.0.3' } }
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { copyPrivateTree } from './space-sharing-copy.mjs'
|
||||
import { spawnProcess } from './script-child-process.mjs'
|
||||
import { spawnProcess } from '@orca/process-host'
|
||||
import { preparePrAppImageTools } from './package-linux-formats-appimage.mjs'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { mkdir, writeFile } from 'node:fs/promises'
|
||||
import { createRequire } from 'node:module'
|
||||
import { join } from 'node:path'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const requireFromBuilder = createRequire(require.resolve('electron-builder/package.json'))
|
||||
const { copyFiles, FileMatcher } = requireFromBuilder('app-builder-lib/out/fileMatcher')
|
||||
const { createPackagedRuntimeNodeModuleResources } = require('../packaged-runtime-node-modules.cjs')
|
||||
|
||||
export async function stagePackagedProcessHost(resourcesDir) {
|
||||
const resource = createPackagedRuntimeNodeModuleResources().find(
|
||||
(entry) => entry.to === join('node_modules', '@orca', 'process-host')
|
||||
)
|
||||
if (!resource) {
|
||||
throw new Error('Packaged process-host resource is missing')
|
||||
}
|
||||
const packageDir = join(resourcesDir, resource.to)
|
||||
await copyFiles([new FileMatcher(resource.from, packageDir, (value) => value, resource.filter)])
|
||||
return packageDir
|
||||
}
|
||||
|
||||
/** Writes an unpacked-CLI consumer that checks require/import identity, shared state, and subprocesses. */
|
||||
export async function writePackagedProcessHostConsumer(resourcesDir) {
|
||||
const outDir = join(resourcesDir, 'app.asar.unpacked', 'out')
|
||||
const cliDir = join(outDir, 'cli')
|
||||
await mkdir(cliDir, { recursive: true })
|
||||
await writeFile(join(outDir, 'package.json'), '{"type":"commonjs"}\n', 'utf8')
|
||||
const entry = join(cliDir, 'index.js')
|
||||
await writeFile(
|
||||
entry,
|
||||
[
|
||||
"const assert = require('node:assert/strict')",
|
||||
"const { once } = require('node:events')",
|
||||
"const { runProcessSync, runProcess, spawnProcess } = require('@orca/process-host')",
|
||||
"const { setSpawnObserver } = require('@orca/process-host/spawn-observer')",
|
||||
"const { setProcessTreeKillGate } = require('@orca/process-host/process-tree-kill-gate')",
|
||||
"const { signalProcessTree } = require('@orca/process-host/process-tree-termination')",
|
||||
"import('@orca/process-host').then(async ({ runProcessSync: esmRunProcessSync }) => {",
|
||||
'assert.equal(runProcessSync, esmRunProcessSync)',
|
||||
'const result = runProcessSync({',
|
||||
' program: process.execPath,',
|
||||
" args: ['-e', 'process.stdout.write(\"copied-runtime\")']",
|
||||
'})',
|
||||
'assert.equal(result.code, 0)',
|
||||
'const observedCommands = []',
|
||||
'const gateCalls = []',
|
||||
'setSpawnObserver((command) => observedCommands.push(command))',
|
||||
'let child, closed, watchdog',
|
||||
'try {',
|
||||
'const observed = await runProcess({',
|
||||
' program: process.execPath,',
|
||||
" args: ['-e', 'process.stdout.write(\"observed-spawn\")']",
|
||||
'})',
|
||||
'assert.equal(observed.code, 0)',
|
||||
'assert.equal(observed.stdout, "observed-spawn")',
|
||||
'assert.deepEqual(observedCommands, [process.execPath])',
|
||||
'child = spawnProcess({',
|
||||
' program: process.execPath,',
|
||||
" args: ['-e', 'setInterval(() => {}, 1000)'],",
|
||||
' detached: true',
|
||||
'})',
|
||||
'closed = once(child, "close")',
|
||||
'watchdog = setTimeout(() => child.kill("SIGKILL"), 5000)',
|
||||
'const rootSignals = []',
|
||||
'const killRoot = child.kill.bind(child)',
|
||||
'child.kill = (signal) => { rootSignals.push(signal); return killRoot(signal) }',
|
||||
'setProcessTreeKillGate((request) => { gateCalls.push(request); return false })',
|
||||
'const verified = await signalProcessTree(child, "SIGTERM")',
|
||||
'await closed',
|
||||
'assert.equal(verified, false)',
|
||||
'assert.equal(gateCalls.length, 1)',
|
||||
'assert.equal(gateCalls[0].pid, child.pid)',
|
||||
'assert.equal(gateCalls[0].scope, process.platform === "win32" ? "win-taskkill-tree" : "posix-process-group")',
|
||||
'assert.deepEqual(rootSignals, ["SIGTERM"])',
|
||||
'assert.deepEqual(observedCommands, [process.execPath, process.execPath])',
|
||||
'} finally {',
|
||||
'clearTimeout(watchdog)',
|
||||
'setSpawnObserver(null)',
|
||||
'setProcessTreeKillGate(null)',
|
||||
'if (child && child.exitCode === null && child.signalCode === null) {',
|
||||
' child.kill("SIGKILL")',
|
||||
' await closed',
|
||||
'}',
|
||||
'}',
|
||||
'process.stdout.write(result.stdout)',
|
||||
'}).catch((error) => { console.error(error); process.exitCode = 1 })'
|
||||
].join('\n'),
|
||||
'utf8'
|
||||
)
|
||||
return { outDir, entry }
|
||||
}
|
||||
|
||||
export function runPackagedProcessHostConsumer(nodeExecutable, resourcesDir, entry) {
|
||||
return runProcessSync({
|
||||
program: nodeExecutable,
|
||||
args: [entry],
|
||||
cwd: resourcesDir,
|
||||
timeoutMs: 15_000,
|
||||
env: { ...process.env, NODE_PATH: '', NODE_OPTIONS: '', ORCA_BACKGROUND_LAUNCH: '1' }
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { existsSync, realpathSync } from 'node:fs'
|
||||
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { removeTree } from '../../src/shared/windows-transient-lock-removal.ts'
|
||||
import {
|
||||
runPackagedProcessHostConsumer,
|
||||
stagePackagedProcessHost,
|
||||
writePackagedProcessHostConsumer
|
||||
} from './packaged-process-host-fixture.mjs'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const { createPackagedRuntimeNodeModuleResources } = require('../packaged-runtime-node-modules.cjs')
|
||||
const { collectRuntimeClosure } = require('./verify-skills-cli-runtime.cjs')
|
||||
const nodeExecutable = process.env.ORCA_TEST_NODE_EXECUTABLE ?? process.execPath
|
||||
|
||||
it('loads the copied process package from an unpacked CLI without checkout dependencies', async () => {
|
||||
const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-packaged-process-host-'))
|
||||
try {
|
||||
const resource = createPackagedRuntimeNodeModuleResources().find(
|
||||
(entry) => entry.to === join('node_modules', '@orca', 'process-host')
|
||||
)
|
||||
expect(resource).toBeDefined()
|
||||
expect(resource.filter).toEqual(['package.json', 'dist/**/*'])
|
||||
const packageDir = await stagePackagedProcessHost(resourcesDir)
|
||||
expect(existsSync(join(packageDir, 'src'))).toBe(false)
|
||||
|
||||
const { outDir, entry } = await writePackagedProcessHostConsumer(resourcesDir)
|
||||
const closure = collectRuntimeClosure(outDir, resourcesDir)
|
||||
expect(closure).toContain(realpathSync(join(packageDir, 'dist', 'run-process.js')))
|
||||
const result = runPackagedProcessHostConsumer(nodeExecutable, resourcesDir, entry)
|
||||
expect(result.code, result.stderr).toBe(0)
|
||||
expect(result.stdout).toBe('copied-runtime')
|
||||
} finally {
|
||||
await removeTree(resourcesDir)
|
||||
}
|
||||
})
|
||||
|
||||
describe('process-host output packaging guard', () => {
|
||||
const { assertProcessHostOutputBuilt } = require('../packaged-runtime-node-modules.cjs')
|
||||
const electronBuilderConfig = require('../electron-builder.config.cjs')
|
||||
|
||||
async function builtPackage() {
|
||||
const packageDir = await mkdtemp(join(tmpdir(), 'orca-process-host-output-'))
|
||||
await mkdir(join(packageDir, 'src', '__fixtures__'), { recursive: true })
|
||||
await mkdir(join(packageDir, 'dist'))
|
||||
await writeFile(
|
||||
join(packageDir, 'package.json'),
|
||||
JSON.stringify({
|
||||
main: './dist/entry.js',
|
||||
exports: {
|
||||
'.': { types: './dist/entry.d.ts', default: './dist/entry.js' },
|
||||
'./feature': { default: './dist/feature.js' }
|
||||
}
|
||||
})
|
||||
)
|
||||
for (const module of ['entry', 'feature', 'internal']) {
|
||||
await writeFile(join(packageDir, 'src', `${module}.ts`), '')
|
||||
await writeFile(join(packageDir, 'dist', `${module}.js`), '')
|
||||
}
|
||||
await writeFile(join(packageDir, 'src', 'entry.test.ts'), '')
|
||||
await writeFile(join(packageDir, 'src', '__fixtures__', 'child.ts'), '')
|
||||
return packageDir
|
||||
}
|
||||
|
||||
it('accepts complete output and ignores test and fixture sources', async () => {
|
||||
const packageDir = await builtPackage()
|
||||
try {
|
||||
expect(() => assertProcessHostOutputBuilt(packageDir)).not.toThrow()
|
||||
} finally {
|
||||
await removeTree(packageDir)
|
||||
}
|
||||
})
|
||||
|
||||
it('accepts the checkout package', () => {
|
||||
expect(() => assertProcessHostOutputBuilt()).not.toThrow()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['a public export target', 'feature.js', /missing: \.\/dist\/feature\.js/],
|
||||
['an internal module', 'internal.js', /missing: \.\/dist\/internal\.js/]
|
||||
])('fails when %s was never emitted', async (_label, file, message) => {
|
||||
const packageDir = await builtPackage()
|
||||
try {
|
||||
await rm(join(packageDir, 'dist', file))
|
||||
expect(() => assertProcessHostOutputBuilt(packageDir)).toThrow(message)
|
||||
} finally {
|
||||
await removeTree(packageDir)
|
||||
}
|
||||
})
|
||||
|
||||
it('fails when dist is absent', async () => {
|
||||
const packageDir = await builtPackage()
|
||||
try {
|
||||
await rm(join(packageDir, 'dist'), { recursive: true })
|
||||
expect(() => assertProcessHostOutputBuilt(packageDir)).toThrow(/pnpm build:packages/)
|
||||
} finally {
|
||||
await removeTree(packageDir)
|
||||
}
|
||||
})
|
||||
|
||||
it('fails when dist still holds output of a removed source', async () => {
|
||||
const packageDir = await builtPackage()
|
||||
try {
|
||||
await writeFile(join(packageDir, 'dist', 'renamed-away.js'), '')
|
||||
expect(() => assertProcessHostOutputBuilt(packageDir)).toThrow(
|
||||
/stale: \.\/dist\/renamed-away\.js/
|
||||
)
|
||||
} finally {
|
||||
await removeTree(packageDir)
|
||||
}
|
||||
})
|
||||
|
||||
it('runs in beforePack', () => {
|
||||
expect(String(electronBuilderConfig.beforePack)).toContain('assertProcessHostOutputBuilt()')
|
||||
})
|
||||
})
|
||||
@@ -10,10 +10,11 @@ import {
|
||||
import { tmpdir } from 'node:os'
|
||||
import { isAbsolute, join, parse, resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { runProcessSync } from '../../src/shared/child-process/run-process.ts'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { resolveCliCommand } from '../../src/shared/node-cli-command-resolution.ts'
|
||||
import { removeTreeSync } from '../../src/shared/windows-transient-lock-removal.ts'
|
||||
import { resolvePnpmCliInvocation } from './pnpm-cli-invocation.mjs'
|
||||
import { workspacePackageManifests } from './workspace-source-exports.mjs'
|
||||
|
||||
/**
|
||||
* Run the command that actually consumes the patch hashes.
|
||||
@@ -58,14 +59,15 @@ describe('patched dependencies', () => {
|
||||
// lockfile this repo does not track, and the real one must not move.
|
||||
const scratch = mkdtempSync(join(tmpdir(), 'orca-frozen-install-'))
|
||||
try {
|
||||
for (const file of ['package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml']) {
|
||||
for (const file of [
|
||||
'package.json',
|
||||
'pnpm-lock.yaml',
|
||||
'pnpm-workspace.yaml',
|
||||
...workspacePackageManifests(PROJECT_DIR)
|
||||
]) {
|
||||
mkdirSync(resolve(scratch, file, '..'), { recursive: true })
|
||||
copyFileSync(join(PROJECT_DIR, file), join(scratch, file))
|
||||
}
|
||||
mkdirSync(join(scratch, 'native', 'windows-registry'), { recursive: true })
|
||||
copyFileSync(
|
||||
join(PROJECT_DIR, 'native', 'windows-registry', 'package.json'),
|
||||
join(scratch, 'native', 'windows-registry', 'package.json')
|
||||
)
|
||||
mkdirSync(join(scratch, 'config'), { recursive: true })
|
||||
cpSync(join(PROJECT_DIR, 'config', 'patches'), join(scratch, 'config', 'patches'), {
|
||||
recursive: true
|
||||
|
||||
@@ -4,7 +4,7 @@ import { tmpdir } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { expect, it } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
import { runProcess } from '../../src/shared/child-process/run-process'
|
||||
import { runProcess } from '@orca/process-host'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const projectDir = resolve(import.meta.dirname, '../..')
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { classifyPrJobs } from './pr-code-change-scope.mjs'
|
||||
|
||||
it.each([
|
||||
'src/packages/process-host/package.json',
|
||||
'src/packages/process-host/src/run-process.ts',
|
||||
'src/packages/process-host/src/growing-byte-buffer.ts'
|
||||
])('runs the packaged surfaces and mobile web build checks for %s', (file) => {
|
||||
expect(classifyPrJobs([file])).toMatchObject({
|
||||
should_run: true,
|
||||
test: true,
|
||||
typecheck: true,
|
||||
package: true,
|
||||
package_windows: true,
|
||||
mobile_web_app: true
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps unrelated host packages out of the mobile web build checks', () => {
|
||||
expect(classifyPrJobs(['src/packages/unrelated/src/index.ts']).mobile_web_app).toBe(false)
|
||||
})
|
||||
|
||||
it.each([
|
||||
'src/packages/process-host/src/run-process.ts',
|
||||
'config/scripts/smoke-process-host-node18.mjs',
|
||||
'config/scripts/packaged-process-host-fixture.mjs'
|
||||
])('qualifies the Node 18 package floor for %s', (file) => {
|
||||
expect(classifyPrJobs([file]).managed_hook_node18).toBe(true)
|
||||
})
|
||||
@@ -150,8 +150,7 @@ const ORCAD_BROWSER_PREFIXES = [
|
||||
const MOBILE_WEB_APP_PREFIXES = [
|
||||
'config/scripts/build-mobile-web-app',
|
||||
'config/scripts/run-mobile-web-app-checks',
|
||||
'config/scripts/script-child-process.mjs',
|
||||
'src/shared/child-process/',
|
||||
'src/packages/process-host/',
|
||||
'config/scripts/verify-mobile-web-app-bundle',
|
||||
'config/scripts/mobile-web-app-',
|
||||
'config/scripts/mobile-web-bundle-',
|
||||
@@ -246,6 +245,9 @@ const CROSS_VERSION_WIRE_PREFIXES = [
|
||||
|
||||
const MANAGED_HOOK_PREFIXES = [
|
||||
'config/scripts/smoke-managed-hook-runtime-node18',
|
||||
'config/scripts/smoke-process-host-node18',
|
||||
'config/scripts/packaged-process-host-fixture',
|
||||
'src/packages/process-host/',
|
||||
'config/scripts/build-relay',
|
||||
'src/relay/',
|
||||
'src/shared/agent-hook',
|
||||
@@ -321,7 +323,7 @@ const SHARED_PACKAGE_PREFIXES = [
|
||||
const LINUX_PACKAGE_PREFIXES = [
|
||||
...SHARED_PACKAGE_PREFIXES,
|
||||
'config/scripts/package-linux-formats',
|
||||
'config/scripts/script-child-process.mjs',
|
||||
'config/scripts/process-failure-message.mjs',
|
||||
'config/scripts/space-sharing-copy.mjs',
|
||||
'.github/actions/prepare-linux-package-fixture/',
|
||||
'config/docker/cli-launch-contract/',
|
||||
@@ -369,9 +371,9 @@ const WINDOWS_PACKAGE_TESTS = [
|
||||
'src/main/windows-registry-addon.test.ts',
|
||||
'src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts',
|
||||
'src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts',
|
||||
'src/shared/child-process/windows-command-line.win32.test.ts',
|
||||
'src/shared/child-process/windows-cmd-shim-resolution.test.ts',
|
||||
'src/shared/child-process/windows-cmd-shim-resolution.win32.test.ts',
|
||||
'src/shared/__tests__/process-host/windows-command-line.win32.test.ts',
|
||||
'src/shared/__tests__/process-host/windows-cmd-shim-resolution.test.ts',
|
||||
'src/shared/__tests__/process-host/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',
|
||||
|
||||
@@ -216,7 +216,7 @@ describe('per-job path classification', () => {
|
||||
for (const file of [
|
||||
'config/scripts/package-linux-formats.mjs',
|
||||
'config/scripts/package-linux-formats-appimage.mjs',
|
||||
'config/scripts/script-child-process.mjs',
|
||||
'config/scripts/process-failure-message.mjs',
|
||||
'config/scripts/space-sharing-copy.mjs',
|
||||
'.github/actions/prepare-linux-package-fixture/action.yml',
|
||||
'config/docker/cli-launch-contract/Dockerfile',
|
||||
@@ -229,8 +229,7 @@ describe('per-job path classification', () => {
|
||||
'config/scripts/static-appimage-package-contract.cjs'
|
||||
]) {
|
||||
expectClassification([file], {
|
||||
package: true,
|
||||
mobile_web_app: file === 'config/scripts/script-child-process.mjs'
|
||||
package: true
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -244,8 +243,7 @@ describe('per-job path classification', () => {
|
||||
'config/scripts/run-daemon-shutdown-descendants-docker.mjs'
|
||||
]) {
|
||||
expectClassification([file], {
|
||||
package: true,
|
||||
mobile_web_app: file === 'config/scripts/script-child-process.mjs'
|
||||
package: true
|
||||
})
|
||||
}
|
||||
for (const file of [
|
||||
@@ -329,8 +327,7 @@ describe('per-job path classification', () => {
|
||||
for (const file of [
|
||||
'config/scripts/build-mobile-web-app-bundle.mjs',
|
||||
'config/scripts/run-mobile-web-app-checks.mjs',
|
||||
'config/scripts/script-child-process.mjs',
|
||||
'src/shared/child-process/run-process.ts',
|
||||
'src/packages/process-host/src/run-process.ts',
|
||||
'config/scripts/mobile-web-app-route-manifest.mjs',
|
||||
'mobile/web-entry/index.tsx',
|
||||
'mobile/app/h/[hostId]/index.tsx',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
import { runProcess } from '../../src/shared/child-process/run-process'
|
||||
import { runProcess } from '@orca/process-host'
|
||||
import { PR_CHECK_JOBS } from './pr-code-change-scope.mjs'
|
||||
|
||||
const workflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8'))
|
||||
|
||||
@@ -53,6 +53,14 @@ const realZshUsage =
|
||||
/(?:spawnSync|execFileSync|spawn)\(\s*['"](?:\/(?:usr\/)?bin\/)?zsh['"]|program:\s*['"](?:\/(?:usr\/)?bin\/)?zsh['"]|spawnSync\(\s*['"]which['"]\s*,\s*\[\s*['"]zsh['"]|name:\s*['"]zsh['"]\s*,\s*path:\s*executablePath|from '[^']*zsh-startup-hook-pty-harness'/
|
||||
|
||||
describe('PR workflow parallelism', () => {
|
||||
it('enforces process-host boundaries in the required static-analysis checks', () => {
|
||||
const guard = workflow.jobs.preflight.steps.find((step) =>
|
||||
step.run?.includes('pnpm run check:process-host-imports')
|
||||
)
|
||||
expect(guard?.if).toBe("needs.code_paths.outputs.static_analysis == 'true'")
|
||||
expect(guard?.['continue-on-error']).toBeUndefined()
|
||||
})
|
||||
|
||||
it('keeps lightweight orchestration jobs on the free slim runner', () => {
|
||||
expect(workflow.jobs.code_paths['runs-on']).toBe('ubuntu-slim')
|
||||
expect(workflow.jobs.preflight['runs-on']).toBe('ubuntu-24.04-arm')
|
||||
@@ -294,9 +302,14 @@ describe('PR workflow parallelism', () => {
|
||||
(step) => step.run === 'node config/scripts/smoke-managed-hook-runtime-node18.mjs'
|
||||
)
|
||||
|
||||
const processHostIndex = steps.findIndex(
|
||||
(step) => step.run === 'node config/scripts/smoke-process-host-node18.mjs'
|
||||
)
|
||||
|
||||
expect(installIndex).toBeLessThan(buildIndex)
|
||||
expect(buildIndex).toBeLessThan(node18Index)
|
||||
expect(node18Index).toBeLessThan(smokeIndex)
|
||||
expect(node18Index).toBeLessThan(processHostIndex)
|
||||
})
|
||||
|
||||
it('restores the pnpm store before dependency installation', () => {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/** Why a failed child failed, for CI logs where an empty stderr alone says nothing. */
|
||||
export function describeProcessFailure(result) {
|
||||
const detail = [
|
||||
`code=${result.code ?? 'none'}`,
|
||||
`signal=${result.signal ?? 'none'}`,
|
||||
...(result.timedOut ? ['timed out'] : []),
|
||||
...(result.outputTruncated ? ['output truncated'] : [])
|
||||
].join(' ')
|
||||
const stream = (name, text) => (text?.trim() ? `\n${name}:\n${text.trim().slice(-4000)}` : '')
|
||||
return `${detail}${stream('stdout', result.stdout)}${stream('stderr', result.stderr)}`
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { describeProcessFailure } from './script-child-process.mjs'
|
||||
import { describeProcessFailure } from './process-failure-message.mjs'
|
||||
|
||||
it('names a timeout and both streams when stderr is empty', () => {
|
||||
expect(
|
||||
@@ -0,0 +1,377 @@
|
||||
import {
|
||||
copyFileSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
realpathSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
symlinkSync,
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { runProcess } from '@orca/process-host'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
const realPackageDir = resolve('src/packages/process-host')
|
||||
const buildScript = join(realPackageDir, 'scripts', 'build-dist.mjs')
|
||||
const packageRequire = createRequire(join(realPackageDir, 'package.json'))
|
||||
const tsxCli = packageRequire.resolve('tsx/cli')
|
||||
const realCompilerDir = dirname(packageRequire.resolve('typescript/package.json'))
|
||||
const nodeExecutable = process.env.ORCA_TEST_NODE_EXECUTABLE ?? process.execPath
|
||||
const temporaryRoots = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of temporaryRoots.splice(0)) {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
function fixture() {
|
||||
const packageDir = realpathSync(mkdtempSync(join(tmpdir(), 'orca-process-host-build-')))
|
||||
temporaryRoots.push(packageDir)
|
||||
mkdirSync(join(packageDir, 'src'))
|
||||
mkdirSync(join(packageDir, 'node_modules'))
|
||||
symlinkSync(realCompilerDir, join(packageDir, 'node_modules', 'typescript'), 'junction')
|
||||
writeFileSync(join(packageDir, 'package.json'), JSON.stringify({ name: 'fixture' }))
|
||||
writeFileSync(
|
||||
join(packageDir, 'tsconfig.json'),
|
||||
JSON.stringify({
|
||||
compilerOptions: {
|
||||
target: 'ES2022',
|
||||
module: 'Node16',
|
||||
moduleResolution: 'Node16',
|
||||
rootDir: 'src',
|
||||
outDir: 'dist',
|
||||
declaration: true,
|
||||
declarationMap: true,
|
||||
sourceMap: true,
|
||||
noEmitOnError: true,
|
||||
types: []
|
||||
},
|
||||
include: ['src/**/*.ts']
|
||||
})
|
||||
)
|
||||
writeFileSync(join(packageDir, 'src', 'entry.ts'), "export { value } from './internal'\n")
|
||||
writeFileSync(join(packageDir, 'src', 'internal.ts'), 'export const value = 1\n')
|
||||
return packageDir
|
||||
}
|
||||
|
||||
function runBuild(packageDir, { script = buildScript, cwd = realPackageDir } = {}) {
|
||||
const args = [tsxCli, '--conditions=orca-source', script, ...(packageDir ? [packageDir] : [])]
|
||||
return runProcess({ program: nodeExecutable, args, cwd, timeoutMs: 120_000 })
|
||||
}
|
||||
|
||||
// Emits replacement output, then takes over or deletes the build lock as a competing build would.
|
||||
function installLockStealingCompiler(packageDir, { recreateLock }) {
|
||||
const compilerDir = join(packageDir, 'node_modules', 'typescript')
|
||||
rmSync(compilerDir, { recursive: true })
|
||||
mkdirSync(join(compilerDir, 'bin'), { recursive: true })
|
||||
writeFileSync(
|
||||
join(compilerDir, 'package.json'),
|
||||
JSON.stringify({ name: 'typescript', bin: { tsc: './bin/tsc' } })
|
||||
)
|
||||
writeFileSync(
|
||||
join(compilerDir, 'bin', 'tsc'),
|
||||
`const { mkdirSync, rmSync, utimesSync, writeFileSync } = require('node:fs')
|
||||
const { join } = require('node:path')
|
||||
const outDir = process.argv[process.argv.indexOf('--outDir') + 1]
|
||||
mkdirSync(outDir, { recursive: true })
|
||||
writeFileSync(join(outDir, 'entry.js'), 'module.exports = "from a build that lost its lock"')
|
||||
writeFileSync(join(outDir, 'added.js'), '')
|
||||
rmSync('.dist-build.lock', { recursive: true, force: true })
|
||||
${recreateLock ? "mkdirSync('.dist-build.lock')\nconst foreignMtime = new Date(Date.now() + 60_000)\nutimesSync('.dist-build.lock', foreignMtime, foreignMtime)" : ''}
|
||||
// Hold compilation past proper-lockfile's 15-second refresh so compromise is observed.
|
||||
setTimeout(() => {}, 17_000)
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
async function build(packageDir) {
|
||||
const result = await runBuild(packageDir)
|
||||
expect(result).toMatchObject({ code: 0, stderr: '' })
|
||||
return result
|
||||
}
|
||||
|
||||
function distSnapshot(packageDir) {
|
||||
const distDir = join(packageDir, 'dist')
|
||||
return Object.fromEntries(
|
||||
readdirSync(distDir)
|
||||
.sort()
|
||||
.map((file) => {
|
||||
const stat = statSync(join(distDir, file))
|
||||
return [file, { ino: stat.ino, mtimeMs: stat.mtimeMs }]
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function leftovers(packageDir) {
|
||||
return readdirSync(packageDir).filter((name) => name.startsWith('.dist-'))
|
||||
}
|
||||
|
||||
describe('process-host dist build', () => {
|
||||
it('leaves an up-to-date dist untouched on repeated builds', async () => {
|
||||
const packageDir = fixture()
|
||||
await build(packageDir)
|
||||
const before = distSnapshot(packageDir)
|
||||
|
||||
const repeated = await build(packageDir)
|
||||
|
||||
expect(repeated.stdout).toBe('')
|
||||
expect(distSnapshot(packageDir)).toEqual(before)
|
||||
expect(leftovers(packageDir)).toEqual([])
|
||||
})
|
||||
|
||||
it('rewrites only changed outputs and keeps source maps pointing at src', async () => {
|
||||
const packageDir = fixture()
|
||||
await build(packageDir)
|
||||
const before = distSnapshot(packageDir)
|
||||
|
||||
writeFileSync(join(packageDir, 'src', 'internal.ts'), 'export const value = 2\n')
|
||||
const changed = await build(packageDir)
|
||||
|
||||
expect(changed.stdout).toContain('2 written, 0 removed, 6 unchanged')
|
||||
const after = distSnapshot(packageDir)
|
||||
for (const file of ['entry.js', 'entry.d.ts', 'internal.js.map']) {
|
||||
expect(after[file]).toEqual(before[file])
|
||||
}
|
||||
expect(after['internal.js']).not.toEqual(before['internal.js'])
|
||||
const map = JSON.parse(readFileSync(join(packageDir, 'dist', 'internal.js.map'), 'utf8'))
|
||||
expect(map.sources).toEqual(['../src/internal.ts'])
|
||||
})
|
||||
|
||||
it('removes outputs of renamed sources', async () => {
|
||||
const packageDir = fixture()
|
||||
await build(packageDir)
|
||||
renameSync(join(packageDir, 'src', 'internal.ts'), join(packageDir, 'src', 'renamed.ts'))
|
||||
writeFileSync(join(packageDir, 'src', 'entry.ts'), "export { value } from './renamed'\n")
|
||||
|
||||
await build(packageDir)
|
||||
|
||||
expect(readdirSync(join(packageDir, 'dist')).sort()).toEqual([
|
||||
'entry.d.ts',
|
||||
'entry.d.ts.map',
|
||||
'entry.js',
|
||||
'entry.js.map',
|
||||
'renamed.d.ts',
|
||||
'renamed.d.ts.map',
|
||||
'renamed.js',
|
||||
'renamed.js.map'
|
||||
])
|
||||
})
|
||||
|
||||
it('preserves the previous dist when compilation fails', async () => {
|
||||
const packageDir = fixture()
|
||||
await build(packageDir)
|
||||
const before = distSnapshot(packageDir)
|
||||
writeFileSync(join(packageDir, 'src', 'internal.ts'), "export const value: number = 'bad'\n")
|
||||
|
||||
const failed = await runBuild(packageDir)
|
||||
|
||||
expect(failed.code).toBe(1)
|
||||
expect(failed.stderr).toContain('compilation failed')
|
||||
expect(distSnapshot(packageDir)).toEqual(before)
|
||||
expect(leftovers(packageDir)).toEqual([])
|
||||
})
|
||||
|
||||
it('publishes new dependencies before replacing their importer and removes stale output last', async () => {
|
||||
const packageDir = fixture()
|
||||
await build(packageDir)
|
||||
renameSync(join(packageDir, 'src', 'internal.ts'), join(packageDir, 'src', 'z-new.ts'))
|
||||
writeFileSync(join(packageDir, 'src', 'z-new.ts'), 'export const value = 2\n')
|
||||
writeFileSync(join(packageDir, 'src', 'entry.ts'), "export { value } from './z-new'\n")
|
||||
const script = join(packageDir, 'read-during-publication.mjs')
|
||||
const observation = join(packageDir, 'publication-read.json')
|
||||
const entry = join(packageDir, 'dist', 'entry.js')
|
||||
const oldDependency = join(packageDir, 'dist', 'internal.js')
|
||||
// Pause immediately after replacing the importer; the owned reader exits before publication resumes.
|
||||
writeFileSync(
|
||||
script,
|
||||
`import fs from 'node:fs'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { syncBuiltinESMExports } from 'node:module'
|
||||
const rename = fs.renameSync
|
||||
fs.renameSync = (source, target) => {
|
||||
const result = rename(source, target)
|
||||
if (target === ${JSON.stringify(entry)}) {
|
||||
const reader = spawnSync(process.execPath, ['-e', ${JSON.stringify(
|
||||
`console.log(JSON.stringify({ value: require(${JSON.stringify(entry)}).value, oldDependencyExists: require('node:fs').existsSync(${JSON.stringify(oldDependency)}) }))`
|
||||
)}], { encoding: 'utf8', timeout: 10_000 })
|
||||
fs.writeFileSync(${JSON.stringify(observation)}, JSON.stringify({ code: reader.status, stdout: reader.stdout, stderr: reader.stderr }))
|
||||
}
|
||||
return result
|
||||
}
|
||||
syncBuiltinESMExports()
|
||||
const { buildPackageDist } = await import(${JSON.stringify(pathToFileURL(buildScript).href)})
|
||||
await buildPackageDist(process.argv[2])
|
||||
`
|
||||
)
|
||||
|
||||
const result = await runBuild(packageDir, { script })
|
||||
|
||||
expect(result).toMatchObject({ code: 0, stderr: '' })
|
||||
const reader = JSON.parse(readFileSync(observation, 'utf8'))
|
||||
expect(reader).toMatchObject({ code: 0, stderr: '' })
|
||||
expect(JSON.parse(reader.stdout)).toEqual({ value: 2, oldDependencyExists: true })
|
||||
expect(existsSync(oldDependency)).toBe(false)
|
||||
expect(leftovers(packageDir)).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps dist loadable while concurrent builds and a reader run', async () => {
|
||||
const packageDir = fixture()
|
||||
await build(packageDir)
|
||||
writeFileSync(
|
||||
join(packageDir, 'src', 'internal.ts'),
|
||||
`export const value = ${'2 + '.repeat(10_000)}2\n`
|
||||
)
|
||||
const entry = join(packageDir, 'dist', 'entry.js')
|
||||
const finished = join(packageDir, 'builds-finished')
|
||||
const reader = runProcess({
|
||||
program: nodeExecutable,
|
||||
args: [
|
||||
'-e',
|
||||
`const { existsSync } = require('node:fs')
|
||||
let reads = 0
|
||||
while (!existsSync(${JSON.stringify(finished)}) || reads === 0) {
|
||||
for (const key of Object.keys(require.cache)) delete require.cache[key]
|
||||
if (typeof require(${JSON.stringify(entry)}).value !== 'number') process.exit(2)
|
||||
reads += 1
|
||||
}`
|
||||
],
|
||||
timeoutMs: 60_000
|
||||
})
|
||||
|
||||
let builds
|
||||
let read
|
||||
try {
|
||||
builds = await Promise.all([1, 2, 3].map(() => runBuild(packageDir)))
|
||||
} finally {
|
||||
writeFileSync(finished, '')
|
||||
read = await reader
|
||||
}
|
||||
|
||||
expect(builds.map((result) => [result.code, result.stderr])).toEqual(builds.map(() => [0, '']))
|
||||
expect(read).toMatchObject({ code: 0, stderr: '' })
|
||||
expect(readFileSync(join(packageDir, 'dist', 'internal.js'), 'utf8')).toContain('2 + 2')
|
||||
expect(leftovers(packageDir)).toEqual([])
|
||||
})
|
||||
|
||||
it('serializes builds so a later source snapshot is the one published', async () => {
|
||||
const packageDir = fixture()
|
||||
await build(packageDir)
|
||||
// Hold the build lock while the source changes, as an in-flight earlier build would.
|
||||
mkdirSync(join(packageDir, '.dist-build.lock'))
|
||||
const waiting = runBuild(packageDir)
|
||||
await new Promise((resolveDelay) => setTimeout(resolveDelay, 500))
|
||||
writeFileSync(join(packageDir, 'src', 'internal.ts'), 'export const value = 9\n')
|
||||
rmSync(join(packageDir, '.dist-build.lock'), { recursive: true })
|
||||
|
||||
expect(await waiting).toMatchObject({ code: 0, stderr: '' })
|
||||
expect(readFileSync(join(packageDir, 'dist', 'internal.js'), 'utf8')).toContain('value = 9')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['taken over by another build', true],
|
||||
['removed', false]
|
||||
])(
|
||||
'fails without publishing when the held build lock is %s',
|
||||
async (_label, recreateLock) => {
|
||||
const packageDir = fixture()
|
||||
await build(packageDir)
|
||||
const before = distSnapshot(packageDir)
|
||||
const entry = readFileSync(join(packageDir, 'dist', 'entry.js'), 'utf8')
|
||||
installLockStealingCompiler(packageDir, { recreateLock })
|
||||
|
||||
const result = await runBuild(packageDir)
|
||||
|
||||
expect(result.code).toBe(1)
|
||||
expect(result.stderr).toContain('build lock was compromised')
|
||||
expect(distSnapshot(packageDir)).toEqual(before)
|
||||
expect(readFileSync(join(packageDir, 'dist', 'entry.js'), 'utf8')).toBe(entry)
|
||||
// The other build's lock is left in place.
|
||||
expect(existsSync(join(packageDir, '.dist-build.lock'))).toBe(recreateLock)
|
||||
expect(leftovers(packageDir).filter((name) => name.startsWith('.dist-staging-'))).toEqual([])
|
||||
},
|
||||
30_000
|
||||
)
|
||||
|
||||
it('reclaims staging directories abandoned by a killed build', async () => {
|
||||
const packageDir = fixture()
|
||||
mkdirSync(join(packageDir, '.dist-staging-abandoned'))
|
||||
writeFileSync(join(packageDir, '.dist-staging-abandoned', 'partial.js'), '')
|
||||
|
||||
await build(packageDir)
|
||||
|
||||
expect(existsSync(join(packageDir, 'dist', 'entry.js'))).toBe(true)
|
||||
expect(existsSync(join(packageDir, 'dist', 'partial.js'))).toBe(false)
|
||||
expect(leftovers(packageDir)).toEqual([])
|
||||
})
|
||||
|
||||
it('compiles with the package-local TypeScript', async () => {
|
||||
const packageDir = fixture()
|
||||
rmSync(join(packageDir, 'node_modules', 'typescript'))
|
||||
const localCompilerDir = join(packageDir, 'node_modules', 'typescript')
|
||||
mkdirSync(join(localCompilerDir, 'bin'), { recursive: true })
|
||||
writeFileSync(
|
||||
join(localCompilerDir, 'package.json'),
|
||||
JSON.stringify({ name: 'typescript', bin: { tsc: './bin/tsc' } })
|
||||
)
|
||||
writeFileSync(
|
||||
join(localCompilerDir, 'bin', 'tsc'),
|
||||
"require('node:fs').writeFileSync('compiled-by', 'package')\nprocess.exit(1)\n"
|
||||
)
|
||||
|
||||
const failed = await runBuild(packageDir)
|
||||
|
||||
expect(failed.code).toBe(1)
|
||||
expect(readFileSync(join(packageDir, 'compiled-by'), 'utf8')).toBe('package')
|
||||
})
|
||||
|
||||
it('builds its own package when reached through a symlinked directory', async () => {
|
||||
const packageDir = fixture()
|
||||
mkdirSync(join(packageDir, 'scripts'))
|
||||
copyFileSync(buildScript, join(packageDir, 'scripts', 'build-dist.mjs'))
|
||||
writeFileSync(
|
||||
join(packageDir, 'package.json'),
|
||||
JSON.stringify({
|
||||
name: '@orca/process-host',
|
||||
exports: { '.': { 'orca-source': './src/run-process.ts' } }
|
||||
})
|
||||
)
|
||||
// The script imports runProcess through its own package name; forward to the real one.
|
||||
writeFileSync(
|
||||
join(packageDir, 'src', 'run-process.ts'),
|
||||
`export { runProcess } from ${JSON.stringify(join(realPackageDir, 'src', 'run-process.ts'))}\n`
|
||||
)
|
||||
writeFileSync(
|
||||
join(packageDir, 'tsconfig.json'),
|
||||
readFileSync(join(packageDir, 'tsconfig.json'), 'utf8').replace(
|
||||
'"include":["src/**/*.ts"]',
|
||||
'"include":["src/entry.ts","src/internal.ts"]'
|
||||
)
|
||||
)
|
||||
symlinkSync(
|
||||
dirname(packageRequire.resolve('proper-lockfile/package.json')),
|
||||
join(packageDir, 'node_modules', 'proper-lockfile'),
|
||||
'junction'
|
||||
)
|
||||
const linkRoot = mkdtempSync(join(tmpdir(), 'orca-process-host-link-'))
|
||||
temporaryRoots.push(linkRoot)
|
||||
const linkDir = join(linkRoot, 'package')
|
||||
symlinkSync(packageDir, linkDir, 'junction')
|
||||
|
||||
const result = await runBuild(undefined, {
|
||||
script: join(linkDir, 'scripts', 'build-dist.mjs'),
|
||||
cwd: linkDir
|
||||
})
|
||||
|
||||
expect(result).toMatchObject({ code: 0, stderr: '' })
|
||||
expect(existsSync(join(packageDir, 'dist', 'entry.js'))).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,297 @@
|
||||
import {
|
||||
existsSync,
|
||||
mkdtempSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
symlinkSync,
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { basename, dirname, join, resolve } from 'node:path'
|
||||
import { createRequire } from 'node:module'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { rolldown, watch } from 'rolldown'
|
||||
import type { Plugin, RolldownWatcher } from 'rolldown'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { createProcessHostDevRebuildPlugin } from '../build-plugins/process-host-dev-rebuild'
|
||||
|
||||
const realPackageDir = resolve('src/packages/process-host')
|
||||
const temporaryRoots: string[] = []
|
||||
const watchers: RolldownWatcher[] = []
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(watchers.splice(0).map((watcher) => watcher.close()))
|
||||
for (const root of temporaryRoots.splice(0)) {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
function fixture(): { root: string; packageDir: string; main: string } {
|
||||
const root = mkdtempSync(join(tmpdir(), 'orca-process-host-dev-'))
|
||||
temporaryRoots.push(root)
|
||||
const packageDir = join(root, 'src', 'packages', 'process-host')
|
||||
const sourceDir = join(packageDir, 'src')
|
||||
mkdirSync(sourceDir, { recursive: true })
|
||||
mkdirSync(join(packageDir, 'scripts'))
|
||||
// Runs the real package build against this fixture package.
|
||||
writeFileSync(
|
||||
join(packageDir, 'scripts', 'build-dist.mjs'),
|
||||
`const { buildPackageDist } = await import(${JSON.stringify(pathToFileURL(resolve(realPackageDir, 'scripts', 'build-dist.mjs')).href)})\nawait buildPackageDist(${JSON.stringify(packageDir)})\n`
|
||||
)
|
||||
mkdirSync(join(root, 'node_modules', '@orca'), { recursive: true })
|
||||
const packageRequire = createRequire(join(realPackageDir, 'package.json'))
|
||||
for (const tool of ['typescript', 'tsx']) {
|
||||
symlinkSync(
|
||||
dirname(packageRequire.resolve(`${tool}/package.json`)),
|
||||
join(root, 'node_modules', tool),
|
||||
'junction'
|
||||
)
|
||||
}
|
||||
symlinkSync(packageDir, join(root, 'node_modules', '@orca', 'process-host'), 'junction')
|
||||
writeFileSync(join(root, 'pnpm-workspace.yaml'), 'packages:\n - src/packages/*\n')
|
||||
writeFileSync(
|
||||
join(packageDir, 'package.json'),
|
||||
JSON.stringify({
|
||||
name: '@orca/process-host',
|
||||
type: 'commonjs',
|
||||
exports: { '.': { 'orca-source': './src/entry.ts', default: './dist/entry.js' } }
|
||||
})
|
||||
)
|
||||
writeFileSync(
|
||||
join(root, 'tsconfig.base.json'),
|
||||
JSON.stringify({ compilerOptions: { strict: true } })
|
||||
)
|
||||
writeFileSync(
|
||||
join(packageDir, 'tsconfig.json'),
|
||||
JSON.stringify({
|
||||
extends: '../../../tsconfig.base.json',
|
||||
compilerOptions: {
|
||||
target: 'ES2022',
|
||||
module: 'Node16',
|
||||
moduleResolution: 'Node16',
|
||||
rootDir: 'src',
|
||||
outDir: 'dist',
|
||||
noEmitOnError: true,
|
||||
types: []
|
||||
},
|
||||
include: ['src/**/*.ts']
|
||||
})
|
||||
)
|
||||
writeFileSync(join(sourceDir, 'entry.ts'), "export { version } from './internal'\n")
|
||||
writeFileSync(join(sourceDir, 'internal.ts'), '// preserved comment\nexport const version = 1\n')
|
||||
const main = join(root, 'main.cjs')
|
||||
writeFileSync(main, "module.exports = require('@orca/process-host')\n")
|
||||
return { root, packageDir, main }
|
||||
}
|
||||
|
||||
function nextEvent(watcher: RolldownWatcher, code: 'END' | 'ERROR'): Promise<void> {
|
||||
return new Promise((resolveEvent, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${code}`)), 15_000)
|
||||
watcher.on('event', function listener(event) {
|
||||
if (event.code === code) {
|
||||
clearTimeout(timeout)
|
||||
watcher.off('event', listener)
|
||||
resolveEvent()
|
||||
} else if (event.code === 'ERROR' && code !== 'ERROR') {
|
||||
clearTimeout(timeout)
|
||||
watcher.off('event', listener)
|
||||
reject(event.error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function closeSuccessfulBundles(watcher: RolldownWatcher): void {
|
||||
// Vite closes successful watch outputs, which invokes electron-vite's reload hook.
|
||||
watcher.on('event', async (event) => {
|
||||
if (event.code === 'BUNDLE_END') {
|
||||
await event.result.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function readEmittedVersion(root: string): number {
|
||||
const result = runProcessSync({
|
||||
program: process.execPath,
|
||||
args: ['-e', `console.log(require(${JSON.stringify(join(root, 'out', 'main.cjs'))}).version)`],
|
||||
cwd: root,
|
||||
env: { ...process.env, NODE_OPTIONS: '', NODE_PATH: '' }
|
||||
})
|
||||
if (result.code !== 0) {
|
||||
throw new Error(result.stderr || 'The emitted main entry did not load')
|
||||
}
|
||||
return Number(result.stdout.trim())
|
||||
}
|
||||
|
||||
describe('External process-host development rebuilds', () => {
|
||||
it('emits private source edits and new imports before the Electron reload hook', async () => {
|
||||
const { root, packageDir, main } = fixture()
|
||||
const loadedVersions: number[] = []
|
||||
const reloadHook: Plugin = {
|
||||
name: 'fake-electron-reload',
|
||||
closeBundle() {
|
||||
loadedVersions.push(readEmittedVersion(root))
|
||||
}
|
||||
}
|
||||
const watcher = watch({
|
||||
input: main,
|
||||
external: ['@orca/process-host'],
|
||||
plugins: [createProcessHostDevRebuildPlugin(root), reloadHook],
|
||||
output: { file: join(root, 'out', 'main.cjs'), format: 'cjs' }
|
||||
})
|
||||
watchers.push(watcher)
|
||||
closeSuccessfulBundles(watcher)
|
||||
await nextEvent(watcher, 'END')
|
||||
expect(loadedVersions).toEqual([1])
|
||||
expect(readFileSync(join(root, 'out', 'main.cjs'), 'utf8')).toContain('@orca/process-host')
|
||||
|
||||
writeFileSync(join(packageDir, 'src', 'internal.ts'), 'export const version = 2\n')
|
||||
await expect.poll(() => loadedVersions.at(-1), { timeout: 15_000 }).toBe(2)
|
||||
|
||||
writeFileSync(join(packageDir, 'src', 'new-feature.ts'), 'export const added = 3\n')
|
||||
writeFileSync(
|
||||
join(packageDir, 'src', 'internal.ts'),
|
||||
"import { added } from './new-feature'\nexport const version = added\n"
|
||||
)
|
||||
await expect.poll(() => loadedVersions.at(-1), { timeout: 15_000 }).toBe(3)
|
||||
await expect
|
||||
.poll(() => existsSync(join(packageDir, 'dist', 'new-feature.js')), { timeout: 15_000 })
|
||||
.toBe(true)
|
||||
expect(readFileSync(join(packageDir, 'dist', 'new-feature.js'), 'utf8')).toContain('added = 3')
|
||||
})
|
||||
|
||||
it('rebuilds when an inherited compiler configuration changes', async () => {
|
||||
const { root, packageDir, main } = fixture()
|
||||
const watcher = watch({
|
||||
input: main,
|
||||
external: ['@orca/process-host'],
|
||||
plugins: [createProcessHostDevRebuildPlugin(root)],
|
||||
output: { file: join(root, 'out', 'main.cjs'), format: 'cjs' }
|
||||
})
|
||||
watchers.push(watcher)
|
||||
closeSuccessfulBundles(watcher)
|
||||
await nextEvent(watcher, 'END')
|
||||
const emittedSource = join(packageDir, 'dist', 'internal.js')
|
||||
expect(readFileSync(emittedSource, 'utf8')).toContain('preserved comment')
|
||||
|
||||
writeFileSync(
|
||||
join(root, 'tsconfig.base.json'),
|
||||
JSON.stringify({ compilerOptions: { strict: true, removeComments: true } })
|
||||
)
|
||||
await expect
|
||||
.poll(() => readFileSync(emittedSource, 'utf8'), { timeout: 15_000 })
|
||||
.not.toContain('preserved comment')
|
||||
})
|
||||
|
||||
it('reports a failed compilation, preserves prior output, and recovers after a fix', async () => {
|
||||
const { root, packageDir, main } = fixture()
|
||||
const loadedVersions: number[] = []
|
||||
const watcher = watch({
|
||||
input: main,
|
||||
external: ['@orca/process-host'],
|
||||
plugins: [
|
||||
createProcessHostDevRebuildPlugin(root),
|
||||
{
|
||||
name: 'fake-electron-reload',
|
||||
closeBundle() {
|
||||
loadedVersions.push(readEmittedVersion(root))
|
||||
}
|
||||
}
|
||||
],
|
||||
output: { file: join(root, 'out', 'main.cjs'), format: 'cjs' }
|
||||
})
|
||||
watchers.push(watcher)
|
||||
closeSuccessfulBundles(watcher)
|
||||
await nextEvent(watcher, 'END')
|
||||
|
||||
const failed = nextEvent(watcher, 'ERROR')
|
||||
writeFileSync(join(packageDir, 'src', 'internal.ts'), "export const version: number = 'bad'\n")
|
||||
await failed
|
||||
expect(loadedVersions.at(-1)).toBe(1)
|
||||
expect(readEmittedVersion(root)).toBe(1)
|
||||
|
||||
writeFileSync(join(packageDir, 'src', 'internal.ts'), 'export const version = 4\n')
|
||||
await expect.poll(() => loadedVersions.at(-1), { timeout: 15_000 }).toBe(4)
|
||||
})
|
||||
|
||||
it('recovers when a missing import is created in a new source directory', async () => {
|
||||
const { root, packageDir, main } = fixture()
|
||||
const loadedVersions: number[] = []
|
||||
const watchSignals = new Set<string>()
|
||||
const watcher = watch({
|
||||
input: main,
|
||||
external: ['@orca/process-host'],
|
||||
plugins: [
|
||||
createProcessHostDevRebuildPlugin(root),
|
||||
{
|
||||
name: 'fake-electron-reload',
|
||||
closeBundle() {
|
||||
loadedVersions.push(readEmittedVersion(root))
|
||||
}
|
||||
}
|
||||
],
|
||||
output: { file: join(root, 'out', 'main.cjs'), format: 'cjs' }
|
||||
})
|
||||
watcher.on('change', (file) => {
|
||||
if (basename(file) === 'source-directory-change') {
|
||||
watchSignals.add(file)
|
||||
}
|
||||
})
|
||||
watchers.push(watcher)
|
||||
closeSuccessfulBundles(watcher)
|
||||
await nextEvent(watcher, 'END')
|
||||
|
||||
const failed = nextEvent(watcher, 'ERROR')
|
||||
writeFileSync(
|
||||
join(packageDir, 'src', 'internal.ts'),
|
||||
"export { version } from './features/new-feature'\n"
|
||||
)
|
||||
await failed
|
||||
expect(loadedVersions.at(-1)).toBe(1)
|
||||
expect(readEmittedVersion(root)).toBe(1)
|
||||
|
||||
mkdirSync(join(packageDir, 'src', 'features'))
|
||||
writeFileSync(
|
||||
join(packageDir, 'src', 'features', 'new-feature.ts'),
|
||||
'export const version = 5\n'
|
||||
)
|
||||
await expect.poll(() => loadedVersions.at(-1), { timeout: 15_000 }).toBe(5)
|
||||
expect(existsSync(join(packageDir, 'dist', 'features', 'new-feature.js'))).toBe(true)
|
||||
expect(watchSignals.size).toBe(1)
|
||||
|
||||
const failedAgain = nextEvent(watcher, 'ERROR')
|
||||
writeFileSync(
|
||||
join(packageDir, 'src', 'internal.ts'),
|
||||
"export { version } from './features/still-missing'\n"
|
||||
)
|
||||
await failedAgain
|
||||
expect(readEmittedVersion(root)).toBe(5)
|
||||
|
||||
await watcher.close()
|
||||
watchers.splice(watchers.indexOf(watcher), 1)
|
||||
for (const signal of watchSignals) {
|
||||
expect(existsSync(dirname(signal))).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
it('leaves non-watch release compilation to the build:packages prerequisite', async () => {
|
||||
const bundle = await rolldown({
|
||||
input: resolve('src/packages/process-host/src/process-spec.ts'),
|
||||
plugins: [createProcessHostDevRebuildPlugin('/unavailable-development-root')]
|
||||
})
|
||||
try {
|
||||
await bundle.generate({ format: 'cjs' })
|
||||
} finally {
|
||||
await bundle.close()
|
||||
}
|
||||
})
|
||||
|
||||
it('leaves pnpm dev in no-watch mode unless the caller passes --watch', () => {
|
||||
// Watch mode restarts Electron on every main-process save, so it stays opt-in.
|
||||
const launcher = readFileSync(resolve('config/scripts/run-electron-vite-dev.mjs'), 'utf8')
|
||||
expect(launcher).toContain("const forwardedArgs = ['dev', ...forwardedRaw, ...forwardedExtras]")
|
||||
expect(launcher).not.toMatch(/['"](?:--watch|-w)['"]/)
|
||||
})
|
||||
})
|
||||
@@ -6,7 +6,8 @@ import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { Worker } from 'node:worker_threads'
|
||||
import { describeProcessFailure, runProcessSync } from './script-child-process.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { describeProcessFailure } from './process-failure-message.mjs'
|
||||
import {
|
||||
ORCAD_PROFILE_PREFLIGHT_FLAG,
|
||||
parseOrcadProfilePreflight
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { runProcess } from '../../src/shared/child-process/run-process'
|
||||
import { runProcess } from '@orca/process-host'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
const scriptPath = resolve('config/scripts/project-renderer-web-client.mjs')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { join } from 'node:path'
|
||||
import { runProcessSync } from './script-child-process.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { relayWindowsProcessTreeAddonDefect } from './windows-process-tree-gyp-rebuild.mjs'
|
||||
|
||||
const root = join(import.meta.dirname, '../..')
|
||||
|
||||
@@ -6,7 +6,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
import { peImage } from './windows-pe-image-fixture.mjs'
|
||||
import { canReusePreparedRelayAddon } from './relay-windows-process-tree-prepared-addon.mjs'
|
||||
import { runProcessSync } from './script-child-process.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
|
||||
const directories = []
|
||||
afterEach(() => {
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { runProcessSync, describeProcessFailure } from './script-child-process.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { describeProcessFailure } from './process-failure-message.mjs'
|
||||
import { isDirectInvocation } from './script-entry-detection.mjs'
|
||||
import { DESKTOP_RC_TAG, DESKTOP_STABLE_TAG } from './release-tag-patterns.mjs'
|
||||
import { getTarProgram } from './zip-extractor-command.mjs'
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
restoreReleaseJavascript
|
||||
} from './release-javascript-artifact.mjs'
|
||||
import { resolvePnpmCliInvocation } from './pnpm-cli-invocation.mjs'
|
||||
import { describeProcessFailure, runProcessSync } from './script-child-process.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { describeProcessFailure } from './process-failure-message.mjs'
|
||||
import {
|
||||
annotateJavascriptParityFiles,
|
||||
compareJavascriptParityFiles
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createRequire } from 'node:module'
|
||||
import { mobileWebAppModuleClosure } from './build-mobile-web-app-bundle.mjs'
|
||||
import { readRouteSnapshot } from './mobile-web-app-route-snapshot.mjs'
|
||||
import { PAGE_ROUTE_MODULES } from './mobile-web-app-page-route-modules.mjs'
|
||||
import { spawnProcess } from './script-child-process.mjs'
|
||||
import { spawnProcess } from '@orca/process-host'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const mobileWebCheckArgs = [
|
||||
|
||||
@@ -15,7 +15,8 @@ import { packagedNodeRuntimePath } from './build-orcad-node.mjs'
|
||||
import { ensurePinnedNodeExecutable } from './pinned-node-downloads.mjs'
|
||||
import { currentTarget } from './server-build-target.mjs'
|
||||
import { UNIT_INCLUDE } from './ci-unit-files.mjs'
|
||||
import { describeProcessFailure, runProcessSync } from './script-child-process.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { describeProcessFailure } from './process-failure-message.mjs'
|
||||
import {
|
||||
CROSS_RUNTIME_TEST_PATHS,
|
||||
nodeServerTestPaths,
|
||||
|
||||
@@ -16,11 +16,12 @@ import { tmpdir } from 'node:os'
|
||||
import { basename, join, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { describeProcessFailure, runProcessSync } from './script-child-process.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { describeProcessFailure } from './process-failure-message.mjs'
|
||||
const repoRoot = resolve(import.meta.dirname, '../..')
|
||||
const BUILD_STAMP = join('out', '.terminal-layout-parity-build')
|
||||
|
||||
// Bundled like script-child-process.mjs: importing the .ts directly makes Node warn MODULE_TYPELESS_PACKAGE_JSON.
|
||||
// Bundle TypeScript parity modules so plain Node does not need a loader.
|
||||
async function importParityModules() {
|
||||
const temporary = mkdtempSync(join(tmpdir(), 'orca-layout-parity-'))
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { spawn } from 'node:child_process'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { availableParallelism, totalmem } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
const BYTES_PER_GIB = 1024 ** 3
|
||||
@@ -9,9 +12,11 @@ const BYTES_PER_GIB = 1024 ** 3
|
||||
// out-of-memory runner is killed mid-check, so the job reports a lost runner instead of a
|
||||
// type error. Admission is therefore by memory, not by core count alone.
|
||||
export const TYPECHECK_PROJECTS = [
|
||||
{ config: 'tsconfig.node.json', heapGib: 7 },
|
||||
{ config: 'tsconfig.tc.web.json', heapGib: 6 },
|
||||
{ config: 'tsconfig.tc.cli.json', heapGib: 2 }
|
||||
{ config: 'config/tsconfig.node.json', heapGib: 7 },
|
||||
{ config: 'config/tsconfig.tc.web.json', heapGib: 6 },
|
||||
{ config: 'config/tsconfig.tc.cli.json', heapGib: 2 },
|
||||
// Root projects exclude workspace package tests; each runs with its package's compiler.
|
||||
{ config: 'src/packages/process-host/tsconfig.test.json', heapGib: 1 }
|
||||
]
|
||||
|
||||
// The OS, node itself, and the runner agent need their share; the rest is what tsc may hold.
|
||||
@@ -53,21 +58,31 @@ export function planTypecheckBatches(projects, { budgetGib, parallelism }) {
|
||||
}
|
||||
|
||||
const repoRoot = fileURLToPath(new URL('../..', import.meta.url))
|
||||
const tsc = fileURLToPath(new URL('../../node_modules/typescript/bin/tsc', import.meta.url))
|
||||
|
||||
function checkProject(project) {
|
||||
export function resolveProjectCompiler(config, root = repoRoot) {
|
||||
const require = createRequire(join(root, dirname(config), 'package.json'))
|
||||
const manifestPath = require.resolve('typescript/package.json')
|
||||
const { bin } = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
||||
return join(dirname(manifestPath), typeof bin === 'string' ? bin : bin.tsc)
|
||||
}
|
||||
|
||||
function checkProject(config) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(process.execPath, [tsc, '--noEmit', '-p', `config/${project}`], {
|
||||
cwd: repoRoot,
|
||||
stdio: 'inherit'
|
||||
})
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
[resolveProjectCompiler(config), '--noEmit', '-p', config],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
stdio: 'inherit'
|
||||
}
|
||||
)
|
||||
|
||||
child.on('error', reject)
|
||||
child.on('exit', (code, signal) => {
|
||||
if (signal) {
|
||||
reject(new Error(`tsc ${project} exited with signal ${signal}`))
|
||||
reject(new Error(`tsc ${config} exited with signal ${signal}`))
|
||||
} else if (code !== 0) {
|
||||
reject(new Error(`tsc ${project} exited with code ${code}`))
|
||||
reject(new Error(`tsc ${config} exited with code ${code}`))
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
|
||||
@@ -1,10 +1,25 @@
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readdirSync,
|
||||
realpathSync,
|
||||
rmSync,
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, relative, sep } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
TYPECHECK_PROJECTS,
|
||||
admissibleHeapGib,
|
||||
planTypecheckBatches
|
||||
planTypecheckBatches,
|
||||
resolveProjectCompiler
|
||||
} from './run-typecheck-projects-in-parallel.mjs'
|
||||
|
||||
const repoRoot = fileURLToPath(new URL('../..', import.meta.url))
|
||||
|
||||
const CI_RUNNER = { totalBytes: 16 * 1024 ** 3, parallelism: 4 }
|
||||
const DEV_LAPTOP = { totalBytes: 64 * 1024 ** 3, parallelism: 18 }
|
||||
|
||||
@@ -22,8 +37,8 @@ function batchOf(batches, config) {
|
||||
describe('typecheck project admission', () => {
|
||||
it('keeps the two expensive projects off the same CI runner', () => {
|
||||
const batches = planFor(CI_RUNNER)
|
||||
expect(batchOf(batches, 'tsconfig.node.json')).not.toBe(
|
||||
batchOf(batches, 'tsconfig.tc.web.json')
|
||||
expect(batchOf(batches, 'config/tsconfig.node.json')).not.toBe(
|
||||
batchOf(batches, 'config/tsconfig.tc.web.json')
|
||||
)
|
||||
})
|
||||
|
||||
@@ -72,3 +87,42 @@ describe('typecheck project admission', () => {
|
||||
expect(heaviest).toBeLessThanOrEqual(admissibleHeapGib(CI_RUNNER.totalBytes))
|
||||
})
|
||||
})
|
||||
|
||||
describe('workspace package test typecheck', () => {
|
||||
const packagesDir = join(repoRoot, 'src', 'packages')
|
||||
const packageTestProjects = readdirSync(packagesDir)
|
||||
.map((name) => join(packagesDir, name, 'tsconfig.test.json'))
|
||||
.filter((config) => existsSync(config))
|
||||
.map((config) => relative(repoRoot, config).split(sep).join('/'))
|
||||
|
||||
it('type-checks every package test project, which no root project includes', () => {
|
||||
expect(packageTestProjects).toContain('src/packages/process-host/tsconfig.test.json')
|
||||
const scheduled = TYPECHECK_PROJECTS.map((project) => project.config)
|
||||
expect(scheduled).toEqual(expect.arrayContaining(packageTestProjects))
|
||||
})
|
||||
|
||||
it("resolves a package project's compiler from that package, not the root", () => {
|
||||
const root = realpathSync(mkdtempSync(join(tmpdir(), 'orca-typecheck-compiler-')))
|
||||
try {
|
||||
const writeCompiler = (directory, version) => {
|
||||
const compilerDir = join(root, directory, 'node_modules', 'typescript')
|
||||
mkdirSync(join(compilerDir, 'bin'), { recursive: true })
|
||||
writeFileSync(
|
||||
join(compilerDir, 'package.json'),
|
||||
JSON.stringify({ name: 'typescript', version, bin: { tsc: './bin/tsc' } })
|
||||
)
|
||||
return join(compilerDir, 'bin', 'tsc')
|
||||
}
|
||||
const rootCompiler = writeCompiler('.', '6.0.0')
|
||||
const packageCompiler = writeCompiler('src/packages/example', '7.0.0')
|
||||
mkdirSync(join(root, 'config'))
|
||||
|
||||
expect(resolveProjectCompiler('src/packages/example/tsconfig.test.json', root)).toBe(
|
||||
packageCompiler
|
||||
)
|
||||
expect(resolveProjectCompiler('config/tsconfig.node.json', root)).toBe(rootCompiler)
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { resolve } from 'node:path'
|
||||
import { runProcessSync } from './script-child-process.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
|
||||
const root = resolve(import.meta.dirname, '../..')
|
||||
try {
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { build } from 'esbuild'
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
const root = resolve(import.meta.dirname, '../..')
|
||||
const temporary = mkdtempSync(join(tmpdir(), 'orca-script-child-process-'))
|
||||
const output = join(temporary, 'child-process.mjs')
|
||||
let implementation
|
||||
|
||||
try {
|
||||
await build({
|
||||
stdin: {
|
||||
contents: [
|
||||
`export { runProcessSync, spawnProcess } from ${JSON.stringify(join(root, 'src/shared/child-process/run-process.ts'))}`
|
||||
].join('\n'),
|
||||
resolveDir: root,
|
||||
sourcefile: 'script-child-process-entry.ts'
|
||||
},
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
target: 'node20',
|
||||
format: 'esm',
|
||||
outfile: output,
|
||||
logLevel: 'silent'
|
||||
})
|
||||
implementation = await import(pathToFileURL(output).href)
|
||||
} finally {
|
||||
rmSync(temporary, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
export const runProcessSync = implementation.runProcessSync
|
||||
|
||||
export const spawnProcess = implementation.spawnProcess
|
||||
|
||||
/** Why a failed child failed, for CI logs where an empty stderr alone says nothing. */
|
||||
export function describeProcessFailure(result) {
|
||||
const detail = [
|
||||
`code=${result.code ?? 'none'}`,
|
||||
`signal=${result.signal ?? 'none'}`,
|
||||
...(result.timedOut ? ['timed out'] : []),
|
||||
...(result.outputTruncated ? ['output truncated'] : [])
|
||||
].join(' ')
|
||||
const stream = (name, text) => (text?.trim() ? `\n${name}:\n${text.trim().slice(-4000)}` : '')
|
||||
return `${detail}${stream('stdout', result.stdout)}${stream('stderr', result.stderr)}`
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { mkdirSync, 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 { runProcessSync } from '@orca/process-host'
|
||||
import { describeProcessFailure } from './process-failure-message.mjs'
|
||||
import { ORCAD_SESSION_SCANNER_SERVICE_ENTRY } from '../../src/shared/orcad-artifacts.ts'
|
||||
import { AI_VAULT_SERVICE_PROTOCOL_VERSION } from '../../src/main/ai-vault/session-scanner-service-protocol.ts'
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { mkdir, readFile, writeFile, copyFile } from 'node:fs/promises'
|
||||
import { resolve, join, relative } from 'node:path'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { build } from 'esbuild'
|
||||
import { runProcess } from '../../../src/shared/child-process/run-process'
|
||||
import { runProcess } from '@orca/process-host'
|
||||
|
||||
const root = process.cwd()
|
||||
const evidence = resolve('notes/search-ipc')
|
||||
@@ -53,7 +53,7 @@ export { resetAiVaultScannerServiceForTests, reconcileSessionSearchInService } f
|
||||
export { setAppEnvironment } from ${source('shared/app-environment.ts')};
|
||||
export { isolatedScanRoots } from ${source('main/ai-vault/session-scanner-test-fixtures.ts')};
|
||||
export { claudeLines } from ${source('main/ai-vault-search/session-search-indexer-test-fixture.ts')};
|
||||
export { runProcess } from ${source('shared/child-process/run-process.ts')};
|
||||
export { runProcess } from '@orca/process-host';
|
||||
export { UnixSocketTransport } from ${source('main/runtime/rpc/unix-socket-transport.ts')};
|
||||
export { RpcDispatcher } from ${source('main/runtime/rpc/dispatcher.ts')};
|
||||
export { AI_VAULT_METHODS } from ${source('main/runtime/rpc/methods/ai-vault.ts')};
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
runPackagedProcessHostConsumer,
|
||||
stagePackagedProcessHost,
|
||||
writePackagedProcessHostConsumer
|
||||
} from './packaged-process-host-fixture.mjs'
|
||||
|
||||
// Qualifies the package's engines floor against the staged public output, not the checkout source.
|
||||
assert.match(process.versions.node, /^18\./, 'This smoke test must run under Node 18')
|
||||
|
||||
const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-process-host-node18-'))
|
||||
try {
|
||||
await stagePackagedProcessHost(resourcesDir)
|
||||
const { entry } = await writePackagedProcessHostConsumer(resourcesDir)
|
||||
const result = runPackagedProcessHostConsumer(process.execPath, resourcesDir, entry)
|
||||
assert.equal(result.code, 0, result.stderr)
|
||||
assert.equal(result.stdout, 'copied-runtime')
|
||||
} finally {
|
||||
await rm(resourcesDir, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
console.log(`Node ${process.versions.node} process-host package smoke passed.`)
|
||||
@@ -0,0 +1,240 @@
|
||||
import ts from 'typescript-api'
|
||||
|
||||
const MODULE_BUILTINS = new Set(['module', 'node:module'])
|
||||
const PROCESS_BUILTINS = new Set(['process', 'node:process'])
|
||||
|
||||
function unwrap(node) {
|
||||
while (
|
||||
node &&
|
||||
(ts.isParenthesizedExpression(node) ||
|
||||
ts.isAsExpression(node) ||
|
||||
ts.isSatisfiesExpression(node) ||
|
||||
ts.isNonNullExpression(node) ||
|
||||
ts.isTypeAssertionExpression(node) ||
|
||||
ts.isAwaitExpression(node))
|
||||
) {
|
||||
node = node.expression
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
function isNamed(node, name) {
|
||||
return ts.isIdentifier(node) && node.text === name
|
||||
}
|
||||
|
||||
function literalString(node) {
|
||||
node = unwrap(node)
|
||||
return node && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
|
||||
? node.text
|
||||
: null
|
||||
}
|
||||
|
||||
function propertyName(node) {
|
||||
if (ts.isPropertyAccessExpression(node)) {
|
||||
return node.name.text
|
||||
}
|
||||
return ts.isElementAccessExpression(node) ? literalString(node.argumentExpression) : null
|
||||
}
|
||||
|
||||
function isConstDeclaration(node) {
|
||||
return ts.isVariableDeclarationList(node.parent) && (node.parent.flags & ts.NodeFlags.Const) !== 0
|
||||
}
|
||||
|
||||
function collectBindings(source) {
|
||||
const strings = new Map()
|
||||
const declarations = []
|
||||
const destructures = []
|
||||
const imports = []
|
||||
function visit(node) {
|
||||
if (ts.isVariableDeclaration(node) && node.initializer) {
|
||||
const initializer = unwrap(node.initializer)
|
||||
if (ts.isIdentifier(node.name)) {
|
||||
declarations.push({ name: node.name.text, initializer })
|
||||
const value = literalString(initializer)
|
||||
if (isConstDeclaration(node) && value !== null) {
|
||||
strings.set(node.name.text, [...(strings.get(node.name.text) ?? []), value])
|
||||
}
|
||||
} else if (ts.isObjectBindingPattern(node.name)) {
|
||||
for (const element of node.name.elements) {
|
||||
const key = element.propertyName ?? element.name
|
||||
const name = ts.isIdentifier(key) ? key.text : literalString(key)
|
||||
if (ts.isIdentifier(element.name) && name !== null) {
|
||||
destructures.push({ name: element.name.text, key: name, initializer })
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
||||
imports.push({ module: node.moduleSpecifier.text, clause: node.importClause })
|
||||
} else if (
|
||||
ts.isImportEqualsDeclaration(node) &&
|
||||
ts.isExternalModuleReference(node.moduleReference) &&
|
||||
ts.isStringLiteral(node.moduleReference.expression)
|
||||
) {
|
||||
imports.push({ module: node.moduleReference.expression.text, namespace: node.name })
|
||||
}
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
visit(source)
|
||||
return { strings, declarations, destructures, imports }
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifiers a file statically loads: imports, re-exports, `import()`, `require`/`require.resolve`,
|
||||
* `module.require`, `createRequire(...)` loaders and their aliases, and `process.getBuiltinModule`.
|
||||
* A specifier argument may be a literal or a file-level `const` string. Name bindings are
|
||||
* file-wide, not scope-aware. Literal property keys are resolved; computed key values are not.
|
||||
* Computed specifiers (concatenation, templates with substitutions, parameters, values imported
|
||||
* from other files), loaders stored in objects or passed as arguments, and `eval`/`Function`
|
||||
* are not resolved.
|
||||
*/
|
||||
export function collectModuleSpecifiers(file, contents) {
|
||||
const source = ts.createSourceFile(file, contents, ts.ScriptTarget.Latest, true)
|
||||
if (source.parseDiagnostics.length > 0) {
|
||||
throw new Error(`Cannot parse ${file}; the process-host import check must not skip it.`)
|
||||
}
|
||||
const { strings, declarations, destructures, imports } = collectBindings(source)
|
||||
const loaders = new Set(['require'])
|
||||
const createRequires = new Set()
|
||||
const moduleNamespaces = new Set()
|
||||
const processes = new Set(['process'])
|
||||
const builtinLoaders = new Set()
|
||||
|
||||
function specifierValues(node) {
|
||||
node = unwrap(node)
|
||||
const value = literalString(node)
|
||||
if (value !== null) {
|
||||
return [value]
|
||||
}
|
||||
return node && ts.isIdentifier(node) ? (strings.get(node.text) ?? []) : []
|
||||
}
|
||||
function isProcess(node) {
|
||||
node = unwrap(node)
|
||||
return (
|
||||
(ts.isIdentifier(node) && processes.has(node.text)) ||
|
||||
(propertyName(node) === 'process' && isNamed(node.expression, 'globalThis')) ||
|
||||
loadsAny(node, PROCESS_BUILTINS)
|
||||
)
|
||||
}
|
||||
function isBuiltinLoader(node) {
|
||||
node = unwrap(node)
|
||||
return (
|
||||
(ts.isIdentifier(node) && builtinLoaders.has(node.text)) ||
|
||||
(propertyName(node) === 'getBuiltinModule' && isProcess(node.expression))
|
||||
)
|
||||
}
|
||||
function isCreateRequire(node) {
|
||||
node = unwrap(node)
|
||||
return (
|
||||
(ts.isIdentifier(node) && createRequires.has(node.text)) ||
|
||||
(propertyName(node) === 'createRequire' && isModuleNamespace(node.expression))
|
||||
)
|
||||
}
|
||||
function isLoader(node) {
|
||||
node = unwrap(node)
|
||||
return (
|
||||
(ts.isIdentifier(node) && loaders.has(node.text)) ||
|
||||
(propertyName(node) === 'require' && isNamed(node.expression, 'module')) ||
|
||||
(ts.isCallExpression(node) && isCreateRequire(node.expression))
|
||||
)
|
||||
}
|
||||
function loadsAny(node, specifiers) {
|
||||
return (
|
||||
ts.isCallExpression(node) &&
|
||||
(node.expression.kind === ts.SyntaxKind.ImportKeyword ||
|
||||
isLoader(node.expression) ||
|
||||
isBuiltinLoader(node.expression)) &&
|
||||
specifierValues(node.arguments[0]).some((specifier) => specifiers.has(specifier))
|
||||
)
|
||||
}
|
||||
function isModuleNamespace(node) {
|
||||
node = unwrap(node)
|
||||
return (
|
||||
(ts.isIdentifier(node) && moduleNamespaces.has(node.text)) || loadsAny(node, MODULE_BUILTINS)
|
||||
)
|
||||
}
|
||||
|
||||
for (const { module, clause, namespace } of imports) {
|
||||
const named = clause?.namedBindings
|
||||
const local = [
|
||||
namespace,
|
||||
clause?.name,
|
||||
named && ts.isNamespaceImport(named) ? named.name : null
|
||||
]
|
||||
for (const name of local.filter(Boolean)) {
|
||||
if (MODULE_BUILTINS.has(module)) {
|
||||
moduleNamespaces.add(name.text)
|
||||
}
|
||||
if (PROCESS_BUILTINS.has(module)) {
|
||||
processes.add(name.text)
|
||||
}
|
||||
}
|
||||
for (const element of named && ts.isNamedImports(named) ? named.elements : []) {
|
||||
const imported = (element.propertyName ?? element.name).text
|
||||
if (MODULE_BUILTINS.has(module) && (imported === 'default' || imported === 'Module')) {
|
||||
moduleNamespaces.add(element.name.text)
|
||||
}
|
||||
if (PROCESS_BUILTINS.has(module) && imported === 'default') {
|
||||
processes.add(element.name.text)
|
||||
}
|
||||
if (MODULE_BUILTINS.has(module) && imported === 'createRequire') {
|
||||
createRequires.add(element.name.text)
|
||||
}
|
||||
if (PROCESS_BUILTINS.has(module) && imported === 'getBuiltinModule') {
|
||||
builtinLoaders.add(element.name.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Aliases can chain (`const m = require('module'); const r = m.createRequire(x)`).
|
||||
for (let changed = true; changed;) {
|
||||
changed = false
|
||||
const bind = (set, name, matches) => {
|
||||
if (matches && !set.has(name)) {
|
||||
set.add(name)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
for (const { name, initializer } of declarations) {
|
||||
bind(loaders, name, isLoader(initializer))
|
||||
bind(createRequires, name, isCreateRequire(initializer))
|
||||
bind(moduleNamespaces, name, isModuleNamespace(initializer))
|
||||
bind(processes, name, isProcess(initializer))
|
||||
bind(builtinLoaders, name, isBuiltinLoader(initializer))
|
||||
}
|
||||
for (const { name, key, initializer } of destructures) {
|
||||
bind(createRequires, name, key === 'createRequire' && isModuleNamespace(initializer))
|
||||
bind(builtinLoaders, name, key === 'getBuiltinModule' && isProcess(initializer))
|
||||
}
|
||||
}
|
||||
|
||||
const specifiers = new Set()
|
||||
function add(node) {
|
||||
for (const specifier of specifierValues(node)) {
|
||||
specifiers.add(specifier)
|
||||
}
|
||||
}
|
||||
function visit(node) {
|
||||
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
|
||||
add(node.moduleSpecifier)
|
||||
} else if (
|
||||
ts.isImportEqualsDeclaration(node) &&
|
||||
ts.isExternalModuleReference(node.moduleReference)
|
||||
) {
|
||||
add(node.moduleReference.expression)
|
||||
} else if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
|
||||
add(node.argument.literal)
|
||||
} else if (ts.isCallExpression(node)) {
|
||||
const callee = unwrap(node.expression)
|
||||
if (
|
||||
callee.kind === ts.SyntaxKind.ImportKeyword ||
|
||||
isLoader(callee) ||
|
||||
isBuiltinLoader(callee) ||
|
||||
(propertyName(callee) === 'resolve' && isLoader(callee.expression))
|
||||
) {
|
||||
add(node.arguments[0])
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
visit(source)
|
||||
return [...specifiers]
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { assessProcessHostImports } from './check-process-host-imports.mjs'
|
||||
import { collectModuleSpecifiers } from './static-module-specifiers.mjs'
|
||||
|
||||
function specifiers(contents) {
|
||||
return collectModuleSpecifiers('file.ts', contents)
|
||||
}
|
||||
|
||||
describe('static module specifiers', () => {
|
||||
it.each([
|
||||
[
|
||||
'a direct createRequire call',
|
||||
"import { createRequire } from 'node:module'\ncreateRequire(import.meta.url)('child_process')"
|
||||
],
|
||||
[
|
||||
'an assigned createRequire loader',
|
||||
"import { createRequire } from 'node:module'\nconst load = createRequire(__filename)\nload('node:child_process')"
|
||||
],
|
||||
[
|
||||
'a renamed createRequire import',
|
||||
"import { createRequire as make } from 'module'\nconst load = make(__filename)\nload.resolve('child_process')"
|
||||
],
|
||||
[
|
||||
'a namespace createRequire',
|
||||
"import * as nodeModule from 'node:module'\nnodeModule.createRequire(__filename)('child_process')"
|
||||
],
|
||||
[
|
||||
'a default-import createRequire',
|
||||
"import Module from 'module'\nconst load = Module.createRequire(__filename)\nload('child_process')"
|
||||
],
|
||||
[
|
||||
'a named default-import createRequire',
|
||||
"import { default as Module } from 'node:module'\nModule.createRequire(__filename)('child_process')"
|
||||
],
|
||||
[
|
||||
'a named Module constructor',
|
||||
"import { Module } from 'node:module'\nModule.createRequire(__filename)('child_process')"
|
||||
],
|
||||
[
|
||||
'a renamed Module constructor',
|
||||
"import { Module as HostModule } from 'module'\nHostModule.createRequire(__filename)('node:child_process')"
|
||||
],
|
||||
[
|
||||
'an import-equals createRequire',
|
||||
"import Module = require('node:module')\nconst load = Module.createRequire(__filename)\nload('node:child_process')"
|
||||
],
|
||||
[
|
||||
'a literal-property createRequire',
|
||||
"import * as Module from 'node:module'\nModule['createRequire'](__filename)('child_process')"
|
||||
],
|
||||
[
|
||||
'a required createRequire',
|
||||
"const { createRequire: make } = require('node:module')\nmake(__filename)('child_process')"
|
||||
],
|
||||
[
|
||||
'a quoted destructured createRequire',
|
||||
"const { 'createRequire': make } = require('node:module')\nmake(__filename)('child_process')"
|
||||
],
|
||||
[
|
||||
'a chained module alias',
|
||||
"const nodeModule = require('module')\nconst load = nodeModule.createRequire(__filename)\nload('child_process')"
|
||||
],
|
||||
[
|
||||
'an awaited module import',
|
||||
"const { createRequire } = await import('node:module')\ncreateRequire(import.meta.url)('child_process')"
|
||||
],
|
||||
['a reassigned require', "const load = require\nload('child_process')"],
|
||||
['module.require', "module.require('child_process')"],
|
||||
['a literal-property module.require', "module['require']('child_process')"],
|
||||
['a literal-property require.resolve', "require['resolve']('child_process')"],
|
||||
['process.getBuiltinModule', "process.getBuiltinModule('node:child_process')"],
|
||||
['a literal-property getBuiltinModule', "process['getBuiltinModule']('node:child_process')"],
|
||||
['a template-property getBuiltinModule', 'process[`getBuiltinModule`]("child_process")'],
|
||||
['globalThis.process.getBuiltinModule', "globalThis.process.getBuiltinModule('child_process')"],
|
||||
[
|
||||
'literal properties on globalThis.process',
|
||||
"globalThis['process']['getBuiltinModule']('child_process')"
|
||||
],
|
||||
[
|
||||
'an imported process',
|
||||
"import nodeProcess from 'node:process'\nnodeProcess.getBuiltinModule('child_process')"
|
||||
],
|
||||
[
|
||||
'a named default-import process',
|
||||
"import { default as nodeProcess } from 'node:process'\nnodeProcess.getBuiltinModule('child_process')"
|
||||
],
|
||||
[
|
||||
'an import-equals process',
|
||||
"import nodeProcess = require('node:process')\nnodeProcess.getBuiltinModule('child_process')"
|
||||
],
|
||||
[
|
||||
'a destructured getBuiltinModule',
|
||||
"const { getBuiltinModule } = process\ngetBuiltinModule('child_process')"
|
||||
],
|
||||
[
|
||||
'a quoted destructured getBuiltinModule',
|
||||
"const { 'getBuiltinModule': load } = process\nload('child_process')"
|
||||
],
|
||||
[
|
||||
'a builtin module with a literal-property factory',
|
||||
"process['getBuiltinModule']('node:module')['createRequire'](__filename)('child_process')"
|
||||
],
|
||||
[
|
||||
'an imported getBuiltinModule',
|
||||
"import { getBuiltinModule as load } from 'process'\nload('child_process')"
|
||||
],
|
||||
[
|
||||
'a const specifier',
|
||||
"const CHILD_PROCESS = 'node:child_process' as const\nrequire(CHILD_PROCESS)"
|
||||
],
|
||||
[
|
||||
'a const template specifier',
|
||||
'const CHILD_PROCESS = `child_process`\nprocess.getBuiltinModule(CHILD_PROCESS)'
|
||||
]
|
||||
])('resolves child_process loaded through %s', (_label, contents) => {
|
||||
expect(
|
||||
specifiers(contents).filter((specifier) => specifier.endsWith('child_process'))
|
||||
).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('ignores unrelated objects, computed property values, and non-constant specifiers', () => {
|
||||
expect(
|
||||
specifiers(
|
||||
[
|
||||
'const registry = { getBuiltinModule: (name) => name, createRequire: () => () => null }',
|
||||
"registry.getBuiltinModule('child_process')",
|
||||
"registry.createRequire(__filename)('child_process')",
|
||||
"registry['getBuiltinModule']('child_process')",
|
||||
"registry['createRequire'](__filename)('child_process')",
|
||||
"const method = 'getBuiltinModule'",
|
||||
"process[method]('child_process')",
|
||||
"let mutable = 'child_process'",
|
||||
'require(mutable)',
|
||||
"require('child' + '_process')",
|
||||
"const name = 'child'",
|
||||
'require(`${name}_process`)'
|
||||
].join('\n')
|
||||
)
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it('flags a new production file that bypasses the import form', () => {
|
||||
expect(
|
||||
assessProcessHostImports(
|
||||
new Map([
|
||||
[
|
||||
'src/main/bypass.ts',
|
||||
"import { createRequire } from 'node:module'\nconst load = createRequire(import.meta.url)\nload('child_process')"
|
||||
],
|
||||
['src/main/builtin.ts', "process['getBuiltinModule']('node:child_process')"],
|
||||
[
|
||||
'src/main/import-equals.ts',
|
||||
"import Module = require('node:module')\nconst load = Module.createRequire(__filename)\nload('node:child_process')"
|
||||
],
|
||||
[
|
||||
'src/main/named-default-module.ts',
|
||||
"import { default as Module } from 'node:module'\nModule.createRequire(__filename)('child_process')"
|
||||
],
|
||||
[
|
||||
'src/main/named-default-process.ts',
|
||||
"import { default as nodeProcess } from 'node:process'\nnodeProcess.getBuiltinModule('child_process')"
|
||||
]
|
||||
]),
|
||||
{ exports: {} },
|
||||
[]
|
||||
).added
|
||||
).toEqual([
|
||||
'src/main/builtin.ts',
|
||||
'src/main/bypass.ts',
|
||||
'src/main/import-equals.ts',
|
||||
'src/main/named-default-module.ts',
|
||||
'src/main/named-default-process.ts'
|
||||
])
|
||||
})
|
||||
|
||||
it.each([
|
||||
"import { createRequire } from 'node:module'\nconst ELECTRON = 'electron'\ncreateRequire(__filename)(ELECTRON)",
|
||||
"import Module = require('node:module')\nModule.createRequire(__filename)('electron')",
|
||||
"import { Module as HostModule } from 'module'\nHostModule.createRequire(__filename)('electron')",
|
||||
"import * as Module from 'node:module'\nModule['createRequire'](__filename)('electron')",
|
||||
"const { 'createRequire': make } = require('node:module')\nmake(__filename)('electron')"
|
||||
])('applies package dependency rules to dynamically loaded specifiers: %s', (contents) => {
|
||||
expect(
|
||||
assessProcessHostImports(
|
||||
new Map([['src/packages/process-host/src/runner.ts', contents]]),
|
||||
{ exports: {} },
|
||||
[]
|
||||
).violations
|
||||
).toEqual([
|
||||
'src/packages/process-host/src/runner.ts: undeclared process-host dependency: electron'
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -4,7 +4,7 @@ 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'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
|
||||
const workflow = parse(readFileSync('.github/workflows/terminal-perf.yml', 'utf8'))
|
||||
const steps = workflow.jobs['terminal-perf'].steps
|
||||
@@ -14,7 +14,9 @@ const supportedAction = readFileSync('.github/actions/install-node-dependencies/
|
||||
const supportedManifest = {
|
||||
engines: { node: '24' },
|
||||
packageManager: 'pnpm@12.8.1',
|
||||
scripts: { postinstall: 'node config/scripts/rebuild-native-deps.mjs' }
|
||||
scripts: {
|
||||
postinstall: 'pnpm run build:packages && node config/scripts/rebuild-native-deps.mjs'
|
||||
}
|
||||
}
|
||||
|
||||
function select(options = {}) {
|
||||
@@ -64,6 +66,17 @@ it('selects the measured current root profile with the actual installer metadata
|
||||
).toBe('shared=true')
|
||||
})
|
||||
|
||||
it('keeps the shared preparation route for the qualified older lifecycle', () => {
|
||||
expect(
|
||||
select({
|
||||
manifest: {
|
||||
...supportedManifest,
|
||||
scripts: { postinstall: 'node config/scripts/rebuild-native-deps.mjs' }
|
||||
}
|
||||
})
|
||||
).toBe('shared=true')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['historical Node', { manifest: { ...supportedManifest, engines: { node: '22' } } }],
|
||||
['historical pnpm', { manifest: { ...supportedManifest, packageManager: 'pnpm@10.0.0' } }],
|
||||
@@ -73,6 +86,15 @@ it.each([
|
||||
'extra lifecycle work',
|
||||
{ manifest: { ...supportedManifest, scripts: { postinstall: 'generate' } } }
|
||||
],
|
||||
[
|
||||
'additional package lifecycle work',
|
||||
{
|
||||
manifest: {
|
||||
...supportedManifest,
|
||||
scripts: { postinstall: `${supportedManifest.scripts.postinstall} && generate` }
|
||||
}
|
||||
}
|
||||
],
|
||||
['self-hosted runner', { kind: 'self-hosted' }],
|
||||
['job container', { container: 'container-id' }],
|
||||
['another OS', { os: 'Windows' }],
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
} from '../../src/shared/node-runtime-pin.ts'
|
||||
import { currentTarget } from './server-build-target.mjs'
|
||||
import { nodeDistArchiveName, windowsImportLibFile } from './node-dist-archive-name.mjs'
|
||||
import { runProcessSync } from './script-child-process.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { getTarProgram, getZipExtractorCommand } from './zip-extractor-command.mjs'
|
||||
|
||||
const root = resolve(import.meta.dirname, '../..')
|
||||
|
||||
@@ -2,7 +2,7 @@ import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { runProcessSync } from './script-child-process.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { main as verifyTogether } from './verify-localization-catalogs.mjs'
|
||||
import { main as verifyCatalog } from './verify-localization-catalog.mjs'
|
||||
import { main as verifyRuntimeCatalog } from './generate-runtime-required-english-catalog.mjs'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user