mdi-llmkit 0.1.0 → 1.0.1
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/README.md +116 -34
- package/dist/src/comparison/compareLists.d.ts +97 -0
- package/dist/src/comparison/compareLists.js +375 -0
- package/dist/src/comparison/index.d.ts +1 -0
- package/dist/src/comparison/index.js +1 -0
- package/dist/src/gptApi/functions.d.ts +21 -0
- package/dist/src/gptApi/functions.js +154 -0
- package/dist/src/gptApi/gptConversation.d.ts +43 -0
- package/dist/src/gptApi/gptConversation.js +146 -0
- package/dist/src/gptApi/index.d.ts +3 -0
- package/dist/src/gptApi/index.js +3 -0
- package/dist/src/gptApi/jsonSchemaFormat.d.ts +14 -0
- package/dist/src/gptApi/jsonSchemaFormat.js +198 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +3 -0
- package/dist/src/jsonSurgery/jsonSurgery.d.ts +81 -0
- package/dist/src/jsonSurgery/jsonSurgery.js +776 -0
- package/dist/src/jsonSurgery/placemarkedJSON.d.ts +57 -0
- package/dist/src/jsonSurgery/placemarkedJSON.js +151 -0
- package/dist/tests/comparison/compareLists.test.d.ts +1 -0
- package/dist/tests/comparison/compareLists.test.js +434 -0
- package/dist/tests/gptApi/gptConversation.test.d.ts +1 -0
- package/dist/tests/gptApi/gptConversation.test.js +157 -0
- package/dist/tests/gptApi/gptSubmit.test.d.ts +1 -0
- package/dist/tests/gptApi/gptSubmit.test.js +161 -0
- package/dist/tests/gptApi/jsonSchemaFormat.test.d.ts +1 -0
- package/dist/tests/gptApi/jsonSchemaFormat.test.js +372 -0
- package/dist/tests/jsonSurgery/jsonSurgery.test.d.ts +1 -0
- package/dist/tests/jsonSurgery/jsonSurgery.test.js +729 -0
- package/dist/tests/jsonSurgery/placemarkedJSON.test.d.ts +1 -0
- package/dist/tests/jsonSurgery/placemarkedJSON.test.js +209 -0
- package/dist/tests/setupEnv.d.ts +1 -0
- package/dist/tests/setupEnv.js +4 -0
- package/dist/tests/subpathExports.test.d.ts +1 -0
- package/dist/tests/subpathExports.test.js +47 -0
- package/package.json +18 -5
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
import { OpenAI } from 'openai';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { jsonSurgery, } from '../../src/jsonSurgery/jsonSurgery.js';
|
|
4
|
+
const OPENAI_API_KEY = process.env.OPENAI_API_KEY?.trim();
|
|
5
|
+
if (!OPENAI_API_KEY) {
|
|
6
|
+
throw new Error('OPENAI_API_KEY is required for jsonSurgery live API tests. Configure your test environment to provide it.');
|
|
7
|
+
}
|
|
8
|
+
const createClient = () => new OpenAI({
|
|
9
|
+
apiKey: OPENAI_API_KEY,
|
|
10
|
+
});
|
|
11
|
+
describe.concurrent('jsonSurgery (live API)', () => {
|
|
12
|
+
describe('atomic operations', () => {
|
|
13
|
+
it('applies a simple scalar update without mutating original input', async () => {
|
|
14
|
+
const original = {
|
|
15
|
+
id: 'task-1',
|
|
16
|
+
status: 'pending',
|
|
17
|
+
notes: ['created'],
|
|
18
|
+
};
|
|
19
|
+
const result = await jsonSurgery(createClient(), original, 'Set the status field to "approved". Do not change any other fields.', {
|
|
20
|
+
schemaDescription: `
|
|
21
|
+
{
|
|
22
|
+
"type": "object",
|
|
23
|
+
"properties": {
|
|
24
|
+
"id": { "type": "string" },
|
|
25
|
+
"status": { "type": "string" },
|
|
26
|
+
"notes": {
|
|
27
|
+
"type": "array",
|
|
28
|
+
"items": { "type": "string" }
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"required": ["id", "status", "notes"],
|
|
32
|
+
"additionalProperties": false
|
|
33
|
+
}
|
|
34
|
+
`,
|
|
35
|
+
});
|
|
36
|
+
expect(result.status).toBe('approved');
|
|
37
|
+
expect(result.id).toBe(original.id);
|
|
38
|
+
expect(result.notes).toEqual(original.notes);
|
|
39
|
+
// Expect the original to be unmodified.
|
|
40
|
+
expect(original).not.toBe(result);
|
|
41
|
+
expect(original.status).toBe('pending');
|
|
42
|
+
}, 180000);
|
|
43
|
+
it('renames a nested property while preserving value', async () => {
|
|
44
|
+
const original = {
|
|
45
|
+
address: {
|
|
46
|
+
zip: '98101',
|
|
47
|
+
city: 'Seattle',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
const result = await jsonSurgery(createClient(), original, 'Inside address, rename the key "zip" to "postalCode" and keep the same value. Do not change anything else.');
|
|
51
|
+
expect(result.address.postalCode).toBe('98101');
|
|
52
|
+
expect(result.address.city).toBe('Seattle');
|
|
53
|
+
expect('zip' in result.address).toBe(false);
|
|
54
|
+
// Expect the original to be unmodified.
|
|
55
|
+
expect(original).not.toBe(result);
|
|
56
|
+
expect(original.address.zip).toBe('98101');
|
|
57
|
+
}, 180000);
|
|
58
|
+
it('handles array insert/append style updates', async () => {
|
|
59
|
+
const original = {
|
|
60
|
+
tags: ['alpha', 'beta'],
|
|
61
|
+
};
|
|
62
|
+
const result = await jsonSurgery(createClient(), original, 'In the tags array, insert "urgent" at the beginning and append "done" at the end. Keep existing tags.');
|
|
63
|
+
expect(Array.isArray(result.tags)).toBe(true);
|
|
64
|
+
expect(result.tags[0]).toBe('urgent');
|
|
65
|
+
expect(result.tags[result.tags.length - 1]).toBe('done');
|
|
66
|
+
expect(result.tags).toContain('alpha');
|
|
67
|
+
expect(result.tags).toContain('beta');
|
|
68
|
+
// Expect the original to be unmodified.
|
|
69
|
+
expect(original).not.toBe(result);
|
|
70
|
+
expect(original.tags).toEqual(['alpha', 'beta']);
|
|
71
|
+
}, 180000);
|
|
72
|
+
it('builds nested object structure from plain-English instructions', async () => {
|
|
73
|
+
const original = {
|
|
74
|
+
profile: {},
|
|
75
|
+
};
|
|
76
|
+
const result = await jsonSurgery(createClient(), original, 'Create profile.contact with email "person@example.com" and phone "555-0100".');
|
|
77
|
+
expect(result.profile).toBeTruthy();
|
|
78
|
+
expect(result.profile.contact).toBeTruthy();
|
|
79
|
+
expect(result.profile.contact.email).toBe('person@example.com');
|
|
80
|
+
expect(result.profile.contact.phone).toBe('555-0100');
|
|
81
|
+
// Expect the original to be unmodified.
|
|
82
|
+
expect(original).not.toBe(result);
|
|
83
|
+
expect(original.profile).toEqual({});
|
|
84
|
+
}, 180000);
|
|
85
|
+
it('honors skippedKeys while still modifying visible fields', async () => {
|
|
86
|
+
const original = {
|
|
87
|
+
name: 'Widget',
|
|
88
|
+
secretToken: 'SECRET-123',
|
|
89
|
+
audit: {
|
|
90
|
+
createdBy: 'u1',
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
const result = await jsonSurgery(createClient(), original, 'Change name to "Widget Pro" and set audit.createdBy to "u2".', { skippedKeys: ['secretToken'] });
|
|
94
|
+
expect(result.name).toBe('Widget Pro');
|
|
95
|
+
expect(result.audit.createdBy).toBe('u2');
|
|
96
|
+
expect(result.secretToken).toBe('SECRET-123');
|
|
97
|
+
// Expect the original to be unmodified.
|
|
98
|
+
expect(original).not.toBe(result);
|
|
99
|
+
expect(original.name).toBe('Widget');
|
|
100
|
+
expect(original.audit.createdBy).toBe('u1');
|
|
101
|
+
expect(original.secretToken).toBe('SECRET-123');
|
|
102
|
+
}, 180000);
|
|
103
|
+
it('follows schema-constrained type updates for primitives', async () => {
|
|
104
|
+
const original = {
|
|
105
|
+
age: 40,
|
|
106
|
+
active: true,
|
|
107
|
+
};
|
|
108
|
+
const result = await jsonSurgery(createClient(), original, 'Set age to 41 and active to false.', {
|
|
109
|
+
schemaDescription: `
|
|
110
|
+
{
|
|
111
|
+
"type": "object",
|
|
112
|
+
"properties": {
|
|
113
|
+
"age": { "type": "number" },
|
|
114
|
+
"active": { "type": "boolean" }
|
|
115
|
+
},
|
|
116
|
+
"required": ["age", "active"],
|
|
117
|
+
"additionalProperties": false
|
|
118
|
+
}
|
|
119
|
+
`,
|
|
120
|
+
});
|
|
121
|
+
expect(result.age).toBe(41);
|
|
122
|
+
expect(typeof result.age).toBe('number');
|
|
123
|
+
expect(result.active).toBe(false);
|
|
124
|
+
expect(typeof result.active).toBe('boolean');
|
|
125
|
+
// Expect the original to be unmodified.
|
|
126
|
+
expect(original).not.toBe(result);
|
|
127
|
+
expect(original.age).toBe(40);
|
|
128
|
+
expect(original.active).toBe(true);
|
|
129
|
+
}, 180000);
|
|
130
|
+
it('removes requested properties without disturbing unrelated fields', async () => {
|
|
131
|
+
const original = {
|
|
132
|
+
id: 'rec-1',
|
|
133
|
+
name: 'Sample',
|
|
134
|
+
obsoleteField: 'remove-me',
|
|
135
|
+
metadata: {
|
|
136
|
+
owner: 'team-a',
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
const result = await jsonSurgery(createClient(), original, 'Delete obsoleteField. Keep id, name, and metadata unchanged.');
|
|
140
|
+
expect('obsoleteField' in result).toBe(false);
|
|
141
|
+
expect(result.id).toBe(original.id);
|
|
142
|
+
expect(result.name).toBe(original.name);
|
|
143
|
+
expect(result.metadata).toEqual(original.metadata);
|
|
144
|
+
// Expect the original to be unmodified.
|
|
145
|
+
expect(original).not.toBe(result);
|
|
146
|
+
expect(original.obsoleteField).toBe('remove-me');
|
|
147
|
+
}, 180000);
|
|
148
|
+
it('keeps object effectively unchanged for explicit no-op instructions', async () => {
|
|
149
|
+
const original = {
|
|
150
|
+
status: 'complete',
|
|
151
|
+
tags: ['alpha', 'beta'],
|
|
152
|
+
details: {
|
|
153
|
+
priority: 2,
|
|
154
|
+
archived: false,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
const result = await jsonSurgery(createClient(), original, 'Do not make any modifications. Confirm the object already satisfies the request as-is.');
|
|
158
|
+
expect(result).toEqual(original);
|
|
159
|
+
// Expect the original to be unmodified.
|
|
160
|
+
expect(original).not.toBe(result);
|
|
161
|
+
expect(original.status).toBe('complete');
|
|
162
|
+
expect(original.tags).toEqual(['alpha', 'beta']);
|
|
163
|
+
expect(original.details).toEqual({ priority: 2, archived: false });
|
|
164
|
+
}, 180000);
|
|
165
|
+
it('handles combined rename and value update in one request', async () => {
|
|
166
|
+
const original = {
|
|
167
|
+
profile: {
|
|
168
|
+
first_name: 'Sam',
|
|
169
|
+
last_name: 'Lee',
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
const result = await jsonSurgery(createClient(), original, 'In profile, rename first_name to firstName and update last_name to "Li".');
|
|
173
|
+
expect(result.profile.firstName).toBe('Sam');
|
|
174
|
+
expect(result.profile.last_name).toBe('Li');
|
|
175
|
+
expect('first_name' in result.profile).toBe(false);
|
|
176
|
+
// Expect the original to be unmodified.
|
|
177
|
+
expect(original).not.toBe(result);
|
|
178
|
+
expect(original.profile).toEqual({
|
|
179
|
+
first_name: 'Sam',
|
|
180
|
+
last_name: 'Lee',
|
|
181
|
+
});
|
|
182
|
+
}, 180000);
|
|
183
|
+
it('supports order-sensitive array edits for checklist-style prompts', async () => {
|
|
184
|
+
const original = {
|
|
185
|
+
steps: ['draft', 'review', 'publish'],
|
|
186
|
+
};
|
|
187
|
+
const result = await jsonSurgery(createClient(), original, 'In steps, insert "plan" at the beginning and remove "review". Keep the remaining order intact.');
|
|
188
|
+
expect(Array.isArray(result.steps)).toBe(true);
|
|
189
|
+
expect(result.steps[0]).toBe('plan');
|
|
190
|
+
expect(result.steps).not.toContain('review');
|
|
191
|
+
expect(result.steps).toContain('draft');
|
|
192
|
+
expect(result.steps).toContain('publish');
|
|
193
|
+
expect(result.steps.indexOf('draft')).toBeLessThan(result.steps.indexOf('publish'));
|
|
194
|
+
// Expect the original to be unmodified.
|
|
195
|
+
expect(original).not.toBe(result);
|
|
196
|
+
expect(original.steps).toEqual(['draft', 'review', 'publish']);
|
|
197
|
+
}, 180000);
|
|
198
|
+
});
|
|
199
|
+
describe('onValidateBeforeReturn', () => {
|
|
200
|
+
it('should validate if errors array is missing', async () => {
|
|
201
|
+
const original = {
|
|
202
|
+
name: 'Test Product',
|
|
203
|
+
price: 100,
|
|
204
|
+
};
|
|
205
|
+
// Validator returns an empty object.
|
|
206
|
+
// No correction, no errors.
|
|
207
|
+
const onValidateBeforeReturn = async (obj) => ({});
|
|
208
|
+
const result = await jsonSurgery(createClient(), original, 'Increase the price by 10%', { onValidateBeforeReturn });
|
|
209
|
+
expect(result.price).toBe(110);
|
|
210
|
+
// Expect the original to be unmodified.
|
|
211
|
+
expect(original).not.toBe(result);
|
|
212
|
+
expect(original.price).toBe(100);
|
|
213
|
+
expect(original.name).toBe('Test Product');
|
|
214
|
+
}, 180000);
|
|
215
|
+
it('should validate if errors array is empty', async () => {
|
|
216
|
+
const original = {
|
|
217
|
+
name: 'Test Product',
|
|
218
|
+
price: 100,
|
|
219
|
+
};
|
|
220
|
+
// Validator returns an empty object.
|
|
221
|
+
// No correction, no errors.
|
|
222
|
+
const onValidateBeforeReturn = async (obj) => ({
|
|
223
|
+
errors: [],
|
|
224
|
+
});
|
|
225
|
+
const result = await jsonSurgery(createClient(), original, 'Increase the price by 10%', { onValidateBeforeReturn });
|
|
226
|
+
expect(result.price).toBe(110);
|
|
227
|
+
// Expect the original to be unmodified.
|
|
228
|
+
expect(original).not.toBe(result);
|
|
229
|
+
expect(original.price).toBe(100);
|
|
230
|
+
expect(original.name).toBe('Test Product');
|
|
231
|
+
}, 180000);
|
|
232
|
+
it('should apply changes enumerated in validation errors', async () => {
|
|
233
|
+
const original = {
|
|
234
|
+
name: 'Test Product',
|
|
235
|
+
price: 100,
|
|
236
|
+
};
|
|
237
|
+
// Validator returns an empty object.
|
|
238
|
+
// No correction, no errors.
|
|
239
|
+
const onValidateBeforeReturn = async (obj) => {
|
|
240
|
+
const errors = [];
|
|
241
|
+
if (!obj.id) {
|
|
242
|
+
errors.push('Object needs an `id` field. Set its value to `001`.');
|
|
243
|
+
}
|
|
244
|
+
if (!obj.date) {
|
|
245
|
+
errors.push('Object needs a `date` field. Set its value to `2024-01-01`.');
|
|
246
|
+
}
|
|
247
|
+
return { errors };
|
|
248
|
+
};
|
|
249
|
+
const result = await jsonSurgery(createClient(), original, 'Increase the price by 10%', { onValidateBeforeReturn });
|
|
250
|
+
expect(result.price).toBe(110);
|
|
251
|
+
expect(result.id).toBe('001');
|
|
252
|
+
expect(result.date).toBe('2024-01-01');
|
|
253
|
+
// Expect the original to be unmodified.
|
|
254
|
+
expect(original).not.toBe(result);
|
|
255
|
+
expect(original.price).toBe(100);
|
|
256
|
+
expect(original.name).toBe('Test Product');
|
|
257
|
+
}, 180000);
|
|
258
|
+
it('should use corrected object', async () => {
|
|
259
|
+
const original = {
|
|
260
|
+
name: 'Test Product',
|
|
261
|
+
price: 100,
|
|
262
|
+
};
|
|
263
|
+
// Validator returns an empty object.
|
|
264
|
+
// No correction, no errors.
|
|
265
|
+
const onValidateBeforeReturn = async (obj) => {
|
|
266
|
+
obj.id = '001';
|
|
267
|
+
obj.date = '2024-01-01';
|
|
268
|
+
return { objCorrected: obj };
|
|
269
|
+
};
|
|
270
|
+
const result = await jsonSurgery(createClient(), original, 'Increase the price by 10%', { onValidateBeforeReturn });
|
|
271
|
+
expect(result.price).toBe(110);
|
|
272
|
+
expect(result.id).toBe('001');
|
|
273
|
+
expect(result.date).toBe('2024-01-01');
|
|
274
|
+
// Expect the original to be unmodified.
|
|
275
|
+
expect(original).not.toBe(result);
|
|
276
|
+
expect(original.price).toBe(100);
|
|
277
|
+
expect(original.name).toBe('Test Product');
|
|
278
|
+
}, 180000);
|
|
279
|
+
it('should use corrected object and also apply enumerated changes', async () => {
|
|
280
|
+
const original = {
|
|
281
|
+
name: 'Test Product',
|
|
282
|
+
price: 100,
|
|
283
|
+
};
|
|
284
|
+
// Validator returns an empty object.
|
|
285
|
+
// No correction, no errors.
|
|
286
|
+
const onValidateBeforeReturn = async (obj) => {
|
|
287
|
+
const errors = [];
|
|
288
|
+
obj.id = '001';
|
|
289
|
+
if (!obj.date) {
|
|
290
|
+
errors.push('Object needs a `date` field. Set its value to `2024-01-01`.');
|
|
291
|
+
}
|
|
292
|
+
return { objCorrected: obj, errors };
|
|
293
|
+
};
|
|
294
|
+
const result = await jsonSurgery(createClient(), original, 'Increase the price by 10%', { onValidateBeforeReturn });
|
|
295
|
+
expect(result.price).toBe(110);
|
|
296
|
+
expect(result.id).toBe('001');
|
|
297
|
+
expect(result.date).toBe('2024-01-01');
|
|
298
|
+
// Expect the original to be unmodified.
|
|
299
|
+
expect(original).not.toBe(result);
|
|
300
|
+
expect(original.price).toBe(100);
|
|
301
|
+
expect(original.name).toBe('Test Product');
|
|
302
|
+
}, 180000);
|
|
303
|
+
it('should be okay if onValidateBeforeReturn returns undefined', async () => {
|
|
304
|
+
const original = {
|
|
305
|
+
name: 'Test Product',
|
|
306
|
+
price: 100,
|
|
307
|
+
};
|
|
308
|
+
// Validator returns an empty object.
|
|
309
|
+
// No correction, no errors.
|
|
310
|
+
const onValidateBeforeReturn = async (obj) => undefined;
|
|
311
|
+
const result = await jsonSurgery(createClient(), original, 'Increase the price by 10%', { onValidateBeforeReturn });
|
|
312
|
+
expect(result.price).toBe(110);
|
|
313
|
+
// Expect the original to be unmodified.
|
|
314
|
+
expect(original).not.toBe(result);
|
|
315
|
+
expect(original.price).toBe(100);
|
|
316
|
+
}, 180000);
|
|
317
|
+
});
|
|
318
|
+
describe('onWorkInProgress', () => {
|
|
319
|
+
it('should call onWorkInProgress after each iteration', async () => {
|
|
320
|
+
const original = {
|
|
321
|
+
name: 'Test Product',
|
|
322
|
+
price: 100,
|
|
323
|
+
};
|
|
324
|
+
let workInProgressCallCount = 0;
|
|
325
|
+
const onWorkInProgress = async (obj) => {
|
|
326
|
+
workInProgressCallCount++;
|
|
327
|
+
};
|
|
328
|
+
const result = await jsonSurgery(createClient(), original, 'Increase the price by 10%', { onWorkInProgress });
|
|
329
|
+
expect(result.price).toBe(110);
|
|
330
|
+
expect(workInProgressCallCount).toEqual(1);
|
|
331
|
+
}, 180000);
|
|
332
|
+
it('should replace object when onWorkInProgress returns a new object', async () => {
|
|
333
|
+
const original = {
|
|
334
|
+
name: 'Test Product',
|
|
335
|
+
price: 100,
|
|
336
|
+
};
|
|
337
|
+
let workInProgressCallCount = 0;
|
|
338
|
+
const onWorkInProgress = async (obj) => {
|
|
339
|
+
workInProgressCallCount++;
|
|
340
|
+
return {
|
|
341
|
+
...obj,
|
|
342
|
+
id: '001',
|
|
343
|
+
date: '2024-01-01',
|
|
344
|
+
};
|
|
345
|
+
};
|
|
346
|
+
const result = await jsonSurgery(createClient(), original, 'Increase the price by 10%', { onWorkInProgress });
|
|
347
|
+
expect(result.price).toBe(110);
|
|
348
|
+
expect(result.id).toBe('001');
|
|
349
|
+
expect(result.date).toBe('2024-01-01');
|
|
350
|
+
expect(workInProgressCallCount).toEqual(1);
|
|
351
|
+
}, 180000);
|
|
352
|
+
it('should propagate an error from onWorkInProgress', async () => {
|
|
353
|
+
const original = {
|
|
354
|
+
name: 'Test Product',
|
|
355
|
+
price: 100,
|
|
356
|
+
};
|
|
357
|
+
const uniqueErrorString = 'Super unique error string very recognize much distinct wow';
|
|
358
|
+
const onWorkInProgress = async (obj) => {
|
|
359
|
+
throw new Error(uniqueErrorString);
|
|
360
|
+
};
|
|
361
|
+
await expect(jsonSurgery(createClient(), original, 'Increase the price by 10%', {
|
|
362
|
+
onWorkInProgress,
|
|
363
|
+
})).rejects.toThrow(uniqueErrorString);
|
|
364
|
+
}, 180000);
|
|
365
|
+
});
|
|
366
|
+
describe('bulk operations', () => {
|
|
367
|
+
it('should copy a nested object in a dict in a single iteration', async () => {
|
|
368
|
+
const original = {
|
|
369
|
+
ernie: {
|
|
370
|
+
species: 'muppet',
|
|
371
|
+
gender: 'male',
|
|
372
|
+
address: {
|
|
373
|
+
street_name: 'Sesame St',
|
|
374
|
+
house_number: '123',
|
|
375
|
+
unit: { floor: 1, number: '1D' },
|
|
376
|
+
city: 'Sesame City',
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
let numTimesWorkInProgressCalled = 0;
|
|
381
|
+
const onWorkInProgress = async (obj) => {
|
|
382
|
+
numTimesWorkInProgressCalled++;
|
|
383
|
+
};
|
|
384
|
+
// NOTE: In order to make sure the AI understands that it's supposed to use the
|
|
385
|
+
// bulk "copy" operation, we need to explicitly tell it to do so.
|
|
386
|
+
const result = await jsonSurgery(createClient(), original, 'Create a new character, `bert`, by copying the entire `ernie` object ' +
|
|
387
|
+
'using the bulk "copy" action. Keep all data the same.', { onWorkInProgress });
|
|
388
|
+
// Expect the original to have 1 character, and the result to have 2.
|
|
389
|
+
expect(Object.keys(original)).toHaveLength(1);
|
|
390
|
+
expect(Object.keys(result)).toHaveLength(2);
|
|
391
|
+
// Expect the result to have both `ernie` and `bert` with the same data.
|
|
392
|
+
expect(result.ernie).toEqual(original.ernie);
|
|
393
|
+
expect(result.bert).toEqual(original.ernie);
|
|
394
|
+
// Expect the work in progress callback to have only been called once.
|
|
395
|
+
// Without the bulk operator, it would have been called at least 3 times minimum.
|
|
396
|
+
expect(numTimesWorkInProgressCalled).toEqual(1);
|
|
397
|
+
}, 180000);
|
|
398
|
+
it('should append a copy of a nested object in a list in 2 or fewer iterations', async () => {
|
|
399
|
+
const original = {
|
|
400
|
+
sesame_street_characters: [
|
|
401
|
+
{
|
|
402
|
+
name: 'Ernie',
|
|
403
|
+
species: 'muppet',
|
|
404
|
+
gender: 'male',
|
|
405
|
+
address: {
|
|
406
|
+
street_name: 'Sesame St',
|
|
407
|
+
house_number: '123',
|
|
408
|
+
unit: { floor: 1, number: '1D' },
|
|
409
|
+
city: 'Sesame City',
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
name: 'Oscar',
|
|
414
|
+
species: 'muppet',
|
|
415
|
+
gender: 'male',
|
|
416
|
+
address: {
|
|
417
|
+
street_name: 'Sesame St',
|
|
418
|
+
house_number: 'none',
|
|
419
|
+
unit: { floor: 0, number: 'none' },
|
|
420
|
+
city: 'Sesame City',
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
};
|
|
425
|
+
let numTimesWorkInProgressCalled = 0;
|
|
426
|
+
const onWorkInProgress = async (obj) => {
|
|
427
|
+
numTimesWorkInProgressCalled++;
|
|
428
|
+
};
|
|
429
|
+
// NOTE: In order to make sure the AI understands that it's supposed to use the
|
|
430
|
+
// bulk "copy" operation, we need to explicitly use the word "copy" in the instructions.
|
|
431
|
+
const result = await jsonSurgery(createClient(), original, 'Create a new character, "Bert", by copying the entire "Ernie" object ' +
|
|
432
|
+
'using the bulk "copy" action. Keep all data the same (except the name, of course). ' +
|
|
433
|
+
'Append Bert to the end of the sesame_street_characters array.', { onWorkInProgress });
|
|
434
|
+
// Expect the result to have both `Ernie` and `Bert` with the same data.
|
|
435
|
+
expect(original.sesame_street_characters).toHaveLength(2);
|
|
436
|
+
expect(result.sesame_street_characters).toHaveLength(3);
|
|
437
|
+
expect(result.sesame_street_characters[0]).toEqual(original.sesame_street_characters[0]);
|
|
438
|
+
const expectBert = JSON.parse(JSON.stringify(original.sesame_street_characters[0]));
|
|
439
|
+
expectBert.name = 'Bert';
|
|
440
|
+
expect(result.sesame_street_characters[2]).toEqual(expectBert);
|
|
441
|
+
// Expect the work in progress callback to have been called 2 times or fewer.
|
|
442
|
+
// It's possible that the AI will be smart enough to do this in a single iteration,
|
|
443
|
+
// but we want to allow for the possibility that it'll use its first iteration
|
|
444
|
+
// to create the copy and its second one to name it Bert. The important thing is that
|
|
445
|
+
// if it *doesn't* use the copy operation, then the smallest number of steps it'll be
|
|
446
|
+
// able to use is 3, so this test will detect that.
|
|
447
|
+
expect(numTimesWorkInProgressCalled).toBeLessThanOrEqual(2);
|
|
448
|
+
}, 180000);
|
|
449
|
+
it('should insert a copy of a nested object in a list in 2 or fewer iterations', async () => {
|
|
450
|
+
const original = {
|
|
451
|
+
sesame_street_characters: [
|
|
452
|
+
{
|
|
453
|
+
name: 'Ernie',
|
|
454
|
+
species: 'muppet',
|
|
455
|
+
gender: 'male',
|
|
456
|
+
address: {
|
|
457
|
+
street_name: 'Sesame St',
|
|
458
|
+
house_number: '123',
|
|
459
|
+
unit: { floor: 1, number: '1D' },
|
|
460
|
+
city: 'Sesame City',
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
name: 'Oscar',
|
|
465
|
+
species: 'muppet',
|
|
466
|
+
gender: 'male',
|
|
467
|
+
address: {
|
|
468
|
+
street_name: 'Sesame St',
|
|
469
|
+
house_number: 'none',
|
|
470
|
+
unit: { floor: 0, number: 'none' },
|
|
471
|
+
city: 'Sesame City',
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
],
|
|
475
|
+
};
|
|
476
|
+
let numTimesWorkInProgressCalled = 0;
|
|
477
|
+
const onWorkInProgress = async (obj) => {
|
|
478
|
+
numTimesWorkInProgressCalled++;
|
|
479
|
+
};
|
|
480
|
+
// NOTE: In order to make sure the AI understands that it's supposed to use the
|
|
481
|
+
// bulk "copy" operation, we need to explicitly use the word "copy" in the instructions.
|
|
482
|
+
const result = await jsonSurgery(createClient(), original, 'Create a new character, "Bert", by copying the entire "Ernie" object ' +
|
|
483
|
+
'using the bulk "copy" action. Keep all data the same (except the name, of course). ' +
|
|
484
|
+
'Insert Bert after Ernie in the sesame_street_characters array.', { onWorkInProgress });
|
|
485
|
+
// Expect the result to have both `Ernie` and `Bert` with the same data.
|
|
486
|
+
expect(original.sesame_street_characters).toHaveLength(2);
|
|
487
|
+
expect(result.sesame_street_characters).toHaveLength(3);
|
|
488
|
+
expect(result.sesame_street_characters[0]).toEqual(original.sesame_street_characters[0]);
|
|
489
|
+
const expectBert = JSON.parse(JSON.stringify(original.sesame_street_characters[0]));
|
|
490
|
+
expectBert.name = 'Bert';
|
|
491
|
+
expect(result.sesame_street_characters[1]).toEqual(expectBert);
|
|
492
|
+
// Expect the work in progress callback to have been called 2 times or fewer.
|
|
493
|
+
// It's possible that the AI will be smart enough to do this in a single iteration,
|
|
494
|
+
// but we want to allow for the possibility that it'll use its first iteration
|
|
495
|
+
// to create the copy and its second one to name it Bert. The important thing is that
|
|
496
|
+
// if it *doesn't* use the copy operation, then the smallest number of steps it'll be
|
|
497
|
+
// able to use is 3, so this test will detect that.
|
|
498
|
+
expect(numTimesWorkInProgressCalled).toBeLessThanOrEqual(2);
|
|
499
|
+
}, 180000);
|
|
500
|
+
it('should move a nested object in a dict in 2 or fewer iterations', async () => {
|
|
501
|
+
const original = {
|
|
502
|
+
sesame_street_characters: {
|
|
503
|
+
ernie: {
|
|
504
|
+
species: 'muppet',
|
|
505
|
+
gender: 'male',
|
|
506
|
+
address: {
|
|
507
|
+
street_name: 'Sesame St',
|
|
508
|
+
house_number: '123',
|
|
509
|
+
unit: { floor: 1, number: '1D' },
|
|
510
|
+
city: 'Sesame City',
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
adventure_time_characters: {
|
|
515
|
+
finn: {
|
|
516
|
+
species: 'human',
|
|
517
|
+
gender: 'male',
|
|
518
|
+
address: {
|
|
519
|
+
street_name: 'Tree Fort Ln',
|
|
520
|
+
house_number: '1',
|
|
521
|
+
unit: { floor: 1, number: '1A' },
|
|
522
|
+
city: 'Ooo',
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
bert: {
|
|
526
|
+
species: 'muppet',
|
|
527
|
+
gender: 'male',
|
|
528
|
+
address: {
|
|
529
|
+
street_name: 'Sesame St',
|
|
530
|
+
house_number: '123',
|
|
531
|
+
unit: { floor: 1, number: '1D' },
|
|
532
|
+
city: 'Sesame City',
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
};
|
|
537
|
+
let numTimesWorkInProgressCalled = 0;
|
|
538
|
+
const onWorkInProgress = async (obj) => {
|
|
539
|
+
numTimesWorkInProgressCalled++;
|
|
540
|
+
};
|
|
541
|
+
// NOTE: The AI should be smart enough to realize that it needs to use the
|
|
542
|
+
// bulk operation in order to complete this task, but let's explicitly tell
|
|
543
|
+
// it to do so just to be sure.
|
|
544
|
+
const result = await jsonSurgery(createClient(), original, 'The character "bert" is misfiled under adventure_time_characters. ' +
|
|
545
|
+
'He should be under sesame_street_characters. Move him there, preferably ' +
|
|
546
|
+
'using a bulk copy/move operation if possible.', { onWorkInProgress });
|
|
547
|
+
// Expect the original to be untouched.
|
|
548
|
+
expect(Object.keys(original.sesame_street_characters)).toHaveLength(1);
|
|
549
|
+
expect(Object.keys(original.adventure_time_characters)).toHaveLength(2);
|
|
550
|
+
// Expect the correct number of characters in each group.
|
|
551
|
+
expect(Object.keys(result.sesame_street_characters)).toHaveLength(2);
|
|
552
|
+
expect(Object.keys(result.adventure_time_characters)).toHaveLength(1);
|
|
553
|
+
// Expect Bert to have the same data as he originally did.
|
|
554
|
+
expect(result.sesame_street_characters['bert']).toEqual(original.adventure_time_characters['bert']);
|
|
555
|
+
// Expect the move to have completed in 2 or fewer iterations.
|
|
556
|
+
// One to copy the object to the new group, one to delete the original.
|
|
557
|
+
// We're not really testing the implementation, because the point is that
|
|
558
|
+
// atomic piecewise movement would take 4 or 5 steps.
|
|
559
|
+
expect(numTimesWorkInProgressCalled).toBeLessThanOrEqual(2);
|
|
560
|
+
}, 180000);
|
|
561
|
+
it('should move a nested object from one list to another in few iterations', async () => {
|
|
562
|
+
const original = {
|
|
563
|
+
sesame_street_characters: [
|
|
564
|
+
{
|
|
565
|
+
name: 'Ernie',
|
|
566
|
+
species: 'muppet',
|
|
567
|
+
gender: 'male',
|
|
568
|
+
address: {
|
|
569
|
+
street_name: 'Sesame St',
|
|
570
|
+
house_number: '123',
|
|
571
|
+
unit: { floor: 1, number: '1D' },
|
|
572
|
+
city: 'Sesame City',
|
|
573
|
+
},
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
name: 'Oscar',
|
|
577
|
+
species: 'muppet',
|
|
578
|
+
gender: 'male',
|
|
579
|
+
address: {
|
|
580
|
+
street_name: 'Sesame St',
|
|
581
|
+
house_number: 'none',
|
|
582
|
+
unit: { floor: 0, number: 'none' },
|
|
583
|
+
city: 'Sesame City',
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
],
|
|
587
|
+
adventure_time_characters: [
|
|
588
|
+
{
|
|
589
|
+
name: 'Finn',
|
|
590
|
+
species: 'human',
|
|
591
|
+
gender: 'male',
|
|
592
|
+
address: {
|
|
593
|
+
street_name: 'Tree Fort Ln',
|
|
594
|
+
house_number: '1',
|
|
595
|
+
unit: { floor: 1, number: '1A' },
|
|
596
|
+
city: 'Ooo',
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
name: 'Bert',
|
|
601
|
+
species: 'muppet',
|
|
602
|
+
gender: 'male',
|
|
603
|
+
address: {
|
|
604
|
+
street_name: 'Sesame St',
|
|
605
|
+
house_number: '123',
|
|
606
|
+
unit: { floor: 1, number: '1D' },
|
|
607
|
+
city: 'Sesame City',
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
],
|
|
611
|
+
};
|
|
612
|
+
let numTimesWorkInProgressCalled = 0;
|
|
613
|
+
const onWorkInProgress = async (obj) => {
|
|
614
|
+
numTimesWorkInProgressCalled++;
|
|
615
|
+
};
|
|
616
|
+
// NOTE: The AI should be smart enough to realize that it needs to use the
|
|
617
|
+
// bulk operation in order to complete this task, but let's explicitly tell
|
|
618
|
+
// it to do so just to be sure.
|
|
619
|
+
const result = await jsonSurgery(createClient(), original, 'The character "Bert" is misfiled under adventure_time_characters. ' +
|
|
620
|
+
'He should be under sesame_street_characters. Move him there, right ' +
|
|
621
|
+
'after Ernie, preferably using a bulk copy/move operation if possible.', { onWorkInProgress });
|
|
622
|
+
// Expect the original to be untouched.
|
|
623
|
+
expect(original.sesame_street_characters).toHaveLength(2);
|
|
624
|
+
expect(original.adventure_time_characters).toHaveLength(2);
|
|
625
|
+
// Expect the correct number of characters in each group.
|
|
626
|
+
expect(result.sesame_street_characters).toHaveLength(3);
|
|
627
|
+
expect(result.adventure_time_characters).toHaveLength(1);
|
|
628
|
+
// Expect Bert to have the same data as he originally did.
|
|
629
|
+
expect(result.sesame_street_characters[1]).toEqual(original.adventure_time_characters[1]);
|
|
630
|
+
// Expect the move to have completed in 2 or fewer iterations.
|
|
631
|
+
// One to copy the object to the new group, one to delete the original.
|
|
632
|
+
// We're not really testing the implementation, because the point is that
|
|
633
|
+
// atomic piecewise movement would take 4 or 5 steps.
|
|
634
|
+
expect(numTimesWorkInProgressCalled).toBeLessThanOrEqual(2);
|
|
635
|
+
}, 180000);
|
|
636
|
+
});
|
|
637
|
+
describe('giveUp limits', () => {
|
|
638
|
+
it('should give up after iteration limit', async () => {
|
|
639
|
+
// We create a hostile validation function that keeps telling it to provide
|
|
640
|
+
// one previously undocumented field after another. "Oh, just one more thing."
|
|
641
|
+
// Eventually, it should give up after a number of iterations.
|
|
642
|
+
const onValidateBeforeReturn = async (obj) => {
|
|
643
|
+
const undocumentedFields = [
|
|
644
|
+
'id',
|
|
645
|
+
'date',
|
|
646
|
+
'description',
|
|
647
|
+
'category',
|
|
648
|
+
'status',
|
|
649
|
+
'type',
|
|
650
|
+
];
|
|
651
|
+
for (const field of undocumentedFields) {
|
|
652
|
+
if (!obj[field]) {
|
|
653
|
+
return { errors: [`Object needs a \`${field}\` field.`] };
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return { errors: [] };
|
|
657
|
+
};
|
|
658
|
+
const original = {
|
|
659
|
+
name: 'Test Product',
|
|
660
|
+
price: 100,
|
|
661
|
+
};
|
|
662
|
+
try {
|
|
663
|
+
await jsonSurgery(createClient(), original, 'Increase the price by 10%', {
|
|
664
|
+
onValidateBeforeReturn,
|
|
665
|
+
giveUpAfterIterations: 2,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
catch (error) {
|
|
669
|
+
expect(error.message.toLowerCase()).toContain('iteration');
|
|
670
|
+
const result = error.obj;
|
|
671
|
+
// Expect the result to have at least made it through one iteration.
|
|
672
|
+
expect(result.price).toEqual(110);
|
|
673
|
+
// Make sure that it didn't modify the original object.
|
|
674
|
+
expect(original).not.toEqual(result);
|
|
675
|
+
expect(original.price).toEqual(100);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
throw new Error('Expected jsonSurgery to throw after reaching iteration limit.');
|
|
679
|
+
}, 180000);
|
|
680
|
+
it('should give up after time limit', async () => {
|
|
681
|
+
// We create a hostile validation function that keeps telling it to provide
|
|
682
|
+
// one previously undocumented field after another. "Oh, just one more thing."
|
|
683
|
+
// We'll also make the validation callback wait a few seconds.
|
|
684
|
+
// In order to avoid instrumenting a mock, we'll use real time delays.
|
|
685
|
+
// This is generally ill-advised for tests that get run in a CICD pipeline, but
|
|
686
|
+
// we're already issuing real API calls to the OpenAI service in these tests,
|
|
687
|
+
// so this isn't any *more* egregious than that.
|
|
688
|
+
const onValidateBeforeReturn = async (obj) => {
|
|
689
|
+
// Sleep for 3 seconds to simulate a delay.
|
|
690
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
691
|
+
await sleep(3000);
|
|
692
|
+
const undocumentedFields = ['id', 'date', 'description'];
|
|
693
|
+
for (const field of undocumentedFields) {
|
|
694
|
+
if (!obj[field]) {
|
|
695
|
+
return { errors: [`Object needs a \`${field}\` field.`] };
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return { errors: [] };
|
|
699
|
+
};
|
|
700
|
+
const original = {
|
|
701
|
+
name: 'Test Product',
|
|
702
|
+
price: 100,
|
|
703
|
+
};
|
|
704
|
+
try {
|
|
705
|
+
await jsonSurgery(createClient(), original, 'Increase the price by 10%', {
|
|
706
|
+
onValidateBeforeReturn,
|
|
707
|
+
giveUpAfterSeconds: 2,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
catch (error) {
|
|
711
|
+
// We expect the result to hit the time limit before ever completing all validations.
|
|
712
|
+
// Note that we set the time limit to 2 seconds, but each validation
|
|
713
|
+
// callback sleeps for 3 seconds, and we've made it impossible to complete validation
|
|
714
|
+
// in just one pass. As such, it should time out before ever completing all validations.
|
|
715
|
+
// (It's also possible, in fact likely, that it never even reaches the
|
|
716
|
+
// validation step, because it times out just from the time spent on API calls.)
|
|
717
|
+
expect(error.message.toLowerCase()).toContain('seconds');
|
|
718
|
+
const result = error.obj;
|
|
719
|
+
// Expect the result to have at least made it through one iteration.
|
|
720
|
+
expect(result.price).toEqual(110);
|
|
721
|
+
// Make sure that it didn't modify the original object.
|
|
722
|
+
expect(original).not.toEqual(result);
|
|
723
|
+
expect(original.price).toEqual(100);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
throw new Error('Expected jsonSurgery to throw after reaching time limit.');
|
|
727
|
+
}, 180000);
|
|
728
|
+
});
|
|
729
|
+
});
|