import { test, before, after } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import vm from 'node:vm'; import { PGlite } from '@electric-sql/pglite'; const db = new PGlite(); const query = async (sql, args = []) => (await db.query(sql, args)).rows; const one = async (sql, args = []) => (await query(sql, args))[0]; const context = vm.createContext({ DEFAULT_COMPANY_ID: 'co', number: Number, ownLegacyId: user => user.uid, fail: (_, message) => { throw Error(message); }, Psql: { query, one, oneOrNone: one }, }); const teacher = { uid: 30, get: key => key === 'company' ? { id: 'co' } : undefined }; const order = { GeneralID: '901', szyh: '20', pl: '30', dszt: '0' }; before(async () => { await db.exec(` CREATE TABLE "_User" ("objectId" text, company text, "legacyUserId" numeric, "isDisabled" boolean, "isDeleted" boolean); CREATE TABLE "CommonModel" ("objectId" text, company text, "modelId" numeric, "itemId" numeric, "generalId" numeric); CREATE TABLE "CourseAppointment" (company text, id numeric, "sourceKey" text, kcid text, "courseCategoryKey" text, "updatedAt" timestamptz, bxrq text, yysj text, "creditHoldStatus" text, "creditHoldReason" text, "bindingId" text, "creditCostsSnapshot" jsonb, kssj text, jssj text); CREATE TABLE "LessonRecord" (company text, yyds text, pf numeric, pjnr text, pldp text, "createdAt" timestamptz); CREATE TABLE "LessonReview" (company text, "appointmentGeneralId" text, score numeric, comment text, "submittedAt" timestamptz); CREATE FUNCTION xs_appointment_valid_at(text,text) RETURNS boolean LANGUAGE sql AS $$ SELECT true $$; INSERT INTO "_User" VALUES ('student','co',20,false,false),('foreign','other',20,false,false); INSERT INTO "CommonModel" VALUES ('appointment','co',54,1,901); INSERT INTO "CourseAppointment" VALUES ('co',1,'schedule:test','405','word',now(),'2026-11-12','2026-11-12 22:00',NULL,NULL,'binding','[{"account":"SilverCoin","amount":1}]',NULL,NULL); `); vm.runInContext(readFileSync(new URL('../cloud/lesson-workflow.js', import.meta.url), 'utf8'), context); }); after(() => db.close()); test('a scheduled course stops offering start or continue while its student is disabled, and recovers when enabled', async () => { assert.ok((await context.lessonOrderCapabilities(order, teacher)).allowedActions.includes('start')); await db.exec(`UPDATE "_User" SET "isDisabled"=true WHERE company='co'`); try { for (const dszt of ['0', '10']) { const detail = await context.lessonOrderCapabilities({ ...order, dszt }, teacher); assert.equal(detail.allowedActions.length, 0); assert.match(detail.blockedReason, /学员.*停用/); } } finally { await db.exec(`UPDATE "_User" SET "isDisabled"=false WHERE company='co'`); } assert.ok((await context.lessonOrderCapabilities(order, teacher)).allowedActions.includes('start')); }); test('deleted and missing students cannot be made eligible by a matching student in another tenant', async () => { await db.exec(`UPDATE "_User" SET "isDeleted"=true WHERE company='co'`); try { assert.equal((await context.lessonOrderCapabilities(order, teacher)).allowedActions.length, 0); const missing = await context.lessonOrderCapabilities({ ...order, szyh: '21' }, teacher); assert.equal(missing.allowedActions.length, 0); assert.match(missing.blockedReason, /学员/); } finally { await db.exec(`UPDATE "_User" SET "isDeleted"=false WHERE company='co'`); } });