mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
* fix(release): prune optional natives before the linux arch floor check
The linux-arm64 release build failed packaging, and retried three times:
[verify-linux-glibc-floor] 1 bundled native binary is built for the
wrong architecture (target arm64):
resources/node_modules/@parcel/watcher-linux-x64-glibc/watcher.node
is x64, expected arm64 (from its own path)
`afterPack` ran `verifyLinuxGlibcFloor` at its top, before
`prunePackagedRuntimeNodeModules`. A cross-build intentionally installs
every optional native variant, so at that point the arm64 slice still
carries the x64 `@parcel/watcher` package that the prune exists to drop.
The check was reading a file that was never going to ship.
Move the floor check below the prune so it inspects the binaries actually
packed. Same ordering as 35f1b0ebb9 on the v1.4.197 release branch, which
shipped green and was never merged back to main.
(cherry picked from commit 6400598212)
(cherry picked from commit a883e17116)
* fix(release): restore the minified telemetry constant fallback
The macOS, Windows, and both Linux release builds all failed on "Verify
telemetry constants present in app.asar", blocking publish-release:
::error::BUILD_IDENTITY constant missing or unexpected value in
dist/mac-arm64/Orca.app/Contents/Resources/app.asar
The verifier printed no context sample, because the string
`BUILD_IDENTITY` does not occur anywhere in the shipped bundle at all.
#17527 added `minify: 'oxc'` to the main bundle, so oxc renames the
module-local `const BUILD_IDENTITY` to a short identifier.
`BUILD_IDENTITY_RE` keys off the literal name and therefore cannot match a
production bundle. `MINIFIED_TELEMETRY_RE` matches the adjacent injected
identity/key pair instead, which survives renaming.
#11019 created these patterns without that fallback, and its own comment
predicted this exact failure if minification were ever enabled on the main
bundle. The fallback was written on the v1.4.197 release branch in
35f1b0ebb9 and never merged back to main, so main has never been able to
verify a minified bundle.
Verified against a real local bundle built with the release env vars
(ORCA_BUILD_IDENTITY=stable, ORCA_POSTHOG_WRITE_KEY=phc_...): the bundle
does not contain "BUILD_IDENTITY"; both the narrowed and the widened
BUILD_IDENTITY_RE fail; MINIFIED_TELEMETRY_RE matches and recovers
identity=stable plus the write key.
(cherry picked from commit 39c3e44cd1)
(cherry picked from commit d0bbe4475d)
* fix(skills): stop asserting readdir order in the skill root walk test
The Windows skill-sharing release gate — which blocks publish-release —
failed on this test, wedging the 1.4.198 cut. The implementation is
correct; the assertion was not.
`findSkillFiles` pushes results in `readdir` order and dedupes visited
directories by realpath, so of the 32 junctions pointing at one target
exactly one survives. The assertion hardcoded both the array order and
which link won. `readdir` order is filesystem-dependent: APFS and ext4
return SKILL.md first, while NTFS enumerates its filename index
alphabetically on the uppercased name, where "LINK00" sorts before
"SKILL.MD" (L=0x4C < S=0x53). Windows therefore returned the same two
paths in the opposite order.
Assert the contract instead: the real file is present, exactly one
link-routed path survives dedup, and nothing else does.
(cherry picked from commit 9a832e9bda)
(cherry picked from commit 505b85cd0d)
* test(release): pin ported release gate fixes
* style(skills): apply pinned test formatting
---------
Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: Merge Sim <sim@local>
605 lines
26 KiB
JavaScript
605 lines
26 KiB
JavaScript
import { readFileSync, readdirSync } from 'node:fs'
|
|
import { cp, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises'
|
|
import { createRequire } from 'node:module'
|
|
import { tmpdir } from 'node:os'
|
|
import { delimiter, dirname, join, relative, resolve } from 'node:path'
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
const require = createRequire(import.meta.url)
|
|
const projectRoot = resolve(import.meta.dirname, '..', '..')
|
|
const electronBuilderConfig = require('../electron-builder.config.cjs')
|
|
const {
|
|
createPackagedRuntimeNodeModuleResources,
|
|
findAsarEntry,
|
|
isPackagedExternalSpecifier,
|
|
packageNameFromSpecifier,
|
|
prunePackagedNodePty,
|
|
prunePackagedParcelWatcher,
|
|
prunePackagedSherpaOnnx,
|
|
prunePackagedRuntimeTypeAndSourceMapArtifacts,
|
|
prunePackagedZodSources,
|
|
verifyPackagedMainRuntimeDeps
|
|
} = require('../packaged-runtime-node-modules.cjs')
|
|
|
|
describe('packaged runtime resources', () => {
|
|
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('verifies literal dynamic imports from the packaged main bundle', async () => {
|
|
const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-runtime-dynamic-imports-'))
|
|
try {
|
|
await writeFile(join(resourcesDir, 'app.asar'), '', 'utf8')
|
|
|
|
// The first is the exact shape oxc emits for the memoized SDK import in a
|
|
// shipped build; the second is the spaced variant the pattern also accepts.
|
|
const sources = new Map([
|
|
[
|
|
'out/main/index.js',
|
|
'let p=null;function q(){return p??=import(`@anthropic-ai/claude-agent-sdk`),p}'
|
|
],
|
|
[
|
|
'out/main/agent-hooks/managed-agent-hook-controls.js',
|
|
'import (`@anthropic-ai/claude-agent-sdk`)'
|
|
]
|
|
])
|
|
const asar = {
|
|
listPackage: () => [...sources.keys()].map((entry) => `/${entry}`),
|
|
extractFile: (_asarPath, internalPath) => Buffer.from(sources.get(internalPath), 'utf8')
|
|
}
|
|
|
|
expect(() => verifyPackagedMainRuntimeDeps(resourcesDir, asar)).toThrow(
|
|
/@anthropic-ai\/claude-agent-sdk/
|
|
)
|
|
|
|
await mkdir(join(resourcesDir, 'node_modules', '@anthropic-ai', 'claude-agent-sdk'), {
|
|
recursive: true
|
|
})
|
|
expect(() => verifyPackagedMainRuntimeDeps(resourcesDir, asar)).not.toThrow()
|
|
} finally {
|
|
await rm(resourcesDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('still fails when a required packaged main entry is missing entirely', async () => {
|
|
const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-runtime-missing-entry-'))
|
|
try {
|
|
await writeFile(join(resourcesDir, 'app.asar'), '', 'utf8')
|
|
|
|
const asar = {
|
|
listPackage: () => ['/out/main/index.js'],
|
|
extractFile: () => Buffer.from('', 'utf8')
|
|
}
|
|
|
|
expect(() => verifyPackagedMainRuntimeDeps(resourcesDir, asar)).toThrow(
|
|
/managed-agent-hook-controls\.js was not found/
|
|
)
|
|
} finally {
|
|
await rm(resourcesDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('verifies bare imports that rolldown hoisted into a shared main chunk', async () => {
|
|
const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-runtime-chunk-imports-'))
|
|
try {
|
|
await writeFile(join(resourcesDir, 'app.asar'), '', 'utf8')
|
|
|
|
// The entry points themselves carry no specifier; only the shared chunk does.
|
|
const sources = new Map([
|
|
['out/main/index.js', ''],
|
|
['out/main/agent-hooks/managed-agent-hook-controls.js', ''],
|
|
['out/main/chunks/managed-agent-hook-controls-CWf8D-KR.js', 'require(`jsonc-parser`)']
|
|
])
|
|
// Real listPackage emits directory nodes too, and extractFile throws on them,
|
|
// so the `.js` anchor is load-bearing -- keep the mock able to catch that.
|
|
const directories = ['/out', '/out/main', '/out/main/chunks']
|
|
const asar = {
|
|
listPackage: () => [...directories, ...[...sources.keys()].map((entry) => `/${entry}`)],
|
|
extractFile: (_asarPath, internalPath) => {
|
|
const source = sources.get(internalPath)
|
|
if (source === undefined) {
|
|
throw new Error(`Expected to find file at: ${internalPath} but found a directory`)
|
|
}
|
|
return Buffer.from(source, 'utf8')
|
|
}
|
|
}
|
|
|
|
expect(() => verifyPackagedMainRuntimeDeps(resourcesDir, asar)).toThrow(/jsonc-parser/)
|
|
|
|
await mkdir(join(resourcesDir, 'node_modules', 'jsonc-parser'), { recursive: true })
|
|
expect(() => verifyPackagedMainRuntimeDeps(resourcesDir, asar)).not.toThrow()
|
|
} finally {
|
|
await rm(resourcesDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('reads a spread require, whose leading dots are not member access', async () => {
|
|
const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-runtime-spread-require-'))
|
|
try {
|
|
await writeFile(join(resourcesDir, 'app.asar'), '', 'utf8')
|
|
|
|
const sources = new Map([
|
|
['out/main/index.js', 'const all=[...require("jsonc-parser")]'],
|
|
['out/main/agent-hooks/managed-agent-hook-controls.js', '']
|
|
])
|
|
const asar = {
|
|
listPackage: () => [...sources.keys()].map((entry) => `/${entry}`),
|
|
extractFile: (_asarPath, internalPath) => Buffer.from(sources.get(internalPath), 'utf8')
|
|
}
|
|
|
|
expect(() => verifyPackagedMainRuntimeDeps(resourcesDir, asar)).toThrow(/jsonc-parser/)
|
|
} finally {
|
|
await rm(resourcesDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('ignores member calls onto Orca methods that are themselves named require', async () => {
|
|
const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-runtime-member-require-'))
|
|
try {
|
|
await writeFile(join(resourcesDir, 'app.asar'), '', 'utf8')
|
|
|
|
// electron-sidecar-tab-registry and browser-execution-host-grant-registry both
|
|
// expose require(key); a literal key must never read as a packaged specifier.
|
|
const sources = new Map([
|
|
['out/main/index.js', 'registry.require("public-a");grants.require(`host-key`)'],
|
|
['out/main/agent-hooks/managed-agent-hook-controls.js', 'state.import("android-sdk")']
|
|
])
|
|
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 architecture outputs from packaged runtime resources', async () => {
|
|
const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-node-pty-prune-'))
|
|
try {
|
|
const nodePtyDir = join(resourcesDir, 'node_modules', 'node-pty')
|
|
const prebuildsDir = join(nodePtyDir, 'prebuilds')
|
|
const binDir = join(nodePtyDir, 'bin')
|
|
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(binDir, 'darwin-arm64-148'), { recursive: true })
|
|
await mkdir(join(binDir, 'darwin-x64-148'), { recursive: true })
|
|
await mkdir(join(nodePtyDir, 'third_party', 'conpty'), {
|
|
recursive: true
|
|
})
|
|
await mkdir(join(nodePtyDir, 'deps', 'winpty'), { recursive: true })
|
|
|
|
prunePackagedNodePty(resourcesDir, 'darwin', 3)
|
|
|
|
await expect(readdir(prebuildsDir)).resolves.toEqual(['darwin-arm64'])
|
|
await expect(readdir(binDir)).resolves.toEqual(['darwin-arm64-148'])
|
|
await expect(readdir(join(nodePtyDir, 'third_party'))).resolves.toEqual([])
|
|
await expect(readdir(join(nodePtyDir, 'deps'))).resolves.toEqual([])
|
|
expect(() => prunePackagedNodePty(resourcesDir, 'darwin', 4)).toThrow(
|
|
'Unsupported packaged runtime architecture: 4'
|
|
)
|
|
} finally {
|
|
await rm(resourcesDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('copies the Windows node-pty ConPTY runtime beside the rebuilt addon', async () => {
|
|
for (const [arch, electronArch] of [
|
|
['x64', 1],
|
|
['arm64', 3]
|
|
]) {
|
|
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', electronArch)
|
|
|
|
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 external main dependencies 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)
|
|
expect(packagedTargets).toContain(join('node_modules', 'proper-lockfile'))
|
|
})
|
|
|
|
it('includes the Claude agent SDK in every desktop package plan', () => {
|
|
for (const platform of ['darwin', 'linux', 'win32']) {
|
|
const packagedTargets = createPackagedRuntimeNodeModuleResources(platform).map(
|
|
(resource) => resource.to
|
|
)
|
|
expect(packagedTargets).toContain(join('node_modules', '@anthropic-ai', 'claude-agent-sdk'))
|
|
}
|
|
})
|
|
|
|
it('prunes non-target @parcel/watcher architecture subpackages', 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', 'arm64')
|
|
|
|
await expect(readdir(parcelDir).then((entries) => entries.sort())).resolves.toEqual([
|
|
'watcher',
|
|
'watcher-linux-arm64-glibc'
|
|
])
|
|
expect(() => prunePackagedParcelWatcher(resourcesDir, 'linux', 'universal')).toThrow(
|
|
'Unsupported packaged runtime architecture: universal'
|
|
)
|
|
} 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', 1)
|
|
|
|
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')
|
|
|
|
prunePackagedRuntimeTypeAndSourceMapArtifacts(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('fails when the packaged resources directory is missing', async () => {
|
|
const root = await mkdtemp(join(tmpdir(), 'orca-electron-builder-config-'))
|
|
try {
|
|
await expect(
|
|
electronBuilderConfig.afterPack({
|
|
appOutDir: root,
|
|
electronPlatformName: 'win32'
|
|
})
|
|
).rejects.toThrow(/Missing packaged resources directory/)
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it.skipIf(process.platform === 'win32')(
|
|
'prunes non-target native packages before the Linux glibc gate',
|
|
async () => {
|
|
const root = await mkdtemp(join(tmpdir(), 'orca-after-pack-prune-order-'))
|
|
const previousPath = process.env.PATH
|
|
try {
|
|
const appOutDir = join(root, 'linux-unpacked')
|
|
const resourcesDir = join(appOutDir, 'resources')
|
|
await cp(
|
|
join(process.cwd(), 'resources', 'plugins', 'launch'),
|
|
join(resourcesDir, 'plugins', 'launch'),
|
|
{ recursive: true }
|
|
)
|
|
|
|
const unpackedMainDir = join(resourcesDir, 'app.asar.unpacked', 'out', 'main')
|
|
await mkdir(unpackedMainDir, { recursive: true })
|
|
await writeFile(join(unpackedMainDir, 'daemon-entry.js'), '', 'utf8')
|
|
await writeFile(
|
|
join(resourcesDir, 'app.asar.unpacked', 'out', 'package.json'),
|
|
`${JSON.stringify({ name: 'orca-compiled-output', type: 'commonjs', private: true })}\n`,
|
|
'utf8'
|
|
)
|
|
|
|
const unpackedCliDir = join(resourcesDir, 'app.asar.unpacked', 'out', 'cli')
|
|
await mkdir(join(unpackedCliDir, 'handlers'), { recursive: true })
|
|
await writeFile(join(unpackedCliDir, 'handlers', 'skills.js'), '', 'utf8')
|
|
await writeFile(join(unpackedCliDir, 'index.js'), '', 'utf8')
|
|
|
|
const target =
|
|
process.arch === 'x64'
|
|
? { electronArch: 3, machine: 0xb7, nonTarget: 'x64' }
|
|
: { electronArch: 1, machine: 0x3e, nonTarget: 'arm64' }
|
|
const wrongArchPackage = join(
|
|
resourcesDir,
|
|
'node_modules',
|
|
'@parcel',
|
|
`watcher-linux-${target.nonTarget}-glibc`
|
|
)
|
|
await mkdir(wrongArchPackage, { recursive: true })
|
|
const wrongArchElf = Buffer.alloc(20)
|
|
wrongArchElf.set([0x7f, 0x45, 0x4c, 0x46])
|
|
wrongArchElf[5] = 1
|
|
wrongArchElf.writeUInt16LE(target.machine, 18)
|
|
await writeFile(join(wrongArchPackage, 'watcher.node'), wrongArchElf)
|
|
|
|
const stubBinDir = join(root, 'bin')
|
|
await mkdir(stubBinDir)
|
|
await writeFile(join(stubBinDir, 'objdump'), '#!/bin/sh\nexit 0\n', { mode: 0o755 })
|
|
process.env.PATH = `${stubBinDir}${delimiter}${previousPath ?? ''}`
|
|
|
|
await expect(
|
|
electronBuilderConfig.afterPack({
|
|
appOutDir,
|
|
electronPlatformName: 'linux',
|
|
arch: target.electronArch,
|
|
packager: { appInfo: { version: '9.9.9' } }
|
|
})
|
|
).resolves.toBeUndefined()
|
|
await expect(stat(wrongArchPackage)).rejects.toMatchObject({ code: 'ENOENT' })
|
|
} finally {
|
|
process.env.PATH = previousPath
|
|
await rm(root, { 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 cp(
|
|
join(process.cwd(), 'resources', 'plugins', 'launch'),
|
|
join(resourcesDir, 'plugins', 'launch'),
|
|
{ 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(
|
|
join(resourcesDir, 'app.asar.unpacked', 'out', 'package.json'),
|
|
`${JSON.stringify({ name: 'orca-compiled-output', type: 'commonjs', private: true })}\n`,
|
|
'utf8'
|
|
)
|
|
const unpackedCliDir = join(resourcesDir, 'app.asar.unpacked', 'out', 'cli')
|
|
await mkdir(join(unpackedCliDir, 'handlers'), { recursive: true })
|
|
await writeFile(join(unpackedCliDir, 'handlers', 'skills.js'), '', 'utf8')
|
|
await writeFile(
|
|
join(unpackedCliDir, 'index.js'),
|
|
[
|
|
'const args = process.argv.slice(2)',
|
|
"if (args[1] === 'list') console.log(JSON.stringify({ topics: [{ name: 'orca-cli' }, { name: 'computer-use' }] }))",
|
|
"else if (args[1] === 'get') console.log(`---\\nname: ${args[2]}\\n---`)",
|
|
'else console.log(JSON.stringify({ executed: false }))'
|
|
].join('\n'),
|
|
'utf8'
|
|
)
|
|
await writeFile(launcherPath, '#!/usr/bin/env bash\n', { encoding: 'utf8', mode: 0o644 })
|
|
|
|
await electronBuilderConfig.afterPack({
|
|
appOutDir: join(root, 'linux-unpacked'),
|
|
electronPlatformName: 'linux',
|
|
arch: 1,
|
|
packager: { appInfo: { version: '9.9.9' } }
|
|
})
|
|
|
|
expect((await stat(launcherPath)).mode & 0o111).not.toBe(0)
|
|
await expect(
|
|
readFile(join(resourcesDir, 'app.asar.unpacked', 'out', 'package.json'), 'utf8')
|
|
).resolves.toContain('"version": "9.9.9"')
|
|
await expect(readFile(join(resourcesDir, 'package-type'), 'utf8')).resolves.toBe('AppImage')
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true })
|
|
}
|
|
}
|
|
)
|
|
})
|
|
|
|
// Why source-anchored: the bundler renames a createRequire()'d require, so
|
|
// verifyPackagedMainRuntimeDeps' `require("x")` scan cannot see these specifiers — packaging
|
|
// stays green while the packaged app throws MODULE_NOT_FOUND the first time the path runs.
|
|
function collectLazyRequireSpecifiers(directory, found = new Map()) {
|
|
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
const entryPath = join(directory, entry.name)
|
|
if (entry.isDirectory()) {
|
|
collectLazyRequireSpecifiers(entryPath, found)
|
|
continue
|
|
}
|
|
if (!entry.isFile() || !entry.name.endsWith('.ts') || entry.name.includes('.test.')) {
|
|
continue
|
|
}
|
|
const source = readFileSync(entryPath, 'utf8')
|
|
if (!source.includes('createRequire(')) {
|
|
continue
|
|
}
|
|
for (const match of source.matchAll(/\brequire[A-Za-z0-9_]*\(\s*'([^']+)'\s*\)/g)) {
|
|
if (isPackagedExternalSpecifier(match[1])) {
|
|
found.set(match[1], relative(projectRoot, entryPath).replaceAll('\\', '/'))
|
|
}
|
|
}
|
|
}
|
|
return found
|
|
}
|
|
|
|
function packagedResourceDestinations(platform) {
|
|
return new Set(
|
|
(electronBuilderConfig[platform].extraResources ?? []).map((resource) =>
|
|
String(resource.to).replaceAll('\\', '/')
|
|
)
|
|
)
|
|
}
|
|
|
|
describe('lazily required packages reach Resources/node_modules', () => {
|
|
it('copies every createRequire specifier main uses into the packaged resource plan', () => {
|
|
const specifiers = collectLazyRequireSpecifiers(join(projectRoot, 'src', 'main'))
|
|
expect(specifiers.size).toBeGreaterThan(0)
|
|
|
|
const destinations = {
|
|
win: packagedResourceDestinations('win'),
|
|
mac: packagedResourceDestinations('mac'),
|
|
linux: packagedResourceDestinations('linux')
|
|
}
|
|
for (const [specifier, source] of specifiers) {
|
|
const packageName = packageNameFromSpecifier(specifier)
|
|
const covered = (platform) =>
|
|
destinations[platform].has(`node_modules/${packageName}`) ||
|
|
destinations[platform].has(`node_modules/${specifier}`)
|
|
// Windows carries the full closure, so an uncovered specifier is uncovered everywhere.
|
|
expect(
|
|
covered('win'),
|
|
`${source} lazily requires '${specifier}', but nothing copies it to Resources/node_modules`
|
|
).toBe(true)
|
|
if (covered('mac') && covered('linux')) {
|
|
continue
|
|
}
|
|
// Only the Windows-native loaders may be absent from the mac/linux plans.
|
|
expect(source, `'${specifier}' is packaged for Windows only`).toContain('windows')
|
|
}
|
|
})
|
|
|
|
it('resolves the copied emoji dataset the way the packaged main bundle does', async () => {
|
|
const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-lazy-require-'))
|
|
try {
|
|
const datasetPath = 'node_modules/emojibase-data/en/shortcodes/emojibase.json'
|
|
const entry = electronBuilderConfig.mac.extraResources.find(
|
|
(resource) => String(resource.to) === datasetPath
|
|
)
|
|
expect(entry).toBeDefined()
|
|
const destination = join(resourcesDir, ...datasetPath.split('/'))
|
|
await mkdir(dirname(destination), { recursive: true })
|
|
await cp(join(projectRoot, ...String(entry.from).split('/')), destination)
|
|
|
|
// app.asar's parent is Resources, so main's bare require walks into Resources/node_modules.
|
|
const packagedMainDir = join(resourcesDir, 'app.asar', 'out', 'main')
|
|
await mkdir(packagedMainDir, { recursive: true })
|
|
const probe = join(packagedMainDir, 'probe.cjs')
|
|
await writeFile(probe, 'module.exports = require', 'utf8')
|
|
|
|
const dataset = require(probe)('emojibase-data/en/shortcodes/emojibase.json')
|
|
expect(Object.keys(dataset).length).toBeGreaterThan(1000)
|
|
} finally {
|
|
await rm(resourcesDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
})
|