diff --git a/.aiox-core/core/config/config-cache.js b/.aiox-core/core/config/config-cache.js index 707de0a576..3c40c7bfd9 100644 --- a/.aiox-core/core/config/config-cache.js +++ b/.aiox-core/core/config/config-cache.js @@ -72,7 +72,23 @@ class ConfigCache { * @returns {boolean} True if key exists and is valid */ has(key) { - return this.get(key) !== null; + // Check directly instead of delegating to get(), which would + // increment hits/misses and inflate cache statistics (fixes #497) + if (!this.cache.has(key)) return false; + + // If timestamp is missing, maps are out-of-sync — treat as invalid + if (!this.timestamps.has(key)) { + this.cache.delete(key); + return false; + } + + const timestamp = this.timestamps.get(key); + if (Date.now() - timestamp > this.ttl) { + this.cache.delete(key); + this.timestamps.delete(key); + return false; + } + return true; } /** diff --git a/.aiox-core/install-manifest.yaml b/.aiox-core/install-manifest.yaml index 2516e4c299..927743ae9d 100644 --- a/.aiox-core/install-manifest.yaml +++ b/.aiox-core/install-manifest.yaml @@ -8,9 +8,13 @@ # - File types for categorization # version: 5.0.3 +<<<<<<< HEAD generated_at: "2026-03-10T17:08:07.160Z" +======= +generated_at: "2026-03-08T05:50:11.695Z" +>>>>>>> 1f7063eb (fix(config-cache): corrige has() para mapas dessincronizados e restaura timers) generator: scripts/generate-install-manifest.js -file_count: 1089 +file_count: 1090 files: - path: cli/commands/config/index.js hash: sha256:25c4b9bf4e0241abf7754b55153f49f1a214f1fb5fe904a576675634cb7b3da9 @@ -237,9 +241,9 @@ files: type: core size: 10891 - path: core/config/config-cache.js - hash: sha256:527a788cbe650aa6b13d1101ebc16419489bfef20b2ee93042f6eb6a51e898e9 + hash: sha256:13bf18a59ac3f1db96e6bf527a2d0155c5c5a09770b08b8f2d6546fc9daf95bc type: core - size: 4704 + size: 5220 - path: core/config/config-loader.js hash: sha256:e19fee885b060c85ee75df71a016259b8e4c21e6c7045a58514afded3a2386a8 type: core @@ -932,6 +936,10 @@ files: hash: sha256:92e9d5bea78c3db4940c39f79e537821b36451cd524d69e6b738272aa63c08b6 type: core size: 12849 + - path: core/orchestration/swarm-intelligence.js + hash: sha256:e5a3e085d439f0ef1d8fcdba0f1c4b212075f9dc0a45903dd409cd6163c2a2bb + type: core + size: 35845 - path: core/orchestration/task-complexity-classifier.js hash: sha256:33b3b7c349352d607c156e0018c508f0869a1c7d233d107bed194a51bc608c93 type: core @@ -2583,19 +2591,19 @@ files: - path: development/templates/service-template/__tests__/index.test.ts.hbs hash: sha256:4617c189e75ab362d4ef2cabcc3ccce3480f914fd915af550469c17d1b68a4fe type: template - size: 9810 + size: 9573 - path: development/templates/service-template/client.ts.hbs hash: sha256:f342c60695fe611192002bdb8c04b3a0dbce6345b7fa39834ea1898f71689198 type: template - size: 12213 + size: 11810 - path: development/templates/service-template/errors.ts.hbs hash: sha256:e0be40d8be19b71b26e35778eadffb20198e7ca88e9d140db9da1bfe12de01ec type: template - size: 5395 + size: 5213 - path: development/templates/service-template/index.ts.hbs hash: sha256:d44012d54b76ab98356c7163d257ca939f7fed122f10fecf896fe1e7e206d10a type: template - size: 3206 + size: 3086 - path: development/templates/service-template/jest.config.js hash: sha256:1681bfd7fbc0d330d3487d3427515847c4d57ef300833f573af59e0ad69ed159 type: template @@ -2603,11 +2611,11 @@ files: - path: development/templates/service-template/package.json.hbs hash: sha256:d89d35f56992ee95c2ceddf17fa1d455c18007a4d24af914ba83cf4abc38bca9 type: template - size: 2314 + size: 2227 - path: development/templates/service-template/README.md.hbs hash: sha256:2c3dd4c2bf6df56b9b6db439977be7e1cc35820438c0e023140eccf6ccd227a0 type: template - size: 3584 + size: 3426 - path: development/templates/service-template/tsconfig.json hash: sha256:8b465fcbdd45c4d6821ba99aea62f2bd7998b1bca8de80486a1525e77d43c9a1 type: template @@ -2615,7 +2623,7 @@ files: - path: development/templates/service-template/types.ts.hbs hash: sha256:3e52e0195003be8cd1225a3f27f4d040686c8b8c7762f71b41055f04cd1b841b type: template - size: 2661 + size: 2516 - path: development/templates/squad-template/agents/example-agent.yaml hash: sha256:824a1b349965e5d4ae85458c231b78260dc65497da75dada25b271f2cabbbe67 type: agent @@ -2623,7 +2631,7 @@ files: - path: development/templates/squad-template/LICENSE hash: sha256:ff7017aa403270cf2c440f5ccb4240d0b08e54d8bf8a0424d34166e8f3e10138 type: template - size: 1092 + size: 1071 - path: development/templates/squad-template/package.json hash: sha256:8f68627a0d74e49f94ae382d0c2b56ecb5889d00f3095966c742fb5afaf363db type: template @@ -3367,11 +3375,11 @@ files: - path: infrastructure/templates/aiox-sync.yaml.template hash: sha256:0040ad8a9e25716a28631b102c9448b72fd72e84f992c3926eb97e9e514744bb type: template - size: 8567 + size: 8385 - path: infrastructure/templates/coderabbit.yaml.template hash: sha256:91a4a76bbc40767a4072fb6a87c480902bb800cfb0a11e9fc1b3183d8f7f3a80 type: template - size: 8321 + size: 8042 - path: infrastructure/templates/core-config/core-config-brownfield.tmpl.yaml hash: sha256:9bdb0c0e09c765c991f9f142921f7f8e2c0d0ada717f41254161465dc0622d02 type: template @@ -3383,11 +3391,11 @@ files: - path: infrastructure/templates/github-workflows/ci.yml.template hash: sha256:acbfa2a8a84141fd6a6b205eac74719772f01c221c0afe22ce951356f06a605d type: template - size: 5089 + size: 4920 - path: infrastructure/templates/github-workflows/pr-automation.yml.template hash: sha256:c236077b4567965a917e48df9a91cc42153ff97b00a9021c41a7e28179be9d0f type: template - size: 10939 + size: 10609 - path: infrastructure/templates/github-workflows/README.md hash: sha256:6b7b5cb32c28b3e562c81a96e2573ea61849b138c93ccac6e93c3adac26cadb5 type: template @@ -3395,23 +3403,23 @@ files: - path: infrastructure/templates/github-workflows/release.yml.template hash: sha256:b771145e61a254a88dc6cca07869e4ece8229ce18be87132f59489cdf9a66ec6 type: template - size: 6791 + size: 6595 - path: infrastructure/templates/gitignore/gitignore-aiox-base.tmpl hash: sha256:9088975ee2bf4d88e23db6ac3ea5d27cccdc72b03db44450300e2f872b02e935 type: template - size: 851 + size: 788 - path: infrastructure/templates/gitignore/gitignore-brownfield-merge.tmpl hash: sha256:ce4291a3cf5677050c9dafa320809e6b0ca5db7e7f7da0382d2396e32016a989 type: template - size: 506 + size: 488 - path: infrastructure/templates/gitignore/gitignore-node.tmpl hash: sha256:5179f78de7483274f5d7182569229088c71934db1fd37a63a40b3c6b815c9c8e type: template - size: 1036 + size: 951 - path: infrastructure/templates/gitignore/gitignore-python.tmpl hash: sha256:d7aac0b1e6e340b774a372a9102b4379722588449ca82ac468cf77804bbc1e55 type: template - size: 1725 + size: 1580 - path: infrastructure/templates/project-docs/coding-standards-tmpl.md hash: sha256:377acf85463df8ac9923fc59d7cfeba68a82f8353b99948ea1d28688e88bc4a9 type: template @@ -3507,43 +3515,43 @@ files: - path: monitor/hooks/lib/__init__.py hash: sha256:bfab6ee249c52f412c02502479da649b69d044938acaa6ab0aa39dafe6dee9bf type: monitor - size: 30 + size: 29 - path: monitor/hooks/lib/enrich.py hash: sha256:20dfa73b4b20d7a767e52c3ec90919709c4447c6e230902ba797833fc6ddc22c type: monitor - size: 1702 + size: 1644 - path: monitor/hooks/lib/send_event.py hash: sha256:59d61311f718fb373a5cf85fd7a01c23a4fd727e8e022ad6930bba533ef4615d type: monitor - size: 1237 + size: 1190 - path: monitor/hooks/notification.py hash: sha256:8a1a6ce0ff2b542014de177006093b9caec9b594e938a343dc6bd62df2504f22 type: monitor - size: 528 + size: 499 - path: monitor/hooks/post_tool_use.py hash: sha256:47dbe37073d432c55657647fc5b907ddb56efa859d5c3205e8362aa916d55434 type: monitor - size: 1185 + size: 1140 - path: monitor/hooks/pre_compact.py hash: sha256:f287cf45e83deed6f1bc0e30bd9348dfa1bf08ad770c5e58bb34e3feb210b30b type: monitor - size: 529 + size: 500 - path: monitor/hooks/pre_tool_use.py hash: sha256:a4d1d3ffdae9349e26a383c67c9137effff7d164ac45b2c87eea9fa1ab0d6d98 type: monitor - size: 1021 + size: 981 - path: monitor/hooks/stop.py hash: sha256:edb382f0cf46281a11a8588bc20eafa7aa2b5cc3f4ad775d71b3d20a7cfab385 type: monitor - size: 519 + size: 490 - path: monitor/hooks/subagent_stop.py hash: sha256:fa5357309247c71551dba0a19f28dd09bebde749db033d6657203b50929c0a42 type: monitor - size: 541 + size: 512 - path: monitor/hooks/user_prompt_submit.py hash: sha256:af57dca79ef55cdf274432f4abb4c20a9778b95e107ca148f47ace14782c5828 type: monitor - size: 856 + size: 818 - path: package.json hash: sha256:9fdf0dcee2dcec6c0643634ee384ba181ad077dcff1267d8807434d4cb4809c7 type: other @@ -3691,7 +3699,7 @@ files: - path: product/templates/adr.hbs hash: sha256:d68653cae9e64414ad4f58ea941b6c6e337c5324c2c7247043eca1461a652d10 type: template - size: 2337 + size: 2212 - path: product/templates/agent-template.yaml hash: sha256:98676fcc493c0d5f09264dcc52fcc2cf1129f9a195824ecb4c2ec035c2515121 type: template @@ -3743,7 +3751,7 @@ files: - path: product/templates/dbdr.hbs hash: sha256:5a2781ffaa3da9fc663667b5a63a70b7edfc478ed14cad02fc6ed237ff216315 type: template - size: 4380 + size: 4139 - path: product/templates/design-story-tmpl.yaml hash: sha256:2bfefc11ae2bcfc679dbd924c58f8b764fa23538c14cb25344d6edef41968f29 type: template @@ -3807,7 +3815,7 @@ files: - path: product/templates/epic.hbs hash: sha256:dcbcc26f6dd8f3782b3ef17aee049b689f1d6d92931615c3df9513eca0de2ef7 type: template - size: 4080 + size: 3868 - path: product/templates/eslintrc-security.json hash: sha256:657d40117261d6a52083984d29f9f88e79040926a64aa4c2058a602bfe91e0d5 type: template @@ -3915,7 +3923,7 @@ files: - path: product/templates/pmdr.hbs hash: sha256:d529cebbb562faa82c70477ece70de7cda871eaa6896f2962b48b2a8b67b1cbe type: template - size: 3425 + size: 3239 - path: product/templates/prd-tmpl.yaml hash: sha256:25c239f40e05f24aee1986601a98865188dbe3ea00a705028efc3adad6d420f3 type: template @@ -3923,11 +3931,11 @@ files: - path: product/templates/prd-v2.0.hbs hash: sha256:21a20ef5333a85a11f5326d35714e7939b51bab22bd6e28d49bacab755763bea type: template - size: 4728 + size: 4512 - path: product/templates/prd.hbs hash: sha256:4a1a030a5388c6a8bf2ce6ea85e54cae6cf1fe64f1bb2af7f17d349d3c24bf1d type: template - size: 3626 + size: 3425 - path: product/templates/project-brief-tmpl.yaml hash: sha256:b8d388268c24dc5018f48a87036d591b11cb122fafe9b59c17809b06ea5d9d58 type: template @@ -3975,7 +3983,7 @@ files: - path: product/templates/story.hbs hash: sha256:3f0ac8b39907634a2b53f43079afc33663eee76f46e680d318ff253e0befc2c4 type: template - size: 5846 + size: 5583 - path: product/templates/task-execution-report.md hash: sha256:e0f08a3e199234f3d2207ba8f435786b7d8e1b36174f46cb82fc3666b9a9309e type: template @@ -3987,67 +3995,67 @@ files: - path: product/templates/task.hbs hash: sha256:621e987e142c455cd290dc85d990ab860faa0221f66cf1f57ac296b076889ea5 type: template - size: 2875 + size: 2705 - path: product/templates/tmpl-comment-on-examples.sql hash: sha256:254002c3fbc63cfcc5848b1d4b15822ce240bf5f57e6a1c8bb984e797edc2691 type: template - size: 6373 + size: 6215 - path: product/templates/tmpl-migration-script.sql hash: sha256:44ef63ea475526d21a11e3c667c9fdb78a9fddace80fdbaa2312b7f2724fbbb5 type: template - size: 3038 + size: 2947 - path: product/templates/tmpl-rls-granular-policies.sql hash: sha256:36c2fd8c6d9eebb5d164acb0fb0c87bc384d389264b4429ce21e77e06318f5f3 type: template - size: 3426 + size: 3322 - path: product/templates/tmpl-rls-kiss-policy.sql hash: sha256:5210d37fce62e5a9a00e8d5366f5f75653cd518be73fbf96333ed8a6712453c7 type: template - size: 309 + size: 299 - path: product/templates/tmpl-rls-roles.sql hash: sha256:2d032a608a8e87440c3a430c7d69ddf9393d8813d8d4129270f640dd847425c3 type: template - size: 4727 + size: 4592 - path: product/templates/tmpl-rls-simple.sql hash: sha256:f67af0fa1cdd2f2af9eab31575ac3656d82457421208fd9ccb8b57ca9785275e type: template - size: 2992 + size: 2915 - path: product/templates/tmpl-rls-tenant.sql hash: sha256:36629ed87a2c72311809cc3fb96298b6f38716bba35bc56c550ac39d3321757a type: template - size: 5130 + size: 4978 - path: product/templates/tmpl-rollback-script.sql hash: sha256:8b84046a98f1163faf7350322f43831447617c5a63a94c88c1a71b49804e022b type: template - size: 2734 + size: 2657 - path: product/templates/tmpl-seed-data.sql hash: sha256:a65e73298f46cd6a8e700f29b9d8d26e769e12a57751a943a63fd0fe15768615 type: template - size: 5716 + size: 5576 - path: product/templates/tmpl-smoke-test.sql hash: sha256:aee7e48bb6d9c093769dee215cacc9769939501914e20e5ea8435b25fad10f3c type: template - size: 739 + size: 723 - path: product/templates/tmpl-staging-copy-merge.sql hash: sha256:55988caeb47cc04261665ba7a37f4caa2aa5fac2e776fdbc5964e0587af24450 type: template - size: 4220 + size: 4081 - path: product/templates/tmpl-stored-proc.sql hash: sha256:2b205ff99dc0adfade6047a4d79f5b50109e50ceb45386e5c886437692c7a2a3 type: template - size: 3979 + size: 3839 - path: product/templates/tmpl-trigger.sql hash: sha256:93abdc92e1b475d1370094e69a9d1b18afd804da6acb768b878355c798bd8e0e type: template - size: 5424 + size: 5272 - path: product/templates/tmpl-view-materialized.sql hash: sha256:47935510f03d4ad9b2200748e65441ce6c2d6a7c74750395eca6831d77c48e91 type: template - size: 4496 + size: 4363 - path: product/templates/tmpl-view.sql hash: sha256:22557b076003a856b32397f05fa44245a126521de907058a95e14dd02da67aff type: template - size: 5093 + size: 4916 - path: product/templates/token-exports-css-tmpl.css hash: sha256:d937b8d61cdc9e5b10fdff871c6cb41c9f756004d060d671e0ae26624a047f62 type: template diff --git a/tests/core/config/config-cache.test.js b/tests/core/config/config-cache.test.js new file mode 100644 index 0000000000..7b444a7443 --- /dev/null +++ b/tests/core/config/config-cache.test.js @@ -0,0 +1,322 @@ +'use strict'; + +/** + * Unit tests for config-cache + * + * Tests ConfigCache class: TTL expiration, get/set/has, invalidation, + * clearExpired, statistics, entries, serialization, and global singleton. + * + * Fixes #497 — has() was inflating hit/miss stats by calling get() + */ + +jest.useFakeTimers(); + +const { ConfigCache, globalConfigCache } = require('../../../.aiox-core/core/config/config-cache'); + +afterAll(() => jest.useRealTimers()); + +describe('config-cache', () => { + let cache; + + beforeEach(() => { + cache = new ConfigCache(1000); // 1 second TTL for tests + }); + + // ── Constructor ───────────────────────────────────────────────── + + describe('constructor', () => { + test('uses default TTL of 5 minutes', () => { + const defaultCache = new ConfigCache(); + expect(defaultCache.ttl).toBe(5 * 60 * 1000); + }); + + test('accepts custom TTL', () => { + expect(cache.ttl).toBe(1000); + }); + + test('initializes empty stats', () => { + expect(cache.hits).toBe(0); + expect(cache.misses).toBe(0); + expect(cache.size).toBe(0); + }); + }); + + // ── set / get ─────────────────────────────────────────────────── + + describe('set and get', () => { + test('stores and retrieves a value', () => { + cache.set('key1', 'value1'); + expect(cache.get('key1')).toBe('value1'); + }); + + test('stores objects', () => { + const obj = { nested: { data: true } }; + cache.set('obj', obj); + expect(cache.get('obj')).toBe(obj); + }); + + test('returns null for missing key', () => { + expect(cache.get('missing')).toBeNull(); + }); + + test('returns null for expired key', () => { + cache.set('key1', 'value1'); + jest.advanceTimersByTime(1500); // past 1s TTL + expect(cache.get('key1')).toBeNull(); + }); + + test('removes expired entry from cache on get', () => { + cache.set('key1', 'value1'); + jest.advanceTimersByTime(1500); + cache.get('key1'); + expect(cache.size).toBe(0); + }); + + test('overwrites existing key', () => { + cache.set('key1', 'v1'); + cache.set('key1', 'v2'); + expect(cache.get('key1')).toBe('v2'); + }); + }); + + // ── has ───────────────────────────────────────────────────────── + + describe('has', () => { + test('returns true for valid entry', () => { + cache.set('key1', 'value1'); + expect(cache.has('key1')).toBe(true); + }); + + test('returns false for missing entry', () => { + expect(cache.has('missing')).toBe(false); + }); + + test('returns false for expired entry', () => { + cache.set('key1', 'value1'); + jest.advanceTimersByTime(1500); + expect(cache.has('key1')).toBe(false); + }); + + test('cleans up expired entry on check', () => { + cache.set('key1', 'value1'); + jest.advanceTimersByTime(1500); + cache.has('key1'); + expect(cache.size).toBe(0); + }); + + // ── BUG #497 regression — has() must NOT affect stats ───── + + test('does not increment hits counter (fix #497)', () => { + cache.set('key1', 'value1'); + cache.has('key1'); // should NOT touch stats + expect(cache.hits).toBe(0); + expect(cache.misses).toBe(0); + }); + + test('does not increment misses counter (fix #497)', () => { + cache.has('missing'); // should NOT touch stats + expect(cache.hits).toBe(0); + expect(cache.misses).toBe(0); + }); + + test('returns false and cleans up when timestamp is missing (out-of-sync maps)', () => { + cache.set('key1', 'value1'); + // Simulate out-of-sync: delete timestamp but keep cache entry + cache.timestamps.delete('key1'); + expect(cache.has('key1')).toBe(false); + expect(cache.size).toBe(0); + }); + + test('has() + get() counts exactly one hit, not two (fix #497)', () => { + cache.set('key1', 'value1'); + cache.has('key1'); // no stat change + const val = cache.get('key1'); // exactly 1 hit + expect(val).toBe('value1'); + expect(cache.hits).toBe(1); + expect(cache.misses).toBe(0); + }); + }); + + // ── invalidate ───────────────────────────────────────────────── + + describe('invalidate', () => { + test('removes specific entry', () => { + cache.set('key1', 'v1'); + cache.set('key2', 'v2'); + const deleted = cache.invalidate('key1'); + expect(deleted).toBe(true); + expect(cache.get('key1')).toBeNull(); + expect(cache.get('key2')).toBe('v2'); + }); + + test('returns false for non-existent key', () => { + expect(cache.invalidate('missing')).toBe(false); + }); + }); + + // ── clear ────────────────────────────────────────────────────── + + describe('clear', () => { + test('removes all entries and resets stats', () => { + cache.set('a', 1); + cache.set('b', 2); + cache.get('a'); // hit + cache.get('missing'); // miss + cache.clear(); + expect(cache.size).toBe(0); + expect(cache.hits).toBe(0); + expect(cache.misses).toBe(0); + }); + }); + + // ── clearExpired ─────────────────────────────────────────────── + + describe('clearExpired', () => { + test('removes only expired entries', () => { + cache.set('old', 'data'); + jest.advanceTimersByTime(800); + cache.set('new', 'data'); + jest.advanceTimersByTime(300); // old is 1100ms, new is 300ms + const cleared = cache.clearExpired(); + expect(cleared).toBe(1); + expect(cache.get('new')).toBe('data'); + }); + + test('returns 0 when nothing expired', () => { + cache.set('fresh', 'data'); + expect(cache.clearExpired()).toBe(0); + }); + + test('returns 0 on empty cache', () => { + expect(cache.clearExpired()).toBe(0); + }); + }); + + // ── size ─────────────────────────────────────────────────────── + + describe('size', () => { + test('returns number of entries', () => { + expect(cache.size).toBe(0); + cache.set('a', 1); + expect(cache.size).toBe(1); + cache.set('b', 2); + expect(cache.size).toBe(2); + }); + }); + + // ── statistics ───────────────────────────────────────────────── + + describe('statistics', () => { + test('tracks hits and misses', () => { + cache.set('key1', 'v1'); + cache.get('key1'); // hit + cache.get('key1'); // hit + cache.get('missing'); // miss + expect(cache.hits).toBe(2); + expect(cache.misses).toBe(1); + }); + + test('getStats returns correct statistics', () => { + cache.set('key1', 'v1'); + cache.get('key1'); // hit + cache.get('missing'); // miss + const stats = cache.getStats(); + expect(stats.size).toBe(1); + expect(stats.hits).toBe(1); + expect(stats.misses).toBe(1); + expect(stats.total).toBe(2); + expect(stats.hitRate).toBe('50.0%'); + expect(stats.ttl).toBe(1000); + expect(stats.ttlMinutes).toBe('0.0'); + }); + + test('getStats returns 0.0% hit rate when no requests', () => { + const stats = cache.getStats(); + expect(stats.hitRate).toBe('0.0%'); + }); + + test('resetStats clears counters but keeps cache', () => { + cache.set('key1', 'v1'); + cache.get('key1'); + cache.resetStats(); + expect(cache.hits).toBe(0); + expect(cache.misses).toBe(0); + expect(cache.get('key1')).toBe('v1'); + }); + }); + + // ── keys ─────────────────────────────────────────────────────── + + describe('keys', () => { + test('returns array of cache keys', () => { + cache.set('a', 1); + cache.set('b', 2); + expect(cache.keys()).toEqual(['a', 'b']); + }); + + test('returns empty array for empty cache', () => { + expect(cache.keys()).toEqual([]); + }); + }); + + // ── entries ──────────────────────────────────────────────────── + + describe('entries', () => { + test('returns entries with age and expiry info', () => { + cache.set('key1', 'value1'); + jest.advanceTimersByTime(200); + const entries = cache.entries(); + expect(entries).toHaveLength(1); + expect(entries[0].key).toBe('key1'); + expect(entries[0].value).toBe('value1'); + expect(entries[0].age).toBe(200); + expect(entries[0].ageSeconds).toBe('0.2'); + expect(entries[0].expires).toBe(800); + expect(entries[0].expiresSeconds).toBe('0.8'); + }); + }); + + // ── setTTL ───────────────────────────────────────────────────── + + describe('setTTL', () => { + test('updates TTL for future expiration checks', () => { + cache.set('key1', 'v1'); + cache.setTTL(500); + jest.advanceTimersByTime(600); + expect(cache.get('key1')).toBeNull(); + }); + }); + + // ── toJSON ───────────────────────────────────────────────────── + + describe('toJSON', () => { + test('serializes cache state to JSON string', () => { + cache.set('key1', 'v1'); + cache.get('key1'); // hit + const json = cache.toJSON(); + const parsed = JSON.parse(json); + expect(parsed.size).toBe(1); + expect(parsed.stats).toBeDefined(); + expect(parsed.entries).toHaveLength(1); + expect(parsed.entries[0].key).toBe('key1'); + }); + + test('produces valid JSON for empty cache', () => { + const parsed = JSON.parse(cache.toJSON()); + expect(parsed.size).toBe(0); + expect(parsed.entries).toEqual([]); + }); + }); + + // ── globalConfigCache singleton ──────────────────────────────── + + describe('globalConfigCache', () => { + test('is a ConfigCache instance', () => { + expect(globalConfigCache).toBeInstanceOf(ConfigCache); + }); + + test('has default 5 minute TTL', () => { + expect(globalConfigCache.ttl).toBe(5 * 60 * 1000); + }); + }); +});