Blame Raw
Cohee · e3f41666 · · 499 lines (20.0 KB)
1 contributor
1import { test, expect } from '@playwright/test';
2import { testSetup } from './frontent-test-utils.js';
3
4test.describe('MacroEnvBuilder', () => {
5 test.beforeEach(testSetup.awaitST);
6
7 test('builds names from overrides without relying on globals', async ({ page }) => {
8 const result = await page.evaluate(async () => {
9 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
10 const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
11
12 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js').MacroEnvRawContext} */
13 const ctx = {
14 content: 'ignored',
15 name1Override: 'UserOverride',
16 name2Override: 'CharOverride',
17 };
18
19 const env = MacroEnvBuilder.buildFromRawEnv(ctx);
20 return {
21 user: env.names?.user,
22 char: env.names?.char,
23 };
24 });
25
26 expect(result).toEqual({
27 user: 'UserOverride',
28 char: 'CharOverride',
29 });
30 });
31
32 test('falls back to global name1/name2 when overrides are not provided', async ({ page }) => {
33 const result = await page.evaluate(async () => {
34 const script = await import('./script.js');
35 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
36 const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
37
38 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js').MacroEnvRawContext} */
39 const ctx = {
40 content: '',
41 };
42
43 const env = MacroEnvBuilder.buildFromRawEnv(ctx);
44 return {
45 globalUser: script.name1,
46 globalChar: script.name2,
47 envUser: env.names?.user,
48 envChar: env.names?.char,
49 };
50 });
51
52 expect(result.envUser).toBe(result.globalUser);
53 expect(result.envChar).toBe(result.globalChar);
54 });
55
56 test('does not populate character fields when replaceCharacterCard is false', async ({ page }) => {
57 const keys = await page.evaluate(async () => {
58 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
59 const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
60
61 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js').MacroEnvRawContext} */
62 const ctx = {
63 content: '',
64 replaceCharacterCard: false,
65 };
66
67 const env = MacroEnvBuilder.buildFromRawEnv(ctx);
68 return Object.keys(env.character || {});
69 });
70
71 expect(keys).toEqual([]);
72 });
73
74 test('populates character fields when replaceCharacterCard is true', async ({ page }) => {
75 const keys = await page.evaluate(async () => {
76 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
77 const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
78
79 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js').MacroEnvRawContext} */
80 const ctx = {
81 content: '',
82 replaceCharacterCard: true,
83 };
84
85 const env = MacroEnvBuilder.buildFromRawEnv(ctx);
86 return Object.keys(env.character || {});
87 });
88
89 // We do not assert on concrete values, only that the known keys exist
90 expect(keys).toEqual(expect.arrayContaining([
91 'charPrompt',
92 'charInstruction',
93 'description',
94 'personality',
95 'scenario',
96 'persona',
97 'mesExamplesRaw',
98 'version',
99 'charDepthPrompt',
100 'creatorNotes',
101 'firstMessage',
102 'alternateGreetings',
103 ]));
104 });
105
106 test('wraps original string into a one-shot helper function', async ({ page }) => {
107 const result = await page.evaluate(async () => {
108 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
109 const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
110
111 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js').MacroEnvRawContext} */
112 const ctx = {
113 content: '',
114 original: 'ORIGINAL_VALUE',
115 };
116
117 const env = MacroEnvBuilder.buildFromRawEnv(ctx);
118 const hasFn = typeof env.functions?.original === 'function';
119 const first = hasFn ? env.functions.original() : null;
120 const second = hasFn ? env.functions.original() : null;
121
122 return { hasFn, first, second };
123 });
124
125 expect(result).toEqual({
126 hasFn: true,
127 first: 'ORIGINAL_VALUE',
128 second: '',
129 });
130 });
131
132 test('does not expose original helper when original is not a string', async ({ page }) => {
133 const hasFn = await page.evaluate(async () => {
134 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
135 const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
136
137 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js').MacroEnvRawContext} */
138 const ctx = {
139 content: '',
140 original: undefined,
141 };
142
143 const env = MacroEnvBuilder.buildFromRawEnv(ctx);
144 return typeof env.functions?.original === 'function';
145 });
146
147 expect(hasFn).toBe(false);
148 });
149
150 test('uses groupOverride string for all group-related name fields', async ({ page }) => {
151 const result = await page.evaluate(async () => {
152 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
153 const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
154
155 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js').MacroEnvRawContext} */
156 const ctx = {
157 content: '',
158 groupOverride: 'Group One, Group Two',
159 };
160
161 const env = MacroEnvBuilder.buildFromRawEnv(ctx);
162 return {
163 group: env.names?.group,
164 groupNotMuted: env.names?.groupNotMuted,
165 notChar: env.names?.notChar,
166 };
167 });
168
169 expect(result).toEqual({
170 group: 'Group One, Group Two',
171 groupNotMuted: 'Group One, Group Two',
172 notChar: 'Group One, Group Two',
173 });
174 });
175
176 test('uses solo-chat semantics when no group is selected', async ({ page }) => {
177 const result = await page.evaluate(async () => {
178 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
179 const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
180 const groupChats = await import('./scripts/group-chats.js');
181
182 // Ensure we are in a solo-chat like state for this test
183 if (typeof groupChats.resetSelectedGroup === 'function') {
184 groupChats.resetSelectedGroup();
185 }
186
187 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js').MacroEnvRawContext} */
188 const ctx = {
189 content: '',
190 name1Override: 'UserSolo',
191 name2Override: 'CharSolo',
192 groupOverride: undefined,
193 };
194
195 const env = MacroEnvBuilder.buildFromRawEnv(ctx);
196 return {
197 group: env.names?.group,
198 groupNotMuted: env.names?.groupNotMuted,
199 notChar: env.names?.notChar,
200 };
201 });
202
203 expect(result).toEqual({
204 group: 'CharSolo',
205 groupNotMuted: 'CharSolo',
206 notChar: 'UserSolo',
207 });
208 });
209
210 test.describe('Dynamic macros in env', () => {
211 test('merges dynamicMacros properties into env.dynamicMacros', async ({ page }) => {
212 const dynamicMacros = await page.evaluate(async () => {
213 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
214 const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
215
216 const ctx = {
217 content: '',
218 dynamicMacros: {
219 simple: 'value',
220 number: 42,
221 },
222 };
223
224 const env = MacroEnvBuilder.buildFromRawEnv(ctx);
225 return env.dynamicMacros;
226 });
227
228 expect(dynamicMacros.simple).toBe('value');
229 expect(dynamicMacros.number).toBe(42);
230 });
231
232 test('normalizes dynamic macro keys to lowercase', async ({ page }) => {
233 const result = await page.evaluate(async () => {
234 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
235 const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
236
237 const ctx = {
238 content: '',
239 dynamicMacros: {
240 MyMacro: 'value1',
241 UPPERCASE: 'value2',
242 lowercase: 'value3',
243 },
244 };
245
246 const env = MacroEnvBuilder.buildFromRawEnv(ctx);
247 return {
248 hasLower: 'mymacro' in env.dynamicMacros,
249 hasUpper: 'uppercase' in env.dynamicMacros,
250 hasLowercase: 'lowercase' in env.dynamicMacros,
251 value1: env.dynamicMacros['mymacro'],
252 value2: env.dynamicMacros['uppercase'],
253 value3: env.dynamicMacros['lowercase'],
254 };
255 });
256
257 expect(result.hasLower).toBe(true);
258 expect(result.hasUpper).toBe(true);
259 expect(result.hasLowercase).toBe(true);
260 expect(result.value1).toBe('value1');
261 expect(result.value2).toBe('value2');
262 expect(result.value3).toBe('value3');
263 });
264
265 test('accepts string values for dynamic macros', async ({ page }) => {
266 const result = await page.evaluate(async () => {
267 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
268 const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
269
270 const ctx = {
271 content: '',
272 dynamicMacros: {
273 greeting: 'Hello, World!',
274 },
275 };
276
277 const env = MacroEnvBuilder.buildFromRawEnv(ctx);
278 return {
279 value: env.dynamicMacros['greeting'],
280 type: typeof env.dynamicMacros['greeting'],
281 };
282 });
283
284 expect(result.value).toBe('Hello, World!');
285 expect(result.type).toBe('string');
286 });
287
288 test('accepts handler functions for dynamic macros', async ({ page }) => {
289 const result = await page.evaluate(async () => {
290 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
291 const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
292
293 const ctx = {
294 content: '',
295 dynamicMacros: {
296 dyn: () => 'handler result',
297 },
298 };
299
300 const env = MacroEnvBuilder.buildFromRawEnv(ctx);
301 const storedValue = env.dynamicMacros['dyn'];
302 return {
303 isFunction: typeof storedValue === 'function',
304 callResult: typeof storedValue === 'function' ? storedValue() : null,
305 };
306 });
307
308 expect(result.isFunction).toBe(true);
309 expect(result.callResult).toBe('handler result');
310 });
311
312 test('accepts MacroDefinitionOptions objects for dynamic macros', async ({ page }) => {
313 const result = await page.evaluate(async () => {
314 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
315 const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
316
317 const ctx = {
318 content: '',
319 dynamicMacros: {
320 greet: {
321 description: 'A greeting macro',
322 unnamedArgs: [{ name: 'name' }],
323 handler: ({ unnamedArgs: [name] }) => `Hello, ${name}!`,
324 },
325 },
326 };
327
328 const env = MacroEnvBuilder.buildFromRawEnv(ctx);
329 const storedValue = env.dynamicMacros['greet'];
330 return {
331 isObject: typeof storedValue === 'object' && storedValue !== null,
332 hasHandler: typeof storedValue?.handler === 'function',
333 hasDescription: typeof storedValue?.description === 'string',
334 hasUnnamedArgs: Array.isArray(storedValue?.unnamedArgs),
335 };
336 });
337
338 expect(result.isObject).toBe(true);
339 expect(result.hasHandler).toBe(true);
340 expect(result.hasDescription).toBe(true);
341 expect(result.hasUnnamedArgs).toBe(true);
342 });
343
344 test('supports mixed dynamic macro value types', async ({ page }) => {
345 const result = await page.evaluate(async () => {
346 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
347 const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
348
349 const ctx = {
350 content: '',
351 dynamicMacros: {
352 stringVal: 'direct string',
353 funcVal: () => 'from function',
354 optionsVal: {
355 handler: () => 'from options',
356 unnamedArgs: 1,
357 },
358 },
359 };
360
361 const env = MacroEnvBuilder.buildFromRawEnv(ctx);
362 return {
363 stringType: typeof env.dynamicMacros['stringval'],
364 funcType: typeof env.dynamicMacros['funcval'],
365 optionsType: typeof env.dynamicMacros['optionsval'],
366 optionsHasHandler: typeof env.dynamicMacros['optionsval']?.handler === 'function',
367 };
368 });
369
370 expect(result.stringType).toBe('string');
371 expect(result.funcType).toBe('function');
372 expect(result.optionsType).toBe('object');
373 expect(result.optionsHasHandler).toBe(true);
374 });
375
376 test('handles null dynamicMacros gracefully', async ({ page }) => {
377 const result = await page.evaluate(async () => {
378 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
379 const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
380
381 const ctx = {
382 content: '',
383 dynamicMacros: null,
384 };
385
386 const env = MacroEnvBuilder.buildFromRawEnv(ctx);
387 return {
388 hasDynamicMacros: 'dynamicMacros' in env,
389 isEmpty: Object.keys(env.dynamicMacros).length === 0,
390 };
391 });
392
393 expect(result.hasDynamicMacros).toBe(true);
394 expect(result.isEmpty).toBe(true);
395 });
396
397 test('handles undefined dynamicMacros gracefully', async ({ page }) => {
398 const result = await page.evaluate(async () => {
399 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
400 const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
401
402 const ctx = {
403 content: '',
404 // dynamicMacros not provided
405 };
406
407 const env = MacroEnvBuilder.buildFromRawEnv(ctx);
408 return {
409 hasDynamicMacros: 'dynamicMacros' in env,
410 isEmpty: Object.keys(env.dynamicMacros).length === 0,
411 };
412 });
413
414 expect(result.hasDynamicMacros).toBe(true);
415 expect(result.isEmpty).toBe(true);
416 });
417 });
418
419 test('sets system.model field from getGeneratingModel helper', async ({ page }) => {
420 const model = await page.evaluate(async () => {
421 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
422 const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
423
424 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js').MacroEnvRawContext} */
425 const ctx = {
426 content: '',
427 };
428
429 const env = MacroEnvBuilder.buildFromRawEnv(ctx);
430 return env.system?.model;
431 });
432
433 expect(typeof model === 'string' || model === undefined).toBe(true);
434 });
435
436 test('applies providers in the expected order buckets', async ({ page }) => {
437 const order = await page.evaluate(async () => {
438 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
439 const { MacroEnvBuilder, env_provider_order } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
440
441 MacroEnvBuilder.registerProvider((env) => {
442 env.extra.order = [...(env.extra.order || []), 'EARLY'];
443 }, env_provider_order.EARLY);
444
445 MacroEnvBuilder.registerProvider((env) => {
446 env.extra.order = [...(env.extra.order || []), 'LATE'];
447 }, env_provider_order.LATE);
448
449 MacroEnvBuilder.registerProvider((env) => {
450 env.extra.order = [...(env.extra.order || []), 'NORMAL'];
451 }, env_provider_order.NORMAL);
452
453 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js').MacroEnvRawContext} */
454 const ctx = {
455 content: '',
456 };
457
458 const env = MacroEnvBuilder.buildFromRawEnv(ctx);
459 return env.extra.order;
460 });
461
462 // We only guarantee relative ordering between the buckets we added,
463 // not that there are no other entries from other providers.
464 const earlyIndex = order.indexOf('EARLY');
465 const normalIndex = order.indexOf('NORMAL');
466 const lateIndex = order.indexOf('LATE');
467
468 expect(earlyIndex).toBeGreaterThanOrEqual(0);
469 expect(normalIndex).toBeGreaterThan(earlyIndex);
470 expect(lateIndex).toBeGreaterThan(normalIndex);
471 });
472
473 test('ignores provider errors without breaking env construction', async ({ page }) => {
474 const result = await page.evaluate(async () => {
475 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
476 const { MacroEnvBuilder, env_provider_order } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
477
478 MacroEnvBuilder.registerProvider(() => {
479 throw new Error('intentional test error');
480 }, env_provider_order.NORMAL);
481
482 /** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js').MacroEnvRawContext} */
483 const ctx = {
484 content: '',
485 name1Override: 'User',
486 dynamicMacros: { marker: 'value' },
487 };
488
489 const env = MacroEnvBuilder.buildFromRawEnv(ctx);
490 return {
491 namesUser: env.names?.user,
492 hasDynamicMacro: env.dynamicMacros?.marker === 'value',
493 };
494 });
495
496 expect(result.hasDynamicMacro).toBe(true);
497 expect(result.namesUser).toBe('User');
498 });
499});