mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-11 02:33:10 +08:00
* feat: add worktree-lifecycle service (ecc.worktree-lifecycle.v1) The "unowned moat" from the orchestrator landscape research: no existing tool ships deterministic merge-conflict prediction or a safe worktree GC. - scripts/lib/worktree-lifecycle/git.js: injectable, hermetic git layer. Predicts merge conflicts WITHOUT touching the working tree via `git merge-tree`. Strips inherited GIT_* env so it is safe inside hooks. - scripts/lib/worktree-lifecycle/lifecycle.js: deterministic state machine (main/dirty/conflict/merge-ready/merged/stale/idle) + planCleanup that buckets worktrees into remove / salvage / keep. Only fully-merged trees are auto-removable; stale (unmerged+inactive) => salvage, never deleted. - scripts/worktree-lifecycle.js: CLI (--json/--conflicts/--stale/ --cleanup-plan/--base/--stale-days/--repo). - tests/lib/worktree-lifecycle.test.js: 11 tests (fake-git + real-git). Safety model mirrors the reference-arch salvage rule, validated by the 2026-06-05 MacBook->Mac Mini consolidation. Tests: 11/0. * fix: hermetic git env in session adapters + mcp-inventory lint - session adapters (codex-worktree, opencode): resolveGitBranch stripped no git env, so the "outside a repo" path returned the host branch when run inside a git hook (GIT_DIR set). Strip GIT_* before rev-parse. - mcp-inventory: fix eslint no-unused-vars (signatures) and a stale eslint-disable directive in the merged code. * test: run each test with inherited git env stripped (hermetic runner) When the suite runs inside a git hook (pre-push), git sets GIT_DIR/ GIT_WORK_TREE, which hijack 'git -C <dir>' calls in tests that exercise real git, making them operate on the host repo. Strip GIT_* before spawning each test so the suite is isolated from ambient git state. --------- Co-authored-by: ECC Test <ecc@example.test>
129 lines
3.9 KiB
JavaScript
129 lines
3.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Run all tests
|
|
*
|
|
* Usage: node tests/run-all.js
|
|
*/
|
|
|
|
const { spawnSync } = require('child_process');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
const testsDir = __dirname;
|
|
const repoRoot = path.resolve(testsDir, '..');
|
|
const TEST_GLOB = 'tests/**/*.test.js';
|
|
|
|
function matchesTestGlob(relativePath) {
|
|
const normalized = relativePath.split(path.sep).join('/');
|
|
if (typeof path.matchesGlob === 'function') {
|
|
return path.matchesGlob(normalized, TEST_GLOB);
|
|
}
|
|
|
|
return /^tests\/(?:.+\/)?[^/]+\.test\.js$/.test(normalized);
|
|
}
|
|
|
|
function walkFiles(dir, acc = []) {
|
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
walkFiles(fullPath, acc);
|
|
} else if (entry.isFile()) {
|
|
acc.push(fullPath);
|
|
}
|
|
}
|
|
return acc;
|
|
}
|
|
|
|
function discoverTestFiles() {
|
|
return walkFiles(testsDir)
|
|
.map(fullPath => path.relative(repoRoot, fullPath))
|
|
.filter(matchesTestGlob)
|
|
.map(repoRelativePath => path.relative(testsDir, path.join(repoRoot, repoRelativePath)))
|
|
.sort();
|
|
}
|
|
|
|
const testFiles = discoverTestFiles();
|
|
|
|
const BOX_W = 58; // inner width between ║ delimiters
|
|
const boxLine = s => `║${s.padEnd(BOX_W)}║`;
|
|
|
|
console.log('╔' + '═'.repeat(BOX_W) + '╗');
|
|
console.log(boxLine(' Everything Claude Code - Test Suite'));
|
|
console.log('╚' + '═'.repeat(BOX_W) + '╝');
|
|
console.log();
|
|
|
|
if (testFiles.length === 0) {
|
|
console.log(`✗ No test files matched ${TEST_GLOB}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
let totalPassed = 0;
|
|
let totalFailed = 0;
|
|
let totalTests = 0;
|
|
|
|
for (const testFile of testFiles) {
|
|
const testPath = path.join(testsDir, testFile);
|
|
const displayPath = testFile.split(path.sep).join('/');
|
|
|
|
if (!fs.existsSync(testPath)) {
|
|
console.log(`WARNING Skipping ${displayPath} (file not found)`);
|
|
continue;
|
|
}
|
|
|
|
console.log(`\n━━━ Running ${displayPath} ━━━`);
|
|
|
|
// Run each test hermetically: strip inherited git env vars. When the suite
|
|
// runs inside a git hook (e.g. pre-push), git sets GIT_DIR/GIT_WORK_TREE,
|
|
// which would hijack `git -C <dir>` calls in tests that exercise real git
|
|
// and make them operate on the host repo instead of their own fixtures.
|
|
const childEnv = { ...process.env };
|
|
for (const key of ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_COMMON_DIR', 'GIT_PREFIX']) {
|
|
delete childEnv[key];
|
|
}
|
|
|
|
const result = spawnSync('node', [testPath], {
|
|
encoding: 'utf8',
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
env: childEnv
|
|
});
|
|
|
|
const stdout = result.stdout || '';
|
|
const stderr = result.stderr || '';
|
|
|
|
// Show both stdout and stderr so hook warnings are visible
|
|
if (stdout) console.log(stdout);
|
|
if (stderr) console.log(stderr);
|
|
|
|
// Parse results from combined output
|
|
const combined = stdout + stderr;
|
|
const passedMatch = combined.match(/Passed:\s*(\d+)/);
|
|
const failedMatch = combined.match(/Failed:\s*(\d+)/);
|
|
|
|
if (passedMatch) totalPassed += parseInt(passedMatch[1], 10);
|
|
if (failedMatch) totalFailed += parseInt(failedMatch[1], 10);
|
|
|
|
if (result.error) {
|
|
console.log(`✗ ${displayPath} failed to start: ${result.error.message}`);
|
|
totalFailed += failedMatch ? 0 : 1;
|
|
continue;
|
|
}
|
|
|
|
if (result.status !== 0) {
|
|
console.log(`✗ ${displayPath} exited with status ${result.status}`);
|
|
totalFailed += failedMatch ? 0 : 1;
|
|
}
|
|
}
|
|
|
|
totalTests = totalPassed + totalFailed;
|
|
|
|
console.log('\n╔' + '═'.repeat(BOX_W) + '╗');
|
|
console.log(boxLine(' Final Results'));
|
|
console.log('╠' + '═'.repeat(BOX_W) + '╣');
|
|
console.log(boxLine(` Total Tests: ${String(totalTests).padStart(4)}`));
|
|
console.log(boxLine(` Passed: ${String(totalPassed).padStart(4)} ✓`));
|
|
console.log(boxLine(` Failed: ${String(totalFailed).padStart(4)} ${totalFailed > 0 ? '✗' : ' '}`));
|
|
console.log('╚' + '═'.repeat(BOX_W) + '╝');
|
|
|
|
process.exit(totalFailed > 0 ? 1 : 0);
|