482 lines
20 KiB
JavaScript
482 lines
20 KiB
JavaScript
#!/usr/bin/env node
|
||
'use strict';
|
||
|
||
// 该回归直接调用生产运行时 factory / 事件入口,确认完整采集不再被预览限额裁剪。
|
||
// WebSocket 仅为内存接收器;本脚本不读写真实会话,也不启动外部进程。
|
||
|
||
const assert = require('node:assert/strict');
|
||
const { execFileSync } = require('node:child_process');
|
||
const fs = require('node:fs');
|
||
const Module = require('node:module');
|
||
const path = require('node:path');
|
||
const { createAgentRuntime } = require('../lib/agent-runtime');
|
||
const { createCodexAppRuntime } = require('../lib/codex-app-runtime');
|
||
|
||
const REPO_DIR = path.join(__dirname, '..');
|
||
const SERVER_PATH = path.join(REPO_DIR, 'server.js');
|
||
const PERSIST_TRUNCATED_TEXT = '[cc-web: 内容过大,已截断以保护服务稳定性]';
|
||
const APP_TRUNCATED_TEXT = '[cc-web: 内容过长,已截断以保护服务稳定性]';
|
||
const EXPECTED_BASELINE_FAILURES = [
|
||
'Claude user/tool_result 长结果完整采集',
|
||
'Codex CLI 正文与 JSON 内容块混合采集',
|
||
];
|
||
const invalidEventHandler = () => {
|
||
throw new Error('运行时完整采集回归不应调用 saveSession');
|
||
};
|
||
|
||
function createRuntimeHarness(options = {}) {
|
||
const sent = [];
|
||
const ws = {};
|
||
const previewApi = buildProductionPreviewApi();
|
||
const wsSend = (target, payload) => previewApi.sendRuntimePreview(target, payload);
|
||
const agentRuntimeFactory = options.createAgentRuntime || createAgentRuntime;
|
||
const codexAppRuntimeFactory = options.createCodexAppRuntime || createCodexAppRuntime;
|
||
const cliRuntime = agentRuntimeFactory({
|
||
processEnv: {},
|
||
wsSend,
|
||
sanitizeToolInput: previewApi.sanitizeToolInput,
|
||
loadSession: () => null,
|
||
saveSession: invalidEventHandler,
|
||
});
|
||
const codexAppRuntime = codexAppRuntimeFactory({
|
||
wsSend,
|
||
loadSession: () => null,
|
||
saveSession: invalidEventHandler,
|
||
});
|
||
return { cliRuntime, codexAppRuntime, previewApi, ws, sent };
|
||
}
|
||
|
||
function entry(agent, ws) {
|
||
return { agent, ws, fullText: '', toolCalls: [] };
|
||
}
|
||
|
||
function extractFunctionSource(source, name) {
|
||
const start = source.indexOf(`function ${name}(`);
|
||
assert(start >= 0, `server.js 应定义 ${name}`);
|
||
let parenDepth = 0;
|
||
let signatureEnd = -1;
|
||
for (let index = start; index < source.length; index += 1) {
|
||
if (source[index] === '(') parenDepth += 1;
|
||
if (source[index] === ')') {
|
||
parenDepth -= 1;
|
||
if (parenDepth === 0) {
|
||
signatureEnd = index;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
assert(signatureEnd > start, `${name} 应有完整签名`);
|
||
const open = source.indexOf('{', signatureEnd);
|
||
assert(open > start, `${name} 应有函数体`);
|
||
let depth = 0;
|
||
for (let index = open; index < source.length; index += 1) {
|
||
if (source[index] === '{') depth += 1;
|
||
if (source[index] === '}') {
|
||
depth -= 1;
|
||
if (depth === 0) return source.slice(start, index + 1);
|
||
}
|
||
}
|
||
throw new Error(`${name} 函数体不完整`);
|
||
}
|
||
|
||
function extractConstSource(source, name) {
|
||
const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||
const match = source.match(new RegExp(`^const ${escapedName} = .*;$`, 'm'));
|
||
assert(match, `server.js 应定义 ${name}`);
|
||
return match[0];
|
||
}
|
||
|
||
function buildProductionPreviewApi() {
|
||
const source = fs.readFileSync(SERVER_PATH, 'utf8');
|
||
const functionNames = [
|
||
'readPositiveIntEnv',
|
||
'truncateTextValue',
|
||
'sanitizePersistValue',
|
||
'sanitizeToolCallsForPersist',
|
||
'sanitizeMessageForPersist',
|
||
'sanitizeMessageForTransport',
|
||
'safeJsonParse',
|
||
'sanitizeToolInput',
|
||
'sendRuntimePreview',
|
||
];
|
||
const constantNames = [
|
||
'SESSION_MESSAGE_CONTENT_MAX_CHARS',
|
||
'SESSION_TOOL_INPUT_MAX_CHARS',
|
||
'SESSION_TOOL_RESULT_MAX_CHARS',
|
||
'SESSION_MAX_TOOL_CALLS_PER_MESSAGE',
|
||
'SESSION_TRANSPORT_MESSAGE_CONTENT_MAX_CHARS',
|
||
'SESSION_TRANSPORT_TOOL_INPUT_MAX_CHARS',
|
||
'SESSION_TRANSPORT_TOOL_RESULT_MAX_CHARS',
|
||
'SESSION_TRANSPORT_MAX_TOOL_CALLS_PER_MESSAGE',
|
||
'PERSIST_TRUNCATED_TEXT',
|
||
];
|
||
return new Function('wsSend', `
|
||
const process = { env: {} };
|
||
${functionNames.map((name) => extractFunctionSource(source, name)).join('\n')}
|
||
${constantNames.map((name) => extractConstSource(source, name)).join('\n')}
|
||
return { sendRuntimePreview, sanitizeMessageForTransport, sanitizeToolInput };
|
||
`)((target, payload) => target.received.push(payload));
|
||
}
|
||
|
||
function assertPreviewStringIsBounded(value, maxChars, label) {
|
||
assert.equal(typeof value, 'string', `${label}预览应保留字符串形状`);
|
||
assert(value.length <= maxChars, `${label}预览不应超过 ${maxChars} 个字符`);
|
||
assert(
|
||
value.includes(PERSIST_TRUNCATED_TEXT) || value.includes(APP_TRUNCATED_TEXT),
|
||
`${label}预览应标记被省略的内容,不能伪装成完整 record`
|
||
);
|
||
}
|
||
|
||
function assertPreviewIsBounded(value, maxChars, label) {
|
||
assert(value !== null && typeof value === 'object', `${label}预览应是对象`);
|
||
assertPreviewStringIsBounded(value.prompt, maxChars, `${label}正文`);
|
||
assert(value.rows.length <= 81, `${label}数组预览应保持有界`);
|
||
assert(
|
||
value.rows.some((row) => typeof row?.__truncated === 'string'),
|
||
`${label}数组预览应标注省略项目数`
|
||
);
|
||
}
|
||
|
||
function loadGitHeadFactory(relativePath, exportName) {
|
||
const source = execFileSync('git', ['show', `HEAD:${relativePath}`], {
|
||
cwd: REPO_DIR,
|
||
encoding: 'utf8',
|
||
});
|
||
const filename = path.join(REPO_DIR, relativePath);
|
||
const loadedModule = new Module(filename, module);
|
||
loadedModule.filename = filename;
|
||
loadedModule.paths = Module._nodeModulePaths(path.dirname(filename));
|
||
loadedModule._compile(source, filename);
|
||
assert.equal(typeof loadedModule.exports[exportName], 'function', `HEAD:${relativePath} 应导出 ${exportName}`);
|
||
return loadedModule.exports[exportName];
|
||
}
|
||
|
||
function assertRuntimeHistoryContract(options = {}) {
|
||
const { cliRuntime, codexAppRuntime, previewApi, ws, sent } = createRuntimeHarness(options);
|
||
ws.received = sent;
|
||
const longText = '长正文'.repeat(350000) + '[正文尾部]';
|
||
const longToolInput = '工具参数'.repeat(6000) + '[参数尾部]';
|
||
const failures = [];
|
||
const passed = [];
|
||
const check = (name, callback) => {
|
||
try {
|
||
callback();
|
||
passed.push(name);
|
||
console.log(`通过:${name}`);
|
||
} catch (error) {
|
||
failures.push(`${name}:${error.message}`);
|
||
console.error(`失败:${name}:${error.message}`);
|
||
}
|
||
};
|
||
|
||
const claudeEntry = entry('claude', ws);
|
||
check('Claude CLI 长正文完整采集', () => {
|
||
cliRuntime.processRuntimeEvent(claudeEntry, {
|
||
type: 'assistant',
|
||
message: { content: [{ type: 'text', text: longText }] },
|
||
}, 'fixture-full');
|
||
assert.equal(claudeEntry.fullText, longText, 'Claude 1,050,006 字符正文应原样进入生产 runtime 记录');
|
||
assert(!claudeEntry.fullText.includes(PERSIST_TRUNCATED_TEXT), 'Claude 完整正文不应包含裁剪标记');
|
||
});
|
||
|
||
const codexEntry = entry('codex', ws);
|
||
check('Codex CLI 长正文完整采集', () => {
|
||
cliRuntime.processRuntimeEvent(codexEntry, {
|
||
type: 'item.completed',
|
||
item: { id: 'codex-message', type: 'agent_message', text: longText },
|
||
}, 'fixture-full');
|
||
assert.equal(codexEntry.fullText, longText, 'Codex 1,050,006 字符正文应原样进入生产 runtime 记录');
|
||
assert(!codexEntry.fullText.includes(PERSIST_TRUNCATED_TEXT), 'Codex 完整正文不应包含裁剪标记');
|
||
});
|
||
|
||
const codexAppEntry = entry('codexapp', ws);
|
||
const appTextStartIndex = sent.length;
|
||
check('Codex App 长正文完整采集', () => {
|
||
codexAppRuntime.processCodexAppNotification(codexAppEntry, {
|
||
method: 'item/agentMessage/delta',
|
||
params: { itemId: 'codexapp-message', delta: longText },
|
||
}, 'fixture-full');
|
||
assert.equal(codexAppEntry.fullText, longText, 'Codex App 1,050,006 字符增量应原样进入生产记录');
|
||
assert(
|
||
!codexAppEntry.fullText.includes(PERSIST_TRUNCATED_TEXT),
|
||
'Codex App 完整正文不应包含裁剪标记'
|
||
);
|
||
});
|
||
|
||
const fullToolResult = '完整工具结果'.repeat(4200) + '[结果尾部]';
|
||
const claudeToolEntry = entry('claude', ws);
|
||
cliRuntime.processRuntimeEvent(claudeToolEntry, {
|
||
type: 'assistant',
|
||
message: {
|
||
content: [{
|
||
type: 'tool_use',
|
||
id: 'claude-tool',
|
||
name: 'Read',
|
||
input: JSON.stringify({ path: '/fixture.txt', prompt: longToolInput }),
|
||
}],
|
||
},
|
||
}, 'fixture-tool');
|
||
check('Claude CLI JSON 字符串工具参数使用生产解析并完整保留', () => {
|
||
const parsedInput = claudeToolEntry.toolCalls.find((tool) => tool.id === 'claude-tool')?.input;
|
||
assert.equal(parsedInput?.path, '/fixture.txt', 'Claude 工具 JSON 输入应由生产 sanitizer 解析为对象');
|
||
assert.equal(parsedInput?.prompt, longToolInput, '生产输入解析不应裁剪工具参数长字符串');
|
||
});
|
||
|
||
check('Claude user/tool_result 长结果完整采集', () => {
|
||
cliRuntime.processRuntimeEvent(claudeToolEntry, {
|
||
type: 'user',
|
||
message: {
|
||
content: [{ type: 'tool_result', tool_use_id: 'claude-tool', content: fullToolResult }],
|
||
},
|
||
}, 'fixture-tool');
|
||
const claudeTool = claudeToolEntry.toolCalls.find((tool) => tool.id === 'claude-tool');
|
||
assert.equal(claudeTool?.done, true, 'Claude user/tool_result 应结束对应工具调用');
|
||
assert.equal(claudeTool?.result, fullToolResult, 'Claude 完整工具结果应在用户事件中进入生产记录');
|
||
});
|
||
|
||
const codexToolEntry = entry('codex', ws);
|
||
check('Codex CLI 长工具结果完整采集', () => {
|
||
cliRuntime.processRuntimeEvent(codexToolEntry, {
|
||
type: 'item.completed',
|
||
item: {
|
||
id: 'codex-tool',
|
||
type: 'command_execution',
|
||
command: 'printf fixture',
|
||
status: 'completed',
|
||
aggregated_output: fullToolResult,
|
||
},
|
||
}, 'fixture-tool');
|
||
assert.equal(
|
||
codexToolEntry.toolCalls.find((tool) => tool.id === 'codex-tool')?.result,
|
||
fullToolResult,
|
||
'Codex CLI 完整聚合输出应在生产记录中原样保留'
|
||
);
|
||
});
|
||
|
||
const codexLongInput = 'Codex 命令参数'.repeat(5000) + '[参数尾部]';
|
||
const codexInputEntry = entry('codex', ws);
|
||
check('Codex CLI 长工具输入完整采集', () => {
|
||
cliRuntime.processRuntimeEvent(codexInputEntry, {
|
||
type: 'item.started',
|
||
item: {
|
||
id: 'codex-long-command',
|
||
type: 'command_execution',
|
||
command: codexLongInput,
|
||
},
|
||
}, 'fixture-tool-input');
|
||
assert.equal(
|
||
codexInputEntry.toolCalls[0]?.input?.command,
|
||
codexLongInput,
|
||
'Codex CLI 完整工具输入不应受传输预览上限影响'
|
||
);
|
||
});
|
||
|
||
const longAppToolResult = '工具返回'.repeat(12000) + '[工具尾部]';
|
||
const codexAppToolEntry = entry('codexapp', ws);
|
||
const largeTool = {
|
||
id: 'codexapp-large-tool',
|
||
type: 'mcpToolCall',
|
||
server: 'fixture',
|
||
tool: 'long_payload',
|
||
arguments: {
|
||
prompt: longToolInput,
|
||
rows: Array.from({ length: 150 }, (_, index) => ({ index, values: [index, index + 1] })),
|
||
},
|
||
result: { content: [{ type: 'text', text: longAppToolResult }] },
|
||
};
|
||
check('Codex App 超限工具输入、结果与数组完整采集', () => {
|
||
codexAppRuntime.processCodexAppNotification(codexAppToolEntry, {
|
||
method: 'item/completed',
|
||
params: { item: largeTool },
|
||
}, 'fixture-tool');
|
||
assert.deepEqual(
|
||
codexAppToolEntry.toolCalls[0]?.input.arguments,
|
||
largeTool.arguments,
|
||
'Codex App 完整输入对象、长字符串和大数组都应在 entry 原样保留'
|
||
);
|
||
assert.equal(
|
||
codexAppToolEntry.toolCalls[0]?.result,
|
||
longAppToolResult,
|
||
'Codex App 完整工具结果应原样序列化'
|
||
);
|
||
const largeToolInputPreview = sent.find((payload) => payload.toolUseId === 'codexapp-large-tool' && payload.type === 'tool_start');
|
||
const largeToolResultPreview = sent.find((payload) => payload.toolUseId === 'codexapp-large-tool' && payload.type === 'tool_end');
|
||
assert(largeToolInputPreview, 'Codex App 启动的大工具调用应产生真实 WebSocket 输入预览');
|
||
assert(largeToolResultPreview, 'Codex App 完成的大工具调用应产生真实 WebSocket 结果预览');
|
||
assertPreviewIsBounded(largeToolInputPreview.input.arguments, 8 * 1024, 'Codex App 工具输入');
|
||
assertPreviewStringIsBounded(largeToolResultPreview.result, 16 * 1024, 'Codex App 工具结果');
|
||
});
|
||
|
||
const manyToolsEntry = entry('codexapp', ws);
|
||
const toolLimit = 40;
|
||
const toolCount = 130;
|
||
check(`超出 WebSocket 预览条数上限 ${toolLimit} 的 ${toolCount} 条工具仍完整采集`, () => {
|
||
for (let index = 0; index < toolCount; index += 1) {
|
||
codexAppRuntime.processCodexAppNotification(manyToolsEntry, {
|
||
method: 'item/completed',
|
||
params: {
|
||
item: {
|
||
id: `codexapp-tool-${index}`,
|
||
type: 'mcpToolCall',
|
||
server: 'fixture',
|
||
tool: 'short_payload',
|
||
arguments: { index, text: `输入-${index}` },
|
||
result: { content: [{ type: 'text', text: `结果-${index}` }] },
|
||
},
|
||
},
|
||
}, 'fixture-many-tools');
|
||
}
|
||
assert.equal(
|
||
manyToolsEntry.toolCalls.length,
|
||
toolCount,
|
||
`超过传输预览条数上限 ${toolLimit} 的 ${toolCount} 条工具都应保留在 entry`
|
||
);
|
||
assert.equal(manyToolsEntry.toolCalls[0]?.id, 'codexapp-tool-0', '工具数组应保留首项及原始顺序');
|
||
assert.equal(
|
||
manyToolsEntry.toolCalls[toolCount - 1]?.result,
|
||
`结果-${toolCount - 1}`,
|
||
'工具数组应保留最后一项完整结果'
|
||
);
|
||
assert.equal(
|
||
sent.filter((payload) => payload.type === 'tool_end' && /^codexapp-tool-/.test(payload.toolUseId)).length,
|
||
toolCount,
|
||
'每条工具事件均应发送生产预览,完整采集不可让工具数超限跳过事件'
|
||
);
|
||
});
|
||
|
||
const mixedCodexEntry = entry('codex', ws);
|
||
check('Codex CLI 正文与 JSON 内容块混合采集', () => {
|
||
const mixedParts = [
|
||
['mixed-1', '开头正文'],
|
||
['mixed-2', JSON.stringify([{ type: 'text', text: '内容块中正文' }])],
|
||
['mixed-3', '末尾正文'],
|
||
];
|
||
for (const [itemId, text] of mixedParts) {
|
||
cliRuntime.processRuntimeEvent(mixedCodexEntry, {
|
||
type: 'item.completed',
|
||
item: { id: itemId, type: 'agent_message', text },
|
||
}, 'fixture-mixed');
|
||
}
|
||
assert(mixedCodexEntry.fullText.includes('开头正文'), 'Codex CLI 完整正文应保留 JSON 内容块之前的文本');
|
||
assert(mixedCodexEntry.fullText.includes('末尾正文'), 'Codex CLI 完整正文应保留 JSON 内容块之后的文本');
|
||
assert(
|
||
mixedCodexEntry.contentBlocks?.some((block) => block.text === '内容块中正文'),
|
||
'Codex CLI 正文数组应保留完整的 JSON 内容块'
|
||
);
|
||
const consumerSelectedContent = mixedCodexEntry.contentBlocks || mixedCodexEntry.fullText;
|
||
const selectedText = typeof consumerSelectedContent === 'string'
|
||
? consumerSelectedContent
|
||
: JSON.stringify(consumerSelectedContent);
|
||
assert(
|
||
selectedText.includes('开头正文') && selectedText.includes('末尾正文'),
|
||
'Codex CLI 混合记录按生产完成路径选择 contentBlocks || fullText 时应不丢两端文本'
|
||
);
|
||
});
|
||
|
||
const completedAppEntry = entry('codexapp', ws);
|
||
check('Codex App 增量与完成事件补尾且不重复', () => {
|
||
codexAppRuntime.processCodexAppNotification(completedAppEntry, {
|
||
method: 'item/agentMessage/delta',
|
||
params: { itemId: 'codexapp-finished-message', delta: '流式正文' },
|
||
}, 'fixture-complete');
|
||
codexAppRuntime.processCodexAppNotification(completedAppEntry, {
|
||
method: 'item/completed',
|
||
params: {
|
||
item: {
|
||
id: 'codexapp-finished-message',
|
||
type: 'agentMessage',
|
||
text: '流式正文与完成事件补尾',
|
||
},
|
||
},
|
||
}, 'fixture-complete');
|
||
assert.equal(
|
||
completedAppEntry.fullText,
|
||
'流式正文与完成事件补尾',
|
||
'完成事件只应将尚未收到的尾部追加到完整正文'
|
||
);
|
||
});
|
||
|
||
check('生产 WebSocket 工具预览有界且 Codex App 正文分片完整', () => {
|
||
const cliWireText = sent.find((payload) => payload.type === 'text_delta' && payload.text?.includes('长正文'));
|
||
assert(cliWireText, 'CLI 长正文应发出真实 WebSocket 增量');
|
||
assertPreviewStringIsBounded(cliWireText.text, 16 * 1024, 'CLI 文本增量');
|
||
const claudeWireResult = sent.find((payload) => payload.type === 'tool_end' && payload.toolUseId === 'claude-tool');
|
||
if (claudeWireResult) assertPreviewStringIsBounded(claudeWireResult.result, 16 * 1024, 'Claude 工具结果');
|
||
else assert(failures.includes('Claude user/tool_result 长结果完整采集'), 'Claude 用户工具结果缺少 tool_end 时应由对应事件断言报告');
|
||
const appChunks = sent.slice(appTextStartIndex).filter((payload) => (
|
||
payload.type === 'text_delta'
|
||
&& payload.sessionId === 'fixture-full'
|
||
));
|
||
assert(appChunks.length > 1, 'Codex App 大增量应分片发送');
|
||
assert(appChunks.every((payload) => payload.text.length <= 16 * 1024), 'Codex App 每片正文保持传输大小上限');
|
||
assert.equal(appChunks.map((payload) => payload.text).join(''), longText, 'Codex App 所有分片拼接后完整保留正文');
|
||
});
|
||
|
||
check('完成及历史正文读取保留 130 条工具和完整输入结果', () => {
|
||
// 历史与 done 读取完整记录;实时 tool_start/tool_end 的预览上限另有断言。
|
||
const completeTools = manyToolsEntry.toolCalls.map((tool, index) => index === 65
|
||
? { ...tool, input: largeTool.arguments, result: longAppToolResult }
|
||
: tool);
|
||
const transportMessage = previewApi.sanitizeMessageForTransport({
|
||
role: 'assistant',
|
||
content: longText,
|
||
toolCalls: completeTools,
|
||
});
|
||
assert.equal(transportMessage.content, longText, '历史与完成是正文读取入口,必须保留完整正文供折叠和复制');
|
||
assert.equal(transportMessage.toolCalls.length, toolCount, '完成和历史不可用工具预览条数上限替代完整列表');
|
||
assert.deepEqual(transportMessage.toolCalls, completeTools, '完整工具数量、顺序、输入对象和结果不得缩水');
|
||
assert.equal(transportMessage.toolCalls[65].input.prompt, longToolInput, '完成及历史工具输入长字符串完整读回');
|
||
assert.equal(transportMessage.toolCalls[65].result, longAppToolResult, '完成及历史工具结果长字符串完整读回');
|
||
assert(!transportMessage.toolCalls.some(tool => tool.id === 'ccweb-toolcalls-truncated'), '完整读取不得注入工具省略标记');
|
||
});
|
||
|
||
if (failures.length > 0 && !options.returnFailures) {
|
||
throw new Error(`运行时完整采集回归发现 ${failures.length} 个失败:\n${failures.map((message) => `- ${message}`).join('\n')}`);
|
||
}
|
||
return {
|
||
checkedTextChars: longText.length,
|
||
checkedToolInputChars: longToolInput.length,
|
||
checkedToolResultChars: longAppToolResult.length,
|
||
checkedToolCalls: toolCount,
|
||
webSocketPreviews: sent.length,
|
||
passed,
|
||
failures,
|
||
};
|
||
}
|
||
|
||
function runBaselineHead() {
|
||
const result = assertRuntimeHistoryContract({
|
||
createAgentRuntime: loadGitHeadFactory('lib/agent-runtime.js', 'createAgentRuntime'),
|
||
createCodexAppRuntime: loadGitHeadFactory('lib/codex-app-runtime.js', 'createCodexAppRuntime'),
|
||
returnFailures: true,
|
||
});
|
||
const failedNames = new Set(result.failures.map((failure) => failure.split(':', 1)[0]));
|
||
const missingProofs = EXPECTED_BASELINE_FAILURES.filter((name) => !failedNames.has(name));
|
||
assert.equal(
|
||
missingProofs.length,
|
||
0,
|
||
`HEAD 基线未复现预期缺陷:${missingProofs.join('、') || '未知'}`
|
||
);
|
||
console.log(`通过:HEAD 旧版已复现预期失败(${JSON.stringify(result.failures)})`);
|
||
}
|
||
|
||
function run() {
|
||
if (process.argv.includes('--baseline-head')) {
|
||
runBaselineHead();
|
||
return;
|
||
}
|
||
const result = assertRuntimeHistoryContract();
|
||
console.log(`通过:生产 runtime 完整采集与有界传输(${JSON.stringify({
|
||
checkedTextChars: result.checkedTextChars,
|
||
checkedToolInputChars: result.checkedToolInputChars,
|
||
checkedToolResultChars: result.checkedToolResultChars,
|
||
checkedToolCalls: result.checkedToolCalls,
|
||
webSocketPreviews: result.webSocketPreviews,
|
||
passed: result.passed.length,
|
||
})})`);
|
||
}
|
||
|
||
if (require.main === module) run();
|
||
|
||
module.exports = { assertRuntimeHistoryContract };
|