Files
CollegeAIcenter/src/integration/core-invariants.integration.spec.ts
T

240 lines
8.5 KiB
TypeScript

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);
});
});