From cf2d66ca0d801d7c461138b7f093e9c7d9d8533a Mon Sep 17 00:00:00 2001 From: garrytan Date: Mon, 26 Oct 2026 00:51:06 +0000 Subject: [PATCH] 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> --- test/proactive-core-set.test.ts | 39 ++++++++++++++++ test/proactive-skill-start.test.ts | 75 ++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 test/proactive-skill-start.test.ts diff --git a/test/proactive-core-set.test.ts b/test/proactive-core-set.test.ts index 4caf55910..304dd8780 100644 --- a/test/proactive-core-set.test.ts +++ b/test/proactive-core-set.test.ts @@ -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 = { + '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'); + }); +}); diff --git a/test/proactive-skill-start.test.ts b/test/proactive-skill-start.test.ts new file mode 100644 index 000000000..72d43d55a --- /dev/null +++ b/test/proactive-skill-start.test.ts @@ -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 }); + } + } + }); +});