test(modules): cover bin/gstack-modules and the loadouts schema; allow modules/ in pregate strays

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-28 16:53:11 +00:00
1 parent 00bd08f108
commit a919a41678
2 files changed
+58 -1

No files matched your search

+1 -1
View File
@@ -20,5 +20,5 @@
"windows-free-tests.yml/windows-free-shard": "bun run scripts/test-free-shards.ts --windows-only --list"
},
"warn": [],
"strays": { "allow": [] }
"strays": { "allow": ["modules/**"] }
}
+57
View File
@@ -5,9 +5,11 @@
* only exclusion coverage; committed bytes are covered by Skill Docs Freshness.
*/
import { afterAll, 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';
import { bunIsolationArgs } from '../lib/bun-isolation';
import { MODEL_CATALOG } from '../lib/model-catalog';
import { runGeneration } from '../scripts/gen-skill-docs';
import { ModuleError, excludedSkillDirs, loadLoadouts, moduleCatalog, normalizeModelId, rungFor, selectModules, validateLoadouts, type ModuleErrorCode } from '../scripts/modules';
@@ -217,3 +219,58 @@ describe('gstack-modules render', () => {
expect(result.diagnostics.filter(d => d.kind === 'stale' || d.kind === 'error')).toEqual([]);
});
});
describe('modules/loadouts.schema.json', () => {
const schema = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules/loadouts.schema.json'), 'utf-8'));
const raw = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules/loadouts.json'), 'utf-8'));
test('the manifest carries every required key and no key the schema lacks', () => {
for (const key of schema.required) expect(raw).toHaveProperty(key);
for (const key of Object.keys(raw)) expect(Object.keys(schema.properties)).toContain(key);
expect(raw.version).toBe(schema.properties.version.const);
});
test('rung names and patterns agree between the schema and the manifest', () => {
const rungs = schema.properties.rungs.required as string[];
expect(Object.keys(raw.rungs).sort()).toEqual([...rungs].sort());
expect([...schema.properties.aliases.additionalProperties.enum].sort()).toEqual([...rungs].sort());
const pattern = new RegExp(schema.properties.rungs.properties.frontier.items.pattern);
for (const rung of rungs) for (const glob of raw.rungs[rung]) expect(glob).toMatch(pattern);
});
});
describe('bin/gstack-modules', () => {
const run = (...args: string[]) => {
const result = spawnSync(process.execPath, [...bunIsolationArgs(), path.join(ROOT, 'bin/gstack-modules'), ...args], { cwd: ROOT, encoding: 'utf-8', timeout: 60_000 });
return { code: result.status, stdout: result.stdout, stderr: result.stderr };
};
test('validate and list read the committed manifest', () => {
expect(run('validate')).toMatchObject({ code: 0, stdout: expect.stringContaining('LOADOUTS: ok') });
const listed = JSON.parse(run('list', '--json').stdout);
expect(listed.modules.map((m: { id: string }) => m.id)).toEqual(catalog.map(e => e.id));
const pinned = run('list', '--pinned').stdout;
expect(pinned).toContain('preamble:bash');
expect(pinned).not.toContain('skill:design-shotgun');
});
test('refused inputs exit 2 with their code; unknown commands print usage', () => {
const out = path.join(tmp, 'cli-refused');
expect(run('render', '--host', 'claude', '--modules', 'full,-skill:nope', '--out-dir', out)).toMatchObject({ code: 2, stderr: expect.stringContaining('MODULE_UNKNOWN') });
expect(run('render', '--host', 'claude', '--modules', 'full,-preamble:bash', '--out-dir', out)).toMatchObject({ code: 2, stderr: expect.stringContaining('MODULE_PINNED') });
expect(run('render', '--host', 'claude', '--loadout', 'full', '--out-dir', out, '--plan-mode', 'maybe')).toMatchObject({ code: 2, stderr: expect.stringContaining('MODULE_SPEC_INVALID') });
expect(run('render', '--host', 'claude', '--loadout', 'full', '--out-dir', out, '--model', 'no-such-model')).toMatchObject({ code: 2, stderr: expect.stringContaining('MODULE_SPEC_INVALID') });
expect(run('render', '--host', 'claude', '--out-dir', out)).toMatchObject({ code: 2, stderr: expect.stringContaining('MODULE_SPEC_INVALID') });
expect(fs.existsSync(out)).toBe(false);
expect(run('bogus')).toMatchObject({ code: 2, stderr: expect.stringContaining('Usage:') });
expect(run('--help')).toMatchObject({ code: 0, stdout: expect.stringContaining('gstack-modules render') });
});
test('render prints one RENDERED line and writes the lock', () => {
const out = path.join(tmp, 'cli-render');
const result = run('render', '--host', 'claude', '--modules', 'full,-skill:design-shotgun', '--out-dir', out);
expect(result.code).toBe(0);
expect(result.stdout).toMatch(/^RENDERED: loadout=adhoc host=claude files=\d+ bytes=\d+ excluded=1 lock=/);
expect(JSON.parse(fs.readFileSync(path.join(out, LOCK_FILE), 'utf-8')).excluded).toEqual(['skill:design-shotgun']);
});
});