/** * Property 3: 低置信触发候选确认 的属性化测试(Classifier,Req 1.4, 1.5)。 * * 属性陈述:对任意分类结果,当某判定的 Confidence 低于 0.6(行业判定附加条件:行业标记 * 不为"未识别")时,System 必返回按 Confidence 降序排列、数量至多 3 项的候选列表并置 * 确认标志为真;否则不触发确认。 * * 本测试以智能生成器构造任意有效项目描述(有效字符数 ≥ 10 且非纯空白,满足 Req 1.6 * 前置以避免 InsufficientInputError),其中混入业务类型与行业关键词片段,使识别打分 * 真实跨越"无命中 / 部分命中 / 多类命中"的输入空间,从而覆盖高/低置信两类分支。 * * 对每个分类结果断言: * 业务类型(Req 1.4): * - 置信度 < 0.6 → needsBusinessTypeConfirm 为真,候选非空、≤3 项且按置信度降序; * - 否则 needsBusinessTypeConfirm 为假,候选列表为空。 * 行业(Req 1.5): * - 置信度 < 0.6 且行业 ≠ "未识别" → needsIndustryConfirm 为真,候选非空、≤3 项且降序; * - 否则 needsIndustryConfirm 为假,候选列表为空。 * * Feature: outsourcing-risk-assessment, Property 3: 低置信触发候选确认 * Validates: Requirements 1.4, 1.5 */ import { describe, expect, it } from 'vitest'; import fc from 'fast-check'; import { classify, countValidChars, CONFIRMATION_CONFIDENCE_THRESHOLD, MAX_CANDIDATES, type ScoredCandidate, } from '../classifier.js'; import { BUSINESS_TYPE_KEYWORDS, INDUSTRY_KEYWORDS } from '../keywords.js'; import { INDUSTRY_UNRECOGNIZED } from '../../domain/common.js'; // ---------------------------------------------------------------------------- // 生成器:构造任意有效项目描述。 // // 输入空间设计: // - 关键词片段池汇集全部业务类型与行业关键词,使描述高概率命中识别词典, // 覆盖单类命中、多类命中、跨业务/行业混合命中等分支(驱动高/低置信两类结果)。 // - 随机自由文本片段引入噪声与无命中情形(行业可能落到"未识别",业务类型可能低置信)。 // - 末尾补足填充字符,保证有效字符数 ≥ 10(满足 Req 1.6 前置,避免抛错)。 // ---------------------------------------------------------------------------- /** 全部识别关键词词条,作为可拼接的描述片段池。 */ const KEYWORD_TERMS: string[] = [ ...Object.values(BUSINESS_TYPE_KEYWORDS).flatMap((kws) => kws.map((k) => k.term)), ...Object.values(INDUSTRY_KEYWORDS).flatMap((kws) => kws.map((k) => k.term)), ]; /** 片段:关键词词条或任意自由文本(含中英文与空白)。 */ const fragmentArb: fc.Arbitrary = fc.oneof( fc.constantFrom(...KEYWORD_TERMS), fc.string({ minLength: 0, maxLength: 12 }), fc.constantFrom('外包', '用工', '服务', '项目', '客户', '的', ',', '。', ' '), ); /** * 有效项目描述:拼接若干片段后补足填充字符使有效字符数 ≥ 10。 * 保证生成的描述恒为合法输入(不触发 InsufficientInputError)。 */ const validDescriptionArb: fc.Arbitrary = fc .array(fragmentArb, { minLength: 1, maxLength: 8 }) .map((fragments) => { let desc = fragments.join(''); while (countValidChars(desc) < 10) { desc += '项'; } return desc; }); /** 断言候选列表按置信度由高到低排序(允许相等)。 */ function expectSortedDesc( candidates: ScoredCandidate[], ): void { for (let i = 1; i < candidates.length; i++) { expect(candidates[i - 1]!.confidence).toBeGreaterThanOrEqual( candidates[i]!.confidence, ); } } describe('Property 3: 低置信触发候选确认 (Req 1.4, 1.5)', () => { it('低置信判定返回降序、≤3 项候选并置确认标志;否则不触发确认', () => { fc.assert( fc.property(validDescriptionArb, (description) => { const result = classify(description); // --- 业务类型(Req 1.4)--- const businessLow = result.businessTypeConfidence < CONFIRMATION_CONFIDENCE_THRESHOLD; expect(result.needsBusinessTypeConfirm).toBe(businessLow); if (businessLow) { // 触发确认:候选非空、至多 3 项、按置信度降序。 expect(result.businessTypeCandidates.length).toBeGreaterThan(0); expect(result.businessTypeCandidates.length).toBeLessThanOrEqual( MAX_CANDIDATES, ); expectSortedDesc(result.businessTypeCandidates); } else { // 不触发确认:候选列表为空。 expect(result.businessTypeCandidates).toHaveLength(0); } // --- 行业(Req 1.5)--- const industryLow = result.industry !== INDUSTRY_UNRECOGNIZED && result.industryConfidence < CONFIRMATION_CONFIDENCE_THRESHOLD; expect(result.needsIndustryConfirm).toBe(industryLow); if (industryLow) { // 触发确认:候选非空、至多 3 项、按置信度降序。 expect(result.industryCandidates.length).toBeGreaterThan(0); expect(result.industryCandidates.length).toBeLessThanOrEqual( MAX_CANDIDATES, ); expectSortedDesc(result.industryCandidates); } else { // 不触发确认(含行业为"未识别"或高置信):候选列表为空。 expect(result.industryCandidates).toHaveLength(0); } }), { numRuns: 100 }, ); }); });