Skip to content

Commit 36ef1aa

Browse files
dimakisclaude
andcommitted
fix(server): address PR #326 review — hoist constant + add test coverage
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 01cfc09 commit 36ef1aa

2 files changed

Lines changed: 106 additions & 2 deletions

File tree

server/__tests__/session-cache.test.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,51 @@ describe('getSessionsCached', () => {
217217
cwd: '/projects/bar',
218218
});
219219
});
220+
221+
it('keeps a recent zero-turn inactive session within the grace period', async () => {
222+
const thirtyMinutesAgo = Date.now() - 30 * 60 * 1000;
223+
mockListSessionsMeta.mockReturnValue([
224+
{
225+
sessionId: 'sess-recent-zero',
226+
summary: 'Just created',
227+
numTurns: 0,
228+
promptCount: 0,
229+
isActive: false,
230+
createdAt: thirtyMinutesAgo,
231+
updatedAt: thirtyMinutesAgo,
232+
branch: null,
233+
cwd: null,
234+
},
235+
]);
236+
237+
const { getSessionsCached } = await import('../chat.js');
238+
const { sessions } = getSessionsCached();
239+
240+
expect(sessions).toHaveLength(1);
241+
expect(sessions[0].id).toBe('sess-recent-zero');
242+
});
243+
244+
it('hides an old zero-turn inactive session past the grace period', async () => {
245+
const twoHoursAgo = Date.now() - 2 * 60 * 60 * 1000;
246+
mockListSessionsMeta.mockReturnValue([
247+
{
248+
sessionId: 'sess-old-zero',
249+
summary: 'Stale empty session',
250+
numTurns: 0,
251+
promptCount: 0,
252+
isActive: false,
253+
createdAt: twoHoursAgo,
254+
updatedAt: twoHoursAgo,
255+
branch: null,
256+
cwd: null,
257+
},
258+
]);
259+
260+
const { getSessionsCached } = await import('../chat.js');
261+
const { sessions } = getSessionsCached();
262+
263+
expect(sessions).toHaveLength(0);
264+
});
220265
});
221266

222267
describe('syncSessionTimestamps', () => {
@@ -347,6 +392,63 @@ describe('syncSessionTimestamps', () => {
347392
});
348393
});
349394

395+
describe('resume timestamp preservation', () => {
396+
it('preserves updatedAt when resuming an existing session', async () => {
397+
const originalUpdatedAt = Date.now() - 3600_000; // 1 hour ago
398+
const existingMeta = {
399+
sessionId: 'sess-resume',
400+
summary: 'Old session',
401+
updatedAt: originalUpdatedAt,
402+
createdAt: originalUpdatedAt - 7200_000,
403+
numTurns: 5,
404+
promptCount: 3,
405+
isActive: false,
406+
branch: 'main',
407+
cwd: '/projects/foo',
408+
manuallyRenamed: false,
409+
};
410+
mockGetSession.mockReturnValue(existingMeta);
411+
412+
// Simulate the resume upsert pattern from startSession (chat.ts L688-701):
413+
// const existingMeta = eventStore.getSession(id);
414+
// eventStore.upsertSession({ ..., ...(existingMeta ? { updatedAt: existingMeta.updatedAt } : {}) });
415+
const sessionId = 'sess-resume';
416+
const retrieved = mockEventStore.getSession(sessionId);
417+
mockEventStore.upsertSession({
418+
sessionId,
419+
cwd: '/projects/foo',
420+
mode: 'code',
421+
branch: 'main',
422+
...(retrieved ? { updatedAt: retrieved.updatedAt } : {}),
423+
});
424+
425+
expect(mockUpsertSession).toHaveBeenCalledWith(
426+
expect.objectContaining({
427+
sessionId: 'sess-resume',
428+
updatedAt: originalUpdatedAt,
429+
}),
430+
);
431+
});
432+
433+
it('omits updatedAt when resuming a session with no prior metadata', async () => {
434+
mockGetSession.mockReturnValue(null);
435+
436+
const sessionId = 'sess-new-resume';
437+
const retrieved = mockEventStore.getSession(sessionId);
438+
mockEventStore.upsertSession({
439+
sessionId,
440+
cwd: '/projects/foo',
441+
mode: 'code',
442+
branch: 'main',
443+
...(retrieved ? { updatedAt: retrieved.updatedAt } : {}),
444+
});
445+
446+
const call = mockUpsertSession.mock.calls[0][0];
447+
expect(call.sessionId).toBe('sess-new-resume');
448+
expect(call).not.toHaveProperty('updatedAt');
449+
});
450+
});
451+
350452
describe('hideSession persistence', () => {
351453
it('calls eventStore.hideSession to persist deletion', async () => {
352454
const { hideSession } = await import('../chat.js');

server/chat.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ import { INTERNAL_TOKEN } from './internal-token.js';
3535
import { buildTaskSystemPrompt } from './task-context.js';
3636
import type { TaskStore } from './task-store.js';
3737

38+
/** Grace period for zero-turn inactive sessions — recently created ones stay visible. */
39+
const ZERO_TURN_GRACE_MS = 60 * 60 * 1000; // 1 hour
40+
3841
let _taskStore: TaskStore | null = null;
3942
export function setTaskStore(store: TaskStore): void {
4043
_taskStore = store;
@@ -1354,8 +1357,7 @@ export function getSessionsCached(offset = 0, limit = SESSION_PAGE_SIZE) {
13541357
// always show regardless of turn count. Recently created sessions
13551358
// (< 1 hour) are kept even with no turns — they may still be starting.
13561359
if (m.numTurns === 0 && m.promptCount === 0 && !m.isActive) {
1357-
const ONE_HOUR = 60 * 60 * 1000;
1358-
return Date.now() - m.createdAt < ONE_HOUR;
1360+
return Date.now() - m.createdAt < ZERO_TURN_GRACE_MS;
13591361
}
13601362
return true;
13611363
});

0 commit comments

Comments
 (0)