mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-05 06:59:42 +02:00
fix(history): never treat the empty split segment as a directory name
Follow-up to #202. The dotdir decode landed there was reachable only when nothing else matched first, and in the greedy half it was not reachable at all. decodeProjectKey() splits the project key on '-', so the '/.' that the encoder collapses leaves an EMPTY segment behind. Both loops offered that empty string as a candidate directory name, and isDir(current + '/' + '') stats current + '/', which always succeeds. So the empty segment matched unconditionally: - backtracking half: ~/.sib resolved to "/home/x//sib" whenever a non-dot sibling ~/sib existed (wrong directory, and a doubled slash that then fails every string comparison against session.workingDir). Without a sibling it only backtracked out by luck. - greedy half: that loop is shortest-match-first, so the empty candidate matched on the FIRST iteration and set matched=true, leaving #202's dotdir branch permanently dead there. An empty string is never a real path component, so skip it in both loops. The unmatched tail then has to handle the empty segment too, or it would append a bare '/' and re-introduce the '//' path it just stopped producing; it now emits the dotdir guess instead, which is what the encoder implies. Regression test asserts both halves: the dotdir wins over the non-dot sibling, and the result never contains '//'. Verified it fails on #202 as merged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1321,6 +1321,35 @@ describe('session-routes', () => {
|
||||
expect(row.workingDir).toBe(realDir);
|
||||
expect(row.workingDir).not.toBe(home);
|
||||
});
|
||||
|
||||
it('prefers the dotdir over a same-named non-dot sibling, and never emits a "//" path', async () => {
|
||||
// A doubled dash also lets the decoder read the empty split segment as a
|
||||
// directory NAME. `isDir(current + '/' + '')` stats `current + '/'`, which
|
||||
// always succeeds, so `~/.sib` + `~/sib` both existing used to resolve to
|
||||
// "/home/x//sib": the wrong directory, spelled with a double slash that
|
||||
// then fails every string comparison against session.workingDir. The empty
|
||||
// candidate is never a real path component, so it is skipped outright,
|
||||
// which is also what lets the dotdir branch below it run at all.
|
||||
const home = process.env.HOME as string;
|
||||
const dotDir = join(home, '.sib');
|
||||
await mkdir(dotDir, { recursive: true });
|
||||
await mkdir(join(home, 'sib'), { recursive: true });
|
||||
|
||||
const projectKey = dotDir.replace(/\//g, '-').replace(/\./g, '-');
|
||||
const projDir = join(home, '.claude', 'projects', projectKey);
|
||||
await mkdir(projDir, { recursive: true });
|
||||
|
||||
const sessionId = '22222222-2222-2222-2222-222222222222';
|
||||
const line = JSON.stringify({ type: 'user', message: { role: 'user', content: 'hello world' } }) + '\n';
|
||||
await writeFile(join(projDir, `${sessionId}.jsonl`), line + '#'.repeat(4200 - line.length));
|
||||
|
||||
const res = await harness.app.inject({ method: 'GET', url: `/api/history/sessions?projectKey=${projectKey}` });
|
||||
expect(res.statusCode).toBe(200);
|
||||
const row = JSON.parse(res.body).data.sessions.find((s: { sessionId: string }) => s.sessionId === sessionId);
|
||||
expect(row).toBeDefined();
|
||||
expect(row.workingDir).toBe(dotDir);
|
||||
expect(row.workingDir).not.toContain('//');
|
||||
});
|
||||
});
|
||||
|
||||
// ========== POST /api/sessions (with resumeSessionId) ==========
|
||||
|
||||
Reference in New Issue
Block a user