// Config validation of the growth extras: web, revenuecat, prices.ppp, apple.teamId / adhoc. import { describe, expect, test } from 'bun:test'; import { type ShipConfig, validateConfig, validateExtras } from '../cli/lib/config'; import { TOOLS, toArgv } from '../cli/mcp-tools'; const base: ShipConfig = { app: { name: 'Example', privacyPolicyUrl: 'https://example.com/privacy', supportUrl: 'https://example.com/support', website: 'https://example.com', contactEmail: 'hi@example.com' }, languages: [{ id: 'en', play: 'en-US', apple: 'en-US' }], apple: { bundleId: 'com.example.app' }, play: { packageName: 'com.example.app' }, }; const fp = Array.from({ length: 32 }, (_, i) => (i * 7 % 256).toString(16).toUpperCase().padStart(2, '0')).join(':'); describe('growth extras config', () => { test('a full, valid set passes', () => { const c: ShipConfig = { ...base, apple: { ...base.apple!, teamId: 'ABCDEFGHIJ', adhoc: { method: 'ad-hoc' } }, prices: { ppp: { source: 'worldbank', floor: 0.25, ceiling: 1 } }, web: { domain: 'links.example.com', paths: ['/invite/*'], exclude: ['/admin/*'], androidSha256: [fp], landing: true }, revenuecat: { projectId: 'proj0example', entitlements: { pro: ['com.example.app.pro'] }, offering: 'default' } }; expect(validateExtras(c)).toEqual([]); expect(validateConfig(c)).toEqual([]); }); test('each mistake is named', () => { const c = { ...base, apple: { ...base.apple!, teamId: 'abc', adhoc: { method: 'enterprise' } }, prices: { ppp: { source: 'file', floor: 2, ceiling: 1 } }, web: { domain: 'https://example.com/x', paths: ['invite'], androidSha256: ['ab:cd'] }, revenuecat: { projectId: '', entitlements: { pro: [] }, offering: 'a b' } } as unknown as ShipConfig; const e = validateExtras(c).join('\n'); for (const s of ['apple.teamId', 'apple.adhoc.method', 'prices.ppp.file', 'floor is above ceiling', 'web.domain', 'must start with /', 'web.androidSha256', 'revenuecat.projectId', 'revenuecat.entitlements.pro', 'revenuecat.offering']) expect(e).toContain(s); }); test('no extras, no problems', () => expect(validateExtras(base)).toEqual([])); }); describe('growth extras over MCP', () => { test('no console tool that drives a browser exists in MCP (only the read-only plan and verify)', () => { expect(TOOLS.filter(t => (t.command[0] === 'console' || /console/.test(t.name)) && !(t.write === false && ['plan', 'verify'].includes(t.command[1] ?? ''))).map(t => t.name)).toEqual([]); }); test('Apple Ads, RevenueCat, device registration and internal sharing are read-only in MCP', () => { const risky = TOOLS.filter(t => (t.command[0] === 'ads' && t.command[1] === 'apple') || t.command[0] === 'revenuecat' || (t.command[0] === 'play' && t.command[1] === 'share') || (t.command[0] === 'apple' && t.command[1] === 'adhoc' && !['devices', 'options'].includes(t.command[2]!))); expect(risky.filter(t => t.write)).toEqual([]); for (const t of TOOLS.filter(x => (x.command[0] === 'ads' && x.command[1] === 'apple') || x.command[0] === 'revenuecat')) expect(['status', 'campaigns', 'report', 'diff']).toContain(t.command.at(-1)!); }); test('local writes default to a dry run', () => { for (const n of ['prices_ppp', 'web_init', 'apple_adhoc_options']) { const t = TOOLS.find(x => x.name === n)!; expect(toArgv(t, {}).argv).toContain('--dry'); } expect(toArgv(TOOLS.find(x => x.name === 'prices_ppp')!, { base: 4.99, source: 'worldbank' }).argv).toEqual(['prices', 'ppp', '--base', '4.99', '--source', 'worldbank', '--dry']); }); });