const { describe, it } = require('node:test');
const assert = require('node:assert');
const { createBrowserContext, loadSource } = require('./helpers');
const vm = require('vm');
/**
* Load Events + Extensions into a browser context.
*/
function loadExtensions(overrides = {}) {
const sandbox = createBrowserContext(overrides);
loadSource(sandbox, 'events.js');
loadSource(sandbox, 'extensions.js');
const ctx = vm.createContext(sandbox);
return {
Extensions: vm.runInContext('Extensions', ctx),
Events: vm.runInContext('Events', ctx),
sandbox,
};
}
// ═══════════════════════════════════════════
// KaTeX Renderer Extension
// ═══════════════════════════════════════════
describe('KaTeX renderer extension', () => {
function loadWithKaTeX() {
const ctx = loadExtensions();
ctx.Extensions.register({
id: 'katex-renderer',
_katexReady: false,
_katexLoading: null,
_escapeHtml(str) {
return str.replace(/&/g, '&').replace(//g, '>');
},
init(extCtx) {
const self = this;
extCtx.renderers.register('katex-block', {
type: 'block',
priority: 10,
match(lang) {
const l = (lang || '').toLowerCase();
return l === 'latex' || l === 'math' || l === 'tex' || l === 'katex';
},
render(lang, code, container) {
container.innerHTML = `
View source
${self._escapeHtml(code.trim())}
`;
}
});
extCtx.renderers.register('katex-post', {
type: 'post',
priority: 10,
render(container) {
container._katexPostRan = true;
}
});
},
destroy() {}
});
return ctx;
}
it('registers both block and post renderers', async () => {
const ctx = loadWithKaTeX();
await ctx.Extensions.initAll();
const info = ctx.Extensions.debug();
assert.strictEqual(info.extensions['katex-renderer'].active, true);
assert.strictEqual(info.extensions['katex-renderer'].renderers.length, 2);
});
it('matches latex, math, tex, and katex languages', async () => {
const ctx = loadWithKaTeX();
await ctx.Extensions.initAll();
for (const lang of ['latex', 'math', 'tex', 'katex']) {
const container = { innerHTML: '' };
const handled = ctx.Extensions.runBlockRenderers(lang, 'E = mc^2', container);
assert.ok(handled, `should match language: ${lang}`);
assert.ok(container.innerHTML.includes('katex-block'), `should render katex block for: ${lang}`);
}
});
it('does not match other languages', async () => {
const ctx = loadWithKaTeX();
await ctx.Extensions.initAll();
for (const lang of ['python', 'javascript', 'mermaid', 'csv']) {
const container = { innerHTML: '' };
const handled = ctx.Extensions.runBlockRenderers(lang, 'code', container);
assert.strictEqual(handled, false, `should not match language: ${lang}`);
}
});
it('includes encoded source in data attribute', async () => {
const ctx = loadWithKaTeX();
await ctx.Extensions.initAll();
const container = { innerHTML: '' };
ctx.Extensions.runBlockRenderers('latex', '\\frac{a}{b}', container);
assert.ok(container.innerHTML.includes(encodeURIComponent('\\frac{a}{b}')));
});
it('escapes HTML in source view', async () => {
const ctx = loadWithKaTeX();
await ctx.Extensions.initAll();
const container = { innerHTML: '' };
ctx.Extensions.runBlockRenderers('math', '', container);
assert.ok(container.innerHTML.includes('<script>'));
assert.ok(!container.innerHTML.includes('', container);
assert.ok(container.innerHTML.includes('<script>'));
});
});
// ═══════════════════════════════════════════
// JS Sandbox Extension (Tool)
// ═══════════════════════════════════════════
describe('JS Sandbox extension', () => {
it('registers js_eval tool handler', async () => {
const ctx = loadExtensions();
ctx.Extensions.register({
id: 'js-sandbox',
init(extCtx) {
extCtx.tools.handle('js_eval', async (args) => {
return { success: true, result: 'mock' };
});
},
destroy() {}
});
await ctx.Extensions.initAll();
assert.ok(ctx.Extensions._toolHandlers.has('js_eval'));
});
it('routes tool calls through the bridge', async () => {
const ctx = loadExtensions();
let receivedCode = null;
ctx.Extensions.register({
id: 'js-sandbox',
init(extCtx) {
extCtx.tools.handle('js_eval', async (args) => {
receivedCode = args.code;
return { success: true, result: '42', output: [] };
});
},
destroy() {}
});
await ctx.Extensions.initAll();
let resultEmitted = null;
ctx.Events.on('tool.result.eval-1', (payload) => {
resultEmitted = payload;
});
ctx.Events.emit('tool.call.eval-1', {
call_id: 'eval-1',
tool: 'js_eval',
arguments: { code: '21 * 2' },
}, { localOnly: true });
await new Promise(r => setTimeout(r, 10));
assert.strictEqual(receivedCode, '21 * 2');
assert.ok(resultEmitted);
const parsed = JSON.parse(resultEmitted.result);
assert.strictEqual(parsed.success, true);
assert.strictEqual(parsed.result, '42');
});
});
// ═══════════════════════════════════════════
// Regex Tester Extension (Tool)
// ═══════════════════════════════════════════
describe('Regex Tester extension', () => {
function loadWithRegex() {
const ctx = loadExtensions();
ctx.Extensions.register({
id: 'regex-tester',
_formatMatch(match) {
const result = {
fullMatch: match[0],
index: match.index,
length: match[0].length,
};
if (match.groups) {
result.namedGroups = {};
for (const [name, value] of Object.entries(match.groups)) {
result.namedGroups[name] = value;
}
}
if (match.length > 1) {
result.groups = [];
for (let i = 1; i < match.length; i++) {
result.groups.push(match[i] !== undefined ? match[i] : null);
}
}
return result;
},
_test(pattern, flags, inputs) {
let regex;
try {
regex = new RegExp(pattern, flags);
} catch (e) {
return { success: false, error: `Invalid regex: ${e.message}`, pattern, flags };
}
const results = inputs.map(input => {
regex.lastIndex = 0;
const isGlobal = regex.global;
const matches = [];
if (isGlobal) {
let match;
while ((match = regex.exec(input)) !== null) {
matches.push(this._formatMatch(match));
if (match[0].length === 0) regex.lastIndex++;
}
} else {
const match = regex.exec(input);
if (match) matches.push(this._formatMatch(match));
}
return { input, matched: matches.length > 0, matchCount: matches.length, matches };
});
return {
success: true, pattern, flags,
summary: `${results.filter(r => r.matched).length}/${inputs.length} inputs matched`,
results,
};
},
init(extCtx) {
const self = this;
extCtx.tools.handle('regex_test', async (args) => {
return self._test(args.pattern, args.flags || '', args.inputs || []);
});
},
destroy() {}
});
return ctx;
}
it('registers regex_test tool handler', async () => {
const ctx = loadWithRegex();
await ctx.Extensions.initAll();
assert.ok(ctx.Extensions._toolHandlers.has('regex_test'));
});
it('matches simple patterns', async () => {
const ctx = loadWithRegex();
await ctx.Extensions.initAll();
const ext = ctx.Extensions._registry.get('regex-tester').def;
const result = ext._test('^\\d+$', '', ['123', 'abc', '456']);
assert.strictEqual(result.success, true);
assert.strictEqual(result.summary, '2/3 inputs matched');
assert.strictEqual(result.results[0].matched, true);
assert.strictEqual(result.results[1].matched, false);
assert.strictEqual(result.results[2].matched, true);
});
it('captures groups', async () => {
const ctx = loadWithRegex();
await ctx.Extensions.initAll();
const ext = ctx.Extensions._registry.get('regex-tester').def;
const result = ext._test('(\\d{3})-(\\d{4})', '', ['555-1234']);
assert.strictEqual(result.success, true);
assert.strictEqual(result.results[0].matches[0].fullMatch, '555-1234');
assert.deepStrictEqual(result.results[0].matches[0].groups, ['555', '1234']);
});
it('handles global flag with multiple matches', async () => {
const ctx = loadWithRegex();
await ctx.Extensions.initAll();
const ext = ctx.Extensions._registry.get('regex-tester').def;
const result = ext._test('\\d+', 'g', ['a1b2c3']);
assert.strictEqual(result.results[0].matchCount, 3);
assert.strictEqual(result.results[0].matches[0].fullMatch, '1');
assert.strictEqual(result.results[0].matches[1].fullMatch, '2');
assert.strictEqual(result.results[0].matches[2].fullMatch, '3');
});
it('returns error for invalid patterns', async () => {
const ctx = loadWithRegex();
await ctx.Extensions.initAll();
const ext = ctx.Extensions._registry.get('regex-tester').def;
const result = ext._test('[invalid', '', ['test']);
assert.strictEqual(result.success, false);
assert.ok(result.error.includes('Invalid regex'));
});
it('handles named groups', async () => {
const ctx = loadWithRegex();
await ctx.Extensions.initAll();
const ext = ctx.Extensions._registry.get('regex-tester').def;
const result = ext._test('(?\\d{4})-(?\\d{2})', '', ['2025-02']);
assert.strictEqual(result.results[0].matches[0].namedGroups.year, '2025');
assert.strictEqual(result.results[0].matches[0].namedGroups.month, '02');
});
it('routes through tool bridge', async () => {
const ctx = loadWithRegex();
await ctx.Extensions.initAll();
let resultEmitted = null;
ctx.Events.on('tool.result.regex-1', (payload) => {
resultEmitted = payload;
});
ctx.Events.emit('tool.call.regex-1', {
call_id: 'regex-1',
tool: 'regex_test',
arguments: { pattern: '^hello', flags: 'i', inputs: ['Hello world', 'goodbye'] },
}, { localOnly: true });
await new Promise(r => setTimeout(r, 10));
assert.ok(resultEmitted);
const parsed = JSON.parse(resultEmitted.result);
assert.strictEqual(parsed.success, true);
assert.strictEqual(parsed.summary, '1/2 inputs matched');
});
});