Files
st/web-app/tests/frontend/MacroEnvBuilder.e2e.js
2026-02-10 17:48:27 +08:00

313 lines
12 KiB
JavaScript

import { test, expect } from '@playwright/test';
import { testSetup } from './frontent-test-utils.js';
test.describe('MacroEnvBuilder', () => {
test.beforeEach(testSetup.awaitST);
test('builds names from overrides without relying on globals', async ({ page }) => {
const result = await page.evaluate(async () => {
/** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
/** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js').MacroEnvRawContext} */
const ctx = {
content: 'ignored',
name1Override: 'UserOverride',
name2Override: 'CharOverride',
};
const env = MacroEnvBuilder.buildFromRawEnv(ctx);
return {
user: env.names?.user,
char: env.names?.char,
};
});
expect(result).toEqual({
user: 'UserOverride',
char: 'CharOverride',
});
});
test('falls back to global name1/name2 when overrides are not provided', async ({ page }) => {
const result = await page.evaluate(async () => {
const script = await import('./script.js');
/** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
/** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js').MacroEnvRawContext} */
const ctx = {
content: '',
};
const env = MacroEnvBuilder.buildFromRawEnv(ctx);
return {
globalUser: script.name1,
globalChar: script.name2,
envUser: env.names?.user,
envChar: env.names?.char,
};
});
expect(result.envUser).toBe(result.globalUser);
expect(result.envChar).toBe(result.globalChar);
});
test('does not populate character fields when replaceCharacterCard is false', async ({ page }) => {
const keys = await page.evaluate(async () => {
/** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
/** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js').MacroEnvRawContext} */
const ctx = {
content: '',
replaceCharacterCard: false,
};
const env = MacroEnvBuilder.buildFromRawEnv(ctx);
return Object.keys(env.character || {});
});
expect(keys).toEqual([]);
});
test('populates character fields when replaceCharacterCard is true', async ({ page }) => {
const keys = await page.evaluate(async () => {
/** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
/** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js').MacroEnvRawContext} */
const ctx = {
content: '',
replaceCharacterCard: true,
};
const env = MacroEnvBuilder.buildFromRawEnv(ctx);
return Object.keys(env.character || {});
});
// We do not assert on concrete values, only that the known keys exist
expect(keys).toEqual(expect.arrayContaining([
'charPrompt',
'charInstruction',
'description',
'personality',
'scenario',
'persona',
'mesExamplesRaw',
'version',
'charDepthPrompt',
'creatorNotes',
]));
});
test('wraps original string into a one-shot helper function', async ({ page }) => {
const result = await page.evaluate(async () => {
/** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
/** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js').MacroEnvRawContext} */
const ctx = {
content: '',
original: 'ORIGINAL_VALUE',
};
const env = MacroEnvBuilder.buildFromRawEnv(ctx);
const hasFn = typeof env.functions?.original === 'function';
const first = hasFn ? env.functions.original() : null;
const second = hasFn ? env.functions.original() : null;
return { hasFn, first, second };
});
expect(result).toEqual({
hasFn: true,
first: 'ORIGINAL_VALUE',
second: '',
});
});
test('does not expose original helper when original is not a string', async ({ page }) => {
const hasFn = await page.evaluate(async () => {
/** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
/** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js').MacroEnvRawContext} */
const ctx = {
content: '',
original: undefined,
};
const env = MacroEnvBuilder.buildFromRawEnv(ctx);
return typeof env.functions?.original === 'function';
});
expect(hasFn).toBe(false);
});
test('uses groupOverride string for all group-related name fields', async ({ page }) => {
const result = await page.evaluate(async () => {
/** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
/** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js').MacroEnvRawContext} */
const ctx = {
content: '',
groupOverride: 'Group One, Group Two',
};
const env = MacroEnvBuilder.buildFromRawEnv(ctx);
return {
group: env.names?.group,
groupNotMuted: env.names?.groupNotMuted,
notChar: env.names?.notChar,
};
});
expect(result).toEqual({
group: 'Group One, Group Two',
groupNotMuted: 'Group One, Group Two',
notChar: 'Group One, Group Two',
});
});
test('uses solo-chat semantics when no group is selected', async ({ page }) => {
const result = await page.evaluate(async () => {
/** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
const groupChats = await import('./scripts/group-chats.js');
// Ensure we are in a solo-chat like state for this test
if (typeof groupChats.resetSelectedGroup === 'function') {
groupChats.resetSelectedGroup();
}
/** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js').MacroEnvRawContext} */
const ctx = {
content: '',
name1Override: 'UserSolo',
name2Override: 'CharSolo',
groupOverride: undefined,
};
const env = MacroEnvBuilder.buildFromRawEnv(ctx);
return {
group: env.names?.group,
groupNotMuted: env.names?.groupNotMuted,
notChar: env.names?.notChar,
};
});
expect(result).toEqual({
group: 'CharSolo',
groupNotMuted: 'CharSolo',
notChar: 'UserSolo',
});
});
test('merges dynamicMacros properties into env.dynamicMacros', async ({ page }) => {
const dynamicMacros = await page.evaluate(async () => {
/** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
/** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js').MacroEnvRawContext} */
const ctx = {
content: '',
dynamicMacros: {
simple: 'value',
number: 42,
nested: { foo: 'bar' },
},
};
const env = MacroEnvBuilder.buildFromRawEnv(ctx);
return env.dynamicMacros;
});
expect(dynamicMacros.simple).toBe('value');
expect(dynamicMacros.number).toBe(42);
expect(dynamicMacros.nested).toEqual({ foo: 'bar' });
});
test('sets system.model field from getGeneratingModel helper', async ({ page }) => {
const model = await page.evaluate(async () => {
/** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
const { MacroEnvBuilder } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
/** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js').MacroEnvRawContext} */
const ctx = {
content: '',
};
const env = MacroEnvBuilder.buildFromRawEnv(ctx);
return env.system?.model;
});
expect(typeof model === 'string' || model === undefined).toBe(true);
});
test('applies providers in the expected order buckets', async ({ page }) => {
const order = await page.evaluate(async () => {
/** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
const { MacroEnvBuilder, env_provider_order } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
MacroEnvBuilder.registerProvider((env) => {
env.extra.order = [...(env.extra.order || []), 'EARLY'];
}, env_provider_order.EARLY);
MacroEnvBuilder.registerProvider((env) => {
env.extra.order = [...(env.extra.order || []), 'LATE'];
}, env_provider_order.LATE);
MacroEnvBuilder.registerProvider((env) => {
env.extra.order = [...(env.extra.order || []), 'NORMAL'];
}, env_provider_order.NORMAL);
/** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js').MacroEnvRawContext} */
const ctx = {
content: '',
};
const env = MacroEnvBuilder.buildFromRawEnv(ctx);
return env.extra.order;
});
// We only guarantee relative ordering between the buckets we added,
// not that there are no other entries from other providers.
const earlyIndex = order.indexOf('EARLY');
const normalIndex = order.indexOf('NORMAL');
const lateIndex = order.indexOf('LATE');
expect(earlyIndex).toBeGreaterThanOrEqual(0);
expect(normalIndex).toBeGreaterThan(earlyIndex);
expect(lateIndex).toBeGreaterThan(normalIndex);
});
test('ignores provider errors without breaking env construction', async ({ page }) => {
const result = await page.evaluate(async () => {
/** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js')} */
const { MacroEnvBuilder, env_provider_order } = await import('./scripts/macros/engine/MacroEnvBuilder.js');
MacroEnvBuilder.registerProvider(() => {
throw new Error('intentional test error');
}, env_provider_order.NORMAL);
/** @type {import('../../public/scripts/macros/engine/MacroEnvBuilder.js').MacroEnvRawContext} */
const ctx = {
content: '',
name1Override: 'User',
dynamicMacros: { marker: 'value' },
};
const env = MacroEnvBuilder.buildFromRawEnv(ctx);
return {
namesUser: env.names?.user,
hasDynamicMacro: env.dynamicMacros?.marker === 'value',
};
});
expect(result.hasDynamicMacro).toBe(true);
expect(result.namesUser).toBe('User');
});
});