// `apple upload --platform macos` once printed nothing for 10 minutes over SSH although the build number was // VALID on App Store Connect. Causes: pkgutil --check-signature / --expand ran unbounded and silent, and the processing // wait polled the IOS platform only, so a Mac build was "not listed yet" for the whole --timeout. These tests keep // every slow step logged and bounded, and an already-VALID build number ends at once (no upload, no wait). import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, setDefaultTimeout, test } from 'bun:test'; import { mkdtempSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { hooks, run as appleUpload } from '../cli/apple/upload'; import { PKG_CHECK_MS, PKG_EXPAND_MS, pkgInfo } from '../cli/lib/mas'; import { HEARTBEAT_MS } from '../cli/lib/progress'; import type { SyncResult } from '../cli/lib/rt'; import { ascEnv, BASE, project, restore } from './helpers/store-mock'; import { installStores, type Stores, uninstallStores, writes } from './helpers/fake-stores'; const SIDE = JSON.stringify({ bundleId: 'com.example.notes', version: '1.2.0', build: '7' }); const files = (): Record => ({ 'build/macos/Example.pkg': 'pkg', 'build/macos/Example.pkg.json': SIDE }); const result = (o: Partial): SyncResult => ({ exitCode: 0, signalCode: null, success: true, stdout: Buffer.alloc(0), stderr: Buffer.alloc(0), ...o }); const timedOut = result({ exitCode: null, signalCode: 'SIGTERM', success: false }); const SIGNED = 'Package "Bare.pkg":\n Status: signed by a certificate trusted by macOS\n Certificate Chain:\n 1. 3rd Party Mac Developer Installer: Example (ABCDE12345)\n Expires: 2030-01-01\n 2. Apple Worldwide Developer Relations Certification Authority\n 3. Apple Root CA\n'; type Seen = { argv: string[]; timeout?: number }; const fake = (seen: Seen[], answer: (argv: string[]) => SyncResult) => ((argv: string[], o: { timeout?: number }) => { seen.push({ argv, timeout: o.timeout }); return answer(argv); }) as never; /** Runs fn with log output captured (log.ts writes through console). */ async function logged(fn: () => Promise): Promise<{ text: string; error: Error | null }> { const out: string[] = []; const log = console.log, err = console.error, warn = console.warn; console.log = console.error = console.warn = (...a: unknown[]) => { out.push(a.map(String).join(' ')); }; let error: Error | null = null; try { await fn(); } catch (e) { error = e as Error; } finally { console.log = log; console.error = err; console.warn = warn; } return { text: out.join('\n'), error }; } describe('pkgInfo: bounded and never silent', () => { test('the signature check has a timeout and a log line; when it times out the .json apple build wrote is used', () => { const p = project(BASE, files()); const said: string[] = [], seen: Seen[] = []; const info = pkgInfo(join(p.root, 'build/macos/Example.pkg'), 'com.example.notes', { say: m => said.push(m), spawnSync: fake(seen, () => timedOut) }); expect(info).toEqual({ bundleId: 'com.example.notes', version: '1.2.0', build: '7' }); expect(seen).toHaveLength(1); expect(seen[0]!.timeout).toBe(PKG_CHECK_MS); expect(said[0]).toContain('pkgutil --check-signature'); expect(said.at(-1)).toContain('gave no answer'); expect(said.at(-1)).toContain('Example.pkg.json'); }); test('the expand is bounded too; a timeout without the .json file is an error that names the cause', () => { const p = project(BASE, { 'build/macos/Bare.pkg': 'pkg' }); const said: string[] = [], seen: Seen[] = []; const spawnSync = fake(seen, argv => (argv[1] === '--check-signature' ? result({ stdout: Buffer.from(SIGNED) }) : timedOut)); let error: Error | null = null; try { pkgInfo(join(p.root, 'build/macos/Bare.pkg'), undefined, { say: m => said.push(m), spawnSync, expandMs: 5000 }); } catch (e) { error = e as Error; } expect(error?.message).toContain('pkgutil --expand gave no answer'); expect(error?.message).toContain('Bare.pkg.json'); expect(seen.map(x => x.argv[1])).toEqual(['--check-signature', '--expand']); expect(seen[1]!.timeout).toBe(5000); expect(said.some(m => m.includes('pkgutil --expand'))).toBe(true); expect(PKG_EXPAND_MS).toBeGreaterThan(PKG_CHECK_MS); }); }); describe('apple upload of a .pkg (fake stores)', () => { setDefaultTimeout(30_000); let s: Stores; const path = process.env.PATH; beforeAll(() => { ascEnv(); }); afterAll(restore); beforeEach(() => { s = installStores(); process.env.PATH = mkdtempSync(join(tmpdir(), 'sb-nopkgutil-')); }); // no pkgutil: the .json is read afterEach(() => { uninstallStores(); process.env.PATH = path; hooks.heartbeat = undefined; }); test('an already VALID build number ends at once: clear message, no upload, no wait, every slow step logged first', async () => { s.asc.put({ type: 'builds', id: 'b-7', attributes: { version: '7', processingState: 'VALID' }, rels: { app: '6400000101' } }); const p = project(BASE, files()); const started = Date.now(); const r = await logged(() => appleUpload(p.ctx(['build/macos/Example.pkg', '--platform', 'macos', '--yes']))); expect(Date.now() - started).toBeLessThan(10_000); expect(r.error?.message).toContain('already on App Store Connect (VALID)'); expect(writes(s.calls)).toEqual([]); expect(s.calls.some(c => decodeURIComponent(c.path).includes('preReleaseVersion.platform'))).toBe(false); // no processing poll expect(s.asc.parts).toEqual([]); expect(r.text).toContain('reading the package'); expect(r.text).toContain('asking App Store Connect'); expect(r.text.indexOf('reading the package')).toBeLessThan(r.text.indexOf('asking App Store Connect')); }); test('--allow-existing on a Mac build: the wait polls MAC_OS and prints a heartbeat while a poll is in flight', async () => { s.asc.put({ type: 'builds', id: 'b-7', attributes: { version: '7', processingState: 'VALID' }, rels: { app: '6400000101' } }); const p = project(BASE, files()); let beat: (() => void) | null = null; hooks.heartbeat = { everyMs: 1000, ticker: f => { beat = f; return () => { beat = null; }; } }; const inner = globalThis.fetch, polls: string[] = []; globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => { if (decodeURIComponent(String(input)).includes('preReleaseVersion.platform')) { (beat as (() => void) | null)?.(); // the poll is in flight: time passes polls.push(decodeURIComponent(String(input))); // the fake does not implement include=: answer the way App Store Connect does return new Response(JSON.stringify({ data: [{ type: 'builds', id: 'b-7', attributes: { version: '7', processingState: 'VALID' }, relationships: { preReleaseVersion: { data: { type: 'preReleaseVersions', id: 'pv-1' } } } }], included: [{ type: 'preReleaseVersions', id: 'pv-1', attributes: { version: '1.2.0' } }] }), { status: 200 }); } return inner(input as never, init); }) as typeof fetch; const r = await logged(() => appleUpload(p.ctx(['build/macos/Example.pkg', '--platform', 'macos', '--yes', '--allow-existing']))); expect(r.error).toBeNull(); expect(r.text).toMatch(/still waiting for build 7: first check/); expect(r.text).toContain('build 7 is VALID'); expect(polls).toHaveLength(1); expect(polls[0]).toContain('filter[preReleaseVersion.platform]=MAC_OS'); expect(writes(s.calls)).toEqual([]); }); test('the default heartbeat is at least once a minute', () => { expect(HEARTBEAT_MS).toBeLessThanOrEqual(60_000); }); });