程序员阿江(Relakkes) 232855fe77 fix(desktop): serve packaged H5 shell from Electron builds
Electron's sidecar runs outside app.asar, so H5 static files must be available as normal unpacked files. Point the sidecar at the unpacked renderer dist and keep a server fallback for stale app.asar-style paths.

Constraint: Packaged Bun sidecars cannot read app.asar paths with ordinary fs stat calls.
Rejected: Serve H5 from app.asar directly | the external sidecar is not Electron and does not get asar filesystem support.
Confidence: high
Scope-risk: narrow
Directive: Keep package-smoke checking app.asar.unpacked/dist/index.html before changing asarUnpack or H5 dist paths.
Tested: bun test src/server/__tests__/h5-access-auth.test.ts src/server/__tests__/h5-access-policy.test.ts
Tested: bun test desktop/electron/services/sidecarManager.test.ts scripts/quality-gate/package-smoke/index.test.ts
Tested: bun run check:server
Tested: cd desktop && bun run check:electron
Tested: git diff --check
Tested: SKIP_INSTALL=1 SIGN_BUILD=0 MAC_TARGETS=dmg desktop/scripts/build-macos-arm64.sh
Tested: packaged sidecar curl /?serverUrl=...&h5Token=... returned HTTP 200
Not-tested: Gatekeeper notarization for the local ad-hoc DMG
2026-06-01 23:32:56 +08:00

434 lines
27 KiB
TypeScript

