import { before, after, test } 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 gateway = {}; vm.createContext(gateway); const source = readFileSync(new URL('../deploy-admin-functions.mjs', import.meta.url), 'utf8'); vm.runInContext(source.match(/const operationsGatewayCode = String.raw`([\s\S]*?)\n`;/)[1], gateway); gateway.companyIdOf = () => 'co'; gateway.companyPointer = () => ({ id: 'co' }); gateway.Psql = { query: async (sql, args) => (await db.query(sql, args)).rows, oneOrNone: async (sql, args) => (await db.query(sql, args)).rows[0] || null, }; // Parse is external; SQL evidence queries and persisted identity changes use the real database. gateway.Parse = { Query: class { async get(id) { const row = (await db.query('SELECT * FROM "_User" WHERE "objectId"=$1', [id])).rows[0]; return { get: key => row[key], set: (key, value) => { row[key] = value; }, save: async () => db.query('UPDATE "_User" SET "identityType"=$2 WHERE "objectId"=$1', [id, row.identityType]), }; } }, Object: class { set() {} async save() {} }, }; before(async () => { await db.exec(` CREATE TABLE "_User"("objectId" text,company text,"legacyUserId" int,"legacyGroupId" int,"legacyUserData" jsonb,"isAdmin" boolean,"isDeleted" boolean,"identityType" text); CREATE TABLE "CourseAppointment"(company text,pl text,fxpl text,szyh text); CREATE TABLE "LessonRecord"(company text,jsmz text,xymz text); CREATE TABLE "DailyStudyRecord"(company text,"userId" int); CREATE TABLE "CourseBinding"(company text,yhid text); CREATE TABLE "PracticeRecord"(company text,yhid text); CREATE TABLE "LegacySyncConflict"("objectId" text,company text,dataset text,"legacyKey" text,resolved boolean); INSERT INTO "_User" VALUES ('teacher','co',1535,3,'{}',false,false,'conflict'), ('student','co',2,1,'{}',false,false,'member'), ('ambiguous','co',3,1,'{}',false,false,'conflict'), ('dual-binding','co',4,3,'{}',false,false,'conflict'), ('dual-lesson','co',5,3,'{}',false,false,'conflict'), ('store','co',6,2,'{}',false,false,'store'); INSERT INTO "CourseAppointment" VALUES ('co','1535','1535','99'),('co','3',null,'99'),('co','4',null,'99'),('co','6',null,'6'),('other','99',null,'1535'); INSERT INTO "LessonRecord" VALUES ('co','5','5'); INSERT INTO "CourseBinding" VALUES ('co','4'); INSERT INTO "PracticeRecord" VALUES ('co','1535'),('co','2'),('co','3'); `); await gateway.refreshIdentityProjection({}, {}); }); after(() => db.close()); const identity = async id => (await db.query('SELECT "identityType" FROM "_User" WHERE "objectId"=$1', [id])).rows[0].identityType; test('正式老师自己的单词练习不再把老师误标为冲突,其他帐套学员证据不串入', async () => { assert.equal(await identity('teacher'), 'coach'); await gateway.refreshIdentityProjection({}, {}); assert.equal(await identity('teacher'), 'coach'); }); test('仅练习的普通学员仍是学员,分组未经确认的双重身份仍需核实', async () => { assert.equal(await identity('student'), 'member'); assert.equal(await identity('ambiguous'), 'conflict'); }); test('老师同时有正式学员课程或课后记录时仍保留冲突,门店角色不改变', async () => { assert.equal(await identity('dual-binding'), 'conflict'); assert.equal(await identity('dual-lesson'), 'conflict'); assert.equal(await identity('store'), 'store'); });