const { describe, it } = require('node:test'); const assert = require('node:assert'); const fs = require('fs'); const path = require('path'); const { createBrowserContext, loadSource, SRC } = require('./helpers'); const vm = require('vm'); // Skip all tests if the source file doesn't exist. const EXTENSIONS_EXISTS = fs.existsSync(path.join(SRC, 'extensions.js')); const maybeDescribe = EXTENSIONS_EXISTS ? describe : describe.skip; /** * Load Events + Extensions into a browser context. */ function loadExtensions(overrides = {}) { const sandbox = createBrowserContext(overrides); loadSource(sandbox, 'sb.js'); 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 // ═══════════════════════════════════════════ maybeDescribe('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 = `
Rendering math…
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) // ═══════════════════════════════════════════ maybeDescribe('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) // ═══════════════════════════════════════════ maybeDescribe('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'); }); });