Files
gongxue-base/scripts/student-supervision-worker-integration-test.js
2026-06-30 17:40:03 +08:00

210 lines
7.1 KiB
JavaScript

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