ai-props 2.0.2 → 2.1.3

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