import assert from "node:assert/strict"; import { existsSync, readdirSync, readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import test from "node:test"; import { natalConsultationThinkingPlan } from "../src/lib/consultation-thinking-plan.ts"; const migrationsDir = new URL("../supabase/migrations/", import.meta.url); const originalSql = readFileSync( new URL("../supabase/migrations/20260901010000_append_consultation_question.sql", import.meta.url), "utf8", ); const sql = readFileSync( new URL("../supabase/migrations/20260916010000_consultation_session_capacity.sql", import.meta.url), "utf8", ); const itemRoute = readFileSync( new URL("../src/app/api/sessions/[id]/route.ts", import.meta.url), "utf8", ); const consultRoute = readFileSync( new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8", ); const SESSION_SELECT = "id,title,theme,model_id,messages,session_type,rectification_case_id,chart_profile_id,chart_profile_name,chart_profile_role,updated_at,pinned,archived_at"; function functionBody(source: string): string { const match = source.match(/as \$\$\r?\n([\s\S]*?)\r?\n\$\$;/); assert.ok(match?.[1], "function body must be present"); return match[1]; } function withoutLineComments(source: string): string { return source.replace(/--[^\n]*/g, ""); } function utf8Bytes(value: string): number { return Buffer.byteLength(value, "utf8"); } function typicalReceipts(domains: readonly string[]) { const workflowReceipt = { route: "multi-domain", status: "ready", preciseTiming: "allowed", missingLayers: [] as string[], domains, }; const agentExecutionReceipt = { runId: "run-typical", runtime: "mastra-agentic" as const, skill: { name: "jyotish-vedic-astrology" as const, loaded: true, version: "1.0.0", referenceReads: 2, methodologySections: 4, }, steps: [ { sequence: 1, kind: "skill" as const, name: "jyotish-vedic-astrology", status: "completed" as const, durationMs: 120 }, { sequence: 2, kind: "tool" as const, name: "run-jyotish-consultation", status: "completed" as const, durationMs: 1800 }, ], stepBudget: { planned: 8, used: 2, remaining: 6, truncated: false }, workflow: workflowReceipt, techniqueTruth: "verified", techniqueAuditTable: [ { technique: "VedAstro Cloud State", status: "blocked" as const, note: "未经核验的官方云证据,置信度封顶" }, { technique: "Functional Benefic/Malefic", status: "executed" as const, note: "功能属性与自然属性冲突时降置信" }, { technique: "Formal Vargas D1–D60", status: "executed" as const, note: "20/20 传统命名分盘" }, { technique: "Vimshottari sub-periods", status: "executed" as const, note: "本轮已纳入证据计划" }, { technique: "Narayana Dasha", status: "executed" as const, note: "本轮无独立当前读数" }, { technique: "Yogas", status: "executed" as const, note: "成盘与落空均列出" }, { technique: "Ashtakavarga", status: "executed" as const, note: "本轮已纳入证据计划" }, { technique: "Shadbala components", status: "executed" as const, note: "本轮已纳入证据计划" }, { technique: "MEVG / Global Web Evidence", status: "blocked" as const, note: "本轮未闭合" }, { technique: "Real Case Calibration", status: "blocked" as const, note: "本轮未闭合" }, { technique: "Timing Precision Gate", status: "executed" as const, note: "按声明精度封顶" }, { technique: "Prashna chart", status: "not_applicable" as const, note: "本轮为本命咨询" }, ], }; return { techniqueTruth: "verified", workflowReceipt, agentExecutionReceipt, }; } function thinkingPlan(domains: readonly ("career" | "wealth" | "marriage")[]) { return natalConsultationThinkingPlan({ domains, requiredBlocks: [ "raw_structure", "raman_six_step", "yoga_table", "timing", "synthesis", "technique_audit_table", "modern_wrap", ], mustUseLayers: ["D1", "run-jyotish-consultation", "D10", "skill_read"], }); } function assistantMessage(_index: number, domains: readonly ("career" | "wealth" | "marriage")[]) { const receipts = typicalReceipts(domains); return { role: "assistant" as const, text: "正".repeat(4000), thinkingText: "思".repeat(4000), thinkingSections: thinkingPlan(domains), ...receipts, }; } function userMessage(index: number) { return { role: "user" as const, text: `第${String(index + 1).padStart(2, "0")}问`.padEnd(100, "问"), requestId: `capacity-req-${index + 1}`, }; } function sessionDetailPayload(rounds: number, domains: readonly ("career" | "wealth" | "marriage")[]) { const messages = Array.from({ length: rounds }, (_, index) => [ userMessage(index), assistantMessage(index, domains), ]).flat(); return { session: { id: "00000000-0000-4000-8000-000000000001", title: "事业方向", theme: "career", model_id: "test-model", messages, session_type: "consultation", rectification_case_id: null, chart_profile_id: null, chart_profile_name: null, chart_profile_role: null, updated_at: "2026-09-15T00:00:00.000Z", pinned: false, archived_at: null, }, }; } function countedText(messages: readonly { text?: string }[]): number { return messages.reduce((sum, message) => sum + (message.text ?? "").length, 0); } function oldCountedChars(messages: readonly Record[]): number { return messages.reduce((sum, message) => { const text = typeof message.text === "string" ? message.text.length : 0; const thinking = typeof message.thinkingText === "string" ? message.thinkingText.length : 0; const sections = "thinkingSections" in message ? JSON.stringify(message.thinkingSections).length : 0; return sum + text + thinking + sections; }, 0); } test("the capacity migration is the latest replace of append_consultation_question", () => { const names = readdirSync(fileURLToPath(migrationsDir)) .filter((name) => name.endsWith(".sql")) .sort(); const replaces = names.filter((name) => name.includes("append_consultation_question") || name.includes("consultation_session_capacity"), ); assert.deepEqual(replaces.at(-1), "20260916010000_consultation_session_capacity.sql"); assert.equal( existsSync(fileURLToPath(new URL("../db/migrations/20260916010000_consultation_session_capacity.sql", import.meta.url))), false, "business migration must not be copied into frontend/db/migrations (BUG-127/BUG-144)", ); }); test("CREATE OR REPLACE keeps the BUG-464 signature, lock, idempotency, and error codes", () => { assert.match( sql, /create or replace function public\.append_consultation_question\(\s*p_user_id uuid,\s*p_request_id text,\s*p_session_id uuid,\s*p_question_message jsonb\s*\)/, ); assert.match(sql, /returns table\(success boolean, error_code text\)/); const body = functionBody(sql); assert.match(body, /pg_advisory_xact_lock/); assert.match(body, /elem->>'requestId' = v_request_id/); assert.match(body, /return query select true, null::text;/); assert.match(body, /'session_missing'::text/); assert.match(body, /'session_full'::text/); assert.match(body, /'invalid_request'::text/); assert.match(body, /'invalid_question_message'::text/); assert.match(body, /char_length\(v_text\) > 16000/); assert.match(body, /v_count >= 200/); assert.match(body, /\(v_chars \+ v_new_chars\) > 200000/); assert.match( sql, /revoke all on function public\.append_consultation_question\(uuid, text, uuid, jsonb\)\s+from public, anon, authenticated/, ); assert.match( sql, /grant execute on function public\.append_consultation_question\(uuid, text, uuid, jsonb\)\s+to service_role/, ); assert.doesNotMatch(body, /session_physically_full|quota_exceeded|storage_full/); }); test("conversation quota sum counts only text; physical cap counts whole JSON", () => { assert.match(originalSql, /length\(coalesce\(elem->>'thinkingText', ''\)\)/); assert.match(originalSql, /elem \? 'thinkingSections'/); const body = functionBody(sql); const code = withoutLineComments(body); assert.doesNotMatch(code, /thinkingText/); assert.doesNotMatch(code, /thinkingSections/); assert.match(code, /coalesce\(sum\(length\(coalesce\(elem->>'text', ''\)\)\), 0\)/); assert.match(code, /coalesce\(sum\(length\(elem::text\)\), 0\)/); assert.match(code, /\(v_physical_chars \+ v_new_physical\) > 1000000/); assert.match(sql, /50 rounds × \(~4,000 body \+ ~4,000 thinkingText \+ ~3,000/); assert.match(sql, /thinkingSections \+ ~3,000 receipts\) ≈ 700,000\. Headroom → 1,000,000\./); }); test("both caps still surface as session_full; consult route is unchanged", () => { const body = functionBody(sql); const fullReturns = body.match(/return query select false, 'session_full'::text;/g) ?? []; assert.equal(fullReturns.length, 1); assert.match(consultRoute, /error_code === "session_full"/); assert.match(consultRoute, /code: "session_full"/); assert.doesNotMatch(consultRoute, /session_physically_full|physical_cap|storage_full/); assert.match(itemRoute, new RegExp(`sessionSelect = "${SESSION_SELECT}"`)); }); test("measured thinkingSections and receipts stay inside the physical-cap arithmetic", () => { const one = JSON.stringify(thinkingPlan(["career"])); const two = JSON.stringify(thinkingPlan(["career", "wealth"])); const three = JSON.stringify(thinkingPlan(["career", "wealth", "marriage"])); assert.ok(one.length >= 1200 && one.length <= 2500, `1-domain sections JSON length ${one.length}`); assert.ok(two.length >= 1800 && two.length <= 3500, `2-domain sections JSON length ${two.length}`); assert.ok(three.length >= 2400 && three.length <= 4500, `3-domain sections JSON length ${three.length}`); const receipts = typicalReceipts(["career", "wealth", "marriage"]); const receiptJson = JSON.stringify({ techniqueTruth: receipts.techniqueTruth, workflowReceipt: receipts.workflowReceipt, agentExecutionReceipt: receipts.agentExecutionReceipt, }); const receiptChars = receiptJson.length; assert.ok( receiptChars <= 4000, `typical three-receipt JSON is ${receiptChars} chars; update the 3,000 term and 1,000,000 cap if this is a real stored shape`, ); const perRound = 4000 + 4000 + three.length + receiptChars; const fiftyRoundPhysical = 50 * perRound; assert.ok( fiftyRoundPhysical < 1_000_000, `50 × typical stored round (${perRound} chars) = ${fiftyRoundPhysical}; must stay under the physical cap`, ); }); test("session-detail JSON at the old ~19-round cap versus the new ~50-round cap", () => { const domains = ["career", "wealth", "marriage"] as const; const oldCap = sessionDetailPayload(19, domains); const newCap = sessionDetailPayload(50, domains); const oldJson = JSON.stringify(oldCap); const newJson = JSON.stringify(newCap); const oldBytes = utf8Bytes(oldJson); const newBytes = utf8Bytes(newJson); assert.equal(oldCap.session.messages.length, 38); assert.equal(newCap.session.messages.length, 100); assert.ok(oldCountedChars(oldCap.session.messages) > 200_000, "19 full rounds already exceeded the old combined quota"); assert.ok(countedText(newCap.session.messages) >= 200_000, "50 rounds of 100+4000 visible text reach the conversation quota"); // Measured 2026-09-16 on this fixture: 19-round 562,240 B (0.536 MiB); // 50-round 1,479,034 B (1.411 MiB); ratio 2.631. assert.ok(oldBytes > 540_000 && oldBytes < 590_000, `19-round detail JSON was ${oldBytes} bytes`); assert.ok(newBytes > 1_450_000 && newBytes < 1_510_000, `50-round detail JSON was ${newBytes} bytes`); assert.ok(newBytes > oldBytes, "50-round detail JSON must be larger than 19-round"); const ratio = newBytes / oldBytes; assert.ok( ratio > 2.5 && ratio < 2.8, `50/19 size ratio was ${ratio.toFixed(3)} (utf8 ${oldBytes} → ${newBytes})`, ); });