#!/usr/bin/env node import fs from 'node:fs/promises'; import path from 'node:path'; import mysql from 'mysql2/promise'; import pg from 'pg'; import { assertPortalRuntimePath, hashArtifact } from '../release-gate/artifact.mjs'; import { createLocalGateStack } from '../release-gate/local-stack.mjs'; const { Client: PgClient } = pg; const ROOT = path.resolve(new URL('..', import.meta.url).pathname); const runtimeRoot = assertPortalRuntimePath(path.join(ROOT, '.runtime', 'portal'), { repoRoot: ROOT }); const artifactBefore = await hashArtifact(runtimeRoot); const runId = `upgrade-${new Date().toISOString().replace(/\D/g, '').slice(0, 14)}`; const markerUserId = '00000000-0000-4000-8000-000000000701'; const markerUsername = 'gate_upgrade_fixture'; const markerValue = 'sanitized-page-data-fixture'; const stack = await createLocalGateStack({ root: ROOT, runId, port: Number(process.env.MEMIND_RELEASE_GATE_UPGRADE_PORT ?? 19085), portalRoot: runtimeRoot, nodeEnv: 'production', runtimeProfile: 'production', async beforePortalStart({ mysqlUrl, pgUrl }) { const schemaSql = await fs.readFile(path.join(ROOT, 'schema.sql'), 'utf8'); const mysqlConnection = await mysql.createConnection({ uri: mysqlUrl, multipleStatements: true, }); try { await mysqlConnection.query(schemaSql); const now = Date.now(); await mysqlConnection.query( `INSERT INTO h5_users (id, username, slug, email, display_name, salt, password_hash, password_algorithm, role, status, plan_type, workspace_root, low_balance_gift_eligible, low_balance_gift_granted_at, created_at, updated_at) VALUES (?, ?, ?, NULL, ?, ?, ?, 'pbkdf2-sha512', 'user', 'active', 'free', ?, 0, NULL, ?, ?)`, [ markerUserId, markerUsername, markerUsername, '脱敏升级测试用户', 'fixture-salt', 'fixture-hash', `/sanitized/${markerUserId}`, now, now, ], ); await mysqlConnection.query('ALTER TABLE h5_users DROP INDEX uq_h5_users_slug'); await mysqlConnection.query('ALTER TABLE h5_users DROP INDEX uq_h5_users_email'); await mysqlConnection.query( `ALTER TABLE h5_users DROP COLUMN slug, DROP COLUMN email, DROP COLUMN password_algorithm, DROP COLUMN plan_type`, ); } finally { await mysqlConnection.end(); } const pgClient = new PgClient({ connectionString: pgUrl }); await pgClient.connect(); try { await pgClient.query( `CREATE TABLE gate_sanitized_upgrade_fixture ( id TEXT PRIMARY KEY, value TEXT NOT NULL )`, ); await pgClient.query( 'INSERT INTO gate_sanitized_upgrade_fixture (id, value) VALUES ($1, $2)', ['fixture-1', markerValue], ); } finally { await pgClient.end(); } }, }); try { async function verifyPreservedState() { const mysqlConnection = await mysql.createConnection(stack.mysqlUrl); try { const [users] = await mysqlConnection.query( `SELECT username, slug, password_algorithm, plan_type FROM h5_users WHERE id = ?`, [markerUserId], ); if (users.length !== 1) throw new Error('sanitized MySQL user fixture was lost'); if (users[0].slug !== markerUsername) throw new Error('legacy user slug was not migrated'); if (users[0].password_algorithm !== 'pbkdf2-sha512') { throw new Error('legacy password algorithm default was not restored'); } if (users[0].plan_type !== 'free') throw new Error('legacy plan default was not restored'); } finally { await mysqlConnection.end(); } const pgClient = new PgClient({ connectionString: stack.pgUrl }); await pgClient.connect(); try { const result = await pgClient.query( 'SELECT value FROM gate_sanitized_upgrade_fixture WHERE id = $1', ['fixture-1'], ); if (result.rows[0]?.value !== markerValue) { throw new Error('sanitized PostgreSQL fixture was lost'); } } finally { await pgClient.end(); } } await verifyPreservedState(); await stack.restartPortal(); await verifyPreservedState(); console.log('PASS REL-07 legacy MySQL columns migrate and sanitized user data is preserved'); console.log('PASS REL-07 Page Data PostgreSQL fixture survives two idempotent packaged-runtime starts'); } finally { await stack.cleanup(); const artifactAfter = await hashArtifact(runtimeRoot); if (artifactAfter.sha256 !== artifactBefore.sha256) { throw new Error( `packaged runtime mutated its own artifact during upgrade verification: ${artifactBefore.sha256} -> ${artifactAfter.sha256}`, ); } }