@@ -3,6 +3,7 @@ import os from 'node:os';
33import path from 'node:path' ;
44import { afterEach , beforeEach , describe , expect , it } from 'vitest' ;
55import {
6+ DEFAULT_TTL_DAYS ,
67 listRepos ,
78 loadRegistry ,
89 pruneRegistry ,
@@ -142,12 +143,24 @@ describe('registerRepo', () => {
142143 expect ( Object . keys ( reg . repos ) ) . toHaveLength ( 1 ) ;
143144 } ) ;
144145
145- it ( 'sets addedAt as ISO string ' , ( ) => {
146+ it ( 'sets addedAt and lastAccessedAt as ISO strings ' , ( ) => {
146147 const dir = path . join ( tmpDir , 'proj' ) ;
147148 fs . mkdirSync ( dir , { recursive : true } ) ;
148149
149150 const { entry } = registerRepo ( dir , 'proj' , registryPath ) ;
150151 expect ( entry . addedAt ) . toMatch ( / ^ \d { 4 } - \d { 2 } - \d { 2 } T / ) ;
152+ expect ( entry . lastAccessedAt ) . toMatch ( / ^ \d { 4 } - \d { 2 } - \d { 2 } T / ) ;
153+ } ) ;
154+
155+ it ( 'preserves original addedAt on re-registration' , ( ) => {
156+ const dir = path . join ( tmpDir , 'proj' ) ;
157+ fs . mkdirSync ( dir , { recursive : true } ) ;
158+
159+ const { entry : first } = registerRepo ( dir , 'proj' , registryPath ) ;
160+ const originalAddedAt = first . addedAt ;
161+ const { entry : second } = registerRepo ( dir , 'proj' , registryPath ) ;
162+
163+ expect ( second . addedAt ) . toBe ( originalAddedAt ) ;
151164 } ) ;
152165
153166 it ( 'auto-suffixes when basename collides with different path' , ( ) => {
@@ -239,7 +252,7 @@ describe('listRepos', () => {
239252 expect ( repos ) . toEqual ( [ ] ) ;
240253 } ) ;
241254
242- it ( 'returns repos sorted by name' , ( ) => {
255+ it ( 'returns repos sorted by name with lastAccessedAt ' , ( ) => {
243256 const dirA = path . join ( tmpDir , 'aaa' ) ;
244257 const dirZ = path . join ( tmpDir , 'zzz' ) ;
245258 const dirM = path . join ( tmpDir , 'mmm' ) ;
@@ -253,6 +266,9 @@ describe('listRepos', () => {
253266
254267 const repos = listRepos ( registryPath ) ;
255268 expect ( repos . map ( ( r ) => r . name ) ) . toEqual ( [ 'aaa' , 'mmm' , 'zzz' ] ) ;
269+ for ( const r of repos ) {
270+ expect ( r . lastAccessedAt ) . toMatch ( / ^ \d { 4 } - \d { 2 } - \d { 2 } T / ) ;
271+ }
256272 } ) ;
257273} ) ;
258274
@@ -289,7 +305,7 @@ describe('resolveRepoDbPath', () => {
289305// ─── pruneRegistry ─────────────────────────────────────────────────
290306
291307describe ( 'pruneRegistry' , ( ) => {
292- it ( 'removes entries whose directories no longer exist' , ( ) => {
308+ it ( 'removes entries whose directories no longer exist (reason: missing) ' , ( ) => {
293309 const dir1 = path . join ( tmpDir , 'exists' ) ;
294310 const dir2 = path . join ( tmpDir , 'gone' ) ;
295311 fs . mkdirSync ( dir1 , { recursive : true } ) ;
@@ -305,12 +321,96 @@ describe('pruneRegistry', () => {
305321 expect ( pruned ) . toHaveLength ( 1 ) ;
306322 expect ( pruned [ 0 ] . name ) . toBe ( 'gone' ) ;
307323 expect ( pruned [ 0 ] . path ) . toBe ( dir2 ) ;
324+ expect ( pruned [ 0 ] . reason ) . toBe ( 'missing' ) ;
308325
309326 const reg = loadRegistry ( registryPath ) ;
310327 expect ( reg . repos . exists ) . toBeDefined ( ) ;
311328 expect ( reg . repos . gone ) . toBeUndefined ( ) ;
312329 } ) ;
313330
331+ it ( 'removes entries idle beyond TTL (reason: expired)' , ( ) => {
332+ const dir = path . join ( tmpDir , 'old-project' ) ;
333+ fs . mkdirSync ( dir , { recursive : true } ) ;
334+
335+ // Manually write a registry entry with an old lastAccessedAt
336+ const oldDate = new Date ( Date . now ( ) - 60 * 24 * 60 * 60 * 1000 ) . toISOString ( ) ; // 60 days ago
337+ const registry = {
338+ repos : {
339+ 'old-project' : {
340+ path : dir ,
341+ dbPath : path . join ( dir , '.codegraph' , 'graph.db' ) ,
342+ addedAt : oldDate ,
343+ lastAccessedAt : oldDate ,
344+ } ,
345+ } ,
346+ } ;
347+ saveRegistry ( registry , registryPath ) ;
348+
349+ const pruned = pruneRegistry ( registryPath , 30 ) ;
350+ expect ( pruned ) . toHaveLength ( 1 ) ;
351+ expect ( pruned [ 0 ] . name ) . toBe ( 'old-project' ) ;
352+ expect ( pruned [ 0 ] . reason ) . toBe ( 'expired' ) ;
353+ } ) ;
354+
355+ it ( 'keeps entries within TTL window' , ( ) => {
356+ const dir = path . join ( tmpDir , 'fresh' ) ;
357+ fs . mkdirSync ( dir , { recursive : true } ) ;
358+ registerRepo ( dir , 'fresh' , registryPath ) ;
359+
360+ const pruned = pruneRegistry ( registryPath , 30 ) ;
361+ expect ( pruned ) . toEqual ( [ ] ) ;
362+
363+ const reg = loadRegistry ( registryPath ) ;
364+ expect ( reg . repos . fresh ) . toBeDefined ( ) ;
365+ } ) ;
366+
367+ it ( 'falls back to addedAt when lastAccessedAt is missing' , ( ) => {
368+ const dir = path . join ( tmpDir , 'legacy' ) ;
369+ fs . mkdirSync ( dir , { recursive : true } ) ;
370+
371+ const oldDate = new Date ( Date . now ( ) - 60 * 24 * 60 * 60 * 1000 ) . toISOString ( ) ;
372+ const registry = {
373+ repos : {
374+ legacy : {
375+ path : dir ,
376+ dbPath : path . join ( dir , '.codegraph' , 'graph.db' ) ,
377+ addedAt : oldDate ,
378+ } ,
379+ } ,
380+ } ;
381+ saveRegistry ( registry , registryPath ) ;
382+
383+ const pruned = pruneRegistry ( registryPath , 30 ) ;
384+ expect ( pruned ) . toHaveLength ( 1 ) ;
385+ expect ( pruned [ 0 ] . reason ) . toBe ( 'expired' ) ;
386+ } ) ;
387+
388+ it ( 'respects custom TTL' , ( ) => {
389+ const dir = path . join ( tmpDir , 'project' ) ;
390+ fs . mkdirSync ( dir , { recursive : true } ) ;
391+
392+ // 10 days ago
393+ const recentDate = new Date ( Date . now ( ) - 10 * 24 * 60 * 60 * 1000 ) . toISOString ( ) ;
394+ const registry = {
395+ repos : {
396+ project : {
397+ path : dir ,
398+ dbPath : path . join ( dir , '.codegraph' , 'graph.db' ) ,
399+ addedAt : recentDate ,
400+ lastAccessedAt : recentDate ,
401+ } ,
402+ } ,
403+ } ;
404+ saveRegistry ( registry , registryPath ) ;
405+
406+ // 30-day TTL: should keep
407+ expect ( pruneRegistry ( registryPath , 30 ) ) . toEqual ( [ ] ) ;
408+ // 7-day TTL: should prune
409+ const pruned = pruneRegistry ( registryPath , 7 ) ;
410+ expect ( pruned ) . toHaveLength ( 1 ) ;
411+ expect ( pruned [ 0 ] . reason ) . toBe ( 'expired' ) ;
412+ } ) ;
413+
314414 it ( 'returns empty array when nothing to prune' , ( ) => {
315415 const dir = path . join ( tmpDir , 'healthy' ) ;
316416 fs . mkdirSync ( dir , { recursive : true } ) ;
@@ -336,3 +436,36 @@ describe('pruneRegistry', () => {
336436 expect ( pruned ) . toEqual ( [ ] ) ;
337437 } ) ;
338438} ) ;
439+
440+ // ─── DEFAULT_TTL_DAYS ──────────────────────────────────────────────
441+
442+ describe ( 'DEFAULT_TTL_DAYS' , ( ) => {
443+ it ( 'is 30 days' , ( ) => {
444+ expect ( DEFAULT_TTL_DAYS ) . toBe ( 30 ) ;
445+ } ) ;
446+ } ) ;
447+
448+ // ─── resolveRepoDbPath lastAccessedAt ──────────────────────────────
449+
450+ describe ( 'resolveRepoDbPath updates lastAccessedAt' , ( ) => {
451+ it ( 'touches lastAccessedAt on successful resolve' , ( ) => {
452+ const dir = path . join ( tmpDir , 'proj' ) ;
453+ const dbDir = path . join ( dir , '.codegraph' ) ;
454+ const dbFile = path . join ( dbDir , 'graph.db' ) ;
455+ fs . mkdirSync ( dbDir , { recursive : true } ) ;
456+ fs . writeFileSync ( dbFile , '' ) ;
457+
458+ registerRepo ( dir , 'proj' , registryPath ) ;
459+
460+ // Manually backdate lastAccessedAt
461+ const reg = loadRegistry ( registryPath ) ;
462+ reg . repos . proj . lastAccessedAt = '2025-01-01T00:00:00.000Z' ;
463+ saveRegistry ( reg , registryPath ) ;
464+
465+ resolveRepoDbPath ( 'proj' , registryPath ) ;
466+
467+ const updated = loadRegistry ( registryPath ) ;
468+ expect ( updated . repos . proj . lastAccessedAt ) . not . toBe ( '2025-01-01T00:00:00.000Z' ) ;
469+ expect ( new Date ( updated . repos . proj . lastAccessedAt ) . getFullYear ( ) ) . toBeGreaterThanOrEqual ( 2026 ) ;
470+ } ) ;
471+ } ) ;
0 commit comments