mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
255 lines
8.6 KiB
JavaScript
255 lines
8.6 KiB
JavaScript
/**
|
|
* Tests for worktree project-ID mismatch fix
|
|
*
|
|
* Validates that detect-project.sh uses -e (not -d) for .git existence
|
|
* checks, so that git worktrees (where .git is a file) are detected
|
|
* correctly.
|
|
*
|
|
* Run with: node tests/hooks/detect-project-worktree.test.js
|
|
*/
|
|
|
|
const assert = require('assert');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const { execSync } = require('child_process');
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
console.log(` \u2713 ${name}`);
|
|
passed++;
|
|
} catch (err) {
|
|
console.log(` \u2717 ${name}`);
|
|
console.log(` Error: ${err.message}`);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
function createTempDir() {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-worktree-test-'));
|
|
}
|
|
|
|
function cleanupDir(dir) {
|
|
try {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
} catch {
|
|
// ignore cleanup errors
|
|
}
|
|
}
|
|
|
|
function toBashPath(filePath) {
|
|
if (process.platform !== 'win32') {
|
|
return filePath;
|
|
}
|
|
|
|
return String(filePath)
|
|
.replace(/^([A-Za-z]):/, (_, driveLetter) => `/${driveLetter.toLowerCase()}`)
|
|
.replace(/\\/g, '/');
|
|
}
|
|
|
|
function runBash(command, options = {}) {
|
|
return execSync(`bash -lc '${command.replace(/'/g, "'\\''")}'`, options).toString().trim();
|
|
}
|
|
|
|
const repoRoot = path.resolve(__dirname, '..', '..');
|
|
const detectProjectPath = path.join(
|
|
repoRoot,
|
|
'skills',
|
|
'continuous-learning-v2',
|
|
'scripts',
|
|
'detect-project.sh'
|
|
);
|
|
|
|
console.log('\n=== Worktree Project-ID Mismatch Tests ===\n');
|
|
|
|
// ──────────────────────────────────────────────────────
|
|
// Group 1: Content checks on detect-project.sh
|
|
// ──────────────────────────────────────────────────────
|
|
|
|
console.log('--- Content checks on detect-project.sh ---');
|
|
|
|
test('uses -e (not -d) for .git existence check', () => {
|
|
const content = fs.readFileSync(detectProjectPath, 'utf8');
|
|
assert.ok(
|
|
content.includes('[ -e "${project_root}/.git" ]'),
|
|
'detect-project.sh should use -e for .git check'
|
|
);
|
|
assert.ok(
|
|
!content.includes('[ -d "${project_root}/.git" ]'),
|
|
'detect-project.sh should NOT use -d for .git check'
|
|
);
|
|
});
|
|
|
|
test('has command -v git fallback check', () => {
|
|
const content = fs.readFileSync(detectProjectPath, 'utf8');
|
|
assert.ok(
|
|
content.includes('command -v git'),
|
|
'detect-project.sh should check for git availability with command -v'
|
|
);
|
|
});
|
|
|
|
test('uses git -C for safe directory operations', () => {
|
|
const content = fs.readFileSync(detectProjectPath, 'utf8');
|
|
assert.ok(
|
|
content.includes('git -C'),
|
|
'detect-project.sh should use git -C for directory-scoped operations'
|
|
);
|
|
});
|
|
|
|
// ──────────────────────────────────────────────────────
|
|
// Group 2: Behavior test — -e vs -d
|
|
// ──────────────────────────────────────────────────────
|
|
|
|
console.log('\n--- Behavior test: -e vs -d ---');
|
|
|
|
const behaviorDir = createTempDir();
|
|
|
|
test('[ -d ] returns true for .git directory', () => {
|
|
const dir = path.join(behaviorDir, 'test-d-dir');
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
fs.mkdirSync(path.join(dir, '.git'));
|
|
const result = runBash(`[ -d "${toBashPath(path.join(dir, '.git'))}" ] && echo yes || echo no`);
|
|
assert.strictEqual(result, 'yes');
|
|
});
|
|
|
|
test('[ -d ] returns false for .git file', () => {
|
|
const dir = path.join(behaviorDir, 'test-d-file');
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
fs.writeFileSync(path.join(dir, '.git'), 'gitdir: /some/path\n');
|
|
const result = runBash(`[ -d "${toBashPath(path.join(dir, '.git'))}" ] && echo yes || echo no`);
|
|
assert.strictEqual(result, 'no');
|
|
});
|
|
|
|
test('[ -e ] returns true for .git directory', () => {
|
|
const dir = path.join(behaviorDir, 'test-e-dir');
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
fs.mkdirSync(path.join(dir, '.git'));
|
|
const result = runBash(`[ -e "${toBashPath(path.join(dir, '.git'))}" ] && echo yes || echo no`);
|
|
assert.strictEqual(result, 'yes');
|
|
});
|
|
|
|
test('[ -e ] returns true for .git file', () => {
|
|
const dir = path.join(behaviorDir, 'test-e-file');
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
fs.writeFileSync(path.join(dir, '.git'), 'gitdir: /some/path\n');
|
|
const result = runBash(`[ -e "${toBashPath(path.join(dir, '.git'))}" ] && echo yes || echo no`);
|
|
assert.strictEqual(result, 'yes');
|
|
});
|
|
|
|
test('[ -e ] returns false when .git does not exist', () => {
|
|
const dir = path.join(behaviorDir, 'test-e-none');
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
const result = runBash(`[ -e "${toBashPath(path.join(dir, '.git'))}" ] && echo yes || echo no`);
|
|
assert.strictEqual(result, 'no');
|
|
});
|
|
|
|
cleanupDir(behaviorDir);
|
|
|
|
// ──────────────────────────────────────────────────────
|
|
// Group 3: E2E test — detect-project.sh with worktree .git file
|
|
// ──────────────────────────────────────────────────────
|
|
|
|
console.log('\n--- E2E: detect-project.sh with worktree .git file ---');
|
|
|
|
test('detect-project.sh sets PROJECT_NAME and non-global PROJECT_ID for worktree', () => {
|
|
const testDir = createTempDir();
|
|
|
|
try {
|
|
// Create a "main" repo with git init so we have real git structures
|
|
const mainRepo = path.join(testDir, 'main-repo');
|
|
fs.mkdirSync(mainRepo, { recursive: true });
|
|
execSync('git init', { cwd: mainRepo, stdio: 'pipe' });
|
|
execSync('git commit --allow-empty -m "init"', {
|
|
cwd: mainRepo,
|
|
stdio: 'pipe',
|
|
env: {
|
|
...process.env,
|
|
GIT_AUTHOR_NAME: 'Test',
|
|
GIT_AUTHOR_EMAIL: 'test@test.com',
|
|
GIT_COMMITTER_NAME: 'Test',
|
|
GIT_COMMITTER_EMAIL: 'test@test.com'
|
|
}
|
|
});
|
|
|
|
// Create a worktree-like directory with .git as a file
|
|
const worktreeDir = path.join(testDir, 'my-worktree');
|
|
fs.mkdirSync(worktreeDir, { recursive: true });
|
|
|
|
// Set up the worktree directory structure in the main repo
|
|
const worktreesDir = path.join(mainRepo, '.git', 'worktrees', 'my-worktree');
|
|
fs.mkdirSync(worktreesDir, { recursive: true });
|
|
|
|
// Create the gitdir file and commondir in the worktree metadata
|
|
const mainGitDir = path.join(mainRepo, '.git');
|
|
fs.writeFileSync(
|
|
path.join(worktreesDir, 'commondir'),
|
|
'../..\n'
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(worktreesDir, 'HEAD'),
|
|
fs.readFileSync(path.join(mainGitDir, 'HEAD'), 'utf8')
|
|
);
|
|
|
|
// Write .git file in the worktree directory (this is what git worktree creates)
|
|
fs.writeFileSync(
|
|
path.join(worktreeDir, '.git'),
|
|
`gitdir: ${worktreesDir}\n`
|
|
);
|
|
|
|
// Source detect-project.sh from the worktree directory and capture results
|
|
const script = `
|
|
export CLAUDE_PROJECT_DIR="${toBashPath(worktreeDir)}"
|
|
export HOME="${toBashPath(testDir)}"
|
|
source "${toBashPath(detectProjectPath)}"
|
|
echo "PROJECT_NAME=\${PROJECT_NAME}"
|
|
echo "PROJECT_ID=\${PROJECT_ID}"
|
|
`;
|
|
|
|
const result = execSync(`bash -lc '${script.replace(/'/g, "'\\''")}'`, {
|
|
cwd: worktreeDir,
|
|
timeout: 10000,
|
|
env: {
|
|
...process.env,
|
|
HOME: toBashPath(testDir),
|
|
USERPROFILE: testDir,
|
|
CLAUDE_PROJECT_DIR: toBashPath(worktreeDir)
|
|
}
|
|
}).toString();
|
|
|
|
const lines = result.trim().split('\n');
|
|
const vars = {};
|
|
for (const line of lines) {
|
|
const match = line.match(/^(PROJECT_NAME|PROJECT_ID)=(.*)$/);
|
|
if (match) {
|
|
vars[match[1]] = match[2];
|
|
}
|
|
}
|
|
|
|
assert.ok(
|
|
vars.PROJECT_NAME && vars.PROJECT_NAME.length > 0,
|
|
`PROJECT_NAME should be set, got: "${vars.PROJECT_NAME || ''}"`
|
|
);
|
|
assert.ok(
|
|
vars.PROJECT_ID && vars.PROJECT_ID !== 'global',
|
|
`PROJECT_ID should not be "global", got: "${vars.PROJECT_ID || ''}"`
|
|
);
|
|
} finally {
|
|
cleanupDir(testDir);
|
|
}
|
|
});
|
|
|
|
// ──────────────────────────────────────────────────────
|
|
// Summary
|
|
// ──────────────────────────────────────────────────────
|
|
|
|
console.log('\n=== Test Results ===');
|
|
console.log(`Passed: ${passed}`);
|
|
console.log(`Failed: ${failed}`);
|
|
console.log(`Total: ${passed + failed}\n`);
|
|
|
|
process.exit(failed > 0 ? 1 : 0);
|