forked from wangziqi/gongxue-base
feat: add student supervision automation
This commit is contained in:
@@ -9356,6 +9356,96 @@ async function testTenantStudentOperations() {
|
||||
'teacher follow-up report must not expose unscoped students',
|
||||
);
|
||||
|
||||
const supervisionPreview = await request('/api/tenant-admin/students/supervision/preview', {
|
||||
userId: TENANT_TEACHER_USER_ID,
|
||||
query: {
|
||||
classId: ids.tenantClass,
|
||||
inactivityDays: 1,
|
||||
wrongQuestionThreshold: 1,
|
||||
minAnswers: 1,
|
||||
lowAccuracyThreshold: 0.99,
|
||||
vocabularyDueThreshold: 1,
|
||||
limit: 20,
|
||||
},
|
||||
});
|
||||
assert.equal(supervisionPreview.item?.filters?.scoped, true, 'teacher supervision preview should be scoped');
|
||||
assert.ok(
|
||||
supervisionPreview.item?.candidates?.some(item => item.studentUserId === USER_ID),
|
||||
'supervision preview should include scoped risky student',
|
||||
);
|
||||
assert.ok(
|
||||
!supervisionPreview.item?.candidates?.some(item => item.studentUserId === SECOND_STUDENT_USER_ID),
|
||||
'supervision preview must not expose unscoped student',
|
||||
);
|
||||
const supervisionCandidate = supervisionPreview.item?.candidates?.find(item => item.studentUserId === USER_ID);
|
||||
assert.ok(
|
||||
supervisionCandidate?.reasons?.some(item => ['wrong_backlog', 'low_accuracy', 'vocabulary_due', 'stale_session', 'inactive'].includes(item.code)),
|
||||
'supervision candidate should include rule-based reasons',
|
||||
);
|
||||
|
||||
const generatedSupervision = await request('/api/tenant-admin/students/supervision/generate', {
|
||||
userId: TENANT_TEACHER_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
classId: ids.tenantClass,
|
||||
studentUserIds: [USER_ID],
|
||||
batchKey: 'integration-supervision-teacher',
|
||||
rules: {
|
||||
inactivityDays: 1,
|
||||
wrongQuestionThreshold: 1,
|
||||
minAnswers: 1,
|
||||
lowAccuracyThreshold: 0.99,
|
||||
vocabularyDueThreshold: 1,
|
||||
},
|
||||
limit: 5,
|
||||
},
|
||||
});
|
||||
assert.equal(generatedSupervision.successCount, 1, 'supervision generation should create follow-up for scoped student');
|
||||
assert.equal(generatedSupervision.items?.[0]?.studentUserId, USER_ID, 'supervision generation should target requested student');
|
||||
|
||||
const repeatedSupervision = await request('/api/tenant-admin/students/supervision/generate', {
|
||||
userId: TENANT_TEACHER_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
classId: ids.tenantClass,
|
||||
studentUserIds: [USER_ID],
|
||||
batchKey: 'integration-supervision-teacher',
|
||||
rules: {
|
||||
inactivityDays: 1,
|
||||
wrongQuestionThreshold: 1,
|
||||
minAnswers: 1,
|
||||
lowAccuracyThreshold: 0.99,
|
||||
vocabularyDueThreshold: 1,
|
||||
},
|
||||
limit: 5,
|
||||
},
|
||||
});
|
||||
assert.equal(
|
||||
repeatedSupervision.items?.[0]?.followupId,
|
||||
generatedSupervision.items?.[0]?.followupId,
|
||||
'supervision generation should be idempotent for the same batch and student',
|
||||
);
|
||||
|
||||
const teacherOtherSupervisionDenied = await request('/api/tenant-admin/students/supervision/generate', {
|
||||
userId: TENANT_TEACHER_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
classId: ids.tenantClassOther,
|
||||
studentUserIds: [SECOND_STUDENT_USER_ID],
|
||||
batchKey: 'integration-supervision-denied',
|
||||
rules: { inactivityDays: 1 },
|
||||
},
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(teacherOtherSupervisionDenied.code, 'CLASS_SCOPE_REQUIRED', 'teacher supervision generation must reject unscoped class');
|
||||
|
||||
const operatorSupervisionDenied = await request('/api/tenant-admin/students/supervision/preview', {
|
||||
userId: TENANT_OPERATOR_USER_ID,
|
||||
query: { limit: 5 },
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(operatorSupervisionDenied.code, 'TENANT_PERMISSION_REQUIRED', 'operator without supervision permission should be denied');
|
||||
|
||||
const teacherOtherFollowupDenied = await request('/api/tenant-admin/students/followups', {
|
||||
userId: TENANT_TEACHER_USER_ID,
|
||||
method: 'PUT',
|
||||
@@ -9413,6 +9503,7 @@ async function testTenantStudentOperations() {
|
||||
assert.ok(auditLogs.items?.some(item => item.action === 'tenant.student.status_updated'), 'audit logs should include student status update');
|
||||
assert.ok(auditLogs.items?.some(item => item.action === 'tenant.student_note.upserted'), 'audit logs should include student note upsert');
|
||||
assert.ok(auditLogs.items?.some(item => item.action === 'tenant.student_followup.upserted'), 'audit logs should include student follow-up upsert');
|
||||
assert.ok(auditLogs.items?.some(item => item.action === 'tenant.students.supervision_generated'), 'audit logs should include supervision generation');
|
||||
}
|
||||
|
||||
async function testReferralAndCrmGrowth() {
|
||||
|
||||
Reference in New Issue
Block a user