orca/config/scripts/package-electron-runtime-co...

381 lines
17 KiB
JavaScript

import { readFileSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { parse } from 'yaml'
const projectDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
const packageJson = JSON.parse(readFileSync(join(projectDir, 'package.json'), 'utf8'))
describe('Electron runtime package contract', () => {
it('keeps root postinstall as the single Electron binary install owner', () => {
expect(packageJson.scripts.postinstall).toBe('node config/scripts/rebuild-native-deps.mjs')
expect(packageJson.pnpm.onlyBuiltDependencies).not.toContain('electron')
})
it('guards package scripts that launch Electron tooling', () => {
const scripts = packageJson.scripts
const guardedScripts = [
'start',
'dev',
'dev-stable-name',
'build:unpack',
'build:win',
'build:mac',
'build:mac:release',
'build:linux',
'test:e2e',
'test:e2e:terminal-rendering-golden',
'test:e2e:terminal-rendering-release-evidence',
'test:e2e:headful'
]
for (const scriptName of guardedScripts) {
expect(scripts[scriptName], scriptName).toContain('pnpm run ensure:electron-runtime &&')
}
})
it('keeps Windows and Linux package builds off the macOS native helper build', () => {
const scripts = packageJson.scripts
expect(scripts['build:desktop']).not.toContain('build:computer-macos')
expect(scripts['build:win']).toContain('pnpm run build:desktop')
expect(scripts['build:win']).not.toContain('pnpm run build ')
expect(scripts['build:win']).not.toContain('build:computer-macos')
expect(scripts['build:linux']).toContain('pnpm run build:desktop')
expect(scripts['build:linux']).not.toContain('pnpm run build ')
expect(scripts['build:linux']).not.toContain('build:computer-macos')
expect(scripts['build:mac']).toContain('pnpm run build:computer-macos')
expect(scripts['build:release']).toContain('pnpm run build:native')
expect(scripts['build:release']).not.toContain('build:computer-macos')
})
it('runs the web build through the heap-sized Vite wrapper', () => {
expect(packageJson.scripts['build:web']).toContain('node config/scripts/run-vite-web-build.mjs')
expect(packageJson.scripts['build:web']).toContain('node config/scripts/verify-web-build.mjs')
})
it('guards release publishing before electron-builder runs', () => {
const releaseWorkflow = readFileSync(
join(projectDir, '.github/workflows/release-cut.yml'),
'utf8'
)
const parsedWorkflow = parse(releaseWorkflow)
const releaseCommands = new Map(
parsedWorkflow.jobs.build.strategy.matrix.include.map(({ platform, release_command }) => [
platform,
release_command
])
)
expect([...releaseCommands.keys()].sort()).toEqual(['linux-arm64', 'linux-x64', 'mac', 'win'])
for (const command of releaseCommands.values()) {
expect(command).toContain('node config/scripts/ensure-native-runtime.mjs --runtime=electron')
expect(command).toContain('electron-builder')
expect(command.indexOf('ensure-native-runtime')).toBeLessThan(
command.indexOf('electron-builder')
)
}
expect(releaseCommands.get('mac')).toContain(' && ORCA_MAC_RELEASE=1 ')
expect(releaseCommands.get('linux-x64')).toContain(' && pnpm exec electron-builder ')
expect(releaseCommands.get('linux-x64')).toContain('--linux AppImage deb rpm --x64')
expect(releaseCommands.get('linux-arm64')).toContain('ORCA_LINUX_ARM64_RELEASE=1')
expect(releaseCommands.get('linux-arm64')).toContain('--linux AppImage deb rpm --arm64')
expect(releaseCommands.get('win')).toContain(
'; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }; pnpm exec electron-builder '
)
})
it('preflights SignPath module install before Windows signing side effects', () => {
const releaseWorkflow = readFileSync(
join(projectDir, '.github/workflows/release-cut.yml'),
'utf8'
)
const parsedWorkflow = parse(releaseWorkflow)
const steps = parsedWorkflow.jobs.build.steps
const stepNames = steps.map((step) => step.name)
const installStepIndexes = stepNames.flatMap((name, index) =>
name === 'Install SignPath PowerShell module' ? [index] : []
)
const buildIndex = stepNames.indexOf('Build Windows release artifacts')
const uploadIndex = stepNames.indexOf('Upload unsigned Windows installer for SignPath')
const downloadIndex = stepNames.indexOf('Download signed Windows installer from SignPath')
expect(installStepIndexes).toEqual([buildIndex + 1])
expect(installStepIndexes[0]).toBeLessThan(uploadIndex)
const uploadThroughDownloadScript = steps
.slice(uploadIndex, downloadIndex + 1)
.map((step) => step.run ?? '')
.join('\n')
expect(uploadThroughDownloadScript).not.toContain('Install-Module -Name SignPath')
const installStep = steps[installStepIndexes[0]]
const installRun = installStep.run
const sleepSeconds = [...installRun.matchAll(/Start-Sleep -Seconds (\d+)/g)].map(
([, seconds]) => seconds
)
expect(installStep.if).toBe("matrix.platform == 'win'")
expect(installStep.shell).toBe('pwsh')
expect(installRun).toContain('Set-PSRepository -Name PSGallery -InstallationPolicy Trusted')
expect(installRun).toMatch(/\$env:PSModulePath -split \[System\.IO\.Path\]::PathSeparator/)
expect(installRun).toContain(
"$signPathModulePath = Join-Path -Path $currentUserModuleRoot -ChildPath 'SignPath'"
)
expect(installRun).toMatch(/for \(\$attempt = 1; \$attempt -le 3; \$attempt\+\+\)/)
expect(sleepSeconds).toEqual(['15', '30'])
expect(installRun).toContain(
'Install-Module -Name SignPath -Repository PSGallery -MinimumVersion 4.0.0 -MaximumVersion 4.999.999 -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop'
)
expect(installRun).toContain('Import-Module SignPath')
expect(installRun).toContain(
'Get-Command -Name Get-SignedArtifact -Module SignPath -ErrorAction Stop'
)
expect(installRun).toContain('Remove-Item -LiteralPath $signPathModulePath -Recurse -Force')
expect(installRun).not.toContain('SignPath*')
expect(installRun.indexOf('if ($attempt -eq 3)')).toBeLessThan(
installRun.indexOf('Remove-Item -LiteralPath $signPathModulePath')
)
expect(installRun).toMatch(/if \(\$attempt -eq 3\) {\s+throw\s+}/)
expect(installRun).not.toMatch(/throw\s+\$_/)
})
it('publishes both Linux release matrix entries', () => {
const releaseWorkflow = readFileSync(
join(projectDir, '.github/workflows/release-cut.yml'),
'utf8'
)
const parsedWorkflow = parse(releaseWorkflow)
const publishLinuxStep = parsedWorkflow.jobs.build.steps.find(
(step) => step.name === 'Publish release artifacts (Linux)'
)
expect(publishLinuxStep.if).toContain("matrix.platform == 'linux-x64'")
expect(publishLinuxStep.if).toContain("matrix.platform == 'linux-arm64'")
expect(publishLinuxStep.with.command).toBe('${{ matrix.release_command }}')
})
it('keeps Linux postinstall repairing Chromium sandbox permissions', () => {
const afterInstallScript = readFileSync(
join(projectDir, 'resources/linux/packaging/after-install.sh'),
'utf8'
)
expect(afterInstallScript).toContain('chrome-sandbox')
expect(afterInstallScript).toContain('chmod 4755 "$sandbox"')
expect(afterInstallScript).not.toContain('chmod 0755 "$sandbox"')
})
it('lets release-cut tag a version that is already present on main', () => {
const releaseWorkflow = readFileSync(
join(projectDir, '.github/workflows/release-cut.yml'),
'utf8'
)
const parsedWorkflow = parse(releaseWorkflow)
const bumpStep = parsedWorkflow.jobs.cut.steps.find(
(step) => step.name === 'Bump package.json and tag'
)
expect(bumpStep.run).toContain('git diff --cached --quiet')
expect(bumpStep.run).toContain('git commit --allow-empty -m "$commit_message"')
})
it('keeps release-cut RC retries monotonic across stale attempts', () => {
const releaseWorkflow = readFileSync(
join(projectDir, '.github/workflows/release-cut.yml'),
'utf8'
)
const parsedWorkflow = parse(releaseWorkflow)
const versionStep = parsedWorkflow.jobs.cut.steps.find(
(step) => step.name === 'Compute next version'
)
expect(versionStep.run).toContain('node config/scripts/release-rc-history.mjs "$1"')
expect(versionStep.run).toContain('tag_matches_current_ref')
expect(versionStep.run).toContain('cutting the next version instead of reusing stale artifacts')
expect(versionStep.run).toContain('git rev-parse "$existing_rc_tag"')
})
it('bumps separate Homebrew casks for stable and RC desktop tags', () => {
const releaseWorkflow = parse(
readFileSync(join(projectDir, '.github/workflows/release-cut.yml'), 'utf8')
)
const homebrewWorkflow = parse(
readFileSync(join(projectDir, '.github/workflows/homebrew-bump.yml'), 'utf8')
)
expect(releaseWorkflow.jobs['homebrew-bump'].if).toContain(
"startsWith(needs.cut.outputs.tag, 'v')"
)
expect(releaseWorkflow.jobs['homebrew-bump'].if).not.toContain('-rc.')
expect(releaseWorkflow.jobs['homebrew-bump-published-rc-draft'].with.tag).toBe(
'${{ needs.cut.outputs.latest_published_rc_tag }}'
)
const resolveCaskStep = homebrewWorkflow.jobs['bump-cask'].steps.find(
(step) => step.name === 'Resolve cask target'
)
const renderStep = homebrewWorkflow.jobs['bump-cask'].steps.find(
(step) => step.name === 'Render updated cask file'
)
const copyStep = homebrewWorkflow.jobs['bump-cask'].steps.find(
(step) => step.name === 'Copy cask into tap and open PR'
)
expect(resolveCaskStep.run).toContain('token="orca@rc"')
expect(resolveCaskStep.run).toContain('token="orca"')
expect(renderStep.env.CASK_PATH).toBe('${{ steps.cask.outputs.path }}')
expect(copyStep.run).toContain('cp "$CASK_PATH" "tap/$CASK_PATH"')
expect(copyStep.run).toContain('git add "$CASK_PATH"')
})
it('installs the Electron package binary in PR checks without changing native module ABI', () => {
const prWorkflow = readFileSync(join(projectDir, '.github/workflows/pr.yml'), 'utf8')
const parsedWorkflow = parse(prWorkflow)
const installStep = parsedWorkflow.jobs.verify.steps.find(
(step) => step.name === 'Install Electron package binary for tests'
)
expect(installStep.run).toBe('node config/scripts/install-electron-package-binary.mjs')
})
it('smokes the packaged CLI from outside the checkout in PR checks', () => {
const prWorkflow = readFileSync(join(projectDir, '.github/workflows/pr.yml'), 'utf8')
const parsedWorkflow = parse(prWorkflow)
const smokeStep = parsedWorkflow.jobs.verify.steps.find(
(step) => step.name === 'Smoke packaged CLI'
)
expect(smokeStep.run).toBe(
'node config/scripts/smoke-packaged-cli.mjs --app-dir=dist/linux-unpacked'
)
})
it('keeps terminal scale perf wired to the report budget gate', () => {
const packageScripts = packageJson.scripts
const terminalPerfWorkflow = parse(
readFileSync(join(projectDir, '.github/workflows/terminal-perf.yml'), 'utf8')
)
const steps = terminalPerfWorkflow.jobs['terminal-perf'].steps
const runStep = steps.find((step) => step.name === 'Run terminal scale perf report gate')
const uploadStep = steps.find((step) => step.name === 'Upload terminal perf report')
expect(packageScripts['test:e2e:terminal-perf:scale:report']).toContain(
'run-terminal-scale-perf-report-gate.mjs'
)
expect(runStep.run).toContain('pnpm run test:e2e:terminal-perf:scale:report')
expect(runStep.run).toContain('xvfb-run --auto-servernum')
const manualProfileKnobs = [
['ORCA_TERMINAL_PERF_FRAME_COUNT', 'frame_count', 'ORCA_E2E_OPENCODE_FRAME_COUNT'],
[
'ORCA_TERMINAL_PERF_FRAME_INTERVAL_MS',
'frame_interval_ms',
'ORCA_E2E_OPENCODE_FRAME_INTERVAL_MS'
],
[
'ORCA_TERMINAL_PERF_PRESSURE_OUTPUT_CHARS',
'pressure_output_chars',
'ORCA_E2E_OPENCODE_PRESSURE_OUTPUT_CHARS'
],
['ORCA_TERMINAL_PERF_SCALE_PANES', 'scale_panes', 'ORCA_E2E_OPENCODE_SCALE_PANES'],
[
'ORCA_TERMINAL_PERF_SCALE_CROSS_WORKSPACE_PANES',
'scale_cross_workspace_panes',
'ORCA_E2E_OPENCODE_SCALE_CROSS_WORKSPACE_PANES'
],
[
'ORCA_TERMINAL_PERF_SCALE_PRESSURE_PANES',
'scale_pressure_panes',
'ORCA_E2E_OPENCODE_SCALE_PRESSURE_PANES'
],
[
'ORCA_TERMINAL_PERF_SCALE_HIDDEN_PRESSURE_PANES',
'scale_hidden_pressure_panes',
'ORCA_E2E_OPENCODE_SCALE_HIDDEN_PRESSURE_PANES'
]
]
for (const [workflowEnv, inputName, runnerEnv] of manualProfileKnobs) {
expect(runStep.env[workflowEnv]).toBe(`\${{ inputs.${inputName} }}`)
expect(runStep.run).toContain(runnerEnv)
}
expect(uploadStep.uses).toBe('actions/upload-artifact@v7')
expect(uploadStep.with.path).toBe('${{ env.ORCA_E2E_TERMINAL_PERF_REPORT_PATH }}')
})
it('keeps terminal rendering regressions in the fast golden E2E gate', () => {
const packageScripts = packageJson.scripts
const goldenWorkflow = parse(
readFileSync(join(projectDir, '.github/workflows/golden-e2e-experiment.yml'), 'utf8')
)
const releaseWorkflow = parse(
readFileSync(join(projectDir, '.github/workflows/release-cut.yml'), 'utf8')
)
const steps = goldenWorkflow.jobs['golden-e2e'].steps
const goldenPlatformLabels = new Map([
['linux', 'Linux'],
['mac', 'macOS'],
['windows', 'Windows']
])
const goldenPlatforms = goldenWorkflow.jobs['golden-e2e'].strategy.matrix.include
.map(({ platform }) => platform)
.sort()
const goldenRunSteps = goldenPlatforms.map((platform) => {
const label = goldenPlatformLabels.get(platform)
expect(label, platform).toBeDefined()
return steps.find((step) => step.name === `Run golden E2E tests on ${label}`)
})
const pullRequestPaths = goldenWorkflow.on.pull_request.paths
const releaseGoldenJob = releaseWorkflow.jobs['terminal-rendering-golden']
const releaseEvidenceJob = releaseWorkflow.jobs['terminal-rendering-release-evidence']
const releaseBuildNeeds = releaseWorkflow.jobs.build.needs
const publishReleaseNeeds = releaseWorkflow.jobs['publish-release'].needs
// Why: Windows release evidence is temporarily paused for CI runner PTY readiness.
const releaseEvidencePlatforms = ['linux', 'mac']
expect(packageScripts['test:e2e:terminal-rendering-golden']).toContain(
'@terminal-rendering-golden'
)
expect(packageScripts['test:e2e:terminal-rendering-golden']).toContain(
'terminal-raw-emoji-table-scroll-restore.spec.ts'
)
expect(packageScripts['test:e2e:terminal-rendering-golden']).not.toContain(
'terminal-long-table-scroll-restore.spec.ts'
)
expect(packageScripts['test:e2e:terminal-rendering-release-evidence']).toContain(
'terminal-opencode-emoji-table-rendering.spec.ts'
)
expect(packageScripts['test:e2e:terminal-rendering-release-evidence']).toContain(
'terminal-long-table-scroll-restore.spec.ts'
)
for (const runStep of goldenRunSteps) {
expect(runStep?.run).toContain('pnpm run test:e2e:terminal-rendering-golden')
}
expect(pullRequestPaths).toContain('tests/e2e/terminal-raw-emoji-table-scroll-restore.spec.ts')
expect(pullRequestPaths).toContain('tests/e2e/fixtures/terminal-emoji-table.md')
expect(pullRequestPaths).toContain('src/renderer/src/lib/pane-manager/**')
expect(releaseBuildNeeds).not.toContain('terminal-rendering-golden')
expect(releaseBuildNeeds).not.toContain('terminal-rendering-release-evidence')
expect(publishReleaseNeeds).toContain('terminal-rendering-golden')
expect(publishReleaseNeeds).toContain('build')
expect(publishReleaseNeeds).not.toContain('terminal-rendering-release-evidence')
expect(releaseGoldenJob['continue-on-error']).toBeUndefined()
expect(releaseGoldenJob.strategy.matrix.include.map(({ platform }) => platform).sort()).toEqual(
goldenPlatforms
)
expect(releaseGoldenJob.steps.map((step) => step.run ?? '')).toContain(
'xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:terminal-rendering-golden'
)
expect(releaseEvidenceJob['continue-on-error']).toBe(true)
expect(
releaseEvidenceJob.strategy.matrix.include.map(({ platform }) => platform).sort()
).toEqual(releaseEvidencePlatforms)
expect(releaseEvidenceJob.steps.map((step) => step.run ?? '')).toContain(
'xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:terminal-rendering-release-evidence'
)
})
})