forked from wangziqi/gongxue-base
210 lines
7.1 KiB
JavaScript
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);
|
|
});
|