From 09673f699421ddda1ddee59c1232019addd14cab Mon Sep 17 00:00:00 2001 From: cc Date: Mon, 27 Jul 2026 16:00:36 +0100 Subject: [PATCH 1/6] fix: repair packaged settings and arm64 signing --- .github/workflows/build-desktop.yml | 1 + package-lock.json | 4 +- package.json | 6 ++- scripts/after-pack.mjs | 22 +++++++++ scripts/after-pack.test.mjs | 47 +++++++++++++++++++ scripts/verify-desktop-release-config.mjs | 3 ++ .../verify-desktop-release-config.test.mjs | 11 +++++ scripts/verify-release-artifacts.test.mjs | 26 ++++++---- src/app/AppShell.test.tsx | 13 +++++ src/app/AppShell.tsx | 13 +++-- src/app/rendererRole.test.ts | 22 +++++---- src/app/rendererRole.ts | 7 ++- 12 files changed, 149 insertions(+), 26 deletions(-) create mode 100644 scripts/after-pack.mjs create mode 100644 scripts/after-pack.test.mjs diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 9c2264c..487d14b 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -25,6 +25,7 @@ jobs: - run: npm run test:brand-assets - run: npm run test:desktop-workflow - run: npm run test:electron + - run: npm run test:mac-signing - run: npm run test:release-artifacts - run: npm run test:release-tag - run: npm run test:packaged-resources diff --git a/package-lock.json b/package-lock.json index 25a4106..4bcbac0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codex-pet-pause", - "version": "0.2.0", + "version": "0.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codex-pet-pause", - "version": "0.2.0", + "version": "0.2.1", "license": "MIT", "dependencies": { "@zip.js/zip.js": "^2.8.34", diff --git a/package.json b/package.json index 0740417..2757bb1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "codex-pet-pause", "private": true, - "version": "0.2.0", + "version": "0.2.1", "description": "A playful, local-first break reminder PWA with interactive and Codex-compatible pets.", "license": "MIT", "repository": { @@ -28,6 +28,7 @@ "test": "vitest", "test:run": "vitest run", "test:electron": "vitest run --config vitest.electron.config.js", + "test:mac-signing": "node --test scripts/after-pack.test.mjs", "test:e2e": "playwright test", "check:release-dist": "node scripts/verify-release-dist.mjs", "test:verify-release-dist": "node scripts/test-verify-release-dist.mjs", @@ -62,7 +63,7 @@ "test:release-tag": "node --test scripts/verify-release-tag.test.mjs", "test:packaged-resources": "node --test scripts/verify-packaged-resources.test.mjs", "test:packaged-app": "node --test scripts/verify-packaged-app.test.mjs", - "check:desktop-release": "npm run test:desktop-release-config && npm run test:brand-assets && npm run test:desktop-workflow && npm run test:electron && npm run test:release-artifacts && npm run test:release-tag && npm run test:packaged-resources && npm run test:packaged-app && npm run check:desktop-release-config && npm run check:desktop-workflow && npm run typecheck && npm run test:run && npm run build && npm run test:e2e && npm run desktop:build && npm run desktop:smoke:packaged-resources" + "check:desktop-release": "npm run test:desktop-release-config && npm run test:brand-assets && npm run test:desktop-workflow && npm run test:electron && npm run test:mac-signing && npm run test:release-artifacts && npm run test:release-tag && npm run test:packaged-resources && npm run test:packaged-app && npm run check:desktop-release-config && npm run check:desktop-workflow && npm run typecheck && npm run test:run && npm run build && npm run test:e2e && npm run desktop:build && npm run desktop:smoke:packaged-resources" }, "dependencies": { "@zip.js/zip.js": "^2.8.34", @@ -100,6 +101,7 @@ "appId": "io.elevenlabs.codexpetpause", "productName": "Codex Pet Pause", "asar": true, + "afterPack": "scripts/after-pack.mjs", "files": [ "dist/**/*", "electron/**/*", diff --git a/scripts/after-pack.mjs b/scripts/after-pack.mjs new file mode 100644 index 0000000..8832adc --- /dev/null +++ b/scripts/after-pack.mjs @@ -0,0 +1,22 @@ +import { execFile as execFileCallback } from 'node:child_process'; +import { join } from 'node:path'; +import { promisify } from 'node:util'; +import { Arch } from 'builder-util'; + +const execFileAsync = promisify(execFileCallback); + +export async function afterPack( + context, + { execFile = execFileAsync } = {}, +) { + if (context.electronPlatformName !== 'darwin' || context.arch !== Arch.arm64) { + return; + } + + const appPath = join( + context.appOutDir, + `${context.packager.appInfo.productFilename}.app`, + ); + await execFile('xattr', ['-cr', appPath]); + await execFile('codesign', ['--force', '--deep', '--sign', '-', appPath]); +} diff --git a/scripts/after-pack.test.mjs b/scripts/after-pack.test.mjs new file mode 100644 index 0000000..a62ab01 --- /dev/null +++ b/scripts/after-pack.test.mjs @@ -0,0 +1,47 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { Arch } from 'builder-util'; +import { afterPack } from './after-pack.mjs'; + +function context(electronPlatformName, arch) { + return { + electronPlatformName, + arch, + appOutDir: '/tmp/codex-pet-output', + packager: { + appInfo: { + productFilename: 'Codex Pet Pause', + }, + }, + }; +} + +test('deep-signs only the unpacked macOS ARM64 application', async () => { + const calls = []; + const execFile = async (...args) => { + calls.push(args); + }; + + await afterPack(context('darwin', Arch.arm64), { execFile }); + + assert.deepEqual(calls, [ + ['xattr', ['-cr', '/tmp/codex-pet-output/Codex Pet Pause.app']], + [ + 'codesign', + ['--force', '--deep', '--sign', '-', '/tmp/codex-pet-output/Codex Pet Pause.app'], + ], + ]); +}); + +test('does not change macOS x64, Windows, or Linux packages', async () => { + const calls = []; + const execFile = async (...args) => { + calls.push(args); + }; + + await afterPack(context('darwin', Arch.x64), { execFile }); + await afterPack(context('win32', Arch.x64), { execFile }); + await afterPack(context('linux', Arch.x64), { execFile }); + + assert.deepEqual(calls, []); +}); diff --git a/scripts/verify-desktop-release-config.mjs b/scripts/verify-desktop-release-config.mjs index db31c0d..9674317 100644 --- a/scripts/verify-desktop-release-config.mjs +++ b/scripts/verify-desktop-release-config.mjs @@ -31,6 +31,9 @@ function hasExactlyArchitectures(architectures, expectedArchitectures) { export function verifyDesktopReleaseConfig(packageJson, fileExists = existsSync) { const failures = []; const build = packageJson.build ?? {}; + if (build.afterPack !== 'scripts/after-pack.mjs') { + failures.push('build.afterPack must preserve the macOS ARM64 signing repair'); + } if ( packageJson.author?.name !== expected.authorName || packageJson.author?.email !== expected.authorEmail diff --git a/scripts/verify-desktop-release-config.test.mjs b/scripts/verify-desktop-release-config.test.mjs index e9dfdd8..0ea69f1 100644 --- a/scripts/verify-desktop-release-config.test.mjs +++ b/scripts/verify-desktop-release-config.test.mjs @@ -14,6 +14,7 @@ const validPackage = { build: { appId: 'io.elevenlabs.codexpetpause', productName: 'Codex Pet Pause', + afterPack: 'scripts/after-pack.mjs', mac: { icon: 'build/icons/icon.icns', target: [{ target: 'dmg' }], @@ -53,6 +54,16 @@ test('accepts the approved desktop release contract', () => { ); }); +test('requires the ARM64 signing repair hook', () => { + const missingHook = structuredClone(validPackage); + delete missingHook.build.afterPack; + + assert.ok( + verifyDesktopReleaseConfig(missingHook, () => true) + .some((failure) => failure.includes('afterPack')), + ); +}); + test('requires the approved privacy-preserving Linux maintainer identity', () => { const missingAuthorEmail = structuredClone(validPackage); delete missingAuthorEmail.author.email; diff --git a/scripts/verify-release-artifacts.test.mjs b/scripts/verify-release-artifacts.test.mjs index cc6fa3e..365710d 100644 --- a/scripts/verify-release-artifacts.test.mjs +++ b/scripts/verify-release-artifacts.test.mjs @@ -1,5 +1,6 @@ import assert from 'node:assert/strict'; import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { readFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { spawnSync } from 'node:child_process'; @@ -24,6 +25,15 @@ const platformSidecars = { ], }; const invalidLinuxSidecar = 'Codex-Pet-Pause-0.2.0-linux-x64.AppImage.blockmap'; +const currentVersion = JSON.parse( + readFileSync(new URL('../package.json', import.meta.url), 'utf8'), +).version; +const currentInstallerSet = completeInstallerSet.map( + (fileName) => fileName.replace('0.2.0', currentVersion), +); +const currentMacSidecars = platformSidecars.mac.map( + (fileName) => fileName.replace('0.2.0', currentVersion), +); test('accepts one complete 0.2.0 installer set', () => { assert.deepEqual(verifyReleaseArtifacts(completeInstallerSet, '0.2.0'), []); @@ -185,7 +195,7 @@ test('the executable --platform option validates only that platform', async () = const script = fileURLToPath(new URL('./verify-release-artifacts.mjs', import.meta.url)); try { - for (const fileName of completeInstallerSet.slice(0, 2)) { + for (const fileName of currentInstallerSet.slice(0, 2)) { await writeFile(join(directory, fileName), 'package'); } @@ -209,8 +219,8 @@ test('the executable --target option validates only one isolated matrix target', const script = fileURLToPath(new URL('./verify-release-artifacts.mjs', import.meta.url)); try { - await writeFile(join(directory, completeInstallerSet[0]), 'package'); - await writeFile(join(directory, platformSidecars.mac[0]), 'sidecar'); + await writeFile(join(directory, currentInstallerSet[0]), 'package'); + await writeFile(join(directory, currentMacSidecars[0]), 'sidecar'); await writeFile(join(directory, 'builder-effective-config.yaml'), 'metadata'); const targetResult = spawnSync( @@ -237,15 +247,15 @@ test('the executable rejects zero-byte and non-regular expected artifacts', asyn const script = fileURLToPath(new URL('./verify-release-artifacts.mjs', import.meta.url)); try { - await writeFile(join(directory, completeInstallerSet[0]), ''); - await mkdir(join(directory, completeInstallerSet[1])); + await writeFile(join(directory, currentInstallerSet[0]), ''); + await mkdir(join(directory, currentInstallerSet[1])); const result = spawnSync(process.execPath, [script, directory, '--platform', 'mac'], { encoding: 'utf8', }); assert.notEqual(result.status, 0); - assert.match(result.stderr, /Codex-Pet-Pause-0\.2\.0-mac-arm64\.dmg/); - assert.match(result.stderr, /Codex-Pet-Pause-0\.2\.0-mac-x64\.dmg/); + assert.ok(result.stderr.includes(currentInstallerSet[0])); + assert.ok(result.stderr.includes(currentInstallerSet[1])); } finally { await rm(directory, { recursive: true, force: true }); } @@ -256,7 +266,7 @@ test('the executable rejects an unexpected zero-byte stale package', async () => const script = fileURLToPath(new URL('./verify-release-artifacts.mjs', import.meta.url)); try { - for (const fileName of completeInstallerSet.slice(0, 2)) { + for (const fileName of currentInstallerSet.slice(0, 2)) { await writeFile(join(directory, fileName), 'package'); } await writeFile(join(directory, 'Codex-Pet-Pause-0.1.0-mac-x64.dmg'), ''); diff --git a/src/app/AppShell.test.tsx b/src/app/AppShell.test.tsx index 291fc74..d4f9036 100644 --- a/src/app/AppShell.test.tsx +++ b/src/app/AppShell.test.tsx @@ -60,6 +60,19 @@ test('provides a skip link, status area, and navigation between four focused vie expect(screen.getByRole('button', { name: '摸摸 Momo' })).toBe(cat); }); +test('desktop settings route keeps the full shell, opens settings, and hides the duplicate pet', () => { + window.history.replaceState({}, '', '/?mode=web&view=settings&hidePet=1'); + const controller = createAppController(createFakeDependencies({ now: 0 })); + + renderShell(controller); + + expect(screen.getByRole('navigation', { name: '主要导航' })).toBeVisible(); + expect(screen.getByRole('button', { name: '设置' })).toHaveAttribute('aria-current', 'page'); + expect(screen.getByRole('heading', { name: '设置' })).toBeVisible(); + expect(screen.queryByRole('button', { name: '摸摸 Momo' })).not.toBeInTheDocument(); + window.history.replaceState({}, '', '/'); +}); + test('returns the viewport to the top when the shell opens and views change', async () => { const user = userEvent.setup(); const scrollTo = vi.spyOn(window, 'scrollTo').mockImplementation(() => undefined); diff --git a/src/app/AppShell.tsx b/src/app/AppShell.tsx index a5f194e..0b7e678 100644 --- a/src/app/AppShell.tsx +++ b/src/app/AppShell.tsx @@ -15,7 +15,10 @@ type AppView = 'companion' | 'reminders' | 'pet' | 'settings'; export function AppShell() { const { t } = useI18n(); - const [activeView, setActiveView] = useState('companion'); + const launchQuery = new URLSearchParams(window.location.search); + const hidePetFromHost = launchQuery.get('hidePet') === '1'; + const initialView: AppView = launchQuery.get('view') === 'settings' ? 'settings' : 'companion'; + const [activeView, setActiveView] = useState(initialView); const snapshot = useAppSnapshot(); const pwaSnapshot = useSyncExternalStore(pwaStatus.subscribe, pwaStatus.getSnapshot, pwaStatus.getSnapshot); const enabledReminderCount = snapshot.settings.reminders.filter((reminder) => reminder.enabled).length; @@ -60,9 +63,11 @@ export function AppShell() { offlineReady={pwaSnapshot.offlineReady} /> - {activeImportedPet === undefined - ? - : } + {!hidePetFromHost && ( + activeImportedPet === undefined + ? + : + )}