refactor: replace try/finally with beforeEach/afterEach + add CONTRIBUTING.md

Test suite modernization:
- Converted all try/finally cleanup patterns to beforeEach/afterEach hooks
  across 11 test files (core, copilot-install, config, workstream,
  milestone-summary, forensics, state, antigravity, profile-pipeline,
  workspace)
- Consolidated 40 inline mkdtempSync calls to use centralized helpers
- Added createTempDir() helper for bare temp directories
- Added optional prefix parameter to createTempProject/createTempGitProject
- Fixed config test HOME sandboxing (was reading global defaults.json)

New CONTRIBUTING.md:
- Test standards: hooks over try/finally, centralized helpers, HOME sandboxing
- Node 22/24 compatibility requirements with Node 26 forward-compat
- Code style, PR guidelines, security practices
- File structure overview

All 1382 tests pass, 0 failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-03-24 15:45:39 -04:00
parent 763c6cc642
commit 616c1fa753
12 changed files with 623 additions and 469 deletions

View File

@@ -1017,25 +1017,24 @@ describe('stale hook path', () => {
describe('resolveWorktreeRoot', () => {
const { resolveWorktreeRoot } = require('../get-shit-done/bin/lib/core.cjs');
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('returns cwd when not in a git repo', () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-wt-test-'));
try {
assert.strictEqual(resolveWorktreeRoot(tmpDir), tmpDir);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
assert.strictEqual(resolveWorktreeRoot(tmpDir), tmpDir);
});
test('returns cwd in a normal git repo (not a worktree)', () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-wt-test-'));
try {
const { execSync } = require('child_process');
execSync('git init', { cwd: tmpDir, stdio: 'pipe' });
assert.strictEqual(resolveWorktreeRoot(tmpDir), tmpDir);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
const { execSync: execSyncLocal } = require('child_process');
execSyncLocal('git init', { cwd: tmpDir, stdio: 'pipe' });
assert.strictEqual(resolveWorktreeRoot(tmpDir), tmpDir);
});
});
@@ -1043,78 +1042,70 @@ describe('resolveWorktreeRoot', () => {
describe('resolveWorktreeRoot with linked worktree .planning/', () => {
const { resolveWorktreeRoot } = require('../get-shit-done/bin/lib/core.cjs');
const { execSync } = require('child_process');
const { execSync: execSyncLocal } = require('child_process');
// On Windows CI, os.tmpdir() may return 8.3 short paths (RUNNER~1) while
// git returns long paths (runneradmin). realpathSync.native resolves both.
const normalizePath = (p) => {
try { return fs.realpathSync.native(p); } catch { return fs.realpathSync(p); }
};
test('returns linked worktree cwd when it has its own .planning/', () => {
const mainDir = normalizePath(fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-wt-main-')));
let worktreeDir;
try {
// Set up main repo with a commit
execSync('git init', { cwd: mainDir, stdio: 'pipe' });
execSync('git config user.email "test@test.com"', { cwd: mainDir, stdio: 'pipe' });
execSync('git config user.name "Test"', { cwd: mainDir, stdio: 'pipe' });
execSync('git config commit.gpgsign false', { cwd: mainDir, stdio: 'pipe' });
fs.mkdirSync(path.join(mainDir, '.planning'), { recursive: true });
fs.writeFileSync(path.join(mainDir, 'README.md'), '# Main');
execSync('git add -A', { cwd: mainDir, stdio: 'pipe' });
execSync('git commit -m "initial"', { cwd: mainDir, stdio: 'pipe' });
let mainDir;
let worktreeDir;
// Create a linked worktree
worktreeDir = normalizePath(fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-wt-linked-')));
fs.rmSync(worktreeDir, { recursive: true, force: true });
execSync(`git worktree add "${worktreeDir}" -b test-linked`, { cwd: mainDir, stdio: 'pipe' });
function initBareGitRepo() {
const dir = normalizePath(fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-wt-main-')));
execSyncLocal('git init', { cwd: dir, stdio: 'pipe' });
execSyncLocal('git config user.email "test@test.com"', { cwd: dir, stdio: 'pipe' });
execSyncLocal('git config user.name "Test"', { cwd: dir, stdio: 'pipe' });
execSyncLocal('git config commit.gpgsign false', { cwd: dir, stdio: 'pipe' });
fs.writeFileSync(path.join(dir, 'README.md'), '# Main');
execSyncLocal('git add -A', { cwd: dir, stdio: 'pipe' });
execSyncLocal('git commit -m "initial"', { cwd: dir, stdio: 'pipe' });
return dir;
}
// Give the linked worktree its own .planning/
fs.mkdirSync(path.join(worktreeDir, '.planning'), { recursive: true });
beforeEach(() => {
mainDir = initBareGitRepo();
worktreeDir = null;
});
// resolveWorktreeRoot should return the linked worktree dir, not the main repo
const result = normalizePath(resolveWorktreeRoot(worktreeDir));
assert.strictEqual(result, worktreeDir,
'linked worktree with .planning/ should resolve to itself, not the main repo');
} finally {
if (worktreeDir) {
try { execSync(`git worktree remove "${worktreeDir}" --force`, { cwd: mainDir, stdio: 'pipe' }); } catch { /* ok */ }
try { fs.rmSync(worktreeDir, { recursive: true, force: true }); } catch { /* ok */ }
}
fs.rmSync(mainDir, { recursive: true, force: true });
afterEach(() => {
if (worktreeDir) {
try { execSyncLocal(`git worktree remove "${worktreeDir}" --force`, { cwd: mainDir, stdio: 'pipe' }); } catch { /* ok */ }
try { fs.rmSync(worktreeDir, { recursive: true, force: true }); } catch { /* ok */ }
}
cleanup(mainDir);
});
test('returns linked worktree cwd when it has its own .planning/', () => {
// Add .planning/ to main repo
fs.mkdirSync(path.join(mainDir, '.planning'), { recursive: true });
// Create a linked worktree
worktreeDir = normalizePath(fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-wt-linked-')));
fs.rmSync(worktreeDir, { recursive: true, force: true });
execSyncLocal(`git worktree add "${worktreeDir}" -b test-linked`, { cwd: mainDir, stdio: 'pipe' });
// Give the linked worktree its own .planning/
fs.mkdirSync(path.join(worktreeDir, '.planning'), { recursive: true });
// resolveWorktreeRoot should return the linked worktree dir, not the main repo
const result = normalizePath(resolveWorktreeRoot(worktreeDir));
assert.strictEqual(result, worktreeDir,
'linked worktree with .planning/ should resolve to itself, not the main repo');
});
test('returns main repo root when linked worktree has no .planning/', () => {
const mainDir = normalizePath(fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-wt-main-')));
let worktreeDir;
try {
// Set up main repo with a commit
execSync('git init', { cwd: mainDir, stdio: 'pipe' });
execSync('git config user.email "test@test.com"', { cwd: mainDir, stdio: 'pipe' });
execSync('git config user.name "Test"', { cwd: mainDir, stdio: 'pipe' });
execSync('git config commit.gpgsign false', { cwd: mainDir, stdio: 'pipe' });
fs.writeFileSync(path.join(mainDir, 'README.md'), '# Main');
execSync('git add -A', { cwd: mainDir, stdio: 'pipe' });
execSync('git commit -m "initial"', { cwd: mainDir, stdio: 'pipe' });
// Create a linked worktree (no .planning/ in main or worktree)
worktreeDir = normalizePath(fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-wt-linked-')));
fs.rmSync(worktreeDir, { recursive: true, force: true });
execSyncLocal(`git worktree add "${worktreeDir}" -b test-linked-no-plan`, { cwd: mainDir, stdio: 'pipe' });
// Create a linked worktree (no .planning/)
worktreeDir = normalizePath(fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-wt-linked-')));
fs.rmSync(worktreeDir, { recursive: true, force: true });
execSync(`git worktree add "${worktreeDir}" -b test-linked-no-plan`, { cwd: mainDir, stdio: 'pipe' });
// resolveWorktreeRoot should return the main repo root
const result = normalizePath(resolveWorktreeRoot(worktreeDir));
const expected = normalizePath(mainDir);
assert.strictEqual(result, expected,
'linked worktree without .planning/ should resolve to main repo root');
} finally {
if (worktreeDir) {
try { execSync(`git worktree remove "${worktreeDir}" --force`, { cwd: mainDir, stdio: 'pipe' }); } catch { /* ok */ }
try { fs.rmSync(worktreeDir, { recursive: true, force: true }); } catch { /* ok */ }
}
fs.rmSync(mainDir, { recursive: true, force: true });
}
// resolveWorktreeRoot should return the main repo root
const result = normalizePath(resolveWorktreeRoot(worktreeDir));
const expected = normalizePath(mainDir);
assert.strictEqual(result, expected,
'linked worktree without .planning/ should resolve to main repo root');
});
});
@@ -1122,37 +1113,36 @@ describe('resolveWorktreeRoot with linked worktree .planning/', () => {
describe('monorepo worktree CWD preservation', () => {
const { resolveWorktreeRoot } = require('../get-shit-done/bin/lib/core.cjs');
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-monorepo-wt-'));
});
afterEach(() => {
cleanup(tmpDir);
});
test('CWD with .planning/ skips worktree resolution (monorepo subdirectory)', () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-monorepo-wt-'));
const subDir = path.join(tmpDir, 'service-alpha');
fs.mkdirSync(path.join(subDir, '.planning'), { recursive: true });
try {
let cwd = subDir;
if (!fs.existsSync(path.join(cwd, '.planning'))) {
const worktreeRoot = resolveWorktreeRoot(cwd);
if (worktreeRoot !== cwd) cwd = worktreeRoot;
}
assert.strictEqual(cwd, subDir, 'CWD with .planning/ must not be overridden by worktree resolution');
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
let cwd = subDir;
if (!fs.existsSync(path.join(cwd, '.planning'))) {
const worktreeRoot = resolveWorktreeRoot(cwd);
if (worktreeRoot !== cwd) cwd = worktreeRoot;
}
assert.strictEqual(cwd, subDir, 'CWD with .planning/ must not be overridden by worktree resolution');
});
test('CWD without .planning/ still goes through worktree resolution', () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-monorepo-wt-'));
try {
let cwd = tmpDir;
let worktreeResolutionCalled = false;
if (!fs.existsSync(path.join(cwd, '.planning'))) {
worktreeResolutionCalled = true;
const worktreeRoot = resolveWorktreeRoot(cwd);
if (worktreeRoot !== cwd) cwd = worktreeRoot;
}
assert.ok(worktreeResolutionCalled, 'worktree resolution must be called when .planning/ is absent');
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
let cwd = tmpDir;
let worktreeResolutionCalled = false;
if (!fs.existsSync(path.join(cwd, '.planning'))) {
worktreeResolutionCalled = true;
const worktreeRoot = resolveWorktreeRoot(cwd);
if (worktreeRoot !== cwd) cwd = worktreeRoot;
}
assert.ok(worktreeResolutionCalled, 'worktree resolution must be called when .planning/ is absent');
});
});
@@ -1160,50 +1150,40 @@ describe('monorepo worktree CWD preservation', () => {
describe('withPlanningLock', () => {
const { withPlanningLock, planningDir } = require('../get-shit-done/bin/lib/core.cjs');
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('executes function and returns result', () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-lock-test-'));
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
try {
const result = withPlanningLock(tmpDir, () => 42);
assert.strictEqual(result, 42);
// Lock file should be cleaned up
assert.ok(!fs.existsSync(path.join(planningDir(tmpDir), '.lock')));
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
const result = withPlanningLock(tmpDir, () => 42);
assert.strictEqual(result, 42);
// Lock file should be cleaned up
assert.ok(!fs.existsSync(path.join(planningDir(tmpDir), '.lock')));
});
test('cleans up lock file even on error', () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-lock-test-'));
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
try {
assert.throws(() => {
withPlanningLock(tmpDir, () => { throw new Error('test'); });
}, /test/);
assert.ok(!fs.existsSync(path.join(planningDir(tmpDir), '.lock')));
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
assert.throws(() => {
withPlanningLock(tmpDir, () => { throw new Error('test'); });
}, /test/);
assert.ok(!fs.existsSync(path.join(planningDir(tmpDir), '.lock')));
});
test('recovers from stale lock (>30s old)', () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-lock-test-'));
const planDir = path.join(tmpDir, '.planning');
fs.mkdirSync(planDir, { recursive: true });
const lockPath = path.join(planDir, '.lock');
try {
// Create a stale lock
fs.writeFileSync(lockPath, '{"pid":99999}');
// Backdate the lock file by 31 seconds
const staleTime = new Date(Date.now() - 31000);
fs.utimesSync(lockPath, staleTime, staleTime);
const lockPath = path.join(tmpDir, '.planning', '.lock');
// Create a stale lock
fs.writeFileSync(lockPath, '{"pid":99999}');
// Backdate the lock file by 31 seconds
const staleTime = new Date(Date.now() - 31000);
fs.utimesSync(lockPath, staleTime, staleTime);
const result = withPlanningLock(tmpDir, () => 'recovered');
assert.strictEqual(result, 'recovered');
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
const result = withPlanningLock(tmpDir, () => 'recovered');
assert.strictEqual(result, 'recovered');
});
});