ai-props 0.1.2 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +5 -0
- package/CHANGELOG.md +15 -0
- package/README.md +345 -352
- package/dist/ai.d.ts +125 -0
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +199 -0
- package/dist/ai.js.map +1 -0
- package/dist/cache.d.ts +66 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +183 -0
- package/dist/cache.js.map +1 -0
- package/dist/generate.d.ts +69 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +221 -0
- package/dist/generate.js.map +1 -0
- package/dist/hoc.d.ts +164 -0
- package/dist/hoc.d.ts.map +1 -0
- package/dist/hoc.js +236 -0
- package/dist/hoc.js.map +1 -0
- package/dist/index.d.ts +14 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +152 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/validate.d.ts +58 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +253 -0
- package/dist/validate.js.map +1 -0
- package/package.json +16 -63
- package/src/ai.ts +264 -0
- package/src/cache.ts +216 -0
- package/src/generate.ts +276 -0
- package/src/hoc.ts +309 -0
- package/src/index.ts +66 -0
- package/src/types.ts +167 -0
- package/src/validate.ts +333 -0
- package/test/ai.test.ts +327 -0
- package/test/cache.test.ts +236 -0
- package/test/generate.test.ts +406 -0
- package/test/hoc.test.ts +411 -0
- package/test/validate.test.ts +324 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +15 -0
- package/LICENSE +0 -21
- package/dist/AI.d.ts +0 -27
- package/dist/AI.d.ts.map +0 -1
- package/dist/AI.test.d.ts +0 -2
- package/dist/AI.test.d.ts.map +0 -1
- package/dist/ai-props.es.js +0 -3697
- package/dist/ai-props.umd.js +0 -30
- package/dist/components/ErrorBoundary.d.ts +0 -17
- package/dist/components/ErrorBoundary.d.ts.map +0 -1
- package/dist/examples/BlogList.d.ts +0 -2
- package/dist/examples/BlogList.d.ts.map +0 -1
- package/dist/examples/BlogList.fixture.d.ts +0 -5
- package/dist/examples/BlogList.fixture.d.ts.map +0 -1
- package/dist/examples/HeroSection.d.ts +0 -2
- package/dist/examples/HeroSection.d.ts.map +0 -1
- package/dist/examples/HeroSection.fixture.d.ts +0 -5
- package/dist/examples/HeroSection.fixture.d.ts.map +0 -1
- package/dist/test/setup.d.ts +0 -2
- package/dist/test/setup.d.ts.map +0 -1
- package/dist/utils/schema.d.ts +0 -28
- package/dist/utils/schema.d.ts.map +0 -1
- package/dist/utils/styles.d.ts +0 -3
- package/dist/utils/styles.d.ts.map +0 -1
package/test/ai.test.ts
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for AI() wrapper and component functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
6
|
+
import {
|
|
7
|
+
AI,
|
|
8
|
+
createAIComponent,
|
|
9
|
+
definePropsSchema,
|
|
10
|
+
createComponentFactory,
|
|
11
|
+
composeAIComponents,
|
|
12
|
+
} from '../src/ai.js'
|
|
13
|
+
import { configureAIProps, resetConfig, clearCache } from '../src/index.js'
|
|
14
|
+
|
|
15
|
+
// Mock the generateObject function
|
|
16
|
+
vi.mock('ai-functions', () => ({
|
|
17
|
+
generateObject: vi.fn().mockImplementation(async ({ schema }) => {
|
|
18
|
+
// Generate mock data based on schema
|
|
19
|
+
const mockData: Record<string, unknown> = {}
|
|
20
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
21
|
+
if (typeof value === 'string') {
|
|
22
|
+
if (value.includes('(number)')) {
|
|
23
|
+
mockData[key] = 42
|
|
24
|
+
} else if (value.includes('(boolean)')) {
|
|
25
|
+
mockData[key] = true
|
|
26
|
+
} else {
|
|
27
|
+
mockData[key] = `generated-${key}`
|
|
28
|
+
}
|
|
29
|
+
} else if (Array.isArray(value)) {
|
|
30
|
+
mockData[key] = ['item1', 'item2']
|
|
31
|
+
} else if (typeof value === 'object') {
|
|
32
|
+
mockData[key] = { nested: 'value' }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { object: mockData }
|
|
36
|
+
}),
|
|
37
|
+
schema: vi.fn((s) => s),
|
|
38
|
+
}))
|
|
39
|
+
|
|
40
|
+
describe('AI()', () => {
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
resetConfig()
|
|
43
|
+
clearCache()
|
|
44
|
+
vi.clearAllMocks()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('creates an AI component wrapper', () => {
|
|
48
|
+
const UserCard = AI({
|
|
49
|
+
schema: {
|
|
50
|
+
name: 'User name',
|
|
51
|
+
bio: 'User biography',
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
expect(typeof UserCard).toBe('function')
|
|
56
|
+
expect(UserCard.schema).toBeDefined()
|
|
57
|
+
expect(UserCard.generateProps).toBeDefined()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('generates missing props', async () => {
|
|
61
|
+
const UserCard = AI({
|
|
62
|
+
schema: {
|
|
63
|
+
name: 'User name',
|
|
64
|
+
bio: 'User biography',
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const props = await UserCard({})
|
|
69
|
+
|
|
70
|
+
expect(props.name).toBe('generated-name')
|
|
71
|
+
expect(props.bio).toBe('generated-bio')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('preserves provided props', async () => {
|
|
75
|
+
const UserCard = AI({
|
|
76
|
+
schema: {
|
|
77
|
+
name: 'User name',
|
|
78
|
+
bio: 'User biography',
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const props = await UserCard({ name: 'John Doe' })
|
|
83
|
+
|
|
84
|
+
expect(props.name).toBe('John Doe')
|
|
85
|
+
expect(props.bio).toBe('generated-bio')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('applies defaults', async () => {
|
|
89
|
+
const UserCard = AI({
|
|
90
|
+
schema: {
|
|
91
|
+
name: 'User name',
|
|
92
|
+
role: 'User role',
|
|
93
|
+
},
|
|
94
|
+
defaults: {
|
|
95
|
+
role: 'member',
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const props = await UserCard({})
|
|
100
|
+
|
|
101
|
+
expect(props.role).toBe('member')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('throws for missing required props', async () => {
|
|
105
|
+
const UserCard = AI({
|
|
106
|
+
schema: {
|
|
107
|
+
name: 'User name',
|
|
108
|
+
email: 'Email address',
|
|
109
|
+
},
|
|
110
|
+
required: ['email'],
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
await expect(UserCard({ name: 'John' })).rejects.toThrow('Missing required props')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('excludes specified props from generation', async () => {
|
|
117
|
+
const UserCard = AI({
|
|
118
|
+
schema: {
|
|
119
|
+
name: 'User name',
|
|
120
|
+
avatar: 'Avatar URL',
|
|
121
|
+
internal: 'Internal data',
|
|
122
|
+
},
|
|
123
|
+
exclude: ['internal'],
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const props = await UserCard({})
|
|
127
|
+
|
|
128
|
+
expect(props.name).toBe('generated-name')
|
|
129
|
+
expect(props.avatar).toBe('generated-avatar')
|
|
130
|
+
// internal should not be generated
|
|
131
|
+
expect(props.internal).toBeUndefined()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('exposes generateProps method', async () => {
|
|
135
|
+
const UserCard = AI({
|
|
136
|
+
schema: {
|
|
137
|
+
name: 'User name',
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const props = await UserCard.generateProps({ name: 'Context Name' })
|
|
142
|
+
|
|
143
|
+
expect(props).toBeDefined()
|
|
144
|
+
expect(props.name).toBe('Context Name')
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe('createAIComponent', () => {
|
|
149
|
+
beforeEach(() => {
|
|
150
|
+
resetConfig()
|
|
151
|
+
clearCache()
|
|
152
|
+
vi.clearAllMocks()
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('creates typed AI component', async () => {
|
|
156
|
+
const ProductCard = createAIComponent<{
|
|
157
|
+
title: string
|
|
158
|
+
price: number
|
|
159
|
+
description: string
|
|
160
|
+
}>({
|
|
161
|
+
schema: {
|
|
162
|
+
title: 'Product title',
|
|
163
|
+
price: 'Price (number)',
|
|
164
|
+
description: 'Description',
|
|
165
|
+
},
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const props = await ProductCard({})
|
|
169
|
+
|
|
170
|
+
expect(typeof props.title).toBe('string')
|
|
171
|
+
expect(typeof props.price).toBe('number')
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe('definePropsSchema', () => {
|
|
176
|
+
it('returns schema unchanged', () => {
|
|
177
|
+
const schema = definePropsSchema({
|
|
178
|
+
name: 'User name',
|
|
179
|
+
email: 'Email address',
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
expect(schema).toEqual({
|
|
183
|
+
name: 'User name',
|
|
184
|
+
email: 'Email address',
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('preserves complex schemas', () => {
|
|
189
|
+
const schema = definePropsSchema({
|
|
190
|
+
user: {
|
|
191
|
+
name: 'Name',
|
|
192
|
+
profile: {
|
|
193
|
+
bio: 'Biography',
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
tags: ['Tag list'],
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
expect(schema.user).toBeDefined()
|
|
200
|
+
expect(schema.tags).toBeDefined()
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
describe('createComponentFactory', () => {
|
|
205
|
+
beforeEach(() => {
|
|
206
|
+
resetConfig()
|
|
207
|
+
clearCache()
|
|
208
|
+
vi.clearAllMocks()
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('creates component factory with generate method', async () => {
|
|
212
|
+
const factory = createComponentFactory({
|
|
213
|
+
schema: {
|
|
214
|
+
name: 'Product name',
|
|
215
|
+
price: 'Price (number)',
|
|
216
|
+
},
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
expect(factory.component).toBeDefined()
|
|
220
|
+
expect(factory.generate).toBeDefined()
|
|
221
|
+
expect(factory.generateMany).toBeDefined()
|
|
222
|
+
expect(factory.generateWith).toBeDefined()
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('generates single instance', async () => {
|
|
226
|
+
const factory = createComponentFactory({
|
|
227
|
+
schema: {
|
|
228
|
+
name: 'Product name',
|
|
229
|
+
},
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const product = await factory.generate({ category: 'electronics' })
|
|
233
|
+
|
|
234
|
+
expect(product.name).toBeDefined()
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('generates multiple instances', async () => {
|
|
238
|
+
const factory = createComponentFactory({
|
|
239
|
+
schema: {
|
|
240
|
+
name: 'Product name',
|
|
241
|
+
},
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
const products = await factory.generateMany([
|
|
245
|
+
{ category: 'electronics' },
|
|
246
|
+
{ category: 'clothing' },
|
|
247
|
+
{ category: 'food' },
|
|
248
|
+
])
|
|
249
|
+
|
|
250
|
+
expect(products).toHaveLength(3)
|
|
251
|
+
expect(products[0]?.name).toBeDefined()
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('generates with overrides', async () => {
|
|
255
|
+
const factory = createComponentFactory({
|
|
256
|
+
schema: {
|
|
257
|
+
name: 'Product name',
|
|
258
|
+
price: 'Price (number)',
|
|
259
|
+
},
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
const product = await factory.generateWith(
|
|
263
|
+
{ category: 'tech' },
|
|
264
|
+
{ price: 99 }
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
expect(product.price).toBe(99)
|
|
268
|
+
expect(product.name).toBeDefined()
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
describe('composeAIComponents', () => {
|
|
273
|
+
beforeEach(() => {
|
|
274
|
+
resetConfig()
|
|
275
|
+
clearCache()
|
|
276
|
+
vi.clearAllMocks()
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('composes multiple schemas', async () => {
|
|
280
|
+
const FullProfile = composeAIComponents({
|
|
281
|
+
user: {
|
|
282
|
+
schema: { name: 'User name' },
|
|
283
|
+
},
|
|
284
|
+
settings: {
|
|
285
|
+
schema: { theme: 'Theme preference' },
|
|
286
|
+
},
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
expect(FullProfile.schema).toBeDefined()
|
|
290
|
+
expect(typeof FullProfile).toBe('function')
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('generates composed props', async () => {
|
|
294
|
+
const FullProfile = composeAIComponents({
|
|
295
|
+
user: {
|
|
296
|
+
schema: { name: 'User name' },
|
|
297
|
+
},
|
|
298
|
+
settings: {
|
|
299
|
+
schema: { theme: 'Theme preference' },
|
|
300
|
+
},
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
const profile = await FullProfile({
|
|
304
|
+
user: { name: 'John' },
|
|
305
|
+
settings: {},
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
expect(profile.user.name).toBe('John')
|
|
309
|
+
expect(profile.settings.theme).toBeDefined()
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('generates all missing sections', async () => {
|
|
313
|
+
const FullProfile = composeAIComponents({
|
|
314
|
+
user: {
|
|
315
|
+
schema: { name: 'User name' },
|
|
316
|
+
},
|
|
317
|
+
prefs: {
|
|
318
|
+
schema: { notifications: 'Notifications (boolean)' },
|
|
319
|
+
},
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
const profile = await FullProfile({})
|
|
323
|
+
|
|
324
|
+
expect(profile.user).toBeDefined()
|
|
325
|
+
expect(profile.prefs).toBeDefined()
|
|
326
|
+
})
|
|
327
|
+
})
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for ai-props cache
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
6
|
+
import {
|
|
7
|
+
MemoryPropsCache,
|
|
8
|
+
LRUPropsCache,
|
|
9
|
+
createCacheKey,
|
|
10
|
+
getDefaultCache,
|
|
11
|
+
configureCache,
|
|
12
|
+
clearCache,
|
|
13
|
+
DEFAULT_CACHE_TTL,
|
|
14
|
+
} from '../src/cache.js'
|
|
15
|
+
|
|
16
|
+
describe('createCacheKey', () => {
|
|
17
|
+
it('creates consistent keys for same inputs', () => {
|
|
18
|
+
const key1 = createCacheKey({ name: 'string' }, { id: 1 })
|
|
19
|
+
const key2 = createCacheKey({ name: 'string' }, { id: 1 })
|
|
20
|
+
expect(key1).toBe(key2)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('creates different keys for different schemas', () => {
|
|
24
|
+
const key1 = createCacheKey({ name: 'string' })
|
|
25
|
+
const key2 = createCacheKey({ name: 'string', age: 'number' })
|
|
26
|
+
expect(key1).not.toBe(key2)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('creates different keys for different contexts', () => {
|
|
30
|
+
const key1 = createCacheKey({ name: 'string' }, { id: 1 })
|
|
31
|
+
const key2 = createCacheKey({ name: 'string' }, { id: 2 })
|
|
32
|
+
expect(key1).not.toBe(key2)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('handles undefined context', () => {
|
|
36
|
+
const key1 = createCacheKey({ name: 'string' })
|
|
37
|
+
const key2 = createCacheKey({ name: 'string' }, undefined)
|
|
38
|
+
expect(key1).toBe(key2)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('handles string schemas', () => {
|
|
42
|
+
const key = createCacheKey('user name')
|
|
43
|
+
expect(typeof key).toBe('string')
|
|
44
|
+
expect(key.length).toBeGreaterThan(0)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('creates same key regardless of object key order', () => {
|
|
48
|
+
const key1 = createCacheKey({ a: '1' }, { x: 1, y: 2 })
|
|
49
|
+
const key2 = createCacheKey({ a: '1' }, { y: 2, x: 1 })
|
|
50
|
+
expect(key1).toBe(key2)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('MemoryPropsCache', () => {
|
|
55
|
+
let cache: MemoryPropsCache
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
cache = new MemoryPropsCache()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('stores and retrieves values', () => {
|
|
62
|
+
cache.set('key1', { name: 'test' })
|
|
63
|
+
const entry = cache.get<{ name: string }>('key1')
|
|
64
|
+
|
|
65
|
+
expect(entry).toBeDefined()
|
|
66
|
+
expect(entry?.props.name).toBe('test')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('returns undefined for missing keys', () => {
|
|
70
|
+
const entry = cache.get('nonexistent')
|
|
71
|
+
expect(entry).toBeUndefined()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('deletes entries', () => {
|
|
75
|
+
cache.set('key1', { value: 1 })
|
|
76
|
+
expect(cache.delete('key1')).toBe(true)
|
|
77
|
+
expect(cache.get('key1')).toBeUndefined()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('clears all entries', () => {
|
|
81
|
+
cache.set('key1', { value: 1 })
|
|
82
|
+
cache.set('key2', { value: 2 })
|
|
83
|
+
cache.clear()
|
|
84
|
+
|
|
85
|
+
expect(cache.size).toBe(0)
|
|
86
|
+
expect(cache.get('key1')).toBeUndefined()
|
|
87
|
+
expect(cache.get('key2')).toBeUndefined()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('tracks size correctly', () => {
|
|
91
|
+
expect(cache.size).toBe(0)
|
|
92
|
+
|
|
93
|
+
cache.set('key1', { value: 1 })
|
|
94
|
+
expect(cache.size).toBe(1)
|
|
95
|
+
|
|
96
|
+
cache.set('key2', { value: 2 })
|
|
97
|
+
expect(cache.size).toBe(2)
|
|
98
|
+
|
|
99
|
+
cache.delete('key1')
|
|
100
|
+
expect(cache.size).toBe(1)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('respects TTL', async () => {
|
|
104
|
+
const shortCache = new MemoryPropsCache(50) // 50ms TTL
|
|
105
|
+
shortCache.set('key1', { value: 1 })
|
|
106
|
+
|
|
107
|
+
expect(shortCache.get('key1')).toBeDefined()
|
|
108
|
+
|
|
109
|
+
// Wait for expiration
|
|
110
|
+
await new Promise(resolve => setTimeout(resolve, 60))
|
|
111
|
+
|
|
112
|
+
expect(shortCache.get('key1')).toBeUndefined()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('cleans up expired entries', async () => {
|
|
116
|
+
const shortCache = new MemoryPropsCache(50)
|
|
117
|
+
shortCache.set('key1', { value: 1 })
|
|
118
|
+
shortCache.set('key2', { value: 2 })
|
|
119
|
+
|
|
120
|
+
await new Promise(resolve => setTimeout(resolve, 60))
|
|
121
|
+
|
|
122
|
+
const removed = shortCache.cleanup()
|
|
123
|
+
expect(removed).toBe(2)
|
|
124
|
+
expect(shortCache.size).toBe(0)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('stores entry metadata', () => {
|
|
128
|
+
cache.set('key1', { value: 1 })
|
|
129
|
+
const entry = cache.get('key1')
|
|
130
|
+
|
|
131
|
+
expect(entry).toBeDefined()
|
|
132
|
+
expect(entry?.key).toBe('key1')
|
|
133
|
+
expect(entry?.timestamp).toBeDefined()
|
|
134
|
+
expect(typeof entry?.timestamp).toBe('number')
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
describe('LRUPropsCache', () => {
|
|
139
|
+
let cache: LRUPropsCache
|
|
140
|
+
|
|
141
|
+
beforeEach(() => {
|
|
142
|
+
cache = new LRUPropsCache(3) // Max 3 entries
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('stores and retrieves values', () => {
|
|
146
|
+
cache.set('key1', { value: 1 })
|
|
147
|
+
const entry = cache.get<{ value: number }>('key1')
|
|
148
|
+
|
|
149
|
+
expect(entry?.props.value).toBe(1)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('evicts oldest entries when at capacity', () => {
|
|
153
|
+
cache.set('key1', { value: 1 })
|
|
154
|
+
cache.set('key2', { value: 2 })
|
|
155
|
+
cache.set('key3', { value: 3 })
|
|
156
|
+
cache.set('key4', { value: 4 }) // Should evict key1
|
|
157
|
+
|
|
158
|
+
expect(cache.get('key1')).toBeUndefined()
|
|
159
|
+
expect(cache.get('key2')).toBeDefined()
|
|
160
|
+
expect(cache.get('key3')).toBeDefined()
|
|
161
|
+
expect(cache.get('key4')).toBeDefined()
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('updates LRU order on access', () => {
|
|
165
|
+
cache.set('key1', { value: 1 })
|
|
166
|
+
cache.set('key2', { value: 2 })
|
|
167
|
+
cache.set('key3', { value: 3 })
|
|
168
|
+
|
|
169
|
+
// Access key1, making it most recently used
|
|
170
|
+
cache.get('key1')
|
|
171
|
+
|
|
172
|
+
// Add key4, should evict key2 (oldest now)
|
|
173
|
+
cache.set('key4', { value: 4 })
|
|
174
|
+
|
|
175
|
+
expect(cache.get('key1')).toBeDefined() // Still there
|
|
176
|
+
expect(cache.get('key2')).toBeUndefined() // Evicted
|
|
177
|
+
expect(cache.get('key3')).toBeDefined()
|
|
178
|
+
expect(cache.get('key4')).toBeDefined()
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('respects TTL in addition to capacity', async () => {
|
|
182
|
+
const shortCache = new LRUPropsCache(10, 50) // 50ms TTL
|
|
183
|
+
shortCache.set('key1', { value: 1 })
|
|
184
|
+
|
|
185
|
+
await new Promise(resolve => setTimeout(resolve, 60))
|
|
186
|
+
|
|
187
|
+
expect(shortCache.get('key1')).toBeUndefined()
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('maintains correct size', () => {
|
|
191
|
+
cache.set('key1', { value: 1 })
|
|
192
|
+
cache.set('key2', { value: 2 })
|
|
193
|
+
cache.set('key3', { value: 3 })
|
|
194
|
+
|
|
195
|
+
expect(cache.size).toBe(3)
|
|
196
|
+
|
|
197
|
+
cache.set('key4', { value: 4 })
|
|
198
|
+
expect(cache.size).toBe(3) // Still 3 after eviction
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe('Default cache management', () => {
|
|
203
|
+
beforeEach(() => {
|
|
204
|
+
clearCache()
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('getDefaultCache returns singleton', () => {
|
|
208
|
+
const cache1 = getDefaultCache()
|
|
209
|
+
const cache2 = getDefaultCache()
|
|
210
|
+
expect(cache1).toBe(cache2)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('configureCache creates new instance with TTL', () => {
|
|
214
|
+
const oldCache = getDefaultCache()
|
|
215
|
+
configureCache(1000)
|
|
216
|
+
const newCache = getDefaultCache()
|
|
217
|
+
|
|
218
|
+
// Should be a different instance
|
|
219
|
+
expect(newCache).not.toBe(oldCache)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('clearCache empties the cache', () => {
|
|
223
|
+
const cache = getDefaultCache()
|
|
224
|
+
cache.set('key1', { value: 1 })
|
|
225
|
+
|
|
226
|
+
clearCache()
|
|
227
|
+
|
|
228
|
+
expect(cache.size).toBe(0)
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
describe('DEFAULT_CACHE_TTL', () => {
|
|
233
|
+
it('is 5 minutes in milliseconds', () => {
|
|
234
|
+
expect(DEFAULT_CACHE_TTL).toBe(5 * 60 * 1000)
|
|
235
|
+
})
|
|
236
|
+
})
|