verify-ledger-consistency.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. #!/usr/bin/env bun
  2. import postgres from "postgres";
  3. if (!process.env.DSN && process.env.DATABASE_URL) {
  4. process.env.DSN = process.env.DATABASE_URL;
  5. }
  6. type CountCheckRow = {
  7. message_request_count: number;
  8. usage_ledger_count: number;
  9. };
  10. type CostCheckRow = {
  11. message_total_cost: string;
  12. ledger_total_cost: string;
  13. is_equal: boolean;
  14. };
  15. type WarmupLeakRow = {
  16. warmup_leak_count: number;
  17. };
  18. type OrphanRow = {
  19. orphan_ledger_count: number;
  20. };
  21. function getFirstRow<T>(rows: T[]): T {
  22. const row = rows[0];
  23. if (!row) {
  24. throw new Error("expected query to return one row");
  25. }
  26. return row;
  27. }
  28. async function main(): Promise<void> {
  29. const dsn = process.env.DSN;
  30. if (!dsn) {
  31. console.log("[SKIP] DSN/DATABASE_URL not configured, skipping ledger consistency verification.");
  32. process.exit(0);
  33. }
  34. const client = postgres(dsn, {
  35. max: 1,
  36. idle_timeout: 5,
  37. connect_timeout: 5,
  38. });
  39. try {
  40. const countRow = getFirstRow<CountCheckRow>(await client`
  41. SELECT
  42. (
  43. SELECT COUNT(*)::integer
  44. FROM message_request
  45. WHERE blocked_by IS DISTINCT FROM 'warmup'
  46. ) AS message_request_count,
  47. (
  48. SELECT COUNT(*)::integer
  49. FROM usage_ledger
  50. ) AS usage_ledger_count
  51. `);
  52. const costRow = getFirstRow<CostCheckRow>(await client`
  53. WITH message_sum AS (
  54. SELECT COALESCE(SUM(cost_usd), 0) AS total_cost
  55. FROM message_request
  56. WHERE blocked_by IS DISTINCT FROM 'warmup'
  57. ),
  58. ledger_sum AS (
  59. SELECT COALESCE(SUM(cost_usd), 0) AS total_cost
  60. FROM usage_ledger
  61. )
  62. SELECT
  63. message_sum.total_cost::text AS message_total_cost,
  64. ledger_sum.total_cost::text AS ledger_total_cost,
  65. (message_sum.total_cost = ledger_sum.total_cost) AS is_equal
  66. FROM message_sum, ledger_sum
  67. `);
  68. const warmupRow = getFirstRow<WarmupLeakRow>(await client`
  69. SELECT COUNT(*)::integer AS warmup_leak_count
  70. FROM usage_ledger
  71. WHERE blocked_by = 'warmup'
  72. `);
  73. const orphanRow = getFirstRow<OrphanRow>(await client`
  74. SELECT COUNT(*)::integer AS orphan_ledger_count
  75. FROM usage_ledger ul
  76. LEFT JOIN message_request mr ON mr.id = ul.request_id
  77. WHERE mr.id IS NULL
  78. `);
  79. console.log("Ledger consistency verification");
  80. console.log("=============================");
  81. console.log(
  82. `Count parity: message_request(non-warmup)=${countRow.message_request_count}, usage_ledger=${countRow.usage_ledger_count}`
  83. );
  84. console.log(
  85. `Cost parity: message_request=${costRow.message_total_cost}, usage_ledger=${costRow.ledger_total_cost}`
  86. );
  87. console.log(`Warmup leak count in usage_ledger: ${warmupRow.warmup_leak_count}`);
  88. console.log(`Orphan ledger rows (expected/OK after log deletion): ${orphanRow.orphan_ledger_count}`);
  89. const criticalIssues: string[] = [];
  90. if (countRow.message_request_count !== countRow.usage_ledger_count) {
  91. criticalIssues.push("count mismatch");
  92. }
  93. if (!costRow.is_equal) {
  94. criticalIssues.push("cost mismatch");
  95. }
  96. if (warmupRow.warmup_leak_count > 0) {
  97. criticalIssues.push("warmup rows leaked into usage_ledger");
  98. }
  99. if (criticalIssues.length > 0) {
  100. console.error(`RESULT: FAILED (${criticalIssues.join(", ")})`);
  101. process.exit(1);
  102. }
  103. console.log("RESULT: PASS (no critical mismatches)");
  104. process.exit(0);
  105. } finally {
  106. await client.end();
  107. }
  108. }
  109. main().catch((error) => {
  110. console.error("Ledger consistency verification failed with error:", error);
  111. process.exit(1);
  112. });