diff --git a/.veritas-kanban/telemetry/events-2026-01-28.ndjson b/.veritas-kanban/telemetry/events-2026-01-28.ndjson index aaf27990..76843c05 100644 --- a/.veritas-kanban/telemetry/events-2026-01-28.ndjson +++ b/.veritas-kanban/telemetry/events-2026-01-28.ndjson @@ -2110,3 +2110,4 @@ {"type":"task.created","taskId":"task_20260128_Usuv2Y","project":"project-a","status":"todo","id":"evt_aG2Tll254MYY","timestamp":"2026-01-28T18:14:17.785Z"} {"type":"task.created","taskId":"task_20260128_13CrZo","project":"project-b","status":"todo","id":"evt_YNGhKOJL_Tmu","timestamp":"2026-01-28T18:14:17.786Z"} {"type":"task.created","taskId":"task_20260128_RJXi4k","status":"todo","id":"evt_21ZTMT4yYCOp","timestamp":"2026-01-28T18:14:17.799Z"} +{"type":"task.status_changed","taskId":"task_20260128_9t1KEa","project":"veritas-kanban","status":"in-progress","previousStatus":"todo","id":"evt_E2py7Rsv0FZs","timestamp":"2026-01-28T18:14:39.641Z"} diff --git a/server/.veritas-kanban/activity.json b/server/.veritas-kanban/activity.json index 1bc5358b..3a6da53c 100644 --- a/server/.veritas-kanban/activity.json +++ b/server/.veritas-kanban/activity.json @@ -1,4 +1,15 @@ [ + { + "id": "activity_1769624079642_g7c232tud", + "type": "status_changed", + "taskId": "task_20260128_9t1KEa", + "taskTitle": "SECURITY: Add server-side MIME type validation for attachments", + "details": { + "from": "todo", + "status": "in-progress" + }, + "timestamp": "2026-01-28T18:14:39.642Z" + }, { "id": "activity_1769623785282_5ct7g0hqu", "type": "comment_added", diff --git a/server/src/__tests__/settings-service.test.ts b/server/src/__tests__/settings-service.test.ts index dd74b203..cae66d51 100644 --- a/server/src/__tests__/settings-service.test.ts +++ b/server/src/__tests__/settings-service.test.ts @@ -201,4 +201,59 @@ describe('ConfigService', () => { expect(results[1]).toEqual(results[2]); }); }); + + describe('Cache Behavior', () => { + it('should return cached config within TTL', async () => { + // First read populates cache + const config1 = await service.getConfig(); + + // Modify file on disk behind the service's back + const raw = JSON.parse(await fs.readFile(configFile, 'utf-8')); + raw.defaultAgent = 'amp'; + // Mark as our own write so the file watcher ignores it, + // simulating a read within the TTL window + (service as any).lastWriteTime = Date.now(); + await fs.writeFile(configFile, JSON.stringify(raw, null, 2)); + + // Should still return the cached version (TTL hasn't expired) + const config2 = await service.getConfig(); + expect(config2.defaultAgent).toBe(config1.defaultAgent); + }); + + it('should re-read from disk after invalidateCache()', async () => { + const config1 = await service.getConfig(); + expect(config1.defaultAgent).toBe('claude-code'); + + // Modify file on disk + const raw = JSON.parse(await fs.readFile(configFile, 'utf-8')); + raw.defaultAgent = 'amp'; + // Suppress watcher for this manual write + (service as any).lastWriteTime = Date.now(); + await fs.writeFile(configFile, JSON.stringify(raw, null, 2)); + + // Explicitly invalidate + service.invalidateCache(); + + const config2 = await service.getConfig(); + expect(config2.defaultAgent).toBe('amp'); + }); + + it('should update cache on saveConfig without re-reading disk', async () => { + const config = await service.getConfig(); + config.defaultAgent = 'gemini'; + await service.saveConfig(config); + + // Should return updated value from cache, not disk + const config2 = await service.getConfig(); + expect(config2.defaultAgent).toBe('gemini'); + }); + + it('should clean up watcher on dispose()', async () => { + await service.getConfig(); // triggers watcher setup + service.dispose(); + // After dispose, cache should be cleared + expect((service as any).config).toBeNull(); + expect((service as any).watcher).toBeNull(); + }); + }); });