console plans: guardrails from a real run (2026-10-21): health none label and Sign in details Yes/No + Add details (old labels as altLabels, verify accepts both), finance boxes on step 1, App Privacy Additional Setup Required OK, verify from DOM input states (verify.capture, --from), Save + confirm before every Next on Play pages, IARC Digital purchases boxes and an exact-label in-page helper that refuses patterns and blanket answers; review contact found in parent folders, the profile and more key shapes with the files checked listed; one-line errors for missing required values (Args.required); pitfalls 86-87, tests

This commit is contained in:
CRTkafa 2026-10-21 05:03:02 +07:00
parent a6dab8f3aa
commit 335a4124ab
23 changed files with 486 additions and 74 deletions

View file

@ -337,6 +337,23 @@ export function verifyAppContent(text: string): VerifyResult {
return result('play-app-content', st ? 'inputs' : 'page-text', rows, st ? [] : ['The start buttons are aria-labels, not page text: pass the JSON of the plan\'s capture snippet.']);
}
/** One labelled control read from the DOM by INPUT_STATES_CAPTURE. */
export interface InputState { label: string; role: 'radio' | 'checkbox' | 'option' | string; checked: boolean; group?: string }
export { INPUT_STATES_CAPTURE } from './console-play-plans';
/** The input states of a capture JSON ({ states, text }); null for plain page text. */
export function parseStates(raw: string): { states: InputState[]; text: string } | null {
const s = raw.trim();
if (!s.startsWith('{')) return null;
try {
const j = JSON.parse(s) as { states?: unknown; text?: unknown };
if (!Array.isArray(j.states)) return null;
const states = j.states.filter((x): x is InputState => !!x && typeof x === 'object' && typeof (x as InputState).label === 'string' && typeof (x as InputState).checked === 'boolean');
return { states, text: typeof j.text === 'string' ? j.text : '' };
} catch { return null; }
}
/**
* Store settings and the single App content declarations, from the page text after Save: text answers (the category,
* the privacy policy URL) and option labels must appear; the App category's "Not selected" is a mismatch. Yes / No
@ -344,19 +361,39 @@ export function verifyAppContent(text: string): VerifyResult {
*/
export function verifyPlayDeclaration(form: Exclude<PlanOnlyFormId, 'play-app-content'>, config: ShipConfig, text: string): VerifyResult {
const part = playOnlyPart(form, config, {});
const blob = fold(text);
const st = parseStates(text);
const blob = fold(st ? st.text : text);
const rows: VerifyRow[] = [];
const radios = st?.states.filter(x => /radio/.test(x.role)) ?? [];
const radioQuestions = part.answers.filter(a => a.control === 'radio').length;
for (const a of part.answers) {
if (a.status === 'agreement' || a.id === 'credentials') continue; // the demo account is never read back or printed
if (a.status === 'ask-owner' || a.value === null) { rows.push({ item: a.question, expected: '(owner decides)', seen: '-', status: 'ask-owner' }); continue; }
const labels = [a.label, ...(a.altLabels ?? [])].filter((l): l is string => !!l);
const want = a.action === 'type' || a.action === 'choose' ? String(a.value) : a.label ?? '';
if (st && (a.control === 'radio' || a.control === 'checkbox') && labels.length) {
// input states from the DOM: the control with this (or an older) label, checked as the sheet says
let pool = a.control === 'radio' ? radios : st.states.filter(x => /checkbox|option/.test(x.role));
if (a.control === 'radio') {
const byQ = pool.filter(x => x.group && fold(x.group).startsWith(fold(a.question)));
if (byQ.length) pool = byQ;
else if (radioQuestions > 1) pool = []; // several radio questions: only an exact question group tells them apart
}
const hit = pool.find(x => labels.some(l => fold(x.label) === fold(l)));
const wantOn = a.action !== 'untick';
const picked = a.control === 'radio' ? pool.filter(x => x.checked).map(x => x.label) : [];
rows.push(hit
? { item: a.question, expected: `${labels[0]} ${wantOn ? 'checked' : 'unchecked'}`, seen: `${hit.label} ${hit.checked ? 'checked' : 'unchecked'}${!hit.checked && picked.length ? ` (checked: ${picked.join(', ')})` : ''}`, status: hit.checked === wantOn ? 'match' : 'mismatch' }
: { item: a.question, expected: `${labels[0]} ${wantOn ? 'checked' : 'unchecked'}`, seen: 'no control with that label', status: 'not-found' });
continue;
}
const plain = !want || /^(yes|no)$/i.test(want) || a.action === 'untick';
if (plain) { rows.push({ item: a.question, expected: a.text, seen: 'not readable from page text', status: 'not-found', note: 'read the control (aria-checked) in the page' }); continue; }
const shown = blob.includes(fold(want));
if (plain) { rows.push({ item: a.question, expected: a.text, seen: 'not readable from page text', status: 'not-found', note: 'pass the input states: run the plan\'s verify.capture snippet and `--from -`' }); continue; }
const shownAs = a.action === 'type' || a.action === 'choose' ? (blob.includes(fold(want)) ? want : null) : labels.find(l => blob.includes(fold(l))) ?? null;
const unset = a.id === 'category' && /not selected|select a category/.test(blob);
rows.push({ item: a.question, expected: want, seen: shown ? want : unset ? 'Not selected' : 'not found', status: shown && !unset ? 'match' : unset ? 'mismatch' : 'not-found' });
rows.push({ item: a.question, expected: want, seen: shownAs ?? (unset ? 'Not selected' : 'not found'), status: shownAs && !unset ? 'match' : unset ? 'mismatch' : 'not-found' });
}
return result(form, 'page-text', rows, ['Page text after Save: Yes / No radios and unticked boxes are not readable from text and are never counted as a match.']);
return result(form, st ? 'inputs' : 'page-text', rows, st ? ['Input states from the DOM (aria-checked, checked, aria-selected).'] : ['Page text after Save: Yes / No radios and unticked boxes are not readable from text and are never counted as a match; pass the input states (plan verify.capture, `--from -`).']);
}
/** The production track's Countries / regions tab: "<N> countries / regions" (0 is a mismatch) and, with