mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-18 02:22:04 +08:00
merge: incorporate origin/main into community-mode branch
Conflicts resolved: - README.md: merge skill lists — keep /gstack-submit from our branch, add /plan-devex-review, /devex-review, /pair-agent from main. Accept main's team mode step 2 text. - setup: keep both our install ping (step 9) and main's team mode hook registration (step 10) - supabase/functions/telemetry-ingest/index.ts: keep our deletion (dead code removed earlier on this branch, main modified it) Main brought in: team mode (--team flag, auto-update hook, session tracking), /plan-devex-review + /devex-review skills, /pair-agent skill, open-gstack-browser, /checkpoint, /health, /humanizer. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -213,11 +213,20 @@ describe('gen-skill-docs', () => {
|
||||
expect(browseTmpl).toContain('{{PREAMBLE}}');
|
||||
});
|
||||
|
||||
test('generated SKILL.md contains contributor mode check', () => {
|
||||
test('generated SKILL.md contains operational self-improvement (replaced contributor mode)', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('Contributor Mode');
|
||||
expect(content).toContain('gstack_contributor');
|
||||
expect(content).toContain('contributor-logs');
|
||||
expect(content).not.toContain('Contributor Mode');
|
||||
expect(content).not.toContain('gstack_contributor');
|
||||
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');
|
||||
});
|
||||
|
||||
test('generated SKILL.md with LEARNINGS_LOG contains operational type', () => {
|
||||
// Check a skill that has LEARNINGS_LOG (e.g., review)
|
||||
const content = fs.readFileSync(path.join(ROOT, 'review', 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('operational');
|
||||
});
|
||||
|
||||
test('generated SKILL.md contains session awareness', () => {
|
||||
@@ -740,6 +749,22 @@ describe('TEST_COVERAGE_AUDIT placeholders', () => {
|
||||
expect(shipSkill).toContain(phrase);
|
||||
}
|
||||
});
|
||||
|
||||
test('ship SKILL.md contains review army specialist dispatch', () => {
|
||||
expect(shipSkill).toContain('Specialist Dispatch');
|
||||
expect(shipSkill).toContain('Step 3.55');
|
||||
expect(shipSkill).toContain('Step 3.56');
|
||||
});
|
||||
|
||||
test('ship SKILL.md contains cross-review finding dedup', () => {
|
||||
expect(shipSkill).toContain('Cross-review finding dedup');
|
||||
expect(shipSkill).toContain('Step 3.57');
|
||||
});
|
||||
|
||||
test('ship SKILL.md contains re-run idempotency behavior', () => {
|
||||
expect(shipSkill).toContain('Re-run behavior (idempotency)');
|
||||
expect(shipSkill).toContain('Never skip a verification step');
|
||||
});
|
||||
});
|
||||
|
||||
// --- {{TEST_FAILURE_TRIAGE}} resolver tests ---
|
||||
@@ -979,6 +1004,18 @@ describe('Plan status footer in preamble', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// --- Skill invocation during plan mode in preamble ---
|
||||
|
||||
describe('Skill invocation during plan mode in preamble', () => {
|
||||
test('preamble contains skill invocation plan mode section', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'office-hours', 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('Skill Invocation During Plan Mode');
|
||||
expect(content).toContain('precedence over generic plan mode behavior');
|
||||
expect(content).toContain('Do not continue the workflow');
|
||||
expect(content).toContain('cancel the skill or leave plan mode');
|
||||
});
|
||||
});
|
||||
|
||||
// --- {{SPEC_REVIEW_LOOP}} resolver tests ---
|
||||
|
||||
describe('SPEC_REVIEW_LOOP resolver', () => {
|
||||
@@ -1718,7 +1755,10 @@ describe('Codex generation (--host codex)', () => {
|
||||
test('Claude output unchanged: all Claude skills have zero Codex paths', () => {
|
||||
for (const skill of ALL_SKILLS) {
|
||||
const content = fs.readFileSync(path.join(ROOT, skill.dir, 'SKILL.md'), 'utf-8');
|
||||
expect(content).not.toContain('~/.codex/');
|
||||
// pair-agent legitimately documents how Codex agents store credentials
|
||||
if (skill.dir !== 'pair-agent') {
|
||||
expect(content).not.toContain('~/.codex/');
|
||||
}
|
||||
// gstack-upgrade legitimately references .agents/skills for cross-platform detection
|
||||
if (skill.dir !== 'gstack-upgrade') {
|
||||
expect(content).not.toContain('.agents/skills');
|
||||
@@ -1877,19 +1917,95 @@ describe('Factory generation (--host factory)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Parameterized host smoke tests (config-driven) ─────────
|
||||
|
||||
import { ALL_HOST_CONFIGS, getExternalHosts } from '../hosts/index';
|
||||
|
||||
describe('Parameterized host smoke tests', () => {
|
||||
for (const hostConfig of getExternalHosts()) {
|
||||
describe(`${hostConfig.displayName} (--host ${hostConfig.name})`, () => {
|
||||
const hostDir = path.join(ROOT, hostConfig.hostSubdir, 'skills');
|
||||
|
||||
test('generates output that exists on disk', () => {
|
||||
// Generated dir should exist (created by earlier bun run gen:skill-docs --host all)
|
||||
if (!fs.existsSync(hostDir)) {
|
||||
// Generate if not already done
|
||||
Bun.spawnSync(['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', hostConfig.name], {
|
||||
cwd: ROOT, stdout: 'pipe', stderr: 'pipe',
|
||||
});
|
||||
}
|
||||
expect(fs.existsSync(hostDir)).toBe(true);
|
||||
const skills = fs.readdirSync(hostDir).filter(d =>
|
||||
fs.existsSync(path.join(hostDir, d, 'SKILL.md'))
|
||||
);
|
||||
expect(skills.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('no .claude/skills path leakage in non-root skills', () => {
|
||||
if (!fs.existsSync(hostDir)) return; // skip if not generated
|
||||
const skills = fs.readdirSync(hostDir);
|
||||
for (const skill of skills) {
|
||||
// Skip root gstack skill — it contains preamble with intentional .claude/skills
|
||||
// fallback paths for binary lookup and skill prefix instructions
|
||||
if (skill === 'gstack') continue;
|
||||
const skillMd = path.join(hostDir, skill, 'SKILL.md');
|
||||
if (!fs.existsSync(skillMd)) continue;
|
||||
const content = fs.readFileSync(skillMd, 'utf-8');
|
||||
// Strip bash blocks (which have legitimate fallback paths)
|
||||
const noBash = content.replace(/```bash\n[\s\S]*?```/g, '');
|
||||
const leaks = noBash.split('\n').filter(l => l.includes('.claude/skills'));
|
||||
if (leaks.length > 0) {
|
||||
throw new Error(`${skill}: .claude/skills leakage:\n${leaks.slice(0, 3).join('\n')}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('frontmatter has name and description', () => {
|
||||
if (!fs.existsSync(hostDir)) return;
|
||||
const skills = fs.readdirSync(hostDir);
|
||||
for (const skill of skills) {
|
||||
const skillMd = path.join(hostDir, skill, 'SKILL.md');
|
||||
if (!fs.existsSync(skillMd)) continue;
|
||||
const content = fs.readFileSync(skillMd, 'utf-8');
|
||||
expect(content).toMatch(/^---\n/);
|
||||
expect(content).toMatch(/^name:\s/m);
|
||||
expect(content).toMatch(/^description:\s/m);
|
||||
}
|
||||
});
|
||||
|
||||
test('--dry-run freshness check passes', () => {
|
||||
const result = Bun.spawnSync(
|
||||
['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', hostConfig.name, '--dry-run'],
|
||||
{ cwd: ROOT, stdout: 'pipe', stderr: 'pipe' }
|
||||
);
|
||||
expect(result.exitCode).toBe(0);
|
||||
const output = result.stdout.toString();
|
||||
expect(output).not.toContain('STALE');
|
||||
});
|
||||
|
||||
if (hostConfig.generation.skipSkills?.includes('codex')) {
|
||||
test('/codex skill excluded', () => {
|
||||
expect(fs.existsSync(path.join(hostDir, 'gstack-codex', 'SKILL.md'))).toBe(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ─── --host all tests ────────────────────────────────────────
|
||||
|
||||
describe('--host all', () => {
|
||||
test('--host all generates for claude, codex, and factory', () => {
|
||||
test('--host all generates for all registered hosts', () => {
|
||||
const result = Bun.spawnSync(['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', 'all', '--dry-run'], {
|
||||
cwd: ROOT, stdout: 'pipe', stderr: 'pipe',
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
const output = result.stdout.toString();
|
||||
// All three hosts should appear in output
|
||||
// All hosts should appear in output
|
||||
expect(output).toContain('FRESH: SKILL.md'); // claude
|
||||
expect(output).toContain('FRESH: .agents/skills/'); // codex
|
||||
expect(output).toContain('FRESH: .factory/skills/'); // factory
|
||||
for (const hostConfig of getExternalHosts()) {
|
||||
expect(output).toContain(`FRESH: ${hostConfig.hostSubdir}/skills/`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1960,13 +2076,43 @@ describe('setup script validation', () => {
|
||||
expect(fnBody).toContain('gstack*');
|
||||
});
|
||||
|
||||
test('link_claude_skill_dirs creates relative symlinks', () => {
|
||||
// Claude links should be relative: ln -snf "gstack/$dir_name"
|
||||
// Uses dir_name (not skill_name) because symlink target must point to the physical directory
|
||||
test('link_claude_skill_dirs creates real directories with absolute SKILL.md symlinks', () => {
|
||||
// Claude links should be real directories with absolute SKILL.md symlinks
|
||||
// to ensure Claude Code discovers them as top-level skills (not nested under gstack/)
|
||||
const fnStart = setupContent.indexOf('link_claude_skill_dirs()');
|
||||
const fnEnd = setupContent.indexOf('}', setupContent.indexOf('linked[@]}', fnStart));
|
||||
const fnBody = setupContent.slice(fnStart, fnEnd);
|
||||
expect(fnBody).toContain('ln -snf "gstack/$dir_name"');
|
||||
expect(fnBody).toContain('mkdir -p "$target"');
|
||||
expect(fnBody).toContain('ln -snf "$gstack_dir/$dir_name/SKILL.md" "$target/SKILL.md"');
|
||||
});
|
||||
|
||||
// REGRESSION: cleanup functions must handle both old symlinks AND new real-directory pattern
|
||||
test('cleanup functions handle real directories with symlinked SKILL.md', () => {
|
||||
// cleanup_old_claude_symlinks must detect and remove real dirs with SKILL.md symlinks
|
||||
const cleanupOldStart = setupContent.indexOf('cleanup_old_claude_symlinks()');
|
||||
const cleanupOldEnd = setupContent.indexOf('}', setupContent.indexOf('cleaned up old', cleanupOldStart));
|
||||
const cleanupOldBody = setupContent.slice(cleanupOldStart, cleanupOldEnd);
|
||||
expect(cleanupOldBody).toContain('-d "$old_target"');
|
||||
expect(cleanupOldBody).toContain('-L "$old_target/SKILL.md"');
|
||||
expect(cleanupOldBody).toContain('rm -rf "$old_target"');
|
||||
|
||||
// cleanup_prefixed_claude_symlinks must also handle the new pattern
|
||||
const cleanupPrefixedStart = setupContent.indexOf('cleanup_prefixed_claude_symlinks()');
|
||||
const cleanupPrefixedEnd = setupContent.indexOf('}', setupContent.indexOf('cleaned up prefixed', cleanupPrefixedStart));
|
||||
const cleanupPrefixedBody = setupContent.slice(cleanupPrefixedStart, cleanupPrefixedEnd);
|
||||
expect(cleanupPrefixedBody).toContain('-d "$prefixed_target"');
|
||||
expect(cleanupPrefixedBody).toContain('-L "$prefixed_target/SKILL.md"');
|
||||
expect(cleanupPrefixedBody).toContain('rm -rf "$prefixed_target"');
|
||||
});
|
||||
|
||||
// REGRESSION: link function must upgrade old directory symlinks
|
||||
test('link_claude_skill_dirs removes old directory symlinks before creating real dirs', () => {
|
||||
const fnStart = setupContent.indexOf('link_claude_skill_dirs()');
|
||||
const fnEnd = setupContent.indexOf('}', setupContent.indexOf('linked[@]}', fnStart));
|
||||
const fnBody = setupContent.slice(fnStart, fnEnd);
|
||||
// Must check for and remove old symlinks before mkdir
|
||||
expect(fnBody).toContain('if [ -L "$target" ]');
|
||||
expect(fnBody).toContain('rm -f "$target"');
|
||||
});
|
||||
|
||||
test('setup supports --host auto|claude|codex|kiro', () => {
|
||||
@@ -2542,3 +2688,51 @@ describe('gen-skill-docs prefix warning (#620/#578)', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('voice-triggers processing', () => {
|
||||
const { extractVoiceTriggers, processVoiceTriggers } = require('../scripts/gen-skill-docs') as {
|
||||
extractVoiceTriggers: (content: string) => string[];
|
||||
processVoiceTriggers: (content: string) => string;
|
||||
};
|
||||
|
||||
test('extractVoiceTriggers parses valid YAML list', () => {
|
||||
const content = `---\nname: cso\ndescription: |\n Security audit.\nvoice-triggers:\n - "see-so"\n - "security review"\n---\nBody`;
|
||||
const triggers = extractVoiceTriggers(content);
|
||||
expect(triggers).toEqual(['see-so', 'security review']);
|
||||
});
|
||||
|
||||
test('extractVoiceTriggers returns [] when no field present', () => {
|
||||
const content = `---\nname: qa\ndescription: |\n QA testing.\n---\nBody`;
|
||||
expect(extractVoiceTriggers(content)).toEqual([]);
|
||||
});
|
||||
|
||||
test('processVoiceTriggers appends voice triggers to description', () => {
|
||||
const content = `---\nname: cso\ndescription: |\n Security audit. (gstack)\nvoice-triggers:\n - "see-so"\n - "security review"\n---\nBody`;
|
||||
const result = processVoiceTriggers(content);
|
||||
expect(result).toContain('Voice triggers (speech-to-text aliases): "see-so", "security review".');
|
||||
});
|
||||
|
||||
test('processVoiceTriggers strips voice-triggers field from output', () => {
|
||||
const content = `---\nname: cso\ndescription: |\n Security audit. (gstack)\nvoice-triggers:\n - "see-so"\n---\nBody`;
|
||||
const result = processVoiceTriggers(content);
|
||||
expect(result).not.toContain('voice-triggers:');
|
||||
});
|
||||
|
||||
test('processVoiceTriggers returns content unchanged when no voice-triggers', () => {
|
||||
const content = `---\nname: qa\ndescription: |\n QA testing.\n---\nBody`;
|
||||
expect(processVoiceTriggers(content)).toBe(content);
|
||||
});
|
||||
|
||||
test('generated CSO SKILL.md contains voice triggers in description', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'cso', 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('"see-so"');
|
||||
expect(content).toContain('Voice triggers (speech-to-text aliases):');
|
||||
});
|
||||
|
||||
test('generated CSO SKILL.md does NOT contain raw voice-triggers field', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'cso', 'SKILL.md'), 'utf-8');
|
||||
const fmEnd = content.indexOf('\n---', 4);
|
||||
const frontmatter = content.slice(0, fmEnd);
|
||||
expect(frontmatter).not.toContain('voice-triggers:');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user