test(proactive): STATUS line carries the stored value; every surface names the same core four and three values

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-26 00:51:06 +00:00
1 parent 9e22d7f326
commit cf2d66ca0d
2 files changed
+114

No files matched your search

+39
View File
@@ -84,3 +84,42 @@ describe('proactive core set: the one-time prompt and the registry', () => {
expect(QUESTIONS['preamble-proactive-behavior'].options).toEqual(['core', 'all', 'off']);
});
});
/**
* Value: protects=the core four and the three proactive values agree across
* the config header, the default table, the router, the preamble, the one-time
* prompt and the docs; fails_when=the core set or a value name changes on one
* surface only; why_new=the tests above pin the template, preamble and prompt
* options but not the docs, nor the skill names the prompt and header speak;
* seam=none; no_claim=none beyond protects (static text, no model run)
*/
describe('proactive core set: every surface names the same four skills and three values', () => {
const configSrc = fs.readFileSync(path.join(ROOT, 'bin', 'gstack-config'), 'utf-8');
const promptAt = skillStart.indexOf('_emit_block proactive-prompt');
const docsSkills = fs.readFileSync(path.join(ROOT, 'docs', 'skills.md'), 'utf-8');
const docsSkillsAt = docsSkills.indexOf('## Proactive skill suggestions');
const surfaces: Record<string, string> = {
'bin/gstack-config header': configSrc.slice(configSrc.indexOf('# proactive:'), configSrc.indexOf('# routing_declined:')),
'bin/gstack-config lookup_default': configSrc.slice(configSrc.indexOf('lookup_default()')).match(/^\s*proactive\).*$/m)?.[0] ?? '',
'SKILL.md.tmpl core block': routerTmpl.slice(routerTmpl.indexOf('If `PROACTIVE` is `core` (default)'), routerTmpl.indexOf('If `PROACTIVE` is `all`')),
'preamble rule': generateUpgradeCheck({ paths: { skillRoot: '~/.claude/skills/gstack' } } as never),
'gstack-skill-start prompt': skillStart.slice(promptAt, skillStart.indexOf('.proactive-prompted"\\`', promptAt)),
'docs/proactive.md': fs.readFileSync(path.join(ROOT, 'docs', 'proactive.md'), 'utf-8'),
'docs/skills.md section': docsSkills.slice(docsSkillsAt, docsSkills.indexOf('\n## ', docsSkillsAt + 1)),
};
test.each(Object.entries(surfaces))('%s names the core four and no sampled non-core skill', (name, text) => {
expect(text.length, name).toBeGreaterThan(20);
for (const skill of CORE) expect(text, `${name} lacks ${skill}`).toContain(skill);
for (const skill of NON_CORE_SAMPLE) expect(text, `${name} names ${skill}`).not.toContain(skill);
});
test('the docs and the config header spell the three values, and the router names the restore-default command', () => {
for (const name of ['docs/proactive.md', 'docs/skills.md section', 'bin/gstack-config header']) {
for (const value of ['core', 'all', 'off']) expect(surfaces[name], `${name} lacks ${value}`).toMatch(new RegExp(`\\b${value}\\b`));
}
expect(surfaces['docs/proactive.md']).toContain('`true` and `false`');
for (const value of ['all', 'off', 'core']) expect(surfaces['docs/proactive.md']).toContain(`gstack-config set proactive ${value}`);
expect(routerTmpl.replace(/\s+/g, ' ')).toContain('`gstack-config set proactive core` restores the default');
});
});
+75
View File
@@ -0,0 +1,75 @@
/**
* The PROACTIVE STATUS line (GSTA-93 fix 12, proactive core default): every
* skill reads `PROACTIVE:` from bin/gstack-skill-start, which asks
* bin/gstack-config and falls back to a literal when the config binary is
* missing. The default moved from `true` to `core`.
*
* Value: protects=the PROACTIVE STATUS line a skill reads carries the stored
* proactive value and core when unset; fails_when=gstack-skill-start stops
* consulting gstack-config for proactive or the default moves back to true;
* why_new=proactive-config.test.ts exercises gstack-config alone and
* gstack-skill-start.test.ts asserts only that the key is emitted; seam=none;
* no_claim=how the model applies the value (prose, not executed here)
*/
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 { runBin } from './helpers/run-bin';
const ROOT = path.resolve(import.meta.dir, '..');
const BIN = path.join(ROOT, 'bin');
let tmpHome: string;
beforeAll(() => {
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-proactive-ss-home-'));
});
afterAll(() => {
fs.rmSync(tmpHome, { recursive: true, force: true });
});
function freshState(config: string): string {
const state = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-proactive-ss-state-'));
fs.writeFileSync(path.join(state, 'config.yaml'), config);
return state;
}
function runStart(script: string, state: string): string {
const r = runBin(script, ['--skill', 'testskill'], {
cwd: tmpHome,
timeoutMs: 30_000,
env: {
PATH: process.env.PATH,
HOME: tmpHome,
GSTACK_HOME: state,
GSTACK_STATE_DIR: state,
GSTACK_LEASE_DIR: path.join(state, 'leases'),
},
});
expect(r.status, r.stderr).toBe(0);
return r.stdout;
}
const proactiveLine = (out: string) => out.match(/^PROACTIVE: (.*)$/m)?.[1];
describe('gstack-skill-start PROACTIVE status line', () => {
test('carries core when nothing is stored, and the stored value (new or legacy spelling) otherwise', () => {
const cases: Array<[string, string]> = [
['update_check: false\n', 'core'],
['update_check: false\nproactive: all\n', 'all'],
['update_check: false\nproactive: off\n', 'off'],
['update_check: false\nproactive: true\n', 'true'],
];
for (const [config, expected] of cases) {
const state = freshState(config);
try {
expect(proactiveLine(runStart(path.join(BIN, 'gstack-skill-start'), state)), config).toBe(expected);
} finally {
fs.rmSync(state, { recursive: true, force: true });
}
}
});
});