import test from 'node:test'
import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import { createRequire } from 'node:module'
import { execFileSync, spawnSync } from 'node:child_process'
import {
copyFileSync,
mkdirSync,
mkdtempSync,
readFileSync,
readdirSync,
realpathSync,
rmSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
const require = createRequire(import.meta.url)
const packageLock = JSON.parse(readFileSync(new URL('../package-lock.json', import.meta.url), 'utf8'))
const catalogSource = JSON.parse(readFileSync(new URL('../catalog-source/pearbrowser-network.catalog.json', import.meta.url), 'utf8'))
const { SEED_APPS } = require('../backend/catalogue-seed.js')
const rootLicense = readFileSync(new URL('../LICENSE', import.meta.url), 'utf8')
const applingPkg = JSON.parse(readFileSync(new URL('../appling/package.json', import.meta.url), 'utf8'))
const applingLock = JSON.parse(readFileSync(new URL('../appling/package-lock.json', import.meta.url), 'utf8'))
const applingCmake = readFileSync(new URL('../appling/CMakeLists.txt', import.meta.url), 'utf8')
const nativeReleaseWorkflow = readFileSync(new URL('../.github/workflows/desktop-native-release.yml', import.meta.url), 'utf8')
const desktopCiWorkflow = readFileSync(new URL('../.github/workflows/desktop-ci.yml', import.meta.url), 'utf8')
const applingReleaseCheck = readFileSync(new URL('../scripts/check-appling-release.mjs', import.meta.url), 'utf8')
const applingArtifactCollector = readFileSync(new URL('../scripts/collect-appling-artifacts.mjs', import.meta.url), 'utf8')
const applingArtifactCollectorPath = fileURLToPath(new URL('../scripts/collect-appling-artifacts.mjs', import.meta.url))
const nativeSigningCheck = readFileSync(new URL('../scripts/check-native-signing-credentials.mjs', import.meta.url), 'utf8')
const nativeSigningCheckPath = fileURLToPath(new URL('../scripts/check-native-signing-credentials.mjs', import.meta.url))
const nativeSigningSecretPlan = readFileSync(new URL('../scripts/generate-native-signing-secret-plan.mjs', import.meta.url), 'utf8')
const nativeSigningSecretPlanPath = fileURLToPath(new URL('../scripts/generate-native-signing-secret-plan.mjs', import.meta.url))
const nativeReleaseAssetCheck = readFileSync(new URL('../scripts/check-native-release-assets.mjs', import.meta.url), 'utf8')
const nativeReleaseAssetCheckPath = fileURLToPath(new URL('../scripts/check-native-release-assets.mjs', import.meta.url))
const nativeReleaseAssetResolver = readFileSync(new URL('../scripts/resolve-native-release-asset.mjs', import.meta.url), 'utf8')
const nativeReleaseAssetResolverPath = fileURLToPath(new URL('../scripts/resolve-native-release-asset.mjs', import.meta.url))
const nativeDownloadVerifier = readFileSync(new URL('../scripts/verify-native-downloads.mjs', import.meta.url), 'utf8')
const nativeDownloadVerifierPath = fileURLToPath(new URL('../scripts/verify-native-downloads.mjs', import.meta.url))
const nativeInstallSnippet = readFileSync(new URL('../scripts/generate-native-install-snippet.mjs', import.meta.url), 'utf8')
const nativeInstallSnippetPath = fileURLToPath(new URL('../scripts/generate-native-install-snippet.mjs', import.meta.url))
const nativeInstallGuide = readFileSync(new URL('../docs/INSTALL_NATIVE_PACKAGES.md', import.meta.url), 'utf8')
const nativeInstallSmokePlan = readFileSync(new URL('../scripts/generate-native-install-smoke-plan.mjs', import.meta.url), 'utf8')
const nativeInstallSmokePlanPath = fileURLToPath(new URL('../scripts/generate-native-install-smoke-plan.mjs', import.meta.url))
const originIsolationSmokePlan = readFileSync(new URL('../scripts/generate-origin-isolation-smoke-plan.mjs', import.meta.url), 'utf8')
const originIsolationSmokePlanPath = fileURLToPath(new URL('../scripts/generate-origin-isolation-smoke-plan.mjs', import.meta.url))
const originIsolationSmokeEvidence = readFileSync(new URL('../scripts/generate-origin-isolation-smoke-evidence.mjs', import.meta.url), 'utf8')
const packageManagerManifests = readFileSync(new URL('../scripts/generate-package-manager-manifests.mjs', import.meta.url), 'utf8')
const packageManagerManifestsPath = fileURLToPath(new URL('../scripts/generate-package-manager-manifests.mjs', import.meta.url))
const publicTrustReadiness = readFileSync(new URL('../scripts/check-public-trust-readiness.mjs', import.meta.url), 'utf8')
const publicTrustReadinessPath = fileURLToPath(new URL('../scripts/check-public-trust-readiness.mjs', import.meta.url))
const publicTrustOperatorReport = readFileSync(new URL('../scripts/generate-public-trust-operator-report.mjs', import.meta.url), 'utf8')
const publicTrustOperatorReportPath = fileURLToPath(new URL('../scripts/generate-public-trust-operator-report.mjs', import.meta.url))
const linuxAppImageMetadata = readFileSync(new URL('../scripts/check-linux-appimage-metadata.mjs', import.meta.url), 'utf8')
const linuxAppImageMetadataPath = fileURLToPath(new URL('../scripts/check-linux-appimage-metadata.mjs', import.meta.url))
const linuxMetainfo = readFileSync(new URL('../appling/assets/linux/io.github.bigdestiny2.pearbrowser.metainfo.xml', import.meta.url), 'utf8')
const macosDmgPackager = readFileSync(new URL('../scripts/create-macos-dmg.mjs', import.meta.url), 'utf8')
const macosNotarizeScript = readFileSync(new URL('../scripts/notarize-appling-macos.mjs', import.meta.url), 'utf8')
const releaseScript = readFileSync(new URL('../scripts/release-prod.sh', import.meta.url), 'utf8')
const sheetsBundleScript = readFileSync(new URL('../scripts/build-sheets-bundle.sh', import.meta.url), 'utf8')
const mainEntry = readFileSync(new URL('../index.js', import.meta.url), 'utf8')
const bootEntry = readFileSync(new URL('../ui/boot.js', import.meta.url), 'utf8')
const rpcWebSocketAuth = readFileSync(new URL('../backend/rpc-websocket-auth.cjs', import.meta.url), 'utf8')
const tabRuntime = readFileSync(new URL('../backend/tab-runtime.js', import.meta.url), 'utf8')
const runtimeSmoke = readFileSync(new URL('../scripts/runtime-rpc-smoke.mjs', import.meta.url), 'utf8')
const releaseStorySmoke = readFileSync(new URL('../scripts/release-rpc-story-smoke.mjs', import.meta.url), 'utf8')
const liveCatalogVerifier = readFileSync(new URL('../scripts/verify-live-catalog.js', import.meta.url), 'utf8')
const hiveRelayLayout = readFileSync(new URL('../scripts/check-hiverelay-layout.mjs', import.meta.url), 'utf8')
const hiveRelayCheckPath = fileURLToPath(new URL('../scripts/check-hiverelay-layout.mjs', import.meta.url))
const relayClient = readFileSync(new URL('../backend/relay-client.js', import.meta.url), 'utf8')
const verifyPin = readFileSync(new URL('../scripts/verify-pin.js', import.meta.url), 'utf8')
const pinAppOnHiveRelay = readFileSync(new URL('../scripts/pin-app-on-hiverelay.js', import.meta.url), 'utf8')
const npmHiveRelayPackages = ['p2p-hiverelay', 'p2p-hiverelay-client', 'p2p-hiverelay-verifier']
const releaseVersion = pkg.version
const releaseTag = `v${releaseVersion}`
const releaseVersionPattern = releaseVersion.replaceAll('.', '\\.')
const releaseTagPattern = releaseTag.replaceAll('.', '\\.')
function writePackageManagerReleaseFixture (fixture, names) {
const shaByName = new Map()
const assets = names.map((name, i) => {
if (name.endsWith('.sha256')) {
const assetName = name.slice(0, -'.sha256'.length)
const sha256 = shaByName.get(assetName) || createHash('sha256').update(assetName).digest('hex')
shaByName.set(assetName, sha256)
const sidecarPath = join(fixture, name)
const sidecar = `${sha256} ${assetName}\n`
writeFileSync(sidecarPath, sidecar)
return {
name,
size: sidecar.length,
url: pathToFileURL(sidecarPath).toString()
}
}
const assetPath = join(fixture, name)
writeFileSync(assetPath, name)
const sha256 = createHash('sha256').update(name).digest('hex')
shaByName.set(name, sha256)
return {
name,
size: readFileSync(assetPath).length,
url: pathToFileURL(assetPath).toString()
}
})
const releasePath = join(fixture, 'release.json')
writeFileSync(releasePath, JSON.stringify({
tagName: 'v0.5.0',
isDraft: false,
isPrerelease: false,
assets
}, null, 2))
return {
releasePath,
shaFor: (name) => shaByName.get(name)
}
}
function writeCompleteReleaseEvidenceFixture (path) {
writeFileSync(path, `
# Release Smoke Evidence Log
## Run Metadata
| Field | Value |
| --- | --- |
| Operator | Fixture |
| Desktop repo/branch/head | fixture@abc123 |
## Desktop Automated Baseline
| Gate | Expected | Result | Evidence |
| --- | --- | --- | --- |
| Public-trust readiness | all machine gates represented | PASS | fixture command output |
## Announcement Decision
| Question | Answer |
| --- | --- |
| Are all required desktop automated gates PASS? | YES |
| Are all required desktop GUI/user-story gates PASS? | YES |
| Was Peercord trust approved manually and did the standalone window open? | OUT OF SCOPE |
| Are all required mobile automated gates PASS? | OUT OF SCOPE |
| Are production mobile signing/store gates PASS, or explicitly out of announcement scope? | OUT OF SCOPE |
| Are residual risks documented in release notes? | YES |
| Final decision (GO, NO-GO, or GO desktop only) | GO desktop only |
`)
}
function writeBlockedPublicTrustReadinessFixture (path) {
writeFileSync(path, JSON.stringify({
ok: false,
repo: 'example/pearbrowser',
tag: 'v9.9.9',
sourceRef: 'abc123',
mode: 'public-trust',
checks: [
{
id: 'native-signing',
label: 'Native signing credentials',
ok: false,
status: 'block',
summary: 'mode=public-trust; pass=2; warn=0; fail=3',
command: 'node scripts/check-native-signing-credentials.mjs --require-public-trust --json',
blockers: [
'macos-certificate: macOS Developer ID certificate is missing',
'windows-certificate: Windows signing certificate is missing'
],
warnings: []
},
{
id: 'native-downloads',
label: 'Native package byte verification',
ok: true,
status: 'pass',
summary: 'verified=4; errors=0',
command: 'node scripts/verify-native-downloads.mjs --all --json',
blockers: [],
warnings: []
},
{
id: 'release-evidence',
label: 'Operator release evidence log',
ok: false,
status: 'block',
summary: 'passed=38; deferred=3; incomplete=1; failures=0',
command: 'node scripts/check-release-evidence.mjs --json',
blockers: [
'incomplete: Announcement Decision / Final decision (GO, NO-GO, or GO desktop only): answer is blank; final decision is missing'
],
warnings: [
'deferred: Desktop Automated Baseline / Peercord bundle: publisher reseed required'
]
}
],
blockers: [
{ check: 'native-signing', message: 'macos-certificate: macOS Developer ID certificate is missing' },
{ check: 'native-signing', message: 'windows-certificate: Windows signing certificate is missing' },
{ check: 'release-evidence', message: 'incomplete: Announcement Decision / Final decision (GO, NO-GO, or GO desktop only): answer is blank; final decision is missing' }
],
warnings: [
{ check: 'release-evidence', message: 'deferred: Desktop Automated Baseline / Peercord bundle: publisher reseed required' }
]
}, null, 2))
}
function publicTrustSigningEnv () {
return {
PEARBROWSER_MACOS_CERTIFICATE_P12_BASE64: Buffer.from('dummy p12').toString('base64'),
PEARBROWSER_MACOS_CERTIFICATE_PASSWORD: 'secret',
PEARBROWSER_MACOS_SIGNING_IDENTITY: 'Developer ID Application: PearBrowser Desktop (TEAMID)',
PEARBROWSER_MACOS_NOTARY_APPLE_ID: 'release@example.com',
PEARBROWSER_MACOS_NOTARY_PASSWORD: 'secret',
PEARBROWSER_MACOS_NOTARY_TEAM_ID: 'TEAMID',
PEARBROWSER_WINDOWS_CERTIFICATE_PFX_BASE64: Buffer.from('dummy pfx').toString('base64'),
PEARBROWSER_WINDOWS_CERTIFICATE_PASSWORD: 'secret'
}
}
function writeGithubSigningSecretsFixture (path, options = {}) {
const omit = new Set(options.omit || [])
const names = [
'PEARBROWSER_MACOS_CERTIFICATE_P12_BASE64',
'PEARBROWSER_MACOS_CERTIFICATE_PASSWORD',
'PEARBROWSER_MACOS_SIGNING_IDENTITY',
'PEARBROWSER_MACOS_NOTARY_APPLE_ID',
'PEARBROWSER_MACOS_NOTARY_PASSWORD',
'PEARBROWSER_MACOS_NOTARY_TEAM_ID',
'PEARBROWSER_WINDOWS_CERTIFICATE_PFX_BASE64',
'PEARBROWSER_WINDOWS_CERTIFICATE_PASSWORD'
].filter((name) => !omit.has(name))
writeFileSync(path, JSON.stringify(names.map((name) => ({ name })), null, 2))
}
function writeLinuxAppDirFixture (fixture, options = {}) {
const appDir = join(fixture, 'PearBrowser.AppDir')
const metainfoDir = join(appDir, 'usr', 'share', 'metainfo')
mkdirSync(metainfoDir, { recursive: true })
writeFileSync(join(appDir, 'AppRun'), '#!/bin/sh\nexec usr/bin/pearbrowser "$@"\n')
writeFileSync(join(appDir, 'PearBrowser.desktop'), [
'[Desktop Entry]',
'Version=1.0',
'Name=PearBrowser',
'Comment=P2P browser, app store, and publishing platform',
'Exec=pearbrowser',
`Icon=${options.badIcon ? 'wrong-icon' : 'icon'}`,
'Type=Application',
'Categories=Network;',
''
].join('\n'))
writeFileSync(join(appDir, 'icon.png'), 'png bytes')
if (!options.missingMetainfo) {
writeFileSync(join(metainfoDir, 'io.github.bigdestiny2.pearbrowser.metainfo.xml'), linuxMetainfo)
}
return appDir
}
test('Pear stage ignore excludes local release/operator scratch files', () => {
const ignored = pkg.pear?.stage?.ignore || []
assert.ok(ignored.includes('/.landing-seed.mjs'))
assert.ok(ignored.includes('/pearbrowser-storage'))
assert.ok(ignored.includes('/docs'))
assert.ok(ignored.includes('/scripts'))
assert.ok(ignored.includes('/test'))
})
test('PearBrowser catalogue row stays migration-only until its Pear v3 production quorum is released', () => {
const source = catalogSource.apps.find((app) => app.id === 'pearbrowser-desktop')
const seed = SEED_APPS.find((app) => app.name === 'PearBrowser Desktop')
const homepageKey = '03f0060a35451cfb6b68ad1dda1b8474ebb43fd9100071ccf7d67679a83ebb4f'
assert.ok(source, 'catalogue source row missing')
assert.ok(seed, 'offline catalogue seed row missing')
assert.equal(source.version, pkg.version)
assert.equal(source.type, 'standalone')
assert.equal(source.legacyMigrationId, pkg.upgrade.replace(/^pear:\/\//, ''))
assert.deepEqual(source.nativeDelivery, { status: 'migration-required' })
assert.equal(source.driveKey, homepageKey)
assert.equal(source.homepage, `hyper://${homepageKey}/`)
assert.equal(source.link, source.homepage)
assert.equal(seed.version, source.version)
assert.equal(seed.nativeDeliveryStatus, source.nativeDelivery.status)
assert.equal(seed.nativeInstallLink, undefined)
assert.equal(seed.link, source.link)
assert.equal(seed.driveKey, source.driveKey)
assert.equal(seed.homepage, source.homepage)
})
test('release script purges ignored files from previous Pear stages', () => {
assert.match(releaseScript, /native-release preflight/)
assert.match(releaseScript, /no publication authority/)
assert.doesNotMatch(releaseScript, /pear stage/)
assert.doesNotMatch(releaseScript, /pear release/)
})
test('v3 release preflight leaves availability verification to the approved release workflow', () => {
assert.doesNotMatch(releaseScript, /verify-pin\.js/)
assert.match(releaseScript, /independent HiveRelay availability evidence/)
assert.match(verifyPin, /HiveRelayClient/)
assert.match(verifyPin, /proveSeeded/)
assert.match(verifyPin, /verifySeededFallback/)
})
test('foreign-key app pin refuses empty checkouts before broadcasting seed', () => {
assert.match(pinAppOnHiveRelay, /current checkout has 0 file entries/)
assert.match(pinAppOnHiveRelay, /refusing to seed an empty\/unresolved drive/)
assert.match(pinAppOnHiveRelay, /process\.exit\(4\)/)
assert.ok(
pinAppOnHiveRelay.indexOf('if (fileCount === 0)') < pinAppOnHiveRelay.indexOf('client.seed(drive.key'),
'empty-checkout guard must run before HiveRelay seed broadcast'
)
})
test('HiveRelay source install defaults to npm latest packages locked at 0.20.2', () => {
for (const name of npmHiveRelayPackages) {
assert.equal(pkg.dependencies?.[name], 'latest')
const entry = packageLock.packages?.[`node_modules/${name}`]
assert.equal(entry?.version, '0.20.2')
assert.match(entry?.resolved || '', new RegExp(`registry\\.npmjs\\.org/${name}/-/${name}-0\\.20\\.2\\.tgz`))
}
assert.match(hiveRelayLayout, /usesNpmLatestDefaults/)
assert.match(hiveRelayLayout, /verify npm latest resolves to HiveRelay 0\.20\.2/)
assert.match(hiveRelayLayout, /entry\.version !== version/)
assert.match(hiveRelayLayout, /Use latest for standalone installs/)
assert.match(hiveRelayLayout, /process\.exit\(0\)/)
})
test('HiveRelay registry guard is quiet for standalone source installs', () => {
const fixture = realpathSync(mkdtempSync(join(tmpdir(), 'pear-hiverelay-npm-')))
try {
const dependencies = {}
for (const name of npmHiveRelayPackages) dependencies[name] = 'latest'
writeFileSync(join(fixture, 'package.json'), JSON.stringify({ dependencies }, null, 2))
const result = spawnSync(process.execPath, [hiveRelayCheckPath], {
cwd: fixture,
encoding: 'utf8'
})
assert.equal(result.status, 0, result.stderr || result.stdout)
assert.equal(result.stdout, '')
assert.equal(result.stderr, '')
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
test('HiveRelay registry guard fails when the npm release line drifts', () => {
const fixture = realpathSync(mkdtempSync(join(tmpdir(), 'pear-hiverelay-drift-')))
try {
const dependencies = {}
for (const name of npmHiveRelayPackages) dependencies[name] = 'latest'
dependencies['p2p-hiverelay-client'] = '^0.21.0'
writeFileSync(join(fixture, 'package.json'), JSON.stringify({ dependencies }, null, 2))
const result = spawnSync(process.execPath, [hiveRelayCheckPath], {
cwd: fixture,
encoding: 'utf8'
})
assert.equal(result.status, 1)
assert.match(result.stderr, /p2p-hiverelay-client expected latest, found \^0\.21\.0/)
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
test('HiveRelay local workspace guard still fails for missing file dependencies', () => {
const fixture = realpathSync(mkdtempSync(join(tmpdir(), 'pear-hiverelay-local-')))
try {
const dependencies = {}
for (const name of npmHiveRelayPackages) dependencies[name] = `file:../../00-core/hiverelay/packages/${name}`
writeFileSync(join(fixture, 'package.json'), JSON.stringify({ dependencies }, null, 2))
const result = spawnSync(process.execPath, [hiveRelayCheckPath], {
cwd: fixture,
encoding: 'utf8'
})
assert.equal(result.status, 1)
assert.match(result.stderr, /file: HiveRelay dependencies/)
assert.match(result.stderr, /Missing local packages/)
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
test('desktop CI checks out and guards the HiveRelay 0.20.2 release contract', () => {
assert.match(desktopCiWorkflow, /repository: bigdestiny2\/PearBrowser\s+ref: 5c1c920b8b42ffe895f78c49c43d176d5ca93086/)
assert.match(desktopCiWorkflow, /Checkout HiveRelay release contract/)
assert.match(desktopCiWorkflow, /ref: v0\.20\.2/)
assert.match(desktopCiWorkflow, /Guard HiveRelay 0\.20\.2 workspace layout/)
assert.match(desktopCiWorkflow, /pear-ecosystem\/00-core\/hiverelay\/packages\/core\/package\.json/)
assert.match(desktopCiWorkflow, /published to npm/)
assert.match(desktopCiWorkflow, /npm ci/)
assert.doesNotMatch(desktopCiWorkflow, /Checkout HiveRelay workspace packages/)
assert.doesNotMatch(desktopCiWorkflow, /vendor\/hiverelay/)
})
test('RelayClient uses scheme-aware transport for public HTTPS gateways', () => {
assert.match(relayClient, /const https = require\('bare-https'\)/)
assert.match(relayClient, /function relayTransportForUrl/)
assert.match(relayClient, /parsed\.protocol === 'https:' \? 443 : 80/)
assert.match(relayClient, /transport\.get\(relayRequestOptions\(parsed\)/)
assert.match(relayClient, /transport\.request\(\{/)
assert.match(relayClient, /DEFAULT_MAX_RESPONSE_BYTES = 16 \* 1024 \* 1024/)
assert.match(relayClient, /DEFAULT_MAX_CONTROL_RESPONSE_BYTES = 1024 \* 1024/)
})
test('release evidence checker is exposed as an operator script', () => {
assert.equal(pkg.scripts?.['check:release-evidence'], 'node scripts/check-release-evidence.mjs')
})
test('native signing credential checker is exposed as an operator script', () => {
assert.equal(pkg.scripts?.['check:native-signing'], 'node scripts/check-native-signing-credentials.mjs')
assert.match(nativeSigningCheck, /--require-public-trust/)
assert.match(nativeSigningCheck, /--secret-source/)
assert.match(nativeSigningCheck, /gh', \[\s*'secret',\s*'list'/)
assert.match(nativeSigningCheck, /--github-secrets-file/)
assert.match(nativeSigningCheck, /PEARBROWSER_MACOS_CERTIFICATE_P12_BASE64/)
assert.match(nativeSigningCheck, /PEARBROWSER_WINDOWS_CERTIFICATE_PFX_BASE64/)
})
test('native signing secret plan generator is exposed for credential handoff', () => {
assert.equal(pkg.scripts?.['generate:native-signing-secret-plan'], 'node scripts/generate-native-signing-secret-plan.mjs')
assert.match(nativeSigningSecretPlan, /Native Signing Secret Setup/)
assert.match(nativeSigningSecretPlan, /PEARBROWSER_MACOS_CERTIFICATE_P12_BASE64/)
assert.match(nativeSigningSecretPlan, /PEARBROWSER_MACOS_NOTARY_TEAM_ID/)
assert.match(nativeSigningSecretPlan, /PEARBROWSER_WINDOWS_CERTIFICATE_PFX_BASE64/)
assert.match(nativeSigningSecretPlan, /openssl base64 -A/)
assert.match(nativeSigningSecretPlan, /test -s/)
assert.match(nativeSigningSecretPlan, /test -n/)
assert.match(nativeSigningSecretPlan, /check:native-signing/)
assert.match(nativeSigningSecretPlan, /check:public-trust-readiness/)
const markdown = spawnSync(process.execPath, [
nativeSigningSecretPlanPath,
'--repo',
'example/pearbrowser',
'--tag',
'v9.9.9',
'--source-ref',
'abc123'
], {
encoding: 'utf8'
})
assert.equal(markdown.status, 0, markdown.stderr || markdown.stdout)
assert.match(markdown.stdout, /# Native Signing Secret Setup/)
assert.match(markdown.stdout, /Repository: `example\/pearbrowser`/)
assert.match(markdown.stdout, /test -s DeveloperIDApplication\.p12/)
assert.match(markdown.stdout, /openssl base64 -A -in DeveloperIDApplication\.p12/)
assert.match(markdown.stdout, /test -n "\$\{PEARBROWSER_MACOS_CERTIFICATE_PASSWORD:-\}"/)
assert.match(markdown.stdout, /gh secret set PEARBROWSER_WINDOWS_CERTIFICATE_PFX_BASE64 --repo example\/pearbrowser/)
assert.match(markdown.stdout, /npm run check:native-signing -- --require-public-trust --secret-source github --repo example\/pearbrowser/)
assert.match(markdown.stdout, /--source-ref abc123/)
const json = spawnSync(process.execPath, [
nativeSigningSecretPlanPath,
'--repo',
'example/pearbrowser',
'--platform',
'macos',
'--json'
], {
encoding: 'utf8'
})
assert.equal(json.status, 0, json.stderr || json.stdout)
const report = JSON.parse(json.stdout)
assert.equal(report.repo, 'example/pearbrowser')
assert.equal(report.platform, 'macos')
assert.ok(report.requiredSecrets.includes('PEARBROWSER_MACOS_CERTIFICATE_P12_BASE64'))
assert.ok(report.requiredSecrets.includes('PEARBROWSER_MACOS_NOTARY_TEAM_ID'))
assert.ok(report.optionalSecrets.includes('PEARBROWSER_MACOS_KEYCHAIN_PASSWORD'))
assert.ok(!report.requiredSecrets.includes('PEARBROWSER_WINDOWS_CERTIFICATE_PFX_BASE64'))
assert.ok(report.secrets.some((secret) => secret.name === 'PEARBROWSER_MACOS_CERTIFICATE_P12_BASE64' && secret.command.includes('test -s DeveloperIDApplication.p12')))
assert.ok(report.secrets.some((secret) => secret.name === 'PEARBROWSER_MACOS_CERTIFICATE_PASSWORD' && secret.command.includes('test -n "${PEARBROWSER_MACOS_CERTIFICATE_PASSWORD:-}"')))
assert.ok(report.verificationCommands.some((command) => command.includes('check:native-signing')))
})
test('native release asset checker is exposed as an operator script', () => {
assert.equal(pkg.scripts?.['check:native-release-assets'], 'node scripts/check-native-release-assets.mjs')
assert.match(nativeReleaseAssetCheck, /gh', \[\s*'release',\s*'view'/)
assert.match(nativeReleaseAssetCheck, /SHA256SUMS-\$\{escapeRegex\(platform\)\}/)
assert.match(nativeReleaseAssetCheck, /manifest-\$\{escapeRegex\(platform\)\}/)
assert.match(nativeReleaseAssetCheck, /missing SHA-256 sidecar/)
assert.match(nativeReleaseAssetCheck, /--require-published/)
assert.match(nativeReleaseAssetCheck, /--require-public-trust/)
assert.match(nativeReleaseAssetCheck, /--require-backfill-formats/)
assert.match(nativeReleaseAssetCheck, /missing public-trust macOS DMG/)
assert.match(nativeReleaseAssetCheck, /missing required v0\.5\.0 backfill/)
})
test('native release asset resolver is exposed for platform download guidance', () => {
assert.equal(pkg.scripts?.['resolve:native-release'], 'node scripts/resolve-native-release-asset.mjs')
assert.match(nativeReleaseAssetResolver, /normalizePlatform/)
assert.match(nativeReleaseAssetResolver, /artifactRank/)
assert.match(nativeReleaseAssetResolver, /githubReleaseAssetUrl/)
assert.match(nativeReleaseAssetResolver, /missing SHA-256 sidecar/)
})
test('native download verifier is exposed for end-to-end checksum evidence', () => {
assert.equal(pkg.scripts?.['verify:native-downloads'], 'node scripts/verify-native-downloads.mjs')
assert.match(nativeDownloadVerifier, /SUPPORTED_TARGETS/)
assert.match(nativeDownloadVerifier, /readUrlText/)
assert.match(nativeDownloadVerifier, /hashUrl/)
assert.match(nativeDownloadVerifier, /SHA-256 mismatch/)
})
test('Linux AppImage metadata checker is exposed for desktop integration gates', () => {
assert.equal(pkg.scripts?.['check:linux-appimage-metadata'], 'node scripts/check-linux-appimage-metadata.mjs')
assert.match(linuxAppImageMetadata, /PearBrowser\.desktop/)
assert.match(linuxAppImageMetadata, /io\.github\.bigdestiny2\.pearbrowser\.metainfo\.xml/)
assert.match(linuxAppImageMetadata, /metadata_license/)
assert.match(linuxAppImageMetadata, /--build-dir/)
assert.match(linuxAppImageMetadata, /isProductAppImage/)
assert.match(linuxAppImageMetadata, /exactly one PearBrowser AppImage/)
assert.doesNotMatch(linuxAppImageMetadata, /if \(appDirs\.length\) \{[\s\S]*?return\n\s*\}/)
assert.match(linuxMetainfo, /<component type="desktop-application">/)
assert.match(linuxMetainfo, /<launchable type="desktop-id">PearBrowser\.desktop<\/launchable>/)
assert.match(linuxMetainfo, /<binary>pearbrowser<\/binary>/)
})
test('native install snippet generator is exposed for release notes', () => {
assert.equal(pkg.scripts?.['generate:native-install-snippet'], 'node scripts/generate-native-install-snippet.mjs')
assert.equal(pkg.scripts?.['generate:native-install-guide'], 'node scripts/generate-native-install-snippet.mjs --format guide')
assert.match(nativeInstallSnippet, /SUPPORTED_TARGETS/)
assert.match(nativeInstallSnippet, /Native Installers/)
assert.match(nativeInstallSnippet, /artifactRank/)
assert.match(nativeInstallSnippet, /--format/)
assert.match(nativeInstallSnippet, /Install Native Packages/)
assert.match(nativeInstallSnippet, /Trust Note/)
assert.match(nativeInstallGuide, new RegExp(`releases/download/${releaseTagPattern}/PearBrowser-${releaseVersionPattern}-macos-arm64\\.app\\.zip`))
assert.match(nativeInstallGuide, /Apple could not verify that\s+PearBrowser is free of malware/)
assert.match(nativeInstallGuide, /Open Anyway/)
assert.match(nativeInstallGuide, /generate:native-install-guide/)
})
test('native install smoke plan generator is exposed for clean-machine evidence', () => {
assert.equal(pkg.scripts?.['generate:native-install-smoke-plan'], 'node scripts/generate-native-install-smoke-plan.mjs')
assert.match(nativeInstallSmokePlan, /SUPPORTED_TARGETS/)
assert.match(nativeInstallSmokePlan, /Native Clean-Install Smoke Plan/)
assert.match(nativeInstallSmokePlan, /clean host or VM/)
assert.match(nativeInstallSmokePlan, /runtime-rpc-smoke\.mjs/)
assert.match(nativeInstallSmokePlan, /--source-ref/)
assert.match(nativeInstallSmokePlan, /public-trust clean-install smoke requires notarized macOS DMG/)
})
test('origin isolation smoke plan generator is exposed for feature-flagged GUI evidence', () => {
assert.equal(pkg.scripts?.['generate:origin-isolation-smoke-plan'], 'node scripts/generate-origin-isolation-smoke-plan.mjs')
assert.equal(pkg.scripts?.['generate:origin-isolation-smoke-evidence'], 'node scripts/generate-origin-isolation-smoke-evidence.mjs')
assert.match(originIsolationSmokePlan, /PEARBROWSER_PER_DRIVE_ORIGINS=1/)
assert.match(originIsolationSmokePlan, /localStorage/)
assert.match(originIsolationSmokePlan, /indexedDB/)
assert.match(originIsolationSmokePlan, /document\.cookie/)
assert.match(originIsolationSmokePlan, /strict-CSP real app/)
assert.match(originIsolationSmokePlan, /Peerit identity\/sync/)
assert.match(originIsolationSmokePlan, /generate-origin-isolation-smoke-evidence\.mjs/)
assert.match(originIsolationSmokeEvidence, /local-hyperproxy-httpbridge-fixture/)
assert.match(originIsolationSmokeEvidence, /new HyperProxy/)
assert.match(originIsolationSmokeEvidence, /new HttpBridge/)
})
test('origin isolation smoke plan generator emits app-specific acceptance evidence', () => {
const appA = `hyper://${'a'.repeat(64)}/index.html`
const appB = `hyper://${'b'.repeat(64)}/index.html`
const result = spawnSync(process.execPath, [
originIsolationSmokePlanPath,
'--app-a',
appA,
'--app-b',
appB,
'--label-a',
'Peerit fixture',
'--label-b',
'Poked fixture',
'--json'
], {
encoding: 'utf8'
})
assert.equal(result.status, 0, result.stderr || result.stdout)
const report = JSON.parse(result.stdout)
assert.equal(report.ok, true)
assert.equal(report.kind, 'pearbrowser-origin-isolation-smoke-plan')
assert.equal(report.featureFlag, 'PEARBROWSER_PER_DRIVE_ORIGINS=1')
assert.equal(report.apps[0].driveKey, 'a'.repeat(64))
assert.equal(report.apps[1].driveKey, 'b'.repeat(64))
assert.ok(report.commands.some((command) => command.id === 'launch-feature-flagged-desktop'))
assert.ok(report.commands.some((command) => command.command.includes('--homepage-url')))
assert.ok(report.commands.some((command) => command.id === 'automated-origin-isolation-evidence'))
assert.match(report.automatedVerifier.command, /generate:origin-isolation-smoke-evidence/)
assert.match(report.automatedVerifier.validatesWith, /check:origin-isolation-smoke-evidence/)
assert.ok(report.manualSteps.some((step) => step.id === 'origin-split'))
assert.ok(report.manualSteps.some((step) => step.id === 'real-app-bridge'))
assert.equal(report.evidenceTemplate.kind, 'pearbrowser-origin-isolation-smoke-evidence')
assert.equal(report.evidenceTemplate.apps[0].driveKey, 'a'.repeat(64))
assert.equal(report.evidenceTemplate.storage.proofKey, 'pear-origin-isolation-proof')
assert.equal(report.evidenceTemplate.realAppBridge.routes.swarmEvents, true)
assert.match(report.evidenceTemplate.automatedVerifier.command, /generate:origin-isolation-smoke-evidence/)
assert.match(report.snippets.writeStorageInAppA, /localStorage\.setItem/)
assert.match(report.snippets.writeStorageInAppA, /indexedDB\.open/)
assert.match(report.snippets.readStorageInAppB, /document\.cookie/)
assert.ok(report.acceptance.some((item) => item.includes('different `location.origin`')))
const rejected = spawnSync(process.execPath, [
originIsolationSmokePlanPath,
'--app-a',
appA,
'--app-b',
appA,
'--json'
], {
encoding: 'utf8'
})
assert.equal(rejected.status, 2)
assert.match(rejected.stderr, /different drive keys/)
})
test('package-manager manifest generator is exposed for channel expansion drafts', () => {
assert.equal(pkg.scripts?.['generate:package-manager-manifests'], 'node scripts/generate-package-manager-manifests.mjs')
assert.match(packageManagerManifests, /generateHomebrewCask/)
assert.match(packageManagerManifests, /generateWingetSingleton/)
assert.match(packageManagerManifests, /projectLicenseFromPackage/)
assert.match(packageManagerManifests, /InstallerSha256/)
assert.match(packageManagerManifests, /public-trust Homebrew Cask requires notarized macOS DMG/)
})
test('project license metadata is explicit for public package-manager drafts', () => {
assert.equal(pkg.license, 'Apache-2.0 AND MIT')
assert.match(rootLicense, /SPDX-License-Identifier: Apache-2\.0 AND MIT/)
assert.match(rootLicense, /appling\/LICENSE/)
assert.match(rootLicense, /MIT License/)
assert.match(linuxMetainfo, /<project_license>Apache-2\.0 AND MIT<\/project_license>/)
})
test('public-trust readiness checker is exposed as the announcement gate', () => {
assert.equal(pkg.scripts?.['check:public-trust-readiness'], 'node scripts/check-public-trust-readiness.mjs')
assert.match(publicTrustReadiness, /check-native-signing-credentials\.mjs/)
assert.match(publicTrustReadiness, /check-native-release-assets\.mjs/)
assert.match(publicTrustReadiness, /verify-native-downloads\.mjs/)
assert.match(publicTrustReadiness, /check-linux-appimage-metadata\.mjs/)
assert.match(publicTrustReadiness, /generate-native-install-smoke-plan\.mjs/)
assert.match(publicTrustReadiness, /generate-package-manager-manifests\.mjs/)
assert.match(publicTrustReadiness, /check-release-evidence\.mjs/)
assert.match(publicTrustReadiness, /--require-public-trust/)
assert.match(publicTrustReadiness, /--require-published/)
assert.match(publicTrustReadiness, /--require-backfill-formats/)
assert.match(publicTrustReadiness, /--source-ref/)
assert.match(publicTrustReadiness, /--signing-secret-source/)
assert.match(publicTrustReadiness, /--dry-run/)
})
test('public-trust operator report is exposed for release handoff', () => {
assert.equal(pkg.scripts?.['generate:public-trust-operator-report'], 'node scripts/generate-public-trust-operator-report.mjs')
assert.match(publicTrustOperatorReport, /Public-Trust Release Operator Report/)
assert.match(publicTrustOperatorReport, /check-public-trust-readiness\.mjs/)
assert.match(publicTrustOperatorReport, /generate:native-signing-secret-plan/)
assert.match(publicTrustOperatorReport, /gh workflow run desktop-native-release\.yml/)
assert.match(publicTrustOperatorReport, /generate:native-install-smoke-plan/)
assert.match(publicTrustOperatorReport, /check:release-evidence/)
})
test('public-trust operator report formats readiness blockers into next actions', () => {
const fixture = realpathSync(mkdtempSync(join(tmpdir(), 'pear-public-trust-operator-report-')))
try {
const readinessPath = join(fixture, 'readiness.json')
writeBlockedPublicTrustReadinessFixture(readinessPath)
const markdown = spawnSync(process.execPath, [
publicTrustOperatorReportPath,
'--readiness-file',
readinessPath,
'--evidence-file',
'docs/custom-evidence.md'
], {
encoding: 'utf8'
})
assert.equal(markdown.status, 1)
assert.match(markdown.stdout, /# Public-Trust Release Operator Report/)
assert.match(markdown.stdout, /Status: `BLOCKED`/)
assert.match(markdown.stdout, /### Signing Credentials/)
assert.match(markdown.stdout, /\[ \] macos-certificate: macOS Developer ID certificate is missing/)
assert.match(markdown.stdout, /### Operator Evidence/)
assert.match(markdown.stdout, /Final decision is missing/i)
assert.match(markdown.stdout, /npm run -s generate:native-signing-secret-plan -- --repo example\/pearbrowser --tag v9\.9\.9 --source-ref abc123/)
assert.match(markdown.stdout, /gh workflow run desktop-native-release\.yml --repo example\/pearbrowser --ref main -f tag=v9\.9\.9 -f source_ref=abc123 -f release_mode=public-trust/)
assert.match(markdown.stdout, /npm run -s generate:release-evidence-handoff -- --file docs\/custom-evidence\.md/)
assert.match(markdown.stdout, /npm run check:release-evidence -- --file docs\/custom-evidence\.md/)
const json = spawnSync(process.execPath, [
publicTrustOperatorReportPath,
'--readiness-file',
readinessPath,
'--evidence-file',
'docs/custom-evidence.md',
'--json'
], {
encoding: 'utf8'
})
assert.equal(json.status, 1)
const report = JSON.parse(json.stdout)
assert.equal(report.ok, false)
assert.equal(report.repo, 'example/pearbrowser')
assert.equal(report.tag, 'v9.9.9')
assert.equal(report.sourceRef, 'abc123')
assert.equal(report.evidenceFile, 'docs/custom-evidence.md')
assert.ok(report.blockerGroups.some((group) => group.id === 'native-signing' && group.blockers.length === 2))
assert.ok(report.nextCommands.some((command) => command.id === 'dispatch-public-trust-workflow'))
assert.ok(report.nextCommands.some((command) => command.id === 'generate-release-evidence-handoff' && command.command.includes('--file docs/custom-evidence.md')))
assert.ok(report.warnings.some((warning) => warning.check === 'release-evidence'))
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
test('macOS DMG packager is exposed for public-trust native releases', () => {
assert.equal(pkg.scripts?.['package:macos-dmg'], 'node scripts/create-macos-dmg.mjs')
assert.match(macosDmgPackager, /hdiutil/)
assert.match(macosDmgPackager, /notarytool/)
assert.match(macosDmgPackager, /stapler/)
assert.match(macosDmgPackager, /Applications/)
})
test('appling release metadata stays in sync with the production Pear channel', () => {
const productionId = pkg.upgrade.replace(/^pear:\/\//, '')
assert.match(applingCmake, new RegExp(`ID "${productionId}"`))
assert.match(applingCmake, new RegExp(`VERSION ${pkg.version.replace(/\./g, '\\.')}`))
assert.equal(applingPkg.name, 'pearbrowser-desktop-appling')
assert.equal(applingPkg.devDependencies?.['bare-headers'], '1.28.7')
assert.equal(applingPkg.devDependencies?.['bare-make'], '1.8.0')
assert.equal(applingLock.packages?.['node_modules/bare-headers']?.version, '1.28.7')
assert.equal(applingLock.packages?.['node_modules/bare-make']?.version, '1.8.0')
assert.equal(applingPkg.scripts.generate, 'bare-make generate')
assert.equal(applingPkg.scripts.build, 'bare-make build')
assert.equal(pkg.scripts?.['check:appling-release'], 'node scripts/check-appling-release.mjs')
assert.equal(pkg.scripts?.['package:appling'], 'node scripts/collect-appling-artifacts.mjs')
assert.equal(pkg.scripts?.['package:macos-dmg'], 'node scripts/create-macos-dmg.mjs')
assert.match(applingReleaseCheck, /appling CMake ID/)
assert.match(applingReleaseCheck, /release tag must look like vX\.Y\.Z/)
assert.match(applingReleaseCheck, /\['macOS icon', '\.\.\/appling\/assets\/darwin\/icon\.png'\]/)
assert.match(applingReleaseCheck, /\['macOS icns icon', '\.\.\/appling\/assets\/darwin\/icon\.icns'\]/)
assert.match(applingReleaseCheck, /Linux AppStream metainfo/)
assert.match(applingCmake, /PEARBROWSER_BARE_HEADERS_VERSION "1\.28\.7"/)
assert.match(applingCmake, /PEARBROWSER_LINUX_METAINFO/)
assert.match(applingCmake, /function\(configure_pear_appling_linux target\)/)
assert.match(applingCmake, /usr\/share\/metainfo\/io\.github\.bigdestiny2\.pearbrowser\.metainfo\.xml/)
assert.match(applingCmake, /PEARBROWSER_MACOS_SIGNING_IDENTITY\s+"-"\s+CACHE/)
assert.match(applingCmake, /PEARBROWSER_WINDOWS_SIGNING_SUBJECT\s+"CN=PearBrowser Desktop"\s+CACHE/)
assert.match(applingCmake, /WINDOWS_SIGNING_SUBJECT "\$\{PEARBROWSER_WINDOWS_SIGNING_SUBJECT\}"/)
assert.match(applingCmake, /WINDOWS_SIGNING_THUMBPRINT "\$\{PEARBROWSER_WINDOWS_SIGNING_THUMBPRINT\}"/)
assert.match(applingCmake, /function\(download_bare_headers result\)/)
assert.match(applingCmake, /function\(code_sign_macos target\)/)
assert.match(applingCmake, /function\(code_sign_windows target\)/)
assert.match(applingReleaseCheck, /bare-make generate/)
})
test('native release workflow builds and attaches appling artifacts for every desktop OS', () => {
assert.match(nativeReleaseWorkflow, /name: Desktop Native Release/)
assert.match(nativeReleaseWorkflow, /workflow_dispatch:/)
assert.match(nativeReleaseWorkflow, /source_ref:/)
assert.match(nativeReleaseWorkflow, /release_mode:/)
assert.match(nativeReleaseWorkflow, /package-proof/)
assert.match(nativeReleaseWorkflow, /public-trust/)
assert.match(nativeReleaseWorkflow, /SOURCE_REF:/)
assert.match(nativeReleaseWorkflow, /RELEASE_MODE:/)
assert.match(nativeReleaseWorkflow, /ref: \$\{\{ env\.SOURCE_REF \}\}/)
assert.match(nativeReleaseWorkflow, /release:/)
assert.match(nativeReleaseWorkflow, /push:\n\s+tags:/)
assert.match(nativeReleaseWorkflow, /macOS Apple Silicon/)
assert.match(nativeReleaseWorkflow, /macos-15/)
assert.match(nativeReleaseWorkflow, /macOS Intel/)
assert.match(nativeReleaseWorkflow, /macos-15-intel/)
assert.match(nativeReleaseWorkflow, /windows-latest/)
assert.match(nativeReleaseWorkflow, /ubuntu-latest/)
assert.match(nativeReleaseWorkflow, /libgtk-4-dev/)
assert.match(nativeReleaseWorkflow, /core\.longpaths true/)
assert.match(nativeReleaseWorkflow, /MakeAppx\.exe/)
assert.match(nativeReleaseWorkflow, /npm ci --prefix appling/)
assert.doesNotMatch(nativeReleaseWorkflow, /npm install -g bare-make/)
assert.match(nativeReleaseWorkflow, /PEARBROWSER_MACOS_SIGNING_IDENTITY/)
assert.match(nativeReleaseWorkflow, /args=\(--platform "\$RUNNER_OS"\)/)
assert.match(nativeReleaseWorkflow, /--require-public-trust/)
assert.match(nativeReleaseWorkflow, /node scripts\/check-native-signing-credentials\.mjs "\$\{args\[@\]\}"/)
assert.match(nativeReleaseWorkflow, /PEARBROWSER_MACOS_CERTIFICATE_P12_BASE64/)
assert.match(nativeReleaseWorkflow, /PEARBROWSER_MACOS_NOTARY_APPLE_ID/)
assert.match(nativeReleaseWorkflow, /Import macOS signing certificate/)
assert.match(nativeReleaseWorkflow, /node scripts\/notarize-appling-macos\.mjs/)
assert.match(nativeReleaseWorkflow, /Create public-trust macOS DMG/)
assert.match(nativeReleaseWorkflow, /npm run package:macos-dmg -- --tag "\$RELEASE_TAG"/)
assert.match(nativeReleaseWorkflow, /security set-key-partition-list/)
assert.match(nativeReleaseWorkflow, /security delete-keychain/)
assert.match(nativeReleaseWorkflow, /PEARBROWSER_WINDOWS_SIGNING_THUMBPRINT/)
assert.match(nativeReleaseWorkflow, /PEARBROWSER_WINDOWS_CERTIFICATE_PFX_BASE64/)
assert.match(nativeReleaseWorkflow, /Import-PfxCertificate/)
assert.match(nativeReleaseWorkflow, /Sign additional Windows installer artifacts/)
assert.match(nativeReleaseWorkflow, /signtool verify/)
assert.match(nativeReleaseWorkflow, /npm run --prefix appling generate/)
assert.match(nativeReleaseWorkflow, /npm run --prefix appling build/)
assert.match(nativeReleaseWorkflow, /Verify Linux AppImage metadata/)
assert.match(nativeReleaseWorkflow, /check:linux-appimage-metadata -- --build-dir appling\/build/)
assert.match(nativeReleaseWorkflow, /-name 'PearBrowser\.AppImage'/)
assert.match(nativeReleaseWorkflow, /Expected exactly one PearBrowser\.AppImage/)
assert.doesNotMatch(nativeReleaseWorkflow, /AppImage' -type f \| head -n1/)
assert.match(nativeReleaseWorkflow, /actions\/upload-artifact@v4/)
assert.match(nativeReleaseWorkflow, /actions\/download-artifact@v4/)
assert.match(nativeReleaseWorkflow, /release-platform: macos/)
assert.match(nativeReleaseWorkflow, /gh release view "\$RELEASE_TAG"/)
assert.match(nativeReleaseWorkflow, /SHA256SUMS-\$\{platform\}-\*\.txt/)
assert.match(nativeReleaseWorkflow, /Expected at least one SHA256SUMS file/)
assert.match(nativeReleaseWorkflow, /Expected at least one \$\{platform\} backfill artifact matching \$\{pattern\}/)
assert.match(nativeReleaseWorkflow, /Missing SHA-256 sidecar/)
assert.match(nativeReleaseWorkflow, /gh release upload "\$RELEASE_TAG" "\$\{assets\[@\]\}"/)
assert.match(nativeReleaseWorkflow, /Checkout release verifier/)
assert.match(nativeReleaseWorkflow, /check-native-release-assets\.mjs/)
assert.match(nativeReleaseWorkflow, /--require-backfill-formats/)
assert.match(nativeReleaseWorkflow, /Verify public-trust release downloads/)
assert.match(nativeReleaseWorkflow, /verify-native-downloads\.mjs/)
assert.match(nativeReleaseWorkflow, /--require-published/)
assert.match(nativeReleaseWorkflow, /--require-public-trust/)
assert.doesNotMatch(nativeReleaseWorkflow, /gh release create/)
assert.match(nativeReleaseWorkflow, /contents: write/)
})
test('native release workflow defaults manual runs to package proof and public release events to public trust', () => {
assert.match(nativeReleaseWorkflow, /default: package-proof/)
assert.match(nativeReleaseWorkflow, /RELEASE_MODE: \$\{\{ github\.event\.inputs\.release_mode \|\| 'public-trust' \}\}/)
assert.match(nativeReleaseWorkflow, /case "\$RELEASE_MODE" in/)
assert.match(nativeReleaseWorkflow, /if \[\[ "\$RELEASE_MODE" == "public-trust" \]\]; then/)
assert.ok(
nativeReleaseWorkflow.indexOf('args+=(--require-public-trust)') <
nativeReleaseWorkflow.indexOf('node scripts/check-native-signing-credentials.mjs "${args[@]}"'),
'public-trust mode must add the native signing hard gate before running the checker'
)
assert.ok(
nativeReleaseWorkflow.indexOf('args+=(--require-published)') <
nativeReleaseWorkflow.indexOf('check-native-release-assets.mjs "${args[@]}"'),
'public-trust mode must require a published release in the post-upload asset check'
)
assert.ok(
nativeReleaseWorkflow.indexOf('Create public-trust macOS DMG') <
nativeReleaseWorkflow.indexOf('node scripts/collect-appling-artifacts.mjs'),
'public-trust macOS DMG must be created before release artifact collection'
)
assert.ok(
nativeReleaseWorkflow.indexOf("if: env.RELEASE_MODE == 'public-trust'") <
nativeReleaseWorkflow.indexOf('verify-native-downloads.mjs'),
'public-trust mode must run byte-level native download verification after upload'
)
})
test('native signing credential checker separates package proof from public trust gates', () => {
const run = (env, args = []) => {
const result = spawnSync(process.execPath, [
nativeSigningCheckPath,
'--json',
...args
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8',
env
})
return {
...result,
report: result.stdout ? JSON.parse(result.stdout) : null
}
}
const proof = run({})
assert.equal(proof.status, 0)
assert.equal(proof.report.mode, 'package-proof')
assert.equal(proof.report.counts.fail, 0)
assert.ok(proof.report.counts.warn >= 3)
const publicTrustMissing = run({}, ['--require-public-trust'])
assert.notEqual(publicTrustMissing.status, 0)
assert.equal(publicTrustMissing.report.mode, 'public-trust')
assert.ok(publicTrustMissing.report.checks.some((check) => check.id === 'macos-certificate' && check.status === 'fail'))
assert.ok(publicTrustMissing.report.checks.some((check) => check.id === 'macos-notary' && check.status === 'fail'))
assert.ok(publicTrustMissing.report.checks.some((check) => check.id === 'windows-certificate' && check.status === 'fail'))
const macosComplete = run({
PEARBROWSER_MACOS_CERTIFICATE_P12_BASE64: Buffer.from('dummy p12').toString('base64'),
PEARBROWSER_MACOS_CERTIFICATE_PASSWORD: 'secret',
PEARBROWSER_MACOS_SIGNING_IDENTITY: 'Developer ID Application: PearBrowser Desktop (TEAMID)',
PEARBROWSER_MACOS_NOTARY_APPLE_ID: 'release@example.com',
PEARBROWSER_MACOS_NOTARY_PASSWORD: 'secret',
PEARBROWSER_MACOS_NOTARY_TEAM_ID: 'TEAMID'
}, ['--platform', 'macos', '--require-public-trust'])
assert.equal(macosComplete.status, 0)
assert.equal(macosComplete.report.counts.fail, 0)
const windowsComplete = run({
PEARBROWSER_WINDOWS_CERTIFICATE_PFX_BASE64: Buffer.from('dummy pfx').toString('base64'),
PEARBROWSER_WINDOWS_CERTIFICATE_PASSWORD: 'secret'
}, ['--platform', 'windows', '--require-public-trust'])
assert.equal(windowsComplete.status, 0)
assert.equal(windowsComplete.report.counts.fail, 0)
// Azure Trusted Signing is an accepted EV-equivalent alternative to the PFX path.
const windowsAzureComplete = run({
AZURE_TENANT_ID: 'tenant',
AZURE_CLIENT_ID: 'client',
AZURE_CLIENT_SECRET: 'secret',
AZURE_TRUSTED_SIGNING_ENDPOINT: 'https://eus.codesigning.azure.net/',
AZURE_TRUSTED_SIGNING_ACCOUNT: 'pearbrowser-signing',
AZURE_TRUSTED_SIGNING_CERT_PROFILE: 'pearbrowser'
}, ['--platform', 'windows', '--require-public-trust'])
assert.equal(windowsAzureComplete.status, 0)
assert.equal(windowsAzureComplete.report.counts.fail, 0)
assert.ok(windowsAzureComplete.report.checks.some((check) => check.id === 'windows-certificate' && check.status === 'pass'))
const partialAzure = run({
AZURE_TRUSTED_SIGNING_ACCOUNT: 'pearbrowser-signing'
}, ['--platform', 'windows', '--require-public-trust'])
assert.notEqual(partialAzure.status, 0)
assert.ok(partialAzure.report.checks.some((check) => check.id === 'windows-certificate' && check.status === 'fail'))
const partialWindows = run({
PEARBROWSER_WINDOWS_CERTIFICATE_PFX_BASE64: Buffer.from('dummy pfx').toString('base64')
}, ['--platform', 'windows'])
assert.notEqual(partialWindows.status, 0)
assert.ok(partialWindows.report.checks.some((check) => check.id === 'windows-certificate' && check.status === 'fail'))
const fixture = realpathSync(mkdtempSync(join(tmpdir(), 'pear-github-signing-secrets-')))
try {
const secretsPath = join(fixture, 'github-secrets.json')
writeGithubSigningSecretsFixture(secretsPath)
const githubComplete = run({}, [
'--require-public-trust',
'--secret-source',
'github',
'--repo',
'bigdestiny2/pearbrowser-desktop',
'--github-secrets-file',
secretsPath
])
assert.equal(githubComplete.status, 0, githubComplete.stderr || githubComplete.stdout)
assert.equal(githubComplete.report.secretSource, 'github')
assert.equal(githubComplete.report.repo, 'bigdestiny2/pearbrowser-desktop')
assert.equal(githubComplete.report.counts.fail, 0)
assert.ok(githubComplete.report.checks.some((check) => check.id === 'secret-values-unreadable' && check.status === 'warn'))
writeGithubSigningSecretsFixture(secretsPath, { omit: ['PEARBROWSER_MACOS_NOTARY_PASSWORD'] })
const githubIncomplete = run({}, [
'--require-public-trust',
'--secret-source',
'github',
'--github-secrets-file',
secretsPath
])
assert.notEqual(githubIncomplete.status, 0)
assert.ok(githubIncomplete.report.checks.some((check) => check.id === 'macos-notary' && check.status === 'fail'))
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
test('macOS notarization helper submits, staples, and verifies app bundles', () => {
assert.match(macosNotarizeScript, /PEARBROWSER_MACOS_NOTARY_APPLE_ID/)
assert.match(macosNotarizeScript, /notarytool/)
assert.match(macosNotarizeScript, /stapler/)
assert.match(macosNotarizeScript, /codesign/)
assert.match(macosNotarizeScript, /Skipping macOS notarization/)
assert.match(macosNotarizeScript, /--password/)
assert.match(macosNotarizeScript, /\*\*\*\*\*\*\*\*/)
})
test('appling artifact collector emits checksummed release assets', () => {
assert.match(applingArtifactCollector, /createHash\('sha256'\)/)
assert.match(applingArtifactCollector, /\.app\.zip/)
assert.match(applingArtifactCollector, /\.msix/)
assert.match(applingArtifactCollector, /SHA256SUMS-\$\{releasePlatform\}-\$\{arch\}\.txt/)
assert.match(applingArtifactCollector, /\$\{appName\}-\$\{version\}-\$\{releasePlatform\}-\$\{arch\}/)
assert.match(applingArtifactCollector, /no \$\{releasePlatform\} appling artifacts found/)
assert.match(applingArtifactCollector, /release version \$\{version\} does not match package\.json version/)
assert.match(applingArtifactCollector, /refusing to clear unsafe output directory/)
})
test('appling artifact collector emits normalized assets and checksum manifests', () => {
const fixture = realpathSync(mkdtempSync(join(tmpdir(), 'pear-appling-release-')))
try {
const buildDir = join(fixture, 'appling', 'build', 'nested')
mkdirSync(buildDir, { recursive: true })
writeFileSync(join(buildDir, 'PearBrowser Setup.exe'), 'windows installer bytes')
writeFileSync(join(buildDir, 'PearBrowser.exe'), 'raw bare-pear launcher — must be excluded')
writeFileSync(join(buildDir, 'PearBrowser.dmg'), 'wrong platform bytes')
writeFileSync(join(buildDir, 'notes.txt'), 'not a release artifact')
execFileSync(process.execPath, [
applingArtifactCollectorPath,
'--tag',
releaseTag,
'--platform',
'windows',
'--arch',
'X64',
'--build-dir',
join(fixture, 'appling', 'build')
], { cwd: fixture, encoding: 'utf8' })
const outDir = join(fixture, 'dist', 'appling-release', releaseTag, 'windows')
assert.deepEqual(readdirSync(outDir).sort(), [
`PearBrowser-${releaseVersion}-windows-x64.exe`,
`PearBrowser-${releaseVersion}-windows-x64.exe.sha256`,
'SHA256SUMS-windows-x64.txt',
'manifest-windows-x64.json'
])
const sidecar = readFileSync(join(outDir, `PearBrowser-${releaseVersion}-windows-x64.exe.sha256`), 'utf8')
assert.match(sidecar, new RegExp(`^[a-f0-9]{64} PearBrowser-${releaseVersion.replaceAll('.', '\\.')}-windows-x64\\.exe\\n$`))
const sums = readFileSync(join(outDir, 'SHA256SUMS-windows-x64.txt'), 'utf8')
assert.equal(sums, sidecar)
const manifest = JSON.parse(readFileSync(join(outDir, 'manifest-windows-x64.json'), 'utf8'))
assert.equal(manifest.tag, releaseTag)
assert.equal(manifest.version, releaseVersion)
assert.equal(manifest.platform, 'windows')
assert.equal(manifest.arch, 'x64')
assert.equal(manifest.artifacts.length, 1)
assert.equal(manifest.artifacts[0].name, `PearBrowser-${releaseVersion}-windows-x64.exe`)
assert.equal(manifest.artifacts[0].source, 'appling/build/nested/PearBrowser Setup.exe')
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
test('appling artifact collector excludes AppImageTool and packages PearBrowser', () => {
const fixture = realpathSync(mkdtempSync(join(tmpdir(), 'pear-linux-appling-release-')))
try {
const buildDir = join(fixture, 'appling', 'build', 'nested')
mkdirSync(buildDir, { recursive: true })
writeFileSync(join(buildDir, 'appimagetool-x86_64.AppImage'), 'packaging tool bytes')
writeFileSync(join(buildDir, 'PearBrowser.AppImage'), 'pearbrowser product bytes')
execFileSync(process.execPath, [
applingArtifactCollectorPath,
'--tag',
releaseTag,
'--platform',
'linux',
'--arch',
'x64',
'--build-dir',
join(fixture, 'appling', 'build')
], { cwd: fixture, encoding: 'utf8' })
const outDir = join(fixture, 'dist', 'appling-release', releaseTag, 'linux')
const assetName = `PearBrowser-${releaseVersion}-linux-x64.AppImage`
assert.equal(readFileSync(join(outDir, assetName), 'utf8'), 'pearbrowser product bytes')
const manifest = JSON.parse(readFileSync(join(outDir, 'manifest-linux-x64.json'), 'utf8'))
assert.equal(manifest.artifacts.length, 1)
assert.equal(manifest.artifacts[0].source, 'appling/build/nested/PearBrowser.AppImage')
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
test('appling artifact collector refuses unsafe output directories before clearing them', () => {
const fixture = realpathSync(mkdtempSync(join(tmpdir(), 'pear-appling-release-')))
try {
const buildDir = join(fixture, 'appling', 'build')
const unsafeDir = join(fixture, 'unsafe-output')
const sentinel = join(unsafeDir, 'keep.txt')
mkdirSync(buildDir, { recursive: true })
mkdirSync(unsafeDir, { recursive: true })
writeFileSync(join(buildDir, 'PearBrowser.exe'), 'windows installer bytes')
writeFileSync(sentinel, 'do not delete')
const result = spawnSync(process.execPath, [
applingArtifactCollectorPath,
'--tag',
releaseTag,
'--platform',
'windows',
'--arch',
'x64',
'--build-dir',
buildDir,
'--out-dir',
unsafeDir
], { cwd: fixture, encoding: 'utf8' })
assert.notEqual(result.status, 0)
assert.match(result.stderr, /refusing to clear unsafe output directory/)
assert.equal(readFileSync(sentinel, 'utf8'), 'do not delete')
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
test('native release asset checker accepts complete attached asset fixtures', () => {
const fixture = realpathSync(mkdtempSync(join(tmpdir(), 'pear-native-assets-')))
try {
const releasePath = join(fixture, 'release.json')
writeFileSync(releasePath, JSON.stringify({
tagName: 'v0.5.0',
isDraft: false,
isPrerelease: false,
assets: [
'PearBrowser-0.5.0-macos-arm64.app.zip',
'PearBrowser-0.5.0-macos-arm64.app.zip.sha256',
'SHA256SUMS-macos-arm64.txt',
'manifest-macos-arm64.json',
'PearBrowser-0.5.0-macos-x64.app.zip',
'PearBrowser-0.5.0-macos-x64.app.zip.sha256',
'SHA256SUMS-macos-x64.txt',
'manifest-macos-x64.json',
'PearBrowser-0.5.0-windows-x64.msix',
'PearBrowser-0.5.0-windows-x64.msix.sha256',
'PearBrowser-0.5.0-windows-x64-installer.exe',
'PearBrowser-0.5.0-windows-x64-installer.exe.sha256',
'SHA256SUMS-windows-x64.txt',
'manifest-windows-x64.json',
'PearBrowser-0.5.0-linux-x64.AppImage',
'PearBrowser-0.5.0-linux-x64.AppImage.sha256',
'SHA256SUMS-linux-x64.txt',
'manifest-linux-x64.json'
].map((name, i) => ({ name, size: i + 1 }))
}, null, 2))
const result = spawnSync(process.execPath, [
nativeReleaseAssetCheckPath,
'--fixture',
releasePath,
'--tag',
'v0.5.0',
'--require-backfill-formats',
'--json'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8'
})
assert.equal(result.status, 0, result.stderr || result.stdout)
const report = JSON.parse(result.stdout)
assert.equal(report.ok, true)
assert.equal(report.counts.assets, 18)
assert.deepEqual(report.platforms.macos.arches, ['arm64', 'x64'])
assert.equal(report.platforms.macos.artifacts.length, 2)
assert.equal(report.platforms.macos.sums.length, 2)
assert.equal(report.platforms.macos.manifests.length, 2)
assert.equal(report.platforms.windows.artifacts.length, 2)
assert.equal(report.platforms.linux.artifacts.length, 1)
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
test('native release asset checker requires macOS DMGs for public-trust assets', () => {
const fixture = realpathSync(mkdtempSync(join(tmpdir(), 'pear-native-public-trust-')))
try {
const releasePath = join(fixture, 'release.json')
const publicTrustAssets = [
'PearBrowser-0.5.0-macos-arm64.dmg',
'PearBrowser-0.5.0-macos-arm64.dmg.sha256',
'PearBrowser-0.5.0-macos-arm64.app.zip',
'PearBrowser-0.5.0-macos-arm64.app.zip.sha256',
'SHA256SUMS-macos-arm64.txt',
'manifest-macos-arm64.json',
'PearBrowser-0.5.0-macos-x64.dmg',
'PearBrowser-0.5.0-macos-x64.dmg.sha256',
'PearBrowser-0.5.0-macos-x64.app.zip',
'PearBrowser-0.5.0-macos-x64.app.zip.sha256',
'SHA256SUMS-macos-x64.txt',
'manifest-macos-x64.json',
'PearBrowser-0.5.0-windows-x64.exe',
'PearBrowser-0.5.0-windows-x64.exe.sha256',
'SHA256SUMS-windows-x64.txt',
'manifest-windows-x64.json',
'PearBrowser-0.5.0-linux-x64.AppImage',
'PearBrowser-0.5.0-linux-x64.AppImage.sha256',
'SHA256SUMS-linux-x64.txt',
'manifest-linux-x64.json'
]
writeFileSync(releasePath, JSON.stringify({
tagName: 'v0.5.0',
isDraft: false,
isPrerelease: false,
assets: publicTrustAssets.map((name, i) => ({ name, size: i + 1 }))
}, null, 2))
const ok = spawnSync(process.execPath, [
nativeReleaseAssetCheckPath,
'--fixture',
releasePath,
'--tag',
'v0.5.0',
'--require-public-trust',
'--json'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8'
})
assert.equal(ok.status, 0, ok.stderr || ok.stdout)
const okReport = JSON.parse(ok.stdout)
assert.equal(okReport.ok, true)
assert.equal(okReport.platforms.macos.artifacts.filter((name) => name.endsWith('.dmg')).length, 2)
writeFileSync(releasePath, JSON.stringify({
tagName: 'v0.5.0',
isDraft: false,
isPrerelease: false,
assets: publicTrustAssets
.filter((name) => !name.includes('macos-x64.dmg'))
.map((name, i) => ({ name, size: i + 1 }))
}, null, 2))
const missing = spawnSync(process.execPath, [
nativeReleaseAssetCheckPath,
'--fixture',
releasePath,
'--tag',
'v0.5.0',
'--require-public-trust',
'--json'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8'
})
assert.notEqual(missing.status, 0)
const missingReport = JSON.parse(missing.stdout)
assert.ok(missingReport.errors.some((error) => error.includes('missing public-trust macOS DMG for macos/x64')))
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
test('native release asset checker requires v0.5.0 backfill package formats when requested', () => {
const fixture = realpathSync(mkdtempSync(join(tmpdir(), 'pear-native-backfill-formats-')))
try {
const releasePath = join(fixture, 'release.json')
writeFileSync(releasePath, JSON.stringify({
tagName: 'v0.5.0',
isDraft: false,
isPrerelease: false,
assets: [
'PearBrowser-0.5.0-macos-arm64.dmg',
'PearBrowser-0.5.0-macos-arm64.dmg.sha256',
'SHA256SUMS-macos-arm64.txt',
'manifest-macos-arm64.json',
'PearBrowser-0.5.0-windows-x64.exe',
'PearBrowser-0.5.0-windows-x64.exe.sha256',
'SHA256SUMS-windows-x64.txt',
'manifest-windows-x64.json',
'PearBrowser-0.5.0-linux-x64.deb',
'PearBrowser-0.5.0-linux-x64.deb.sha256',
'SHA256SUMS-linux-x64.txt',
'manifest-linux-x64.json'
].map((name, i) => ({ name, size: i + 1 }))
}, null, 2))
const result = spawnSync(process.execPath, [
nativeReleaseAssetCheckPath,
'--fixture',
releasePath,
'--tag',
'v0.5.0',
'--require-backfill-formats',
'--json'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8'
})
assert.notEqual(result.status, 0)
const report = JSON.parse(result.stdout)
assert.equal(report.ok, false)
assert.ok(report.errors.some((error) => error.includes('missing required v0.5.0 backfill macOS .app.zip artifact for macos/arm64')))
assert.ok(report.errors.some((error) => error.includes('missing required v0.5.0 backfill Windows .msix artifact for windows/x64')))
assert.ok(report.errors.some((error) => error.includes('missing required v0.5.0 backfill Linux .AppImage artifact for linux/x64')))
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
test('native release asset resolver chooses the recommended package for each desktop platform', () => {
const fixture = realpathSync(mkdtempSync(join(tmpdir(), 'pear-native-resolver-')))
try {
const releasePath = join(fixture, 'release.json')
writeFileSync(releasePath, JSON.stringify({
tagName: 'v0.5.0',
isDraft: false,
isPrerelease: false,
assets: [
'PearBrowser-0.5.0-macos-arm64.dmg',
'PearBrowser-0.5.0-macos-arm64.dmg.sha256',
'PearBrowser-0.5.0-macos-arm64.app.zip',
'PearBrowser-0.5.0-macos-arm64.app.zip.sha256',
'PearBrowser-0.5.0-windows-x64.msix',
'PearBrowser-0.5.0-windows-x64.msix.sha256',
'PearBrowser-0.5.0-windows-x64.exe',
'PearBrowser-0.5.0-windows-x64.exe.sha256',
'PearBrowser-0.5.0-linux-x64.AppImage',
'PearBrowser-0.5.0-linux-x64.AppImage.sha256'
].map((name, i) => ({
name,
size: i + 1,
url: `https://example.invalid/${name}`
}))
}, null, 2))
const resolve = (platform, arch) => {
const result = spawnSync(process.execPath, [
nativeReleaseAssetResolverPath,
'--fixture',
releasePath,
'--tag',
'v0.5.0',
'--platform',
platform,
'--arch',
arch,
'--json'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8'
})
assert.equal(result.status, 0, result.stderr || result.stdout)
return JSON.parse(result.stdout)
}
assert.equal(resolve('macos', 'arm64').asset.name, 'PearBrowser-0.5.0-macos-arm64.dmg')
assert.equal(resolve('windows', 'x64').asset.name, 'PearBrowser-0.5.0-windows-x64.exe')
assert.equal(resolve('linux', 'x64').asset.name, 'PearBrowser-0.5.0-linux-x64.AppImage')
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
test('native release asset resolver rejects ambiguous equally preferred packages', () => {
const fixture = realpathSync(mkdtempSync(join(tmpdir(), 'pear-native-resolver-ambiguous-')))
try {
const releasePath = join(fixture, 'release.json')
writeFileSync(releasePath, JSON.stringify({
tagName: 'v0.5.0',
isDraft: false,
isPrerelease: false,
assets: [
'PearBrowser-0.5.0-linux-x64.AppImage',
'PearBrowser-0.5.0-linux-x64.AppImage.sha256',
'PearBrowser-0.5.0-linux-x64-PearBrowser.AppImage',
'PearBrowser-0.5.0-linux-x64-PearBrowser.AppImage.sha256'
].map((name, i) => ({ name, size: i + 1 }))
}, null, 2))
const assetCheck = spawnSync(process.execPath, [
nativeReleaseAssetCheckPath,
'--fixture',
releasePath,
'--tag',
'v0.5.0',
'--json'
], { encoding: 'utf8' })
assert.notEqual(assetCheck.status, 0)
assert.ok(JSON.parse(assetCheck.stdout).errors.some((error) => error.includes('expected exactly one Linux .AppImage')))
const result = spawnSync(process.execPath, [
nativeReleaseAssetResolverPath,
'--fixture',
releasePath,
'--tag',
'v0.5.0',
'--platform',
'linux',
'--arch',
'x64',
'--json'
], { encoding: 'utf8' })
assert.notEqual(result.status, 0)
assert.match(JSON.parse(result.stdout).error, /ambiguous linux\/x64 native artifacts/)
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
test('native install snippet generator emits release-note packages for every desktop target', () => {
const fixture = realpathSync(mkdtempSync(join(tmpdir(), 'pear-native-install-snippet-')))
try {
const releasePath = join(fixture, 'release.json')
writeFileSync(releasePath, JSON.stringify({
tagName: 'v0.5.0',
isDraft: false,
isPrerelease: false,
assets: [
'PearBrowser-0.5.0-macos-arm64.dmg',
'PearBrowser-0.5.0-macos-arm64.dmg.sha256',
'PearBrowser-0.5.0-macos-arm64.app.zip',
'PearBrowser-0.5.0-macos-arm64.app.zip.sha256',
'PearBrowser-0.5.0-macos-x64.dmg',
'PearBrowser-0.5.0-macos-x64.dmg.sha256',
'PearBrowser-0.5.0-windows-x64.msix',
'PearBrowser-0.5.0-windows-x64.msix.sha256',
'PearBrowser-0.5.0-windows-x64.exe',
'PearBrowser-0.5.0-windows-x64.exe.sha256',
'PearBrowser-0.5.0-linux-x64.AppImage',
'PearBrowser-0.5.0-linux-x64.AppImage.sha256'
].map((name, i) => ({
name,
size: i + 1,
url: `https://example.invalid/${name}`
}))
}, null, 2))
const json = spawnSync(process.execPath, [
nativeInstallSnippetPath,
'--fixture',
releasePath,
'--tag',
'v0.5.0',
'--trust-mode',
'public-trust',
'--json'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8'
})
assert.equal(json.status, 0, json.stderr || json.stdout)
const report = JSON.parse(json.stdout)
assert.equal(report.ok, true)
assert.equal(report.targets.length, 4)
assert.equal(report.targets.find((target) => target.label === 'macOS Apple Silicon').asset.name, 'PearBrowser-0.5.0-macos-arm64.dmg')
assert.equal(report.targets.find((target) => target.label === 'macOS Intel').asset.name, 'PearBrowser-0.5.0-macos-x64.dmg')
assert.equal(report.targets.find((target) => target.label === 'Windows x64').asset.name, 'PearBrowser-0.5.0-windows-x64.exe')
assert.equal(report.targets.find((target) => target.label === 'Linux x64').asset.name, 'PearBrowser-0.5.0-linux-x64.AppImage')
assert.ok(report.targets.every((target) => target.checksum.name === `${target.asset.name}.sha256`))
assert.ok(report.targets.some((target) => target.install.includes('drag PearBrowser.app to /Applications')))
const markdown = spawnSync(process.execPath, [
nativeInstallSnippetPath,
'--fixture',
releasePath,
'--tag',
'v0.5.0',
'--trust-mode',
'public-trust'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8'
})
assert.equal(markdown.status, 0, markdown.stderr || markdown.stdout)
assert.match(markdown.stdout, /## Native Installers/)
assert.match(markdown.stdout, /PearBrowser-0\.5\.0-macos-arm64\.dmg/)
assert.match(markdown.stdout, /PearBrowser-0\.5\.0-windows-x64\.exe/)
assert.match(markdown.stdout, /These assets are expected to be signed\/notarized/)
const guide = spawnSync(process.execPath, [
nativeInstallSnippetPath,
'--fixture',
releasePath,
'--tag',
'v0.5.0',
'--trust-mode',
'public-trust',
'--format',
'guide'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8'
})
assert.equal(guide.status, 0, guide.stderr || guide.stdout)
assert.match(guide.stdout, /# Install Native Packages/)
assert.match(guide.stdout, /\[PearBrowser-0\.5\.0-macos-arm64\.dmg\]\(https:\/\/example\.invalid\/PearBrowser-0\.5\.0-macos-arm64\.dmg\)/)
assert.match(guide.stdout, /npm run -s generate:native-install-guide/)
assert.match(guide.stdout, /PowerShell/)
assert.doesNotMatch(guide.stdout, /legacy migration record/i)
const packageProofDir = join(fixture, 'package-proof')
mkdirSync(packageProofDir)
const { releasePath: packageProofRelease } = writePackageManagerReleaseFixture(packageProofDir, [
'PearBrowser-0.5.0-macos-arm64.app.zip',
'PearBrowser-0.5.0-macos-arm64.app.zip.sha256',
'PearBrowser-0.5.0-macos-x64.app.zip',
'PearBrowser-0.5.0-macos-x64.app.zip.sha256',
'PearBrowser-0.5.0-windows-x64.msix',
'PearBrowser-0.5.0-windows-x64.msix.sha256',
'PearBrowser-0.5.0-linux-x64.AppImage',
'PearBrowser-0.5.0-linux-x64.AppImage.sha256'
])
const packageProofGuide = spawnSync(process.execPath, [
nativeInstallSnippetPath,
'--fixture',
packageProofRelease,
'--tag',
'v0.5.0',
'--format',
'guide'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8'
})
assert.equal(packageProofGuide.status, 0, packageProofGuide.stderr || packageProofGuide.stdout)
assert.match(packageProofGuide.stdout, /Apple could not verify that PearBrowser is free of malware/)
assert.match(packageProofGuide.stdout, /Control-click `PearBrowser\.app` -> Open -> Open/)
assert.match(packageProofGuide.stdout, /Open Anyway/)
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
test('native install smoke plan generator emits clean-machine commands for every desktop target', () => {
const fixture = realpathSync(mkdtempSync(join(tmpdir(), 'pear-native-install-smoke-plan-')))
try {
const { releasePath } = writePackageManagerReleaseFixture(fixture, [
'PearBrowser-0.5.0-macos-arm64.app.zip',
'PearBrowser-0.5.0-macos-arm64.app.zip.sha256',
'PearBrowser-0.5.0-macos-x64.app.zip',
'PearBrowser-0.5.0-macos-x64.app.zip.sha256',
'PearBrowser-0.5.0-windows-x64.exe',
'PearBrowser-0.5.0-windows-x64.exe.sha256',
'PearBrowser-0.5.0-linux-x64.AppImage',
'PearBrowser-0.5.0-linux-x64.AppImage.sha256'
])
const json = spawnSync(process.execPath, [
nativeInstallSmokePlanPath,
'--fixture',
releasePath,
'--tag',
'v0.5.0',
'--source-ref',
'release-smoke-source',
'--json'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8'
})
assert.equal(json.status, 0, json.stderr || json.stdout)
const report = JSON.parse(json.stdout)
assert.equal(report.ok, true)
assert.equal(report.trustMode, 'package-proof')
assert.equal(report.sourceRef, 'release-smoke-source')
assert.equal(report.runtimeSmokeScript, 'https://raw.githubusercontent.com/bigdestiny2/pearbrowser-desktop/release-smoke-source/scripts/runtime-rpc-smoke.mjs')
assert.equal(report.targets.length, 4)
assert.ok(report.warnings.some((warning) => warning.includes('package-proof clean-install smoke')))
assert.ok(report.targets.find((target) => target.label === 'macOS Apple Silicon').commands.some((command) => command.includes('ditto -x -k')))
assert.ok(report.targets.find((target) => target.label === 'macOS Apple Silicon').commands.some((command) => command.includes('codesign --verify')))
assert.ok(report.targets.find((target) => target.label === 'macOS Apple Silicon').commands.some((command) => command.includes('runtime-rpc-smoke.mjs --timeout 20000 --max-storage-percent 100 --json')))
assert.ok(report.targets.find((target) => target.label === 'Windows x64').commands.some((command) => command.includes('Get-AuthenticodeSignature')))
assert.ok(report.targets.find((target) => target.label === 'Windows x64').commands.some((command) => command.includes('Start menu')))
assert.ok(report.targets.find((target) => target.label === 'Windows x64').commands.some((command) => command.includes('Invoke-WebRequest -Uri') && command.includes('release-smoke-source/scripts/runtime-rpc-smoke.mjs')))
assert.ok(report.targets.find((target) => target.label === 'Windows x64').commands.some((command) => command.includes('node .\\pearbrowser-runtime-rpc-smoke.mjs')))
assert.ok(report.targets.find((target) => target.label === 'Linux x64').commands.some((command) => command.includes('chmod +x')))
assert.ok(report.targets.find((target) => target.label === 'Linux x64').commands.some((command) => command.includes('curl -L -o pearbrowser-runtime-rpc-smoke.mjs')))
assert.ok(report.targets.every((target) => target.evidence.some((item) => item.includes('source checkout'))))
assert.ok(report.targets.every((target) => target.evidence.some((item) => item.includes('runtime-rpc-smoke JSON output'))))
const markdown = spawnSync(process.execPath, [
nativeInstallSmokePlanPath,
'--fixture',
releasePath,
'--tag',
'v0.5.0',
'--source-ref',
'release-smoke-source'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8'
})
assert.equal(markdown.status, 0, markdown.stderr || markdown.stdout)
assert.match(markdown.stdout, /## Native Clean-Install Smoke Plan/)
assert.match(markdown.stdout, /Smoke helper source: \[release-smoke-source\]/)
assert.match(markdown.stdout, /### macOS Apple Silicon/)
assert.match(markdown.stdout, /```powershell/)
assert.match(markdown.stdout, /Evidence to record:/)
const blocked = spawnSync(process.execPath, [
nativeInstallSmokePlanPath,
'--fixture',
releasePath,
'--tag',
'v0.5.0',
'--trust-mode',
'public-trust',
'--json'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8'
})
assert.notEqual(blocked.status, 0)
const blockedReport = JSON.parse(blocked.stdout)
assert.ok(blockedReport.errors.some((error) => error.includes('public-trust clean-install smoke requires notarized macOS DMG')))
const publicTrustDir = join(fixture, 'public-trust')
mkdirSync(publicTrustDir)
const { releasePath: publicTrustRelease } = writePackageManagerReleaseFixture(publicTrustDir, [
'PearBrowser-0.5.0-macos-arm64.dmg',
'PearBrowser-0.5.0-macos-arm64.dmg.sha256',
'PearBrowser-0.5.0-macos-x64.dmg',
'PearBrowser-0.5.0-macos-x64.dmg.sha256',
'PearBrowser-0.5.0-windows-x64.exe',
'PearBrowser-0.5.0-windows-x64.exe.sha256',
'PearBrowser-0.5.0-linux-x64.AppImage',
'PearBrowser-0.5.0-linux-x64.AppImage.sha256'
])
const publicTrust = spawnSync(process.execPath, [
nativeInstallSmokePlanPath,
'--fixture',
publicTrustRelease,
'--tag',
'v0.5.0',
'--trust-mode',
'public-trust',
'--json'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8'
})
assert.equal(publicTrust.status, 0, publicTrust.stderr || publicTrust.stdout)
const publicTrustReport = JSON.parse(publicTrust.stdout)
assert.equal(publicTrustReport.ok, true)
assert.deepEqual(publicTrustReport.warnings, [])
const macos = publicTrustReport.targets.find((target) => target.label === 'macOS Apple Silicon')
assert.ok(macos.commands.some((command) => command.includes('hdiutil attach')))
assert.ok(macos.commands.some((command) => command.includes('xcrun stapler validate')))
assert.ok(macos.commands.some((command) => command.includes('spctl --assess')))
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
test('Linux AppImage metadata checker validates source metadata and AppDir contents', () => {
const source = spawnSync(process.execPath, [
linuxAppImageMetadataPath,
'--json'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8'
})
assert.equal(source.status, 0, source.stderr || source.stdout)
const sourceReport = JSON.parse(source.stdout)
assert.equal(sourceReport.ok, true)
assert.equal(sourceReport.inspections[0].kind, 'source')
const fixture = realpathSync(mkdtempSync(join(tmpdir(), 'pear-linux-appdir-')))
try {
const appDir = writeLinuxAppDirFixture(fixture)
const appdir = spawnSync(process.execPath, [
linuxAppImageMetadataPath,
'--appdir',
appDir,
'--json'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8'
})
assert.equal(appdir.status, 0, appdir.stderr || appdir.stdout)
const appdirReport = JSON.parse(appdir.stdout)
assert.equal(appdirReport.ok, true)
assert.ok(appdirReport.inspections.some((inspection) => inspection.kind === 'appdir'))
const buildDir = join(fixture, 'build')
mkdirSync(buildDir)
const buildAppDir = writeLinuxAppDirFixture(buildDir)
const dependencyFixtureAppDir = join(buildDir, '_deps', 'libappling-src', 'test', 'fixtures', 'app', 'linux-x64', 'Example.AppDir')
mkdirSync(dependencyFixtureAppDir, { recursive: true })
writeFileSync(join(dependencyFixtureAppDir, 'README.txt'), 'dependency fixture, not a release AppDir')
const build = spawnSync(process.execPath, [
linuxAppImageMetadataPath,
'--build-dir',
buildDir,
'--json'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8'
})
assert.equal(build.status, 0, build.stderr || build.stdout)
const buildReport = JSON.parse(build.stdout)
assert.equal(buildReport.ok, true)
assert.ok(buildReport.inspections.some((inspection) => inspection.appDir === buildAppDir))
assert.ok(!buildReport.inspections.some((inspection) => inspection.appDir === dependencyFixtureAppDir))
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
test('Linux AppImage metadata checker blocks AppDirs without AppStream metainfo', () => {
const fixture = realpathSync(mkdtempSync(join(tmpdir(), 'pear-linux-appdir-missing-meta-')))
try {
const appDir = writeLinuxAppDirFixture(fixture, { missingMetainfo: true })
const result = spawnSync(process.execPath, [
linuxAppImageMetadataPath,
'--appdir',
appDir,
'--json'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8'
})
assert.notEqual(result.status, 0)
const report = JSON.parse(result.stdout)
assert.equal(report.ok, false)
assert.ok(report.errors.some((error) => error.includes('AppStream metainfo')))
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
test('package-manager manifest generator emits Homebrew and WinGet drafts from public-trust assets', () => {
const fixture = realpathSync(mkdtempSync(join(tmpdir(), 'pear-package-manifests-')))
try {
const { releasePath, shaFor } = writePackageManagerReleaseFixture(fixture, [
'PearBrowser-0.5.0-macos-arm64.dmg',
'PearBrowser-0.5.0-macos-arm64.dmg.sha256',
'PearBrowser-0.5.0-macos-x64.dmg',
'PearBrowser-0.5.0-macos-x64.dmg.sha256',
'PearBrowser-0.5.0-windows-x64.exe',
'PearBrowser-0.5.0-windows-x64.exe.sha256'
])
const outDir = join(fixture, 'out')
const result = spawnSync(process.execPath, [
packageManagerManifestsPath,
'--fixture',
releasePath,
'--tag',
'v0.5.0',
'--out-dir',
outDir,
'--json'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8'
})
assert.equal(result.status, 0, result.stderr || result.stdout)
const report = JSON.parse(result.stdout)
assert.equal(report.ok, true)
assert.equal(report.trustMode, 'public-trust')
assert.equal(report.files.length, 2)
assert.deepEqual(report.warnings, [])
const caskPath = join(outDir, 'homebrew', 'pearbrowser.rb')
const cask = readFileSync(caskPath, 'utf8')
assert.match(cask, /cask "pearbrowser"/)
assert.match(cask, /arch arm: "arm64", intel: "x64"/)
assert.match(cask, new RegExp(shaFor('PearBrowser-0.5.0-macos-arm64.dmg')))
assert.match(cask, new RegExp(shaFor('PearBrowser-0.5.0-macos-x64.dmg')))
assert.match(cask, /PearBrowser-#\{version\}-macos-#\{arch\}\.dmg/)
assert.match(cask, /app "PearBrowser\.app"/)
const wingetPath = join(outDir, 'winget', 'manifests', 'p', 'PearBrowser', 'PearBrowser', '0.5.0', 'PearBrowser.PearBrowser.yaml')
const winget = readFileSync(wingetPath, 'utf8')
assert.match(winget, /PackageIdentifier: "PearBrowser\.PearBrowser"/)
assert.match(winget, /PackageVersion: "0\.5\.0"/)
assert.match(winget, /License: "Apache-2\.0 AND MIT"/)
assert.match(winget, /InstallerType: exe/)
assert.match(winget, new RegExp(`InstallerSha256: ${shaFor('PearBrowser-0.5.0-windows-x64.exe').toUpperCase()}`))
assert.match(winget, /ManifestType: singleton/)
const customOut = join(fixture, 'custom-out')
const custom = spawnSync(process.execPath, [
packageManagerManifestsPath,
'--fixture',
releasePath,
'--tag',
'v0.5.0',
'--out-dir',
customOut,
'--license',
'MPL-2.0',
'--package-identifier',
'PearBrowser.Experimental',
'--json'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8'
})
assert.equal(custom.status, 0, custom.stderr || custom.stdout)
const customReport = JSON.parse(custom.stdout)
assert.ok(customReport.files.some((file) => {
return file.path.endsWith('winget/manifests/p/PearBrowser/Experimental/0.5.0/PearBrowser.Experimental.yaml')
}))
const customWinget = readFileSync(join(customOut, 'winget', 'manifests', 'p', 'PearBrowser', 'Experimental', '0.5.0', 'PearBrowser.Experimental.yaml'), 'utf8')
assert.match(customWinget, /PackageIdentifier: "PearBrowser\.Experimental"/)
assert.match(customWinget, /License: "MPL-2\.0"/)
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
test('package-manager manifest generator gates package-proof assets by default', () => {
const fixture = realpathSync(mkdtempSync(join(tmpdir(), 'pear-package-proof-manifests-')))
try {
const { releasePath } = writePackageManagerReleaseFixture(fixture, [
'PearBrowser-0.5.0-macos-arm64.app.zip',
'PearBrowser-0.5.0-macos-arm64.app.zip.sha256',
'PearBrowser-0.5.0-macos-x64.app.zip',
'PearBrowser-0.5.0-macos-x64.app.zip.sha256',
'PearBrowser-0.5.0-windows-x64.exe',
'PearBrowser-0.5.0-windows-x64.exe.sha256'
])
const blocked = spawnSync(process.execPath, [
packageManagerManifestsPath,
'--fixture',
releasePath,
'--tag',
'v0.5.0',
'--out-dir',
join(fixture, 'blocked'),
'--json'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8'
})
assert.notEqual(blocked.status, 0)
const blockedReport = JSON.parse(blocked.stdout)
assert.equal(blockedReport.ok, false)
assert.ok(blockedReport.errors.some((error) => error.includes('public-trust Homebrew Cask requires notarized macOS DMG assets')))
const rehearsal = spawnSync(process.execPath, [
packageManagerManifestsPath,
'--fixture',
releasePath,
'--tag',
'v0.5.0',
'--trust-mode',
'package-proof',
'--out-dir',
join(fixture, 'rehearsal'),
'--json'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8'
})
assert.equal(rehearsal.status, 0, rehearsal.stderr || rehearsal.stdout)
const rehearsalReport = JSON.parse(rehearsal.stdout)
assert.equal(rehearsalReport.ok, true)
assert.ok(rehearsalReport.warnings.some((warning) => warning.includes('package-proof manifests are rehearsal artifacts')))
const cask = readFileSync(join(fixture, 'rehearsal', 'homebrew', 'pearbrowser.rb'), 'utf8')
assert.match(cask, /PearBrowser-#\{version\}-macos-#\{arch\}\.app\.zip/)
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
test('public-trust readiness checker passes when all release gates are represented', () => {
const fixture = realpathSync(mkdtempSync(join(tmpdir(), 'pear-public-trust-readiness-')))
try {
const { releasePath } = writePackageManagerReleaseFixture(fixture, [
'PearBrowser-0.5.0-macos-arm64.dmg',
'PearBrowser-0.5.0-macos-arm64.dmg.sha256',
'PearBrowser-0.5.0-macos-arm64.app.zip',
'PearBrowser-0.5.0-macos-arm64.app.zip.sha256',
'SHA256SUMS-macos-arm64.txt',
'manifest-macos-arm64.json',
'PearBrowser-0.5.0-macos-x64.dmg',
'PearBrowser-0.5.0-macos-x64.dmg.sha256',
'PearBrowser-0.5.0-macos-x64.app.zip',
'PearBrowser-0.5.0-macos-x64.app.zip.sha256',
'SHA256SUMS-macos-x64.txt',
'manifest-macos-x64.json',
'PearBrowser-0.5.0-windows-x64.exe',
'PearBrowser-0.5.0-windows-x64.exe.sha256',
'PearBrowser-0.5.0-windows-x64.msix',
'PearBrowser-0.5.0-windows-x64.msix.sha256',
'SHA256SUMS-windows-x64.txt',
'manifest-windows-x64.json',
'PearBrowser-0.5.0-linux-x64.AppImage',
'PearBrowser-0.5.0-linux-x64.AppImage.sha256',
'SHA256SUMS-linux-x64.txt',
'manifest-linux-x64.json'
])
const evidencePath = join(fixture, 'evidence.md')
writeCompleteReleaseEvidenceFixture(evidencePath)
const result = spawnSync(process.execPath, [
publicTrustReadinessPath,
'--fixture',
releasePath,
'--tag',
'v0.5.0',
'--source-ref',
'release-smoke-source',
'--evidence-file',
evidencePath,
'--json'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8',
env: publicTrustSigningEnv()
})
assert.equal(result.status, 0, result.stderr || result.stdout)
const report = JSON.parse(result.stdout)
assert.equal(report.ok, true)
assert.equal(report.mode, 'public-trust')
assert.equal(report.sourceRef, 'release-smoke-source')
assert.equal(report.checks.length, 7)
assert.deepEqual(report.blockers, [])
assert.ok(report.checks.every((check) => check.ok))
assert.match(report.checks.find((check) => check.id === 'native-install-smoke-plan').command, /--source-ref release-smoke-source/)
assert.equal(report.checks.find((check) => check.id === 'linux-appimage-metadata').status, 'pass')
assert.ok(report.checks.some((check) => check.id === 'native-downloads' && check.summary.includes('verified=4')))
assert.deepEqual(report.warnings, [])
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
test('public-trust readiness checker can read signing gate from GitHub Actions secret names', () => {
const fixture = realpathSync(mkdtempSync(join(tmpdir(), 'pear-public-trust-github-secrets-')))
try {
const { releasePath } = writePackageManagerReleaseFixture(fixture, [
'PearBrowser-0.5.0-macos-arm64.dmg',
'PearBrowser-0.5.0-macos-arm64.dmg.sha256',
'PearBrowser-0.5.0-macos-arm64.app.zip',
'PearBrowser-0.5.0-macos-arm64.app.zip.sha256',
'SHA256SUMS-macos-arm64.txt',
'manifest-macos-arm64.json',
'PearBrowser-0.5.0-macos-x64.dmg',
'PearBrowser-0.5.0-macos-x64.dmg.sha256',
'PearBrowser-0.5.0-macos-x64.app.zip',
'PearBrowser-0.5.0-macos-x64.app.zip.sha256',
'SHA256SUMS-macos-x64.txt',
'manifest-macos-x64.json',
'PearBrowser-0.5.0-windows-x64.exe',
'PearBrowser-0.5.0-windows-x64.exe.sha256',
'PearBrowser-0.5.0-windows-x64.msix',
'PearBrowser-0.5.0-windows-x64.msix.sha256',
'SHA256SUMS-windows-x64.txt',
'manifest-windows-x64.json',
'PearBrowser-0.5.0-linux-x64.AppImage',
'PearBrowser-0.5.0-linux-x64.AppImage.sha256',
'SHA256SUMS-linux-x64.txt',
'manifest-linux-x64.json'
])
const evidencePath = join(fixture, 'evidence.md')
const secretsPath = join(fixture, 'github-secrets.json')
writeCompleteReleaseEvidenceFixture(evidencePath)
writeGithubSigningSecretsFixture(secretsPath)
const result = spawnSync(process.execPath, [
publicTrustReadinessPath,
'--fixture',
releasePath,
'--tag',
'v0.5.0',
'--source-ref',
'release-smoke-source',
'--evidence-file',
evidencePath,
'--signing-secret-source',
'github',
'--signing-github-secrets-file',
secretsPath,
'--json'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8',
env: {}
})
assert.equal(result.status, 0, result.stderr || result.stdout)
const report = JSON.parse(result.stdout)
assert.equal(report.ok, true)
const nativeSigning = report.checks.find((check) => check.id === 'native-signing')
assert.equal(nativeSigning.status, 'warn')
assert.match(nativeSigning.command, /--secret-source github/)
assert.match(nativeSigning.command, /--github-secrets-file/)
assert.ok(report.warnings.some((warning) => warning.message.includes('secret-values-unreadable')))
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
test('public-trust readiness checker aggregates package-proof blockers', () => {
const fixture = realpathSync(mkdtempSync(join(tmpdir(), 'pear-public-trust-blocked-')))
try {
const { releasePath } = writePackageManagerReleaseFixture(fixture, [
'PearBrowser-0.5.0-macos-arm64.app.zip',
'PearBrowser-0.5.0-macos-arm64.app.zip.sha256',
'SHA256SUMS-macos-arm64.txt',
'manifest-macos-arm64.json',
'PearBrowser-0.5.0-macos-x64.app.zip',
'PearBrowser-0.5.0-macos-x64.app.zip.sha256',
'SHA256SUMS-macos-x64.txt',
'manifest-macos-x64.json',
'PearBrowser-0.5.0-windows-x64.exe',
'PearBrowser-0.5.0-windows-x64.exe.sha256',
'SHA256SUMS-windows-x64.txt',
'manifest-windows-x64.json',
'PearBrowser-0.5.0-linux-x64.AppImage',
'PearBrowser-0.5.0-linux-x64.AppImage.sha256',
'SHA256SUMS-linux-x64.txt',
'manifest-linux-x64.json'
])
const evidencePath = join(fixture, 'evidence.md')
writeCompleteReleaseEvidenceFixture(evidencePath)
const result = spawnSync(process.execPath, [
publicTrustReadinessPath,
'--fixture',
releasePath,
'--tag',
'v0.5.0',
'--source-ref',
'release-smoke-source',
'--evidence-file',
evidencePath,
'--json'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8',
env: publicTrustSigningEnv()
})
assert.notEqual(result.status, 0)
const report = JSON.parse(result.stdout)
assert.equal(report.ok, false)
assert.equal(report.checks.find((check) => check.id === 'native-signing').status, 'pass')
assert.equal(report.checks.find((check) => check.id === 'native-downloads').status, 'pass')
assert.equal(report.checks.find((check) => check.id === 'linux-appimage-metadata').status, 'pass')
assert.equal(report.checks.find((check) => check.id === 'native-release-assets').status, 'block')
assert.equal(report.checks.find((check) => check.id === 'native-install-smoke-plan').status, 'block')
assert.match(report.checks.find((check) => check.id === 'native-install-smoke-plan').command, /--source-ref release-smoke-source/)
assert.equal(report.checks.find((check) => check.id === 'package-manager-manifests').status, 'block')
assert.ok(report.blockers.some((blocker) => blocker.message.includes('missing public-trust macOS DMG for macos/arm64')))
assert.ok(report.blockers.some((blocker) => blocker.message.includes('public-trust clean-install smoke requires notarized macOS DMG')))
assert.ok(report.blockers.some((blocker) => blocker.message.includes('public-trust Homebrew Cask requires notarized macOS DMG assets')))
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
test('native download verifier checks package bytes against the SHA-256 sidecar', () => {
const fixture = realpathSync(mkdtempSync(join(tmpdir(), 'pear-native-downloads-')))
try {
const assetName = 'PearBrowser-0.5.0-macos-arm64.app.zip'
const assetPath = join(fixture, assetName)
const sidecarPath = join(fixture, `${assetName}.sha256`)
writeFileSync(assetPath, 'native package bytes')
const sha256 = createHash('sha256').update(readFileSync(assetPath)).digest('hex')
writeFileSync(sidecarPath, `${sha256} ${assetName}\n`)
const releasePath = join(fixture, 'release.json')
writeFileSync(releasePath, JSON.stringify({
tagName: 'v0.5.0',
isDraft: false,
isPrerelease: false,
assets: [
{
name: assetName,
size: readFileSync(assetPath).length,
url: pathToFileURL(assetPath).toString()
},
{
name: `${assetName}.sha256`,
size: readFileSync(sidecarPath).length,
url: pathToFileURL(sidecarPath).toString()
}
]
}, null, 2))
const result = spawnSync(process.execPath, [
nativeDownloadVerifierPath,
'--fixture',
releasePath,
'--tag',
'v0.5.0',
'--platform',
'macos',
'--arch',
'arm64',
'--json'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8'
})
assert.equal(result.status, 0, result.stderr || result.stdout)
const report = JSON.parse(result.stdout)
assert.equal(report.ok, true)
assert.equal(report.targets.length, 1)
assert.equal(report.targets[0].asset, assetName)
assert.equal(report.targets[0].sha256, sha256)
writeFileSync(sidecarPath, `${'0'.repeat(64)} ${assetName}\n`)
const mismatch = spawnSync(process.execPath, [
nativeDownloadVerifierPath,
'--fixture',
releasePath,
'--tag',
'v0.5.0',
'--platform',
'macos',
'--arch',
'arm64',
'--json'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8'
})
assert.notEqual(mismatch.status, 0)
const mismatchReport = JSON.parse(mismatch.stdout)
assert.equal(mismatchReport.ok, false)
assert.ok(mismatchReport.errors.some((error) => error.includes('SHA-256 mismatch')))
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
test('native release asset checker fails when an installer sidecar is missing', () => {
const fixture = realpathSync(mkdtempSync(join(tmpdir(), 'pear-native-assets-')))
try {
const releasePath = join(fixture, 'release.json')
writeFileSync(releasePath, JSON.stringify({
tagName: 'v0.5.0',
isDraft: false,
isPrerelease: false,
assets: [
'PearBrowser-0.5.0-macos-arm64.app.zip',
'PearBrowser-0.5.0-macos-arm64.app.zip.sha256',
'SHA256SUMS-macos-arm64.txt',
'manifest-macos-arm64.json',
'PearBrowser-0.5.0-windows-x64.msix',
'SHA256SUMS-windows-x64.txt',
'manifest-windows-x64.json',
'PearBrowser-0.5.0-linux-x64.AppImage',
'PearBrowser-0.5.0-linux-x64.AppImage.sha256',
'SHA256SUMS-linux-x64.txt',
'manifest-linux-x64.json'
].map((name, i) => ({ name, size: i + 1 }))
}, null, 2))
const result = spawnSync(process.execPath, [
nativeReleaseAssetCheckPath,
'--fixture',
releasePath,
'--tag',
'v0.5.0',
'--json'
], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8'
})
assert.notEqual(result.status, 0)
const report = JSON.parse(result.stdout)
assert.equal(report.ok, false)
assert.ok(report.errors.some((error) => error.includes('missing SHA-256 sidecar for PearBrowser-0.5.0-windows-x64.msix')))
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
test('schema-sheets bundle keeps native addons in package context', () => {
assert.match(sheetsBundleScript, /--external:quickbit-native/)
assert.match(sheetsBundleScript, /--external:simdle-native/)
})
test('runtime smoke is authenticated, status-only by default, and does not become the renderer', () => {
assert.match(rpcWebSocketAuth, /\/status-smoke/)
assert.match(mainEntry, /function listenRpcServer/)
assert.match(mainEntry, /http\.createServer/)
assert.match(mainEntry, /authorizeRpcWebSocket/)
assert.match(mainEntry, /DiagnosticRpcRouter/)
assert.match(bootEntry, /function probeBackend/)
assert.match(bootEntry, /diagnosticUrlFor/)
assert.match(bootEntry, /RPC_SESSION_TOKEN/)
assert.match(bootEntry, /CMD_GET_STATUS/)
assert.match(rpcWebSocketAuth, /Diagnostic RPC only allows CMD_GET_STATUS/)
assert.match(rpcWebSocketAuth, /routeBackend/)
assert.match(tabRuntime, /function listenWsServer/)
assert.match(tabRuntime, /http\.createServer/)
assert.match(mainEntry, /onDiagnosticSocket/)
assert.match(mainEntry, /diagnostics\.add\(socket\)/)
assert.match(mainEntry, /diagnostics\.delete\(socket\)/)
assert.match(mainEntry, /teardown\('renderer-ws-close'\)/)
assert.match(mainEntry, /RENDERER_RECONNECT_GRACE_MS/)
assert.match(mainEntry, /renderer reconnected within grace period/)
assert.match(bootEntry, /enableReconnect/)
assert.match(bootEntry, /reconnect-failed/)
assert.doesNotMatch(mainEntry.match(/const onDiagnosticSocket[\s\S]*?\n}\n\nconst onSocket/)?.[0] || '', /teardown\(/)
})
test('runtime smoke asserts backend readiness fields', () => {
assert.match(runtimeSmoke, /\/status-smoke/)
assert.match(runtimeSmoke, /CMD_GET_STATUS = 2/)
assert.match(runtimeSmoke, /status\.dhtConnected !== true/)
assert.match(runtimeSmoke, /status\.proxyPort/)
assert.match(runtimeSmoke, /status\.hiveRelays/)
})
test('runtime smoke can enforce a clean release profile storage ceiling', () => {
assert.match(runtimeSmoke, /--max-storage-percent/)
assert.match(runtimeSmoke, /parseNonNegativeNumber/)
assert.match(runtimeSmoke, /storagePercent exceeds/)
assert.match(runtimeSmoke, /args\.maxStoragePercent/)
})
test('release story smoke covers browse, catalogue, local stories, and opt-in site publishing without launching third-party apps', () => {
assert.equal(pkg.scripts?.['smoke:release-stories'], 'node scripts/release-rpc-story-smoke.mjs')
assert.match(releaseStorySmoke, /CMD_NAVIGATE/)
assert.match(releaseStorySmoke, /CMD_LOAD_CATALOG_BEE/)
assert.match(releaseStorySmoke, /CMD_GET_CATALOG_APPS/)
assert.match(releaseStorySmoke, /--local-stories/)
assert.match(releaseStorySmoke, /--site-story/)
assert.match(releaseStorySmoke, /--desktop-gui-stories/)
assert.match(releaseStorySmoke, /siteStory: false/)
assert.match(releaseStorySmoke, /desktopGuiStories: false/)
assert.match(releaseStorySmoke, /CMD_SEARCH_INDEX/)
assert.match(releaseStorySmoke, /CMD_SEARCH/)
assert.match(releaseStorySmoke, /searchIndexEnabled: true/)
assert.match(releaseStorySmoke, /searchIndexEnabled: false/)
assert.match(releaseStorySmoke, /restoredSearchIndexSetting/)
assert.match(releaseStorySmoke, /CMD_NAME_RESOLVE/)
assert.match(releaseStorySmoke, /CMD_USERDATA_ADD_BOOKMARK/)
assert.match(releaseStorySmoke, /CMD_USERDATA_SAVE_SESSION/)
assert.match(releaseStorySmoke, /CMD_GET_DRIVE_INFO/)
assert.match(releaseStorySmoke, /CMD_CREATE_SITE/)
assert.match(releaseStorySmoke, /CMD_UPDATE_SITE/)
assert.match(releaseStorySmoke, /CMD_PUBLISH_SITE/)
assert.match(releaseStorySmoke, /CMD_DELETE_SITE/)
assert.match(releaseStorySmoke, /runDesktopGuiStories/)
assert.match(releaseStorySmoke, /runNostrTrustedContactStory/)
assert.match(releaseStorySmoke, /buildReleaseEvidence/)
assert.match(releaseStorySmoke, /Browse story/)
assert.match(releaseStorySmoke, /Fresh-launch landing story/)
assert.match(releaseStorySmoke, /Catalogue story/)
assert.match(releaseStorySmoke, /Latest-app-without-download story/)
assert.match(releaseStorySmoke, /Nostr trusted-contact story/)
assert.match(releaseStorySmoke, /Library\/session story/)
assert.match(releaseStorySmoke, /PearBrowser\|Pear Browser/)
assert.match(releaseStorySmoke, /REQUIRED_FEATURED = \['Keet', 'PearPass', 'anonGPT', 'Paste', 'Peercord'\]/)
assert.match(releaseStorySmoke, /PEERCORD_MIGRATION_ID/)
assert.match(releaseStorySmoke, /runMode: 'migration-required'/)
assert.doesNotMatch(releaseStorySmoke, /CMD_LAUNCH_PEAR_LINK/)
assert.doesNotMatch(releaseStorySmoke, /CMD_RUN_APP_IN_TAB/)
})
test('live catalogue verifier asserts PearBrowser release contract and Peercord provenance metadata', () => {
assert.match(liveCatalogVerifier, /PearBrowser version mismatch/)
assert.match(liveCatalogVerifier, /PearBrowser native install link mismatch/)
assert.match(liveCatalogVerifier, /PearBrowser native delivery must be an available Pear v3 release/)
assert.match(liveCatalogVerifier, /PearBrowser migration identity mismatch/)
assert.match(liveCatalogVerifier, /PearBrowser homepage mismatch/)
assert.match(liveCatalogVerifier, /Peercord sourceUrl mismatch/)
assert.match(liveCatalogVerifier, /https:\/\/git\.churchofmalware\.org\/mastercodeon\/Peercord/)
assert.match(liveCatalogVerifier, /Peercord license mismatch/)
assert.match(liveCatalogVerifier, /GPL-3\.0/)
})