storebound/tests/do-it-dont-ask.test.ts
CRT 710c3a53d0 README, docs and site: one story (the whole job, no avoidable rejections, the desktop app, your AI, install)
README is a short front page (about 120 lines) with one real visual per section; every former section moved
unchanged into docs/*.md with a docs/README.md index (journey, rejections, getting-started, commands, growth,
studios, store-art, listing-languages, api, skills, mcp, safety, faq, project). The site tells the same story on one
page: the teaser in the hero, real CLI output as text, the desktop app as a section with light/dark captures of eight
screens and the per-system downloads (desktop/latest.json); /desktop/ redirects to /#download. New capture tools:
tools/site-desktop-shots.ts (fixture-mode desktop frames) and tools/site-term-shots.ts (real CLI output as text and
WebP). tests/docs-links.test.ts checks README, docs and site links resolve.
2026-10-13 03:29:59 +08:00

103 lines
6.3 KiB
TypeScript

// Pitfall 26: an agent spent hours handing the owner one click, command or answer at a time. The first rule
// "Do it, don't ask" must head the agent rules (and so every generated adapter), README and the pitfalls skill, and
// no skill or rule text may hand navigation, draft saves, exports, uploads or form filling to the owner.
import { describe, expect, test } from 'bun:test';
import { readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
const root = join(import.meta.dir, '..');
const read = (rel: string) => readFileSync(join(root, rel), 'utf8');
const RULE = "do it, don't ask";
describe("first rule: do it, don't ask", () => {
test('is the first section of docs/agents-core.md, above the numbered rules', () => {
const core = read('docs/agents-core.md');
const firstHeading = /^## (.*)$/m.exec(core)?.[1] ?? '';
expect(firstHeading.toLowerCase()).toContain(RULE);
expect(core.indexOf('## First rule')).toBeLessThan(core.indexOf('1. **The owner presses only the owner steps**'));
for (const owner of ['Submit for review', 'App Privacy Publish', 'legal', 'live prices', 'review replies', 'testers'])
expect(core).toContain(owner);
expect(core).toMatch(/ONE batch at the start/);
});
test('reaches every generated adapter, README and the lead of store-pitfalls', () => {
for (const f of ['AGENTS.md', 'GEMINI.md', '.github/copilot-instructions.md', '.cursor/rules/storebound.mdc'])
expect(read(f).toLowerCase()).toContain(RULE);
// the first section of README carries the rule; the full text lives in docs/journey.md
const readme = read('README.md');
const first = readme.split('\n## ')[1] ?? '';
expect(first.toLowerCase()).toContain(RULE);
expect(read('docs/journey.md')).toContain('## How agents use storebound');
const pit = read('skills/store-pitfalls/SKILL.md');
expect(/^## (.*)$/m.exec(pit)?.[1]?.toLowerCase()).toContain(RULE);
for (const s of ['console-autofill', 'play-first-release', 'appstore-first-release', 'store-declarations'])
expect(read(`skills/${s}/SKILL.md`).toLowerCase()).toContain(RULE);
});
// Phrases that hand agent work (navigation, draft saves, exports, uploads, form filling, commands) to the owner.
const HANDOFF: RegExp[] = [
/\bUI = [^)\n]*by hand/i,
/\bask the owner to (click|press next|open|navigate|download|upload|export|fill|save|copy|run)\b/i,
/\b(have|let) the owner (click|open|navigate|download|upload|export|fill|save|copy|run)\b/i,
/\bthe owner (clicks|opens|navigates|downloads|uploads|exports|fills|copies|pastes)\b/i,
/\b(click|download|upload|export|navigate)\b[^.\n]{0,60}\byourself\b/i,
/\b(do|fill|finish) (that|this) (part )?by hand\b/i,
/\bfor the owner to copy\b/i,
];
const files = ['docs/agents-core.md', 'README.md', // the README's former sections, now in docs/
...['journey', 'getting-started', 'commands', 'rejections', 'growth', 'studios', 'store-art', 'faq', 'safety', 'mcp', 'skills', 'listing-languages', 'api', 'project'].map(n => `docs/${n}.md`),
...readdirSync(join(root, 'skills')).map(n => `skills/${n}/SKILL.md`).filter(f => { try { read(f); return true; } catch { return false; } })];
test('no skill or rule hands navigation, exports, uploads or forms to the owner', () => {
const hits: string[] = [];
for (const f of files) {
read(f).split('\n').forEach((line, i) => {
if (/never tell the owner/i.test(line)) return; // the rule quoting what not to say
for (const re of HANDOFF) if (re.test(line)) hits.push(`${f}:${i + 1}: ${line.trim().slice(0, 120)}`);
});
}
expect(hits).toEqual([]);
});
test('the handoff patterns catch the phrases that cost hours', () => {
const bad = ['## Steps (API = storebound, UI = Play Console by hand)', 'ask the owner to click Next on each page',
'upload them in the experiment form yourself', 'Do that part by hand (the manual steps follow)',
'The owner opens App content > Data safety > Export to CSV', 'prints the list for the owner to copy'];
for (const b of bad) expect(HANDOFF.some(re => re.test(b))).toBe(true);
const ok = ['drive the page yourself (Claude in Chrome)', 'the owner presses Publish', 'the owner signs in once',
'press Create app yourself', 'Granting them is the owner\'s (a permission grant)'];
for (const o of ok) expect(HANDOFF.some(re => re.test(o))).toBe(false);
});
// The CLI's own output: when storebound's scripted part cannot go on, what is left goes to the calling agent (its
// browser tool with `console plan` / `console verify`, or an edit of the config), not "do it yourself" to the owner.
// Sign-in, the final Submit / Create / Publish and agreements stay the owner's.
const CLI_HANDOFF: RegExp[] = [
/\b(open|fill|answer|check|do|merge|add|set)\b[^.\n]{0,40}\b(yourself|by hand)\b/i,
/\bdo (this|that) part\b/i,
/\bto check yourself\b/i,
];
const cliFiles = (dir: string): string[] => readdirSync(join(root, dir), { withFileTypes: true }).flatMap(e =>
e.isDirectory() ? (e.name === 'data' ? [] : cliFiles(`${dir}/${e.name}`)) : e.name.endsWith('.ts') ? [`${dir}/${e.name}`] : []);
test('CLI output never hands agent work to the owner', () => {
const hits: string[] = [];
for (const f of cliFiles('cli')) {
read(f).split('\n').forEach((line, i) => {
const t = line.trim();
if (t.startsWith('//') || t.startsWith('*') || t.startsWith('/*')) return; // comments are for the reader of the code
for (const re of CLI_HANDOFF) if (re.test(line)) hits.push(`${f}:${i + 1}: ${t.slice(0, 140)}`);
});
}
expect(hits).toEqual([]);
});
test('the CLI patterns catch the old console fill lines and keep the owner-only ones', () => {
for (const b of ['Open it yourself; storebound keeps watching', 'fill them yourself', 'do this part yourself; storebound keeps watching',
'3 answer(s) to check yourself.', 'not mapped (do these by hand)'])
expect([b, CLI_HANDOFF.some(re => re.test(b))]).toEqual([b, true]);
for (const o of ['review every field and press Submit yourself', 'sign in to App Store Connect yourself (Apple ID, password, two-factor code)',
'Read and tick them yourself, then press Create app.', 'storebound console plan <form> [--json] [--lookup] (then drive the page yourself'])
expect([o, CLI_HANDOFF.some(re => re.test(o))]).toEqual([o, false]);
});
});