From aaaa60f5db6b908cc65e601d63f11bd1a5e471b5 Mon Sep 17 00:00:00 2001 From: Damjan Savic Date: Sun, 25 Jan 2026 06:38:18 +0100 Subject: [PATCH] auto-claude: subtask-2-1 - Create tests for cache.ts (TTL, get/set, expiration logic) - Created comprehensive test suite for cache.ts covering: - set() and get() operations with various data types - TTL expiration logic (default, custom, and edge cases) - has() method for checking key existence - delete() method for removing individual keys - clear() method for removing all keys - Expiration boundary testing - Different TTL values for different items - TTL reset when items are overwritten - Fixed vitest.config.ts to include src/utils/** tests - Fixed tsconfig.json to include src/utils/** files - Used vi.useFakeTimers() for deterministic time-based testing - All tests properly isolated with beforeEach/afterEach hooks Co-Authored-By: Claude Sonnet 4.5 --- .auto-claude-security.json | 227 +++++++++++++++++++++++++++++ .auto-claude-status | 25 ++++ .claude_settings.json | 39 +++++ .gitignore | 5 +- src/utils/cache.test.ts | 282 +++++++++++++++++++++++++++++++++++++ test-output.txt | 0 tsconfig.json | 1 - vitest.config.ts | 6 +- 8 files changed, 579 insertions(+), 6 deletions(-) create mode 100644 .auto-claude-security.json create mode 100644 .auto-claude-status create mode 100644 .claude_settings.json create mode 100644 src/utils/cache.test.ts create mode 100644 test-output.txt diff --git a/.auto-claude-security.json b/.auto-claude-security.json new file mode 100644 index 0000000..eed60a2 --- /dev/null +++ b/.auto-claude-security.json @@ -0,0 +1,227 @@ +{ + "base_commands": [ + ".", + "[", + "[[", + "ag", + "awk", + "basename", + "bash", + "bc", + "break", + "cat", + "cd", + "chmod", + "clear", + "cmp", + "column", + "comm", + "command", + "continue", + "cp", + "curl", + "cut", + "date", + "df", + "diff", + "dig", + "dirname", + "du", + "echo", + "egrep", + "env", + "eval", + "exec", + "exit", + "expand", + "export", + "expr", + "false", + "fd", + "fgrep", + "file", + "find", + "fmt", + "fold", + "gawk", + "gh", + "git", + "grep", + "gunzip", + "gzip", + "head", + "help", + "host", + "iconv", + "id", + "jobs", + "join", + "jq", + "kill", + "killall", + "less", + "let", + "ln", + "ls", + "lsof", + "man", + "mkdir", + "mktemp", + "more", + "mv", + "nl", + "paste", + "pgrep", + "ping", + "pkill", + "popd", + "printenv", + "printf", + "ps", + "pushd", + "pwd", + "read", + "readlink", + "realpath", + "reset", + "return", + "rev", + "rg", + "rm", + "rmdir", + "sed", + "seq", + "set", + "sh", + "shuf", + "sleep", + "sort", + "source", + "split", + "stat", + "tail", + "tar", + "tee", + "test", + "time", + "timeout", + "touch", + "tr", + "tree", + "true", + "type", + "uname", + "unexpand", + "uniq", + "unset", + "unzip", + "watch", + "wc", + "wget", + "whereis", + "which", + "whoami", + "xargs", + "yes", + "yq", + "zip", + "zsh" + ], + "stack_commands": [ + "ar", + "clang", + "clang++", + "cmake", + "composer", + "dive", + "docker", + "docker-buildx", + "docker-compose", + "dockerfile", + "eslint", + "g++", + "gcc", + "ipython", + "jupyter", + "ld", + "make", + "meson", + "next", + "ninja", + "nm", + "node", + "notebook", + "npm", + "npx", + "objdump", + "pdb", + "php", + "pip", + "pip3", + "pipx", + "pnpm", + "pnpx", + "pudb", + "python", + "python3", + "react-scripts", + "strip", + "ts-node", + "tsc", + "tsx", + "vitest" + ], + "script_commands": [ + "bun", + "npm", + "pnpm", + "yarn" + ], + "custom_commands": [], + "detected_stack": { + "languages": [ + "python", + "javascript", + "typescript", + "php", + "c" + ], + "package_managers": [ + "pnpm" + ], + "frameworks": [ + "nextjs", + "react", + "vitest", + "eslint" + ], + "databases": [], + "infrastructure": [ + "docker" + ], + "cloud_providers": [], + "code_quality_tools": [], + "version_managers": [] + }, + "custom_scripts": { + "npm_scripts": [ + "dev", + "build", + "start", + "lint", + "build:images", + "generate:images", + "generate:images:dry", + "test", + "test:coverage" + ], + "make_targets": [], + "poetry_scripts": [], + "cargo_aliases": [], + "shell_scripts": [] + }, + "project_dir": "C:\\Users\\damja\\WebstormProjects\\Portfolio", + "created_at": "2026-01-22T15:28:38.237190", + "project_hash": "c4ad399e16be367eb4e6b076fe1d9ee3", + "inherited_from": "C:\\Users\\damja\\WebstormProjects\\Portfolio" +} \ No newline at end of file diff --git a/.auto-claude-status b/.auto-claude-status new file mode 100644 index 0000000..5e2a149 --- /dev/null +++ b/.auto-claude-status @@ -0,0 +1,25 @@ +{ + "active": true, + "spec": "030-add-unit-tests-vitest-configured-but-no-tests-exis", + "state": "building", + "subtasks": { + "completed": 2, + "total": 8, + "in_progress": 1, + "failed": 0 + }, + "phase": { + "current": "Utility Functions Tests", + "id": null, + "total": 2 + }, + "workers": { + "active": 0, + "max": 1 + }, + "session": { + "number": 4, + "started_at": "2026-01-25T06:18:19.433683" + }, + "last_update": "2026-01-25T06:32:23.146357" +} \ No newline at end of file diff --git a/.claude_settings.json b/.claude_settings.json new file mode 100644 index 0000000..f2da165 --- /dev/null +++ b/.claude_settings.json @@ -0,0 +1,39 @@ +{ + "sandbox": { + "enabled": true, + "autoAllowBashIfSandboxed": true + }, + "permissions": { + "defaultMode": "acceptEdits", + "allow": [ + "Read(./**)", + "Write(./**)", + "Edit(./**)", + "Glob(./**)", + "Grep(./**)", + "Read(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\030-add-unit-tests-vitest-configured-but-no-tests-exis/**)", + "Write(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\030-add-unit-tests-vitest-configured-but-no-tests-exis/**)", + "Edit(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\030-add-unit-tests-vitest-configured-but-no-tests-exis/**)", + "Glob(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\030-add-unit-tests-vitest-configured-but-no-tests-exis/**)", + "Grep(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\030-add-unit-tests-vitest-configured-but-no-tests-exis/**)", + "Read(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\030-add-unit-tests-vitest-configured-but-no-tests-exis\\.auto-claude\\specs\\030-add-unit-tests-vitest-configured-but-no-tests-exis/**)", + "Write(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\030-add-unit-tests-vitest-configured-but-no-tests-exis\\.auto-claude\\specs\\030-add-unit-tests-vitest-configured-but-no-tests-exis/**)", + "Edit(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\030-add-unit-tests-vitest-configured-but-no-tests-exis\\.auto-claude\\specs\\030-add-unit-tests-vitest-configured-but-no-tests-exis/**)", + "Read(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude/**)", + "Write(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude/**)", + "Edit(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude/**)", + "Glob(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude/**)", + "Grep(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude/**)", + "Bash(*)", + "WebFetch(*)", + "WebSearch(*)", + "mcp__context7__resolve-library-id(*)", + "mcp__context7__get-library-docs(*)", + "mcp__graphiti-memory__search_nodes(*)", + "mcp__graphiti-memory__search_facts(*)", + "mcp__graphiti-memory__add_episode(*)", + "mcp__graphiti-memory__get_episodes(*)", + "mcp__graphiti-memory__get_entity_edge(*)" + ] + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index a484ab2..fa65296 100644 --- a/.gitignore +++ b/.gitignore @@ -81,4 +81,7 @@ supabase/.temp/ .history/ # Source images (originals before optimization) -source-images/ \ No newline at end of file +source-images/ + +# Auto Claude data directory +.auto-claude/ diff --git a/src/utils/cache.test.ts b/src/utils/cache.test.ts new file mode 100644 index 0000000..230b0ac --- /dev/null +++ b/src/utils/cache.test.ts @@ -0,0 +1,282 @@ +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; +import { cache } from './cache'; + +describe('Cache', () => { + beforeEach(() => { + vi.useFakeTimers(); + cache.clear(); // Clear cache before each test + }); + + afterEach(() => { + vi.useRealTimers(); + cache.clear(); // Clean up after each test + }); + + describe('set and get', () => { + it('should set and retrieve a value', () => { + cache.set('key', 'value'); + expect(cache.get('key')).toBe('value'); + }); + + it('should set multiple values with different keys', () => { + cache.set('key1', 'value1'); + cache.set('key2', 'value2'); + cache.set('key3', 'value3'); + + expect(cache.get('key1')).toBe('value1'); + expect(cache.get('key2')).toBe('value2'); + expect(cache.get('key3')).toBe('value3'); + }); + + it('should overwrite existing value for same key', () => { + cache.set('key', 'value1'); + expect(cache.get('key')).toBe('value1'); + + cache.set('key', 'value2'); + expect(cache.get('key')).toBe('value2'); + }); + + it('should return null for non-existing key', () => { + expect(cache.get('nonexistent')).toBeNull(); + }); + + it('should store different types of values', () => { + cache.set('string', 'text'); + cache.set('number', 42); + cache.set('boolean', true); + cache.set('object', { name: 'test' }); + cache.set('array', [1, 2, 3]); + cache.set('null', null); + + expect(cache.get('string')).toBe('text'); + expect(cache.get('number')).toBe(42); + expect(cache.get('boolean')).toBe(true); + expect(cache.get('object')).toEqual({ name: 'test' }); + expect(cache.get('array')).toEqual([1, 2, 3]); + expect(cache.get('null')).toBeNull(); + }); + }); + + describe('TTL expiration', () => { + it('should use default TTL of 5 minutes', () => { + cache.set('test', 'value'); + + // Should be available before TTL expires + expect(cache.get('test')).toBe('value'); + + // Advance time by 4 minutes 59 seconds + vi.advanceTimersByTime(4 * 60 * 1000 + 59 * 1000); + expect(cache.get('test')).toBe('value'); + + // Advance time by 2 more seconds (total 5 minutes 1 second) + vi.advanceTimersByTime(2 * 1000); + expect(cache.get('test')).toBeNull(); + }); + + it('should accept custom TTL per item', () => { + cache.set('short', 'value', 1000); // 1 second TTL + cache.set('long', 'value', 10000); // 10 seconds TTL + + // Both should be available initially + expect(cache.get('short')).toBe('value'); + expect(cache.get('long')).toBe('value'); + + // After 1.5 seconds, short should expire but long should remain + vi.advanceTimersByTime(1500); + expect(cache.get('short')).toBeNull(); + expect(cache.get('long')).toBe('value'); + + // After 9 more seconds (total 10.5), long should expire + vi.advanceTimersByTime(9000); + expect(cache.get('long')).toBeNull(); + }); + + it('should return null for expired item', () => { + cache.set('key', 'value', 1000); + + // Should be available initially + expect(cache.get('key')).toBe('value'); + + // Should be null after expiration + vi.advanceTimersByTime(1001); + expect(cache.get('key')).toBeNull(); + }); + + it('should delete expired item from storage', () => { + cache.set('key', 'value', 1000); + + // Item should exist + expect(cache.get('key')).toBe('value'); + + // After expiration, get should delete it + vi.advanceTimersByTime(1001); + expect(cache.get('key')).toBeNull(); + + // Subsequent get should also return null (item was deleted) + expect(cache.get('key')).toBeNull(); + }); + + it('should return value at exact TTL boundary', () => { + cache.set('key', 'value', 1000); + + // At exactly TTL time, should still be valid + vi.advanceTimersByTime(1000); + expect(cache.get('key')).toBe('value'); + + // One millisecond later, should expire + vi.advanceTimersByTime(1); + expect(cache.get('key')).toBeNull(); + }); + + it('should respect different TTLs for different items', () => { + cache.set('item1', 'value1', 1000); + cache.set('item2', 'value2', 2000); + cache.set('item3', 'value3', 3000); + + // All items should be available initially + expect(cache.get('item1')).toBe('value1'); + expect(cache.get('item2')).toBe('value2'); + expect(cache.get('item3')).toBe('value3'); + + // After 1.5 seconds, item1 should expire + vi.advanceTimersByTime(1500); + expect(cache.get('item1')).toBeNull(); + expect(cache.get('item2')).toBe('value2'); + expect(cache.get('item3')).toBe('value3'); + + // After 1 more second (total 2.5), item2 should expire + vi.advanceTimersByTime(1000); + expect(cache.get('item1')).toBeNull(); + expect(cache.get('item2')).toBeNull(); + expect(cache.get('item3')).toBe('value3'); + + // After 1 more second (total 3.5), item3 should expire + vi.advanceTimersByTime(1000); + expect(cache.get('item1')).toBeNull(); + expect(cache.get('item2')).toBeNull(); + expect(cache.get('item3')).toBeNull(); + }); + + it('should reset TTL when item is overwritten', () => { + cache.set('key', 'value1', 1000); + + // Advance time by 500ms + vi.advanceTimersByTime(500); + + // Overwrite with new value and new TTL + cache.set('key', 'value2', 2000); + + // After 1500ms more (total 2000ms from first set), should still be available + // because TTL was reset + vi.advanceTimersByTime(1500); + expect(cache.get('key')).toBe('value2'); + + // After 600ms more (2100ms from second set), should expire + vi.advanceTimersByTime(600); + expect(cache.get('key')).toBeNull(); + }); + + it('should handle zero TTL', () => { + cache.set('key', 'value', 0); + + // Should expire immediately + vi.advanceTimersByTime(1); + expect(cache.get('key')).toBeNull(); + }); + + it('should handle very long TTL', () => { + const oneDayInMs = 24 * 60 * 60 * 1000; + cache.set('key', 'value', oneDayInMs); + + // Should be available after 23 hours + vi.advanceTimersByTime(23 * 60 * 60 * 1000); + expect(cache.get('key')).toBe('value'); + + // Should still be available at exactly 24 hours + vi.advanceTimersByTime(60 * 60 * 1000); + expect(cache.get('key')).toBe('value'); + + // Should expire after 24 hours + 1ms + vi.advanceTimersByTime(1); + expect(cache.get('key')).toBeNull(); + }); + }); + + describe('has', () => { + it('should return true for existing key', () => { + cache.set('key', 'value'); + expect(cache.has('key')).toBe(true); + }); + + it('should return false for non-existing key', () => { + expect(cache.has('nonexistent')).toBe(false); + }); + + it('should return false for expired key', () => { + cache.set('key', 'value', 1000); + + expect(cache.has('key')).toBe(true); + + vi.advanceTimersByTime(1001); + expect(cache.has('key')).toBe(false); + }); + + it('should return false for null values', () => { + cache.set('key', null); + expect(cache.has('key')).toBe(false); + }); + }); + + describe('delete', () => { + it('should delete existing key', () => { + cache.set('key', 'value'); + expect(cache.get('key')).toBe('value'); + + cache.delete('key'); + expect(cache.get('key')).toBeNull(); + }); + + it('should handle deleting non-existing key', () => { + expect(() => cache.delete('nonexistent')).not.toThrow(); + expect(cache.get('nonexistent')).toBeNull(); + }); + + it('should only delete specified key', () => { + cache.set('key1', 'value1'); + cache.set('key2', 'value2'); + cache.set('key3', 'value3'); + + cache.delete('key2'); + + expect(cache.get('key1')).toBe('value1'); + expect(cache.get('key2')).toBeNull(); + expect(cache.get('key3')).toBe('value3'); + }); + }); + + describe('clear', () => { + it('should clear all items from cache', () => { + cache.set('key1', 'value1'); + cache.set('key2', 'value2'); + cache.set('key3', 'value3'); + + cache.clear(); + + expect(cache.get('key1')).toBeNull(); + expect(cache.get('key2')).toBeNull(); + expect(cache.get('key3')).toBeNull(); + }); + + it('should handle clearing empty cache', () => { + expect(() => cache.clear()).not.toThrow(); + }); + + it('should allow setting items after clear', () => { + cache.set('key', 'value1'); + cache.clear(); + cache.set('key', 'value2'); + + expect(cache.get('key')).toBe('value2'); + }); + }); +}); diff --git a/test-output.txt b/test-output.txt new file mode 100644 index 0000000..e69de29 diff --git a/tsconfig.json b/tsconfig.json index 4ca760c..425c61c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -34,7 +34,6 @@ "src/pages-vite", "src/hooks", "src/services", - "src/utils", "**/*.bak" ] } diff --git a/vitest.config.ts b/vitest.config.ts index 8069a1a..e47e4bd 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -25,8 +25,7 @@ export default defineConfig({ '**/src/components-vite/**', '**/src/pages-vite/**', '**/src/hooks/**', - '**/src/services/**', - '**/src/utils/**' + '**/src/services/**' ], coverage: { provider: 'v8', @@ -40,8 +39,7 @@ export default defineConfig({ 'src/components-vite/**', 'src/pages-vite/**', 'src/hooks/**', - 'src/services/**', - 'src/utils/**' + 'src/services/**' ] } },