diff --git a/config/size-baseline.json b/config/size-baseline.json index 450bfe4..5c5d00a 100644 --- a/config/size-baseline.json +++ b/config/size-baseline.json @@ -46,7 +46,7 @@ "scripts/run-eval.mjs": 369, "scripts/run-query-smoke.mjs": 745, "scripts/setup.mjs": 103, - "src/agent.js": 6162, + "src/agent.js": 5864, "src/analysisArtifact.js": 82, "src/analysisContract.js": 309, "src/analysisIntent.js": 37, @@ -154,9 +154,6 @@ "src/oidcAuth.js": 128, "src/openingSuggestions.js": 115, "src/permissions.js": 311, - "src/platformAsyncRepositories.js": 514, - "src/platformBootstrap.js": 94, - "src/platformBackend.js": 31, "src/platformDatabase.js": 61, "src/platformLogRepository.js": 215, "src/presentationFallbacks.js": 63, @@ -201,9 +198,9 @@ "src/trace.js": 26, "src/turnContext.js": 726, "src/turnRouter.js": 143, - "src/uploads.js": 505, "src/uploadAdmission.js": 94, "src/userMemory.js": 637, + "src/uploads.js": 505, "src/userMemoryDistiller.js": 499, "src/userMemoryMaintenance.js": 156, "src/userMemoryModel.js": 138, @@ -213,12 +210,16 @@ "src/valueClarification.js": 186, "src/workflow.js": 230, "src/workspace.js": 464, - "src/workspaceTransforms.js": 189, - "src/workspaceRepository.js": 287, "src/xlsx.js": 369, "src/xlsxReader.js": 480, - "src/skillAudits.js": 119, + "src/agentToolPresentation.js": 255, + "src/platformAsyncRepositories.js": 514, + "src/platformBootstrap.js": 94, + "src/platformBackend.js": 31, + "src/workspaceTransforms.js": 189, + "src/workspaceRepository.js": 287, "src/platformAsyncLogRepository.js": 168, + "src/skillAudits.js": 119, "src/dataSourceRow.js": 25, "src/mcp/configValues.js": 34, "src/feedbackSubmission.js": 69, diff --git a/src/agent.js b/src/agent.js index 72b5c2b..ffae3dc 100644 --- a/src/agent.js +++ b/src/agent.js @@ -1,3 +1,4 @@ +import { toolStageCode, buildToolReview, describeToolStep } from './agentToolPresentation.js'; import { suggestChart } from './chart.js'; import { buildChartRestyleOutcome, @@ -1143,260 +1144,6 @@ function selectColumns(queryResult) { return queryResult?.columns ?? []; } -function toolStageCode(name) { - switch (name) { - case 'search_indicators': - case 'list_business_datasets': - return 'SEMANTIC_DISCOVERY'; - case 'get_indicator': - case 'get_business_dataset_definition': - return 'SEMANTIC_CONFIRM'; - case 'compile_query_contract': - return 'PLAN'; - case 'query_indicator': - case 'query_business_dataset': - case 'execute_query_contract': - return 'EXECUTE'; - case 'execute_analysis_code': - return 'CODE'; - case 'list_workspace_artifacts': - case 'transform_workspace_artifact': - return 'ANALYZE'; - default: - return 'EXECUTE'; - } -} - -function compactReviewValue(value, limit = 800) { - if (value === null || value === undefined || value === '') { - return ''; - } - const text = typeof value === 'string' - ? value - : JSON.stringify(value); - return text.length > limit ? `${text.slice(0, limit)}…` : text; -} - -function formatReviewDuration(value) { - const milliseconds = Number(value); - if (!Number.isFinite(milliseconds)) { - return ''; - } - if (milliseconds < 1000) { - return `${Math.max(0, Math.round(milliseconds))} ms`; - } - return `${(milliseconds / 1000).toFixed(2)} 秒`; -} - -function buildToolReview(name, args = {}, result = {}) { - const facts = []; - const addFact = (label, value) => { - const text = compactReviewValue(value); - if (text) { - facts.push({ label, value: text }); - } - }; - if (name === 'search_indicators') { - addFact('检索词', args.keyword); - addFact('命中', `${result.indicators?.length ?? 0} 个指标`); - } else if (name === 'get_indicator') { - addFact('指标', result.name); - addFact('业务口径', result.businessCaliber); - addFact('字段', `${result.metrics?.length ?? 0} 个指标 / ${result.dimensions?.length ?? 0} 个维度`); - addFact( - '默认值域', - `${(result.dimensions ?? []).filter((item) => (item.values ?? []).length > 0).length} 个字段已初始化`, - ); - } else if (name === 'list_business_datasets') { - addFact('数据集', `${result.datasets?.length ?? 0} 个`); - addFact('名称', (result.datasets ?? []).map((item) => item.name).join('、')); - } else if (name === 'get_business_dataset_definition') { - addFact('数据集', result.dataset?.name); - addFact('字段', `${result.fields?.length ?? 0} 个`); - addFact( - '默认值域', - `${(result.fields ?? []).filter((item) => (item.values ?? []).length > 0).length} 个字段已初始化`, - ); - } else if (name === 'compile_query_contract') { - addFact('契约', result.contractId ?? args.contractId); - addFact('条件', `${args.conditions?.length ?? 0} 条`); - addFact('指标', (args.metricFields ?? []).map((item) => item.field).join('、')); - addFact('维度', (args.dimensionFields ?? []).map((item) => item.field).join('、')); - addFact('过滤', `${args.filterFields?.length ?? 0} 条`); - addFact('时间窗口', `${args.timeWindows?.length ?? 0} 个`); - addFact('计算', args.calculation?.type ?? 'NONE'); - if (args.postProcessing) { - const parts = []; - if (args.postProcessing.groupValues) { - parts.push(`值归并 ${args.postProcessing.groupValues.groups?.length ?? 0} 组`); - } - if (args.postProcessing.periodComparison) { - parts.push(`同期计算 ${args.postProcessing.periodComparison.type}`); - } - if (args.postProcessing.pivot) { - parts.push('透视输出'); - } - addFact('后处理', parts.join(' / ')); - } - if (args.analysisPipeline?.stages?.length) { - addFact( - '分析管线', - `${args.analysisPipeline.stages.length} 个通用阶段:${ - args.analysisPipeline.stages.map((stage) => stage.type).join(' > ') - }`, - ); - } - } else if ( - name === 'query_indicator' - || name === 'query_business_dataset' - || name === 'execute_query_contract' - ) { - addFact('数据源', result.dataset?.name ?? result.indicator?.name ?? args.indicatorId ?? args.datasetId); - addFact('结果', `${result.rowCount ?? 0} 行 / ${result.columns?.length ?? 0} 列`); - addFact('查询指纹', result.queryFingerprint); - } else if (name === 'list_workspace_artifacts') { - addFact('产物', `${result.artifacts?.length ?? 0} 个`); - addFact('文件', (result.artifacts ?? []).map((item) => item.title).join('、')); - } else if (name === 'transform_workspace_artifact') { - addFact('操作', args.operation); - addFact('结果', `${result.rowCount ?? 0} 行 / v${result.version ?? '-'}`); - } else if (name === 'execute_analysis_code') { - addFact('目的', args.purpose); - addFact('运行', result.success ? '成功' : '失败'); - addFact('耗时', formatReviewDuration(result.durationMs)); - addFact('输入产物', `${result.inputArtifacts?.length ?? 0} 个`); - addFact('输出文件', (result.outputs ?? []).map((item) => item.fileName).join('、')); - } - - const details = {}; - if (name === 'compile_query_contract') { - details.conditions = args.conditions ?? []; - details.calculation = args.calculation ?? null; - details.postProcessing = args.postProcessing ?? null; - details.analysisPipeline = args.analysisPipeline ?? null; - } else if ( - name === 'query_indicator' - || name === 'query_business_dataset' - || name === 'execute_query_contract' - ) { - details.contractId = args.contractId ?? null; - details.columns = (result.columns ?? []).map((column) => column.name ?? column.bizName); - details.rowCount = result.rowCount ?? 0; - details.queryFingerprint = result.queryFingerprint ?? null; - } else if (name === 'list_workspace_artifacts') { - details.artifacts = (result.artifacts ?? []).map((item) => ({ - id: item.id, - title: item.title, - type: item.artifactType, - columns: (item.columns ?? []).map((column) => column.name), - })); - } else if (name === 'execute_analysis_code') { - details.runId = result.runId; - details.inputArtifacts = result.inputArtifacts ?? []; - details.outputs = result.outputs ?? []; - } - return { - facts, - details: Object.keys(details).length > 0 ? details : null, - code: name === 'execute_analysis_code' ? String(args.code ?? '') : '', - stdout: name === 'execute_analysis_code' ? String(result.stdout ?? '') : '', - stderr: name === 'execute_analysis_code' ? String(result.stderr ?? '') : '', - }; -} - -function describeToolStep(name, args, result) { - if (name === 'search_indicators') { - const count = result?.indicators?.length ?? 0; - return { - title: '检索主题指标', - detail: `关键词:${args.keyword || '当前问题'},命中 ${count} 个可用指标`, - }; - } - if (name === 'get_indicator') { - const valueDomainCount = (result?.dimensions ?? []) - .filter((dimension) => (dimension.values ?? []).length > 0).length; - return { - title: '确认指标口径与默认值域', - detail: `${result?.name ?? args.indicatorId},${ - result?.metrics?.length ?? 0 - } 个指标字段,${result?.dimensions?.length ?? 0} 个可用维度,${ - valueDomainCount - } 个字段完成默认值域初始化`, - }; - } - if (name === 'compile_query_contract') { - return { - title: result?.valid ? '编译查询契约' : '查询契约待修正', - detail: result?.valid - ? `契约 ${result.contractId.slice(0, 12)} 已通过,覆盖 ${ - result.coverage?.length ?? 0 - } 个问题片段` - : (result?.issues ?? []).map((issue) => issue.message).join(';') - || '契约未通过', - }; - } - if (name === 'execute_query_contract') { - return { - title: '执行查询契约', - detail: `${result?.indicator?.name ?? result?.dataset?.name ?? args.contractId},返回 ${ - result?.rowCount ?? 0 - } 行`, - }; - } - if (name === 'list_business_datasets') { - return { - title: '检索业务数据集', - detail: `发现 ${result?.datasets?.length ?? 0} 个可访问业务数据集`, - }; - } - if (name === 'get_business_dataset_definition') { - const valueDomainCount = (result?.fields ?? []) - .filter((field) => (field.values ?? []).length > 0).length; - return { - title: '确认业务字段与默认值域', - detail: `${result?.dataset?.name ?? args.datasetId},确认 ${ - result?.fields?.length ?? 0 - } 个字段,${valueDomainCount} 个字段完成默认值域初始化`, - }; - } - if (name === 'query_business_dataset') { - return { - title: '执行业务数据集查询', - detail: `${result?.dataset?.name ?? args.datasetId},按 ${ - result?.dimensions?.join('、') || '汇总' - } 分析,返回 ${result?.rowCount ?? 0} 行`, - }; - } - if (name === 'query_indicator') { - const dimensions = result?.dimensions?.length - ? result.dimensions.join('、') - : '汇总'; - const filters = result?.filters?.length - ? `,过滤 ${result.filters.map((filter) => ( - `${filter.bizName} ${filter.operator} ${JSON.stringify(filter.value)}` - )).join(';')}` - : ''; - return { - title: '执行指标查询', - detail: `${result?.indicator?.name ?? args.indicatorId},按${dimensions}分析${filters},返回 ${ - result?.rowCount ?? 0 - } 行`, - }; - } - if (name === 'execute_analysis_code') { - return { - title: '生成并执行分析代码', - detail: `${args.purpose || '复杂数据处理'},${ - result?.success ? '执行成功' : '执行失败' - },生成 ${result?.outputs?.length ?? 0} 个文件产物`, - }; - } - return { - title: name, - detail: JSON.stringify(args), - }; -} - export function buildSystemPrompt({ user, theme, diff --git a/src/agentToolPresentation.js b/src/agentToolPresentation.js new file mode 100644 index 0000000..f6406c3 --- /dev/null +++ b/src/agentToolPresentation.js @@ -0,0 +1,254 @@ +// Presentation of auditable tool events; independent of orchestration and storage. +export function toolStageCode(name) { + switch (name) { + case 'search_indicators': + case 'list_business_datasets': + return 'SEMANTIC_DISCOVERY'; + case 'get_indicator': + case 'get_business_dataset_definition': + return 'SEMANTIC_CONFIRM'; + case 'compile_query_contract': + return 'PLAN'; + case 'query_indicator': + case 'query_business_dataset': + case 'execute_query_contract': + return 'EXECUTE'; + case 'execute_analysis_code': + return 'CODE'; + case 'list_workspace_artifacts': + case 'transform_workspace_artifact': + return 'ANALYZE'; + default: + return 'EXECUTE'; + } +} + +function compactReviewValue(value, limit = 800) { + if (value === null || value === undefined || value === '') { + return ''; + } + const text = typeof value === 'string' + ? value + : JSON.stringify(value); + return text.length > limit ? `${text.slice(0, limit)}…` : text; +} + +function formatReviewDuration(value) { + const milliseconds = Number(value); + if (!Number.isFinite(milliseconds)) { + return ''; + } + if (milliseconds < 1000) { + return `${Math.max(0, Math.round(milliseconds))} ms`; + } + return `${(milliseconds / 1000).toFixed(2)} 秒`; +} + +export function buildToolReview(name, args = {}, result = {}) { + const facts = []; + const addFact = (label, value) => { + const text = compactReviewValue(value); + if (text) { + facts.push({ label, value: text }); + } + }; + if (name === 'search_indicators') { + addFact('检索词', args.keyword); + addFact('命中', `${result.indicators?.length ?? 0} 个指标`); + } else if (name === 'get_indicator') { + addFact('指标', result.name); + addFact('业务口径', result.businessCaliber); + addFact('字段', `${result.metrics?.length ?? 0} 个指标 / ${result.dimensions?.length ?? 0} 个维度`); + addFact( + '默认值域', + `${(result.dimensions ?? []).filter((item) => (item.values ?? []).length > 0).length} 个字段已初始化`, + ); + } else if (name === 'list_business_datasets') { + addFact('数据集', `${result.datasets?.length ?? 0} 个`); + addFact('名称', (result.datasets ?? []).map((item) => item.name).join('、')); + } else if (name === 'get_business_dataset_definition') { + addFact('数据集', result.dataset?.name); + addFact('字段', `${result.fields?.length ?? 0} 个`); + addFact( + '默认值域', + `${(result.fields ?? []).filter((item) => (item.values ?? []).length > 0).length} 个字段已初始化`, + ); + } else if (name === 'compile_query_contract') { + addFact('契约', result.contractId ?? args.contractId); + addFact('条件', `${args.conditions?.length ?? 0} 条`); + addFact('指标', (args.metricFields ?? []).map((item) => item.field).join('、')); + addFact('维度', (args.dimensionFields ?? []).map((item) => item.field).join('、')); + addFact('过滤', `${args.filterFields?.length ?? 0} 条`); + addFact('时间窗口', `${args.timeWindows?.length ?? 0} 个`); + addFact('计算', args.calculation?.type ?? 'NONE'); + if (args.postProcessing) { + const parts = []; + if (args.postProcessing.groupValues) { + parts.push(`值归并 ${args.postProcessing.groupValues.groups?.length ?? 0} 组`); + } + if (args.postProcessing.periodComparison) { + parts.push(`同期计算 ${args.postProcessing.periodComparison.type}`); + } + if (args.postProcessing.pivot) { + parts.push('透视输出'); + } + addFact('后处理', parts.join(' / ')); + } + if (args.analysisPipeline?.stages?.length) { + addFact( + '分析管线', + `${args.analysisPipeline.stages.length} 个通用阶段:${ + args.analysisPipeline.stages.map((stage) => stage.type).join(' > ') + }`, + ); + } + } else if ( + name === 'query_indicator' + || name === 'query_business_dataset' + || name === 'execute_query_contract' + ) { + addFact('数据源', result.dataset?.name ?? result.indicator?.name ?? args.indicatorId ?? args.datasetId); + addFact('结果', `${result.rowCount ?? 0} 行 / ${result.columns?.length ?? 0} 列`); + addFact('查询指纹', result.queryFingerprint); + } else if (name === 'list_workspace_artifacts') { + addFact('产物', `${result.artifacts?.length ?? 0} 个`); + addFact('文件', (result.artifacts ?? []).map((item) => item.title).join('、')); + } else if (name === 'transform_workspace_artifact') { + addFact('操作', args.operation); + addFact('结果', `${result.rowCount ?? 0} 行 / v${result.version ?? '-'}`); + } else if (name === 'execute_analysis_code') { + addFact('目的', args.purpose); + addFact('运行', result.success ? '成功' : '失败'); + addFact('耗时', formatReviewDuration(result.durationMs)); + addFact('输入产物', `${result.inputArtifacts?.length ?? 0} 个`); + addFact('输出文件', (result.outputs ?? []).map((item) => item.fileName).join('、')); + } + + const details = {}; + if (name === 'compile_query_contract') { + details.conditions = args.conditions ?? []; + details.calculation = args.calculation ?? null; + details.postProcessing = args.postProcessing ?? null; + details.analysisPipeline = args.analysisPipeline ?? null; + } else if ( + name === 'query_indicator' + || name === 'query_business_dataset' + || name === 'execute_query_contract' + ) { + details.contractId = args.contractId ?? null; + details.columns = (result.columns ?? []).map((column) => column.name ?? column.bizName); + details.rowCount = result.rowCount ?? 0; + details.queryFingerprint = result.queryFingerprint ?? null; + } else if (name === 'list_workspace_artifacts') { + details.artifacts = (result.artifacts ?? []).map((item) => ({ + id: item.id, + title: item.title, + type: item.artifactType, + columns: (item.columns ?? []).map((column) => column.name), + })); + } else if (name === 'execute_analysis_code') { + details.runId = result.runId; + details.inputArtifacts = result.inputArtifacts ?? []; + details.outputs = result.outputs ?? []; + } + return { + facts, + details: Object.keys(details).length > 0 ? details : null, + code: name === 'execute_analysis_code' ? String(args.code ?? '') : '', + stdout: name === 'execute_analysis_code' ? String(result.stdout ?? '') : '', + stderr: name === 'execute_analysis_code' ? String(result.stderr ?? '') : '', + }; +} + +export function describeToolStep(name, args, result) { + if (name === 'search_indicators') { + const count = result?.indicators?.length ?? 0; + return { + title: '检索主题指标', + detail: `关键词:${args.keyword || '当前问题'},命中 ${count} 个可用指标`, + }; + } + if (name === 'get_indicator') { + const valueDomainCount = (result?.dimensions ?? []) + .filter((dimension) => (dimension.values ?? []).length > 0).length; + return { + title: '确认指标口径与默认值域', + detail: `${result?.name ?? args.indicatorId},${ + result?.metrics?.length ?? 0 + } 个指标字段,${result?.dimensions?.length ?? 0} 个可用维度,${ + valueDomainCount + } 个字段完成默认值域初始化`, + }; + } + if (name === 'compile_query_contract') { + return { + title: result?.valid ? '编译查询契约' : '查询契约待修正', + detail: result?.valid + ? `契约 ${result.contractId.slice(0, 12)} 已通过,覆盖 ${ + result.coverage?.length ?? 0 + } 个问题片段` + : (result?.issues ?? []).map((issue) => issue.message).join(';') + || '契约未通过', + }; + } + if (name === 'execute_query_contract') { + return { + title: '执行查询契约', + detail: `${result?.indicator?.name ?? result?.dataset?.name ?? args.contractId},返回 ${ + result?.rowCount ?? 0 + } 行`, + }; + } + if (name === 'list_business_datasets') { + return { + title: '检索业务数据集', + detail: `发现 ${result?.datasets?.length ?? 0} 个可访问业务数据集`, + }; + } + if (name === 'get_business_dataset_definition') { + const valueDomainCount = (result?.fields ?? []) + .filter((field) => (field.values ?? []).length > 0).length; + return { + title: '确认业务字段与默认值域', + detail: `${result?.dataset?.name ?? args.datasetId},确认 ${ + result?.fields?.length ?? 0 + } 个字段,${valueDomainCount} 个字段完成默认值域初始化`, + }; + } + if (name === 'query_business_dataset') { + return { + title: '执行业务数据集查询', + detail: `${result?.dataset?.name ?? args.datasetId},按 ${ + result?.dimensions?.join('、') || '汇总' + } 分析,返回 ${result?.rowCount ?? 0} 行`, + }; + } + if (name === 'query_indicator') { + const dimensions = result?.dimensions?.length + ? result.dimensions.join('、') + : '汇总'; + const filters = result?.filters?.length + ? `,过滤 ${result.filters.map((filter) => ( + `${filter.bizName} ${filter.operator} ${JSON.stringify(filter.value)}` + )).join(';')}` + : ''; + return { + title: '执行指标查询', + detail: `${result?.indicator?.name ?? args.indicatorId},按${dimensions}分析${filters},返回 ${ + result?.rowCount ?? 0 + } 行`, + }; + } + if (name === 'execute_analysis_code') { + return { + title: '生成并执行分析代码', + detail: `${args.purpose || '复杂数据处理'},${ + result?.success ? '执行成功' : '执行失败' + },生成 ${result?.outputs?.length ?? 0} 个文件产物`, + }; + } + return { + title: name, + detail: JSON.stringify(args), + }; +} diff --git a/tests/agent.test.js b/tests/agent.test.js index e896d51..caa456e 100644 --- a/tests/agent.test.js +++ b/tests/agent.test.js @@ -330,6 +330,29 @@ test('agent emits auditable runtime events while answering', async () => { assert.ok(types.includes('result')); assert.ok(types.includes('done')); assert.ok(types.includes('warning')); + const search = events.find((event) => ( + event.type === 'tool_result' && event.step?.stageCode === 'SEMANTIC_DISCOVERY' + )); + assert.equal(search.step.stageCode, 'SEMANTIC_DISCOVERY'); + assert.match(search.step.title, /检索主题指标|检索业务数据集/); + assert.equal(search.summary, search.step.detail); + assert.ok(search.step.review.facts.length > 0); + assert.ok(search.step.review.facts.every((fact) => typeof fact.label === 'string' && typeof fact.value === 'string')); + assert.equal(search.step.review.details, null); + const executed = events.find((event) => ( + event.type === 'tool_result' && event.step?.stageCode === 'EXECUTE' + )); + assert.equal(executed.step.stageCode, 'EXECUTE'); + assert.match(executed.step.title, /执行指标查询|执行业务数据集查询|执行查询契约/); + assert.equal(executed.step.review.details.rowCount, answer.data.rows.length); + assert.deepEqual( + executed.step.review.details.columns, + answer.data.columns.map((column) => column.name ?? column.bizName), + ); + assert.equal(executed.step.review.details.queryFingerprint, answer.queryFingerprint); + assert.equal(executed.step.review.code, ''); + assert.equal(executed.step.review.stdout, ''); + assert.equal(executed.step.review.stderr, ''); assert.equal( events.find((event) => event.type === 'done').messageId, answer.messageId, diff --git a/tests/agentToolPresentation.test.js b/tests/agentToolPresentation.test.js new file mode 100644 index 0000000..f90114c --- /dev/null +++ b/tests/agentToolPresentation.test.js @@ -0,0 +1,55 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { buildToolReview, describeToolStep, toolStageCode } from '../src/agentToolPresentation.js'; + +test('tool events distinguish discovery, confirmation, planning, execution and analysis', () => { + for (const [name, stage] of [ + ['list_business_datasets', 'SEMANTIC_DISCOVERY'], + ['get_business_dataset_definition', 'SEMANTIC_CONFIRM'], + ['compile_query_contract', 'PLAN'], + ['execute_query_contract', 'EXECUTE'], + ['execute_analysis_code', 'CODE'], + ['transform_workspace_artifact', 'ANALYZE'], + ['extension_tool', 'EXECUTE'], + ]) assert.equal(toolStageCode(name), stage); +}); + +test('dataset query review exposes result metadata and fingerprint', () => { + const review = buildToolReview('query_business_dataset', { datasetId: 'sales' }, { + rowCount: 2, + columns: [{ name: 'revenue' }, { bizName: 'region' }], + queryFingerprint: 'query-1', + }); + assert.deepEqual(review.details, { + contractId: null, columns: ['revenue', 'region'], rowCount: 2, queryFingerprint: 'query-1', + }); + assert.ok(review.facts.some(({ label, value }) => label === '数据源' && value === 'sales')); + assert.equal(review.code, ''); + assert.equal(review.stdout, ''); + assert.equal(review.stderr, ''); +}); + +test('code review retains code and diagnostics while bounding long fact values', () => { + const review = buildToolReview('execute_analysis_code', { purpose: 'x'.repeat(900), code: 'print(1)' }, { + success: false, durationMs: 1234, stdout: '1', stderr: 'error', runId: 'run-1', + outputs: [{ fileName: 'result.csv' }], + }); + assert.equal(review.facts.find(({ label }) => label === '目的').value, `${'x'.repeat(800)}…`); + assert.equal(review.facts.find(({ label }) => label === '耗时').value, '1.23 秒'); + assert.equal(review.code, 'print(1)'); + assert.equal(review.stdout, '1'); + assert.equal(review.stderr, 'error'); + assert.equal(review.details.runId, 'run-1'); +}); + +test('invalid contracts show actionable issues and unknown tools preserve their arguments', () => { + assert.deepEqual(describeToolStep('compile_query_contract', {}, { + valid: false, issues: [{ message: 'Missing metric' }, { message: 'Missing range' }], + }), { title: '查询契约待修正', detail: 'Missing metric;Missing range' }); + assert.deepEqual(describeToolStep('extension_tool', { id: 'a' }, {}), { + title: 'extension_tool', detail: '{"id":"a"}', + }); + assert.deepEqual(buildToolReview('extension_tool'), { + facts: [], details: null, code: '', stdout: '', stderr: '', + }); +});