import assert from 'node:assert/strict'; import pg from 'pg'; import { spawn } from 'node:child_process'; const databaseUrl = process.env.DATABASE_URL || 'postgresql://postgres:postgres@127.0.0.1:54322/postgres'; const MAIN_TENANT_ID = '00000000-0000-0000-0000-000000000001'; const TENANT_CLASS_ID = '00000000-0000-0000-0000-000000000851'; const STUDENT_USER_ID = '00000000-0000-0000-0000-000000000101'; const ids = { rule: '00000000-0000-0000-0000-00000000d701', disabledRule: '00000000-0000-0000-0000-00000000d702', }; function runWorkerOnce() { const child = spawn(process.execPath, ['apps/worker/dist/apps/worker/src/index.js', '--once', '--job', 'student-supervision'], { cwd: process.cwd(), env: { ...process.env, DATABASE_URL: databaseUrl, WORKER_STUDENT_SUPERVISION_BATCH_SIZE: '10', WORKER_STUDENT_SUPERVISION_ID: 'student-supervision-integration-test', }, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, }); let output = ''; child.stdout.on('data', chunk => { output += chunk.toString(); }); child.stderr.on('data', chunk => { output += chunk.toString(); }); return new Promise((resolve, reject) => { child.on('error', reject); child.on('exit', code => { try { assert.equal(code, 0, `worker should exit 0\n${output}`); assert.match(output, /student-supervision batch processed=\d+/, 'worker output should include student supervision summary'); resolve(output); } catch (error) { reject(error); } }); }); } async function cleanup(pool) { await pool.query( ` delete from public.tenant_student_followups where tenant_id = $1 and ( metadata->'autoSupervision'->>'ruleId' in ($2, $3) or metadata->>'ruleId' in ($2, $3) ) `, [MAIN_TENANT_ID, ids.rule, ids.disabledRule], ); await pool.query('delete from public.audit_logs where tenant_id = $1 and target_id in ($2, $3)', [MAIN_TENANT_ID, ids.rule, ids.disabledRule]); await pool.query('delete from public.tenant_student_supervision_rules where tenant_id = $1 and id in ($2, $3)', [MAIN_TENANT_ID, ids.rule, ids.disabledRule]); } async function createRules(pool) { await pool.query( ` insert into public.tenant_student_supervision_rules ( id, tenant_id, name, status, rules, schedule, class_id, assigned_to_user_id, limit_count, metadata, next_run_at ) values ( $1, $2, 'worker 学习督导集成测试', 'active', $3::jsonb, $4::jsonb, $5, null, 5, '{"source":"student-supervision-worker-test"}'::jsonb, now() - interval '1 minute' ) `, [ ids.rule, MAIN_TENANT_ID, JSON.stringify({ inactivityDays: 1, wrongQuestionThreshold: 1, minAnswers: 1, lowAccuracyThreshold: 0.99, vocabularyDueThreshold: 1, }), JSON.stringify({ enabled: true, frequency: 'daily', hour: 9, minute: 0, timezone: 'Asia/Shanghai', }), TENANT_CLASS_ID, ], ); await pool.query( ` insert into public.tenant_student_supervision_rules ( id, tenant_id, name, status, rules, schedule, class_id, assigned_to_user_id, limit_count, metadata, next_run_at ) values ( $1, $2, 'disabled worker 学习督导集成测试', 'disabled', $3::jsonb, $4::jsonb, $5, null, 5, '{"source":"student-supervision-worker-test"}'::jsonb, now() - interval '1 minute' ) `, [ ids.disabledRule, MAIN_TENANT_ID, JSON.stringify({ inactivityDays: 1 }), JSON.stringify({ enabled: true, frequency: 'daily', hour: 9, minute: 0 }), TENANT_CLASS_ID, ], ); } async function main() { const pool = new pg.Pool({ connectionString: databaseUrl }); try { await cleanup(pool); await createRules(pool); const firstOutput = await runWorkerOnce(); assert.match(firstOutput, /processed=1/, 'worker should process the active due supervision rule'); assert.match(firstOutput, /generated=1/, 'worker should generate follow-up tasks for the active rule'); assert.match(firstOutput, /failed=0/, 'worker should not fail active supervision rule'); const rule = await pool.query( ` select last_run_at, next_run_at, last_result, metadata from public.tenant_student_supervision_rules where tenant_id = $1 and id = $2 `, [MAIN_TENANT_ID, ids.rule], ); assert.ok(rule.rows[0]?.last_run_at, 'worker should record last_run_at'); assert.ok(rule.rows[0]?.next_run_at, 'worker should compute next_run_at'); assert.equal(rule.rows[0]?.last_result?.status, 'completed', 'worker should record successful last_result'); assert.equal(rule.rows[0]?.metadata?.studentSupervisionWorker?.lastStatus, 'completed', 'worker metadata should record completion'); const followups = await pool.query( ` select id, student_user_id, followup_type, status, metadata from public.tenant_student_followups where tenant_id = $1 and metadata->'autoSupervision'->>'ruleId' = $2 `, [MAIN_TENANT_ID, ids.rule], ); assert.ok(followups.rowCount >= 1, 'worker should create at least one supervision follow-up'); assert.ok( followups.rows.some(row => row.student_user_id === STUDENT_USER_ID), 'worker follow-up should include risky smoke student', ); assert.ok(followups.rows.every(row => row.followup_type === 'learning'), 'worker follow-ups should use learning type'); assert.ok(followups.rows.every(row => row.status === 'open'), 'worker follow-ups should start open'); const disabled = await pool.query( ` select last_run_at from public.tenant_student_supervision_rules where tenant_id = $1 and id = $2 `, [MAIN_TENANT_ID, ids.disabledRule], ); assert.equal(disabled.rows[0]?.last_run_at, null, 'worker should not process disabled supervision rules'); const secondOutput = await runWorkerOnce(); assert.match(secondOutput, /processed=0/, 'second worker run should not process rule before next_run_at'); const followupCount = await pool.query( ` select count(*)::integer as count from public.tenant_student_followups where tenant_id = $1 and metadata->'autoSupervision'->>'ruleId' = $2 `, [MAIN_TENANT_ID, ids.rule], ); assert.equal(Number(followupCount.rows[0]?.count), followups.rowCount, 'second worker run should not duplicate follow-ups'); const audit = await pool.query( ` select action, details from public.audit_logs where tenant_id = $1 and target_id = $2 and action = 'tenant.students.supervision_rule_worker_completed' order by created_at desc limit 1 `, [MAIN_TENANT_ID, ids.rule], ); assert.equal(audit.rows[0]?.details?.workerId, 'student-supervision-integration-test', 'worker audit should record worker id'); console.log('Student supervision worker integration test complete.'); } finally { await cleanup(pool).catch(() => {}); await pool.end(); } } main().catch(error => { console.error(error); process.exit(1); });