From 9ad258272d3c7713670180ef48d66389f8cea761 Mon Sep 17 00:00:00 2001 From: 777genius Date: Sun, 31 May 2026 19:05:33 +0300 Subject: [PATCH] fix(team): clean corrupt task projection caches --- src/main/workers/team-fs-worker.ts | 10 +++- .../team/TeamFsWorker.integration.test.ts | 52 +++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/main/workers/team-fs-worker.ts b/src/main/workers/team-fs-worker.ts index bd7f9fa6..f9d23038 100644 --- a/src/main/workers/team-fs-worker.ts +++ b/src/main/workers/team-fs-worker.ts @@ -838,18 +838,23 @@ async function readPersistentTaskProjectionCache( const parsed = JSON.parse(raw) as unknown; if (!isRecord(parsed)) { taskDiag.persistentCacheReadFailures++; + await fs.promises.rm(cachePath, { force: true }).catch(() => undefined); return null; } if ( parsed.version !== PERSISTENT_TASK_PROJECTION_CACHE_VERSION || parsed.tasksBase !== payload.tasksBase || parsed.teamName !== teamName || - parsed.optionKey !== optionKey || - !isRecord(parsed.entries) + parsed.optionKey !== optionKey ) { taskDiag.persistentCacheMisses++; return null; } + if (!isRecord(parsed.entries)) { + taskDiag.persistentCacheReadFailures++; + await fs.promises.rm(cachePath, { force: true }).catch(() => undefined); + return null; + } const entries = new Map(); for (const [file, entry] of Object.entries(parsed.entries)) { @@ -866,6 +871,7 @@ async function readPersistentTaskProjectionCache( return null; } taskDiag.persistentCacheReadFailures++; + await fs.promises.rm(cachePath, { force: true }).catch(() => undefined); return null; } } diff --git a/test/main/services/team/TeamFsWorker.integration.test.ts b/test/main/services/team/TeamFsWorker.integration.test.ts index 0c20c3ae..9fab0199 100644 --- a/test/main/services/team/TeamFsWorker.integration.test.ts +++ b/test/main/services/team/TeamFsWorker.integration.test.ts @@ -757,6 +757,58 @@ describe('team-fs-worker integration', () => { } }); + it('removes corrupt persisted task projection caches even when a team has no tasks', 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-corrupt-persistent-cache-team'; + const tasksDir = path.join(tasksBase, teamName); + await fs.mkdir(tasksDir, { recursive: true }); + + const seedWorker = createWorker(workerPath); + try { + const seed = await callGetAllTasks(seedWorker, tasksBase, projectionCacheBase); + expect(seed.tasks).toHaveLength(0); + } finally { + await seedWorker.terminate(); + } + + await fs.writeFile(path.join(tasksDir, '1.json'), JSON.stringify({ id: '1' }), 'utf8'); + const writerWorker = createWorker(workerPath); + try { + const written = await callGetAllTasks(writerWorker, tasksBase, projectionCacheBase); + expect(written.tasks[0]).toMatchObject({ teamName }); + expect(written.diag?.persistentCacheWrites).toBe(1); + } finally { + await writerWorker.terminate(); + } + await fs.rm(path.join(tasksDir, '1.json')); + + const cacheFiles = await fs.readdir(path.join(projectionCacheBase, 'v1')); + const cachePath = path.join(projectionCacheBase, 'v1', cacheFiles[0]); + await fs.writeFile(cachePath, '{bad json', 'utf8'); + + const firstWorker = createWorker(workerPath); + try { + const first = await callGetAllTasks(firstWorker, tasksBase, projectionCacheBase); + expect(first.tasks).toHaveLength(0); + expect(first.diag?.persistentCacheReadFailures).toBe(1); + await expect(fs.stat(cachePath)).rejects.toMatchObject({ code: 'ENOENT' }); + } finally { + await firstWorker.terminate(); + } + + const secondWorker = createWorker(workerPath); + try { + const second = await callGetAllTasks(secondWorker, tasksBase, projectionCacheBase); + expect(second.tasks).toHaveLength(0); + expect(second.diag?.persistentCacheReadFailures).toBe(0); + } finally { + await secondWorker.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-'));