mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-05 23:19:43 +02:00
fix(review): dedupe resumed-session rows + newest-wins lifecycle name/mode (PR #139)
- Duplicate rows: transcript-history rows are keyed by the Claude conversation UUID (.jsonl filename stem), which diverges from the Codeman session id for resumed (claudeSessionId = resumeSessionId != id) and /clear-respawned sessions, so one conversation surfaced as both a live row and a history-only row. mergeUnifiedSessions now builds an alias map (claudeSessionId -> Codeman id) from the live + persisted views and resolves history/lifecycle keys through it; the route feeds SessionState.resumeSessionId as the persisted alias. - Inverted precedence: SessionLifecycleLog.query() returns entries NEWEST-first, but the merge loop unconditionally overwrote name/mode so the OLDEST entry in the window won (stale rename/mode). First-seen now wins, mirroring the existing lastActivityAt guard. - Tests: resumed session yields ONE row (service unit + route end-to-end with a real transcript fixture); renamed-then-deleted session surfaces the NEWEST name/mode. All 4 new tests fail against the pre-fix code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,60 @@ describe('mergeUnifiedSessions', () => {
|
||||
expect(item.isWorking).toBe(true);
|
||||
});
|
||||
|
||||
it('folds a resumed session transcript (claudeSessionId != id) into ONE row', () => {
|
||||
const merged = mergeUnifiedSessions({
|
||||
live: [{ id: 'cm-1', status: 'working', claudeSessionId: 'uuid-resume' }],
|
||||
history: [
|
||||
{
|
||||
sessionId: 'uuid-resume', // transcript rows are keyed by the conversation UUID
|
||||
workingDir: '/w',
|
||||
sizeBytes: 7000,
|
||||
lastModified: '2026-01-03T00:00:00.000Z',
|
||||
firstPrompt: 'resumed prompt',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(merged).toHaveLength(1);
|
||||
const item = merged[0];
|
||||
expect(item.sessionId).toBe('cm-1');
|
||||
expect([...item.sources].sort()).toEqual(['history', 'live']);
|
||||
expect(item.firstPrompt).toBe('resumed prompt');
|
||||
expect(item.sizeBytes).toBe(7000);
|
||||
});
|
||||
|
||||
it('resolves history rows through a persisted claudeSessionId alias', () => {
|
||||
const merged = mergeUnifiedSessions({
|
||||
persisted: [{ id: 'cm-2', name: 'Resumed', claudeSessionId: 'uuid-p' }],
|
||||
history: [{ sessionId: 'uuid-p', workingDir: '/w', sizeBytes: 4200, lastModified: '2026-01-04T00:00:00.000Z' }],
|
||||
});
|
||||
expect(merged).toHaveLength(1);
|
||||
expect(merged[0].sessionId).toBe('cm-2');
|
||||
expect([...merged[0].sources].sort()).toEqual(['history', 'persisted']);
|
||||
});
|
||||
|
||||
it('surfaces the NEWEST lifecycle name/mode (entries arrive newest-first)', () => {
|
||||
const merged = mergeUnifiedSessions({
|
||||
// query() returns newest-first: the rename must win over the original
|
||||
// name — an unconditional overwrite would leave the OLDEST standing.
|
||||
lifecycle: [
|
||||
{ sessionId: 'del-1', name: 'Renamed', mode: 'claude', ts: 2000, event: 'deleted' },
|
||||
{ sessionId: 'del-1', name: 'Original', mode: 'shell', ts: 1000, event: 'created' },
|
||||
],
|
||||
history: [
|
||||
{
|
||||
sessionId: 'del-1',
|
||||
workingDir: '/w',
|
||||
sizeBytes: 5000,
|
||||
lastModified: '2026-01-01T00:00:00.000Z',
|
||||
firstPrompt: 'hello',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(merged).toHaveLength(1);
|
||||
expect(merged[0].name).toBe('Renamed');
|
||||
expect(merged[0].mode).toBe('claude');
|
||||
});
|
||||
|
||||
it('lets live status win over persisted (precedence)', () => {
|
||||
const merged = mergeUnifiedSessions({
|
||||
persisted: [{ id: 's1', status: 'idle', name: 'Persisted Name' }],
|
||||
|
||||
Reference in New Issue
Block a user