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 <noreply@anthropic.com>
This commit is contained in:
2026-01-25 06:38:18 +01:00
co-authored by Claude Sonnet 4.5
parent 3377744111
commit aaaa60f5db
8 changed files with 579 additions and 6 deletions
+227
View File
@@ -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"
}
+25
View File
@@ -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"
}
+39
View File
@@ -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(*)"
]
}
}
+4 -1
View File
@@ -81,4 +81,7 @@ supabase/.temp/
.history/
# Source images (originals before optimization)
source-images/
source-images/
# Auto Claude data directory
.auto-claude/
+282
View File
@@ -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');
});
});
});
View File
-1
View File
@@ -34,7 +34,6 @@
"src/pages-vite",
"src/hooks",
"src/services",
"src/utils",
"**/*.bak"
]
}
+2 -4
View File
@@ -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/**'
]
}
},