import { randomInt, randomUUID } from "node:crypto"; import process from "node:process"; import { createInterface } from "node:readline"; import pg from "pg"; const { Pool } = pg; const TEST_YEAR = 2026; const OWNER_NAME = "Codex Live UI Test"; const PURCHASED_ROOM_TYPE = "RM1"; const USED_ROOM_TYPE = "SU3"; const CHECK_IN = "2026-08-10"; const CHECK_OUT = "2026-08-12"; const EXPECTED_NIGHT = 2; const EXPECTED_MULTIPLIER = 3; const EXPECTED_USE = 6; const EXPECTED_BALANCE = 9; function safeError(error, fallback = "LIVE_UI_FIXTURE_FAILED") { return typeof error?.code === "string" ? error.code : fallback; } function writeEvent(event, details = {}) { process.stdout.write(`${JSON.stringify({ event, ...details })}\n`); } function businessCounts(row) { return { owners: Number(row.owners), periods: Number(row.periods), usage: Number(row.usage), ledger: Number(row.ledger) }; } function allZero(counts) { return Object.values(counts).every(value => value === 0); } async function getBusinessCounts(client) { const result = await client.query(` SELECT (SELECT count(*)::integer FROM condon.owner_accounts) AS owners, (SELECT count(*)::integer FROM condon.entitlement_periods) AS periods, (SELECT count(*)::integer FROM condon.usage_records) AS usage, (SELECT count(*)::integer FROM condon.entitlement_ledger) AS ledger `); return businessCounts(result.rows[0]); } async function setupFixture(pool, fixture) { const client = await pool.connect(); try { await client.query("BEGIN"); const initialCounts = await getBusinessCounts(client); if (!allZero(initialCounts)) { throw Object.assign(new Error("CONDO business tables must be empty"), { code: "TEST_REQUIRES_EMPTY_CONDON" }); } const multiplier = await client.query( ` SELECT condon.calculate_multiplier( $1::varchar, $2::varchar, NULL::smallint )::integer AS value `, [PURCHASED_ROOM_TYPE, USED_ROOM_TYPE] ); if (multiplier.rows[0].value !== EXPECTED_MULTIPLIER) { throw Object.assign(new Error("Unexpected multiplier"), { code: "UNEXPECTED_MULTIPLIER" }); } await client.query( ` INSERT INTO condon.owner_accounts ( id, account_no, transfer_date, owner_name, room_no, purchased_room_type_code, unit_no, member_no ) VALUES ($1, NULL, NULL, $2, $3, $4, $5, $6) `, [ fixture.ownerId, OWNER_NAME, fixture.roomNo, PURCHASED_ROOM_TYPE, fixture.unitNo, fixture.memberNo ] ); const periodResult = await client.query( ` SELECT period.* FROM condon.open_entitlement_period( $1::uuid, $2::uuid, $3::smallint, $4::uuid, NULL::uuid ) AS period `, [ fixture.periodId, fixture.ownerId, TEST_YEAR, fixture.annualGrantLedgerId ] ); const period = periodResult.rows[0]; if ( Number(period.annual_grant) !== 15 || Number(period.carry_forward) !== 0 || Number(period.current_balance) !== 15 ) { throw Object.assign(new Error("Unexpected entitlement period"), { code: "UNEXPECTED_ENTITLEMENT_PERIOD" }); } const setupCounts = await getBusinessCounts(client); if ( setupCounts.owners !== 1 || setupCounts.periods !== 1 || setupCounts.usage !== 0 || setupCounts.ledger !== 1 ) { throw Object.assign(new Error("Unexpected setup counts"), { code: "UNEXPECTED_SETUP_COUNTS" }); } await client.query("COMMIT"); return setupCounts; } catch (error) { await client.query("ROLLBACK").catch(() => undefined); throw error; } finally { client.release(); } } async function observeUsage(pool, fixture) { const result = await pool.query( ` SELECT ur.id, ur.confirmation_no, ur.night_count, ur.applied_multiplier, ur.use_nights, ur.balance_before, ur.balance_after, ur.used_room_type_code, ur.remark, ep.current_balance, ep.row_version, el.id AS usage_ledger_id, el.delta_nights, el.balance_before AS ledger_balance_before, el.balance_after AS ledger_balance_after FROM condon.usage_records AS ur JOIN condon.entitlement_periods AS ep ON ep.id = ur.entitlement_period_id JOIN condon.entitlement_ledger AS el ON el.usage_record_id = ur.id WHERE ur.owner_account_id = $1 AND ur.entitlement_period_id = $2 AND ur.confirmation_no = $3 `, [fixture.ownerId, fixture.periodId, fixture.confirmationNo] ); if (result.rowCount !== 1) return null; const row = result.rows[0]; return { usageRecordId: row.id, usageLedgerId: row.usage_ledger_id, confirmationNo: row.confirmation_no, night: Number(row.night_count), multiplier: Number(row.applied_multiplier), use: Number(row.use_nights), balanceBefore: Number(row.balance_before), balance: Number(row.balance_after), usedRoomType: row.used_room_type_code, remark: row.remark, periodBalance: Number(row.current_balance), periodRowVersion: Number(row.row_version), ledgerDelta: Number(row.delta_nights), ledgerBalanceBefore: Number(row.ledger_balance_before), ledgerBalance: Number(row.ledger_balance_after) }; } function usageIsExpected(usage, fixture) { return usage !== null && usage.confirmationNo === fixture.confirmationNo && usage.night === EXPECTED_NIGHT && usage.multiplier === EXPECTED_MULTIPLIER && usage.use === EXPECTED_USE && usage.balanceBefore === 15 && usage.balance === EXPECTED_BALANCE && usage.usedRoomType === USED_ROOM_TYPE && usage.remark === fixture.remark && usage.periodBalance === EXPECTED_BALANCE && usage.periodRowVersion === 2 && usage.ledgerDelta === -EXPECTED_USE && usage.ledgerBalanceBefore === 15 && usage.ledgerBalance === EXPECTED_BALANCE; } async function cleanupFixture(pool, fixture) { const client = await pool.connect(); let deleted; try { await client.query("BEGIN"); const ownerGuard = await client.query( ` SELECT id FROM condon.owner_accounts WHERE id = $1 AND owner_name = $2 AND room_no = $3 AND member_no = $4 FOR UPDATE `, [fixture.ownerId, OWNER_NAME, fixture.roomNo, fixture.memberNo] ); if (ownerGuard.rowCount !== 1) { throw Object.assign(new Error("Fixture owner guard failed"), { code: "FIXTURE_OWNER_GUARD_FAILED" }); } const ledgerResult = await client.query( ` DELETE FROM condon.entitlement_ledger WHERE entitlement_period_id = $1 RETURNING id, entry_type, usage_record_id `, [fixture.periodId] ); const usageResult = await client.query( ` DELETE FROM condon.usage_records WHERE owner_account_id = $1 AND entitlement_period_id = $2 RETURNING id, confirmation_no `, [fixture.ownerId, fixture.periodId] ); const periodResult = await client.query( ` DELETE FROM condon.entitlement_periods WHERE id = $1 AND owner_account_id = $2 RETURNING id `, [fixture.periodId, fixture.ownerId] ); const ownerResult = await client.query( ` DELETE FROM condon.owner_accounts WHERE id = $1 AND owner_name = $2 AND room_no = $3 AND member_no = $4 RETURNING id `, [fixture.ownerId, OWNER_NAME, fixture.roomNo, fixture.memberNo] ); if (periodResult.rowCount !== 1 || ownerResult.rowCount !== 1) { throw Object.assign(new Error("Fixture cleanup cardinality failed"), { code: "FIXTURE_CLEANUP_CARDINALITY_FAILED" }); } deleted = { ledger: ledgerResult.rowCount, usage: usageResult.rowCount, periods: periodResult.rowCount, owners: ownerResult.rowCount }; await client.query("COMMIT"); } catch (error) { await client.query("ROLLBACK").catch(() => undefined); throw error; } finally { client.release(); } const finalCounts = await getBusinessCounts(pool); if (!allZero(finalCounts)) { throw Object.assign(new Error("CONDO business tables are not empty"), { code: "FINAL_CONDON_NOT_EMPTY" }); } return { deleted, finalCounts }; } const lines = createInterface({ input: process.stdin, terminal: false }); const iterator = lines[Symbol.asyncIterator](); let pool; let fixture; let setupComplete = false; let cleanupComplete = false; let usageObservation = null; let expectUsage = true; let failureCode = null; let signalResolve; const signalPromise = new Promise(resolve => { signalResolve = resolve; }); for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) { process.once(signal, () => signalResolve({ signal })); } try { const first = await iterator.next(); if (first.done || !first.value.trim()) { throw Object.assign(new Error("Missing connection input"), { code: "MISSING_CONNECTION_INPUT" }); } const input = JSON.parse(first.value); if (input.database !== "booking_test") { throw Object.assign(new Error("Unexpected database"), { code: "UNEXPECTED_DATABASE" }); } pool = new Pool({ host: input.host, port: input.port, user: input.user, password: input.password, database: input.database, ssl: input.ssl ? { rejectUnauthorized: false } : undefined, max: 2, application_name: "condon_live_ui_fixture", connectionTimeoutMillis: 10_000, options: "-c statement_timeout=30000 -c lock_timeout=5000" }); const token = randomUUID().replaceAll("-", "").slice(0, 12).toUpperCase(); fixture = { ownerId: randomUUID(), periodId: randomUUID(), annualGrantLedgerId: randomUUID(), roomNo: `E2E-${token}`, unitNo: `TEST-${token.slice(0, 6)}`, memberNo: `E2E-${token}`, confirmationNo: `98${Date.now()}${randomInt(100, 1000)}`, remark: `Live UI E2E ${token}` }; const setupCounts = await setupFixture(pool, fixture); setupComplete = true; writeEvent("fixtureReady", { fixture: { ...fixture, ownerName: OWNER_NAME, purchasedRoomType: PURCHASED_ROOM_TYPE, usedRoomType: USED_ROOM_TYPE, year: TEST_YEAR, checkIn: CHECK_IN, checkOut: CHECK_OUT, expectedNight: EXPECTED_NIGHT, expectedMultiplier: EXPECTED_MULTIPLIER, expectedUse: EXPECTED_USE, expectedBalance: EXPECTED_BALANCE }, setupCounts }); const next = await Promise.race([ iterator.next(), signalPromise ]); if (next?.signal) { expectUsage = false; writeEvent("cleanupRequested", { reason: next.signal }); } else if (next.done) { expectUsage = false; writeEvent("cleanupRequested", { reason: "stdin_closed" }); } else { const command = JSON.parse(next.value); if (command.action !== "cleanup") { throw Object.assign(new Error("Unexpected command"), { code: "UNEXPECTED_FIXTURE_COMMAND" }); } expectUsage = command.expectUsage !== false; writeEvent("cleanupRequested", { reason: "command" }); } usageObservation = await observeUsage(pool, fixture); if (expectUsage && !usageIsExpected(usageObservation, fixture)) { throw Object.assign(new Error("Unexpected usage result"), { code: "UNEXPECTED_USAGE_RESULT" }); } } catch (error) { failureCode = safeError(error); process.exitCode = 1; writeEvent("fixtureError", { errorCode: failureCode }); } finally { if (pool && setupComplete && fixture) { try { const cleanup = await cleanupFixture(pool, fixture); cleanupComplete = true; writeEvent("fixtureCleaned", { usageExpected: expectUsage, usageVerified: expectUsage ? usageIsExpected(usageObservation, fixture) : null, usage: usageObservation, ...cleanup }); } catch (error) { failureCode ??= safeError(error, "LIVE_UI_CLEANUP_FAILED"); process.exitCode = 1; writeEvent("cleanupError", { errorCode: safeError(error, "LIVE_UI_CLEANUP_FAILED") }); } } lines.close(); if (pool) await pool.end().catch(() => { process.exitCode = 1; }); writeEvent("fixtureClosed", { ok: !failureCode && cleanupComplete, errorCode: failureCode }); }