test(docs): 补齐隔离验收与功能实施记录
This commit is contained in:
1 parent
57069a9a75
commit
ead66f0b93
14 files changed
+899
-40
No files matched your search
@@ -8,8 +8,7 @@
|
||||
* 5. 课表按周查询可用
|
||||
*
|
||||
* 用法:
|
||||
* 1) 另开终端启动后端:pnpm dev:server
|
||||
* 2) 执行:pnpm verify:e2e
|
||||
* 执行 pnpm verify:e2e;根启动器自动创建隔离数据库和带前缀账号
|
||||
*
|
||||
* 环境变量:
|
||||
* VERIFY_BASE_URL 默认 http://127.0.0.1:4000
|
||||
@@ -423,6 +422,12 @@ async function main() {
|
||||
const removed = await api(`/schedules/${item.id}`, { method: 'DELETE', token: teacherToken });
|
||||
if (removed.status === 200) removedSchedules += 1;
|
||||
}
|
||||
// 日课表为空仍会生效;清理本次导入的整张日课表,不能靠空表自动回落。
|
||||
const importedPlans = await api(`/class-plans?classId=${classId}`, { token: teacherToken });
|
||||
for (const plan of importedPlans.payload?.data ?? []) {
|
||||
if (['周一-单周', '周一-双周'].includes(plan.name))
|
||||
await api(`/class-plans/${plan.id}`, { method: 'DELETE', token: teacherToken });
|
||||
}
|
||||
const allCourses = await api(`/courses?classId=${classId}`, { token: teacherToken });
|
||||
let removedImportedCourses = 0;
|
||||
for (const course of allCourses.payload?.data ?? []) {
|
||||
@@ -2132,12 +2137,12 @@ async function main() {
|
||||
|
||||
const layoutImport = await api('/imports/time-layout', {
|
||||
method: 'POST',
|
||||
token: teacherToken,
|
||||
token: adminToken,
|
||||
body: { classId, name: '默认时间表', mode: 'replace', payload: sampleLayout },
|
||||
});
|
||||
const layoutId = layoutImport.payload?.data?.layout?.id ?? null;
|
||||
record(
|
||||
'班主任导入本班时间配置(replace)',
|
||||
'管理员导入班级时间配置(replace)',
|
||||
layoutImport.status === 200 && layoutImport.payload?.data?.layout?.items?.length === 3,
|
||||
`status=${layoutImport.status} 节数=${layoutImport.payload?.data?.layout?.items?.length}`,
|
||||
);
|
||||
@@ -2151,7 +2156,7 @@ async function main() {
|
||||
|
||||
const badImport = await api('/imports/time-layout', {
|
||||
method: 'POST',
|
||||
token: teacherToken,
|
||||
token: adminToken,
|
||||
body: {
|
||||
classId,
|
||||
name: '默认时间表',
|
||||
@@ -2169,7 +2174,7 @@ async function main() {
|
||||
|
||||
const mergeImport = await api('/imports/time-layout', {
|
||||
method: 'POST',
|
||||
token: teacherToken,
|
||||
token: adminToken,
|
||||
body: {
|
||||
classId,
|
||||
name: '默认时间表',
|
||||
@@ -2630,7 +2635,7 @@ async function main() {
|
||||
if (layoutId) {
|
||||
const layoutDeleted = await api(`/imports/time-layout/${layoutId}`, {
|
||||
method: 'DELETE',
|
||||
token: teacherToken,
|
||||
token: adminToken,
|
||||
});
|
||||
record('时间配置可删除(清理验证数据)', layoutDeleted.status === 200, `status=${layoutDeleted.status}`);
|
||||
}
|
||||
@@ -3220,6 +3225,14 @@ async function main() {
|
||||
'ClientCommand',
|
||||
'ClientRelease',
|
||||
'AiAudit',
|
||||
'TimeLayoutItem',
|
||||
'ClassPlanGroup',
|
||||
'ClassPlan',
|
||||
'ClassPlanEntry',
|
||||
'ScheduledClassPlan',
|
||||
'PromotionRun',
|
||||
'TermWeek',
|
||||
'Exam',
|
||||
];
|
||||
const dbStatus = await api('/database/status', { token: adminToken });
|
||||
const dbStatusData = dbStatus.payload?.data ?? {};
|
||||
|
||||
@@ -0,0 +1,480 @@
|
||||
import { registerEmitter } from '../src/realtime/bus.js';
|
||||
import { existsSync as awaitedSnapshotPath } from 'node:fs';
|
||||
/** 独立临时数据库上的新增能力验收;不导入 index.ts,不运行自动升级或系统命令。 */
|
||||
import assert from 'node:assert/strict';
|
||||
import { createServer } from 'node:http';
|
||||
import { prisma } from '../src/lib/db.js';
|
||||
import { createApp } from '../src/app.js';
|
||||
import { signToken } from '../src/lib/jwt.js';
|
||||
import { ensureDefaultGroups } from '../src/lib/permissions.js';
|
||||
import { runPromotion } from '../src/modules/term/promotion.service.js';
|
||||
import { exportSnapshot } from '../src/lib/snapshot.js';
|
||||
import { createAutostartController } from '../src/modules/system/autostart.js';
|
||||
import { getMaxTermWeeks } from '../src/lib/term.js';
|
||||
import { setSetting, SETTING_KEYS } from '../src/lib/settings.js';
|
||||
import { normalizeIslandLayouts, defaultIslandLayouts, buildAutoTermWeeks } from '@classhelper/shared';
|
||||
if (!process.env.DATABASE_URL?.includes('v14-verification')) throw new Error('只能对专用临时验证数据库运行');
|
||||
let count = 0;
|
||||
function check(name: string, fn: () => void) {
|
||||
fn();
|
||||
count++;
|
||||
console.log(`通过 ${count}:${name}`);
|
||||
}
|
||||
const prefix = `v14-${Date.now()}`;
|
||||
const admin = await prisma.user.create({
|
||||
data: {
|
||||
username: `${prefix}-admin`,
|
||||
name: '临时管理员',
|
||||
passwordHash: '无登录口令,仅本次签发测试令牌',
|
||||
role: 'ADMIN',
|
||||
},
|
||||
});
|
||||
const teacher = await prisma.user.create({
|
||||
data: {
|
||||
username: `${prefix}-teacher`,
|
||||
name: '临时教师',
|
||||
passwordHash: '无登录口令,仅本次签发测试令牌',
|
||||
role: 'TEACHER',
|
||||
},
|
||||
});
|
||||
const lead = await prisma.user.create({
|
||||
data: {
|
||||
username: `${prefix}-lead`,
|
||||
name: '临时级长',
|
||||
passwordHash: '无登录口令,仅本次签发测试令牌',
|
||||
role: 'TEACHER',
|
||||
},
|
||||
});
|
||||
const division = await prisma.user.create({
|
||||
data: {
|
||||
username: `${prefix}-division`,
|
||||
name: '临时部长',
|
||||
passwordHash: '无登录口令,仅本次签发测试令牌',
|
||||
role: 'TEACHER',
|
||||
},
|
||||
});
|
||||
const head = await prisma.user.create({
|
||||
data: {
|
||||
username: `${prefix}-head`,
|
||||
name: '临时班主任',
|
||||
passwordHash: '无登录口令,仅本次签发测试令牌',
|
||||
role: 'TEACHER',
|
||||
},
|
||||
});
|
||||
const token = (u: { id: string; username: string; name: string; role: string }) =>
|
||||
signToken({
|
||||
sub: u.id,
|
||||
username: u.username,
|
||||
name: u.name,
|
||||
role: u.role as 'ADMIN' | 'TEACHER',
|
||||
classId: null,
|
||||
});
|
||||
await ensureDefaultGroups();
|
||||
await setSetting(SETTING_KEYS.termMaxWeeks, '60');
|
||||
const cls = await prisma.class.create({
|
||||
data: {
|
||||
name: '临时高一班',
|
||||
grade: '高一',
|
||||
enrollmentYear: 2090,
|
||||
classIndex: 1,
|
||||
code: `V${Date.now()}A`,
|
||||
teacherId: teacher.id,
|
||||
termWeeks: 60,
|
||||
termStartDate: '2026-09-07',
|
||||
},
|
||||
});
|
||||
const other = await prisma.class.create({
|
||||
data: {
|
||||
name: '临时高二班',
|
||||
grade: '高二',
|
||||
enrollmentYear: 2089,
|
||||
classIndex: 1,
|
||||
code: `V${Date.now()}B`,
|
||||
teacherId: head.id,
|
||||
termWeeks: 60,
|
||||
termStartDate: '2026-09-07',
|
||||
},
|
||||
});
|
||||
const third = await prisma.class.create({
|
||||
data: { name: '临时其他部班', grade: '初一', code: `V${Date.now()}C`, teacherId: head.id },
|
||||
});
|
||||
const course = await prisma.course.create({ data: { name: '语文', classId: cls.id, teacherId: teacher.id } }),
|
||||
math = await prisma.course.create({ data: { name: '数学', classId: cls.id, teacherId: teacher.id } });
|
||||
const outsider = await prisma.course.create({
|
||||
data: { name: '英语', classId: other.id, teacherId: head.id },
|
||||
});
|
||||
for (const [userId, id, scopeValue] of [
|
||||
[lead.id, 'builtin-grade', '高一'],
|
||||
[division.id, 'builtin-division', '高一,高二'],
|
||||
[head.id, 'builtin-head', 'HEAD'],
|
||||
[teacher.id, 'builtin-teacher', ''],
|
||||
] as const) {
|
||||
await prisma.permissionGroup.update({ where: { id }, data: { scopeValue } });
|
||||
await prisma.userPermissionGroup.create({ data: { userId, groupId: id } });
|
||||
}
|
||||
const server = createServer(createApp());
|
||||
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||
const port = (server.address() as { port: number }).port;
|
||||
async function request(path: string, method = 'GET', data?: unknown, auth = token(admin), expected = 200) {
|
||||
const res = await fetch(`http://127.0.0.1:${port}/api${path}`, {
|
||||
method,
|
||||
headers: { authorization: `Bearer ${auth}`, 'content-type': 'application/json' },
|
||||
...(data === undefined ? {} : { body: JSON.stringify(data) }),
|
||||
});
|
||||
const value = await res.json();
|
||||
assert.equal(res.status, expected, `${method} ${path}:${JSON.stringify(value)}`);
|
||||
return value.data;
|
||||
}
|
||||
try {
|
||||
const points = [
|
||||
{ name: '第一节', startTime: '08:00', endTime: '08:45', timeType: 'LESSON' },
|
||||
{ name: '课间', startTime: '08:45', endTime: '09:00', timeType: 'BREAK' },
|
||||
{ name: '分隔', startTime: '09:00', endTime: '09:00', timeType: 'DIVIDER' },
|
||||
{
|
||||
name: '行动',
|
||||
startTime: '09:00',
|
||||
endTime: '09:05',
|
||||
timeType: 'ACTION',
|
||||
actionSet: '{"测试":"不执行"}',
|
||||
},
|
||||
{ name: '第二节', startTime: '09:05', endTime: '09:50', timeType: 'LESSON' },
|
||||
];
|
||||
await request('/time-layouts', 'POST', { name: '越权', points }, token(teacher), 403);
|
||||
check('教师不能管理全校时间表', () => {});
|
||||
const layout = await request('/time-layouts', 'POST', { name: `${prefix}作息`, points }, undefined, 201);
|
||||
check('四种时间点均保存', () =>
|
||||
assert.equal(new Set(layout.points.map((p: { timeType: string }) => p.timeType)).size, 4),
|
||||
);
|
||||
const plan = await request(
|
||||
'/class-plans',
|
||||
'POST',
|
||||
{
|
||||
classId: cls.id,
|
||||
name: '四周轮换第3周周三',
|
||||
timeLayoutId: layout.id,
|
||||
weekday: 3,
|
||||
weekCountDiv: 3,
|
||||
weekCountDivTotal: 4,
|
||||
weekEnd: 60,
|
||||
},
|
||||
undefined,
|
||||
201,
|
||||
);
|
||||
const entryPoints = layout.points.filter((p: { timeType: string }) => p.timeType === 'LESSON');
|
||||
await request(`/class-plans/${plan.id}/entries`, 'PUT', {
|
||||
entries: entryPoints.map((p: { id: string }, i: number) => ({
|
||||
timeLayoutItemId: p.id,
|
||||
courseId: i ? math.id : course.id,
|
||||
})),
|
||||
});
|
||||
const at = encodeURIComponent('2026-09-23T08:10:00+08:00');
|
||||
let status = await request(`/schedules/current?classId=${cls.id}&at=${at}`);
|
||||
check('第3周周三唯一解析器上课', () => {
|
||||
assert.equal(status.week, 3);
|
||||
assert.equal(status.current.courseName, '语文');
|
||||
assert.equal(status.inClass, true);
|
||||
});
|
||||
const week2 = await request(
|
||||
`/schedules/current?classId=${cls.id}&at=${encodeURIComponent('2026-09-16T08:10:00+08:00')}`,
|
||||
);
|
||||
check('4周轮换不误命中第2周', () => assert.equal(week2.inClass, false));
|
||||
await request(`/time-layouts/${layout.id}`, 'PUT', { ...layout, points: [...layout.points].reverse() });
|
||||
status = await request(`/schedules/current?classId=${cls.id}&at=${at}`);
|
||||
check('排序后显式外键保持语文时段', () => assert.equal(status.current.courseName, '语文'));
|
||||
const beforeFirst = await request(
|
||||
`/schedules/current?classId=${cls.id}&at=${encodeURIComponent('2026-09-23T07:00:00+08:00')}`,
|
||||
);
|
||||
check('时间点排序后下一节仍按实际时间选择', () => assert.equal(beforeFirst.next.courseName, '语文'));
|
||||
await request(
|
||||
`/class-plans/${plan.id}/entries`,
|
||||
'PUT',
|
||||
{ entries: [{ timeLayoutItemId: entryPoints[0].id, courseId: outsider.id }] },
|
||||
undefined,
|
||||
400,
|
||||
);
|
||||
check('跨班课程拒绝写入', () => {});
|
||||
await request(
|
||||
'/schedules/swap',
|
||||
'POST',
|
||||
{
|
||||
classId: cls.id,
|
||||
classPlanId: plan.id,
|
||||
date: '2026-09-23',
|
||||
first: entryPoints[0].id,
|
||||
second: entryPoints[1].id,
|
||||
},
|
||||
undefined,
|
||||
201,
|
||||
);
|
||||
status = await request(`/schedules/current?classId=${cls.id}&at=${at}`);
|
||||
check('换课叠加层生效', () => assert.equal(status.current.courseName, '数学'));
|
||||
const original = await request(`/class-plans/${plan.id}/export`);
|
||||
check('换课不修改原表', () =>
|
||||
assert.equal(
|
||||
original.plan.entries.find(
|
||||
(e: { timeLayoutItemId: string }) => e.timeLayoutItemId === entryPoints[0].id,
|
||||
).courseId,
|
||||
course.id,
|
||||
),
|
||||
);
|
||||
await request(
|
||||
'/schedules/scheduled',
|
||||
'POST',
|
||||
{ classId: cls.id, classPlanId: plan.id, date: '2026-09-23' },
|
||||
undefined,
|
||||
201,
|
||||
);
|
||||
status = await request(`/schedules/current?classId=${cls.id}&at=${at}`);
|
||||
check('预定课表优先于临时层', () => assert.equal(status.current.courseName, '语文'));
|
||||
await request(
|
||||
'/schedules/overlay',
|
||||
'POST',
|
||||
{ classId: cls.id, classPlanId: plan.id, date: '2026-09-24' },
|
||||
undefined,
|
||||
201,
|
||||
);
|
||||
check('临时课表可以预定到指定日期', () => {});
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const student = await prisma.student.create({
|
||||
data: {
|
||||
studentNo: `${prefix}-${i}`.toUpperCase(),
|
||||
name: `临时学生${i}`,
|
||||
classId: cls.id,
|
||||
status: i === 5 ? 'transferred' : 'active',
|
||||
},
|
||||
});
|
||||
await prisma.grade.create({
|
||||
data: {
|
||||
studentId: student.id,
|
||||
classId: cls.id,
|
||||
courseId: course.id,
|
||||
examName: '临时月考',
|
||||
score: 60 + i,
|
||||
totalScore: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
const classToken = signToken({
|
||||
sub: cls.id,
|
||||
classId: cls.id,
|
||||
name: cls.name,
|
||||
username: cls.code,
|
||||
role: 'CLASS_DEVICE',
|
||||
classSession: true,
|
||||
});
|
||||
await prisma.class.update({ where: { id: cls.id }, data: { studentGradeQueryEnabled: false } });
|
||||
const overview = await request('/grades/overview', 'GET', undefined, classToken);
|
||||
check('关闭个人成绩仍可看集体分析且不含身份/极值', () => {
|
||||
assert.equal(overview.mode, 'collective');
|
||||
assert(!/studentNo|studentId|studentName|rank|highest|lowest|临时学生/.test(JSON.stringify(overview)));
|
||||
});
|
||||
let gradeEvent: unknown;
|
||||
registerEmitter((_rooms, event, payload) => {
|
||||
if (event === 'grade:updated') gradeEvent = payload;
|
||||
});
|
||||
const gradeProbe = await prisma.grade.findFirstOrThrow({
|
||||
where: { classId: cls.id, student: { status: 'active' } },
|
||||
});
|
||||
await request(`/grades/${gradeProbe.id}`, 'PATCH', { score: 65 });
|
||||
check('个人查询关闭时实时广播也不含任何个人成绩', () =>
|
||||
assert.deepEqual(gradeEvent, { classId: cls.id, collective: true }),
|
||||
);
|
||||
await request('/grades/my', 'GET', undefined, classToken, 403);
|
||||
await request(`/grades/student/${prefix}-0`, 'GET', undefined, classToken, 403);
|
||||
await request(`/grades/ranking?classId=${cls.id}&examName=临时月考`, 'GET', undefined, classToken, 403);
|
||||
check('个人成绩三个入口均不可绕过关闭开关', () => {});
|
||||
const students = await request(`/classes/${cls.id}/students`, 'GET', undefined, classToken);
|
||||
check('班级端名单不含转出学生', () => assert.equal(students.length, 5));
|
||||
const roster = await request(`/students?classId=${cls.id}`, 'GET', undefined, token(teacher));
|
||||
check('教师名单服务端排除转出', () => assert.equal(roster.length, 5));
|
||||
const transferred = await prisma.student.findUniqueOrThrow({
|
||||
where: { studentNo: (prefix + '-5').toUpperCase() },
|
||||
});
|
||||
const visibleGrades = await request(`/grades?classId=${cls.id}`);
|
||||
assert(!visibleGrades.some((r: { studentId: string }) => r.studentId === transferred.id));
|
||||
await request(
|
||||
'/calls',
|
||||
'POST',
|
||||
{ classId: cls.id, studentId: transferred.id, message: '仅验证转出过滤' },
|
||||
undefined,
|
||||
404,
|
||||
);
|
||||
const homework = await request(
|
||||
'/homeworks',
|
||||
'POST',
|
||||
{ classId: cls.id, courseId: course.id, title: '转出过滤验收', content: '仅临时库,不向真实班级发布' },
|
||||
undefined,
|
||||
201,
|
||||
);
|
||||
const submissions = await request(`/homeworks/${homework.id}/submissions`, 'GET', undefined, classToken);
|
||||
assert.equal(submissions.total, 5);
|
||||
assert(!submissions.students.some((r: { studentId: string }) => r.studentId === transferred.id));
|
||||
await prisma.class.update({ where: { id: cls.id }, data: { studentGradeQueryEnabled: true } });
|
||||
await request(`/grades/student/${transferred.studentNo}`, 'GET', undefined, classToken, 404);
|
||||
await prisma.class.update({ where: { id: cls.id }, data: { studentGradeQueryEnabled: false } });
|
||||
check('转出学生不可录入叫人、查询成绩或进入作业未交名单', () => {});
|
||||
await request(`/classes/${other.id}/students`, 'GET', undefined, token(teacher), 403);
|
||||
check('普通教师不可跨班读名单', () => {});
|
||||
await request(`/grades?classId=${other.id}`, 'GET', undefined, token(lead), 403);
|
||||
await request(`/classes/${cls.id}/students`, 'GET', undefined, token(lead));
|
||||
check('级长只可读本年级', () => {});
|
||||
await request(`/classes/${other.id}/students`, 'GET', undefined, token(division));
|
||||
await request(`/classes/${third.id}/students`, 'GET', undefined, token(division), 403);
|
||||
check('部长只可读配置部范围', () => {});
|
||||
for (const u of [head, teacher, lead, division]) {
|
||||
await request('/system/settings', 'GET', undefined, token(u), 403);
|
||||
await request('/permissions/groups', 'GET', undefined, token(u), 403);
|
||||
await request('/update/client/fetch', 'POST', { version: '9.9.9' }, token(u), 403);
|
||||
}
|
||||
check('四岗位组均无系统/权限/下发更新权限', () => {});
|
||||
check('总周数可到60,生成41周不再截成30', () =>
|
||||
assert.equal(buildAutoTermWeeks('2026-09-07', 41).length, 41),
|
||||
);
|
||||
assert.equal(await getMaxTermWeeks(), 60);
|
||||
const dry = await runPromotion(2099, true);
|
||||
check('升级预览不写执行记录、不改变年级', () => assert.equal(dry.status, 'dry-run'));
|
||||
assert.equal((await prisma.class.findUniqueOrThrow({ where: { id: cls.id } })).grade, '高一');
|
||||
const snapshot = await exportSnapshot(prisma, 'sqlite');
|
||||
check('快照包含日课表/时间点/升级记录与周次', () =>
|
||||
assert(
|
||||
snapshot.data.ClassPlan &&
|
||||
snapshot.data.TimeLayoutItem &&
|
||||
snapshot.data.PromotionRun &&
|
||||
snapshot.data.TermWeek,
|
||||
),
|
||||
);
|
||||
check('岛默认固定;非法配置有界回落', () => {
|
||||
assert.equal(defaultIslandLayouts().layout, 'fixed');
|
||||
const invalid = normalizeIslandLayouts({
|
||||
layout: 'custom',
|
||||
schemes: [
|
||||
{ id: 'x', rows: [{ nodes: [], main: true }], scale: 999, color: 'url(https://example.com)' },
|
||||
],
|
||||
});
|
||||
assert.equal(invalid.schemes[0]!.scale, 3);
|
||||
assert.equal(invalid.schemes[0]!.color, '#202024');
|
||||
});
|
||||
|
||||
const legacy = await request(
|
||||
'/schedules',
|
||||
'POST',
|
||||
{
|
||||
classId: cls.id,
|
||||
courseId: math.id,
|
||||
dayOfWeek: 1,
|
||||
startTime: '10:00',
|
||||
endTime: '10:45',
|
||||
weekStart: 1,
|
||||
weekEnd: 3,
|
||||
},
|
||||
undefined,
|
||||
201,
|
||||
);
|
||||
check('旧 POST 兼容门面写入日课表', () => assert.equal(legacy.courseId, math.id));
|
||||
await request(`/schedules/${legacy.id}`, 'PATCH', { weekStart: 2, weekEnd: 4 });
|
||||
const firstWeek = await request(`/schedules?classId=${cls.id}&week=1`),
|
||||
fourthWeek = await request(`/schedules?classId=${cls.id}&week=4`);
|
||||
check('旧 PATCH 原子调整单节范围,不改变整日其它课程', () => {
|
||||
assert(!firstWeek.some((r: { id: string }) => r.id === legacy.id));
|
||||
assert(fourthWeek.some((r: { id: string }) => r.id === legacy.id));
|
||||
});
|
||||
await request(`/schedules/${legacy.id}`, 'PATCH', { startTime: '11:00', endTime: '11:45' });
|
||||
const moved = await request(`/schedules?classId=${cls.id}&week=4`);
|
||||
check('旧时间编辑投影新时间点', () =>
|
||||
assert.equal(moved.find((r: { id: string }) => r.id === legacy.id).startTime, '11:00'),
|
||||
);
|
||||
await request(`/schedules/${legacy.id}`, 'DELETE');
|
||||
check('旧 DELETE 不从遗留 Schedule 复活', () => {});
|
||||
const payload = {
|
||||
Subjects: { subject: { Name: '语文' } },
|
||||
TimeLayouts: {
|
||||
layout: {
|
||||
Name: '上游测试',
|
||||
Layouts: [
|
||||
{ StartTime: '08:00:00', EndTime: '08:45:00', TimeType: 0 },
|
||||
{ StartTime: '08:45:00', EndTime: '09:00:00', TimeType: 1 },
|
||||
{ StartTime: '09:00:00', EndTime: '09:00:00', TimeType: 2 },
|
||||
{ StartTime: '09:00:00', EndTime: '09:01:00', TimeType: 3 },
|
||||
],
|
||||
},
|
||||
},
|
||||
ClassPlans: {
|
||||
plan: {
|
||||
Name: '上游四周',
|
||||
TimeLayoutId: 'layout',
|
||||
TimeRule: { WeekDay: 4, WeekCountDiv: 3, WeekCountDivTotal: 4 },
|
||||
Classes: [{ SubjectId: 'subject' }],
|
||||
},
|
||||
},
|
||||
};
|
||||
await request('/class-plans/import/profile', 'POST', { classId: cls.id, payload }, undefined, 201);
|
||||
await request('/class-plans/import/profile', 'POST', { classId: cls.id, payload }, undefined, 201);
|
||||
const imported = await request(`/class-plans?classId=${cls.id}`);
|
||||
check('ClassIsland 导入保留4周轮换,merge重复导入不复制课表', () => {
|
||||
const rows = imported.filter((p: { name: string }) => p.name === '上游四周');
|
||||
assert.equal(rows.length, 1);
|
||||
assert.equal(rows[0].weekCountDivTotal, 4);
|
||||
assert.equal(rows[0].layout.points.length, 4);
|
||||
});
|
||||
let enabled = false;
|
||||
const os = {
|
||||
platform: 'win32',
|
||||
root: 'F:\\fake-installed-server',
|
||||
exists: () => true,
|
||||
run: (_file: string, args: string[]) => {
|
||||
if (args.includes('InstallDir'))
|
||||
return { ok: true, output: 'InstallDir REG_SZ F:\\fake-installed-server' };
|
||||
if (args[0] === 'add') {
|
||||
enabled = true;
|
||||
return { ok: true, output: '' };
|
||||
}
|
||||
if (args[0] === 'delete') {
|
||||
enabled = false;
|
||||
return { ok: true, output: '' };
|
||||
}
|
||||
return {
|
||||
ok: enabled,
|
||||
output: enabled ? 'ClassHelperServer REG_SZ "F:\\fake-installed-server\\start.cmd"' : '',
|
||||
};
|
||||
},
|
||||
};
|
||||
const autostart = createAutostartController(os);
|
||||
check('安装形态自启动开启/关闭且回读一致(假系统适配器)', () => {
|
||||
assert.equal(autostart.read().supported, true);
|
||||
assert.equal(autostart.write(true).enabled, true);
|
||||
assert.equal(createAutostartController(os).read().enabled, true);
|
||||
assert.equal(autostart.write(false).enabled, false);
|
||||
});
|
||||
check('非安装形态返回不可用原因', () => {
|
||||
const value = createAutostartController({ ...os, exists: () => false }).read();
|
||||
assert.equal(value.supported, false);
|
||||
assert(value.detail.length > 10);
|
||||
});
|
||||
// 此文件开头已强制临时库,实际升级不会触及演示或业务数据。
|
||||
await prisma.class.create({
|
||||
data: {
|
||||
name: '临时高三班',
|
||||
grade: '高三',
|
||||
enrollmentYear: 2088,
|
||||
classIndex: 1,
|
||||
code: prefix + 'G',
|
||||
teacherId: head.id,
|
||||
},
|
||||
});
|
||||
const [promotion, repeated] = await Promise.all([runPromotion(2099), runPromotion(2099)]);
|
||||
check('临时库并发升级同年幂等且执行前快照存在', () => {
|
||||
assert.equal(promotion.status, 'done');
|
||||
assert.equal(repeated.snapshotPath, promotion.snapshotPath);
|
||||
assert.equal(awaitedSnapshotPath(promotion.snapshotPath), true);
|
||||
});
|
||||
const skeleton = await prisma.class.findFirstOrThrow({ where: { enrollmentYear: 2099, grade: '高一' } });
|
||||
check('新高一骨架不复制班主任或任课授权', () => assert.equal(skeleton.teacherId, null));
|
||||
assert.equal(await prisma.course.count({ where: { classId: skeleton.id } }), 0);
|
||||
check('临时高三已归档,其余只升一级', () => assert.equal(repeated.classCount, 3));
|
||||
assert.equal((await prisma.class.findUniqueOrThrow({ where: { id: cls.id } })).grade, '高二');
|
||||
assert.equal((await prisma.class.findUniqueOrThrow({ where: { id: other.id } })).grade, '高三');
|
||||
console.log(`=== V1.4 新增能力:${count}/${count} 项通过 ===`);
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
Reference in new issue
Block a user