mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
* 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>
165 lines
8.0 KiB
TypeScript
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');
|
|
});
|
|
}
|
|
});
|