mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-02 17:40:02 +02:00
- C0/C1: the five never-green evals (skill-e2e-autoplan-chain and
skill-e2e-plan-{ceo,eng,design,devex}-finding-count) failed on harness and
budget, never on skill behavior; delete them, their touchfile/tier ids,
AUTOPLAN_CHAIN_BUDGET and the dedicated eighth periodic slice (--slices 7).
- C2: delete the helper groups whose only paid consumers were those files
(11 modules), trim claude-pty-runner and eng-seeded-coverage to the paid
closure, and delete the free replay tests whose assertions exercised only
that dead code (89 files, 135 orphaned fixtures). Blocks that used dead code
only as input for a live subject keep their assertions: the multiSelect
default moved to plan-review-decisions, runner PTY tests use inline caller
policies, and the timer-safe budget checks moved to eng-finding-retry-budget.
- The eight production-touching files stay except ceo-current-decision-record
(its template read only feeds the retired counter).
- CARVE_GUARDS.autoplan is behavioral 'none'; TODOS records the lost chain
and per-finding cadence coverage with their re-entry tests.
201 lines
12 KiB
TypeScript
201 lines
12 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { spawnSync } from 'child_process';
|
|
import { createHash } from 'crypto';
|
|
import { ALL_HOST_CONFIGS } from '../hosts';
|
|
import { generateAutoplanReviewFile } from '../scripts/resolvers/composition';
|
|
import { HOST_PATHS, type TemplateContext } from '../scripts/resolvers/types';
|
|
import { E2E_TOUCHFILES } from './helpers/touchfiles';
|
|
|
|
const ROOT = path.resolve(import.meta.dir, '..');
|
|
const REVIEWS = ['plan-ceo-review', 'plan-design-review', 'plan-devex-review', 'plan-eng-review'];
|
|
let owned: string;
|
|
let rendered: string;
|
|
|
|
function installFile(source: string, destination: string, mode: 'copy' | 'symlink') {
|
|
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
if (mode === 'copy') fs.copyFileSync(source, destination);
|
|
else fs.symlinkSync(source, destination);
|
|
}
|
|
|
|
function methodology(phase: string, entry: string, restore: string) {
|
|
return spawnSync(process.execPath, [path.join(ROOT, 'bin/gstack-autoplan-snapshot.ts'), 'methodology', phase, entry, restore], {
|
|
cwd: ROOT, encoding: 'utf8', timeout: 10_000,
|
|
});
|
|
}
|
|
|
|
function assertBundle(result: ReturnType<typeof methodology>, expected: string[]) {
|
|
expect(result.status, result.stderr).toBe(0);
|
|
const manifest = JSON.parse(result.stdout);
|
|
const bundle = fs.readFileSync(manifest.methodologyPath);
|
|
expect(createHash('sha256').update(bundle).digest('hex')).toBe(manifest.sha256);
|
|
expect(bundle.length).toBe(manifest.bytes);
|
|
expect(manifest.sources.map((part: any) => part.path)).toEqual(expected);
|
|
for (const part of manifest.sources) {
|
|
const actual = fs.readFileSync(part.path);
|
|
expect(bundle.subarray(part.startByte, part.endByte)).toEqual(actual);
|
|
expect(createHash('sha256').update(actual).digest('hex')).toBe(part.sha256);
|
|
expect(actual.length).toBe(part.bytes);
|
|
}
|
|
if (process.platform !== 'win32') expect(fs.statSync(manifest.methodologyPath).mode & 0o777).toBe(0o444);
|
|
const active = path.join(path.dirname(manifest.restorePath), 'active.md');
|
|
fs.writeFileSync(active, '## Implementation plan\nKeep every requirement.\n## Review record\n');
|
|
const created = spawnSync(process.execPath, [path.join(ROOT, 'bin/gstack-autoplan-snapshot.ts'), 'create', manifest.phase, active, manifest.restorePath, manifest.methodologyPath], {
|
|
encoding: 'utf8', timeout: 10_000,
|
|
});
|
|
expect(created.status, created.stderr).toBe(0);
|
|
expect(JSON.parse(created.stdout).methodology.sha256).toBe(manifest.sha256);
|
|
return manifest;
|
|
}
|
|
|
|
beforeAll(() => {
|
|
owned = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-autoplan-discovery-'));
|
|
rendered = path.join(owned, 'rendered');
|
|
const result = spawnSync(process.execPath, ['run', 'scripts/gen-skill-docs.ts', '--host', 'all', '--out-dir', rendered], {
|
|
cwd: ROOT, encoding: 'utf8', timeout: 120_000,
|
|
});
|
|
expect(result.status, result.stderr).toBe(0);
|
|
}, 120_000);
|
|
|
|
afterAll(() => { if (owned) fs.rmSync(owned, { recursive: true, force: true }); });
|
|
|
|
describe('autoplan reads installed host methodology', () => {
|
|
for (const host of ALL_HOST_CONFIGS) {
|
|
for (const mode of (process.platform === 'win32' ? ['copy'] : ['copy', 'symlink']) as Array<'copy' | 'symlink'>) {
|
|
test(`${host.name} reads its generated review files from ${host.name === 'claude' ? 'the canonical global path' : 'local and global paths'} in ${mode} installations`, () => {
|
|
const generatedRoot = host.name === 'claude' ? rendered : path.join(rendered, host.hostSubdir, 'skills');
|
|
const entryName = host.name === 'claude' ? 'autoplan' : 'gstack-autoplan';
|
|
const generatedEntry = path.join(generatedRoot, entryName, 'SKILL.md');
|
|
const body = fs.readFileSync(generatedEntry, 'utf8');
|
|
if (host.name === 'claude') {
|
|
for (const review of REVIEWS) expect(body).toContain(`~/.claude/skills/gstack/${review}/SKILL.md`);
|
|
} else {
|
|
// Read the path actually emitted in the entrypoint. Resolving it from
|
|
// the installed entrypoint works for copied installs and symlinked ones.
|
|
const refs = [...body.matchAll(/`(\.\.\/gstack-plan-[a-z-]+\/SKILL\.md)`/g)].map(match => match[1]!);
|
|
expect([...new Set(refs)].sort()).toEqual(REVIEWS.map(name => `../gstack-${name}/SKILL.md`).sort());
|
|
for (const review of REVIEWS) expect(body).not.toContain(`$GSTACK_ROOT/${review}/SKILL.md`);
|
|
expect(body.replace(/\s+/g, ' ')).toContain("Use /autoplan's installed registry; resolve siblings from its discovered SKILL.md");
|
|
}
|
|
const roots = host.name === 'claude'
|
|
? [path.join(owned, host.name, mode, 'home', host.globalRoot)]
|
|
: [path.join(owned, host.name, mode, 'repo', path.dirname(host.localSkillRoot)), path.join(owned, host.name, mode, 'home', path.dirname(host.globalRoot))];
|
|
if (host.name === 'codex') roots.push(path.join(owned, 'codex', mode, 'custom-codex-home', 'skills'));
|
|
for (const registry of roots) {
|
|
const entry = path.join(registry, entryName, 'SKILL.md');
|
|
installFile(generatedEntry, entry, mode);
|
|
for (const review of REVIEWS) {
|
|
const reviewName = host.name === 'claude' ? review : `gstack-${review}`;
|
|
const generatedReview = path.join(generatedRoot, reviewName, 'SKILL.md');
|
|
installFile(generatedReview, path.join(registry, reviewName, 'SKILL.md'), mode);
|
|
// A runtime root may be an unrelated checkout. Its old canonical
|
|
// review file must never be selected instead of the installed host.
|
|
const stale = path.join(registry, 'gstack', review, 'SKILL.md');
|
|
fs.mkdirSync(path.dirname(stale), { recursive: true });
|
|
fs.writeFileSync(stale, 'STALE FOREIGN HARNESS SKILL');
|
|
const reference = host.name === 'claude' ? `../${review}/SKILL.md` : [...body.matchAll(/`(\.\.\/gstack-plan-[a-z-]+\/SKILL\.md)`/g)].find(match => match[1] === `../gstack-${review}/SKILL.md`)![1]!;
|
|
const loaded = fs.readFileSync(path.resolve(path.dirname(entry), reference), 'utf8');
|
|
expect(loaded).toBe(fs.readFileSync(generatedReview, 'utf8'));
|
|
expect(loaded).not.toContain('STALE FOREIGN HARNESS SKILL');
|
|
if (host.name === 'codex') expect(loaded).toContain('"outside_provider":"claude-code"');
|
|
const phase = review === 'plan-devex-review' ? 'dx' : review.split('-')[1]!;
|
|
const phaseBody = host.name === 'claude'
|
|
? fs.readFileSync(path.join(generatedRoot, 'autoplan', 'sections', `${phase}-phase.md`), 'utf8')
|
|
: body;
|
|
const directive = phaseBody.split('\n').find(line => line.startsWith('Before dispatch, Read ')
|
|
&& (line.includes(`methodology ${phase} `) || line.includes(`/${review}/SKILL.md`) || line.includes(`/gstack-${review}/SKILL.md`)));
|
|
expect(directive).toBeDefined();
|
|
expect(phaseBody.indexOf(directive!)).toBeLessThan(phaseBody.indexOf(`create ${phase} `));
|
|
expect(directive).toContain(`methodology ${phase} `);
|
|
expect(phaseBody).toContain(`create ${phase} \"<ACTIVE_PLAN>\" \"<RESTORE_PATH>\" \"<methodologyPath>\"`);
|
|
// U's CEO loaded this section only after its child finished. Pin the
|
|
// concrete prerequisite, then resolve the rendered path in real
|
|
// copy/symlink installations; references alone are not its contents.
|
|
if (host.name === 'claude') {
|
|
expect(directive).toContain(`methodology ${phase} `);
|
|
expect(directive).toContain('`methodologyPath` from');
|
|
expect(directive).toContain('"<REVIEW_SKILL>"');
|
|
const relative = 'sections/review-sections.md';
|
|
const sectionSource = path.join(generatedRoot, review, relative);
|
|
const sectionInstalled = path.resolve(path.dirname(entry), reference, '..', relative);
|
|
installFile(sectionSource, sectionInstalled, mode);
|
|
const section = fs.readFileSync(sectionInstalled, 'utf8');
|
|
expect(section).toBe(fs.readFileSync(sectionSource, 'utf8'));
|
|
expect(section).toContain('## Review Sections');
|
|
// W read the whole main file but skipped this section before Agent
|
|
// dispatch. A real helper invocation now supplies one complete
|
|
// byte-preserving target, instead of asking the model to concatenate.
|
|
expect(loaded).not.toContain('## Review Sections');
|
|
const restore = path.join(registry, 'restore.md');
|
|
fs.writeFileSync(restore, 'owned restore\n');
|
|
const skillPath = path.resolve(path.dirname(entry), reference);
|
|
assertBundle(methodology(phase, skillPath, restore), [skillPath, sectionInstalled]);
|
|
} else {
|
|
expect(directive).not.toContain('sections/review-sections.md');
|
|
expect(loaded).toContain('## Review Sections');
|
|
const restore = path.join(registry, 'restore.md');
|
|
fs.writeFileSync(restore, 'owned restore\n');
|
|
const skillPath = path.resolve(path.dirname(entry), reference);
|
|
assertBundle(methodology(phase, skillPath, restore), [skillPath]);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
test('host identity, not the model overlay, selects the registry; invalid skills fail closed', () => {
|
|
for (const host of ALL_HOST_CONFIGS) {
|
|
const ctx = { skillName: 'autoplan', tmplPath: '', host: host.name, paths: HOST_PATHS[host.name] } as TemplateContext;
|
|
for (const review of REVIEWS) {
|
|
expect(generateAutoplanReviewFile({ ...ctx, model: 'gpt' }, [review])).toBe(generateAutoplanReviewFile({ ...ctx, model: 'claude' }, [review]));
|
|
}
|
|
expect(() => generateAutoplanReviewFile(ctx, ['../foreign'])).toThrow();
|
|
expect(() => generateAutoplanReviewFile(ctx, ['plan-ceo-review', '../foreign'])).toThrow();
|
|
}
|
|
});
|
|
|
|
test('methodology validation fails before publication; repeated valid loads keep prior bytes', () => {
|
|
const registry = fs.mkdtempSync(path.join(owned, 'method-errors-'));
|
|
const entry = path.join(registry, 'SKILL.md');
|
|
const section = path.join(registry, 'sections/review-sections.md');
|
|
const restore = path.join(registry, 'restore.md');
|
|
fs.mkdirSync(path.dirname(section));
|
|
fs.writeFileSync(restore, 'restore remains exact\n');
|
|
const main = '---\r\nname: plan-ceo-review\r\n---\r\n## Section index\r\n| when | `sections/review-sections.md` |\r\n## Step 0\r\nFull first step 🌱\r\n';
|
|
const deep = '## Review Sections\r\nRequired late verification β\r\n';
|
|
fs.writeFileSync(entry, main);
|
|
const reject = () => {
|
|
const before = fs.readdirSync(registry).sort();
|
|
const result = methodology('ceo', entry, restore);
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain('gstack-autoplan-snapshot:');
|
|
expect(fs.readdirSync(registry).sort()).toEqual(before);
|
|
expect(fs.readFileSync(restore, 'utf8')).toBe('restore remains exact\n');
|
|
};
|
|
reject(); // required section absent
|
|
fs.writeFileSync(section, '```md\n## Review Sections\n```\n'); reject();
|
|
fs.writeFileSync(section, deep + 'Read `sections/missing.md`.\n'); reject();
|
|
fs.writeFileSync(section, deep);
|
|
fs.writeFileSync(entry, main.replace('name: plan-ceo-review', 'name: plan-eng-review')); reject();
|
|
fs.writeFileSync(entry, main.replace('## Step 0', '| extra | `sections/extra.md` |\r\n## Step 0')); reject();
|
|
fs.writeFileSync(entry, main);
|
|
const first = assertBundle(methodology('ceo', entry, restore), [entry, section]);
|
|
const original = fs.readFileSync(first.methodologyPath);
|
|
const second = assertBundle(methodology('ceo', entry, restore), [entry, section]);
|
|
expect(second.methodologyPath).not.toBe(first.methodologyPath);
|
|
expect(fs.readFileSync(first.methodologyPath)).toEqual(original);
|
|
expect(fs.readFileSync(entry, 'utf8')).toBe(main);
|
|
expect(fs.readFileSync(section, 'utf8')).toBe(deep);
|
|
});
|
|
|
|
test('the new discovery contract selects the affected live autoplan workflows', () => {
|
|
for (const name of ['autoplan-dual-voice', 'carve-section-loading']) {
|
|
expect(E2E_TOUCHFILES[name]).toContain('test/autoplan-review-discovery.test.ts');
|
|
expect(E2E_TOUCHFILES[name]).toContain('scripts/resolvers/composition.ts');
|
|
}
|
|
});
|
|
});
|