import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { PGlite } from '@electric-sql/pglite'; import vm from 'node:vm'; const migration = new URL('../sql/legacy-enrollment-reconciliation.sql', import.meta.url); const sql = readFileSync(migration, 'utf8'); const gateway = readFileSync(new URL('../cloud/member-enrollment.js', import.meta.url), 'utf8'); async function fixture(run) { const db = new PGlite(); try { await db.exec(` CREATE TABLE "_User" (company text,"legacyUserId" numeric,"identityType" text,"isDisabled" boolean,"isDeleted" boolean,"legacyUserData" jsonb); CREATE TABLE "Node" (company text,"nodeId" numeric,zstatus numeric,"nodeName" text); CREATE TABLE "CourseTeachingProfile" (company text,"courseId" numeric,"categoryKey" text,confirmed boolean); CREATE TABLE "CommonModel" (company text,"modelId" numeric,"itemId" numeric,status numeric,"objectId" text,"sourceKey" text); CREATE TABLE "CourseBinding" (company text,"objectId" text PRIMARY KEY,id numeric,yhid text,kcid text,"categoryKey" text,"categoryName" text,"enrollmentStatus" text,"sourceKey" text,yxx numeric,cksl numeric,syjd text,"updatedAt" timestamptz); CREATE UNIQUE INDEX enrollment_active_identity ON "CourseBinding" (company,yhid,"categoryKey",kcid) WHERE "enrollmentStatus"='active'; INSERT INTO "_User" VALUES ('co',20,'member',false,false,'{"Purse":10}'); INSERT INTO "Node" VALUES ('co',100,99,'词表'); INSERT INTO "CourseTeachingProfile" VALUES ('co',100,'word',true); `); const binding = async (id, status = 'pending', category = 'word', source = `legacy:model:58:general:${id}`) => db.query( `INSERT INTO "CourseBinding" VALUES ('co',$1,$2,'20','100',$3,'单词课',$4,$5,123,500,'{"101":"101@第一本@1"}',now())`, [String(id),id,category,status,source]); const common = async (id, status = 99) => db.query(`INSERT INTO "CommonModel" VALUES ('co',58,$1,$2,$3,$4)`, [id,status,'c'+id,`legacy:model:58:general:${id}`]); const get = async id => (await db.query('SELECT * FROM "CourseBinding" WHERE "objectId"=$1',[String(id)])).rows[0]; const install = () => db.exec(sql); await run({db,binding,common,get,install}); } finally { await db.close(); } } test('历史待确认开课恢复后进入真实排课查询,保留编号、进度和余额,重复迁移无变化', () => fixture(async ({db,binding,common,get,install}) => { await binding(1); await common(1); const before = await get(1); await install(); const after = await get(1); assert.equal(after.enrollmentStatus, 'active'); for (const key of ['objectId','id','sourceKey','yxx','cksl','syjd']) assert.equal(after[key],before[key]); const c = { Psql: { query: async (s,p) => (await db.query(s,p)).rows }, companyIdOf: () => 'co', text: x => String(x ?? ''), number: Number, TEACHING_CATEGORY_LABELS: new Map([['word','单词课']]) }; vm.createContext(c); vm.runInContext(gateway,c); const courses = await c.enrollmentBindings({}, {},20); assert.equal(courses.length,1); assert.equal(courses[0].bindingId,'1'); await install(); assert.deepEqual(await get(1),after); assert.deepEqual((await db.query('SELECT "legacyUserData" FROM "_User"')).rows[0].legacyUserData,{Purse:10}); })); for (const order of ['binding-first','common-first']) test(`未来同步两张表按${order}顺序写入时自动激活`, () => fixture(async ({binding,common,get,install}) => { await install(); if(order==='binding-first'){await binding(1,null,null);assert.equal((await get(1)).enrollmentStatus,null);await common(1);} else {await common(1);await binding(1,null,null);} assert.equal((await get(1)).enrollmentStatus,'active'); assert.equal((await get(1)).categoryKey,'word'); })); test('停用学生保留原状,重新启用后无需删除重开;恢复删除的学生同样生效', () => fixture(async ({db,binding,common,get,install}) => { await db.exec('UPDATE "_User" SET "isDisabled"=true,"isDeleted"=true'); await binding(1);await common(1);await install(); assert.equal((await get(1)).enrollmentStatus,'pending'); await db.exec('UPDATE "_User" SET "isDisabled"=false');assert.equal((await get(1)).enrollmentStatus,'pending'); await db.exec('UPDATE "_User" SET "isDeleted"=false');assert.equal((await get(1)).enrollmentStatus,'active'); })); test('后续补入学生资料、确认映射和恢复内容时补齐开课', () => fixture(async ({db,binding,common,get,install}) => { await db.exec('DELETE FROM "_User";UPDATE "CourseTeachingProfile" SET confirmed=false;UPDATE "Node" SET zstatus=0'); await binding(1,null,null);await common(1);await install(); await db.exec(`INSERT INTO "_User" VALUES ('co',20,'member',false,false,'{}');UPDATE "CourseTeachingProfile" SET confirmed=true`); assert.equal((await get(1)).enrollmentStatus,null); await db.exec('UPDATE "Node" SET zstatus=99');assert.equal((await get(1)).enrollmentStatus,'active'); })); test('重复历史开课、已存在有效开课、回收记录及新系统记录保持原状', () => fixture(async ({db,binding,common,get,install}) => { await binding(1);await common(1);await binding(2);await common(2);await install(); assert.equal((await get(1)).enrollmentStatus,'pending');assert.equal((await get(2)).enrollmentStatus,'pending'); await db.exec(`UPDATE "CourseBinding" SET "enrollmentStatus"='active' WHERE "objectId"='2'`); assert.equal((await get(1)).enrollmentStatus,'pending'); await binding(3,'recycled');await common(3,-2);assert.equal((await get(3)).enrollmentStatus,'recycled'); await db.exec('DELETE FROM "CourseBinding";DELETE FROM "CommonModel"'); await binding(4,'pending','word','enroll:4');await common(4);assert.equal((await get(4)).enrollmentStatus,'pending'); })); for(const kind of ['unconfirmed','conflicting-profile','explicit-other-category','deleted-common','foreign-tenant']) test(`${kind}不自动授权`, () => fixture(async ({db,binding,common,get,install}) => { if(kind==='unconfirmed')await db.exec('UPDATE "CourseTeachingProfile" SET confirmed=false'); if(kind==='conflicting-profile')await db.exec(`INSERT INTO "CourseTeachingProfile" VALUES ('co',100,'trial',true)`); if(kind==='foreign-tenant')await db.exec(`UPDATE "CourseTeachingProfile" SET company='other'`); await binding(1,'pending',kind==='explicit-other-category'?'trial':null);await common(1,kind==='deleted-common'?-2:99);await install(); assert.equal((await get(1)).enrollmentStatus,'pending'); }));