/** * Property 36: 报告章节完备 的属性化测试(Report_Generator,Req 10.1)。 * * 属性陈述:对任意风险评分、红线校验、费用测算与可接受性结论均已完成的评估, * `generate` 产出的报告*必*包含全部规定章节(项目概要与业务类型判定、风险总分与分级、 * 风险热力图、各维度风险明细、Top 关键风险与红线校验结果、可接受性结论、应对方案、 * 假设与信息缺口说明);且未命中任一红线时,红线校验结果*必*明确标注为"无红线命中"。 * * 本测试在生成器中构造"完整 Assessment"(评分/红线/费用/可接受性结论均已产生), * 覆盖启用/停用维度与指标、混合数据来源(含"智能体假设")、有/无红线命中等形态, * 对 `generate` 施加上述章节完备性与"无红线命中"标注约束。 * * Feature: outsourcing-risk-assessment, Property 36: 报告章节完备 * Validates: Requirements 10.1 */ import { describe, expect, it } from 'vitest'; import fc from 'fast-check'; import { ACCEPTABILITY_VALUES, REDLINE_STATUS_VALUES, type Acceptability, type Assessment, type RedlineResult, type RedlineStatus, type ScoringItem, } from '../../domain/assessment.js'; import { DATA_PROVENANCE_VALUES, RISK_GRADE_VALUES, RISK_LEVEL_VALUES, type DataProvenance, type RiskGrade, type RiskLevel, } from '../../domain/common.js'; import type { CostEstimate } from '../../domain/cost.js'; import type { Dimension, Indicator, Redline, RiskModel, ScoringRule } from '../../domain/model.js'; import { REGION_CN } from '../../domain/region.js'; import { generate, NO_REDLINE_HIT_LABEL } from '../report.js'; // ---------------------------------------------------------------------------- // 生成器:构造"评分/红线/费用/可接受性结论均已完成"的 Assessment // ---------------------------------------------------------------------------- /** 合法 Indicator 须覆盖 Risk_Level 1-5(Req 11.3);报告不读取规则文本。 */ const scoringRules: ScoringRule[] = (RISK_LEVEL_VALUES as readonly RiskLevel[]).map( (level) => ({ level, label: `L${level}`, description: `规则${level}` }), ); const riskLevelArb = fc.constantFrom(...(RISK_LEVEL_VALUES as readonly RiskLevel[])); const provenanceArb = fc.constantFrom( ...(DATA_PROVENANCE_VALUES as readonly DataProvenance[]), ); const riskGradeArb = fc.constantFrom(...(RISK_GRADE_VALUES as readonly RiskGrade[])); const acceptabilityArb = fc.constantFrom( ...(ACCEPTABILITY_VALUES as readonly Acceptability[]), ); const redlineStatusArb = fc.constantFrom( ...(REDLINE_STATUS_VALUES as readonly RedlineStatus[]), ); /** 置信度:[0,1] 两位小数(Req 4.6, 18.4)。 */ const confidenceArb = fc.integer({ min: 0, max: 100 }).map((n) => n / 100); const weightArb = fc.double({ min: 0, max: 100, noNaN: true }); const moneyArb = fc.double({ min: 0, max: 1_000_000, noNaN: true }); interface RawIndicator { enabled: boolean; weight: number; riskLevel: RiskLevel; provenance: DataProvenance; confidence: number; } interface RawDimension { enabled: boolean; weight: number; indicators: RawIndicator[]; } interface RawRedline { enabled: boolean; status: RedlineStatus; } const rawIndicatorArb: fc.Arbitrary = fc.record({ enabled: fc.boolean(), weight: weightArb, riskLevel: riskLevelArb, provenance: provenanceArb, confidence: confidenceArb, }); const rawDimensionArb: fc.Arbitrary = fc.record({ enabled: fc.boolean(), weight: weightArb, indicators: fc.array(rawIndicatorArb, { minLength: 0, maxLength: 4 }), }); const rawRedlineArb: fc.Arbitrary = fc.record({ enabled: fc.boolean(), status: redlineStatusArb, }); const costEstimateArb: fc.Arbitrary = fc .record({ baseline: moneyArb, premiumLower: fc.double({ min: 0, max: 50, noNaN: true }), premiumDelta: fc.double({ min: 0, max: 50, noNaN: true }), }) .map(({ baseline, premiumLower, premiumDelta }) => ({ riskPremiumRange: { lower: premiumLower, upper: premiumLower + premiumDelta, unit: '百分比' }, advanceInterest: 0, insuranceCost: 0, compensationReserve: 0, badDebtReserve: 0, baselineQuote: baseline, riskAdjustedQuote: baseline, breakdown: [], })); interface AssessmentSpec { dimensions: RawDimension[]; redlines: RawRedline[]; riskScore: number; riskGrade: RiskGrade; acceptability: Acceptability; costEstimate: CostEstimate; /** 强制构造"无红线命中"情形,确保该分支被充分覆盖。 */ forceNoHit: boolean; } const assessmentSpecArb: fc.Arbitrary = fc.record({ dimensions: fc.array(rawDimensionArb, { minLength: 0, maxLength: 4 }), redlines: fc.array(rawRedlineArb, { minLength: 0, maxLength: 5 }), riskScore: fc.integer({ min: 0, max: 100 }), riskGrade: riskGradeArb, acceptability: acceptabilityArb, costEstimate: costEstimateArb, forceNoHit: fc.boolean(), }); /** * 由随机规格构造一个"已完成"的 Assessment: * - 风险模型保留全部随机形态(启用/停用维度与指标、空指标维度、权重为 0 等); * - 每个 Indicator 均有对应 ScoringItem(含来源/置信/三要素); * - 每个**启用**红线均有对应的红线校验结果(满足红线校验完成条件); * - riskScore/riskGrade/costEstimate/acceptability 均已产生。 */ function buildAssessment(spec: AssessmentSpec): Assessment { const dimensions: Dimension[] = spec.dimensions.map((d, i) => ({ id: `d${i}`, name: `维度${i}`, weight: d.weight, enabled: d.enabled, indicators: d.indicators.map( (ind, j): Indicator => ({ id: `i${j}`, name: `指标${i}-${j}`, weight: ind.weight, enabled: ind.enabled, scoringRules, evidenceRequired: '', askPrompt: '', }), ), })); const redlines: Redline[] = spec.redlines.map((r, k) => ({ id: `r${k}`, triggerCondition: `触发条件${k}`, consequence: `后果${k}`, enabled: r.enabled, })); const riskModel: RiskModel = { id: 'm1', name: '风险模型', businessType: '岗位外包', dimensions, redlines, }; // 为每个指标构造评分项(含停用项;报告以对象引用映射,停用项不进入热力图/TopN)。 const scoringItems: ScoringItem[] = spec.dimensions.flatMap((d, i) => d.indicators.map((ind, j): ScoringItem => ({ dimensionId: `d${i}`, indicatorId: `i${j}`, riskLevel: ind.riskLevel, score: ind.riskLevel * ind.weight, provenance: ind.provenance, confidence: ind.confidence, rationale: `判定依据${i}-${j}`, riskImpact: `风险影响${i}-${j}`, recommendation: `建议${i}-${j}`, })), ); // 每个启用红线产生一个红线校验结果(满足红线校验完成条件)。 const redlineResults: RedlineResult[] = spec.redlines .map((r, k): { enabled: boolean; result: RedlineResult } => { const status: RedlineStatus = spec.forceNoHit && r.status === '命中' ? '未命中' : r.status; const base: RedlineResult = { redlineId: `r${k}`, status }; const result: RedlineResult = status === '命中' ? { ...base, triggeredCondition: `触发条件${k}`, evidenceData: `判定依据数据${k}` } : base; return { enabled: r.enabled, result }; }) .filter((entry) => entry.enabled) .map((entry) => entry.result); return { id: 'a1', projectDescription: '某外包项目风险评估', businessType: '岗位外包', industry: '制造业', region: REGION_CN, riskModel, scoringItems, riskScore: spec.riskScore, riskGrade: spec.riskGrade, redlineResults, costEstimate: spec.costEstimate, acceptability: spec.acceptability, metadata: { businessType: '岗位外包', industry: '制造业', region: REGION_CN, riskScore: spec.riskScore, riskGrade: spec.riskGrade, createdAt: '2024-01-01T00:00:00.000Z', assessorId: 'assessor-1', }, createdAt: '2024-01-01T00:00:00.000Z', assessorId: 'assessor-1', }; } // ---------------------------------------------------------------------------- // 属性测试 // ---------------------------------------------------------------------------- describe('Property 36: 报告章节完备 (Req 10.1)', () => { it('对任意已完成评估,报告含全部规定章节;未命中任一红线时明确标注"无红线命中"', () => { fc.assert( fc.property(assessmentSpecArb, (spec) => { const assessment = buildAssessment(spec); const report = generate(assessment); // 1) 全部八个规定章节均存在,且各章节标题非空。 expect(report.projectOverview).toBeDefined(); expect(report.projectOverview.title.length).toBeGreaterThan(0); expect(report.riskScoreGrade).toBeDefined(); expect(report.riskScoreGrade.title.length).toBeGreaterThan(0); expect(report.heatmap).toBeDefined(); expect(report.heatmap.title.length).toBeGreaterThan(0); expect(report.dimensionDetails).toBeDefined(); expect(report.dimensionDetails.title.length).toBeGreaterThan(0); expect(report.keyRisksAndRedlines).toBeDefined(); expect(report.keyRisksAndRedlines.title.length).toBeGreaterThan(0); expect(report.acceptabilityConclusion).toBeDefined(); expect(report.acceptabilityConclusion.title.length).toBeGreaterThan(0); expect(report.responsePlan).toBeDefined(); expect(report.responsePlan.title.length).toBeGreaterThan(0); expect(report.assumptionsAndGaps).toBeDefined(); expect(report.assumptionsAndGaps.title.length).toBeGreaterThan(0); // 2) 章节内容与已完成评估一致的基本结构。 expect(report.riskScoreGrade.riskScore).toBe(assessment.riskScore); expect(report.riskScoreGrade.riskGrade).toBe(assessment.riskGrade); expect(report.acceptabilityConclusion.acceptability).toBe(assessment.acceptability); expect(Array.isArray(report.heatmap.cells)).toBe(true); expect(report.dimensionDetails.dimensions.length).toBe( assessment.riskModel.dimensions.length, ); expect(report.keyRisksAndRedlines.redlineResults.length).toBe( assessment.redlineResults.length, ); expect(Array.isArray(report.assumptionsAndGaps.items)).toBe(true); // 3) 红线命中与"无红线命中"标注。 const anyHit = assessment.redlineResults.some((r) => r.status === '命中'); if (anyHit) { expect(report.keyRisksAndRedlines.hasRedlineHit).toBe(true); expect(report.keyRisksAndRedlines.hitRedlines.length).toBe( assessment.redlineResults.filter((r) => r.status === '命中').length, ); expect(report.keyRisksAndRedlines.redlineSummary).not.toBe(NO_REDLINE_HIT_LABEL); } else { expect(report.keyRisksAndRedlines.hasRedlineHit).toBe(false); expect(report.keyRisksAndRedlines.hitRedlines.length).toBe(0); expect(report.keyRisksAndRedlines.redlineSummary).toBe(NO_REDLINE_HIT_LABEL); } }), { numRuns: 100 }, ); }); });