import assert from 'node:assert/strict'; import {randomUUID} from 'node:crypto'; import {EventEmitter} from 'node:events'; import {createRequire, isBuiltin} from 'node:module'; import {fileURLToPath} from 'node:url'; import test from 'node:test'; import {build} from 'esbuild'; const root = fileURLToPath(new URL('../', import.meta.url)); const require = createRequire(import.meta.url); async function bundle(mock_catalog = false) { const compiled = await build({ stdin: {contents: `export * from './bot/forward/stream.ts'; export * from './bot/forward/manager.ts'; export * from './utils/forward_protocol.ts'; export * from './bot/workflow/catalog.ts'; export * from './service/server/update/workflow_catalog.ts'; export {report_logs} from './utils/console_print.ts';`, resolveDir: root, loader: 'ts'}, bundle: true, platform: 'node', format: 'cjs', write: false, metafile: true, plugins: [{name: 'isolated-node', setup(build) { build.onResolve({filter: /(?:instance\/index\.ts|^socket\.io-client$|console_print\.ts|path_utils\.ts)$/}, args => ({path: args.path.includes('instance/') ? 'registry' : args.path === 'socket.io-client' ? 'socket' : args.path.endsWith('path_utils.ts') ? 'path' : 'console', namespace: 'stub'})); if (mock_catalog) build.onResolve({filter: /(?:^|\/)workflow\/catalog\.ts$/}, () => ({path: 'catalog', namespace: 'stub'})); build.onLoad({filter: /.*/, namespace: 'stub'}, ({path}) => ({loader: 'js', contents: { registry: 'export const get_bot_context = () => undefined; export const observe_bot_context = () => () => {};', socket: 'export const io = () => {throw Error("Unexpected real socket")};', console: 'export const report_logs = []; export const console_print = {success: x => x, log: x => report_logs.push(x)};', path: 'export const get_project_root_path = () => {throw Error("Unexpected filesystem access")};', catalog: 'let reader; export const set_catalog_reader = value => {reader = value}; export const get_workflow_catalog = (...args) => reader(...args);', }[path]})); }}], }); assert.ok(Object.keys(compiled.metafile.inputs).every(path => !path.includes('forward_server'))); const module = {exports: {}}; new Function('require', 'module', 'exports', compiled.outputFiles[0].text)(specifier => { assert.ok(isBuiltin(specifier), `Unexpected external import: ${specifier}`); return require(specifier); }, module, module.exports); return module.exports; } const real = await bundle(); const controlled = await bundle(true); const {BotStream, validate_rpc, validate_workflow_payload, error_ack} = real; const fails = (action, code = 'INVALID_REQUEST') => assert.throws(action, error => error.code === code); const request = (payload = {}, scope = randomUUID(), deadline_at = Date.now() + 1000) => ({ request_id: randomUUID(), correlation_id: randomUUID(), method: 'workflow.catalog', payload, forward_session_id: scope, deadline_at, }); function stream(t, api = real, context, now = () => 100) { const value = new api.BotStream(1, {now, source: {get: () => context, observe: () => () => {}}}); t.after(() => value.dispose()); return value; } const sample = () => ({catalog_version: 1, nodes: [{name: 'sample', config: [{name: 'nested', default: {value: 1}}]}], start_nodes: [], events: [], queries: [], common: {references: []}}); test('catalog RPC accepts only an exact empty payload, including direct stream validation', t => { const value = stream(t); assert.equal(validate_rpc(request()).method, 'workflow.catalog'); assert.deepEqual(validate_workflow_payload('workflow.catalog', Object.create(null)), Object.create(null)); let reads = 0; const accessor = Object.defineProperty({}, 'run_id', {enumerable: true, get() { reads++; return randomUUID(); }}); for (const payload of [undefined, null, [], 1, 'text', new Date(), Object.create({inherited: true}), {run_id: randomUUID()}, {run_id: undefined}, {allow_code: true}, {extra: true}, {[Symbol('extra')]: true}, Object.defineProperty({}, 'hidden', {value: true}), accessor]) { const raw = {...request(), payload}; fails(() => validate_rpc(raw)); fails(() => validate_workflow_payload('workflow.catalog', payload)); fails(() => value.workflow_catalog(payload, 200)); } assert.equal(reads, 0); fails(() => validate_workflow_payload('workflow.unknown', {})); assert.deepEqual(validate_workflow_payload('workflow.snapshot', {}), {}); const run_id = randomUUID(); assert.deepEqual(validate_workflow_payload('workflow.snapshot', {run_id}), {run_id}); fails(() => validate_workflow_payload('workflow.snapshot', {run_id, extra: true})); }); test('real catalog bundles independently and works offline or without any context, returning isolated clones', t => { const absent = stream(t); const offline = stream(t, real, Object.assign(new EventEmitter(), {bot_status: 'offline', bot_instance: null})); const expected = real.get_workflow_catalog(); const result = absent.workflow_catalog({}, 200); assert.equal(result.catalog_version, 1); assert.ok(result.nodes.length > 0); assert.deepEqual(result.nodes.find(node => node.name === 'code').availability, {available: false, reason: 'CODE_EXECUTION_DISABLED'}); assert.deepEqual(result, expected); assert.deepEqual(offline.workflow_catalog({}, 200), expected); assert.notEqual(result, expected); result.nodes[0].name = 'mutated'; result.common.references.length = 0; assert.deepEqual(absent.workflow_catalog({}, 200), expected); }); test('catalog reads pass no trusted code option and clone nested catalog values', t => { const original = sample(); controlled.set_catalog_reader((...args) => { assert.deepEqual(args, []); return original; }); const value = stream(t, controlled); const result = value.workflow_catalog({}, 200); assert.deepEqual(result, original); result.nodes[0].config[0].default.value = 2; assert.equal(original.nodes[0].config[0].default.value, 1); }); test('catalog deadlines, transport and disposal are checked before and after the read', t => { let now = 100; let reads = 0; controlled.set_catalog_reader(() => { reads++; return sample(); }); const value = stream(t, controlled, undefined, () => now); for (const deadline of [100, 99, NaN, Infinity, 1.5, undefined]) fails(() => value.workflow_catalog({}, deadline), 'TIMEOUT'); const stale = () => { throw Object.assign(Error('SECRET transport'), {code: 'SESSION_STALE'}); }; fails(() => value.workflow_catalog({}, 200, stale), 'SESSION_STALE'); assert.equal(reads, 0); let checks = 0; controlled.set_catalog_reader(() => { assert.equal(checks, 1); now = 200; return sample(); }); fails(() => value.workflow_catalog({}, 200, () => checks++), 'TIMEOUT'); assert.equal(checks, 2); now = 100; controlled.set_catalog_reader(() => sample()); checks = 0; fails(() => value.workflow_catalog({}, 200, () => { if (++checks === 2) stale(); }), 'SESSION_STALE'); controlled.set_catalog_reader(() => { value.dispose(); return sample(); }); fails(() => value.workflow_catalog({}, 200), 'CANCELLED'); fails(() => value.workflow_catalog({}, 200), 'CANCELLED'); }); async function relay(t, value) { const command = {forward_id: 1, forward_session_id: randomUUID(), node_session_id: 'session', node_id: 1, bot_id: 1, url: 'http://127.0.0.1:1234', path: '/socket.io', namespace: '/', token: 'opaque', expires_at: Date.now() + 10_000, deadline_at: Date.now() + 2000}; const socket = new EventEmitter(); socket.auth = {}; socket.io = {engine: {writeBuffer: []}}; socket.volatile = {emit() {}}; socket.connect = () => { socket.connected = true; queueMicrotask(() => socket.emit('connect')); }; socket.disconnect = () => { socket.connected = false; socket.emit('disconnect'); }; socket.timeout = () => ({emit(event, _payload, ack) { const {forward_id, forward_session_id, node_session_id, node_id, bot_id, expires_at} = command; const data = event === 'forward_node_ready' ? {node_ready: true} : {forward_id, forward_session_id, node_session_id, node_id, bot_id, expires_at, role: 'node', node_ready: false}; queueMicrotask(() => ack(null, {status: 'success', data})); }}); const manager = new controlled.NodeForwardManager(1, 'session', {socket_factory: () => socket, stream_factory: () => value}); t.after(() => manager.stop()); assert.equal((await manager.ensure(command)).status, 'success'); const handler = socket.listeners('relay_request')[0]; return {socket, manager, rpc: (payload = {}, deadline = Date.now() + 1000) => new Promise(resolve => handler(request(payload, command.forward_session_id, deadline), resolve))}; } test('manager dispatches context-free catalog RPC and sanitizes stale transport and read failures', async t => { const catalog = sample(); let reads = 0; controlled.set_catalog_reader(() => { reads++; return catalog; }); const value = stream(t, controlled, undefined, Date.now); const channel = await relay(t, value); assert.deepEqual(await channel.rpc(), {status: 'success', data: catalog}); assert.deepEqual(await channel.rpc({run_id: randomUUID()}), error_ack('INVALID_REQUEST')); assert.deepEqual(await channel.rpc({}, Date.now() - 1), error_ack('TIMEOUT')); assert.equal(reads, 1); controlled.set_catalog_reader(() => { throw Error('SECRET catalog'); }); assert.deepEqual(await channel.rpc(), error_ack('INTERNAL_ERROR')); controlled.set_catalog_reader(() => { channel.socket.disconnect(); return catalog; }); assert.deepEqual(await channel.rpc(), error_ack('SESSION_STALE')); assert.deepEqual(await channel.rpc(), error_ack('SESSION_STALE')); }); function reporter(t, response = {status: 'success', data: 0}) { const reports = []; const logs = controlled.report_logs; logs.length = 0; t.after(() => { logs.length = 0; }); const catalog = sample(); controlled.set_catalog_reader((...args) => { assert.deepEqual(args, []); return catalog; }); const socket = {connected: true, timeout(ms) { assert.equal(ms, 5000); return this; }, async emitWithAck(...args) { reports.push(args); return response; }}; const state = {current: true}; return {socket, state, catalog, reports, logs, run: () => controlled.update_workflow_catalog(socket, 7, () => state.current)}; } test('catalog reporter emits node metadata, accepts success ACK and supports the default session guard', async t => { const app = reporter(t); await app.run(); assert.deepEqual(app.reports, [['update_workflow_catalog', {id: 7, workflow_catalog: app.catalog}]]); assert.equal(app.logs.length, 1); assert.equal(app.logs[0], '工作流目录更新成功'); app.socket.connected = false; await controlled.update_workflow_catalog(app.socket, 7); assert.equal(app.reports.length, 1); app.socket.connected = true; await controlled.update_workflow_catalog(app.socket, 7); assert.equal(app.reports.length, 2); }); test('catalog reporter safely rejects malformed/error ACKs, read errors and transport failures', async t => { for (const response of [null, {}, {status: 'unknown'}, {status: 'error', data: 'SECRET rejection'}]) { const app = reporter(t, response); await assert.rejects(app.run(), error => error.message === 'Workflow catalog report failed' && error.cause === undefined); assert.equal(app.logs.length, 0); } const app = reporter(t); app.socket.emitWithAck = async () => undefined; await assert.rejects(app.run(), /^Error: Workflow catalog report failed$/); app.socket.emitWithAck = async () => { throw Error('SECRET timeout'); }; await assert.rejects(app.run(), /^Error: Workflow catalog report failed$/); controlled.set_catalog_reader(() => { throw Error('SECRET catalog path'); }); await assert.rejects(app.run(), /^Error: Workflow catalog report failed$/); }); test('catalog reporter skips stale sessions before emission and ignores stale ACKs and rejections', async t => { for (const change of ['disconnect', 'session']) { const app = reporter(t); const stale = () => { if (change === 'disconnect') app.socket.connected = false; else app.state.current = false; }; stale(); await app.run(); assert.equal(app.reports.length, 0); app.socket.connected = app.state.current = true; controlled.set_catalog_reader(() => { stale(); return app.catalog; }); await app.run(); assert.equal(app.reports.length, 0); for (const reject of [false, true]) { app.socket.connected = app.state.current = true; controlled.set_catalog_reader(() => app.catalog); let finish; app.socket.emitWithAck = () => new Promise((resolve, fail) => { finish = reject ? fail : resolve; }); const task = app.run(); stale(); finish(reject ? Error('SECRET late ACK') : {status: 'success'}); await task; assert.equal(app.logs.length, 0); } } });