fix(learning): add project registry maintenance

This commit is contained in:
Affaan Mustafa
2026-05-19 12:26:08 -04:00
committed by Affaan Mustafa
parent 98bd517451
commit bc519e5b8e
7 changed files with 706 additions and 51 deletions
+62 -22
View File
@@ -181,29 +181,14 @@ test('detect-project.sh sets PROJECT_NAME and non-global PROJECT_ID for worktree
}
});
// 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`
execSync(`git worktree add "${worktreeDir}" -b feature/project-id`, {
cwd: mainRepo,
stdio: 'pipe'
});
assert.ok(
fs.statSync(path.join(worktreeDir, '.git')).isFile(),
'linked worktree should expose .git as a file'
);
// Source detect-project.sh from the worktree directory and capture results
@@ -248,6 +233,61 @@ test('detect-project.sh sets PROJECT_NAME and non-global PROJECT_ID for worktree
}
});
test('detect-project.sh uses the main worktree hash when no remote exists', () => {
const testDir = createTempDir();
try {
const mainRepo = path.join(testDir, 'main-repo');
const worktreeDir = path.join(testDir, 'feature-worktree');
const homeDir = path.join(testDir, 'home');
fs.mkdirSync(mainRepo, { recursive: true });
fs.mkdirSync(homeDir, { 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'
}
});
execSync(`git worktree add "${worktreeDir}" -b feature/no-remote`, {
cwd: mainRepo,
stdio: 'pipe'
});
function detectId(targetDir) {
const script = `
export HOME="${toBashPath(homeDir)}"
export USERPROFILE="${toBashPath(homeDir)}"
export CLAUDE_PROJECT_DIR="${toBashPath(targetDir)}"
source "${toBashPath(detectProjectPath)}" >/dev/null
printf "%s" "$PROJECT_ID"
`;
return execFileSync('bash', ['-lc', script], {
cwd: targetDir,
timeout: 10000,
env: {
...process.env,
HOME: toBashPath(homeDir),
USERPROFILE: toBashPath(homeDir),
CLAUDE_PROJECT_DIR: toBashPath(targetDir)
}
}).toString();
}
const mainId = detectId(mainRepo);
const worktreeId = detectId(worktreeDir);
assert.ok(mainId && mainId !== 'global', 'main repo should get a project id');
assert.strictEqual(worktreeId, mainId, 'linked worktree should share the main worktree project id');
} finally {
cleanupDir(testDir);
}
});
// ──────────────────────────────────────────────────────
// Summary
// ──────────────────────────────────────────────────────
+7 -4
View File
@@ -3300,11 +3300,14 @@ async function runTests() {
assert.strictEqual(result.code, 0, `observe.sh should exit successfully, stderr: ${result.stderr}`);
const projectsDir = path.join(homeDir, '.local', 'share', 'ecc-homunculus', 'projects');
const projectIds = fs.readdirSync(projectsDir);
assert.strictEqual(projectIds.length, 1, 'observe.sh should create one project-scoped observation directory');
const homunculusDir = path.join(homeDir, '.local', 'share', 'ecc-homunculus');
const projectsDir = path.join(homunculusDir, 'projects');
assert.ok(
!fs.existsSync(projectsDir) || fs.readdirSync(projectsDir).length === 0,
'observe.sh should not create a project-scoped directory for a non-git cwd'
);
const observationsPath = path.join(projectsDir, projectIds[0], 'observations.jsonl');
const observationsPath = path.join(homunculusDir, 'observations.jsonl');
const observations = fs.readFileSync(observationsPath, 'utf8').trim().split('\n').filter(Boolean);
assert.ok(observations.length > 0, 'observe.sh should append at least one observation');
@@ -135,8 +135,8 @@ test('observe.sh resolves cwd to git root before setting CLAUDE_PROJECT_DIR', ()
'observe.sh should resolve STDIN_CWD to git repo root'
);
assert.ok(
content.includes('${_GIT_ROOT:-$STDIN_CWD}'),
'observe.sh should fall back to raw cwd when git root is unavailable'
content.includes('export CLV2_NO_PROJECT=1'),
'observe.sh should mark non-git cwd payloads as global instead of registering raw cwd'
);
});
@@ -250,7 +250,7 @@ test('observe.sh falls back to CLAUDE_HOOK_EVENT_NAME when no phase argument is
}
});
test('observe.sh keeps the raw cwd when the directory is not inside a git repo', () => {
test('observe.sh records non-git cwd payloads globally without project registry side effects', () => {
const testRoot = createTempDir();
try {
@@ -262,12 +262,17 @@ test('observe.sh keeps the raw cwd when the directory is not inside a git repo',
const result = runObserve({ homeDir, cwd: nonGitDir });
assert.strictEqual(result.status, 0, result.stderr);
const { metadata } = readSingleProjectMetadata(homeDir);
assert.strictEqual(
normalizeComparablePath(metadata.root),
normalizeComparablePath(nonGitDir),
'project metadata root should stay on the non-git cwd'
const homunculusDir = path.join(homeDir, '.local', 'share', 'ecc-homunculus');
const projectsDir = path.join(homunculusDir, 'projects');
const registryPath = path.join(homunculusDir, 'projects.json');
const observationsPath = path.join(homunculusDir, 'observations.jsonl');
assert.ok(!fs.existsSync(registryPath), 'non-git cwd should not create projects.json');
assert.ok(
!fs.existsSync(projectsDir) || fs.readdirSync(projectsDir).length === 0,
'non-git cwd should not create project directories'
);
assert.ok(fs.existsSync(observationsPath), 'non-git cwd should still record a global observation');
} finally {
cleanupDir(testRoot);
}