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.
This commit is contained in:
parent
a9261a8052
commit
c9a7f095c4
34 changed files with 1430 additions and 113 deletions
|
|
@ -37,7 +37,9 @@ const shared = await launchTestBrowser('playwright');
|
|||
const { browser, skipWhy } = shared;
|
||||
if (!browser) console.warn(`console-flow browser tests skipped: no Chromium installed (${skipWhy}). Run \`bunx playwright install chromium\`.`);
|
||||
const scope = testScope(() => browser);
|
||||
const TEST_MS = 60_000;
|
||||
// a whole App Privacy walk (6 types x 5 dialog steps) on a loaded CI machine: generous, still bounded
|
||||
const TEST_MS = 90_000;
|
||||
const WALK_MS = 75_000;
|
||||
|
||||
let server: ReturnType<typeof Bun.serve> | null = null;
|
||||
let base = '';
|
||||
|
|
@ -141,7 +143,7 @@ describe.skipIf(!browser)('App Privacy: the whole questionnaire (fixture apple-a
|
|||
const b = await local();
|
||||
const log = logLines();
|
||||
const run = scope.run(fillSession(b, { form: 'apple-app-privacy', config: privacy(), startUrl: base + 'apple-app-privacy-flow.html', ...fast(log) }));
|
||||
await log.wait('the Publish line', l => l.some(m => m.startsWith(PUBLISH)));
|
||||
await log.wait('the Publish line', l => l.some(m => m.startsWith(PUBLISH)), WALK_MS);
|
||||
const c = await clicks(b);
|
||||
expect(c).not.toContain('Publish');
|
||||
expect(c).not.toContain('See Details');
|
||||
|
|
@ -162,7 +164,7 @@ describe.skipIf(!browser)('App Privacy: the whole questionnaire (fixture apple-a
|
|||
const log = logLines();
|
||||
const config = privacy({ COARSE_LOCATION: { collected: true, purposes: ['ANALYTICS'], linked: false, tracking: false } });
|
||||
const run = scope.run(fillSession(b, { form: 'apple-app-privacy', config, startUrl: base + 'apple-app-privacy-flow.html?existing=1', ...fast(log) }));
|
||||
await log.wait('the Publish line', l => l.some(m => m.startsWith(PUBLISH)));
|
||||
await log.wait('the Publish line', l => l.some(m => m.startsWith(PUBLISH)), WALK_MS);
|
||||
expect((await clicks(b)).filter(x => x === 'Edit')).toHaveLength(1);
|
||||
expect(log.lines.some(m => m.startsWith('Data collection was answered before'))).toBe(true);
|
||||
expect(await b.page.evaluate(() => (window as any).__saves)).toBe(4); // the list + three types
|
||||
|
|
@ -183,7 +185,7 @@ describe.skipIf(!browser)('App Privacy: the whole questionnaire (fixture apple-a
|
|||
expect(await linked.isVisible()).toBe(true);
|
||||
expect(await b.page.getByRole('radio', { name: /^No, we do not use Device ID/ }).isVisible()).toBe(false);
|
||||
await linked.check(); // the owner answers
|
||||
await log.wait('the Publish line', l => l.some(m => m.startsWith(PUBLISH)));
|
||||
await log.wait('the Publish line', l => l.some(m => m.startsWith(PUBLISH)), WALK_MS);
|
||||
expect(log.lines).toContain('"Device ID: linked to the user" answered: storebound goes on');
|
||||
expect(await b.page.evaluate(() => (window as any).__saves)).toBe(3);
|
||||
expect(await clicks(b)).not.toContain('Publish');
|
||||
|
|
@ -198,7 +200,7 @@ describe.skipIf(!browser)('App Privacy: the whole questionnaire (fixture apple-a
|
|||
const types = Object.fromEntries(APPLE_DATA_TYPE_IDS.map(id => [id, { collected: false }]));
|
||||
const config = { ...base0, dataSafety: undefined, appPrivacy: { types } } as unknown as ShipConfig;
|
||||
const run = scope.run(fillSession(b, { form: 'apple-app-privacy', config, startUrl: base + 'apple-app-privacy-flow.html', ...fast(log) }));
|
||||
await log.wait('the Publish line', l => l.some(m => m.startsWith(PUBLISH)));
|
||||
await log.wait('the Publish line', l => l.some(m => m.startsWith(PUBLISH)), WALK_MS);
|
||||
expect(await b.page.evaluate(() => (window as any).__saves)).toBe(1);
|
||||
expect(await clicks(b)).not.toContain('Next');
|
||||
expect(await clicks(b)).not.toContain('Publish');
|
||||
|
|
@ -222,13 +224,13 @@ const SETUP = 'apple-app-privacy-setup.html';
|
|||
const answersOf = (b: ConsoleBrowser) => b.page.evaluate(() => (window as any).__answers);
|
||||
|
||||
describe.skipIf(!browser)('App Privacy: each data type set up from the real page ("Set Up <type>" as plain text; fixture apple-app-privacy-setup.html)', () => {
|
||||
test('every collected type is set up through its dialog (purposes > Next > linked > Next > tracking > Save); "?", Publish and the Edits never clicked; every step dumped', async () => {
|
||||
test('every collected type is set up through its dialog (purposes > Next > linked > Next > two info steps > Next > Next > tracking > Save; purpose checkboxes without an accessible name); "?", Publish and the Edits never clicked; every step dumped', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'sb-console-debug-'));
|
||||
try {
|
||||
const b = await local();
|
||||
const log = logLines();
|
||||
const run = scope.run(fillSession(b, { form: 'apple-app-privacy', config: six(), startUrl: base + SETUP, ...fast(log), debugDir: dir, debugEveryStep: true }));
|
||||
await log.wait('the Publish line', l => l.some(m => m.startsWith(PUBLISH)));
|
||||
await log.wait('the Publish line', l => l.some(m => m.startsWith(PUBLISH)), WALK_MS);
|
||||
expect(await b.page.evaluate(() => (window as any).__setups)).toEqual(SIX_NAMES.map(n => `Set Up ${n}`));
|
||||
expect(await b.page.evaluate(() => (window as any).__saves)).toBe(6);
|
||||
const got = await answersOf(b);
|
||||
|
|
@ -237,8 +239,9 @@ describe.skipIf(!browser)('App Privacy: each data type set up from the real page
|
|||
expect(got['Product Interaction']).toEqual({ purposes: ['Analytics', 'Product Personalization'], linked: 'no', tracking: 'no' });
|
||||
expect(Object.keys(got).sort()).toEqual([...SIX_NAMES].sort());
|
||||
const c = await clicks(b);
|
||||
for (const never of ['?', 'Publish', 'Edit', 'Cancel', 'Back']) expect(c).not.toContain(never);
|
||||
expect(c.filter(x => x === 'Next')).toHaveLength(18);
|
||||
for (const never of ['?', 'Publish', 'Edit', 'Cancel', 'Back', 'Definitions and Examples']) expect(c).not.toContain(never);
|
||||
expect(c.filter(x => x === 'Next')).toHaveLength(24); // purposes, linked and the TWO info steps of each of 6 types
|
||||
expect(log.lines.filter(m => m === 'an information-only step of the dialog: storebound pressed Next')).toHaveLength(12);
|
||||
expect(c.filter(x => x === 'Save')).toHaveLength(6);
|
||||
expect(log.lines).toContain('the Data Types section lists exactly the collected types of the config: storebound sets up each one');
|
||||
expect(log.lines).toContain('clicked "Set Up Purchase History" (navigation)');
|
||||
|
|
@ -266,7 +269,7 @@ describe.skipIf(!browser)('App Privacy: each data type set up from the real page
|
|||
expect(await yes.isVisible()).toBe(true);
|
||||
expect(await b.page.evaluate(() => (window as any).__saves)).toBe(3);
|
||||
await yes.check(); // the owner answers
|
||||
await log.wait('the Publish line', l => l.some(m => m.startsWith(PUBLISH)));
|
||||
await log.wait('the Publish line', l => l.some(m => m.startsWith(PUBLISH)), WALK_MS);
|
||||
expect(log.lines).toContain('"Purchase History: linked to the user" answered: storebound goes on');
|
||||
expect((await answersOf(b))['Purchase History'].linked).toBe('yes');
|
||||
expect(await b.page.evaluate(() => (window as any).__saves)).toBe(6);
|
||||
|
|
@ -297,7 +300,7 @@ describe.skipIf(!browser)('App Privacy: each data type set up from the real page
|
|||
expect(pj.controls).toContainEqual({ role: 'switch', name: 'Account identity', checked: false });
|
||||
await Bun.sleep(500);
|
||||
const c = await clicks(b);
|
||||
expect(c.filter(x => x === 'Next')).toHaveLength(2 * 3 + 1); // two types done (purposes, linked, definitions), Device ID's first step only
|
||||
expect(c.filter(x => x === 'Next')).toHaveLength(2 * 4 + 1); // two types done (purposes, linked, the two info steps), Device ID's first step only
|
||||
expect(c).not.toContain('Publish');
|
||||
expect(c).not.toContain('?');
|
||||
await b.context.close();
|
||||
|
|
@ -310,7 +313,7 @@ describe.skipIf(!browser)('App Privacy: each data type set up from the real page
|
|||
const log = logLines();
|
||||
const url = `${base}${SETUP}?preset=${encodeURIComponent('Crash Data:App Functionality')}&preset=${encodeURIComponent('Device ID:Analytics')}`;
|
||||
const run = scope.run(fillSession(b, { form: 'apple-app-privacy', config: six(), startUrl: url, ...fast(log) }));
|
||||
await log.wait('the Publish line', l => l.some(m => m.startsWith(PUBLISH)));
|
||||
await log.wait('the Publish line', l => l.some(m => m.startsWith(PUBLISH)), WALK_MS);
|
||||
expect(log.lines).toContain('Crash Data was set up before with the purposes of the config: left as it is');
|
||||
expect(log.lines).toContain('Device ID was set up before with other purposes than the config: storebound opens it with its Edit');
|
||||
const got = await answersOf(b);
|
||||
|
|
@ -365,7 +368,8 @@ describe.skipIf(!browser)('the other forms: navigation by itself, final buttons
|
|||
const log = logLines();
|
||||
const run = scope.run(fillSession(b, { form, config: base0, startUrl: base + file, ...fast(log) }));
|
||||
await log.wait('the final line', l => l.some(m => m.includes(`press ${final} yourself`)));
|
||||
expect(await clicks(b)).toEqual([]);
|
||||
// Play: only the read-only package name check (verified 2026-10-12); never the final button
|
||||
expect(await clicks(b)).toEqual(form === 'play-app-record' ? ['Check availability'] : []);
|
||||
expect(navAsked(log.lines)).toEqual([]);
|
||||
await b.context.close();
|
||||
await run;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue