From 9366c770463a5c08ba02cdf14655ff24698a6848 Mon Sep 17 00:00:00 2001 From: 777genius Date: Sun, 31 May 2026 19:10:18 +0300 Subject: [PATCH] fix(team): prune stale empty task projection caches --- src/main/workers/team-fs-worker.ts | 9 ++- .../team/TeamFsWorker.integration.test.ts | 55 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/main/workers/team-fs-worker.ts b/src/main/workers/team-fs-worker.ts index f9d23038..785d3ae5 100644 --- a/src/main/workers/team-fs-worker.ts +++ b/src/main/workers/team-fs-worker.ts @@ -1935,7 +1935,14 @@ async function readTasksDirForTeam( } } } - if (shouldWritePersistentTaskProjectionCache(persistentCache, nextPersistentEntries, taskDiag)) { + if (persistentCache && nextPersistentEntries.size === 0) { + const cachePath = getPersistentTaskProjectionCachePath(payload, teamName); + if (cachePath) { + await fs.promises.rm(cachePath, { force: true }).catch(() => undefined); + } + } else if ( + shouldWritePersistentTaskProjectionCache(persistentCache, nextPersistentEntries, taskDiag) + ) { await writePersistentTaskProjectionCache( payload, teamName, diff --git a/test/main/services/team/TeamFsWorker.integration.test.ts b/test/main/services/team/TeamFsWorker.integration.test.ts index 9fab0199..8035b534 100644 --- a/test/main/services/team/TeamFsWorker.integration.test.ts +++ b/test/main/services/team/TeamFsWorker.integration.test.ts @@ -809,6 +809,61 @@ describe('team-fs-worker integration', () => { } }); + it('removes stale persisted task projection caches when a team has no cacheable task files', async () => { + const workerPath = await getWorkerPath(); + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'team-fs-worker-')); + const tasksBase = path.join(tempDir, 'tasks'); + const projectionCacheBase = path.join(tempDir, 'projection-cache'); + const teamName = 'empty-stale-persistent-cache-team'; + const tasksDir = path.join(tasksBase, teamName); + const taskPath = path.join(tasksDir, '1.json'); + await fs.mkdir(tasksDir, { recursive: true }); + await fs.writeFile( + taskPath, + JSON.stringify({ + id: '1', + subject: 'Temporary subject', + status: 'pending', + createdAt: '2026-05-02T12:00:00.000Z', + }), + 'utf8' + ); + + const firstWorker = createWorker(workerPath); + let cachePath = ''; + try { + const first = await callGetAllTasks(firstWorker, tasksBase, projectionCacheBase); + expect(first.tasks[0]).toMatchObject({ teamName, subject: 'Temporary subject' }); + expect(first.diag?.persistentCacheWrites).toBe(1); + const cacheFiles = await fs.readdir(path.join(projectionCacheBase, 'v1')); + cachePath = path.join(projectionCacheBase, 'v1', cacheFiles[0]); + } finally { + await firstWorker.terminate(); + } + + await fs.rm(taskPath); + + const secondWorker = createWorker(workerPath); + try { + const second = await callGetAllTasks(secondWorker, tasksBase, projectionCacheBase); + expect(second.tasks).toHaveLength(0); + expect(second.diag?.persistentCacheLoads).toBe(1); + await expect(fs.stat(cachePath)).rejects.toMatchObject({ code: 'ENOENT' }); + } finally { + await secondWorker.terminate(); + } + + const thirdWorker = createWorker(workerPath); + try { + const third = await callGetAllTasks(thirdWorker, tasksBase, projectionCacheBase); + expect(third.tasks).toHaveLength(0); + expect(third.diag?.persistentCacheLoads).toBe(0); + expect(third.diag?.persistentCacheReadFailures).toBe(0); + } finally { + await thirdWorker.terminate(); + } + }); + it('rejects persisted task projections that contain deleted tasks as task records', async () => { const workerPath = await getWorkerPath(); tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'team-fs-worker-'));