TauriTavern/tests/browser/macro-env.test.mjs
Darkatse 311287205d feat(a11y): unify character and chat list interactions
Give character, persona, group and chat lists native actions, accessible
names and state, and shared focus, pagination and menu behavior. Update
Font Awesome 6 assets and keep icon fonts independent of text fonts.

Bind chat file actions to their owners, report failures, and preserve
extension-facing chat exports and character-index parameters. Replace
legacy pagination helpers with one initializer and retain focused tests.

Compatibility: deleting the open chat loads a replacement. Discordia
0.7.3's subsequent clearChat flow is unsupported; Folder Manager 3.0.15
may leave an extra new chat after deleting its last current chat. Both
boundaries are documented and accepted for this change.

Removed exports: PAGINATION_TEMPLATE, localizePagination,
renderPaginationDropdown, paginationDropdownChangeHandler. The
jumpToNewChat option is removed from deleteGroupChat.

Validation: pnpm run check; targeted extension-call and Chromium probes.
2026-10-09 18:25:33 -04:00

148 lines
8.1 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { createBrowserRuntime } from './runtime.mjs';
test('macro evaluation environments', async (context) => {
const { window, getModule, load, startHost } = createBrowserRuntime();
try {
await startHost();
await load('script.js');
const { macros, initRegisterMacros } = getModule('scripts/macros/macro-system.js').namespace;
const { MacrosParser } = getModule('scripts/macros.js').namespace;
const { power_user } = getModule('scripts/power-user.js').namespace;
const app = getModule('script.js').namespace;
power_user.experimental_macro_engine = true;
initRegisterMacros();
await context.test('named and shorthand macros share detached variable operations', () => {
const context = macros.captureContext();
context.variables = { local: { score: '2', items: '["first"]' }, global: { code: '007' } };
const text = '{{setvar::score::3}}{{.score += 4}}{{incvar::score}}|{{getglobalvar::code}}'
+ '|{{setvarkey::items::1::second}}{{getvarkey::items::1}}';
assert.equal(macros.evaluateWithContext(text, context), '8|7|second');
const original = app.chat_metadata.variables;
try {
app.chat_metadata.variables = { current: '41' };
assert.equal(app.substituteParams('{{getvar::current}}|{{replaceVariables}}|{{getvar::current}}', {
replaceCharacterCard: false,
dynamicMacros: { replaceVariables: () => { app.chat_metadata.variables = { current: '42' }; return ''; } },
}), '41||42');
} finally {
app.chat_metadata.variables = original;
}
});
await context.test('dynamic definitions govern delayed arguments and scoped arity before evaluation', () => {
const context = macros.captureContext();
context.variables.local.counter = 0;
const hold = { unnamedArgs: 1, delayArgResolution: true, handler: () => 'held' };
for (const [text, dynamicMacros] of [
['{{hold::{{incvar::counter}}}}', { hold }],
// random normally takes a list and cannot accept scoped content.
['{{random}}{{incvar::counter}}{{/random}}', { random: hold }],
['{{if::true}}{{random}}{{incvar::counter}}{{/random}}{{/if}}', { random: hold }],
]) {
assert.equal(macros.evaluateWithContext(text, context, { dynamicMacros }), 'held');
assert.equal(context.variables.local.counter, 0);
}
});
await context.test('legacy card fields retain their eager variable evaluation order', () => {
const context = macros.captureContext();
context.engine = 'legacy';
context.variables = { local: { value: 'before', greetings: 0 }, global: {} };
context.character.persona = '{{setvar::value::from-persona}}';
context.character.scenario = '{{getvar::value}}';
context.character.firstMessage = '{{incvar::greetings}}';
context.character.alternateGreetings = ['{{incvar::greetings}}'];
assert.equal(macros.evaluateWithContext('{{scenario}}', context), 'from-persona');
assert.equal(context.variables.local.greetings, 2);
});
await context.test('legacy phases keep their ordering and pass the explicit environment after nonce', () => {
power_user.experimental_macro_engine = false;
const context = macros.captureContext();
context.names.char = 'Frozen';
context.variables.local.value = 'before';
context.character.charPrompt = 'card';
const text = '{{legacyenvprobe}} {{legacyenvprobe}} {{getvar::value}}{{setvar::value::after}} {{charPrompt}}';
const nonces = [];
const callback = (nonce, suppliedEnv) => {
nonces.push(nonce);
return suppliedEnv.names.char;
};
MacrosParser.registerMacro('legacyenvprobe', callback);
try {
assert.equal(macros.evaluateWithContext(text, context, { dynamicMacros: { CHARPROMPT: 'override' } }), 'Frozen Frozen after override');
assert.equal(typeof nonces[0], 'string');
assert.equal(nonces[0], nonces[1]);
MacrosParser.unregisterMacro('legacyenvprobe');
power_user.experimental_macro_engine = true;
MacrosParser.registerMacro('legacyenvprobe', callback);
context.engine = 'new';
assert.equal(macros.evaluateWithContext('{{legacyenvprobe}}', context), 'Frozen');
} finally {
MacrosParser.unregisterMacro('legacyenvprobe');
power_user.experimental_macro_engine = true;
}
});
await context.test("group name overrides retain each engine's participant rules", () => {
const context = macros.captureContext();
context.names.user = 'User';
context.settings.isGroup = true;
context.settings.groupNames = ['Alice', 'Bob', 'Muted'];
context.settings.groupNamesNotMuted = ['Alice', 'Bob'];
context.engine = 'new';
assert.equal(macros.evaluateWithContext('{{notChar}}', context, { name2Override: 'Bob' }), 'Alice');
context.engine = 'legacy';
assert.equal(macros.evaluateWithContext('{{notChar}}|{{group}}', context,
{ name2Override: 'Bob', groupOverride: 'Explicit group' }), 'Alice, Muted, User|Explicit group');
});
await context.test('direct new-engine callers keep their engine when the UI uses legacy', () => {
const previous = power_user.experimental_macro_engine;
power_user.experimental_macro_engine = false;
try {
const text = '{{notChar}}';
const options = { name1Override: 'User', name2Override: 'Char', groupOverride: 'Group', replaceCharacterCard: false };
assert.equal(app.substituteParams(text, options), 'User');
const env = macros.envBuilder.buildFromRawEnv({ content: text, ...options });
assert.equal(macros.engine.evaluate(text, env), 'Group');
const captured = macros.captureContext();
assert.equal(macros.evaluateWithContext(text, captured, options), 'User');
} finally {
power_user.experimental_macro_engine = previous;
}
});
await context.test('explicit evaluations retain captured names and reset per-text fields', () => {
const previousPersona = power_user.persona_description;
const previousVariables = app.chat_metadata.variables;
let nickname = 'Captured';
macros.envBuilder.registerProvider(env => {
env.names.char = nickname;
});
try {
power_user.persona_description = '{{incvar::frameCounter}}';
app.chat_metadata.variables = { frameCounter: 0 };
const context = macros.captureContext();
nickname = 'Different live chat';
const text = '{{char}}: {{persona}} {{persona}}|{{original}}/{{original}}';
assert.equal(macros.evaluateWithContext(text, context, { original: 'first' }), 'Captured: 1 1|first/');
assert.equal(macros.evaluateWithContext(text, context, { original: 'second' }), 'Captured: 2 2|second/');
context.character.description = '{{char}}';
assert.equal(macros.evaluateWithContext('{{char}}|{{description}}', context, { name2Override: 'Other' }),
'Other|Captured', "An outer name override does not replace the card's own default name");
} finally {
power_user.persona_description = previousPersona;
app.chat_metadata.variables = previousVariables;
}
});
} finally {
await window.happyDOM.close();
}
});