diff --git a/Controllers/ForAgentsController.php b/Controllers/ForAgentsController.php index 467c524..5dda808 100644 --- a/Controllers/ForAgentsController.php +++ b/Controllers/ForAgentsController.php @@ -18,13 +18,22 @@ class ForAgentsController extends Controller { public function __invoke(): JsonResponse { - // Cache for 1 hour since this is static data - $data = Cache::remember('agentic.for-agents.json', 3600, function () { + $ttl = (int) config('mcp.cache.for_agents_ttl', 3600); + + $data = Cache::remember($this->cacheKey(), $ttl, function () { return $this->getAgentData(); }); return response()->json($data) - ->header('Cache-Control', 'public, max-age=3600'); + ->header('Cache-Control', "public, max-age={$ttl}"); + } + + /** + * Namespaced cache key, configurable to prevent cross-module collisions. + */ + public function cacheKey(): string + { + return (string) config('mcp.cache.for_agents_key', 'agentic.for-agents.json'); } private function getAgentData(): array diff --git a/TODO.md b/TODO.md index c69793b..81421a8 100644 --- a/TODO.md +++ b/TODO.md @@ -143,10 +143,12 @@ Production-quality task list for the AI agent orchestration package. - Updated blade template to use `permissions`, `getMaskedKey()`, `togglePermission()` - Added integration tests in `tests/Feature/ApiKeyManagerTest.php` -- [ ] **CQ-003: ForAgentsController cache key not namespaced** +- [x] **CQ-003: ForAgentsController cache key not namespaced** (FIXED 2026-02-23) - Location: `Controllers/ForAgentsController.php` - Issue: `Cache::remember('agentic.for-agents.json', ...)` could collide - - Fix: Add workspace prefix or use config-based key + - Fix: Cache key and TTL now driven by `mcp.cache.for_agents_key` / `mcp.cache.for_agents_ttl` config + - Added `cacheKey()` public method and config entries in `config.php` + - Tests added in `tests/Feature/ForAgentsControllerTest.php` ### Performance diff --git a/config.php b/config.php index 491a94a..3146366 100644 --- a/config.php +++ b/config.php @@ -56,4 +56,19 @@ return [ 'drafts_path' => 'app/Mod/Agentic/Resources/drafts', ], + /* + |-------------------------------------------------------------------------- + | Cache Keys + |-------------------------------------------------------------------------- + | + | Namespaced cache keys used by agentic endpoints. Override these in your + | application config to prevent collisions with other modules. + | + */ + + 'cache' => [ + 'for_agents_key' => 'agentic.for-agents.json', + 'for_agents_ttl' => 3600, + ], + ]; diff --git a/tests/Feature/ForAgentsControllerTest.php b/tests/Feature/ForAgentsControllerTest.php new file mode 100644 index 0000000..99589e3 --- /dev/null +++ b/tests/Feature/ForAgentsControllerTest.php @@ -0,0 +1,148 @@ +cacheKey())->toBe('agentic.for-agents.json'); + }); + + it('uses a custom cache key when configured', function () { + config(['mcp.cache.for_agents_key' => 'custom-module.for-agents.json']); + + $controller = new ForAgentsController(); + + expect($controller->cacheKey())->toBe('custom-module.for-agents.json'); + }); + + it('returns to default key after config is cleared', function () { + config(['mcp.cache.for_agents_key' => null]); + + $controller = new ForAgentsController(); + + expect($controller->cacheKey())->toBe('agentic.for-agents.json'); + }); +}); + +// ========================================================================= +// Cache Behaviour Tests +// ========================================================================= + +describe('ForAgentsController cache behaviour', function () { + it('stores data under the namespaced cache key', function () { + Cache::fake(); + + $controller = new ForAgentsController(); + $controller(); + + $key = $controller->cacheKey(); + expect(Cache::has($key))->toBeTrue(); + }); + + it('returns cached data on subsequent calls', function () { + Cache::fake(); + + $controller = new ForAgentsController(); + $first = $controller(); + $second = $controller(); + + expect($first->getContent())->toBe($second->getContent()); + }); + + it('respects the configured TTL', function () { + config(['mcp.cache.for_agents_ttl' => 7200]); + Cache::fake(); + + $controller = new ForAgentsController(); + $response = $controller(); + + expect($response->headers->get('Cache-Control'))->toContain('max-age=7200'); + }); + + it('uses default TTL of 3600 when not configured', function () { + config(['mcp.cache.for_agents_ttl' => null]); + Cache::fake(); + + $controller = new ForAgentsController(); + $response = $controller(); + + expect($response->headers->get('Cache-Control'))->toContain('max-age=3600'); + }); + + it('can be invalidated using the namespaced key', function () { + Cache::fake(); + + $controller = new ForAgentsController(); + $controller(); + + $key = $controller->cacheKey(); + expect(Cache::has($key))->toBeTrue(); + + Cache::forget($key); + expect(Cache::has($key))->toBeFalse(); + }); + + it('stores data under the custom key when configured', function () { + config(['mcp.cache.for_agents_key' => 'tenant-a.for-agents.json']); + Cache::fake(); + + $controller = new ForAgentsController(); + $controller(); + + expect(Cache::has('tenant-a.for-agents.json'))->toBeTrue(); + expect(Cache::has('agentic.for-agents.json'))->toBeFalse(); + }); +}); + +// ========================================================================= +// Response Structure Tests +// ========================================================================= + +describe('ForAgentsController response', function () { + it('returns a JSON response', function () { + Cache::fake(); + + $controller = new ForAgentsController(); + $response = $controller(); + + expect($response->headers->get('Content-Type'))->toContain('application/json'); + }); + + it('response contains platform information', function () { + Cache::fake(); + + $controller = new ForAgentsController(); + $response = $controller(); + $data = json_decode($response->getContent(), true); + + expect($data)->toHaveKey('platform') + ->and($data['platform'])->toHaveKey('name'); + }); + + it('response contains capabilities', function () { + Cache::fake(); + + $controller = new ForAgentsController(); + $response = $controller(); + $data = json_decode($response->getContent(), true); + + expect($data)->toHaveKey('capabilities') + ->and($data['capabilities'])->toHaveKey('mcp_servers'); + }); +});