mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-10 18:50:18 +01:00
fix(regen): report missing dependencies as REGEN_DEPS_MISSING, not REGEN_STALE
When package.json declares dependencies and node_modules is missing, the scratch worktree runs the generator without them. A generator that degrades and still exits 0 then looked stale, and the fix line sent people to rerun the generator. gstack-regen check and the pregate regen check now report "dependencies missing; run <install>" with its own result code and troubleshooting anchor; the install command follows the lockfile. The fixture reproduces the shape GSTA-31 saw in gbrain. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
9b6e23fd31
commit
c90e39c19a
9 files changed
+117
-8
No files matched your search
@@ -9,13 +9,24 @@ import { afterEach, describe, expect, test } from 'bun:test';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { EXIT } from '../lib/headless-artifacts';
|
||||
import { parseRegistry, selectEntries, validateRegistry } from '../lib/regen';
|
||||
import { missingDependencies, parseRegistry, selectEntries, validateRegistry } from '../lib/regen';
|
||||
import { BASE_REGISTRY, cleanup, commit, makeFixtureRepo, readFile, runBin, write, type FixtureRepo } from './helpers/pregate-fixture';
|
||||
|
||||
const repos: FixtureRepo[] = [];
|
||||
const fixture = (opts?: Parameters<typeof makeFixtureRepo>[0]) => { const r = makeFixtureRepo(opts); repos.push(r); return r; };
|
||||
afterEach(() => { while (repos.length) cleanup(repos.pop()!); });
|
||||
|
||||
/** gbrain's shape (GSTA-31): the generator degrades without its dependency and still exits 0. */
|
||||
const DEPS_FIXTURE = {
|
||||
registry: { entries: [{ id: 'digest', command: 'node gen.js', outputs: ['generated/**'], inputs: ['src/version.js', 'gen.js'] }] },
|
||||
baseFiles: {
|
||||
'package.json': JSON.stringify({ name: 'fixture', version: '1.0.0', dependencies: { 'fixture-dep': '1.0.0' } }, null, 2) + '\n',
|
||||
'bun.lock': '{}\n',
|
||||
'gen.js': "const fs=require('fs');const {VERSION}=require('./src/version.js');let style='plain';try{style=require('fixture-dep').style;}catch{}fs.writeFileSync('generated/digest.md',`# digest v${VERSION} (${style})\\n`);\n",
|
||||
'generated/digest.md': '# digest v1.0.0 (fancy)\n',
|
||||
},
|
||||
};
|
||||
|
||||
describe('validateRegistry (schema + containment, pure)', () => {
|
||||
test('the fixture registry validates; command null is a listed entry; inputs default to always', () => {
|
||||
expect(validateRegistry(BASE_REGISTRY)).toEqual([]);
|
||||
@@ -147,6 +158,43 @@ describe('gstack-regen (bin)', () => {
|
||||
expect(all.out).toContain('regen digest: stale');
|
||||
});
|
||||
|
||||
test('missing dependencies: a generator that exits 0 without node_modules is REGEN_DEPS_MISSING (install, then rerun), not REGEN_STALE; installed deps make it fresh', () => {
|
||||
const repo = fixture(DEPS_FIXTURE);
|
||||
commit(repo, { 'src/version.js': "module.exports = { VERSION: '1.0.0' };\n// touched\n" });
|
||||
const r = runBin(repo, 'gstack-regen', ['check', '--base', 'main', '--allow-repo-commands']);
|
||||
expect(r.status).toBe(EXIT.fail);
|
||||
expect(r.out).toMatch(/^regen digest: deps-missing \(\d+ ms\) — dependencies missing \(no node_modules\); run bun install, then rerun the check — differs without them: generated\/digest\.md \(REGEN_DEPS_MISSING\)$/m);
|
||||
expect(r.out).not.toContain('REGEN_STALE');
|
||||
expect(r.out).toContain('GSTACK_RESULT: skill=regen status=incomplete');
|
||||
const json = JSON.parse(runBin(repo, 'gstack-regen', ['check', '--base', 'main', '--allow-repo-commands', '--json']).out);
|
||||
expect(json.results.map((x: any) => [x.id, x.status])).toEqual([['digest', 'deps-missing']]);
|
||||
expect(repo.git('status', '--porcelain')).toBe('');
|
||||
|
||||
write(repo.work, 'node_modules/fixture-dep/index.js', "exports.style = 'fancy';\n");
|
||||
const installed = runBin(repo, 'gstack-regen', ['check', '--base', 'main', '--allow-repo-commands']);
|
||||
expect(installed.status).toBe(EXIT.ok);
|
||||
expect(installed.out).toContain('regen digest: fresh');
|
||||
|
||||
// With dependencies installed, a real input change is still stale.
|
||||
commit(repo, { 'src/version.js': "module.exports = { VERSION: '1.1.0' };\n" });
|
||||
const stale = runBin(repo, 'gstack-regen', ['check', '--base', 'main', '--allow-repo-commands']);
|
||||
expect(stale.status).toBe(EXIT.fail);
|
||||
expect(stale.out).toContain('regen digest: stale');
|
||||
expect(stale.out).toContain('(REGEN_STALE)');
|
||||
expect(stale.out).not.toContain('REGEN_DEPS_MISSING');
|
||||
});
|
||||
|
||||
test('missing dependencies: the install command follows the lockfile', () => {
|
||||
const repo = fixture(DEPS_FIXTURE);
|
||||
expect(missingDependencies(repo.work)).toBe('bun install');
|
||||
fs.rmSync(path.join(repo.work, 'bun.lock'));
|
||||
write(repo.work, 'package-lock.json', '{}\n');
|
||||
expect(missingDependencies(repo.work)).toBe('npm install');
|
||||
fs.mkdirSync(path.join(repo.work, 'node_modules'));
|
||||
expect(missingDependencies(repo.work)).toBeNull();
|
||||
expect(missingDependencies(fixture().work)).toBeNull();
|
||||
});
|
||||
|
||||
test('validate, list and init: an invalid working-tree registry is REGEN_REGISTRY_INVALID; init refuses to overwrite', () => {
|
||||
const repo = fixture();
|
||||
expect(runBin(repo, 'gstack-regen', ['validate']).out).toContain('REGISTRY_VALID: .gstack/generated.json');
|
||||
|
||||
@@ -221,6 +221,23 @@ describe('gstack-pregate (bin)', () => {
|
||||
expect(readOut(repo).stage2_ms).toBeUndefined();
|
||||
});
|
||||
|
||||
test('missing dependencies: regen is incomplete with REGEN_DEPS_MISSING and the install command, not REGEN_STALE', () => {
|
||||
const repo = fixture({
|
||||
registry: { entries: [{ id: 'digest', command: 'node gen.js', outputs: ['generated/**'], inputs: ['src/version.js', 'gen.js'] }] },
|
||||
baseFiles: {
|
||||
'package.json': JSON.stringify({ name: 'fixture', version: '1.0.0', scripts: { test: 'node --test test/*.test.js' }, devDependencies: { 'fixture-dep': '1.0.0' } }, null, 2) + '\n',
|
||||
'gen.js': "const fs=require('fs');const {VERSION}=require('./src/version.js');let style='plain';try{style=require('fixture-dep').style;}catch{}fs.writeFileSync('generated/digest.md',`# digest v${VERSION} (${style})\\n`);\n",
|
||||
'generated/digest.md': '# digest v1.0.0 (fancy)\n',
|
||||
},
|
||||
});
|
||||
commit(repo, { 'src/version.js': "module.exports = { VERSION: '1.0.0' };\n// touched\n" });
|
||||
const r = pregate(repo, ['--stage', 'preflight', '--only', 'regen']);
|
||||
expect(r.status).not.toBe(EXIT.ok);
|
||||
expect(r.out).toMatch(/^preflight {2}regen {5}incomplete .*dependencies missing; run bun install, then rerun the check \(1 entry unverified: digest\) \(REGEN_DEPS_MISSING\)/m);
|
||||
expect(r.err).toContain('(REGEN_DEPS_MISSING)');
|
||||
expect(r.out + r.err).not.toContain('REGEN_STALE');
|
||||
});
|
||||
|
||||
test('p0-4-scratch-script: zz-probe.sh at the root fails strays (downgraded to warn only when the base config says so); a conventional root file passes', () => {
|
||||
const strict = fixture({ pregate: { ...BASE_PREGATE, warn: [] } });
|
||||
commit(strict, { 'zz-probe.sh': '#!/bin/sh\necho hi\n', 'CONTRIBUTING.md': '# c\n', 'notes.txt': 'x\n', 'newtop/thing.js': 'module.exports = 1;\n' });
|
||||
|
||||
Reference in new issue
Block a user