import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import test from 'node:test' import { fileURLToPath } from 'node:url' import { labelAgentAssetReleaseObservation, normalizeAgentAssetReleaseReviewLabel, normalizeAgentAssetReleaseReviewQueue } from '../src/services/agentAssets.js' import { buildReleaseMonitorMetricCards, buildReleaseReviewDocumentRoute } from '../src/views/scripts/useAuditReleaseMonitor.js' function source(path) { return readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8') } const panel = source('../src/components/audit/AuditReleaseMonitorPanel.vue') const detail = source('../src/components/audit/AuditJsonRiskRuleDetail.vue') const view = source('../src/views/AuditView.vue') const service = source('../src/services/agentAssets.js') const composable = source('../src/views/scripts/useAuditReleaseMonitor.js') test('盲审面板正负样本混排且不读取或暗示模型预测', () => { assert.match(panel, /正负样本随机混排/u) assert.match(panel, /候选与基线结论对复核人隐藏/u) assert.match(panel, /模型结论已隐藏/u) assert.match(panel, /存在真实风险/u) assert.match(panel, /确认无该风险/u) assert.doesNotMatch(panel, /candidate_hit|baseline_hit/u) assert.doesNotMatch(service, /candidate_hit|baseline_hit/u) assert.doesNotMatch(panel, /'confirmed'|'false_positive'/u) assert.match(panel, /'risk_present'/u) assert.match(panel, /'risk_absent'/u) assert.doesNotMatch(panel, /确认命中|标记误报|待复核正例/u) assert.match(panel, /class="review-decision"[\s\S]*class="review-decision"/u) assert.match(panel, /:key="item\.sample_id \|\| item\.observation_id"/u) assert.match(panel, /target="_blank"/u) assert.match(panel, /rel="noopener noreferrer"/u) }) test('复核队列只保留盲审字段并可从 source_document_id 打开单据', () => { const queue = normalizeAgentAssetReleaseReviewQueue({ asset_id: 'asset-1', release_id: 'release-1', stage: 'shadow', version: 'v2', pending_total: 1, telemetry_status: 'collecting', items: [{ sample_id: 'sample-1', observation_id: 'observation-1', source_document_id: 'claim-1', rule_code: 'RISK-001', business_stage: 'reimbursement', prediction_blinded: true, reviewer_count: 1, required_reviewers: 2, conflicted: false, created_at: '2026-07-17T00:00:00Z', candidate_hit: true, baseline_hit: false }] }) assert.deepEqual(Object.keys(queue.items[0]).sort(), [ 'business_stage', 'conflicted', 'created_at', 'observation_id', 'prediction_blinded', 'required_reviewers', 'reviewer_count', 'rule_code', 'sample_id', 'source_document_id' ]) assert.equal(queue.items[0].prediction_blinded, true) assert.deepEqual(buildReleaseReviewDocumentRoute(queue.items[0]), { name: 'app-document-detail', params: { requestId: 'claim-1' } }) assert.equal(buildReleaseReviewDocumentRoute({ source_document_id: '' }), null) }) test('负样本证据未就绪时召回和实际漏检不可用,null 不伪装为 0', () => { const queue = normalizeAgentAssetReleaseReviewQueue({ pending_total: 4, metrics: { observed_count: 20, runtime_failure_count: 1, runtime_failure_rate: 0.05, negative_sample_count: 8, negative_labeled_count: 3, negative_pending_label_count: 5, false_negative_count: null, estimated_false_negative_count: null, false_negative_upper_bound: null, random_negative_population_count: 12, random_negative_sample_count: 4, random_negative_labeled_count: 1, recall: null, recall_lower_bound: null, recall_confidence_level: 0.95, recall_method: 'stratified_random_audit_wilson_upper_bound', negative_ground_truth_status: 'insufficient_random_negative_reviews' } }) const cards = cardsByLabel(buildReleaseMonitorMetricCards(queue)) assert.equal(queue.metrics.false_negative_count, null) assert.equal(cards['实际漏检'].value, '不可用') assert.match(cards['实际漏检'].hint, /不按 0 展示/u) assert.equal(cards['估计漏检'].value, '不可用') assert.equal(cards['召回率'].value, '不可用') assert.equal(cards['召回率'].hint, '随机负样本复核不足') assert.equal(cards['负样本积压'].value, '5') assert.equal(cards['负样本标注进度'].value, '37.5%') assert.equal(cards['随机负样本抽检'].value, '25.0%') }) test('真实负样本证据显示召回点估计、保守下界和漏检上界', () => { const cards = cardsByLabel(buildReleaseMonitorMetricCards({ pending_total: 2, metrics: { observed_count: 100, runtime_failure_count: 0, runtime_failure_rate: 0, negative_sample_count: 20, negative_labeled_count: 18, negative_pending_label_count: 2, false_negative_count: 2, estimated_false_negative_count: 2.5, false_negative_upper_bound: 4.75, random_negative_population_count: 40, random_negative_sample_count: 10, random_negative_labeled_count: 10, recall: 0.8, recall_lower_bound: 0.64, recall_confidence_level: 0.95, recall_method: 'stratified_random_audit_wilson_upper_bound', negative_ground_truth_status: 'available_stratified_random_audit' } })) assert.equal(cards['实际漏检'].value, '2') assert.equal(cards['估计漏检'].value, '2.5') assert.equal(cards['估计漏检'].hint, '保守上界 4.75') assert.equal(cards['负样本标注进度'].value, '90.0%') assert.equal(cards['负样本积压'].value, '2') assert.equal(cards['召回率'].value, '80.0%') assert.match(cards['召回率'].hint, /保守下界 64\.0% · 95% 置信度/u) assert.match(cards['召回率'].hint, /分层随机盲审 Wilson 保守上界/u) }) test('发布复核使用独立租户鉴权 API、幂等请求号和中性结果文案', () => { assert.match(service, /\/release\/review-queue/u) assert.match(service, /\/release\/review-queue\/\$\{observationId\}\/labels/u) assert.match(service, /normalizeAgentAssetReleaseReviewQueue\(payload\)/u) assert.match(composable, /requestId:\s*requestId\(assetId, observationId, label\)/u) assert.match(composable, /Promise\.all\(\[/u) assert.match(composable, /fetchAgentAssetReleaseState/u) assert.match(composable, /fetchAgentAssetReleaseReviewQueue/u) assert.match(composable, /已提交“存在真实风险”/u) assert.match(composable, /已提交“确认无该风险”/u) assert.equal(normalizeAgentAssetReleaseReviewLabel('risk_present'), 'risk_present') assert.equal(normalizeAgentAssetReleaseReviewLabel('risk_absent'), 'risk_absent') assert.throws(() => normalizeAgentAssetReleaseReviewLabel('confirmed'), /risk_present/u) assert.throws(() => normalizeAgentAssetReleaseReviewLabel('false_positive'), /risk_absent/u) }) test('复核服务只发送 risk_present 或 risk_absent 新语义', async () => { const originalFetch = globalThis.fetch const bodies = [] globalThis.fetch = async (_url, options) => { bodies.push(JSON.parse(options.body)) return new Response(JSON.stringify({ label: options.body.includes('risk_present') ? 'risk_present' : 'risk_absent' }), { status: 200, headers: { 'content-type': 'application/json' } }) } try { await labelAgentAssetReleaseObservation('asset-1', 'observation-1', 'risk_present', { actor: 'auditor', requestId: 'blind-review-present' }) await labelAgentAssetReleaseObservation('asset-1', 'observation-2', 'risk_absent', { actor: 'auditor', requestId: 'blind-review-absent' }) assert.throws( () => labelAgentAssetReleaseObservation('asset-1', 'observation-3', 'confirmed'), /risk_present/u ) } finally { globalThis.fetch = originalFetch } assert.deepEqual(bodies, [{ label: 'risk_present' }, { label: 'risk_absent' }]) }) test('规则详情和审计页复用独立发布监控组件', () => { assert.match(detail, / [card.label, card])) }