Files
gstack/test/design-doc-plan-copy.test.ts
Garry Tanandcapy-ai[bot] 956a3f5484 v1.91.220.0 fix(office-hours,plan-reviews): write the plan-mode copy through to the design doc and warn when it is newer (#542) (#3408)
* fix(office-hours,plan-reviews): write the host plan-mode copy through to the design doc and warn when it is newer (#542)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(changelog): #542 under [Unreleased]

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(instruction-facts): drop the office-hours allowlist row for the removed cross-team sentence

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-25 22:31:08 +00:00

165 lines
8.0 KiB
TypeScript

/**
* #542: /office-hours in host plan mode leaves a plan-file copy of its design
* doc. The doc stays the source of truth: office-hours writes plan-copy edits
* through and records the pairing, and the discovery helper every plan review
* runs names the copy and warns when it is newer than the doc.
*/
import { describe, expect, test } from 'bun:test';
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
const ROOT = path.resolve(import.meta.dir, '..');
const FIND = path.join(ROOT, 'bin', 'gstack-design-doc-find');
function fixture(run: (dirs: { root: string; home: string; cwd: string; project: string }) => void): void {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'plan-copy-'));
try {
const home = path.join(root, 'home');
const cwd = path.join(root, 'work');
const project = path.join(home, '.gstack', 'projects', 'fixture');
fs.mkdirSync(project, { recursive: true });
fs.mkdirSync(cwd);
run({ root, home, cwd, project });
} finally { fs.rmSync(root, { recursive: true, force: true }); }
}
function write(file: string, body: string, secondsAgo: number): void {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, body);
const t = new Date(Date.now() - secondsAgo * 1000);
fs.utimesSync(file, t, t);
}
function find(home: string, cwd: string): { stdout: string; stderr: string } {
const r = spawnSync(FIND, ['fixture', 'main'], {
cwd, env: { PATH: process.env.PATH!, HOME: home }, encoding: 'utf8', timeout: 10_000,
});
expect(r.status).toBe(0);
return { stdout: r.stdout, stderr: r.stderr };
}
describe('gstack-design-doc-find names the host plan copy (#542)', () => {
test('no recorded copy: stdout is the doc and stderr stays empty', () => fixture(({ home, cwd, project }) => {
const doc = path.join(project, 'u-main-design-1.md');
write(doc, '# Design\n', 60);
expect(find(home, cwd)).toEqual({ stdout: `${doc}\n`, stderr: '' });
}));
test('a copy in sync is named with its write-through command and no warning', () => fixture(({ root, home, cwd, project }) => {
const doc = path.join(project, 'u-main-design-1.md');
const copy = path.join(root, 'plans', 'calm-otter.md');
write(copy, '# Design\n', 120);
write(doc, '# Design\n', 60);
write(`${doc}.plan-copy`, `${copy}\n`, 60);
const out = find(home, cwd);
expect(out.stdout).toBe(`${doc}\n`);
expect(out.stderr).toContain(`DESIGN_PLAN_COPY: ${copy} is the host plan-mode copy of ${doc}`);
expect(out.stderr).toContain(`cp "${copy}" "${doc}"`);
expect(out.stderr).not.toContain('STALE_DESIGN_DOC');
}));
test('a newer, edited copy warns; running the printed write-through clears it', () => fixture(({ root, home, cwd, project }) => {
const doc = path.join(project, 'u-main-design-1.md');
const copy = path.join(root, 'plans', 'calm-otter.md');
write(doc, '# Design\nold approach\n', 120);
write(`${doc}.plan-copy`, `${copy}\n`, 120);
write(copy, '# Design\nrefined approach\n', 30);
const stale = find(home, cwd);
expect(stale.stdout).toBe(`${doc}\n`);
const warning = stale.stderr.split('\n').find(line => line.startsWith('STALE_DESIGN_DOC: '));
expect(warning).toBe(`STALE_DESIGN_DOC: the plan copy is newer than ${doc}. Write it through now, then read ${doc}: cp "${copy}" "${doc}"`);
const cp = spawnSync('bash', ['-c', warning!.slice(warning!.indexOf(': cp ') + 2)], { encoding: 'utf8', timeout: 10_000 });
expect(cp.status).toBe(0);
expect(fs.readFileSync(doc, 'utf8')).toBe('# Design\nrefined approach\n');
expect(find(home, cwd).stderr).not.toContain('STALE_DESIGN_DOC');
}));
test('a doc edited after the copy is not reported stale', () => fixture(({ root, home, cwd, project }) => {
const doc = path.join(project, 'u-main-design-1.md');
const copy = path.join(root, 'plans', 'calm-otter.md');
write(copy, '# Design\nfirst draft\n', 120);
write(doc, '# Design\nedited doc\n', 30);
write(`${doc}.plan-copy`, `${copy}\n`, 30);
const out = find(home, cwd);
expect(out.stderr).toContain('DESIGN_PLAN_COPY: ');
expect(out.stderr).not.toContain('STALE_DESIGN_DOC');
}));
test('a relative, non-markdown or missing recorded path is ignored', () => fixture(({ root, home, cwd, project }) => {
const doc = path.join(project, 'u-main-design-1.md');
write(doc, '# Design\n', 120);
write(path.join(root, 'notes.txt'), 'x\n', 10);
for (const recorded of ['plans/calm-otter.md', path.join(root, 'notes.txt'), path.join(root, 'plans', 'gone.md')]) {
write(`${doc}.plan-copy`, `${recorded}\n`, 120);
expect(find(home, cwd)).toEqual({ stdout: `${doc}\n`, stderr: '' });
}
}));
test('when the repo copy wins discovery, the write-through still targets the state-root doc', () => fixture(({ root, home, cwd, project }) => {
const init = spawnSync('git', ['init', '-q', '-b', 'main'], { cwd, timeout: 30_000 });
expect(init.status).toBe(0);
const doc = path.join(project, 'u-main-design-1.md');
const repoDoc = path.join(cwd, 'docs', 'designs', 'pricing.md');
const copy = path.join(root, 'plans', 'calm-otter.md');
write(doc, '# Pricing\n', 120);
write(`${doc}.plan-copy`, `${copy}\n`, 120);
write(repoDoc, '# Pricing\n', 100);
write(copy, '# Pricing\nedited in plan mode\n', 30);
const out = find(home, cwd);
expect(fs.realpathSync(out.stdout.trim())).toBe(fs.realpathSync(repoDoc));
expect(out.stderr).toContain(`Write it through now, then read ${doc}: cp "${copy}" "${doc}"`);
}));
});
describe('/office-hours writes the plan copy through before telemetry (#542)', () => {
const skill = fs.readFileSync(path.join(ROOT, 'office-hours', 'SKILL.md'), 'utf8');
const section = skill.slice(skill.indexOf("## Before telemetry: confirm this run's design doc"));
const block = section.slice(section.indexOf('```bash\n') + 8, section.indexOf('\n```', section.indexOf('```bash\n') + 8));
function confirm(home: string, doc: string, plan: string): string {
const script = block.replaceAll('~/.claude/skills/gstack/bin/', `${ROOT}/bin/`)
.replace('DESIGN_DOC_PATH', doc).replace('HOST_PLAN_PATH', plan);
const r = spawnSync('bash', ['-c', script], { env: { PATH: process.env.PATH!, HOME: home }, encoding: 'utf8', timeout: 10_000 });
expect(r.status).toBe(0);
return r.stdout;
}
test('plan-mode edits land in the doc and the pairing is recorded', () => fixture(({ root, home, project }) => {
const doc = path.join(project, 'u-main-design-1.md');
const plan = path.join(root, 'plans', 'calm-otter.md');
write(doc, '# Design\nas approved\n', 120);
write(plan, '# Design\nrevised in plan mode\n', 30);
expect(confirm(home, doc, plan)).toBe('DESIGN_DOC: ok\nPLAN_COPY: written through\n');
expect(fs.readFileSync(doc, 'utf8')).toBe('# Design\nrevised in plan mode\n');
expect(fs.readFileSync(`${doc}.plan-copy`, 'utf8')).toBe(`${plan}\n`);
}));
test('an older plan file never overwrites the doc', () => fixture(({ root, home, project }) => {
const doc = path.join(project, 'u-main-design-1.md');
const plan = path.join(root, 'plans', 'calm-otter.md');
write(plan, '# Design\nfirst draft\n', 120);
write(doc, '# Design\nfinal\n', 30);
expect(confirm(home, doc, plan)).toBe('DESIGN_DOC: ok\n');
expect(fs.readFileSync(doc, 'utf8')).toBe('# Design\nfinal\n');
}));
test('outside plan mode nothing is recorded', () => fixture(({ home, project }) => {
const doc = path.join(project, 'u-main-design-1.md');
write(doc, '# Design\n', 30);
expect(confirm(home, doc, '')).toBe('DESIGN_DOC: ok\n');
expect(fs.existsSync(`${doc}.plan-copy`)).toBe(false);
}));
});
describe('plan reviews act on the helper lines (#542)', () => {
for (const skill of ['plan-ceo-review', 'plan-eng-review']) {
test(`${skill} follows DESIGN_PLAN_COPY and STALE_DESIGN_DOC`, () => {
const text = fs.readFileSync(path.join(ROOT, skill, 'SKILL.md'), 'utf8');
expect(text).toContain('DESIGN_PLAN_COPY');
expect(text).toContain('STALE_DESIGN_DOC');
});
}
});