mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
* Harden layout validation, watcher lifecycle, and connection robustness - Throw instead of silently skipping when the packaged daemon-entry is missing, preventing layout regressions from passing build checks. - Terminate idle parcel-watcher processes to reclaim native handles and avoid crash-prone native node module teardowns on shutdown. - Bind the persisted WS fallback port first to prevent orphaning active mobile pairings when the preferred port becomes free again. - Cap concurrent disk reads for restored dirty tab verification at three to prevent startup connection bottlenecks on remote SSH workspaces. * Queue file IDs instead of snapshots in restored conflict scans This avoids using stale file snapshots (e.g., outdated disk signatures) if a tab is saved, closed, or re-baselined while waiting in the queue behind the concurrency limit. The live state is now fetched from the store and validated immediately before initiating the disk read.
379 lines
16 KiB
JavaScript
379 lines
16 KiB
JavaScript
import { mkdir, mkdtemp, readFile, readdir, rm, stat, 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'
|
|
|
|
const require = createRequire(import.meta.url)
|
|
const electronBuilderConfig = require('../electron-builder.config.cjs')
|
|
const electronBuilderNativeRebuild = require('./electron-builder-native-rebuild.cjs')
|
|
const {
|
|
createPackagedRuntimeNodeModuleResources,
|
|
findAsarEntry,
|
|
prunePackagedNodePty,
|
|
prunePackagedParcelWatcher,
|
|
prunePackagedSherpaOnnx,
|
|
prunePackagedRuntimeTypeDeclarations,
|
|
prunePackagedZodSources,
|
|
verifyPackagedMainRuntimeDeps
|
|
} = require('../packaged-runtime-node-modules.cjs')
|
|
|
|
describe('electron-builder config', () => {
|
|
it('excludes repo-only source trees from app.asar', () => {
|
|
expect(electronBuilderConfig.files).toEqual(
|
|
expect.arrayContaining([
|
|
'!src{,/**/*}',
|
|
'!config{,/**/*}',
|
|
'!docs{,/**/*}',
|
|
'!mobile{,/**/*}',
|
|
'!native{,/**/*}',
|
|
'!skills{,/**/*}',
|
|
'!tests{,/**/*}',
|
|
'!Casks{,/**/*}',
|
|
'!{AGENTS.md,CLAUDE.md,DEVELOPING.md,bundle-size-progress.md}',
|
|
'!out/**/*.test.js'
|
|
])
|
|
)
|
|
})
|
|
|
|
it('keeps runtime resources available through extraResources', () => {
|
|
expect(electronBuilderConfig.mac.extraResources).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
from: 'native/computer-use-macos/.build/release/Orca Computer Use.app',
|
|
to: 'Orca Computer Use.app'
|
|
})
|
|
])
|
|
)
|
|
expect(electronBuilderConfig.linux.extraResources).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
from: 'native/computer-use-linux/runtime.py',
|
|
to: 'computer-use-linux/runtime.py'
|
|
})
|
|
])
|
|
)
|
|
expect(electronBuilderConfig.win.extraResources).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
from: 'native/computer-use-windows/runtime.ps1',
|
|
to: 'computer-use-windows/runtime.ps1'
|
|
})
|
|
])
|
|
)
|
|
})
|
|
|
|
// Why: on macOS 26 UNUserNotificationCenter aborts for executables launched
|
|
// from Contents/Resources, so the helper must ship in Contents/MacOS (#7929).
|
|
it('ships the mac notification-status helper in Contents/MacOS, not Resources', () => {
|
|
expect(electronBuilderConfig.mac.extraFiles).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
from: 'native/notification-status-macos/.build/release/orca-notification-status',
|
|
to: 'MacOS/orca-notification-status'
|
|
})
|
|
])
|
|
)
|
|
expect(electronBuilderConfig.mac.extraResources).not.toEqual(
|
|
expect.arrayContaining([expect.objectContaining({ to: 'orca-notification-status' })])
|
|
)
|
|
})
|
|
|
|
it('unpacks the compiled CommonJS boundary with CLI runtime files', () => {
|
|
expect(electronBuilderConfig.asarUnpack).toEqual(
|
|
expect.arrayContaining(['out/package.json', 'out/cli/**', 'out/shared/**'])
|
|
)
|
|
})
|
|
|
|
// Why: without the unpacked entry the watcher client silently falls back to
|
|
// in-process @parcel/watcher, reintroducing the #7547 main-process crash.
|
|
it('unpacks the forked parcel-watcher process entry', () => {
|
|
expect(electronBuilderConfig.asarUnpack).toEqual(
|
|
expect.arrayContaining(['out/main/parcel-watcher-process-entry.js'])
|
|
)
|
|
})
|
|
|
|
it('uses the multi-size icon source for Linux packages', () => {
|
|
expect(electronBuilderConfig.linux.icon).toBe('resources/build/icon.icns')
|
|
})
|
|
|
|
it('matches the Linux desktop entry to Electron window class', () => {
|
|
expect(electronBuilderConfig.linux.desktop.entry.StartupWMClass).toBe('orca')
|
|
})
|
|
|
|
it('uses AppImage and deb as local Linux targets without changing existing artifact names', () => {
|
|
expect(electronBuilderConfig.linux.target).toEqual(['AppImage', 'deb'])
|
|
expect(electronBuilderConfig.appImage.artifactName).toBe('orca-linux.${ext}')
|
|
expect(electronBuilderConfig.deb.artifactName).toBe('orca-ide_${version}_${arch}.${ext}')
|
|
expect(electronBuilderConfig.rpm).toMatchObject({
|
|
packageName: 'orca-ide',
|
|
artifactName: 'orca-ide-${version}.${arch}.${ext}'
|
|
})
|
|
})
|
|
|
|
it('uses a distinct AppImage name for Linux arm64 release uploads', () => {
|
|
const configPath = require.resolve('../electron-builder.config.cjs')
|
|
const original = process.env.ORCA_LINUX_ARM64_RELEASE
|
|
try {
|
|
delete require.cache[configPath]
|
|
process.env.ORCA_LINUX_ARM64_RELEASE = '1'
|
|
expect(require('../electron-builder.config.cjs').appImage.artifactName).toBe(
|
|
'orca-linux-arm64.${ext}'
|
|
)
|
|
} finally {
|
|
if (original === undefined) {
|
|
delete process.env.ORCA_LINUX_ARM64_RELEASE
|
|
} else {
|
|
process.env.ORCA_LINUX_ARM64_RELEASE = original
|
|
}
|
|
delete require.cache[configPath]
|
|
require('../electron-builder.config.cjs')
|
|
}
|
|
})
|
|
|
|
it('uses Orca native rebuild hook instead of electron-builder default rebuild', () => {
|
|
expect(electronBuilderConfig.beforeBuild).toBe(electronBuilderNativeRebuild)
|
|
expect(electronBuilderConfig.npmRebuild).toBe(true)
|
|
})
|
|
|
|
it('verifies packaged main runtime deps from Windows-style asar entries', async () => {
|
|
const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-runtime-deps-'))
|
|
try {
|
|
await writeFile(join(resourcesDir, 'app.asar'), '', 'utf8')
|
|
await mkdir(join(resourcesDir, 'node_modules', 'yaml'), { recursive: true })
|
|
await mkdir(join(resourcesDir, 'node_modules', 'zod'), { recursive: true })
|
|
|
|
const sources = new Map([
|
|
['out\\main\\index.js', 'const z = require("zod")'],
|
|
['out\\main\\agent-hooks\\managed-agent-hook-controls.js', 'const YAML = require("yaml")']
|
|
])
|
|
const asar = {
|
|
listPackage: () => [...sources.keys()].map((entry) => `\\${entry}`),
|
|
extractFile: (_asarPath, internalPath) => Buffer.from(sources.get(internalPath), 'utf8')
|
|
}
|
|
|
|
expect(() => verifyPackagedMainRuntimeDeps(resourcesDir, asar)).not.toThrow()
|
|
} finally {
|
|
await rm(resourcesDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('normalizes host-specific asar entry separators', () => {
|
|
expect(findAsarEntry(['\\out\\main\\index.js'], 'out/main/index.js')).toBe(
|
|
'\\out\\main\\index.js'
|
|
)
|
|
expect(findAsarEntry(['/out/main/index.js'], 'out/main/index.js')).toBe('/out/main/index.js')
|
|
})
|
|
|
|
it('prunes non-target node-pty prebuilds from packaged runtime resources', async () => {
|
|
const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-node-pty-prune-'))
|
|
try {
|
|
const prebuildsDir = join(resourcesDir, 'node_modules', 'node-pty', 'prebuilds')
|
|
await mkdir(join(prebuildsDir, 'darwin-arm64'), { recursive: true })
|
|
await mkdir(join(prebuildsDir, 'darwin-x64'), { recursive: true })
|
|
await mkdir(join(prebuildsDir, 'linux-x64'), { recursive: true })
|
|
await mkdir(join(prebuildsDir, 'win32-x64'), { recursive: true })
|
|
await mkdir(join(resourcesDir, 'node_modules', 'node-pty', 'third_party', 'conpty'), {
|
|
recursive: true
|
|
})
|
|
await mkdir(join(resourcesDir, 'node_modules', 'node-pty', 'deps', 'winpty'), {
|
|
recursive: true
|
|
})
|
|
|
|
prunePackagedNodePty(resourcesDir, 'darwin')
|
|
|
|
await expect(readdir(prebuildsDir).then((entries) => entries.sort())).resolves.toEqual([
|
|
'darwin-arm64',
|
|
'darwin-x64'
|
|
])
|
|
await expect(
|
|
readdir(join(resourcesDir, 'node_modules', 'node-pty', 'third_party'))
|
|
).resolves.toEqual([])
|
|
await expect(
|
|
readdir(join(resourcesDir, 'node_modules', 'node-pty', 'deps'))
|
|
).resolves.toEqual([])
|
|
} finally {
|
|
await rm(resourcesDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('copies the Windows node-pty ConPTY runtime beside the rebuilt addon', async () => {
|
|
for (const arch of ['x64', 'arm64']) {
|
|
const resourcesDir = await mkdtemp(join(tmpdir(), `orca-node-pty-conpty-${arch}-`))
|
|
try {
|
|
const nodePtyDir = join(resourcesDir, 'node_modules', 'node-pty')
|
|
const releaseDir = join(nodePtyDir, 'build', 'Release')
|
|
const conptyRoot = join(nodePtyDir, 'third_party', 'conpty', '0.1.0')
|
|
await mkdir(releaseDir, { recursive: true })
|
|
await writeFile(join(releaseDir, 'conpty.node'), 'native addon placeholder', 'utf8')
|
|
for (const sourceArch of ['x64', 'arm64']) {
|
|
const sourceDir = join(conptyRoot, `win10-${sourceArch}`)
|
|
await mkdir(sourceDir, { recursive: true })
|
|
await writeFile(join(sourceDir, 'conpty.dll'), `dll payload ${sourceArch}`, 'utf8')
|
|
await writeFile(
|
|
join(sourceDir, 'OpenConsole.exe'),
|
|
`console payload ${sourceArch}`,
|
|
'utf8'
|
|
)
|
|
}
|
|
|
|
prunePackagedNodePty(resourcesDir, 'win32', arch)
|
|
|
|
await expect(readFile(join(releaseDir, 'conpty', 'conpty.dll'), 'utf8')).resolves.toBe(
|
|
`dll payload ${arch}`
|
|
)
|
|
await expect(readFile(join(releaseDir, 'conpty', 'OpenConsole.exe'), 'utf8')).resolves.toBe(
|
|
`console payload ${arch}`
|
|
)
|
|
} finally {
|
|
await rm(resourcesDir, { recursive: true, force: true })
|
|
}
|
|
}
|
|
})
|
|
|
|
it('includes @parcel/watcher in the packaged runtime closure', () => {
|
|
// Why: the main process imports '@parcel/watcher' for filesystem change
|
|
// events; if it is absent from the packaged closure the serve host silently
|
|
// stops propagating file changes to clients (regression guard for #4851).
|
|
const packaged = createPackagedRuntimeNodeModuleResources()
|
|
const packagedTargets = packaged.map((resource) => resource.to)
|
|
expect(packagedTargets).toContain(join('node_modules', '@parcel', 'watcher'))
|
|
expect(
|
|
packagedTargets.some((target) =>
|
|
target.startsWith(join('node_modules', '@parcel', 'watcher-'))
|
|
)
|
|
).toBe(true)
|
|
})
|
|
|
|
it('prunes non-target @parcel/watcher platform subpackages from packaged runtime resources', async () => {
|
|
const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-parcel-watcher-prune-'))
|
|
try {
|
|
const parcelDir = join(resourcesDir, 'node_modules', '@parcel')
|
|
await mkdir(join(parcelDir, 'watcher'), { recursive: true })
|
|
await mkdir(join(parcelDir, 'watcher-darwin-arm64'), { recursive: true })
|
|
await mkdir(join(parcelDir, 'watcher-darwin-x64'), { recursive: true })
|
|
await mkdir(join(parcelDir, 'watcher-linux-x64-glibc'), { recursive: true })
|
|
await mkdir(join(parcelDir, 'watcher-linux-arm64-glibc'), { recursive: true })
|
|
await mkdir(join(parcelDir, 'watcher-win32-x64'), { recursive: true })
|
|
|
|
prunePackagedParcelWatcher(resourcesDir, 'linux')
|
|
|
|
await expect(readdir(parcelDir).then((entries) => entries.sort())).resolves.toEqual([
|
|
'watcher',
|
|
'watcher-linux-arm64-glibc',
|
|
'watcher-linux-x64-glibc'
|
|
])
|
|
} finally {
|
|
await rm(resourcesDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('leaves unrelated @parcel/* runtime deps untouched when pruning the watcher', async () => {
|
|
const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-parcel-watcher-prune-unrelated-'))
|
|
try {
|
|
const parcelDir = join(resourcesDir, 'node_modules', '@parcel')
|
|
await mkdir(join(parcelDir, 'watcher'), { recursive: true })
|
|
await mkdir(join(parcelDir, 'watcher-darwin-arm64'), { recursive: true })
|
|
await mkdir(join(parcelDir, 'watcher-linux-x64-glibc'), { recursive: true })
|
|
// A hypothetical future @parcel/* runtime dep that is NOT a watcher subpackage.
|
|
await mkdir(join(parcelDir, 'transformer-js'), { recursive: true })
|
|
|
|
prunePackagedParcelWatcher(resourcesDir, 'linux')
|
|
|
|
await expect(readdir(parcelDir).then((entries) => entries.sort())).resolves.toEqual([
|
|
'transformer-js',
|
|
'watcher',
|
|
'watcher-linux-x64-glibc'
|
|
])
|
|
} finally {
|
|
await rm(resourcesDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('prunes type declaration artifacts from packaged runtime node_modules', async () => {
|
|
const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-runtime-type-prune-'))
|
|
try {
|
|
const packageDir = join(resourcesDir, 'node_modules', 'example-package')
|
|
await mkdir(join(packageDir, 'dist'), { recursive: true })
|
|
await writeFile(join(packageDir, 'dist', 'index.cjs'), 'module.exports = {}', 'utf8')
|
|
await writeFile(join(packageDir, 'dist', 'index.d.ts'), 'export type Value = string', 'utf8')
|
|
await writeFile(join(packageDir, 'dist', 'index.d.cts'), 'export type Value = string', 'utf8')
|
|
await writeFile(join(packageDir, 'dist', 'index.d.mts.map'), '{}', 'utf8')
|
|
|
|
prunePackagedRuntimeTypeDeclarations(resourcesDir)
|
|
|
|
await expect(readdir(join(packageDir, 'dist'))).resolves.toEqual(['index.cjs'])
|
|
} finally {
|
|
await rm(resourcesDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('prunes duplicate darwin sherpa-onnx runtime dylib aliases', async () => {
|
|
const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-sherpa-prune-'))
|
|
try {
|
|
const packageDir = join(resourcesDir, 'node_modules', 'sherpa-onnx-darwin-arm64')
|
|
await mkdir(packageDir, { recursive: true })
|
|
await writeFile(join(packageDir, 'sherpa-onnx.node'), '', 'utf8')
|
|
await writeFile(join(packageDir, 'libonnxruntime.1.23.2.dylib'), '', 'utf8')
|
|
await writeFile(join(packageDir, 'libonnxruntime.dylib'), '', 'utf8')
|
|
|
|
prunePackagedSherpaOnnx(resourcesDir, 'darwin')
|
|
|
|
await expect(readdir(packageDir).then((entries) => entries.sort())).resolves.toEqual([
|
|
'libonnxruntime.1.23.2.dylib',
|
|
'sherpa-onnx.node'
|
|
])
|
|
} finally {
|
|
await rm(resourcesDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('prunes zod TypeScript sources from packaged runtime resources', async () => {
|
|
const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-zod-prune-'))
|
|
try {
|
|
const packageDir = join(resourcesDir, 'node_modules', 'zod')
|
|
await mkdir(join(packageDir, 'src'), { recursive: true })
|
|
await writeFile(join(packageDir, 'index.cjs'), 'module.exports = {}', 'utf8')
|
|
await writeFile(join(packageDir, 'src', 'index.ts'), 'export const value = true', 'utf8')
|
|
|
|
prunePackagedZodSources(resourcesDir)
|
|
|
|
await expect(readdir(packageDir)).resolves.toEqual(['index.cjs'])
|
|
} finally {
|
|
await rm(resourcesDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it.skipIf(process.platform === 'win32')(
|
|
'marks packaged Unix CLI launchers executable',
|
|
async () => {
|
|
const root = await mkdtemp(join(tmpdir(), 'orca-electron-builder-config-'))
|
|
try {
|
|
const resourcesDir = join(root, 'linux-unpacked', 'resources')
|
|
const launcherPath = join(resourcesDir, 'bin', 'orca-ide')
|
|
await mkdir(join(resourcesDir, 'bin'), { recursive: true })
|
|
await mkdir(join(resourcesDir, 'node_modules', 'zod', 'src'), { recursive: true })
|
|
// Why: afterPack now fails hard when the unpacked daemon entry is
|
|
// missing, so the fixture must carry one like a real package layout.
|
|
const unpackedMainDir = join(resourcesDir, 'app.asar.unpacked', 'out', 'main')
|
|
await mkdir(unpackedMainDir, { recursive: true })
|
|
await writeFile(
|
|
join(unpackedMainDir, 'daemon-entry.js'),
|
|
'console.error("Usage: daemon-entry <socket>"); process.exit(1)\n',
|
|
'utf8'
|
|
)
|
|
await writeFile(launcherPath, '#!/usr/bin/env bash\n', { encoding: 'utf8', mode: 0o644 })
|
|
|
|
await electronBuilderConfig.afterPack({
|
|
appOutDir: join(root, 'linux-unpacked'),
|
|
electronPlatformName: 'linux'
|
|
})
|
|
|
|
expect((await stat(launcherPath)).mode & 0o111).not.toBe(0)
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true })
|
|
}
|
|
}
|
|
)
|
|
})
|