import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { loadConfig } from '../dist/config.js'; import { closeMySqlPool, createMySqlPool } from '../dist/db/mysql.js'; import { LegacyReadRepository } from '../dist/db/legacy-read-repository.js'; import { TaskRepository } from '../dist/tasks/task-repository.js'; import { AmbiguousAppTenantError, PlatformConfigRepository } from '../dist/tenancy/platform-config-repository.js'; import { AuthRepository } from '../dist/auth/auth-repository.js'; import { RbacRepository } from '../dist/auth/rbac-repository.js'; import { UserManagementRepository } from '../dist/auth/user-management-repository.js'; import { StoreRoomRepository, StoreRoomError } from '../dist/stores/store-room-repository.js'; import { ContentRepository, ContentError } from '../dist/content/content-repository.js'; import { StoreDiscoveryRepository } from '../dist/stores/store-discovery-repository.js'; import { StoreAccessRepository, StoreAccessError } from '../dist/stores/access-repository.js'; import { executeMigrationPlan, loadMigrationPlan, splitSqlStatements } from '../dist/db/migration-runner.js'; const expectedTables = [ 'qipai_advertisements', 'qipai_async_tasks', 'qipai_audit_logs', 'qipai_auth_sessions', 'qipai_devices', 'qipai_legacy_table_mappings', 'qipai_media_assets', 'qipai_members', 'qipai_order_user_access', 'qipai_orders', 'qipai_outbox_events', 'qipai_payments', 'qipai_permissions', 'qipai_platform_apps', 'qipai_role_permissions', 'qipai_roles', 'qipai_room_categories', 'qipai_room_disabled_periods', 'qipai_rooms', 'qipai_scene_codes', 'qipai_scene_scan_events', 'qipai_schema_migrations', 'qipai_store_business_hours', 'qipai_store_decorations', 'qipai_stores', 'qipai_tenant_apps', 'qipai_tenant_configs', 'qipai_tenants', 'qipai_user_admin_profiles', 'qipai_user_identities', 'qipai_user_roles', 'qipai_user_store_scopes', 'qipai_users' ]; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); async function readCoreTables(pool) { const placeholders = expectedTables.map(() => '?').join(', '); const [rows] = await pool.query( `SELECT table_name AS tableName FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name IN (${placeholders}) ORDER BY table_name`, expectedTables ); return rows.map((row) => row.tableName); } async function readMigrationVersions(pool) { const [rows] = await pool.query( `SELECT version, name FROM qipai_schema_migrations WHERE version IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ORDER BY version`, ['2026061601', '2026061802', '2026061803', '2026061804', '2026061805', '2026061806', '2026061807', '2026061808', '2026061809', '2026061810'] ); return rows; } async function loadLegacyFixture(pool) { const fixtureSql = await readFile( resolve(repoRoot, 'database/fixtures/2026061801_m01b_legacy_schema.sql'), 'utf8' ); const statements = splitSqlStatements(fixtureSql); for (const statement of statements) { await pool.query(statement); } return statements.length; } async function assertLegacyCompatibility(pool) { const repository = new LegacyReadRepository(pool); const stores = await repository.listStores({ tenantId: 1 }); const rooms = await repository.listRooms({ tenantId: 1, parentId: 101 }); const orders = await repository.listOrders({ tenantId: 1, parentId: 1001 }); const devices = await repository.listDevices({ tenantId: 1, parentId: 1001 }); assert.deepEqual(stores.map((record) => record.legacyId), [101]); assert.deepEqual(rooms.map((record) => record.legacyId), [1001, 1002]); assert.deepEqual(devices.map((record) => record.code), ['SANITIZED-DEVICE-001']); assert.equal(orders.length, 1); assert.equal(orders[0].code, 'LEGACY-SANITIZED-001'); assert.equal(orders[0].totalAmountCents, 2580); assert.equal(orders[0].paidAmountCents, 2000); assert.equal(orders[0].renewalAmountCents, 580); assert.equal(orders[0].groupAmountCents, 0); assert.equal(orders[0].refundAmountCents, null); } async function assertTaskDurability(pool) { await pool.query( `INSERT INTO qipai_tenants (code, name) VALUES ('M01C-TEST', 'M01C sanitized test tenant')` ); const firstRepository = new TaskRepository(pool); const firstEnqueue = await firstRepository.enqueue({ tenantId: '1', taskType: 'order.advance', idempotencyKey: 'order:501:paid', payload: { orderId: '501' }, maxAttempts: 3 }); const duplicateEnqueue = await firstRepository.enqueue({ tenantId: '1', taskType: 'order.advance', idempotencyKey: 'order:501:paid', payload: { orderId: '501' }, maxAttempts: 3 }); assert.equal(firstEnqueue.created, true); assert.equal(duplicateEnqueue.created, false); assert.equal(duplicateEnqueue.id, firstEnqueue.id); const restartedRepository = new TaskRepository(pool); const claimed = await restartedRepository.claimNext('worker-after-restart', 30_000); assert.equal(claimed?.id, firstEnqueue.id); assert.equal(claimed?.attempts, 1); assert.equal(await restartedRepository.complete(claimed.id, 'worker-after-restart'), true); const [rows] = await pool.query( `SELECT status, attempts FROM qipai_async_tasks WHERE id = ?`, [firstEnqueue.id] ); assert.deepEqual(rows, [{ status: 'SUCCEEDED', attempts: 1 }]); } async function assertPlatformTenantIsolation(pool) { const [tenantResult] = await pool.query( `INSERT INTO qipai_tenants (code, name) VALUES ('M02A-A', 'M02A tenant A'), ('M02A-B', 'M02A tenant B')` ); const firstTenantId = Number(tenantResult.insertId); const secondTenantId = firstTenantId + 1; const [appResult] = await pool.query( `INSERT INTO qipai_platform_apps (appid, name) VALUES ('wx-m02a-shared', 'M02A shared app')` ); const platformAppId = Number(appResult.insertId); await pool.query( `INSERT INTO qipai_tenant_apps (tenant_id, platform_app_id, is_default) VALUES (?, ?, 1), (?, ?, 0)`, [firstTenantId, platformAppId, secondTenantId, platformAppId] ); await pool.query( `INSERT INTO qipai_tenant_configs (tenant_id, platform_app_id, brand_name, theme_color) VALUES (?, ?, 'Tenant A Brand', '#111111'), (?, ?, 'Tenant B Brand', '#222222')`, [firstTenantId, platformAppId, secondTenantId, platformAppId] ); const repository = new PlatformConfigRepository(pool); const tenantA = await repository.resolveBootstrap('wx-m02a-shared', String(firstTenantId)); const tenantB = await repository.resolveBootstrap('wx-m02a-shared', String(secondTenantId)); assert.equal(tenantA?.brand.name, 'Tenant A Brand'); assert.equal(tenantB?.brand.name, 'Tenant B Brand'); assert.equal( await repository.resolveBootstrap('wx-m02a-shared', String(secondTenantId + 999)), null ); await assert.rejects( () => repository.resolveBootstrap('wx-m02a-shared'), AmbiguousAppTenantError ); return { tenantId: String(firstTenantId), platformAppId: String(platformAppId), appId: 'wx-m02a-shared' }; } async function assertRevocableAuthSession(pool, context) { const repository = new AuthRepository(pool); const resolved = await repository.resolveLoginContext(context.appId, context.tenantId); assert.deepEqual(resolved, context); const sessionId = '9c47fdb5-0c38-463a-858f-e1d85ce9b3fd'; const session = await repository.loginWithWechat({ context, openid: 'm02b-openid-a', unionid: 'm02b-unionid', sessionId, expiresAt: new Date(Date.now() + 60_000), ip: '127.0.0.1', userAgent: 'M02-B test' }); assert.equal(session.user.userType, 'CUSTOMER'); const rbac = new RbacRepository(pool); assert.deepEqual(await rbac.getAccessProfile(context.tenantId, session.user.id), { roles: ['CUSTOMER'], capabilities: ['order.self.read', 'profile.read'], storeIds: [] }); const [storeResult] = await pool.query( `INSERT INTO qipai_stores (tenant_id, name) VALUES (?, 'M02C Store')`, [context.tenantId] ); assert.equal(await rbac.grantStore({ tenantId: context.tenantId, userId: session.user.id, storeId: String(storeResult.insertId), scopeType: 'STAFF' }), true); assert.equal(await rbac.grantStore({ tenantId: String(Number(context.tenantId) + 1), userId: session.user.id, storeId: String(storeResult.insertId), scopeType: 'STAFF' }), false); assert.equal((await repository.loginWithWechat({ context, openid: 'm02b-openid-a', unionid: 'm02b-unionid', sessionId: 'c07df18c-a90d-4fa6-bea2-640d9710c84e', expiresAt: new Date(Date.now() + 60_000), ip: '127.0.0.1', userAgent: 'M02-B repeat login' })).user.id, session.user.id); assert.ok(await repository.validateSession(sessionId, context.tenantId, session.user.id)); assert.equal(await repository.revokeSession(sessionId), true); assert.equal(await repository.validateSession(sessionId, context.tenantId, session.user.id), null); const roleSessionId = 'd8eb245b-e513-401e-9046-f574447909ad'; await repository.loginWithWechat({ context, openid: 'm02b-openid-a', sessionId: roleSessionId, expiresAt: new Date(Date.now() + 60_000), ip: '127.0.0.1', userAgent: 'M02-B role test' }); await pool.query( 'UPDATE qipai_users SET role_version = role_version + 1 WHERE tenant_id = ? AND id = ?', [context.tenantId, session.user.id] ); assert.equal(await repository.validateSession(roleSessionId, context.tenantId, session.user.id), null); } async function assertUserManagement(pool, context) { const [adminResult] = await pool.query( `INSERT INTO qipai_users (tenant_id, user_type, nickname, phone) VALUES (?, 'STAFF', 'Tenant Admin', '13800000001')`, [context.tenantId] ); const adminId = String(adminResult.insertId); await pool.query( `INSERT INTO qipai_user_roles (tenant_id, user_id, role_id) SELECT ?, ?, id FROM qipai_roles WHERE tenant_id = ? AND code = 'TENANT_ADMIN'`, [context.tenantId, adminId, context.tenantId] ); const rbac = new RbacRepository(pool); const access = await rbac.getAccessProfile(context.tenantId, adminId); assert.ok(access.capabilities.includes('tenant.manage')); assert.ok(access.capabilities.includes('staff.manage')); const [storeResult] = await pool.query( `INSERT INTO qipai_stores (tenant_id, name) VALUES (?, 'M02D Staff Store')`, [context.tenantId] ); const repository = new UserManagementRepository(pool); const actor = { tenantId: context.tenantId, userId: adminId, access, traceId: 'm02d-live-test', ip: '127.0.0.1', userAgent: 'M02-D live test' }; const created = await repository.createStaff(actor, { nickname: 'Live Staff', phone: '13800000002', note: 'sanitized live test', roles: ['STAFF'], storeIds: [String(storeResult.insertId)] }); const sessionId = '7197e528-f727-4c85-a490-f5ec1721594c'; await pool.query( `INSERT INTO qipai_auth_sessions (id, tenant_id, platform_app_id, user_id, role_version, expires_at) SELECT ?, ?, ?, id, role_version, DATE_ADD(UTC_TIMESTAMP(3), INTERVAL 1 HOUR) FROM qipai_users WHERE tenant_id = ? AND id = ?`, [sessionId, context.tenantId, context.platformAppId, context.tenantId, created.userId] ); await repository.updateUser(actor, created.userId, { status: 'DISABLED', roles: ['STAFF'], storeIds: [String(storeResult.insertId)] }); const [sessionRows] = await pool.query( 'SELECT status, revoke_reason AS revokeReason FROM qipai_auth_sessions WHERE id = ?', [sessionId] ); assert.deepEqual(sessionRows, [{ status: 'REVOKED', revokeReason: 'ACCESS_CHANGED' }]); const [auditRows] = await pool.query( `SELECT action FROM qipai_audit_logs WHERE tenant_id = ? AND resource_type = 'USER' AND resource_id = ? ORDER BY id`, [context.tenantId, created.userId] ); assert.deepEqual(auditRows.map((row) => row.action), ['STAFF_CREATED', 'USER_UPDATED']); } async function assertStoreRoomDomain(pool, context) { const [adminRows] = await pool.query( `SELECT u.id FROM qipai_users u INNER JOIN qipai_user_roles ur ON ur.tenant_id = u.tenant_id AND ur.user_id = u.id INNER JOIN qipai_roles r ON r.id = ur.role_id AND r.tenant_id = ur.tenant_id WHERE u.tenant_id = ? AND r.code = 'TENANT_ADMIN' LIMIT 1`, [context.tenantId] ); const adminId = String(adminRows[0].id); const rbac = new RbacRepository(pool); const access = await rbac.getAccessProfile(context.tenantId, adminId); const repository = new StoreRoomRepository(pool); const actor = { tenantId: context.tenantId, userId: adminId, access, traceId: 'm03a-live-test', ip: '127.0.0.1', userAgent: 'M03-A live test' }; const store = await repository.createStore(actor, { name: 'M03A Store', address: 'Sanitized address', city: '上海市', district: '黄浦区', longitude: 121.4737, latitude: 31.2304, contactPhone: '13800000003', timezone: 'Asia/Shanghai', businessStatus: 'OPEN', wifiSsid: 'M03A-WIFI', wifiPassword: 'sanitized-password', notificationUrl: '', sortOrder: 1, businessHours: [ { weekday: 1, openMinute: 600, closeMinute: 1320, isClosed: false }, { weekday: 2, openMinute: 600, closeMinute: 1320, isClosed: false } ] }); const roomInput = { storeId: store.storeId, categoryName: '标准间', name: 'M03A Room', roomNo: 'A01', capacity: 4, basePriceCents: 3000, weekdayPriceCents: 2800, holidayPriceCents: 3500, overnightPriceCents: 12000, depositCents: 5000, minimumMinutes: 60, maxAdvanceStartMinutes: 30, maxAdvanceDays: 30, configurationStatus: 'ENABLED', operationalStatus: 'AVAILABLE', tags: ['麻将', '禁烟'], images: ['https://api.txyundm.cn/uploads/test-room.jpg'], sortOrder: 1 }; const room = await repository.createRoom(actor, roomInput); await repository.addDisabledPeriod(actor, room.roomId, { storeId: store.storeId, startsAt: new Date('2026-06-20T02:00:00.000Z'), endsAt: new Date('2026-06-20T04:00:00.000Z'), reason: 'maintenance rehearsal' }); const stores = await repository.listStores(actor); const rooms = await repository.listRooms(actor, store.storeId); assert.equal(stores.find((item) => item.id === store.storeId)?.timezone, 'Asia/Shanghai'); assert.equal(rooms.find((item) => item.id === room.roomId)?.holidayPriceCents, 3500); assert.deepEqual(rooms.find((item) => item.id === room.roomId)?.tags, ['麻将', '禁烟']); await assert.rejects( () => repository.listRooms({ ...actor, access: { roles: ['STORE_ADMIN'], capabilities: ['store.operation.read', 'store.operation.write'], storeIds: [String(Number(store.storeId) + 999)] } }, store.storeId), (error) => error instanceof StoreRoomError && error.code === 'STORE_SCOPE_FORBIDDEN' ); const [auditRows] = await pool.query( `SELECT action FROM qipai_audit_logs WHERE tenant_id = ? AND trace_id = 'm03a-live-test' ORDER BY id`, [context.tenantId] ); assert.deepEqual(auditRows.map((row) => row.action), [ 'STORE_CREATED', 'ROOM_CREATED', 'ROOM_DISABLED_PERIOD_CREATED' ]); } async function assertStoreDiscovery(pool, context) { const [secondStore] = await pool.query( `INSERT INTO qipai_stores (tenant_id, name, address, city, district, longitude, latitude, business_status, sort_order) VALUES (?, 'M03C Far Store', 'Sanitized address B', '上海市', '浦东新区', 121.6000000, 31.3000000, 'OPEN', 2)`, [context.tenantId] ); await pool.query( `INSERT INTO qipai_store_business_hours (tenant_id, store_id, weekday, open_minute, close_minute, is_closed) VALUES (?, ?, 4, 0, 0, 0)`, [context.tenantId, secondStore.insertId] ); const repository = new StoreDiscoveryRepository(pool); const stores = await repository.findStores({ tenantId: context.tenantId, city: '上海市', latitude: 31.2304, longitude: 121.4737, now: new Date('2026-06-18T04:00:00.000Z') }); assert.equal(stores[0].name, 'M03A Store'); assert.equal(stores[0].distanceMeters, 0); assert.ok(stores[1].distanceMeters > stores[0].distanceMeters); assert.equal(stores.every((store) => store.city === '上海市'), true); assert.equal((await repository.findStores({ tenantId: context.tenantId, city: '不存在的城市' })).length, 0); } async function assertSceneAndWifiAccess(pool, context) { const [adminRows] = await pool.query( `SELECT u.id FROM qipai_users u INNER JOIN qipai_user_roles ur ON ur.tenant_id = u.tenant_id AND ur.user_id = u.id INNER JOIN qipai_roles r ON r.id = ur.role_id AND r.tenant_id = ur.tenant_id WHERE u.tenant_id = ? AND r.code = 'TENANT_ADMIN' LIMIT 1`, [context.tenantId] ); const [customerRows] = await pool.query( `SELECT u.id FROM qipai_users u INNER JOIN qipai_user_identities i ON i.tenant_id = u.tenant_id AND i.user_id = u.id WHERE u.tenant_id = ? AND i.openid = 'm02b-openid-a' LIMIT 1`, [context.tenantId] ); const [targetRows] = await pool.query( `SELECT s.id AS storeId, r.id AS roomId FROM qipai_stores s INNER JOIN qipai_rooms r ON r.tenant_id = s.tenant_id AND r.store_id = s.id WHERE s.tenant_id = ? AND s.name = 'M03A Store' LIMIT 1`, [context.tenantId] ); const adminId = String(adminRows[0].id); const customerId = String(customerRows[0].id); const storeId = String(targetRows[0].storeId); const roomId = String(targetRows[0].roomId); const rbac = new RbacRepository(pool); const adminAccess = await rbac.getAccessProfile(context.tenantId, adminId); const customerAccess = await rbac.getAccessProfile(context.tenantId, customerId); const actor = { tenantId: context.tenantId, userId: adminId, access: adminAccess, traceId: 'm03d-live-test', ip: '127.0.0.1', userAgent: 'M03-D live test' }; const repository = new StoreAccessRepository(pool); const first = await repository.regenerateScene(actor, { targetType: 'ROOM', storeId, roomId }); const firstResolved = await repository.resolveScene({ code: first.code, sourceType: 'QRCODE', traceId: 'm03d-scan-1', ip: '127.0.0.1', userAgent: 'M03-D scan' }); assert.equal(firstResolved.roomId, roomId); assert.deepEqual(firstResolved.permissions, []); const second = await repository.regenerateScene(actor, { targetType: 'ROOM', storeId, roomId }); assert.equal(second.generation, 2); await assert.rejects( () => repository.resolveScene({ code: first.code, sourceType: 'NFC', traceId: 'm03d-old-code', ip: '127.0.0.1', userAgent: 'M03-D old code' }), (error) => error instanceof StoreAccessError && error.code === 'SCENE_CODE_INVALID' ); await repository.resolveScene({ code: second.code, sourceType: 'NFC', traceId: 'm03d-scan-2', ip: '127.0.0.1', userAgent: 'M03-D NFC' }); assert.equal((await repository.sceneStats(actor, storeId))[0].scanCount, 1); await assert.rejects( () => repository.getWifi({ tenantId: context.tenantId, userId: customerId, access: customerAccess, storeId, traceId: 'm03d-wifi-denied', ip: '127.0.0.1', userAgent: 'M03-D denied' }), (error) => error instanceof StoreAccessError && error.code === 'WIFI_ACCESS_FORBIDDEN' ); const [orderResult] = await pool.query( `INSERT INTO qipai_orders (tenant_id, store_id, room_id, order_no, status, start_at, end_at) VALUES (?, ?, ?, 'M03D-WIFI-ORDER', 'IN_USE', DATE_SUB(UTC_TIMESTAMP(3), INTERVAL 10 MINUTE), DATE_ADD(UTC_TIMESTAMP(3), INTERVAL 50 MINUTE))`, [context.tenantId, storeId, roomId] ); await pool.query( `INSERT INTO qipai_order_user_access (tenant_id, order_id, user_id) VALUES (?, ?, ?)`, [context.tenantId, orderResult.insertId, customerId] ); const wifi = await repository.getWifi({ tenantId: context.tenantId, userId: customerId, access: customerAccess, storeId, traceId: 'm03d-wifi-allowed', ip: '127.0.0.1', userAgent: 'M03-D allowed' }); assert.equal(wifi.ssid, 'M03A-WIFI'); assert.equal(wifi.password, 'sanitized-password'); const [auditRows] = await pool.query( `SELECT CAST(metadata AS CHAR) AS metadata FROM qipai_audit_logs WHERE tenant_id = ? AND trace_id = 'm03d-wifi-allowed'`, [context.tenantId] ); assert.equal(auditRows.length, 1); assert.match(auditRows[0].metadata, /M03A-WIFI/); assert.doesNotMatch(auditRows[0].metadata, /sanitized-password/); } async function assertContentManagement(pool, context) { const [adminRows] = await pool.query( `SELECT u.id FROM qipai_users u INNER JOIN qipai_user_roles ur ON ur.tenant_id = u.tenant_id AND ur.user_id = u.id INNER JOIN qipai_roles r ON r.id = ur.role_id AND r.tenant_id = ur.tenant_id WHERE u.tenant_id = ? AND r.code = 'TENANT_ADMIN' LIMIT 1`, [context.tenantId] ); const [storeRows] = await pool.query( `SELECT id FROM qipai_stores WHERE tenant_id = ? AND name = 'M03A Store' LIMIT 1`, [context.tenantId] ); const adminId = String(adminRows[0].id); const storeId = String(storeRows[0].id); const rbac = new RbacRepository(pool); const access = await rbac.getAccessProfile(context.tenantId, adminId); const actor = { tenantId: context.tenantId, userId: adminId, access, traceId: 'm03b-live-test', ip: '127.0.0.1', userAgent: 'M03-B live test' }; const repository = new ContentRepository(pool); const asset = await repository.registerAsset(actor, storeId, { storagePath: `tenants/${context.tenantId}/stores/${storeId}/sanitized.webp`, publicUrl: `https://api.txyundm.cn/uploads/tenants/${context.tenantId}/stores/${storeId}/sanitized.webp`, mimeType: 'image/webp', byteSize: 1024, width: 1200, height: 600, checksumSha256: 'a'.repeat(64) }); const draft1 = await repository.saveDecoration(actor, { storeId, templateCode: 'classic', schemaVersion: 1, content: { components: [{ type: 'HERO', props: { assetId: asset.assetId } }] } }); const draft2 = await repository.saveDecoration(actor, { storeId, templateCode: 'modern', schemaVersion: 1, content: { components: [{ type: 'ROOM_LIST', props: {} }] } }); assert.equal(draft1.version, 1); assert.equal(draft2.version, 2); await repository.publishDecoration(actor, draft1.decorationId, storeId); await repository.publishDecoration(actor, draft2.decorationId, storeId); const [decorationRows] = await pool.query( `SELECT version, status FROM qipai_store_decorations WHERE tenant_id = ? AND store_id = ? ORDER BY version`, [context.tenantId, storeId] ); assert.deepEqual(decorationRows, [ { version: 1, status: 'ARCHIVED' }, { version: 2, status: 'PUBLISHED' } ]); const ad = await repository.saveAdvertisement(actor, { scopeType: 'STORE', storeId, title: 'Store banner', imageAssetId: asset.assetId, targetType: 'PAGE', targetValue: '/pages/index/index', startsAt: null, endsAt: null, status: 'ACTIVE', sortOrder: 1 }); assert.match(ad.advertisementId, /^[1-9]\d*$/); assert.equal((await repository.listAdvertisements(actor))[0].scopeType, 'STORE'); await assert.rejects( () => repository.saveAdvertisement(actor, { scopeType: 'PLATFORM', title: 'forbidden', imageAssetId: asset.assetId, targetType: 'NONE', targetValue: '', status: 'DRAFT', sortOrder: 0 }), (error) => error instanceof ContentError && error.code === 'PLATFORM_AD_FORBIDDEN' ); } const config = loadConfig(); assert.equal(config.mysql.passwordConfigured, true, 'Live migration test requires a temporary password.'); assert.match( config.mysql.database, /^qipai_m01c_test_[a-z0-9_]+$/, 'Live migration test refuses to use a non-temporary database.' ); const plans = { up: await loadMigrationPlan('up'), verify: await loadMigrationPlan('verify'), down: await loadMigrationPlan('down') }; const pool = createMySqlPool(config); try { const legacyFixtureStatements = await loadLegacyFixture(pool); await assertLegacyCompatibility(pool); console.log('PASS: sanitized legacy fixture is readable before migration.'); await executeMigrationPlan(pool, plans.up); await executeMigrationPlan(pool, plans.verify); assert.deepEqual(await readCoreTables(pool), expectedTables); assert.deepEqual(await readMigrationVersions(pool), [ { version: '2026061601', name: 'm01b_core_schema' }, { version: '2026061802', name: 'm01c_async_tasks' }, { version: '2026061803', name: 'm02a_tenant_apps' }, { version: '2026061804', name: 'm02b_wechat_auth' }, { version: '2026061805', name: 'm02c_rbac' }, { version: '2026061806', name: 'm02d_user_management' }, { version: '2026061807', name: 'm03a_store_room_domain' }, { version: '2026061808', name: 'm03b_decoration_ads_media' }, { version: '2026061809', name: 'm03c_store_discovery' }, { version: '2026061810', name: 'm03d_scene_wifi_access' } ]); await assertTaskDurability(pool); const loginContext = await assertPlatformTenantIsolation(pool); await assertRevocableAuthSession(pool, loginContext); await assertUserManagement(pool, loginContext); await assertStoreRoomDomain(pool, loginContext); await assertContentManagement(pool, loginContext); await assertStoreDiscovery(pool, loginContext); await assertSceneAndWifiAccess(pool, loginContext); await assertLegacyCompatibility(pool); console.log('PASS: first up, verify, tenant isolation and revocable auth checks completed.'); await executeMigrationPlan(pool, plans.down); assert.deepEqual(await readCoreTables(pool), []); await assertLegacyCompatibility(pool); console.log('PASS: down removed all M01-B/M01-C tables.'); await executeMigrationPlan(pool, plans.up); await executeMigrationPlan(pool, plans.verify); assert.deepEqual(await readCoreTables(pool), expectedTables); assert.deepEqual(await readMigrationVersions(pool), [ { version: '2026061601', name: 'm01b_core_schema' }, { version: '2026061802', name: 'm01c_async_tasks' }, { version: '2026061803', name: 'm02a_tenant_apps' }, { version: '2026061804', name: 'm02b_wechat_auth' }, { version: '2026061805', name: 'm02c_rbac' }, { version: '2026061806', name: 'm02d_user_management' }, { version: '2026061807', name: 'm03a_store_room_domain' }, { version: '2026061808', name: 'm03b_decoration_ads_media' }, { version: '2026061809', name: 'm03c_store_discovery' }, { version: '2026061810', name: 'm03d_scene_wifi_access' } ]); await assertLegacyCompatibility(pool); console.log('PASS: second up and verify restored the schema.'); console.log(JSON.stringify({ mysqlHost: config.mysql.host, databaseClass: 'temporary', sequence: ['up', 'verify', 'down', 'up', 'verify'], checksums: { up: plans.up.checksum, verify: plans.verify.checksum, down: plans.down.checksum }, statementCounts: { up: plans.up.statements.length, verify: plans.verify.statements.length, down: plans.down.statements.length }, legacyFixtureStatements, legacyCompatibilityChecks: [ 'stores', 'rooms', 'orders', 'devices', 'tenant isolation', 'decimal cents', 'app-to-tenant binding', 'cross-tenant bootstrap rejection', 'openid identity reuse', 'session revocation', 'role-version invalidation', 'customer capabilities', 'cross-tenant store grant rejection', 'staff creation and store assignment', 'access-change session revocation', 'user-management audit log', 'store business hours and coordinates', 'room category and integer-cent pricing', 'configuration and operational status separation', 'room disabled period', 'cross-store management rejection', 'tenant-isolated media asset', 'versioned decoration publish and archive', 'store advertisement delivery scope', 'platform advertisement rejection', 'city fallback store filtering', 'server-side distance sorting', 'empty manual city result', 'scene regeneration revokes old code', 'QR and NFC navigation without permissions', 'scene scan statistics', 'Wi-Fi denied without active order', 'Wi-Fi allowed by active order grant', 'Wi-Fi audit excludes password' ] }, null, 2)); } finally { await closeMySqlPool(pool); }