Initial import from garrytan/gstack@026751e (main snapshot via local relay)
Some checks failed
Workflow Lint / actionlint (push) Has been cancelled
Build CI Image / build (push) Has been cancelled
Skill Docs Freshness / check-freshness (push) Has been cancelled
Periodic Evals / build-image (push) Has been cancelled
Periodic Evals / evals (map[file:test/codex-e2e.test.ts name:e2e-codex]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/gemini-e2e.test.ts name:e2e-gemini]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-design.test.ts name:e2e-design]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-plan.test.ts name:e2e-plan]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-qa-bugs.test.ts name:e2e-qa-bugs]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-qa-workflow.test.ts name:e2e-qa-workflow]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-review.test.ts name:e2e-review]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-workflow.test.ts name:e2e-workflow]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-routing-e2e.test.ts name:e2e-routing]) (push) Has been cancelled
Some checks failed
Workflow Lint / actionlint (push) Has been cancelled
Build CI Image / build (push) Has been cancelled
Skill Docs Freshness / check-freshness (push) Has been cancelled
Periodic Evals / build-image (push) Has been cancelled
Periodic Evals / evals (map[file:test/codex-e2e.test.ts name:e2e-codex]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/gemini-e2e.test.ts name:e2e-gemini]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-design.test.ts name:e2e-design]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-plan.test.ts name:e2e-plan]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-qa-bugs.test.ts name:e2e-qa-bugs]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-qa-workflow.test.ts name:e2e-qa-workflow]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-review.test.ts name:e2e-review]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-workflow.test.ts name:e2e-workflow]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-routing-e2e.test.ts name:e2e-routing]) (push) Has been cancelled
Source: https://github.com/garrytan/gstack/commit/026751e
This commit is contained in:
165
test/uninstall.test.ts
Normal file
165
test/uninstall.test.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { spawnSync } 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 UNINSTALL = path.join(ROOT, 'bin', 'gstack-uninstall');
|
||||
|
||||
describe('gstack-uninstall', () => {
|
||||
test('syntax check passes', () => {
|
||||
const result = spawnSync('bash', ['-n', UNINSTALL], { stdio: 'pipe' });
|
||||
expect(result.status).toBe(0);
|
||||
});
|
||||
|
||||
test('--help prints usage and exits 0', () => {
|
||||
const result = spawnSync('bash', [UNINSTALL, '--help'], { stdio: 'pipe' });
|
||||
expect(result.status).toBe(0);
|
||||
const output = result.stdout.toString();
|
||||
expect(output).toContain('gstack-uninstall');
|
||||
expect(output).toContain('--force');
|
||||
expect(output).toContain('--keep-state');
|
||||
});
|
||||
|
||||
test('unknown flag exits with error', () => {
|
||||
const result = spawnSync('bash', [UNINSTALL, '--bogus'], {
|
||||
stdio: 'pipe',
|
||||
env: { ...process.env, HOME: '/nonexistent' },
|
||||
});
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr.toString()).toContain('Unknown option');
|
||||
});
|
||||
|
||||
describe('integration tests with mock layout', () => {
|
||||
let tmpDir: string;
|
||||
let mockHome: string;
|
||||
let mockGitRoot: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-uninstall-test-'));
|
||||
mockHome = path.join(tmpDir, 'home');
|
||||
mockGitRoot = path.join(tmpDir, 'repo');
|
||||
|
||||
// Create mock gstack install layout
|
||||
fs.mkdirSync(path.join(mockHome, '.claude', 'skills', 'gstack'), { recursive: true });
|
||||
fs.writeFileSync(path.join(mockHome, '.claude', 'skills', 'gstack', 'SKILL.md'), 'test');
|
||||
|
||||
// Create per-skill symlinks (both old unprefixed and new prefixed)
|
||||
fs.symlinkSync('gstack/review', path.join(mockHome, '.claude', 'skills', 'review'));
|
||||
fs.symlinkSync('gstack/ship', path.join(mockHome, '.claude', 'skills', 'gstack-ship'));
|
||||
|
||||
// Create a non-gstack symlink (should NOT be removed)
|
||||
fs.mkdirSync(path.join(mockHome, '.claude', 'skills', 'other-tool'), { recursive: true });
|
||||
|
||||
// Create state directory
|
||||
fs.mkdirSync(path.join(mockHome, '.gstack', 'projects'), { recursive: true });
|
||||
fs.writeFileSync(path.join(mockHome, '.gstack', 'config.json'), '{}');
|
||||
|
||||
// Create mock git repo
|
||||
fs.mkdirSync(mockGitRoot, { recursive: true });
|
||||
spawnSync('git', ['init', '-b', 'main'], { cwd: mockGitRoot, stdio: 'pipe' });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('--force removes global Claude skills and state', () => {
|
||||
const result = spawnSync('bash', [UNINSTALL, '--force'], {
|
||||
stdio: 'pipe',
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: mockHome,
|
||||
GSTACK_DIR: path.join(mockHome, '.claude', 'skills', 'gstack'),
|
||||
GSTACK_STATE_DIR: path.join(mockHome, '.gstack'),
|
||||
},
|
||||
cwd: mockGitRoot,
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
const output = result.stdout.toString();
|
||||
expect(output).toContain('gstack uninstalled');
|
||||
|
||||
// Global skill dir should be removed
|
||||
expect(fs.existsSync(path.join(mockHome, '.claude', 'skills', 'gstack'))).toBe(false);
|
||||
|
||||
// Per-skill symlinks pointing into gstack/ should be removed
|
||||
expect(fs.existsSync(path.join(mockHome, '.claude', 'skills', 'review'))).toBe(false);
|
||||
expect(fs.existsSync(path.join(mockHome, '.claude', 'skills', 'gstack-ship'))).toBe(false);
|
||||
|
||||
// Non-gstack tool should still exist
|
||||
expect(fs.existsSync(path.join(mockHome, '.claude', 'skills', 'other-tool'))).toBe(true);
|
||||
|
||||
// State should be removed
|
||||
expect(fs.existsSync(path.join(mockHome, '.gstack'))).toBe(false);
|
||||
});
|
||||
|
||||
test('--keep-state preserves state directory', () => {
|
||||
const result = spawnSync('bash', [UNINSTALL, '--force', '--keep-state'], {
|
||||
stdio: 'pipe',
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: mockHome,
|
||||
GSTACK_DIR: path.join(mockHome, '.claude', 'skills', 'gstack'),
|
||||
GSTACK_STATE_DIR: path.join(mockHome, '.gstack'),
|
||||
},
|
||||
cwd: mockGitRoot,
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
|
||||
// Skills should be removed
|
||||
expect(fs.existsSync(path.join(mockHome, '.claude', 'skills', 'gstack'))).toBe(false);
|
||||
|
||||
// State should still exist
|
||||
expect(fs.existsSync(path.join(mockHome, '.gstack'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(mockHome, '.gstack', 'config.json'))).toBe(true);
|
||||
});
|
||||
|
||||
test('clean system outputs nothing to remove', () => {
|
||||
const cleanHome = path.join(tmpDir, 'clean-home');
|
||||
fs.mkdirSync(cleanHome, { recursive: true });
|
||||
|
||||
const result = spawnSync('bash', [UNINSTALL, '--force'], {
|
||||
stdio: 'pipe',
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: cleanHome,
|
||||
GSTACK_DIR: path.join(cleanHome, 'nonexistent'),
|
||||
GSTACK_STATE_DIR: path.join(cleanHome, '.gstack'),
|
||||
},
|
||||
cwd: mockGitRoot,
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout.toString()).toContain('Nothing to remove');
|
||||
});
|
||||
|
||||
test('upgrade path: prefixed install + uninstall cleans both old and new symlinks', () => {
|
||||
// Simulate the state after setup --no-prefix followed by setup (with prefix):
|
||||
// Both old unprefixed and new prefixed symlinks exist
|
||||
// (mockHome already has both 'review' and 'gstack-ship' symlinks)
|
||||
|
||||
const result = spawnSync('bash', [UNINSTALL, '--force'], {
|
||||
stdio: 'pipe',
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: mockHome,
|
||||
GSTACK_DIR: path.join(mockHome, '.claude', 'skills', 'gstack'),
|
||||
GSTACK_STATE_DIR: path.join(mockHome, '.gstack'),
|
||||
},
|
||||
cwd: mockGitRoot,
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
|
||||
// Both old (review) and new (gstack-ship) symlinks should be gone
|
||||
expect(fs.existsSync(path.join(mockHome, '.claude', 'skills', 'review'))).toBe(false);
|
||||
expect(fs.existsSync(path.join(mockHome, '.claude', 'skills', 'gstack-ship'))).toBe(false);
|
||||
|
||||
// Non-gstack should survive
|
||||
expect(fs.existsSync(path.join(mockHome, '.claude', 'skills', 'other-tool'))).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user