mirror of
https://github.com/stablyai/orca.git
synced 2026-10-27 00:02:48 +00:00
Merge origin/main into mobile i18n audit
This commit is contained in:
@@ -1,13 +1,72 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import * as nodeFs from 'node:fs'
|
||||
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
||||
import * as nodePath from 'node:path'
|
||||
import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { EventEmitter } from 'node:events'
|
||||
import { runInNewContext } from 'node:vm'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createBootstrapFatalExitBanner, electronViteConfig } from '../../electron.vite.config'
|
||||
import {
|
||||
BOOTSTRAP_FATAL_LOG_ENV_VAR,
|
||||
BOOTSTRAP_FATAL_LOG_FILE_NAME,
|
||||
createBootstrapFatalExitBanner
|
||||
} from '../../build-plugins/bootstrap-fatal-exit-banner'
|
||||
import { electronViteConfig } from '../../electron.vite.config'
|
||||
import { BOOTSTRAP_FATAL_EXIT_GUARD_KEY } from '../../src/main/startup/bootstrap-fatal-exit-guard'
|
||||
|
||||
const targetConfig = readFileSync('config/electron-vite-target.config.ts', 'utf8')
|
||||
const devRunner = readFileSync('config/scripts/run-electron-vite-dev.mjs', 'utf8')
|
||||
|
||||
type BootstrapProcessMock = EventEmitter & {
|
||||
env: Record<string, string>
|
||||
pid: number
|
||||
exit: (code: number) => void
|
||||
exitCode?: number
|
||||
}
|
||||
|
||||
/** Runs the banner in a bare context and raises the bootstrap fault it guards against. */
|
||||
function failBootstrapWithBanner(options: {
|
||||
env: Record<string, string>
|
||||
tmpdir?: string
|
||||
stderrWrites?: string[]
|
||||
}): BootstrapProcessMock {
|
||||
const processMock = new EventEmitter() as BootstrapProcessMock
|
||||
processMock.env = options.env
|
||||
processMock.pid = 4242
|
||||
processMock.exit = () => {}
|
||||
const fsShim = {
|
||||
...nodeFs,
|
||||
writeSync: (descriptor: number, data: string) => {
|
||||
if (descriptor === 2) {
|
||||
options.stderrWrites?.push(data)
|
||||
return data.length
|
||||
}
|
||||
return nodeFs.writeSync(descriptor, data)
|
||||
}
|
||||
}
|
||||
const context = {
|
||||
process: processMock,
|
||||
setImmediate: () => {},
|
||||
require: (specifier: string) => {
|
||||
if (specifier === 'node:fs') {
|
||||
return fsShim
|
||||
}
|
||||
if (specifier === 'node:path') {
|
||||
return nodePath
|
||||
}
|
||||
if (specifier === 'node:os' && options.tmpdir !== undefined) {
|
||||
return { tmpdir: () => options.tmpdir }
|
||||
}
|
||||
// Electron's own module is unreachable from a bootstrap fault this early.
|
||||
throw new Error(`unexpected require: ${specifier}`)
|
||||
}
|
||||
}
|
||||
|
||||
runInNewContext(createBootstrapFatalExitBanner(), context)
|
||||
processMock.emit('uncaughtException', new Error("Cannot find module 'ws'"))
|
||||
return processMock
|
||||
}
|
||||
|
||||
describe('Electron Vite output contract', () => {
|
||||
it('keeps main-process and plain-Node entries at stable CommonJS paths', () => {
|
||||
const output = electronViteConfig.main?.build?.rollupOptions?.output
|
||||
@@ -40,12 +99,20 @@ describe('Electron Vite output contract', () => {
|
||||
const processMock = new EventEmitter() as EventEmitter & {
|
||||
exit: (code: number) => void
|
||||
exitCode?: number
|
||||
stderr: { write: (chunk: string) => boolean }
|
||||
}
|
||||
let scheduledExit: (() => void) | null = null
|
||||
let exitedWith: number | null = null
|
||||
const stderrWrites: string[] = []
|
||||
processMock.exit = (code) => {
|
||||
exitedWith = code
|
||||
}
|
||||
processMock.stderr = {
|
||||
write: (chunk) => {
|
||||
stderrWrites.push(chunk)
|
||||
return true
|
||||
}
|
||||
}
|
||||
const context = {
|
||||
process: processMock,
|
||||
setImmediate: (callback: () => void) => {
|
||||
@@ -61,6 +128,63 @@ describe('Electron Vite output contract', () => {
|
||||
scheduledExit?.()
|
||||
expect(exitedWith).toBe(1)
|
||||
expect(context).toHaveProperty(BOOTSTRAP_FATAL_EXIT_GUARD_KEY)
|
||||
expect(stderrWrites.join('')).toContain("Cannot find module 'zod'")
|
||||
})
|
||||
|
||||
it('records the bootstrap failure it exits on, since the guard hides Electron dialog', () => {
|
||||
const logDirectory = mkdtempSync(join(tmpdir(), 'orca-bootstrap-fatal-'))
|
||||
const logPath = join(logDirectory, 'fatal.log')
|
||||
const stderrWrites: string[] = []
|
||||
|
||||
try {
|
||||
const processMock = failBootstrapWithBanner({
|
||||
env: { [BOOTSTRAP_FATAL_LOG_ENV_VAR]: logPath },
|
||||
stderrWrites
|
||||
})
|
||||
|
||||
expect(stderrWrites.join('')).toContain("Cannot find module 'ws'")
|
||||
const recorded = readFileSync(logPath, 'utf8')
|
||||
expect(recorded).toContain("Cannot find module 'ws'")
|
||||
expect(recorded).toContain('pid=4242')
|
||||
expect(processMock.exitCode).toBe(1)
|
||||
} finally {
|
||||
rmSync(logDirectory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('creates the parent directory an overridden log path names but does not have', () => {
|
||||
const logDirectory = mkdtempSync(join(tmpdir(), 'orca-bootstrap-fatal-'))
|
||||
const logPath = join(logDirectory, 'nested', 'diagnostics', 'fatal.log')
|
||||
|
||||
try {
|
||||
const processMock = failBootstrapWithBanner({
|
||||
env: { [BOOTSTRAP_FATAL_LOG_ENV_VAR]: logPath }
|
||||
})
|
||||
|
||||
expect(readFileSync(logPath, 'utf8')).toContain("Cannot find module 'ws'")
|
||||
expect(processMock.exitCode).toBe(1)
|
||||
} finally {
|
||||
rmSync(logDirectory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('falls back to the default location when the overridden log path is unwritable', () => {
|
||||
const logDirectory = mkdtempSync(join(tmpdir(), 'orca-bootstrap-fatal-'))
|
||||
const fallbackDirectory = join(logDirectory, 'fallback')
|
||||
|
||||
try {
|
||||
const processMock = failBootstrapWithBanner({
|
||||
// A directory can never be opened as the log file, so the override must yield.
|
||||
env: { [BOOTSTRAP_FATAL_LOG_ENV_VAR]: logDirectory },
|
||||
tmpdir: fallbackDirectory
|
||||
})
|
||||
|
||||
const recorded = readFileSync(join(fallbackDirectory, BOOTSTRAP_FATAL_LOG_FILE_NAME), 'utf8')
|
||||
expect(recorded).toContain("Cannot find module 'ws'")
|
||||
expect(processMock.exitCode).toBe(1)
|
||||
} finally {
|
||||
rmSync(logDirectory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('isolates renderer entry side effects behind strict facades', () => {
|
||||
|
||||
@@ -35,6 +35,7 @@ const appDir = resolve(readAppDirArg(process.argv.slice(2)))
|
||||
const tempRoot = await mkdtemp(join(tmpdir(), 'orca-packaged-cli-smoke-'))
|
||||
const copiedAppDir = join(tempRoot, basename(appDir))
|
||||
|
||||
let smokeFailure = null
|
||||
try {
|
||||
await cp(appDir, copiedAppDir, { recursive: true, verbatimSymlinks: true })
|
||||
const cliPath = getPackagedCliPath(copiedAppDir)
|
||||
@@ -73,6 +74,31 @@ try {
|
||||
assert.equal(install.executed, false)
|
||||
assert.equal(update.executed, false)
|
||||
console.log(`[packaged-cli-smoke] help and skills commands passed via ${cliPath}`)
|
||||
} finally {
|
||||
await rm(tempRoot, { recursive: true, force: true })
|
||||
} catch (error) {
|
||||
smokeFailure = error
|
||||
}
|
||||
|
||||
// Why: on Windows the launcher above spawns the copied Orca.exe (and its crashpad/utility children)
|
||||
// once per command; those handles can outlive execFile's exit by a few ms, so this cleanup hits
|
||||
// EBUSY on our own just-exited process after every assertion already passed. Same retry treatment
|
||||
// as removeHostTree(); a lock that never clears still throws — unless the smoke run itself failed,
|
||||
// in which case surfacing EBUSY instead of the real assertion would hide the actual regression.
|
||||
const cleanupFailure = await rm(tempRoot, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
maxRetries: 20,
|
||||
retryDelay: 250
|
||||
}).then(
|
||||
() => null,
|
||||
(error) => error
|
||||
)
|
||||
|
||||
if (smokeFailure) {
|
||||
if (cleanupFailure) {
|
||||
console.warn(`[packaged-cli-smoke] temp cleanup failed: ${cleanupFailure.message}`)
|
||||
}
|
||||
throw smokeFailure
|
||||
}
|
||||
if (cleanupFailure) {
|
||||
throw cleanupFailure
|
||||
}
|
||||
|
||||
@@ -104,6 +104,41 @@ describe('Windows signing workflow contract', () => {
|
||||
expect(installRun).toContain('throw "Unable to install the SignPath PowerShell module')
|
||||
})
|
||||
|
||||
it('still installs SignPath when the cut ref predates the composite action', () => {
|
||||
const parsedWorkflow = readWorkflow('.github/workflows/release-cut.yml')
|
||||
const steps = parsedWorkflow.jobs.build.steps
|
||||
const stepNames = steps.map((step) => step.name)
|
||||
const checkoutIndex = stepNames.indexOf('Checkout')
|
||||
const restoreIndex = stepNames.indexOf('Restore composite actions from the workflow ref')
|
||||
const installIndex = stepNames.indexOf('Install SignPath PowerShell module')
|
||||
|
||||
// Why: the build job checks out the cut tag, which for a hotfix cut from an
|
||||
// older ref can predate `.github/actions/install-signpath-module`; without
|
||||
// this restore the `uses: ./…` step dies on a missing action.yml.
|
||||
expect(restoreIndex).toBeGreaterThan(checkoutIndex)
|
||||
expect(restoreIndex).toBeLessThan(installIndex)
|
||||
|
||||
const restoreStep = steps[restoreIndex]
|
||||
const restoreRun = restoreStep.run
|
||||
|
||||
expect(restoreStep.env.WORKFLOW_SHA).toBe('${{ github.workflow_sha }}')
|
||||
expect(restoreRun).toContain('.github/actions/install-signpath-module/action.yml')
|
||||
expect(restoreRun).toContain('git fetch --no-tags --depth=1 origin "$WORKFLOW_SHA"')
|
||||
expect(restoreRun).toContain('git checkout "$WORKFLOW_SHA" -- .github/actions')
|
||||
|
||||
// Why: restoring the action must not turn signing into a soft dependency —
|
||||
// a missing module still has to fail the Windows job, and the CDN fallback
|
||||
// still has to reject an unexpected payload.
|
||||
expect(steps[installIndex]['continue-on-error']).toBeUndefined()
|
||||
expect(restoreStep['continue-on-error']).toBeUndefined()
|
||||
|
||||
const installRun = readWorkflow('.github/actions/install-signpath-module/action.yml').runs
|
||||
.steps[0].run
|
||||
|
||||
expect(installRun).toContain('$actualHash -ne $expectedHash.ToUpperInvariant()')
|
||||
expect(installRun).toContain('throw "SHA-256 mismatch for $source')
|
||||
})
|
||||
|
||||
it('shares one SignPath module install path between release and rehearsal', () => {
|
||||
const rehearsalWorkflow = readWorkflow('.github/workflows/windows-signing-rehearsal.yml')
|
||||
const stepNames = rehearsalWorkflow.jobs.rehearse.steps.map((step) => step.name)
|
||||
|
||||
Reference in New Issue
Block a user