import { afterEach, describe, expect, test } from 'bun:test'
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import {
currentPackageSmokePlatform,
} from './current'
import {
inspectPackagedArtifacts,
parsePackageSmokeArgs,
} from './index'
function createRepoRoot() {
const rootDir = mkdtempSync(join(tmpdir(), 'package-smoke-'))
mkdirSync(join(rootDir, 'desktop'), { recursive: true })
writeFileSync(
join(rootDir, 'desktop', 'package.json'),
JSON.stringify({
name: 'claude-code-desktop',
version: '0.3.1',
build: {
productName: 'Claude Code Haha',
},
}, null, 2),
)
return rootDir
}
function writeFile(rootDir: string, relativePath: string, content = 'ok') {
const fullPath = join(rootDir, relativePath)
mkdirSync(dirname(fullPath), { recursive: true })
writeFileSync(fullPath, content)
}
const tempDirs: string[] = []
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true })
}
})
describe('package smoke args', () => {
test('requires a supported platform value', () => {
expect(() => parsePackageSmokeArgs([])).toThrow('--platform')
expect(() => parsePackageSmokeArgs(['--platform', 'android'])).toThrow('macos|windows|linux')
expect(() => parsePackageSmokeArgs(['--platform', 'macos', '--package-kind', 'installer'])).toThrow('auto|dir|release')
expect(parsePackageSmokeArgs(['--platform', 'macos']).platform).toBe('macos')
expect(parsePackageSmokeArgs(['--platform', 'macos']).packageKind).toBe('auto')
expect(parsePackageSmokeArgs(['--platform', 'macos', '--package-kind', 'dir']).packageKind).toBe('dir')
expect(parsePackageSmokeArgs(['--platform', 'macos', '--require-macos-gatekeeper']).requireMacosGatekeeper).toBe(true)
})
test('maps host platforms to current package-smoke platforms', () => {
expect(currentPackageSmokePlatform('darwin')).toBe('macos')
expect(currentPackageSmokePlatform('win32')).toBe('windows')
expect(currentPackageSmokePlatform('linux')).toBe('linux')
expect(currentPackageSmokePlatform('freebsd')).toBeNull()
})
})
describe('packaged artifact inspection', () => {
test('passes macOS bundle structure checks and records optional archive artifacts', async () => {
const rootDir = createRepoRoot()
tempDirs.push(rootDir)
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Info.plist')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/MacOS/Claude Code Haha')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app-update.yml')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/dist/index.html')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/src-tauri/binaries/claude-sidecar-aarch64-apple-darwin')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/node_modules/node-pty/package.json')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/node_modules/node-pty/prebuilds/darwin-arm64/pty.node')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper')
writeFile(rootDir, 'desktop/build-artifacts/electron/Claude-Code-Haha-0.3.1-arm64.zip')
writeFile(rootDir, 'desktop/build-artifacts/electron/Claude-Code-Haha-0.3.1-arm64.zip.blockmap')
writeFile(rootDir, 'desktop/build-artifacts/electron/Claude-Code-Haha-0.3.1-arm64.dmg')
writeFile(rootDir, 'desktop/build-artifacts/electron/Claude-Code-Haha-0.3.1-arm64.dmg.blockmap')
writeFile(rootDir, 'desktop/build-artifacts/electron/latest-mac.yml', [
'version: 0.3.1',
'files:',
' - url: Claude-Code-Haha-0.3.1-arm64.zip',
' - url: Claude-Code-Haha-0.3.1-arm64.dmg',
'path: Claude-Code-Haha-0.3.1-arm64.zip',
].join('\n'))
const report = await inspectPackagedArtifacts(rootDir, { platform: 'macos' })
expect(report.passed).toBe(true)
expect(report.verificationMode).toBe('bundle-structure')
expect(report.notes.join('\n')).toContain('No GUI launch was attempted.')
expect(report.optionalArtifacts.some((artifact) => artifact.path.endsWith('.zip'))).toBe(true)
expect(report.optionalArtifacts.some((artifact) => artifact.path.endsWith('.dmg'))).toBe(true)
expect(report.optionalArtifacts.some((artifact) => artifact.path.endsWith('latest-mac.yml'))).toBe(true)
expect(report.passedChecks.some((check) => check.label.includes('update metadata referenced artifact'))).toBe(true)
expect(report.passedChecks.some((check) => check.label.includes('macOS update artifact blockmap'))).toBe(true)
expect(report.passedChecks.some((check) => check.label === 'macOS unpacked H5 shell')).toBe(true)
})
test('fails macOS inspection when the H5 shell is not unpacked for the sidecar', async () => {
const rootDir = createRepoRoot()
tempDirs.push(rootDir)
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Info.plist')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/MacOS/Claude Code Haha')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app-update.yml')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/src-tauri/binaries/claude-sidecar-aarch64-apple-darwin')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/node_modules/node-pty/package.json')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/node_modules/node-pty/prebuilds/darwin-arm64/pty.node')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper')
const report = await inspectPackagedArtifacts(rootDir, { platform: 'macos', packageKind: 'dir' })
expect(report.passed).toBe(false)
expect(report.missingChecks.some((check) => check.label === 'macOS unpacked H5 shell')).toBe(true)
})
test('fails macOS archive checks when latest-mac.yml points at missing assets', async () => {
const rootDir = createRepoRoot()
tempDirs.push(rootDir)
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Info.plist')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/MacOS/Claude Code Haha')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app-update.yml')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/src-tauri/binaries/claude-sidecar-aarch64-apple-darwin')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/node_modules/node-pty/package.json')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/node_modules/node-pty/prebuilds/darwin-arm64/pty.node')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper')
writeFile(rootDir, 'desktop/build-artifacts/electron/Claude Code Haha-0.3.1-arm64-mac.zip')
writeFile(rootDir, 'desktop/build-artifacts/electron/latest-mac.yml', 'path: Claude-Code-Haha-0.3.1-arm64-mac.zip\n')
const report = await inspectPackagedArtifacts(rootDir, { platform: 'macos' })
expect(report.passed).toBe(false)
expect(report.missingChecks.some((check) => check.label.includes('update metadata referenced artifact'))).toBe(true)
})
test('fails macOS inspection when the installed updater metadata is missing from Resources', async () => {
const rootDir = createRepoRoot()
tempDirs.push(rootDir)
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Info.plist')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/MacOS/Claude Code Haha')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/src-tauri/binaries/claude-sidecar-aarch64-apple-darwin')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/node_modules/node-pty/package.json')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/node_modules/node-pty/prebuilds/darwin-arm64/pty.node')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper')
writeFile(rootDir, 'desktop/build-artifacts/electron/Claude-Code-Haha-0.3.1-arm64.zip')
writeFile(rootDir, 'desktop/build-artifacts/electron/latest-mac.yml', [
'version: 0.3.1',
'files:',
' - url: Claude-Code-Haha-0.3.1-arm64.zip',
'path: Claude-Code-Haha-0.3.1-arm64.zip',
].join('\n'))
const report = await inspectPackagedArtifacts(rootDir, { platform: 'macos' })
expect(report.passed).toBe(false)
expect(report.missingChecks.some((check) => check.label === 'macOS app-update.yml')).toBe(true)
})
test('fails release inspection when an update artifact blockmap is missing', async () => {
const rootDir = createRepoRoot()
tempDirs.push(rootDir)
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Info.plist')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/MacOS/Claude Code Haha')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app-update.yml')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/src-tauri/binaries/claude-sidecar-aarch64-apple-darwin')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/node_modules/node-pty/package.json')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/node_modules/node-pty/prebuilds/darwin-arm64/pty.node')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper')
writeFile(rootDir, 'desktop/build-artifacts/electron/Claude-Code-Haha-0.3.1-arm64.zip')
writeFile(rootDir, 'desktop/build-artifacts/electron/latest-mac.yml', 'path: Claude-Code-Haha-0.3.1-arm64.zip\n')
const report = await inspectPackagedArtifacts(rootDir, { platform: 'macos', packageKind: 'release' })
expect(report.passed).toBe(false)
expect(report.missingChecks.some((check) => check.label.includes('macOS update artifact blockmap'))).toBe(true)
})
test('adds codesign diagnostics when macOS Gatekeeper assessment fails', async () => {
const rootDir = createRepoRoot()
tempDirs.push(rootDir)
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Info.plist')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/MacOS/Claude Code Haha')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/src-tauri/binaries/claude-sidecar-aarch64-apple-darwin')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/node_modules/node-pty/package.json')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/node_modules/node-pty/prebuilds/darwin-arm64/pty.node')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper')
const report = await inspectPackagedArtifacts(rootDir, {
platform: 'macos',
packageKind: 'dir',
requireMacosGatekeeper: true,
hostPlatform: 'macos',
commandRunner: (command, args) => {
if (command.endsWith('/spctl')) {
return { status: 1, stdout: '', stderr: 'rejected\nsource=Unnotarized Developer ID\n' }
}
if (command.endsWith('/codesign')) {
return { status: 1, stdout: '', stderr: 'code object is not signed at all\n' }
}
if (command.endsWith('/xcrun') && args[0] === 'stapler') {
return { status: 65, stdout: '', stderr: 'The validate action failed!\n' }
}
return { status: 0, stdout: '', stderr: '' }
},
})
expect(report.passed).toBe(false)
expect(report.missingChecks.some((check) => check.label.includes('macOS Gatekeeper launch approval'))).toBe(true)
expect(report.notes.join('\n')).toContain('spctl Gatekeeper assessment exited with status 1')
expect(report.notes.join('\n')).toContain('codesign verification exited with status 1')
expect(report.notes.join('\n')).toContain('codesign signature details exited with status 1')
expect(report.notes.join('\n')).toContain('notarization ticket validation exited with status 65')
})
test('retries macOS Gatekeeper assessment with a raised file limit when spctl hits open-file limits', async () => {
const rootDir = createRepoRoot()
tempDirs.push(rootDir)
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Info.plist')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/MacOS/Claude Code Haha')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/src-tauri/binaries/claude-sidecar-aarch64-apple-darwin')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/node_modules/node-pty/package.json')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/node_modules/node-pty/prebuilds/darwin-arm64/pty.node')
writeFile(rootDir, 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app/Contents/Resources/app.asar.unpacked/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper')
const commands: string[] = []
const report = await inspectPackagedArtifacts(rootDir, {
platform: 'macos',
packageKind: 'dir',
requireMacosGatekeeper: true,
hostPlatform: 'macos',
commandRunner: (command, args) => {
commands.push(`${command} ${args.join(' ')}`)
if (command.endsWith('/spctl')) {
return { status: 1, stdout: '', stderr: 'Too many open files\n' }
}
if (command.endsWith('/zsh')) {
return { status: 1, stdout: '', stderr: 'bundle format unrecognized, invalid, or unsuitable\n' }
}
if (command.endsWith('/codesign')) {
return { status: 0, stdout: '', stderr: 'valid on disk\n' }
}
if (command.endsWith('/xcrun') && args[0] === 'stapler') {
return { status: 65, stdout: '', stderr: 'does not have a ticket stapled to it\n' }
}
return { status: 0, stdout: '', stderr: '' }
},
})
expect(report.passed).toBe(false)
expect(commands.some(command => command.startsWith('/bin/zsh -lc ulimit -n'))).toBe(true)
expect(report.missingChecks.some((check) => check.label.includes('bundle format unrecognized'))).toBe(true)
expect(report.notes.join('\n')).toContain('retried with a raised file descriptor limit')
expect(report.notes.join('\n')).toContain('spctl Gatekeeper assessment exited with status 1: bundle format unrecognized')
})
test('treats Windows checks as static inspection on non-Windows hosts', async () => {
const rootDir = createRepoRoot()
tempDirs.push(rootDir)
writeFile(rootDir, 'desktop/build-artifacts/electron/Claude Code Haha Setup 0.3.1.exe')
writeFile(rootDir, 'desktop/build-artifacts/electron/Claude Code Haha Setup 0.3.1.exe.blockmap')
writeFile(rootDir, 'desktop/build-artifacts/electron/win-unpacked/resources/app.asar')
writeFile(rootDir, 'desktop/build-artifacts/electron/win-unpacked/resources/app-update.yml')
writeFile(rootDir, 'desktop/build-artifacts/electron/win-unpacked/resources/app.asar.unpacked/src-tauri/binaries/claude-sidecar-x86_64-pc-windows-msvc.exe')
writeFile(rootDir, 'desktop/build-artifacts/electron/win-unpacked/resources/app.asar.unpacked/node_modules/node-pty/package.json')
writeFile(rootDir, 'desktop/build-artifacts/electron/win-unpacked/resources/app.asar.unpacked/node_modules/node-pty/prebuilds/win32-x64/pty.node')
writeFile(rootDir, 'desktop/build-artifacts/electron/latest.yml', 'path: Claude Code Haha Setup 0.3.1.exe\n')
const report = await inspectPackagedArtifacts(rootDir, { platform: 'windows' })
expect(report.passed).toBe(true)
expect(report.verificationMode).toBe('static-artifact')
expect(report.notes.join('\n')).toContain('not claim installer execution or app launch success')
})
test('passes Windows checks against the canonical build script output directory', async () => {
const rootDir = createRepoRoot()
tempDirs.push(rootDir)
writeFile(rootDir, 'desktop/build-artifacts/windows-x64/Claude-Code-Haha-0.3.1-x64.exe')
writeFile(rootDir, 'desktop/build-artifacts/windows-x64/Claude-Code-Haha-0.3.1-x64.exe.blockmap')
writeFile(rootDir, 'desktop/build-artifacts/windows-x64/win-unpacked/resources/app.asar')
writeFile(rootDir, 'desktop/build-artifacts/windows-x64/win-unpacked/resources/app-update.yml')
writeFile(rootDir, 'desktop/build-artifacts/windows-x64/win-unpacked/resources/app.asar.unpacked/src-tauri/binaries/claude-sidecar-x86_64-pc-windows-msvc.exe')
writeFile(rootDir, 'desktop/build-artifacts/windows-x64/win-unpacked/resources/app.asar.unpacked/node_modules/node-pty/package.json')
writeFile(rootDir, 'desktop/build-artifacts/windows-x64/win-unpacked/resources/app.asar.unpacked/node_modules/node-pty/prebuilds/win32-x64/pty.node')
writeFile(rootDir, 'desktop/build-artifacts/windows-x64/latest.yml', 'path: Claude-Code-Haha-0.3.1-x64.exe\n')
const report = await inspectPackagedArtifacts(rootDir, {
platform: 'windows',
packageKind: 'release',
artifactsDir: 'desktop/build-artifacts/windows-x64',
})
expect(report.passed).toBe(true)
expect(report.artifactsDir.endsWith('desktop/build-artifacts/windows-x64')).toBe(true)
})
test('passes Windows directory-only checks for electron-builder --dir output', async () => {
const rootDir = createRepoRoot()
tempDirs.push(rootDir)
writeFile(rootDir, 'desktop/build-artifacts/electron/win-unpacked/Claude Code Haha.exe')
writeFile(rootDir, 'desktop/build-artifacts/electron/win-unpacked/resources/app.asar')
writeFile(rootDir, 'desktop/build-artifacts/electron/win-unpacked/resources/app.asar.unpacked/src-tauri/binaries/claude-sidecar-x86_64-pc-windows-msvc.exe')
writeFile(rootDir, 'desktop/build-artifacts/electron/win-unpacked/resources/app.asar.unpacked/node_modules/node-pty/package.json')
writeFile(rootDir, 'desktop/build-artifacts/electron/win-unpacked/resources/app.asar.unpacked/node_modules/node-pty/prebuilds/win32-x64/pty.node')
const report = await inspectPackagedArtifacts(rootDir, { platform: 'windows', packageKind: 'dir' })
expect(report.passed).toBe(true)
expect(report.notes.join('\n')).toContain('directory-only development package')
expect(report.notes.join('\n')).toContain('Windows app-update.yml was not required')
})
test('does not treat the win-unpacked app executable as a Windows release installer', async () => {
const rootDir = createRepoRoot()
tempDirs.push(rootDir)
writeFile(rootDir, 'desktop/build-artifacts/electron/win-unpacked/Claude Code Haha.exe')
writeFile(rootDir, 'desktop/build-artifacts/electron/win-unpacked/resources/app.asar')
writeFile(rootDir, 'desktop/build-artifacts/electron/win-unpacked/resources/app-update.yml')
writeFile(rootDir, 'desktop/build-artifacts/electron/win-unpacked/resources/app.asar.unpacked/src-tauri/binaries/claude-sidecar-x86_64-pc-windows-msvc.exe')
writeFile(rootDir, 'desktop/build-artifacts/electron/win-unpacked/resources/app.asar.unpacked/node_modules/node-pty/package.json')
writeFile(rootDir, 'desktop/build-artifacts/electron/win-unpacked/resources/app.asar.unpacked/node_modules/node-pty/prebuilds/win32-x64/pty.node')
const report = await inspectPackagedArtifacts(rootDir, { platform: 'windows', packageKind: 'release' })
expect(report.passed).toBe(false)
expect(report.missingChecks.some((check) => check.label.includes('.exe installer'))).toBe(true)
})
test('passes Linux checks against the canonical build script output directory', async () => {
const rootDir = createRepoRoot()
tempDirs.push(rootDir)
writeFile(rootDir, 'desktop/build-artifacts/linux-x64/Claude-Code-Haha-0.3.1-x64.AppImage')
writeFile(rootDir, 'desktop/build-artifacts/linux-x64/Claude-Code-Haha-0.3.1-x64.AppImage.blockmap')
writeFile(rootDir, 'desktop/build-artifacts/linux-x64/claude-code-desktop_0.3.1_amd64.deb')
writeFile(rootDir, 'desktop/build-artifacts/linux-x64/linux-unpacked/resources/app.asar')
writeFile(rootDir, 'desktop/build-artifacts/linux-x64/linux-unpacked/resources/app-update.yml')
writeFile(rootDir, 'desktop/build-artifacts/linux-x64/linux-unpacked/resources/app.asar.unpacked/src-tauri/binaries/claude-sidecar-x86_64-unknown-linux-gnu')
writeFile(rootDir, 'desktop/build-artifacts/linux-x64/linux-unpacked/resources/app.asar.unpacked/node_modules/node-pty/package.json')
writeFile(rootDir, 'desktop/build-artifacts/linux-x64/linux-unpacked/resources/app.asar.unpacked/node_modules/node-pty/prebuilds/linux-x64/pty.node')
writeFile(rootDir, 'desktop/build-artifacts/linux-x64/latest-linux.yml', 'path: Claude-Code-Haha-0.3.1-x64.AppImage\n')
const report = await inspectPackagedArtifacts(rootDir, {
platform: 'linux',
packageKind: 'release',
artifactsDir: 'desktop/build-artifacts/linux-x64',
})
expect(report.passed).toBe(true)
expect(report.artifactsDir.endsWith('desktop/build-artifacts/linux-x64')).toBe(true)
})
test('accepts Linux architecture-specific update metadata from arm64 builds', async () => {
const rootDir = createRepoRoot()
tempDirs.push(rootDir)
writeFile(rootDir, 'desktop/build-artifacts/linux-arm64/Claude-Code-Haha-0.3.1-arm64.AppImage')
writeFile(rootDir, 'desktop/build-artifacts/linux-arm64/Claude-Code-Haha-0.3.1-arm64.AppImage.blockmap')
writeFile(rootDir, 'desktop/build-artifacts/linux-arm64/claude-code-desktop_0.3.1_arm64.deb')
writeFile(rootDir, 'desktop/build-artifacts/linux-arm64/linux-unpacked/resources/app.asar')
writeFile(rootDir, 'desktop/build-artifacts/linux-arm64/linux-unpacked/resources/app-update.yml')
writeFile(rootDir, 'desktop/build-artifacts/linux-arm64/linux-unpacked/resources/app.asar.unpacked/src-tauri/binaries/claude-sidecar-aarch64-unknown-linux-gnu')
writeFile(rootDir, 'desktop/build-artifacts/linux-arm64/linux-unpacked/resources/app.asar.unpacked/node_modules/node-pty/package.json')
writeFile(rootDir, 'desktop/build-artifacts/linux-arm64/linux-unpacked/resources/app.asar.unpacked/node_modules/node-pty/prebuilds/linux-arm64/pty.node')
writeFile(rootDir, 'desktop/build-artifacts/linux-arm64/latest-linux-arm64.yml', 'path: Claude-Code-Haha-0.3.1-arm64.AppImage\n')
const report = await inspectPackagedArtifacts(rootDir, {
platform: 'linux',
packageKind: 'release',
artifactsDir: 'desktop/build-artifacts/linux-arm64',
})
expect(report.passed).toBe(true)
expect(report.optionalArtifacts.some((artifact) => artifact.path.endsWith('latest-linux-arm64.yml'))).toBe(true)
expect(report.passedChecks.some((check) => check.label.includes('update metadata referenced artifact'))).toBe(true)
})
test('passes Linux directory-only checks for electron-builder --dir output', async () => {
const rootDir = createRepoRoot()
tempDirs.push(rootDir)
writeFile(rootDir, 'desktop/build-artifacts/electron/linux-unpacked/resources/app.asar')
writeFile(rootDir, 'desktop/build-artifacts/electron/linux-unpacked/resources/app.asar.unpacked/src-tauri/binaries/claude-sidecar-x86_64-unknown-linux-gnu')
writeFile(rootDir, 'desktop/build-artifacts/electron/linux-unpacked/resources/app.asar.unpacked/node_modules/node-pty/package.json')
writeFile(rootDir, 'desktop/build-artifacts/electron/linux-unpacked/resources/app.asar.unpacked/node_modules/node-pty/prebuilds/linux-x64/pty.node')
const report = await inspectPackagedArtifacts(rootDir, { platform: 'linux', packageKind: 'dir' })
expect(report.passed).toBe(true)
expect(report.notes.join('\n')).toContain('directory-only development package')
expect(report.notes.join('\n')).toContain('Linux app-update.yml was not required')
})
test('fails Linux inspection when no packaged artifact is present', async () => {
const rootDir = createRepoRoot()
tempDirs.push(rootDir)
const report = await inspectPackagedArtifacts(rootDir, { platform: 'linux' })
expect(report.passed).toBe(false)
expect(report.missingChecks.some((check) => check.label.includes('linux packaged artifact'))).toBe(true)
})
test('fails Linux release inspection without linux-unpacked static resources', async () => {
const rootDir = createRepoRoot()
tempDirs.push(rootDir)
writeFile(rootDir, 'desktop/build-artifacts/electron/Claude-Code-Haha-0.3.1-x64.AppImage')
writeFile(rootDir, 'desktop/build-artifacts/electron/latest-linux.yml', 'path: Claude-Code-Haha-0.3.1-x64.AppImage\n')
const report = await inspectPackagedArtifacts(rootDir, { platform: 'linux', packageKind: 'release' })
expect(report.passed).toBe(false)
expect(report.missingChecks.some((check) => check.label.includes('linux-unpacked'))).toBe(true)
})
})