mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-01 19:25:10 +02:00
cdd6f7865d
* test: add 16 failing tests for 6 community fixes
Tests-first for all fixes in this PR wave:
- #594 discoverability: gstack tag in descriptions, 120-char first line
- #573 feature signals: ship/SKILL.md Step 4 detection
- #510 context warnings: no preemptive warnings in generated files
- #474 Safety Net: no find -delete in generated files
- #467 telemetry: JSONL writes gated by _TEL conditional
- #584 sidebar: Write in allowedTools, stderr capture
- #578 relink: prefixed/flat symlinks, cleanup, error, config hook
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: replace find -delete with find -exec rm for Safety Net (#474)
-delete is a non-POSIX extension that fails on Safety Net environments.
-exec rm {} + is POSIX-compliant and works everywhere.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: gate local JSONL writes by telemetry setting (#467)
When telemetry is off, nothing is written anywhere — not just remote,
but local JSONL too. Clean trust contract: off means off everywhere.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove preemptive context warnings from plan-eng-review (#510)
The system handles context compaction automatically. Preemptive warnings
waste tokens and create false urgency. Skills should not warn about
context limits — just describe the compression priority order.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add (gstack) tag to skill descriptions for discoverability (#594)
Every SKILL.md.tmpl description now contains "gstack" on the last line,
making skills findable in Claude Code's command palette. First-line hooks
stay under 120 chars. Split ship description to fix wrapping.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: auto-relink skill symlinks on prefix config change (#578)
New bin/gstack-relink creates prefixed (gstack-*) or flat symlinks
based on skill_prefix config. gstack-config auto-triggers relink
when skill_prefix changes. Setup guards against recursive calls
with GSTACK_SETUP_RUNNING env var.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add feature signal detection to version bump heuristic (#573)
/ship Step 4 now checks for feature signals (new routes, migrations,
test+source pairs, feat/ branches) when deciding version bumps.
PATCH requires no feature signals. MINOR asks the user if any signal
is detected or 500+ lines changed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: sidebar Write tool, stderr capture, cross-platform URL opener (#584)
Add Write to sidebar allowedTools (both sidebar-agent.ts and server.ts).
Write doesn't expand attack surface beyond what Bash already provides.
Replace empty stderr handler with buffer capture for better error
diagnostics. New bin/gstack-open-url for cross-platform URL opening.
Does NOT include Search Before Building intro flow (deferred).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update sidebar-security test for Write tool addition
The fallback allowedTools string now includes Write, matching the
sidebar-agent.ts change from commit 68dc957.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: bump version and changelog (v0.13.5.0)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: prevent gstack-relink from double-prefixing gstack-upgrade
gstack-relink now checks if a skill directory is already named gstack-*
before prepending the prefix. Previously, setting skill_prefix=true would
create gstack-gstack-upgrade, breaking the /gstack-upgrade command.
Matches setup script behavior (setup:260) which already has this guard.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: add double-prefix fix to changelog
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: remove .factory/ from git tracking and add to .gitignore
Generated Factory Droid skills are build output, same as .agents/.
They should not be committed to the repo.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
153 lines
6.0 KiB
TypeScript
153 lines
6.0 KiB
TypeScript
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
import { execSync } from 'child_process';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as os from 'os';
|
|
|
|
const ROOT = path.resolve(import.meta.dir, '..');
|
|
const BIN = path.join(ROOT, 'bin');
|
|
|
|
let tmpDir: string;
|
|
let skillsDir: string;
|
|
let installDir: string;
|
|
|
|
function run(cmd: string, env: Record<string, string> = {}, expectFail = false): string {
|
|
try {
|
|
return execSync(cmd, {
|
|
cwd: ROOT,
|
|
env: { ...process.env, GSTACK_STATE_DIR: tmpDir, ...env },
|
|
encoding: 'utf-8',
|
|
timeout: 10000,
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
}).trim();
|
|
} catch (e: any) {
|
|
if (expectFail) return (e.stderr || e.stdout || '').toString().trim();
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
// Create a mock gstack install directory with skill subdirs
|
|
function setupMockInstall(skills: string[]): void {
|
|
installDir = path.join(tmpDir, 'gstack-install');
|
|
skillsDir = path.join(tmpDir, 'skills');
|
|
fs.mkdirSync(installDir, { recursive: true });
|
|
fs.mkdirSync(skillsDir, { recursive: true });
|
|
|
|
// Copy the real gstack-config and gstack-relink to the mock install
|
|
const mockBin = path.join(installDir, 'bin');
|
|
fs.mkdirSync(mockBin, { recursive: true });
|
|
fs.copyFileSync(path.join(BIN, 'gstack-config'), path.join(mockBin, 'gstack-config'));
|
|
fs.chmodSync(path.join(mockBin, 'gstack-config'), 0o755);
|
|
if (fs.existsSync(path.join(BIN, 'gstack-relink'))) {
|
|
fs.copyFileSync(path.join(BIN, 'gstack-relink'), path.join(mockBin, 'gstack-relink'));
|
|
fs.chmodSync(path.join(mockBin, 'gstack-relink'), 0o755);
|
|
}
|
|
|
|
// Create mock skill directories
|
|
for (const skill of skills) {
|
|
fs.mkdirSync(path.join(installDir, skill), { recursive: true });
|
|
fs.writeFileSync(path.join(installDir, skill, 'SKILL.md'), `# ${skill}`);
|
|
}
|
|
}
|
|
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-relink-test-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('gstack-relink (#578)', () => {
|
|
// Test 11: prefixed symlinks when skill_prefix=true
|
|
test('creates gstack-* symlinks when skill_prefix=true', () => {
|
|
setupMockInstall(['qa', 'ship', 'review']);
|
|
// Set config to prefix mode
|
|
run(`${path.join(installDir, 'bin', 'gstack-config')} set skill_prefix true`);
|
|
// Run relink with env pointing to the mock install
|
|
const output = run(`${path.join(installDir, 'bin', 'gstack-relink')}`, {
|
|
GSTACK_INSTALL_DIR: installDir,
|
|
GSTACK_SKILLS_DIR: skillsDir,
|
|
});
|
|
// Verify gstack-* symlinks exist
|
|
expect(fs.existsSync(path.join(skillsDir, 'gstack-qa'))).toBe(true);
|
|
expect(fs.existsSync(path.join(skillsDir, 'gstack-ship'))).toBe(true);
|
|
expect(fs.existsSync(path.join(skillsDir, 'gstack-review'))).toBe(true);
|
|
expect(output).toContain('gstack-');
|
|
});
|
|
|
|
// Test 12: flat symlinks when skill_prefix=false
|
|
test('creates flat symlinks when skill_prefix=false', () => {
|
|
setupMockInstall(['qa', 'ship', 'review']);
|
|
run(`${path.join(installDir, 'bin', 'gstack-config')} set skill_prefix false`);
|
|
const output = run(`${path.join(installDir, 'bin', 'gstack-relink')}`, {
|
|
GSTACK_INSTALL_DIR: installDir,
|
|
GSTACK_SKILLS_DIR: skillsDir,
|
|
});
|
|
expect(fs.existsSync(path.join(skillsDir, 'qa'))).toBe(true);
|
|
expect(fs.existsSync(path.join(skillsDir, 'ship'))).toBe(true);
|
|
expect(fs.existsSync(path.join(skillsDir, 'review'))).toBe(true);
|
|
expect(output).toContain('flat');
|
|
});
|
|
|
|
// Test 13: cleans stale symlinks from opposite mode
|
|
test('cleans up stale symlinks from opposite mode', () => {
|
|
setupMockInstall(['qa', 'ship']);
|
|
// Create prefixed symlinks first
|
|
run(`${path.join(installDir, 'bin', 'gstack-config')} set skill_prefix true`);
|
|
run(`${path.join(installDir, 'bin', 'gstack-relink')}`, {
|
|
GSTACK_INSTALL_DIR: installDir,
|
|
GSTACK_SKILLS_DIR: skillsDir,
|
|
});
|
|
expect(fs.existsSync(path.join(skillsDir, 'gstack-qa'))).toBe(true);
|
|
|
|
// Switch to flat mode
|
|
run(`${path.join(installDir, 'bin', 'gstack-config')} set skill_prefix false`);
|
|
run(`${path.join(installDir, 'bin', 'gstack-relink')}`, {
|
|
GSTACK_INSTALL_DIR: installDir,
|
|
GSTACK_SKILLS_DIR: skillsDir,
|
|
});
|
|
|
|
// Flat symlinks should exist, prefixed should be gone
|
|
expect(fs.existsSync(path.join(skillsDir, 'qa'))).toBe(true);
|
|
expect(fs.existsSync(path.join(skillsDir, 'gstack-qa'))).toBe(false);
|
|
});
|
|
|
|
// Test 14: error when install dir missing
|
|
test('prints error when install dir missing', () => {
|
|
const output = run(`${BIN}/gstack-relink`, {
|
|
GSTACK_INSTALL_DIR: '/nonexistent/path/gstack',
|
|
GSTACK_SKILLS_DIR: '/nonexistent/path/skills',
|
|
}, true);
|
|
expect(output).toContain('setup');
|
|
});
|
|
|
|
// Test: gstack-upgrade does NOT get double-prefixed
|
|
test('does not double-prefix gstack-upgrade directory', () => {
|
|
setupMockInstall(['qa', 'ship', 'gstack-upgrade']);
|
|
run(`${path.join(installDir, 'bin', 'gstack-config')} set skill_prefix true`);
|
|
run(`${path.join(installDir, 'bin', 'gstack-relink')}`, {
|
|
GSTACK_INSTALL_DIR: installDir,
|
|
GSTACK_SKILLS_DIR: skillsDir,
|
|
});
|
|
// gstack-upgrade should keep its name, NOT become gstack-gstack-upgrade
|
|
expect(fs.existsSync(path.join(skillsDir, 'gstack-upgrade'))).toBe(true);
|
|
expect(fs.existsSync(path.join(skillsDir, 'gstack-gstack-upgrade'))).toBe(false);
|
|
// Regular skills still get prefixed
|
|
expect(fs.existsSync(path.join(skillsDir, 'gstack-qa'))).toBe(true);
|
|
});
|
|
|
|
// Test 15: gstack-config set skill_prefix triggers relink
|
|
test('gstack-config set skill_prefix triggers relink', () => {
|
|
setupMockInstall(['qa', 'ship']);
|
|
// Run gstack-config set which should auto-trigger relink
|
|
run(`${path.join(installDir, 'bin', 'gstack-config')} set skill_prefix true`, {
|
|
GSTACK_INSTALL_DIR: installDir,
|
|
GSTACK_SKILLS_DIR: skillsDir,
|
|
});
|
|
// If relink was triggered, symlinks should exist
|
|
expect(fs.existsSync(path.join(skillsDir, 'gstack-qa'))).toBe(true);
|
|
expect(fs.existsSync(path.join(skillsDir, 'gstack-ship'))).toBe(true);
|
|
});
|
|
});
|