mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
2253d05618
Remove the nested v3/ subdirectory by moving all active source code, tests, configs, and assets directly to the project root. Archive v2/ (dead code, 847MB) to v2-archive.tar.gz. Key changes: - Move v3/src/ → src/, v3/tests/ → tests/, v3/packages/ → packages/ - Move v3/tsconfig.json, v3/vitest.config.ts to root - Merge v3/package.json into root package.json (ESM, scripts, deps, exports) - Merge v3/scripts/ into scripts/, v3/implementation/ into docs/implementation/ - Update all CI workflows to remove "cd v3 &&" and v3/ path prefixes - Fix all init installer path resolution (3-level-up → 2-level-up) - Fix hooks to use "node ./dist/cli/bundle.js" instead of bare "aqe" - Update settings.json ADR/DDD directories to docs/implementation/ - Fix helper scripts (statusline, adr-compliance, ddd-tracker, guidance-hooks) - Update 58 agent definitions: aqe/v3/ → aqe/ memory namespaces - Update 15 guidance shards: v3/src/ → src/ path references - Fix sync interfaces: remove v3/ and v2/ path references - Fix test path calculations (process.cwd() + '..' no longer needed) - Fix flaky perf test threshold (500ms → 1000ms for CI environments) - Fix complexity-analyzer test to match ADR-051 keyword-based eligibility Build passes, 17,901 tests pass, CLI and MCP bundles verified. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
855 lines
25 KiB
TypeScript
855 lines
25 KiB
TypeScript
/**
|
|
* State Delta Cache Unit Tests
|
|
*
|
|
* Tests for pre-computed state delta caching, LRU eviction,
|
|
* cache warming, and fallback computation.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
import {
|
|
StateDeltaCache,
|
|
createStateDeltaCache,
|
|
getAgentStatusValues,
|
|
getProgressMilestones,
|
|
getToolStatusValues,
|
|
type StateDeltaCacheConfig,
|
|
type CacheMetrics,
|
|
} from '../../../../src/adapters/ag-ui/state-delta-cache.js';
|
|
import type { JsonPatchOperation } from '../../../../src/adapters/ag-ui/event-types.js';
|
|
|
|
// ============================================================================
|
|
// Factory Function Tests
|
|
// ============================================================================
|
|
|
|
describe('createStateDeltaCache', () => {
|
|
it('should create cache with default config', () => {
|
|
const cache = createStateDeltaCache();
|
|
expect(cache).toBeInstanceOf(StateDeltaCache);
|
|
expect(cache.maxSize).toBe(1000);
|
|
});
|
|
|
|
it('should create cache with custom config', () => {
|
|
const cache = createStateDeltaCache({
|
|
maxSize: 500,
|
|
warmOnInit: false,
|
|
});
|
|
expect(cache.maxSize).toBe(500);
|
|
expect(cache.size).toBe(0); // No warming
|
|
});
|
|
|
|
it('should create independent instances', () => {
|
|
const cache1 = createStateDeltaCache({ warmOnInit: false });
|
|
const cache2 = createStateDeltaCache({ warmOnInit: false });
|
|
|
|
cache1.precompute({ a: 1 }, { a: 2 });
|
|
expect(cache1.size).toBe(1);
|
|
expect(cache2.size).toBe(0);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Cache Operations Tests
|
|
// ============================================================================
|
|
|
|
describe('StateDeltaCache', () => {
|
|
let cache: StateDeltaCache;
|
|
|
|
beforeEach(() => {
|
|
cache = createStateDeltaCache({
|
|
warmOnInit: false, // Disable for predictable tests
|
|
maxSize: 10,
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Basic Operations
|
|
// ============================================================================
|
|
|
|
describe('getDelta', () => {
|
|
it('should compute delta on cache miss', () => {
|
|
const fromState = { count: 0 };
|
|
const toState = { count: 5 };
|
|
|
|
const delta = cache.getDelta(fromState, toState);
|
|
|
|
expect(delta).toHaveLength(1);
|
|
expect(delta[0]).toEqual({
|
|
op: 'replace',
|
|
path: '/count',
|
|
value: 5,
|
|
});
|
|
|
|
const metrics = cache.getMetrics();
|
|
expect(metrics.misses).toBe(1);
|
|
expect(metrics.hits).toBe(0);
|
|
});
|
|
|
|
it('should return cached delta on cache hit', () => {
|
|
const fromState = { count: 0 };
|
|
const toState = { count: 5 };
|
|
|
|
// First call - miss
|
|
const delta1 = cache.getDelta(fromState, toState);
|
|
// Second call - hit
|
|
const delta2 = cache.getDelta(fromState, toState);
|
|
|
|
expect(delta1).toEqual(delta2);
|
|
|
|
const metrics = cache.getMetrics();
|
|
expect(metrics.misses).toBe(1);
|
|
expect(metrics.hits).toBe(1);
|
|
});
|
|
|
|
it('should handle add operations', () => {
|
|
const fromState = {};
|
|
const toState = { newField: 'value' };
|
|
|
|
const delta = cache.getDelta(fromState, toState);
|
|
|
|
expect(delta).toHaveLength(1);
|
|
expect(delta[0]).toEqual({
|
|
op: 'add',
|
|
path: '/newField',
|
|
value: 'value',
|
|
});
|
|
});
|
|
|
|
it('should handle remove operations', () => {
|
|
const fromState = { oldField: 'value' };
|
|
const toState = {};
|
|
|
|
const delta = cache.getDelta(fromState, toState);
|
|
|
|
expect(delta).toHaveLength(1);
|
|
expect(delta[0]).toEqual({
|
|
op: 'remove',
|
|
path: '/oldField',
|
|
});
|
|
});
|
|
|
|
it('should handle nested object changes', () => {
|
|
const fromState = { agent: { status: 'idle' } };
|
|
const toState = { agent: { status: 'running' } };
|
|
|
|
const delta = cache.getDelta(fromState, toState);
|
|
|
|
expect(delta).toHaveLength(1);
|
|
expect(delta[0]).toEqual({
|
|
op: 'replace',
|
|
path: '/agent/status',
|
|
value: 'running',
|
|
});
|
|
});
|
|
|
|
it('should handle empty state to complex state', () => {
|
|
const fromState = {};
|
|
const toState = {
|
|
agent: { status: 'running', id: '123' },
|
|
progress: { percent: 50 },
|
|
};
|
|
|
|
const delta = cache.getDelta(fromState, toState);
|
|
|
|
expect(delta.length).toBeGreaterThan(0);
|
|
expect(delta.some((op) => op.path === '/agent')).toBe(true);
|
|
});
|
|
|
|
it('should return empty delta for identical states', () => {
|
|
const state = { count: 5 };
|
|
|
|
const delta = cache.getDelta(state, state);
|
|
|
|
expect(delta).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('has', () => {
|
|
it('should return false for uncached states', () => {
|
|
const result = cache.has({ a: 1 }, { a: 2 });
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return true for cached states', () => {
|
|
cache.precompute({ a: 1 }, { a: 2 });
|
|
const result = cache.has({ a: 1 }, { a: 2 });
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should return false for different state order', () => {
|
|
cache.precompute({ a: 1 }, { a: 2 });
|
|
// Different direction
|
|
const result = cache.has({ a: 2 }, { a: 1 });
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('precompute', () => {
|
|
it('should store computed delta in cache', () => {
|
|
const fromState = { status: 'idle' };
|
|
const toState = { status: 'running' };
|
|
|
|
cache.precompute(fromState, toState);
|
|
|
|
expect(cache.size).toBe(1);
|
|
expect(cache.has(fromState, toState)).toBe(true);
|
|
});
|
|
|
|
it('should return the computed delta', () => {
|
|
const fromState = { count: 0 };
|
|
const toState = { count: 100 };
|
|
|
|
const delta = cache.precompute(fromState, toState);
|
|
|
|
expect(delta).toHaveLength(1);
|
|
expect(delta[0].op).toBe('replace');
|
|
});
|
|
|
|
it('should track precomputed entries separately', () => {
|
|
cache.precompute({ a: 1 }, { a: 2 });
|
|
|
|
const metrics = cache.getMetrics();
|
|
expect(metrics.preComputedEntries).toBe(1);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Cache Invalidation
|
|
// ============================================================================
|
|
|
|
describe('invalidate', () => {
|
|
it('should remove specific cache entry', () => {
|
|
const fromState = { a: 1 };
|
|
const toState = { a: 2 };
|
|
|
|
cache.precompute(fromState, toState);
|
|
expect(cache.size).toBe(1);
|
|
|
|
const removed = cache.invalidate(fromState, toState);
|
|
expect(removed).toBe(true);
|
|
expect(cache.size).toBe(0);
|
|
});
|
|
|
|
it('should return false if entry does not exist', () => {
|
|
const removed = cache.invalidate({ x: 1 }, { x: 2 });
|
|
expect(removed).toBe(false);
|
|
});
|
|
|
|
it('should remove entry from precomputed tracking', () => {
|
|
cache.precompute({ a: 1 }, { a: 2 });
|
|
cache.invalidate({ a: 1 }, { a: 2 });
|
|
|
|
const metrics = cache.getMetrics();
|
|
expect(metrics.preComputedEntries).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('invalidateByPath', () => {
|
|
it('should remove entries matching path prefix', () => {
|
|
cache.precompute(
|
|
{ agent: { status: 'idle' } },
|
|
{ agent: { status: 'running' } }
|
|
);
|
|
cache.precompute(
|
|
{ progress: { percent: 0 } },
|
|
{ progress: { percent: 50 } }
|
|
);
|
|
|
|
expect(cache.size).toBe(2);
|
|
|
|
const removed = cache.invalidateByPath('/agent');
|
|
expect(removed).toBe(1);
|
|
expect(cache.size).toBe(1);
|
|
});
|
|
|
|
it('should return 0 if no entries match', () => {
|
|
cache.precompute({ a: 1 }, { a: 2 });
|
|
|
|
const removed = cache.invalidateByPath('/nonexistent');
|
|
expect(removed).toBe(0);
|
|
expect(cache.size).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('clear', () => {
|
|
it('should remove all cache entries', () => {
|
|
cache.precompute({ a: 1 }, { a: 2 });
|
|
cache.precompute({ b: 1 }, { b: 2 });
|
|
|
|
cache.clear();
|
|
|
|
expect(cache.size).toBe(0);
|
|
});
|
|
|
|
it('should reset precomputed tracking', () => {
|
|
cache.precompute({ a: 1 }, { a: 2 });
|
|
cache.clear();
|
|
|
|
const metrics = cache.getMetrics();
|
|
expect(metrics.preComputedEntries).toBe(0);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// LRU Eviction
|
|
// ============================================================================
|
|
|
|
describe('LRU Eviction', () => {
|
|
it('should evict entries when at capacity', () => {
|
|
const smallCache = createStateDeltaCache({
|
|
maxSize: 3,
|
|
warmOnInit: false,
|
|
});
|
|
|
|
// Fill cache
|
|
smallCache.precompute({ a: 1 }, { a: 2 });
|
|
smallCache.precompute({ b: 1 }, { b: 2 });
|
|
smallCache.precompute({ c: 1 }, { c: 2 });
|
|
expect(smallCache.size).toBe(3);
|
|
|
|
// Add one more - should evict
|
|
smallCache.precompute({ d: 1 }, { d: 2 });
|
|
expect(smallCache.size).toBe(3);
|
|
|
|
const metrics = smallCache.getMetrics();
|
|
expect(metrics.evictions).toBe(1);
|
|
});
|
|
|
|
it('should evict least recently accessed entry', async () => {
|
|
const smallCache = createStateDeltaCache({
|
|
maxSize: 3,
|
|
warmOnInit: false,
|
|
});
|
|
|
|
// Fill cache
|
|
smallCache.precompute({ a: 1 }, { a: 2 });
|
|
smallCache.precompute({ b: 1 }, { b: 2 });
|
|
smallCache.precompute({ c: 1 }, { c: 2 });
|
|
|
|
// Access 'a' and 'c' to make 'b' least recently used
|
|
smallCache.getDelta({ a: 1 }, { a: 2 });
|
|
smallCache.getDelta({ c: 1 }, { c: 2 });
|
|
|
|
// Add new entry - should evict 'b'
|
|
smallCache.precompute({ d: 1 }, { d: 2 });
|
|
|
|
// 'b' should be gone
|
|
expect(smallCache.has({ b: 1 }, { b: 2 })).toBe(false);
|
|
// 'a' and 'c' should still be there
|
|
expect(smallCache.has({ a: 1 }, { a: 2 })).toBe(true);
|
|
expect(smallCache.has({ c: 1 }, { c: 2 })).toBe(true);
|
|
});
|
|
|
|
it('should prefer evicting non-precomputed entries', () => {
|
|
const smallCache = createStateDeltaCache({
|
|
maxSize: 3,
|
|
warmOnInit: false,
|
|
});
|
|
|
|
// Add precomputed entry first
|
|
smallCache.precompute({ a: 1 }, { a: 2 });
|
|
|
|
// Add computed entries (via getDelta)
|
|
smallCache.getDelta({ b: 1 }, { b: 2 });
|
|
smallCache.getDelta({ c: 1 }, { c: 2 });
|
|
|
|
// Add new precomputed - should evict 'b' (oldest non-precomputed)
|
|
smallCache.precompute({ d: 1 }, { d: 2 });
|
|
|
|
// Precomputed 'a' should still be there
|
|
expect(smallCache.has({ a: 1 }, { a: 2 })).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// TTL Expiration
|
|
// ============================================================================
|
|
|
|
describe('TTL Expiration', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('should expire entries after TTL', () => {
|
|
const ttlCache = createStateDeltaCache({
|
|
maxSize: 10,
|
|
warmOnInit: false,
|
|
ttl: 1000, // 1 second
|
|
});
|
|
|
|
ttlCache.precompute({ a: 1 }, { a: 2 });
|
|
expect(ttlCache.has({ a: 1 }, { a: 2 })).toBe(true);
|
|
|
|
// Advance time past TTL
|
|
vi.advanceTimersByTime(1500);
|
|
|
|
// Should now be expired
|
|
expect(ttlCache.has({ a: 1 }, { a: 2 })).toBe(false);
|
|
});
|
|
|
|
it('should recompute on expired entry access', () => {
|
|
const ttlCache = createStateDeltaCache({
|
|
maxSize: 10,
|
|
warmOnInit: false,
|
|
ttl: 1000,
|
|
});
|
|
|
|
ttlCache.precompute({ a: 1 }, { a: 2 });
|
|
|
|
// Advance time past TTL
|
|
vi.advanceTimersByTime(1500);
|
|
|
|
// Should trigger recomputation (miss)
|
|
const delta = ttlCache.getDelta({ a: 1 }, { a: 2 });
|
|
expect(delta).toHaveLength(1);
|
|
|
|
const metrics = ttlCache.getMetrics();
|
|
expect(metrics.misses).toBe(1);
|
|
});
|
|
|
|
it('should not expire entries when TTL is 0', () => {
|
|
const noTtlCache = createStateDeltaCache({
|
|
maxSize: 10,
|
|
warmOnInit: false,
|
|
ttl: 0, // No TTL
|
|
});
|
|
|
|
noTtlCache.precompute({ a: 1 }, { a: 2 });
|
|
|
|
// Advance time significantly
|
|
vi.advanceTimersByTime(100000);
|
|
|
|
// Should still be cached
|
|
expect(noTtlCache.has({ a: 1 }, { a: 2 })).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Cache Warming
|
|
// ============================================================================
|
|
|
|
describe('Cache Warming', () => {
|
|
it('should warm cache with agent status transitions', () => {
|
|
const warmedCache = createStateDeltaCache({
|
|
warmOnInit: true,
|
|
maxSize: 100,
|
|
});
|
|
|
|
// Check for idle -> running transition
|
|
const fromState = { agent: { status: 'idle' } };
|
|
const toState = { agent: { status: 'running' } };
|
|
|
|
const delta = warmedCache.getDelta(fromState, toState);
|
|
expect(delta).toHaveLength(1);
|
|
|
|
const metrics = warmedCache.getMetrics();
|
|
expect(metrics.hits).toBe(1); // Should be a cache hit
|
|
});
|
|
|
|
it('should warm cache with progress transitions', () => {
|
|
const warmedCache = createStateDeltaCache({
|
|
warmOnInit: true,
|
|
maxSize: 100,
|
|
});
|
|
|
|
// Check for 0 -> 25 transition
|
|
const fromState = { progress: { percent: 0 } };
|
|
const toState = { progress: { percent: 25 } };
|
|
|
|
warmedCache.getDelta(fromState, toState);
|
|
|
|
const metrics = warmedCache.getMetrics();
|
|
expect(metrics.hits).toBe(1);
|
|
});
|
|
|
|
it('should warm cache with tool status transitions', () => {
|
|
const warmedCache = createStateDeltaCache({
|
|
warmOnInit: true,
|
|
maxSize: 100,
|
|
});
|
|
|
|
// Check for pending -> executing transition
|
|
const fromState = { tool: { status: 'pending' } };
|
|
const toState = { tool: { status: 'executing' } };
|
|
|
|
warmedCache.getDelta(fromState, toState);
|
|
|
|
const metrics = warmedCache.getMetrics();
|
|
expect(metrics.hits).toBe(1);
|
|
});
|
|
|
|
it('should use custom paths for warming', () => {
|
|
const customCache = createStateDeltaCache({
|
|
warmOnInit: true,
|
|
maxSize: 100,
|
|
agentStatusPath: '/custom/agent/state',
|
|
progressPath: '/custom/progress/value',
|
|
toolStatusPath: '/custom/tool/state',
|
|
});
|
|
|
|
// Check custom agent path
|
|
const fromState = { custom: { agent: { state: 'idle' } } };
|
|
const toState = { custom: { agent: { state: 'running' } } };
|
|
|
|
customCache.getDelta(fromState, toState);
|
|
|
|
const metrics = customCache.getMetrics();
|
|
expect(metrics.hits).toBe(1);
|
|
});
|
|
|
|
it('should track precomputed entries from warming', () => {
|
|
const warmedCache = createStateDeltaCache({
|
|
warmOnInit: true,
|
|
maxSize: 100,
|
|
});
|
|
|
|
const metrics = warmedCache.getMetrics();
|
|
// Should have pre-computed entries from all transitions
|
|
// 7 agent + 7 progress + 5 tool = 19
|
|
expect(metrics.preComputedEntries).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Custom Pre-computation
|
|
// ============================================================================
|
|
|
|
describe('precomputeStatusTransition', () => {
|
|
it('should pre-compute custom status transition', () => {
|
|
cache.precomputeStatusTransition('/workflow/status', 'draft', 'published');
|
|
|
|
const fromState = { workflow: { status: 'draft' } };
|
|
const toState = { workflow: { status: 'published' } };
|
|
|
|
cache.getDelta(fromState, toState);
|
|
|
|
const metrics = cache.getMetrics();
|
|
expect(metrics.hits).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('precomputeProgressTransition', () => {
|
|
it('should pre-compute custom progress transition', () => {
|
|
cache.precomputeProgressTransition('/upload/progress', 0, 100);
|
|
|
|
const fromState = { upload: { progress: 0 } };
|
|
const toState = { upload: { progress: 100 } };
|
|
|
|
cache.getDelta(fromState, toState);
|
|
|
|
const metrics = cache.getMetrics();
|
|
expect(metrics.hits).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('precomputeAllTransitions', () => {
|
|
it('should pre-compute all transitions between values', () => {
|
|
const count = cache.precomputeAllTransitions('/color', [
|
|
'red',
|
|
'green',
|
|
'blue',
|
|
]);
|
|
|
|
// 3 values = 3 * 2 = 6 transitions (excluding same-to-same)
|
|
expect(count).toBe(6);
|
|
expect(cache.size).toBe(6);
|
|
});
|
|
|
|
it('should skip identical states', () => {
|
|
const count = cache.precomputeAllTransitions('/value', [1, 1, 2]);
|
|
|
|
// Only 1->2 and 2->1 should be computed (1->1 skipped)
|
|
expect(count).toBe(4); // 1->2, 2->1, 1->2 (dup), 2->1 (dup) but dedup by map
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Metrics
|
|
// ============================================================================
|
|
|
|
describe('getMetrics', () => {
|
|
it('should return accurate metrics', () => {
|
|
// Generate some cache activity
|
|
cache.getDelta({ a: 1 }, { a: 2 }); // Miss
|
|
cache.getDelta({ a: 1 }, { a: 2 }); // Hit
|
|
cache.getDelta({ b: 1 }, { b: 2 }); // Miss
|
|
cache.precompute({ c: 1 }, { c: 2 }); // Precompute
|
|
|
|
const metrics = cache.getMetrics();
|
|
|
|
expect(metrics.hits).toBe(1);
|
|
expect(metrics.misses).toBe(2);
|
|
expect(metrics.size).toBe(3);
|
|
expect(metrics.maxSize).toBe(10);
|
|
expect(metrics.hitRate).toBeCloseTo(33.33, 1);
|
|
expect(metrics.evictions).toBe(0);
|
|
expect(metrics.preComputedEntries).toBe(1);
|
|
});
|
|
|
|
it('should calculate hit rate correctly', () => {
|
|
// 4 hits, 1 miss = 80% hit rate
|
|
cache.precompute({ a: 1 }, { a: 2 });
|
|
cache.getDelta({ a: 1 }, { a: 2 }); // Hit
|
|
cache.getDelta({ a: 1 }, { a: 2 }); // Hit
|
|
cache.getDelta({ a: 1 }, { a: 2 }); // Hit
|
|
cache.getDelta({ a: 1 }, { a: 2 }); // Hit
|
|
cache.getDelta({ b: 1 }, { b: 2 }); // Miss
|
|
|
|
const metrics = cache.getMetrics();
|
|
expect(metrics.hitRate).toBe(80);
|
|
});
|
|
|
|
it('should handle zero lookups', () => {
|
|
const metrics = cache.getMetrics();
|
|
expect(metrics.hitRate).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('resetMetrics', () => {
|
|
it('should reset all metrics to zero', () => {
|
|
cache.getDelta({ a: 1 }, { a: 2 });
|
|
cache.getDelta({ a: 1 }, { a: 2 });
|
|
|
|
cache.resetMetrics();
|
|
|
|
const metrics = cache.getMetrics();
|
|
expect(metrics.hits).toBe(0);
|
|
expect(metrics.misses).toBe(0);
|
|
expect(metrics.evictions).toBe(0);
|
|
});
|
|
|
|
it('should not affect cache contents', () => {
|
|
cache.precompute({ a: 1 }, { a: 2 });
|
|
cache.resetMetrics();
|
|
|
|
expect(cache.size).toBe(1);
|
|
expect(cache.has({ a: 1 }, { a: 2 })).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Edge Cases
|
|
// ============================================================================
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should handle empty objects', () => {
|
|
const delta = cache.getDelta({}, {});
|
|
expect(delta).toHaveLength(0);
|
|
});
|
|
|
|
it('should handle deeply nested state', () => {
|
|
const fromState = {
|
|
level1: {
|
|
level2: {
|
|
level3: {
|
|
value: 'old',
|
|
},
|
|
},
|
|
},
|
|
};
|
|
const toState = {
|
|
level1: {
|
|
level2: {
|
|
level3: {
|
|
value: 'new',
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const delta = cache.getDelta(fromState, toState);
|
|
|
|
expect(delta).toHaveLength(1);
|
|
expect(delta[0].path).toBe('/level1/level2/level3/value');
|
|
});
|
|
|
|
it('should handle arrays in state', () => {
|
|
const fromState = { items: [1, 2, 3] };
|
|
const toState = { items: [1, 2, 3, 4] };
|
|
|
|
const delta = cache.getDelta(fromState, toState);
|
|
|
|
expect(delta.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should handle special characters in paths', () => {
|
|
const fromState = { 'key/with/slashes': 'old' };
|
|
const toState = { 'key/with/slashes': 'new' };
|
|
|
|
const delta = cache.getDelta(fromState, toState);
|
|
|
|
expect(delta).toHaveLength(1);
|
|
});
|
|
|
|
it('should handle null values', () => {
|
|
const fromState = { value: null };
|
|
const toState = { value: 'not null' };
|
|
|
|
const delta = cache.getDelta(fromState, toState);
|
|
|
|
expect(delta).toHaveLength(1);
|
|
expect(delta[0].op).toBe('replace');
|
|
});
|
|
|
|
it('should handle type changes', () => {
|
|
const fromState = { value: 123 };
|
|
const toState = { value: 'string now' };
|
|
|
|
const delta = cache.getDelta(fromState, toState);
|
|
|
|
expect(delta).toHaveLength(1);
|
|
expect(delta[0].value).toBe('string now');
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Memory Bounds
|
|
// ============================================================================
|
|
|
|
describe('Memory Bounds', () => {
|
|
it('should respect maxSize limit', () => {
|
|
const boundedCache = createStateDeltaCache({
|
|
maxSize: 5,
|
|
warmOnInit: false,
|
|
});
|
|
|
|
// Add more than maxSize entries
|
|
for (let i = 0; i < 10; i++) {
|
|
boundedCache.precompute({ n: i }, { n: i + 100 });
|
|
}
|
|
|
|
expect(boundedCache.size).toBe(5);
|
|
|
|
const metrics = boundedCache.getMetrics();
|
|
expect(metrics.evictions).toBe(5);
|
|
});
|
|
|
|
it('should handle very small maxSize', () => {
|
|
const tinyCache = createStateDeltaCache({
|
|
maxSize: 1,
|
|
warmOnInit: false,
|
|
});
|
|
|
|
tinyCache.precompute({ a: 1 }, { a: 2 });
|
|
tinyCache.precompute({ b: 1 }, { b: 2 });
|
|
|
|
expect(tinyCache.size).toBe(1);
|
|
});
|
|
|
|
it('should handle large number of operations', () => {
|
|
const largeCache = createStateDeltaCache({
|
|
maxSize: 1000,
|
|
warmOnInit: false,
|
|
});
|
|
|
|
// Add many entries
|
|
for (let i = 0; i < 500; i++) {
|
|
largeCache.precompute({ index: i }, { index: i + 1 });
|
|
}
|
|
|
|
expect(largeCache.size).toBe(500);
|
|
});
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Utility Function Tests
|
|
// ============================================================================
|
|
|
|
describe('Utility Functions', () => {
|
|
describe('getAgentStatusValues', () => {
|
|
it('should return all agent status values', () => {
|
|
const values = getAgentStatusValues();
|
|
expect(values).toContain('idle');
|
|
expect(values).toContain('running');
|
|
expect(values).toContain('completed');
|
|
expect(values).toContain('error');
|
|
expect(values).toContain('cancelled');
|
|
expect(values).toHaveLength(5);
|
|
});
|
|
});
|
|
|
|
describe('getProgressMilestones', () => {
|
|
it('should return all progress milestones', () => {
|
|
const values = getProgressMilestones();
|
|
expect(values).toContain(0);
|
|
expect(values).toContain(25);
|
|
expect(values).toContain(50);
|
|
expect(values).toContain(75);
|
|
expect(values).toContain(100);
|
|
expect(values).toHaveLength(5);
|
|
});
|
|
});
|
|
|
|
describe('getToolStatusValues', () => {
|
|
it('should return all tool status values', () => {
|
|
const values = getToolStatusValues();
|
|
expect(values).toContain('pending');
|
|
expect(values).toContain('executing');
|
|
expect(values).toContain('success');
|
|
expect(values).toContain('failure');
|
|
expect(values).toHaveLength(4);
|
|
});
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Integration with StateManager
|
|
// ============================================================================
|
|
|
|
describe('Integration Patterns', () => {
|
|
it('should produce valid JSON Patch operations', () => {
|
|
const cache = createStateDeltaCache({ warmOnInit: false });
|
|
|
|
const fromState = {
|
|
agent: { status: 'idle', id: 'agent-1' },
|
|
progress: { percent: 0, message: '' },
|
|
};
|
|
|
|
const toState = {
|
|
agent: { status: 'running', id: 'agent-1' },
|
|
progress: { percent: 25, message: 'Processing...' },
|
|
};
|
|
|
|
const delta = cache.getDelta(fromState, toState);
|
|
|
|
// Verify all operations are valid JSON Patch
|
|
for (const op of delta) {
|
|
expect(['add', 'remove', 'replace', 'move', 'copy', 'test']).toContain(
|
|
op.op
|
|
);
|
|
expect(typeof op.path).toBe('string');
|
|
if (op.op !== 'remove') {
|
|
expect(op).toHaveProperty('value');
|
|
}
|
|
}
|
|
});
|
|
|
|
it('should work with realistic agent state transitions', () => {
|
|
const cache = createStateDeltaCache({ warmOnInit: true });
|
|
|
|
// Simulate agent lifecycle
|
|
const states = [
|
|
{ agent: { status: 'idle' }, tool: { status: 'pending' }, progress: { percent: 0 } },
|
|
{ agent: { status: 'running' }, tool: { status: 'pending' }, progress: { percent: 0 } },
|
|
{ agent: { status: 'running' }, tool: { status: 'executing' }, progress: { percent: 25 } },
|
|
{ agent: { status: 'running' }, tool: { status: 'success' }, progress: { percent: 50 } },
|
|
{ agent: { status: 'running' }, tool: { status: 'pending' }, progress: { percent: 75 } },
|
|
{ agent: { status: 'completed' }, tool: { status: 'pending' }, progress: { percent: 100 } },
|
|
];
|
|
|
|
// Compute all transitions
|
|
for (let i = 0; i < states.length - 1; i++) {
|
|
const delta = cache.getDelta(states[i], states[i + 1]);
|
|
expect(delta.length).toBeGreaterThan(0);
|
|
}
|
|
|
|
// Check metrics show cache activity
|
|
const metrics = cache.getMetrics();
|
|
expect(metrics.hits + metrics.misses).toBe(states.length - 1);
|
|
});
|
|
});
|