- `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.
52 lines
3.5 KiB
TypeScript
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']);
|
|
});
|
|
});
|