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.
103 lines
6.3 KiB
TypeScript
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]);
|
|
});
|
|
});
|