mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 14:38:59 +02:00
test: repin ~70 assertions to the script contract — every literal gets a successor
Assertions that pinned inline-bash internals (update-check guard, _SESSIONS reaping, telemetry start/end blocks, routing probe, repo-strip producer, first-task gating, EXPLAIN_LEVEL/QUESTION_TUNING echoes, #2499 jq scope resolution, Issue-8 CONDUCTOR gate) now pin the same invariants in their new home: bin/gstack-skill-start / bin/gstack-skill-end file content for script internals, the invocation fence + interpretation prose for render-side behavior. No assertion deleted without a successor; live-execution tests (routing probe, brain-sync jq) run against script bytes unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
eb1607aaf8
commit
17bebe33ab
@@ -35,25 +35,30 @@ describe('Audit compliance', () => {
|
||||
|
||||
// Fix 2: Conditional telemetry — binary calls wrapped with existence check
|
||||
test('preamble telemetry calls are conditional on _TEL and binary existence', () => {
|
||||
// After the preamble.ts refactor (Item 9), the bash/telemetry logic lives
|
||||
// in submodules under scripts/resolvers/preamble/. Concatenate all preamble
|
||||
// source (root + submodules) and assert against the combined text so this
|
||||
// test tracks the semantic contract, not the file layout.
|
||||
// Token-reduction Phase 1: the preamble's telemetry bash moved from the
|
||||
// resolvers into bin/gstack-skill-start (pending finalization) and
|
||||
// bin/gstack-skill-end (end-of-skill telemetry). Assert the semantic
|
||||
// contract against the scripts — the new home of the calls.
|
||||
const skillStart = readFileSync(join(ROOT, 'bin/gstack-skill-start'), 'utf-8');
|
||||
// Pending finalization must check _TEL and binary existence
|
||||
expect(skillStart).toContain('_TEL" != "off"');
|
||||
expect(skillStart).toContain('-x ');
|
||||
expect(skillStart).toContain('gstack-telemetry-log');
|
||||
// End-of-skill telemetry (gstack-skill-end) must also be conditional
|
||||
const skillEnd = readFileSync(join(ROOT, 'bin/gstack-skill-end'), 'utf-8');
|
||||
expect(skillEnd).toContain('_TEL" != "off"');
|
||||
expect(skillEnd).toContain('-x ');
|
||||
expect(skillEnd).toContain('gstack-telemetry-log');
|
||||
// The render-side epilogue prose survives in the resolvers and hands off
|
||||
// to gstack-skill-end.
|
||||
const preambleDir = join(ROOT, 'scripts/resolvers/preamble');
|
||||
const submoduleFiles = existsSync(preambleDir)
|
||||
? readdirSync(preambleDir).filter(f => f.endsWith('.ts')).map(f => readFileSync(join(preambleDir, f), 'utf-8'))
|
||||
: [];
|
||||
const rootPreamble = readFileSync(join(ROOT, 'scripts/resolvers/preamble.ts'), 'utf-8');
|
||||
const preamble = [rootPreamble, ...submoduleFiles].join('\n');
|
||||
// Pending finalization must check _TEL and binary existence
|
||||
expect(preamble).toContain('_TEL" != "off"');
|
||||
expect(preamble).toContain('-x ');
|
||||
expect(preamble).toContain('gstack-telemetry-log');
|
||||
// End-of-skill telemetry must also be conditional
|
||||
const preamble = submoduleFiles.join('\n');
|
||||
const completionIdx = preamble.indexOf('Telemetry (run last)');
|
||||
expect(completionIdx).toBeGreaterThan(-1);
|
||||
const completionSection = preamble.slice(completionIdx);
|
||||
expect(completionSection).toContain('_TEL" != "off"');
|
||||
expect(preamble.slice(completionIdx)).toContain('gstack-skill-end');
|
||||
});
|
||||
|
||||
// Round 2 Fix 1: W012 — Bun install uses checksum verification
|
||||
@@ -111,11 +116,27 @@ describe('Audit compliance', () => {
|
||||
// Round 2 Fix 4: Chrome CDP binds to localhost only
|
||||
// Fix 2+6: All generated SKILL.md files with telemetry are conditional
|
||||
test('all generated SKILL.md files with telemetry calls use conditional pattern', () => {
|
||||
// Phase 1 moved the _TEL-gated bash into the scripts. Render-side
|
||||
// gstack-telemetry-log calls (route + first-task events) rely on two
|
||||
// layers instead: every call line is best-effort (`|| true`), and the
|
||||
// binary itself no-ops when the telemetry tier is off.
|
||||
const telLog = readFileSync(join(ROOT, 'bin/gstack-telemetry-log'), 'utf-8');
|
||||
expect(telLog).toContain('if [ "$TIER" = "off" ]');
|
||||
expect(telLog).toMatch(/if \[ "\$TIER" = "off" \][\s\S]{0,200}?exit 0/);
|
||||
|
||||
const skills = getAllSkillMds();
|
||||
let checked = 0;
|
||||
for (const { name, content } of skills) {
|
||||
if (content.includes('gstack-telemetry-log')) {
|
||||
expect(content).toContain('_TEL" != "off"');
|
||||
for (const line of content.split('\n')) {
|
||||
if (!line.includes('gstack-telemetry-log')) continue;
|
||||
// Prose mentions aren't calls; only executable lines invoke the binary.
|
||||
if (!line.includes('bin/gstack-telemetry-log')) continue;
|
||||
checked++;
|
||||
expect(line, `${name}: telemetry call must be best-effort`).toContain('|| true');
|
||||
expect(line, `${name}: telemetry call must not surface errors`).toContain('2>/dev/null');
|
||||
}
|
||||
}
|
||||
// Guard against the scan silently matching nothing.
|
||||
expect(checked).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
+89
-42
@@ -28,6 +28,15 @@ function readShipUnion(): string {
|
||||
return readSkillUnion('ship');
|
||||
}
|
||||
|
||||
// Token-reduction Phase 1: the preamble's inline bash (session bookkeeping,
|
||||
// config echoes, telemetry producers, artifacts sync) moved into
|
||||
// bin/gstack-skill-start / bin/gstack-skill-end. The render carries a one-line
|
||||
// invocation fence + interpretation prose. Assertions that pinned inline-bash
|
||||
// internals now pin the scripts (the new home); render-side assertions pin the
|
||||
// fence + prose. Script behavior is pinned by test/gstack-skill-start.test.ts.
|
||||
const SKILL_START_SCRIPT = fs.readFileSync(path.join(ROOT, 'bin', 'gstack-skill-start'), 'utf-8');
|
||||
const SKILL_END_SCRIPT = fs.readFileSync(path.join(ROOT, 'bin', 'gstack-skill-end'), 'utf-8');
|
||||
|
||||
function extractDescription(content: string): string {
|
||||
const fmEnd = content.indexOf('\n---', 4);
|
||||
expect(fmEnd).toBeGreaterThan(0);
|
||||
@@ -315,7 +324,9 @@ describe('gen-skill-docs', () => {
|
||||
expect(content).not.toContain('contributor-logs');
|
||||
expect(content).toContain('Operational Self-Improvement');
|
||||
expect(content).toContain('gstack-learnings-log');
|
||||
expect(content).toContain('gstack-learnings-search --limit 3');
|
||||
// The learnings-resurface call moved from the inline preamble bash into
|
||||
// the skill-start script (Phase 1) — same command, new home.
|
||||
expect(SKILL_START_SCRIPT).toContain('gstack-learnings-search" --limit 3');
|
||||
});
|
||||
|
||||
test('generated SKILL.md with LEARNINGS_LOG contains operational type', () => {
|
||||
@@ -324,16 +335,20 @@ describe('gen-skill-docs', () => {
|
||||
expect(content).toContain('operational');
|
||||
});
|
||||
|
||||
test('generated SKILL.md contains session awareness', () => {
|
||||
test('session awareness lives in gstack-skill-start (registry touch + stale cleanup)', () => {
|
||||
// The sessions registry moved from inline preamble bash into the script:
|
||||
// it records the harness pid (--parent-pid identity) and expires entries
|
||||
// older than 120 minutes.
|
||||
expect(SKILL_START_SCRIPT).toContain('sessions/$PARENT_PID');
|
||||
expect(SKILL_START_SCRIPT).toContain('-mmin +120');
|
||||
// The render keeps the completion-status protocol the sessions feed into.
|
||||
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('_SESSIONS');
|
||||
expect(content).toContain('RECOMMENDATION');
|
||||
});
|
||||
|
||||
test('generated SKILL.md contains branch detection', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('_BRANCH');
|
||||
expect(content).toContain('git branch --show-current');
|
||||
test('branch detection lives in gstack-skill-start and is echoed as BRANCH', () => {
|
||||
expect(SKILL_START_SCRIPT).toContain('_BRANCH=$(git branch --show-current');
|
||||
expect(SKILL_START_SCRIPT).toContain('echo "BRANCH: $_BRANCH"');
|
||||
});
|
||||
|
||||
// #2001: update_check: false silences the binary but the upgrade-handling
|
||||
@@ -343,19 +358,22 @@ describe('gen-skill-docs', () => {
|
||||
// JUST_UPGRADED prose on it — the same echo-then-gate convention every other
|
||||
// flag (PROACTIVE, SKILL_PREFIX, EXPLAIN_LEVEL, QUESTION_TUNING) follows.
|
||||
test('update_check opt-out gates preamble echo and upgrade-handling prose (issue #2001)', () => {
|
||||
// The config-echo cluster moved into gstack-skill-start: the flag must be
|
||||
// read and echoed there so the render's instruction layer can act on it.
|
||||
expect(SKILL_START_SCRIPT, 'script must read update_check config').toContain('_UPDATE_CHECK=$(');
|
||||
expect(SKILL_START_SCRIPT, 'script must echo UPDATE_CHECK').toContain('echo "UPDATE_CHECK: $_UPDATE_CHECK"');
|
||||
// Whenever a preamble-carrying render ships the upgrade-handling prose, it
|
||||
// must gate on the echoed flag — same echo-then-gate convention as
|
||||
// PROACTIVE/SKILL_PREFIX. (gstack-upgrade itself is out of scope: it has
|
||||
// no preamble fence and handling UPGRADE_AVAILABLE is its whole job.)
|
||||
let checked = 0;
|
||||
for (const skill of CLAUDE_GENERATED_SKILLS) {
|
||||
const content = fs.readFileSync(path.join(ROOT, skill.dir, 'SKILL.md'), 'utf-8');
|
||||
// Scope: only skills that render the runtime config-echo cluster.
|
||||
if (!content.includes('echo "QUESTION_TUNING: $_QUESTION_TUNING"')) continue;
|
||||
if (!content.includes('gstack-skill-start')) continue;
|
||||
if (!content.includes('UPGRADE_AVAILABLE <old> <new>')) continue;
|
||||
checked++;
|
||||
expect(content, `${skill.dir} must echo UPDATE_CHECK`).toContain('echo "UPDATE_CHECK: $_UPDATE_CHECK"');
|
||||
expect(content, `${skill.dir} must read update_check config`).toContain('_UPDATE_CHECK=$(');
|
||||
// Whenever the upgrade-handling prose ships, it must gate on the flag.
|
||||
if (content.includes('UPGRADE_AVAILABLE <old> <new>')) {
|
||||
expect(content, `${skill.dir} upgrade prose must gate on UPDATE_CHECK`)
|
||||
.toContain('If `UPDATE_CHECK` is `"false"`');
|
||||
}
|
||||
expect(content, `${skill.dir} upgrade prose must gate on UPDATE_CHECK`)
|
||||
.toContain('If `UPDATE_CHECK` is `"false"`');
|
||||
}
|
||||
// Guard against the scope filter silently matching nothing.
|
||||
expect(checked).toBeGreaterThan(0);
|
||||
@@ -377,9 +395,12 @@ describe('gen-skill-docs', () => {
|
||||
expect(content).not.toContain('## Completeness Principle');
|
||||
});
|
||||
|
||||
test('generated SKILL.md contains telemetry line', () => {
|
||||
test('telemetry producer lives in the scripts; render documents the analytics sink', () => {
|
||||
// The skill-usage.jsonl producers moved into the scripts (Phase 1).
|
||||
expect(SKILL_START_SCRIPT).toContain('analytics/skill-usage.jsonl');
|
||||
expect(SKILL_END_SCRIPT).toContain('analytics/skill-usage.jsonl');
|
||||
// The render still tells the model where telemetry lands.
|
||||
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('skill-usage.jsonl');
|
||||
expect(content).toContain('~/.gstack/analytics');
|
||||
});
|
||||
|
||||
@@ -499,7 +520,13 @@ describe('gen-skill-docs', () => {
|
||||
];
|
||||
for (const skill of PREAMBLE_SKILLS) {
|
||||
const content = fs.readFileSync(path.join(ROOT, skill.dir, 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain(`"skill":"${skill.name}"`);
|
||||
// The skill name now travels as --skill into gstack-skill-start (the
|
||||
// preamble fence) and gstack-skill-end (the telemetry epilogue) — the
|
||||
// scripts write it into the JSONL events.
|
||||
expect(content, `${skill.dir} preamble fence must pass its own name`)
|
||||
.toMatch(new RegExp(`--skill "${skill.name}" --model`));
|
||||
expect(content, `${skill.dir} epilogue must pass its own name`)
|
||||
.toContain(`gstack-skill-end --skill "${skill.name}"`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1563,11 +1590,13 @@ describe('parameterized resolver support', () => {
|
||||
describe('preamble routing injection', () => {
|
||||
const shipContent = readShipUnion();
|
||||
|
||||
test('preamble bash checks for routing section in CLAUDE.md and AGENTS.md', () => {
|
||||
test('routing probe checks CLAUDE.md and AGENTS.md (now in gstack-skill-start)', () => {
|
||||
// #2500: the probe iterates CLAUDE.md AND AGENTS.md — non-Claude hosts
|
||||
// route skills via AGENTS.md, the cross-harness convention file.
|
||||
expect(shipContent).toContain('for _RF in CLAUDE.md AGENTS.md');
|
||||
expect(shipContent).toContain('grep -q "## Skill routing" "$_RF"');
|
||||
// route skills via AGENTS.md, the cross-harness convention file. The bash
|
||||
// moved into gstack-skill-start; the render acts on the echoed HAS_ROUTING.
|
||||
expect(SKILL_START_SCRIPT).toContain('for _RF in CLAUDE.md AGENTS.md');
|
||||
expect(SKILL_START_SCRIPT).toContain('grep -q "## Skill routing" "$_RF"');
|
||||
expect(SKILL_START_SCRIPT).toContain('echo "HAS_ROUTING: $_HAS_ROUTING"');
|
||||
expect(shipContent).toContain('HAS_ROUTING');
|
||||
});
|
||||
|
||||
@@ -2114,7 +2143,9 @@ describe('Codex generation (--host codex)', () => {
|
||||
expect(override.exitCode).toBe(0);
|
||||
const content = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-ship', 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('Model-Specific Behavioral Patch (claude)');
|
||||
expect(content).toContain('MODEL_OVERLAY: claude');
|
||||
// The overlay now travels as --model into gstack-skill-start, which
|
||||
// echoes MODEL_OVERLAY at runtime.
|
||||
expect(content).toContain('--model "claude"');
|
||||
} finally {
|
||||
// Restore the host-default render — later tests and the host-config
|
||||
// golden read this tree.
|
||||
@@ -2127,6 +2158,7 @@ describe('Codex generation (--host codex)', () => {
|
||||
}
|
||||
const restored = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-ship', 'SKILL.md'), 'utf-8');
|
||||
expect(restored).toContain('Model-Specific Behavioral Patch (gpt)');
|
||||
expect(restored).toContain('--model "gpt"');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2813,13 +2845,18 @@ describe('discover-skills hidden directory filtering', () => {
|
||||
});
|
||||
|
||||
describe('telemetry', () => {
|
||||
test('generated SKILL.md contains telemetry start block', () => {
|
||||
test('telemetry start block lives in gstack-skill-start; render notes the handoff keys', () => {
|
||||
// The start-block bash moved into the script (Phase 1): it reads the
|
||||
// config, mints the session identity, and echoes the STATUS keys.
|
||||
expect(SKILL_START_SCRIPT).toContain('_TEL_START=$(date +%s)');
|
||||
expect(SKILL_START_SCRIPT).toContain('_SESSION_ID=');
|
||||
expect(SKILL_START_SCRIPT).toContain('echo "TELEMETRY:');
|
||||
expect(SKILL_START_SCRIPT).toContain('echo "TEL_PROMPTED:');
|
||||
expect(SKILL_START_SCRIPT).toMatch(/gstack-config" get telemetry/);
|
||||
// The render must tell the model to carry SESSION_ID/TEL_START to skill end.
|
||||
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('_TEL_START');
|
||||
expect(content).toContain('_SESSION_ID');
|
||||
expect(content).toContain('TELEMETRY:');
|
||||
expect(content).toContain('TEL_PROMPTED:');
|
||||
expect(content).toContain('gstack-config get telemetry');
|
||||
expect(content).toContain('SESSION_ID');
|
||||
expect(content).toContain('TEL_START');
|
||||
});
|
||||
|
||||
test('generated SKILL.md contains telemetry opt-in prompt', () => {
|
||||
@@ -2831,21 +2868,26 @@ describe('telemetry', () => {
|
||||
expect(content).toContain('gstack-config set telemetry off');
|
||||
});
|
||||
|
||||
test('generated SKILL.md contains telemetry epilogue', () => {
|
||||
test('generated SKILL.md contains telemetry epilogue (one gstack-skill-end call)', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('Telemetry (run last)');
|
||||
expect(content).toContain('gstack-telemetry-log');
|
||||
expect(content).toContain('_TEL_END');
|
||||
expect(content).toContain('_TEL_DUR');
|
||||
expect(content).toContain('SKILL_NAME');
|
||||
expect(content).toContain('OUTCOME');
|
||||
expect(content).toContain('gstack-skill-end --skill "gstack" --outcome OUTCOME');
|
||||
expect(content).toContain('--tel-start "TEL_START"');
|
||||
expect(content).toContain('PLAN MODE EXCEPTION');
|
||||
// The duration math + remote-log dispatch moved into gstack-skill-end.
|
||||
expect(SKILL_END_SCRIPT).toContain('_TEL_END');
|
||||
expect(SKILL_END_SCRIPT).toContain('_TEL_DUR');
|
||||
expect(SKILL_END_SCRIPT).toContain('SKILL_NAME');
|
||||
expect(SKILL_END_SCRIPT).toContain('OUTCOME');
|
||||
expect(SKILL_END_SCRIPT).toContain('gstack-telemetry-log');
|
||||
});
|
||||
|
||||
test('generated SKILL.md contains pending marker handling', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('.pending');
|
||||
expect(content).toContain('_pending_finalize');
|
||||
test('pending marker handling lives in the scripts', () => {
|
||||
// gstack-skill-start finalizes stale markers; gstack-skill-end clears the
|
||||
// session's own marker.
|
||||
expect(SKILL_START_SCRIPT).toContain("-name '.pending-*'");
|
||||
expect(SKILL_START_SCRIPT).toContain('_pending_finalize');
|
||||
expect(SKILL_END_SCRIPT).toContain('.pending-$SESSION_ID');
|
||||
});
|
||||
|
||||
test('telemetry blocks appear in all skill files that use PREAMBLE', () => {
|
||||
@@ -2854,8 +2896,9 @@ describe('telemetry', () => {
|
||||
const skillPath = path.join(ROOT, skill, 'SKILL.md');
|
||||
if (fs.existsSync(skillPath)) {
|
||||
const content = fs.readFileSync(skillPath, 'utf-8');
|
||||
expect(content).toContain('_TEL_START');
|
||||
expect(content).toContain('Telemetry (run last)');
|
||||
expect(content).toContain(`gstack-skill-end --skill "${skill}"`);
|
||||
expect(content).toContain('--tel-start "TEL_START"');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -3701,7 +3744,11 @@ describe('PREAMBLE resolution requires declared preamble-tier', () => {
|
||||
// user scope, so a correctly configured project-scoped brain was invisible.
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('brain-sync block reads project-scoped MCP registrations (#2499)', () => {
|
||||
const rendered = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
||||
// Phase 1: the artifacts-sync bash (including the MCP-scope jq probe) moved
|
||||
// from the rendered SKILL.md into bin/gstack-skill-start. Pin the LIVE
|
||||
// script bytes — same assertions, new home. The render carries only the
|
||||
// ARTIFACTS_SYNC interpretation prose.
|
||||
const rendered = fs.readFileSync(path.join(ROOT, 'bin', 'gstack-skill-start'), 'utf-8');
|
||||
|
||||
test('rendered _GBRAIN_MCP_ENTRY jq resolves project scope with nearest-ancestor cwd match', () => {
|
||||
const line = rendered.split('\n').find((l) => l.includes('_GBRAIN_MCP_ENTRY=$('));
|
||||
|
||||
@@ -475,7 +475,14 @@ describe('preamble — QUESTION_TUNING injection', () => {
|
||||
preambleTier: 2,
|
||||
};
|
||||
const out = generatePreamble(ctx);
|
||||
expect(out).toContain('QUESTION_TUNING: $_QUESTION_TUNING');
|
||||
// Phase 1: the config echo moved into bin/gstack-skill-start; the render's
|
||||
// section gates itself on the echoed key.
|
||||
const script = fs.readFileSync(
|
||||
path.join(import.meta.dir, '..', 'bin', 'gstack-skill-start'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(script).toContain('echo "QUESTION_TUNING: $_QUESTION_TUNING"');
|
||||
expect(out).toContain('QUESTION_TUNING: false');
|
||||
expect(out).toContain('## Question Tuning');
|
||||
expect(out).toContain('gstack-question-preference --check');
|
||||
expect(out).toContain('gstack-question-log');
|
||||
|
||||
@@ -71,12 +71,24 @@ describe('Preamble composition order', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Conductor signal (preamble bash)', () => {
|
||||
test('claude preamble emits CONDUCTOR_SESSION, gated on != headless (Issue 8)', () => {
|
||||
describe('Conductor signal (skill-start script)', () => {
|
||||
// Token-reduction Phase 1 moved the preamble bash into bin/gstack-skill-start;
|
||||
// the Issue-8 invariant (CONDUCTOR_SESSION emitted, gated on != headless so
|
||||
// eval/CI inside Conductor BLOCKs instead of rendering prose to nobody)
|
||||
// lives in the script now. The render must still invoke the script and the
|
||||
// AUQ prose still branches on the echoed line.
|
||||
test('skill-start script emits CONDUCTOR_SESSION, gated on != headless (Issue 8)', () => {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const script = fs.readFileSync(path.join(import.meta.dir, '..', 'bin', 'gstack-skill-start'), 'utf-8');
|
||||
expect(script).toContain('echo "CONDUCTOR_SESSION: true"');
|
||||
expect(script).toMatch(/"\$_SESSION_KIND" != "headless"[\s\S]*CONDUCTOR_WORKSPACE_PATH[\s\S]*CONDUCTOR_PORT[\s\S]*CONDUCTOR_SESSION: true/);
|
||||
});
|
||||
|
||||
test('claude preamble render invokes the script and interprets CONDUCTOR_SESSION', () => {
|
||||
const out = generatePreamble(makeCtx('claude', 2, 'claude'));
|
||||
expect(out).toContain('echo "CONDUCTOR_SESSION: true"');
|
||||
// The emission must be suppressed when the session is headless (eval/CI
|
||||
// inside Conductor must BLOCK, not render prose to nobody).
|
||||
expect(out).toMatch(/"\$_SESSION_KIND" != "headless"[\s\S]*CONDUCTOR_WORKSPACE_PATH[\s\S]*CONDUCTOR_PORT[\s\S]*CONDUCTOR_SESSION: true/);
|
||||
expect(out).toContain('gstack-skill-start');
|
||||
// The AUQ tool-resolution prose keys off the echoed line.
|
||||
expect(out).toContain('CONDUCTOR_SESSION: true');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -153,8 +153,13 @@ describe('first-run-guidance preamble wiring (generated)', () => {
|
||||
const md = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
|
||||
|
||||
test('detection is gated to the first-ever run only (ACTIVATED=no, not headless)', () => {
|
||||
expect(md).toContain('if [ "$_ACTIVATED" = "no" ] && [ "$_SESSION_KIND" != "headless" ]');
|
||||
expect(md).toContain('gstack-first-task-detect');
|
||||
// Token-reduction Phase 1: the gating bash moved from the rendered
|
||||
// preamble into bin/gstack-skill-start — same gate, new home. The render
|
||||
// acts on the echoed FIRST_TASK/ACTIVATED keys (asserted below).
|
||||
const script = fs.readFileSync(path.join(ROOT, 'bin', 'gstack-skill-start'), 'utf-8');
|
||||
expect(script).toContain('if [ "$_ACTIVATED" = "no" ] && [ "$_SESSION_KIND" != "headless" ]');
|
||||
expect(script).toContain('gstack-first-task-detect');
|
||||
expect(md).toContain('FIRST_TASK:');
|
||||
});
|
||||
|
||||
test('emits the unified first-run guidance section branching on ACTIVATED', () => {
|
||||
|
||||
+20
-11
@@ -38,28 +38,38 @@ function makeCtx(host: 'claude' | 'codex'): TemplateContext {
|
||||
};
|
||||
}
|
||||
|
||||
/** Extract the routing-probe block from the rendered preamble bash. */
|
||||
function extractRoutingProbe(rendered: string): string {
|
||||
const start = rendered.indexOf('_HAS_ROUTING="no"');
|
||||
// Token-reduction Phase 1: the probe bash moved from the rendered preamble
|
||||
// into bin/gstack-skill-start (invoked by every host's preamble fence). The
|
||||
// probe block under test is extracted from the LIVE script bytes.
|
||||
const SKILL_START_SCRIPT = fs.readFileSync(
|
||||
path.join(ROOT, 'bin', 'gstack-skill-start'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
/** Extract the routing-probe block from the skill-start script. */
|
||||
function extractRoutingProbe(scriptText: string): string {
|
||||
const start = scriptText.indexOf('_HAS_ROUTING="no"');
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const end = rendered.indexOf('done', start);
|
||||
const end = scriptText.indexOf('done', start);
|
||||
expect(end).toBeGreaterThan(start);
|
||||
return rendered.slice(start, end + 'done'.length);
|
||||
return scriptText.slice(start, end + 'done'.length);
|
||||
}
|
||||
|
||||
describe('routing probe checks AGENTS.md too (#2500)', () => {
|
||||
for (const host of ['claude', 'codex'] as const) {
|
||||
test(`rendered preamble probes CLAUDE.md AND AGENTS.md (${host})`, () => {
|
||||
test(`preamble reaches the CLAUDE.md AND AGENTS.md probe (${host})`, () => {
|
||||
// The render must invoke the script that owns the probe...
|
||||
const rendered = generatePreambleBash(makeCtx(host));
|
||||
const probe = extractRoutingProbe(rendered);
|
||||
expect(rendered).toContain('gstack-skill-start');
|
||||
// ...and the probe must cover both convention files.
|
||||
const probe = extractRoutingProbe(SKILL_START_SCRIPT);
|
||||
expect(probe).toContain('CLAUDE.md');
|
||||
expect(probe).toContain('AGENTS.md');
|
||||
});
|
||||
}
|
||||
|
||||
test('live probe block: AGENTS.md-only repo reports HAS_ROUTING=yes', () => {
|
||||
const rendered = generatePreambleBash(makeCtx('claude'));
|
||||
const probe = extractRoutingProbe(rendered);
|
||||
const probe = extractRoutingProbe(SKILL_START_SCRIPT);
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'routing-probe-'));
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
@@ -77,8 +87,7 @@ describe('routing probe checks AGENTS.md too (#2500)', () => {
|
||||
});
|
||||
|
||||
test('live probe block: repo with neither file reports HAS_ROUTING=no', () => {
|
||||
const rendered = generatePreambleBash(makeCtx('claude'));
|
||||
const probe = extractRoutingProbe(rendered);
|
||||
const probe = extractRoutingProbe(SKILL_START_SCRIPT);
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'routing-probe-'));
|
||||
try {
|
||||
const out = execSync(
|
||||
|
||||
@@ -277,15 +277,30 @@ describe('Update check preamble', () => {
|
||||
|
||||
for (const skill of skillsWithUpdateCheck) {
|
||||
test(`${skill} update check line ends with || true`, () => {
|
||||
// Token-reduction Phase 1: the inline `_UPD=$(gstack-update-check ...)`
|
||||
// bash moved into bin/gstack-skill-start. The render must (a) invoke the
|
||||
// script with the exact flag shape, (b) carry the exit-0 degraded-install
|
||||
// fallback (the successor of the old `|| true` guard at the fence level),
|
||||
// and (c) keep the UPGRADE_AVAILABLE interpretation prose that acts on
|
||||
// the script's update-check STATUS output.
|
||||
const content = fs.readFileSync(path.join(ROOT, skill), 'utf-8');
|
||||
// The second line of the bash block must end with || true
|
||||
// to avoid exit code 1 when _UPD is empty (up to date)
|
||||
const match = content.match(/\[ -n "\$_UPD" \].*$/m);
|
||||
expect(match).not.toBeNull();
|
||||
expect(match![0]).toContain('|| true');
|
||||
expect(content).toContain('bin/gstack-skill-start');
|
||||
expect(content).toMatch(/--skill "[^"]+" --model "[^"]+" --parent-pid "\$PPID"/);
|
||||
expect(content).toContain('|| echo "SKILL_START: unavailable');
|
||||
expect(content).toContain('UPGRADE_AVAILABLE');
|
||||
});
|
||||
}
|
||||
|
||||
test('bin/gstack-skill-start update check line ends with || true (new home of the inline guard)', () => {
|
||||
// The `[ -n "$_UPD" ] ... || true` guard (empty _UPD must not exit 1 when
|
||||
// up to date) moved verbatim into the consolidated preamble script. Pin it
|
||||
// there so the invariant survives in its new home.
|
||||
const script = fs.readFileSync(path.join(ROOT, 'bin', 'gstack-skill-start'), 'utf-8');
|
||||
const match = script.match(/\[ -n "\$_UPD" \].*$/m);
|
||||
expect(match).not.toBeNull();
|
||||
expect(match![0]).toContain('|| true');
|
||||
});
|
||||
|
||||
test('all skills with update check are generated from .tmpl', () => {
|
||||
for (const skill of skillsWithUpdateCheck) {
|
||||
const tmplPath = path.join(ROOT, skill + '.tmpl');
|
||||
@@ -294,16 +309,19 @@ describe('Update check preamble', () => {
|
||||
});
|
||||
|
||||
test('update check bash block exits 0 when up to date', () => {
|
||||
// Simulate the exact preamble command from SKILL.md
|
||||
// Simulate the exact update-check lines from bin/gstack-skill-start
|
||||
// (per-line `|| true`, sanitize pipe included)
|
||||
const result = Bun.spawnSync(['bash', '-c',
|
||||
'_UPD=$(echo "" || true); [ -n "$_UPD" ] && echo "$_UPD" || true'
|
||||
'_sanitize() { sed "s/GSTACK_INSTRUCTION/GSTACK-INSTRUCTION-(stripped)/g"; }; ' +
|
||||
'_UPD=$(echo "" || true); [ -n "$_UPD" ] && printf "%s\\n" "$_UPD" | _sanitize || true'
|
||||
], { stdout: 'pipe', stderr: 'pipe' });
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('update check bash block exits 0 when upgrade available', () => {
|
||||
const result = Bun.spawnSync(['bash', '-c',
|
||||
'_UPD=$(echo "UPGRADE_AVAILABLE 0.3.3 0.4.0" || true); [ -n "$_UPD" ] && echo "$_UPD" || true'
|
||||
'_sanitize() { sed "s/GSTACK_INSTRUCTION/GSTACK-INSTRUCTION-(stripped)/g"; }; ' +
|
||||
'_UPD=$(echo "UPGRADE_AVAILABLE 0.3.3 0.4.0" || true); [ -n "$_UPD" ] && printf "%s\\n" "$_UPD" | _sanitize || true'
|
||||
], { stdout: 'pipe', stderr: 'pipe' });
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout.toString().trim()).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
|
||||
@@ -617,11 +635,25 @@ describe('v0.4.1 preamble features', () => {
|
||||
|
||||
for (const skill of skillsWithPreamble) {
|
||||
test(`${skill} contains session awareness`, () => {
|
||||
// Token-reduction Phase 1: the inline `_SESSIONS=$(find ~/.gstack/sessions ...)`
|
||||
// bash moved into bin/gstack-skill-start. The render still carries session
|
||||
// identity (--parent-pid feeds the sessions dir with the harness pid) and
|
||||
// the SESSION_KIND STATUS-line interpretation prose.
|
||||
const content = fs.readFileSync(path.join(ROOT, skill), 'utf-8');
|
||||
expect(content).toContain('_SESSIONS');
|
||||
expect(content).toMatch(/--parent-pid "\$PPID"/);
|
||||
expect(content).toContain('SESSION_KIND');
|
||||
});
|
||||
}
|
||||
|
||||
test('bin/gstack-skill-start owns the session-tracking machinery (new home of _SESSIONS)', () => {
|
||||
// The sessions-dir touch + stale-session cleanup that every preamble used
|
||||
// to inline now lives in the consolidated script — pin it there.
|
||||
const script = fs.readFileSync(path.join(ROOT, 'bin', 'gstack-skill-start'), 'utf-8');
|
||||
expect(script).toContain('mkdir -p "$_GH/sessions"');
|
||||
expect(script).toContain('touch "$_GH/sessions/$PARENT_PID"');
|
||||
expect(script).toContain('-mmin +120'); // 120-min freshness window survives the move
|
||||
});
|
||||
|
||||
for (const skill of skillsWithPreamble) {
|
||||
test(`${skill} contains escalation protocol`, () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, skill), 'utf-8');
|
||||
@@ -1459,7 +1491,11 @@ describe('Codex skill', () => {
|
||||
});
|
||||
|
||||
test('codex integration in /plan-eng-review offers plan critique', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'plan-eng-review', 'SKILL.md'), 'utf-8');
|
||||
// Carved skill: the Codex outside-voice plan critique lives in
|
||||
// sections/review-sections.md — read the skeleton+sections union. (The
|
||||
// skeleton alone used to match "Codex" only via an inline-bash comment
|
||||
// that the gstack-skill-start consolidation removed.)
|
||||
const content = readSkillUnion('plan-eng-review');
|
||||
expect(content).toContain('Codex');
|
||||
expect(content).toContain('codex exec');
|
||||
});
|
||||
@@ -1795,9 +1831,14 @@ describe('Codex skill validation', () => {
|
||||
|
||||
describe('Repo mode preamble validation', () => {
|
||||
test('generated SKILL.md preamble contains REPO_MODE output', () => {
|
||||
// Token-reduction Phase 1: the inline `gstack-repo-mode` call moved into
|
||||
// bin/gstack-skill-start. The render pins the script invocation; the
|
||||
// script pins the REPO_MODE echo + the gstack-repo-mode call.
|
||||
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('REPO_MODE:');
|
||||
expect(content).toContain('gstack-repo-mode');
|
||||
expect(content).toContain('bin/gstack-skill-start');
|
||||
const script = fs.readFileSync(path.join(ROOT, 'bin', 'gstack-skill-start'), 'utf-8');
|
||||
expect(script).toContain('REPO_MODE:');
|
||||
expect(script).toContain('gstack-repo-mode');
|
||||
});
|
||||
|
||||
test('tier 3+ skills contain See Something Say Something section', () => {
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
*
|
||||
* The telemetry consent copy promises a user's repo name is recorded locally
|
||||
* only and stripped before any upload (scripts/resolvers/preamble/
|
||||
* generate-telemetry-prompt.ts). Two producers write repo/branch identity into
|
||||
* the local skill-usage.jsonl:
|
||||
* generate-telemetry-prompt.ts). The producers that write repo/branch identity
|
||||
* into the local skill-usage.jsonl (the preamble's inline bash moved into the
|
||||
* skill-start/skill-end scripts in token-reduction Phase 1):
|
||||
*
|
||||
* - the preamble epilogue → "repo"
|
||||
* (scripts/resolvers/preamble/generate-preamble-bash.ts)
|
||||
* - gstack-telemetry-log → "_repo_slug", "_branch"
|
||||
* - gstack-skill-start (skill_run event) → "repo"
|
||||
* (bin/gstack-skill-start)
|
||||
* - gstack-skill-end (completion event) → (no repo identity today,
|
||||
* scanned so drift is caught) (bin/gstack-skill-end)
|
||||
* - gstack-telemetry-log → "_repo_slug", "_branch"
|
||||
* (bin/gstack-telemetry-log)
|
||||
*
|
||||
* gstack-telemetry-sync MUST strip every one of those fields before the remote
|
||||
@@ -37,7 +40,8 @@ import path from 'path';
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const SYNC = path.join(ROOT, 'bin', 'gstack-telemetry-sync');
|
||||
const PREAMBLE = path.join(ROOT, 'scripts', 'resolvers', 'preamble', 'generate-preamble-bash.ts');
|
||||
const SKILL_START = path.join(ROOT, 'bin', 'gstack-skill-start');
|
||||
const SKILL_END = path.join(ROOT, 'bin', 'gstack-skill-end');
|
||||
const TEL_LOG = path.join(ROOT, 'bin', 'gstack-telemetry-log');
|
||||
|
||||
// Fields that identify the user's repo/branch. The promise is that NONE of
|
||||
@@ -94,10 +98,16 @@ describe('telemetry no-repo-identity-egress invariant', () => {
|
||||
// Repo-identity fields the producers emit into the synced file — computed
|
||||
// once, asserted against BOTH strip paths (jq primary, sed fallback). Only
|
||||
// emission lines that target the synced file (skill-usage.jsonl) count: the
|
||||
// preamble appends directly; gstack-telemetry-log builds the synced event
|
||||
// with a `printf '{"v":1,...` line into $JSONL_FILE (= skill-usage.jsonl).
|
||||
const preambleSynced = fs
|
||||
.readFileSync(PREAMBLE, 'utf-8')
|
||||
// skill-start/skill-end scripts append directly (the former inline preamble
|
||||
// bash); gstack-telemetry-log builds the synced event with a
|
||||
// `printf '{"v":1,...` line into $JSONL_FILE (= skill-usage.jsonl). The
|
||||
// timeline log carries "branch" but is local-only and never synced.
|
||||
const skillStartSynced = fs
|
||||
.readFileSync(SKILL_START, 'utf-8')
|
||||
.split('\n')
|
||||
.filter((l) => l.includes('skill-usage.jsonl'));
|
||||
const skillEndSynced = fs
|
||||
.readFileSync(SKILL_END, 'utf-8')
|
||||
.split('\n')
|
||||
.filter((l) => l.includes('skill-usage.jsonl'));
|
||||
const telLogSynced = fs
|
||||
@@ -105,7 +115,8 @@ describe('telemetry no-repo-identity-egress invariant', () => {
|
||||
.split('\n')
|
||||
.filter((l) => l.includes('"v":1') || l.includes('skill-usage'));
|
||||
const emitted = new Set<string>([
|
||||
...emittedRepoFields(preambleSynced),
|
||||
...emittedRepoFields(skillStartSynced),
|
||||
...emittedRepoFields(skillEndSynced),
|
||||
...emittedRepoFields(telLogSynced),
|
||||
]);
|
||||
|
||||
@@ -116,8 +127,8 @@ describe('telemetry no-repo-identity-egress invariant', () => {
|
||||
});
|
||||
|
||||
test('coverage: every repo/branch field the producers emit into skill-usage.jsonl is stripped (sed fallback path)', () => {
|
||||
// The preamble must emit "repo" — guards against the test silently passing
|
||||
// because a regex stopped matching the producer.
|
||||
// gstack-skill-start must emit "repo" — guards against the test silently
|
||||
// passing because a regex stopped matching the producer.
|
||||
expect(emitted.has('repo')).toBe(true);
|
||||
for (const field of emitted) {
|
||||
expect(
|
||||
|
||||
@@ -15,10 +15,20 @@
|
||||
* - Tier-1 preamble does NOT include Writing Style section
|
||||
*/
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type { TemplateContext } from '../scripts/resolvers/types';
|
||||
import { HOST_PATHS } from '../scripts/resolvers/types';
|
||||
import { generatePreamble } from '../scripts/resolvers/preamble';
|
||||
|
||||
// Token-reduction Phase 1: the EXPLAIN_LEVEL config read + echo moved from the
|
||||
// inline preamble bash into bin/gstack-skill-start; the render keeps the
|
||||
// interpretation prose that acts on the echoed key.
|
||||
const SKILL_START_SCRIPT = fs.readFileSync(
|
||||
path.join(import.meta.dir, '..', 'bin', 'gstack-skill-start'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
function makeCtx(host: 'claude' | 'codex', tier: 1 | 2 | 3 | 4): TemplateContext {
|
||||
return {
|
||||
skillName: 'test-skill',
|
||||
@@ -35,9 +45,12 @@ describe('Writing Style preamble section', () => {
|
||||
expect(out).toContain('## Writing Style');
|
||||
});
|
||||
|
||||
test('tier 2+ preamble includes EXPLAIN_LEVEL echo in bash', () => {
|
||||
test('EXPLAIN_LEVEL is echoed by gstack-skill-start and read by tier 2+ prose', () => {
|
||||
// The bash echo lives in the script the preamble fence invokes...
|
||||
expect(SKILL_START_SCRIPT).toContain('_EXPLAIN_LEVEL=$(');
|
||||
expect(SKILL_START_SCRIPT).toContain('echo "EXPLAIN_LEVEL: $_EXPLAIN_LEVEL"');
|
||||
// ...and the tier-2+ render references the echoed key.
|
||||
const out = generatePreamble(makeCtx('claude', 2));
|
||||
expect(out).toContain('_EXPLAIN_LEVEL');
|
||||
expect(out).toContain('EXPLAIN_LEVEL:');
|
||||
});
|
||||
|
||||
@@ -70,13 +83,18 @@ describe('Writing Style preamble section', () => {
|
||||
|
||||
test('Codex tier-2 preamble uses host-aware path (no .claude/)', () => {
|
||||
const out = generatePreamble(makeCtx('codex', 2));
|
||||
// The Writing Style section shouldn't reference a Claude-specific bin path.
|
||||
// Specifically check the EXPLAIN_LEVEL bash line.
|
||||
const explainLine = out.split('\n').find(l => l.includes('_EXPLAIN_LEVEL='));
|
||||
// The config read moved into gstack-skill-start, which resolves its bin
|
||||
// dir $0-relative ($_BIN) — host-neutral by construction.
|
||||
const explainLine = SKILL_START_SCRIPT.split('\n').find(l => l.includes('_EXPLAIN_LEVEL='));
|
||||
expect(explainLine).toBeDefined();
|
||||
expect(explainLine).not.toMatch(/~\/\.claude\//);
|
||||
// Codex uses $GSTACK_BIN
|
||||
expect(explainLine).toContain('$GSTACK_BIN');
|
||||
expect(explainLine).toContain('$_BIN/');
|
||||
// The Codex render's fence must reach the script via the host path, not
|
||||
// a Claude-specific one.
|
||||
const fenceLine = out.split('\n').find(l => l.includes('_SS='));
|
||||
expect(fenceLine).toBeDefined();
|
||||
expect(fenceLine).not.toMatch(/~\/\.claude\//);
|
||||
expect(fenceLine).toContain('$GSTACK_BIN');
|
||||
});
|
||||
|
||||
test('tier 1 preamble does NOT include Writing Style section', () => {
|
||||
|
||||
Reference in New Issue
Block a user