mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
Fix Resolve with AI icon (#2918)
This commit is contained in:
@@ -89,9 +89,7 @@ jobs:
|
||||
# Why: install intentionally blocks Electron's package postinstall, but
|
||||
# some unit tests import `electron` under Node and require path.txt.
|
||||
- name: Install Electron package binary for tests
|
||||
run: |
|
||||
pnpm rebuild electron --pending
|
||||
node -e "require('electron')"
|
||||
run: node config/scripts/install-electron-package-binary.mjs
|
||||
|
||||
- name: Test
|
||||
run: pnpm test
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { createHash } from 'node:crypto'
|
||||
import {
|
||||
createReadStream,
|
||||
createWriteStream,
|
||||
existsSync,
|
||||
mkdtempSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { platform as osPlatform, tmpdir } from 'node:os'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { Readable } from 'node:stream'
|
||||
import { pipeline } from 'node:stream/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const projectDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
const electronPackageDir = resolve(projectDir, 'node_modules/electron')
|
||||
const electronRequire = createRequire(resolve(electronPackageDir, 'package.json'))
|
||||
const { version: electronVersion } = electronRequire('./package.json')
|
||||
const extract = electronRequire('extract-zip')
|
||||
const platformPath = getElectronPlatformPath()
|
||||
|
||||
if (electronPackageLoads()) {
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// Why: PR tests run under system Node after native modules are rebuilt for
|
||||
// Node. Install only Electron's npm package binary here; do not run the full
|
||||
// Electron native-module rebuild path, which would undo the Node ABI rebuild.
|
||||
console.log('[electron-package] Electron package binary is missing; running Electron install.')
|
||||
await installElectronPackageBinary()
|
||||
|
||||
repairElectronPathFile()
|
||||
|
||||
if (!electronPackageLoads()) {
|
||||
logElectronInstallDiagnostics()
|
||||
console.error('[electron-package] Electron package is still unavailable after install.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
function electronPackageLoads() {
|
||||
try {
|
||||
require('electron')
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function repairElectronPathFile() {
|
||||
const electronExecutable = resolve(electronPackageDir, 'dist', platformPath)
|
||||
if (!existsSync(electronExecutable)) {
|
||||
return
|
||||
}
|
||||
|
||||
const pathFile = resolve(electronPackageDir, 'path.txt')
|
||||
let currentPath = ''
|
||||
try {
|
||||
currentPath = readFileSync(pathFile, 'utf8')
|
||||
} catch {
|
||||
// Missing path.txt is the common CI failure this script repairs.
|
||||
}
|
||||
|
||||
if (currentPath !== platformPath) {
|
||||
writeFileSync(pathFile, platformPath)
|
||||
console.log(`[electron-package] Repaired Electron path.txt -> ${platformPath}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function installElectronPackageBinary() {
|
||||
const electronDistDir = resolve(electronPackageDir, 'dist')
|
||||
const artifactName = getElectronArtifactName()
|
||||
const tempDir = mkdtempSync(resolve(tmpdir(), 'orca-electron-'))
|
||||
const zipPath = resolve(tempDir, artifactName)
|
||||
|
||||
try {
|
||||
await downloadElectronArtifact(artifactName, zipPath)
|
||||
await verifyElectronArtifactChecksum(artifactName, zipPath)
|
||||
|
||||
rmSync(electronDistDir, { recursive: true, force: true })
|
||||
await extract(zipPath, { dir: electronDistDir })
|
||||
|
||||
const srcTypeDefPath = resolve(electronDistDir, 'electron.d.ts')
|
||||
if (existsSync(srcTypeDefPath)) {
|
||||
renameSync(srcTypeDefPath, resolve(electronPackageDir, 'electron.d.ts'))
|
||||
}
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadElectronArtifact(artifactName, zipPath) {
|
||||
const artifactUrl = new URL(`v${electronVersion}/${artifactName}`, getElectronReleaseBaseUrl())
|
||||
console.log(`[electron-package] Downloading ${artifactUrl}`)
|
||||
|
||||
const response = await fetch(artifactUrl)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download ${artifactName}: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
if (!response.body) {
|
||||
throw new Error(`Failed to download ${artifactName}: empty response body`)
|
||||
}
|
||||
|
||||
await pipeline(Readable.fromWeb(response.body), createWriteStream(zipPath))
|
||||
}
|
||||
|
||||
async function verifyElectronArtifactChecksum(artifactName, zipPath) {
|
||||
if (
|
||||
process.env.electron_use_remote_checksums ||
|
||||
process.env.npm_config_electron_use_remote_checksums
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const expected = electronRequire('./checksums.json')[artifactName]
|
||||
if (!expected) {
|
||||
throw new Error(`Missing Electron checksum for ${artifactName}`)
|
||||
}
|
||||
|
||||
const hash = createHash('sha256')
|
||||
for await (const chunk of createReadStream(zipPath)) {
|
||||
hash.update(chunk)
|
||||
}
|
||||
const actual = hash.digest('hex')
|
||||
if (actual !== expected) {
|
||||
throw new Error(`Checksum mismatch for ${artifactName}: expected ${expected}, got ${actual}`)
|
||||
}
|
||||
}
|
||||
|
||||
function getElectronArtifactName() {
|
||||
return `electron-v${electronVersion}-${process.env.npm_config_platform || osPlatform()}-${
|
||||
process.env.npm_config_arch || process.arch
|
||||
}.zip`
|
||||
}
|
||||
|
||||
function getElectronReleaseBaseUrl() {
|
||||
const configuredMirror = process.env.ELECTRON_MIRROR || process.env.npm_config_electron_mirror
|
||||
const baseUrl = configuredMirror || 'https://github.com/electron/electron/releases/download/'
|
||||
return baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`
|
||||
}
|
||||
|
||||
function logElectronInstallDiagnostics() {
|
||||
const electronDistDir = resolve(electronPackageDir, 'dist')
|
||||
const pathFile = resolve(electronPackageDir, 'path.txt')
|
||||
console.error('[electron-package] Electron install diagnostics:')
|
||||
console.error(` packageDir=${electronPackageDir} exists=${existsSync(electronPackageDir)}`)
|
||||
console.error(` distDir=${electronDistDir} exists=${existsSync(electronDistDir)}`)
|
||||
console.error(` pathFile=${pathFile} exists=${existsSync(pathFile)}`)
|
||||
console.error(` platformPath=${platformPath}`)
|
||||
if (existsSync(electronDistDir)) {
|
||||
console.error(` distEntries=${safeReaddir(electronDistDir).join(', ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
function safeReaddir(targetPath) {
|
||||
try {
|
||||
return readdirSync(targetPath).slice(0, 40)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function getElectronPlatformPath() {
|
||||
const targetPlatform = process.env.npm_config_platform || osPlatform()
|
||||
switch (targetPlatform) {
|
||||
case 'mas':
|
||||
case 'darwin':
|
||||
return 'Electron.app/Contents/MacOS/Electron'
|
||||
case 'freebsd':
|
||||
case 'openbsd':
|
||||
case 'linux':
|
||||
return 'electron'
|
||||
case 'win32':
|
||||
return 'electron.exe'
|
||||
default:
|
||||
throw new Error(`Electron builds are not available on platform: ${targetPlatform}`)
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,10 @@ describe('Electron runtime package contract', () => {
|
||||
})
|
||||
|
||||
it('guards release publishing before electron-builder runs', () => {
|
||||
const releaseWorkflow = readFileSync(join(projectDir, '.github/workflows/release-cut.yml'), 'utf8')
|
||||
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 }) => [
|
||||
@@ -47,7 +50,9 @@ describe('Electron runtime package contract', () => {
|
||||
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(command.indexOf('ensure-native-runtime')).toBeLessThan(
|
||||
command.indexOf('electron-builder')
|
||||
)
|
||||
}
|
||||
expect(releaseCommands.get('mac')).toContain(' && ORCA_MAC_RELEASE=1 ')
|
||||
expect(releaseCommands.get('linux')).toContain(' && pnpm exec electron-builder ')
|
||||
@@ -55,4 +60,14 @@ describe('Electron runtime package contract', () => {
|
||||
'; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }; pnpm exec electron-builder '
|
||||
)
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -359,4 +359,20 @@ describe('ConflictSummaryCard', () => {
|
||||
expect(mergeMarkup).toContain('Abort merge')
|
||||
expect(rebaseMarkup).not.toContain('Abort merge')
|
||||
})
|
||||
|
||||
it('renders the Sparkles icon on the idle Resolve with AI button', () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<ConflictSummaryCard
|
||||
conflictOperation="merge"
|
||||
unresolvedCount={2}
|
||||
isResolvingWithAI={false}
|
||||
onResolveWithAI={vi.fn()}
|
||||
onReview={vi.fn()}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(markup).toContain('Resolve with AI')
|
||||
expect(markup).toContain('lucide-sparkles')
|
||||
expect(markup).not.toMatch(/\blucide-sparkle(?!s)\b/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5811,7 +5811,7 @@ export function ConflictSummaryCard({
|
||||
{isResolvingWithAI ? (
|
||||
<RefreshCw className="size-3.5 animate-spin" />
|
||||
) : (
|
||||
<Sparkle className="size-3.5" />
|
||||
<Sparkles className="size-3.5" />
|
||||
)}
|
||||
Resolve with AI
|
||||
</Button>
|
||||
|
||||
@@ -56,4 +56,12 @@ describe('MergeConflictNotice', () => {
|
||||
|
||||
expect(markup).toBe('')
|
||||
})
|
||||
|
||||
it('renders the Sparkles icon on the idle Resolve with AI button', () => {
|
||||
const markup = renderNotice(makePR())
|
||||
|
||||
expect(markup).toContain('Resolve with AI')
|
||||
expect(markup).toContain('lucide-sparkles')
|
||||
expect(markup).not.toMatch(/\blucide-sparkle(?!s)\b/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
Check,
|
||||
MessageSquare,
|
||||
ChevronDown,
|
||||
Sparkle,
|
||||
Sparkles,
|
||||
RefreshCw,
|
||||
Wrench
|
||||
} from 'lucide-react'
|
||||
@@ -95,7 +95,7 @@ function ResolveConflictsWithAIButton({
|
||||
{isResolvingWithAI ? (
|
||||
<RefreshCw className="size-3.5 animate-spin" />
|
||||
) : (
|
||||
<Sparkle className="size-3.5" />
|
||||
<Sparkles className="size-3.5" />
|
||||
)}
|
||||
Resolve with AI
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user