feat: add student supervision automation

This commit is contained in:
Codex
2026-06-30 17:07:51 +08:00
parent 7473c7a6d7
commit 0204c934d8
14 changed files with 803 additions and 16 deletions

View File

@@ -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() {