import { describe, expect, test } from 'bun:test'; import type { ProjectSummary } from '../cli/lib/desktop-protocol'; import { appIdentity, applePhase, appleProductIds, appleReleases, catalogParity, cmpVersion, phasedFraction, playProductIds, playReleases, releaseTrains, versionOfName } from '../cli/lib/portfolio'; const summary: ProjectSummary = { dir: '/games/demo', config: '/games/demo/storebound.config.ts', name: 'Demo', languages: ['en'], play: { packageName: 'com.example.demo' }, apple: { bundleId: 'com.example.demo', appId: null }, platforms: ['ios', 'android'], products: 2, productIds: ['coins', 'no_ads'], subscriptions: 1, subscriptionIds: ['pro_monthly'], catalog: { ios: ['coins', 'no_ads', 'pro_monthly'], android: ['coins', 'no_ads', 'pro_monthly'] }, shots: false, testflightGroups: 0, }; const apple = { versions: [ { attributes: { versionString: '2.3.0', appVersionState: 'WAITING_FOR_REVIEW', platform: 'IOS' }, relationships: { build: { data: { id: 'b2' } } } }, { attributes: { versionString: '2.2.1', appStoreState: 'READY_FOR_SALE', platform: 'IOS' }, relationships: { build: { data: { id: 'b1' } } } }, { attributes: { versionString: '9.9.9', appStoreState: 'READY_FOR_SALE', platform: 'TV_OS' } }, ], builds: [{ id: 'b1', attributes: { version: '41' } }, { id: 'b2', attributes: { version: '45' } }], iaps: [{ attributes: { productId: 'coins' } }, { attributes: { productId: 'ios_only' } }], }; const play = { tracks: [ { track: 'production', releases: [{ name: '2.2.1 (220)', versionCodes: ['220'], status: 'completed' }, { name: '2.3.0', versionCodes: ['230', '231'], status: 'inProgress', userFraction: 0.2 }] }, { track: 'internal', releases: [{ name: '2.4.0', versionCodes: ['240'], status: 'completed' }] }, ], products: [{ productId: 'coins' }, { productId: 'no_ads' }, { sku: 'android_only' }], }; describe('portfolio (apps, release trains, catalog parity)', () => { test('identity: one app, both platforms', () => { expect(appIdentity(summary)).toEqual({ id: '/games/demo', name: 'Demo', platforms: { ios: { bundleId: 'com.example.demo', ascAppId: null }, android: { packageName: 'com.example.demo' } } }); expect(appIdentity({ ...summary, apple: null }).platforms.ios).toBeNull(); expect(appIdentity({ ...summary, platforms: ['android'] }).platforms.ios).toBeNull(); }); test('versions and phases', () => { expect(cmpVersion('2.10.0', '2.9.1')).toBeGreaterThan(0); expect(cmpVersion('2.3', '2.3.0')).toBe(0); expect(versionOfName('2.3.0 (45)')).toBe('2.3.0'); expect(versionOfName(undefined)).toBeNull(); expect(applePhase('READY_FOR_SALE')).toBe('live'); expect(applePhase('PENDING_DEVELOPER_RELEASE')).toBe('approved'); expect(applePhase('METADATA_REJECTED')).toBe('rejected'); expect(applePhase('PREPARE_FOR_SUBMISSION')).toBe('draft'); expect(applePhase('SOMETHING_NEW')).toBe('other'); }); test('iOS phased release: day -> share, paused -> halted, complete -> live', () => { expect(phasedFraction({ state: 'ACTIVE', currentDayNumber: 5 })).toBe(0.2); expect(phasedFraction({ state: 'ACTIVE', currentDayNumber: 1 })).toBe(0.01); expect(phasedFraction({ state: 'PAUSED', currentDayNumber: 6 })).toBe(0.5); expect(phasedFraction({ state: 'COMPLETE', currentDayNumber: 7 })).toBe(1); expect(phasedFraction({ state: 'ACTIVE', currentDayNumber: 99 })).toBe(1); expect(phasedFraction(null)).toBeNull(); const v = (id: string, state: string) => ({ id, attributes: { versionString: id, appStoreState: state, platform: 'IOS' } }); const rs = appleReleases({ versions: [v('3.0.0', 'READY_FOR_SALE'), v('2.9.0', 'READY_FOR_SALE'), v('2.8.0', 'READY_FOR_SALE'), v('2.7.0', 'WAITING_FOR_REVIEW')], phased: { '3.0.0': { attributes: { phasedReleaseState: 'ACTIVE', currentDayNumber: 4 } }, '2.9.0': { attributes: { phasedReleaseState: 'PAUSED', currentDayNumber: 2 } }, '2.8.0': { attributes: { phasedReleaseState: 'COMPLETE', currentDayNumber: 7 } } } }); expect(rs.map(r => [r.version, r.phase, r.fraction, r.phased?.state ?? null])).toEqual([['3.0.0', 'rollout', 0.1, 'ACTIVE'], ['2.9.0', 'halted', 0.02, 'PAUSED'], ['2.8.0', 'live', 1, 'COMPLETE'], ['2.7.0', 'review', null, null]]); }); test('store releases from status data; garbage gives nothing', () => { const ios = appleReleases(apple); expect(ios.map(r => [r.version, r.build, r.phase])).toEqual([['2.3.0', '45', 'review'], ['2.2.1', '41', 'live']]); const android = playReleases(play); expect(android.map(r => [r.version, r.build, r.phase, r.fraction, r.track])).toEqual([['2.2.1', '220', 'live', null, 'production'], ['2.3.0', '231', 'rollout', 0.2, 'production'], ['2.4.0', '240', 'testing', null, 'internal']]); expect(appleReleases(null)).toEqual([]); expect(playReleases({ tracks: 'x' })).toEqual([]); }); test('trains: current = highest version reaching users (live or rolling out), next = highest above it, per store', () => { const t = releaseTrains(appleReleases(apple), playReleases(play)); expect(t.current).toMatchObject({ version: '2.3.0', ios: { phase: 'review', build: '45' }, android: { phase: 'rollout', fraction: 0.2, build: '231' } }); expect(t.next).toMatchObject({ version: '2.4.0', ios: null, android: { phase: 'testing' } }); const done = releaseTrains(appleReleases(apple), playReleases({ tracks: [play.tracks[0]!.releases[0] && { track: 'production', releases: [play.tracks[0]!.releases[0]] }] })); expect(done.current).toMatchObject({ version: '2.2.1', ios: { phase: 'live', build: '41' }, android: { phase: 'live', build: '220' } }); expect(done.next).toMatchObject({ version: '2.3.0', ios: { phase: 'review' }, android: null }); expect(releaseTrains([], [])).toEqual({ current: null, next: null }); const onlyNext = releaseTrains(appleReleases({ versions: [apple.versions[0]] }), []); expect(onlyNext.current).toBeNull(); expect(onlyNext.next?.version).toBe('2.3.0'); }); test('catalog parity: config only offline, both stores live', () => { expect(catalogParity(summary)).toEqual({ checked: 'config', products: 2, subscriptions: 1, onlyIos: [], onlyAndroid: [], missingIos: [], missingAndroid: [], ok: true }); const live = catalogParity(summary, appleProductIds(apple), playProductIds(play)); expect(live).toMatchObject({ checked: 'live', onlyIos: ['ios_only'], onlyAndroid: ['no_ads', 'android_only'], missingIos: ['no_ads'], missingAndroid: [], ok: false }); // an item sold on Android only is not missing on iOS expect(catalogParity({ ...summary, catalog: { ios: ['coins'], android: ['coins', 'no_ads'] } }, appleProductIds(apple), playProductIds(play)).missingIos).toEqual([]); // one store unreadable: no cross-store claims expect(catalogParity(summary, undefined, playProductIds(play))).toMatchObject({ onlyIos: [], onlyAndroid: [], missingIos: [], missingAndroid: [] }); }); });