storebound/tests/growth-config.test.ts
CRT c9a7f095c4 console: agent-first mode (console plan / console verify), verified App Privacy and Play Create app facts
- `storebound console plan <form> [--json] [--lookup]`: read-only plan for an AI agent that drives the browser
  itself: start URL (deep links), ordered steps, answer sheet with the console's exact labels (certain / ask owner,
  curly quotes noted), never-press list, verified UI facts, driving guidance (in-page async loop + window.__log),
  verify instructions. All six forms.
- `storebound console verify <form> [--from-text <file|->]`: App Privacy per-type summaries from page text, Play
  Create app from input states, content rating / target audience summaries, App Store record and age rating from
  the API; exit 1 on a mismatch.
- MCP: console_plan, console_verify (read-only); console fill stays off MCP.
- App Privacy (verified 2026-10-12): two information steps, unlabeled purpose checkboxes (nearest parent text),
  "Definitions and Examples" never pressed; fixture and engine follow.
- Play Create app (verified 2026-10-12): /developers/<id>/create-new-app, App package name + read-only Check
  availability, material-radio inner [role=radio] click, app dashboard URL and play.appId, service account
  "Add app" as an owner step.
- console-autofill skill rewritten agent-first; agents-core rule 10; README; pitfalls 12, 18 (confirmed sign-out),
  19; CHANGELOG; play-first-release.
2026-10-12 08:39:12 +08:00

52 lines
3.5 KiB
TypeScript

// 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']);
});
});