import { isErr, isOk } from '@common/result'; import { MAX_PAGE_SIZE } from '@common/pagination'; import { INSUFFICIENT_DATA, ModuleSource, } from '@modules/competency-graph'; import { AchievementType } from '@modules/learning-space'; import { SkillDefinitionService, createBuiltinSkillDefinitions, } from '@modules/skill-center'; import { Action, Purpose, ResourceType, Role, SensitiveFieldCategory, User, } from '@modules/compliance'; import { IntegrationHarness, buildReferenceItem, makeIntegrationHarness, } from './integration-harness'; /** * 端到端集成:核心不变量贯通触达(任务 14;确认 Property 1-8 的核心不变量在装配后成立)。 * * 说明:Property 1-8 的完整属性测试分散在各模块的 `*.property.spec.ts` / * `*.spec.ts` 中(fast-check 驱动),由全量测试套件统一运行。本文件不重复属性测试, * 而是以集成装配的真实服务对每个 Property 所在领域做一次"核心不变量"冒烟断言, * 确认底座装配正确、跨模块共享时各不变量仍然成立。 */ describe('集成:核心不变量贯通触达(Property 1-8 冒烟确认)', () => { let h: IntegrationHarness; const studentId = 'stu-invariant-1'; beforeEach(async () => { h = await makeIntegrationHarness(); }); it('Property 1(技能定义序列化往返一致性):内置技能定义往返等价', () => { const codec = new SkillDefinitionService(); for (const def of createBuiltinSkillDefinitions()) { const roundTripped = codec.deserialize(codec.serialize(def)); expect(codec.equivalent(def, roundTripped)).toBe(true); } }); it('Property 2(能力标签映射有效性):已定义标签映射成功、未定义标签拒绝且不创建记录', async () => { // 已定义标签 → 成功。 const okResult = await h.mappingService.mapData( ModuleSource.LearningSpace, { studentId, referenceId: 'ref-ok', score: 60 }, ['tag.knowledge.basic'], ); expect(isOk(okResult)).toBe(true); // 未定义标签 → 拒绝、不创建映射记录(需求 14.3)。 const before = (await h.mappingService.listMappings(studentId)).length; const badResult = await h.mappingService.mapData( ModuleSource.LearningSpace, { studentId, referenceId: 'ref-bad' }, ['tag.does-not-exist'], ); expect(isErr(badResult)).toBe(true); const after = (await h.mappingService.listMappings(studentId)).length; expect(after).toBe(before); }); it('Property 3(画像维度分值有界性):每个维度为 [0,100] 数值或数据不足标记', async () => { await h.mappingService.mapData( ModuleSource.AIEvaluationEngine, { studentId, referenceId: 'ref-k', score: 150 }, // 越界 → clamp ['tag.knowledge.basic'], ); const profile = await h.profile.generateProfile(studentId); expect(isOk(profile)).toBe(true); if (!isOk(profile)) { return; } for (const dim of profile.value.dimensions) { if (dim.score === INSUFFICIENT_DATA) { expect(dim.traceability).toEqual([]); } else { expect(typeof dim.score).toBe('number'); expect(dim.score as number).toBeGreaterThanOrEqual(0); expect(dim.score as number).toBeLessThanOrEqual(100); } } // 越界分值被 clamp 到 100。 const knowledge = profile.value.dimensions.find( (d) => d.dimensionName === '知识掌握', )!; expect(knowledge.score).toBe(100); }); it('Property 4(授权强制不变量):本人完整、非本人无授权脱敏、授权后释放、撤销后失效', async () => { const owner: User = { id: studentId, role: Role.Student }; const mentor: User = { id: 'mentor-inv-1', role: Role.Mentor }; const profile = { studentId, fields: [ { key: 'dimension.knowledge', value: 80, sensitive: false }, { key: 'dimension.clinical', value: 70, sensitive: true, category: SensitiveFieldCategory.MedicalRecord, }, ], }; // 本人 → 完整。 const ownerView = await h.consent.resolveProfileView( owner, profile, Purpose.CareerGuidance, ); expect(ownerView.fullAccess).toBe(true); expect(ownerView.redactedFieldKeys).toEqual([]); // 非本人无授权 → 敏感字段脱敏。 const noAuth = await h.consent.resolveProfileView( mentor, profile, Purpose.MentorReview, ); expect(noAuth.fullAccess).toBe(false); expect(noAuth.redactedFieldKeys).toContain('dimension.clinical'); // 授权后 → 敏感字段释放。 const now = new Date(); const grant = await h.consent.grantConsent({ studentId, scope: [SensitiveFieldCategory.MedicalRecord], purpose: Purpose.MentorReview, validFrom: new Date(now.getTime() - 1000), validUntil: new Date(now.getTime() + 60_000), }); expect(isOk(grant)).toBe(true); if (!isOk(grant)) { return; } const authed = await h.consent.resolveProfileView( mentor, profile, Purpose.MentorReview, ); expect(authed.redactedFieldKeys).not.toContain('dimension.clinical'); // 撤销后 → 立即按未授权处理(需求 7.8)。 await h.consent.revokeConsent(studentId, grant.value.id); const afterRevoke = await h.consent.resolveProfileView( mentor, profile, Purpose.MentorReview, ); expect(afterRevoke.redactedFieldKeys).toContain('dimension.clinical'); }); it('Property 5(引用可追溯不变量):引用输出仅含可追溯结论,无法溯源不作为引用输出', async () => { const summary = await h.research.summarize([ buildReferenceItem({ id: 'ref-traceable' }), ]); expect(isOk(summary)).toBe(true); if (!isOk(summary)) { return; } for (const conclusion of summary.value.citationOutput) { expect(conclusion.verified).toBe(true); expect(conclusion.citations.length).toBeGreaterThanOrEqual(1); } // 未验证结论绝不出现在引用输出中。 const unverified = summary.value.conclusions.filter((c) => !c.verified); for (const c of unverified) { expect(summary.value.citationOutput).not.toContain(c); } }); it('Property 6(权限判定一致性):允许 ⟺ 操作在角色范围内;越权必记录拒绝', async () => { const admin: User = { id: 'admin-inv-1', role: Role.Administrator }; const student: User = { id: 'stu-inv-2', role: Role.Student }; // 管理员可配置技能定义(范围内)。 const adminDecision = await h.compliance.checkAccess( admin, { resourceType: ResourceType.SkillDefinition, identifier: 's-1' }, Action.Configure, ); expect(adminDecision.allowed).toBe(true); // 学生删除审计日志(越权)→ 拒绝并记录。 const studentDecision = await h.compliance.checkAccess( student, { resourceType: ResourceType.AuditLog, identifier: 'log-1' }, Action.Delete, ); expect(studentDecision.allowed).toBe(false); expect(studentDecision.denialEventId).toBeTruthy(); const denials = await h.compliance.listDenialEventsByActor(student.id); expect(denials.length).toBe(1); }); it('Property 7(错误路径数据保全):非法成果新增被拒且不创建任何记录', async () => { const before = await h.learningSpaceRepo.countAchievements(); // 缺少必填元数据(标题为空)→ 拒绝(需求 1.6)。 const result = await h.learningSpace.addAchievement(studentId, { type: AchievementType.CourseRecord, title: ' ', occurredAt: new Date('2024-10-01T00:00:00Z'), }); expect(isErr(result)).toBe(true); // 数据保全:无任何记录被创建。 expect(await h.learningSpaceRepo.countAchievements()).toBe(before); }); it('Property 8(分页边界不变量):单页结果数量不超过 50', async () => { // 新增 55 条成果。 for (let i = 0; i < 55; i += 1) { const result = await h.learningSpace.addAchievement(studentId, { type: AchievementType.CourseRecord, title: `课程记录 ${i + 1}`, occurredAt: new Date('2024-10-01T00:00:00Z'), }); expect(isOk(result)).toBe(true); } // 请求 pageSize 超过上界 → 被钳制到 MAX_PAGE_SIZE。 const page = await h.learningSpace.listAchievements( studentId, {}, { page: 1, pageSize: 1000 }, ); expect(page.items.length).toBeLessThanOrEqual(MAX_PAGE_SIZE); expect(page.items.length).toBe(MAX_PAGE_SIZE); }); });