import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { parseWorkerCli, resolveWorkerMonth } from '../apps/worker/src/cli.ts'; const root = process.cwd(); function read(relativePath) { return fs.readFileSync(path.join(root, relativePath), 'utf8').replace(/\r\n/g, '\n'); } assert.throws( () => parseWorkerCli(['--loop']), /--job is required exactly once/, 'a production loop without an explicit job must fail closed', ); assert.deepEqual(parseWorkerCli(['--loop', '--job', 'crm']), { job: 'crm', loop: true, month: undefined }); assert.throws( () => parseWorkerCli(['--loop', '--job', 'platform-billing']), /periodic and must be scheduled with --once/, ); assert.deepEqual( parseWorkerCli(['--once', '--job', 'platform-usage', '--month', 'previous']), { job: 'platform-usage', loop: false, month: resolveWorkerMonth('previous') }, ); assert.throws(() => parseWorkerCli(['--once', '--job', 'assets', '--month', 'previous']), /only supported/); assert.throws(() => parseWorkerCli(['--once', '--job', 'crm', '--unknown']), /Unknown worker option/); assert.throws(() => parseWorkerCli(['--once', '--job', 'crm', 'stray']), /Unexpected worker argument/); const workerService = read('scripts/deploy/systemd/tiku-worker@.service'); const periodicService = read('scripts/deploy/systemd/tiku-worker-job@.service'); const monthlyService = read('scripts/deploy/systemd/tiku-worker-monthly-usage.service'); const target = read('scripts/deploy/systemd/tiku-workers.target'); const workerEnv = read('scripts/deploy/env/worker.env.example'); const workerConfig = read('apps/worker/src/config.ts'); const sharedDbConfig = read('packages/db/src/index.ts'); assert.match(workerService, /ExecStart=.*--loop --job %i/); assert.match(periodicService, /Type=oneshot/); assert.match(periodicService, /ExecStart=.*--once --job %i/); assert.match(monthlyService, /--job platform-usage --month previous/); assert.match(monthlyService, /--job platform-usage-overage --month previous/); const continuousJobs = [ 'crm', 'commerce', 'provider-bills', 'platform-dunning-notifications', 'platform-audit-notifications', 'assets', 'imports', 'public-banks', 'exports', ]; for (const job of continuousJobs) { assert.match(target, new RegExp(`Requires=tiku-worker@${job}\\.service`), `missing continuous worker ${job}`); } const timerNames = [ 'platform-billing', 'platform-usage', 'platform-dunning', 'platform-audit-alerts', 'student-supervision', 'monthly-usage', ]; for (const name of timerNames) { const timerPath = `scripts/deploy/systemd/tiku-worker-${name}.timer`; assert.ok(fs.existsSync(path.join(root, timerPath)), `missing timer ${timerPath}`); assert.match(target, new RegExp(`Wants=tiku-worker-${name}\\.timer`), `target must want ${name} timer`); const timer = read(timerPath); if (timer.includes('OnCalendar=')) { assert.match(timer, /\nPersistent=true\n/, `${timerPath} must catch up after downtime`); } else { assert.match(timer, /\nOnBootSec=/, `${timerPath} must resume its monotonic cadence after boot`); } assert.match(timer, /\nUnit=tiku-worker-(?:job@[^\n]+|monthly-usage\.service)\n/, `${timerPath} must name an explicit service`); } const envKeys = new Set( [ ...workerConfig.matchAll(/env(?:String|Number|Boolean|List)\(\s*'([A-Z0-9_]+)'/g), ...sharedDbConfig.matchAll(/positiveEnvNumber\(\s*'([A-Z0-9_]+)'/g), ...sharedDbConfig.matchAll(/process\.env\.([A-Z0-9_]+)/g), ].map(match => match[1]), ); const workerEnvKeys = [...workerEnv.matchAll(/^([A-Z][A-Z0-9_]*)=/gm)].map(match => match[1]); for (const key of workerEnvKeys) { if (['NODE_ENV', 'DATABASE_URL', 'DB_EXPECTED_RUNTIME_ROLE'].includes(key)) continue; assert.ok(envKeys.has(key), `worker.env.example contains a key not read by worker config: ${key}`); } for (const key of [ 'STORAGE_DEFAULT_BUCKET', 'WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT', 'WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN', 'WORKER_ASSET_SECURITY_SCAN_HTTP_TIMEOUT_MS', 'WORKER_CRM_POLL_INTERVAL_MS', 'WORKER_COMMERCE_POLL_INTERVAL_MS', 'WORKER_PROVIDER_BILL_POLL_INTERVAL_MS', 'WORKER_PLATFORM_DUNNING_NOTIFICATION_POLL_INTERVAL_MS', 'WORKER_PLATFORM_AUDIT_NOTIFICATION_POLL_INTERVAL_MS', 'WORKER_ASSET_POLL_INTERVAL_MS', 'WORKER_IMPORT_POLL_INTERVAL_MS', 'WORKER_PUBLIC_BANK_SYNC_POLL_INTERVAL_MS', 'WORKER_EXPORT_POLL_INTERVAL_MS', ]) { assert.match(workerEnv, new RegExp(`^${key}=`, 'm'), `worker env must document ${key}`); } const schedulerEnvKeys = new Set([ 'WORKER_PLATFORM_USAGE_MONTH', 'WORKER_PLATFORM_USAGE_OVERAGE_MONTH', 'ALIYUN_OSS_STS_TOKEN', 'EXPORT_PDF_FONT_PATH', ]); const codeDefaults = new Map( [...workerConfig.matchAll(/env(?:String|Number|Boolean|List)\(\s*'([A-Z0-9_]+)'\s*,\s*([^\n,)]+)/g)] .map(match => [match[1], match[2].trim()]), ); for (const line of workerEnv.split('\n')) { const match = line.match(/^([A-Z][A-Z0-9_]*)=(.*)$/); if (!match || schedulerEnvKeys.has(match[1])) continue; if (codeDefaults.get(match[1]) === "''") { assert.notEqual(match[2], '', `worker env must not leave required ${match[1]} empty`); } } for (const legacyKey of [ 'ALIYUN_OSS_BUCKET', 'ASSET_SECURITY_SCAN_ENDPOINT', 'ASSET_SECURITY_SCAN_TOKEN', 'WORKER_POLL_INTERVAL_MS', ]) { assert.doesNotMatch(workerEnv, new RegExp(`^${legacyKey}=`, 'm'), `worker env must not expose unused ${legacyKey}`); } console.log('[PASS] worker CLI and production scheduling contract');