import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { computeRequestFingerprint } from "../src/lib/personal-report-generation.ts"; import { buildPersonalReportSectionPlan, type PersonalReportSectionPlan, } from "../src/lib/personal-report-plan.ts"; import { safeParseServerReportDocument } from "../src/lib/personal-report-contract.server-core.ts"; import { resolveReportCreate, resolveReportDelete, resolveReportRead, type ReportCreateCoreDeps, type ReportServicePort, } from "../src/lib/personal-report-route-core.ts"; import type { CreateGeneratingInput, CreateGeneratingResult, PersonalReportRecord, } from "../src/lib/personal-report-service-core.ts"; import type { ReportEvidenceBundleV2 } from "../src/lib/report-evidence-bundle-v2.ts"; import type { ReportAgentPort, PersonalReportAgentOutput } from "../src/mastra/personal-report.ts"; const createRoute = readFileSync( new URL("../src/app/api/reports/route.ts", import.meta.url), "utf8", ); const itemRoute = readFileSync( new URL("../src/app/api/reports/[reportId]/route.ts", import.meta.url), "utf8", ); const generationSource = readFileSync( new URL("../src/lib/personal-report-generation.ts", import.meta.url), "utf8", ); const codesSource = readFileSync( new URL("../src/lib/personal-report-codes.ts", import.meta.url), "utf8", ); const coreSource = readFileSync( new URL("../src/lib/personal-report-route-core.ts", import.meta.url), "utf8", ); // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- const UUID_A = "11111111-1111-4111-8111-111111111111"; const UUID_B = "22222222-2222-4222-8222-222222222222"; const REPORT_ID = "33333333-3333-4333-8333-333333333333"; const SESSION_ID = "44444444-4444-4444-8444-444444444444"; function chartPayload() { const planets = ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu"] .map((name, index) => ({ id: name, sign: ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius"][index], degree: 12.5 + index * 10, house: index + 1, retrograde: index === 6, })); const houses = Array.from({ length: 12 }, (_, index) => ({ number: index + 1, sign: ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"][index], })); return { success: true, chart: { ascendant: { sign: "Leo", degree: 12.5 }, planets, houses, dasha: { mahadashas: [{ lord: "Moon", start: "2019-01-01", end: "2029-01-01" }] }, modules: { varga_full: { d9: { houses } }, narayana_dasha: { periods: [] } }, }, consumer_context: { route: "general", core_status: "ready", available_layers: ["D1", "Vimshottari"], missing_route_layers: [], hard_blockers: [], answer_policy: { can_answer_precise_timing: true, deterministic_claims_forbidden_for: [] }, }, machine_evidence_packet: { conflicts: [], sections: [{ name: "Functional Benefic/Malefic", status: "verified", note: "" }], }, }; } function agentOutput(overrides: Partial = {}): PersonalReportAgentOutput { return { executiveSummary: { headline: "综合盘面以事业发展为主线", summary: "事业结构稳定,财富与婚恋需结合分盘审慎解读。", priorities: ["先聚焦职业方向"], }, thematicNarrative: [ { id: "career", theme: "career", title: "事业", narrative: "事业层面以十宫与 D10 结构为主,方向性判断稳定。", actions: ["在稳定领域深耕"], caveats: [], claimStatus: "single_system_inference", evidenceRefs: ["ev-audit-2"], }, ], ...overrides, }; } function agentOutputForBundle( bundle: ReportEvidenceBundleV2, plan: PersonalReportSectionPlan = buildPersonalReportSectionPlan(bundle, "standard"), ): PersonalReportAgentOutput { return agentOutput({ thematicNarrative: plan.sections .filter((section) => section.kind === "thematic" && section.disposition === "write") .map((section) => { const claim = bundle.claimCards.find((entry) => entry.theme === section.theme); assert.ok(claim && section.theme, "a writable plan section must have a claim card"); return { id: section.id, theme: section.theme, title: claim.section, narrative: claim.conclusion, actions: [], caveats: [], claimStatus: claim.assertionLevel, evidenceRefs: [...section.evidenceRefs], }; }), }); } const fakeAgent: ReportAgentPort = { modelId: "test-model", async generate(bundle, plan) { return agentOutputForBundle(bundle, plan); }, }; function capturingAgent(capture: { input: unknown }): ReportAgentPort { return { modelId: "test-model", async generate(bundle, plan) { capture.input = bundle; return agentOutputForBundle(bundle, plan); }, }; } const SKILL_SNAPSHOT = { name: "jyotish-personal-report", version: "1.0.0", sha256: "a".repeat(64), sourceCommit: "b".repeat(40), }; function profileFixture(overrides: Record = {}) { return { name: "测试用户", birth_date: "1997-08-08", active_birth_time: "05:30:00", birth_time_status: "confirmed", latitude: 39.9, longitude: 116.4, timezone_offset: 8, birth_place_label: "北京", ...overrides, }; } class MemoryPersistence implements ReportServicePort { rows = new Map(); constructor(seed: PersonalReportRecord[] = []) { for (const row of seed) this.rows.set(row.id, row); } record(input: CreateGeneratingInput, id: string, status: "generating" | "failed"): PersonalReportRecord { return { id, userId: input.userId, sessionId: input.sessionId ?? null, chartProfileId: input.chartProfileId ?? null, requestId: input.requestId, requestFingerprint: input.requestFingerprint, reportType: input.reportType, status, schemaVersion: "report_document.v1", presentationMode: input.presentationMode, depth: input.depth, requestedThemes: input.requestedThemes ?? [], reportDocument: null, calculationHash: null, evidenceHash: null, skillName: input.skillName, skillVersion: input.skillVersion, skillSourceCommit: input.skillSourceCommit ?? null, skillSnapshotSha256: input.skillSnapshotSha256, failureCode: status === "failed" ? "calculation_unavailable" : null, createdAt: "2026-08-06T00:00:00.000Z", updatedAt: "2026-08-06T00:00:00.000Z", completedAt: null, }; } async getByUserAndRequestId(userId: string, requestId: string) { for (const row of this.rows.values()) { if (row.userId === userId && row.requestId === requestId) return row; } return null; } async createGenerating(input: CreateGeneratingInput): Promise { const existing = await this.getByUserAndRequestId(input.userId, input.requestId); if (existing) { if (existing.requestFingerprint === input.requestFingerprint) { return { kind: "replayed", record: existing }; } return { kind: "request_conflict", record: existing }; } const inFlight = [...this.rows.values()].find( (row) => row.userId === input.userId && row.status === "generating", ); if (inFlight) return { kind: "generation_in_progress", record: inFlight }; const row = this.record(input, REPORT_ID, "generating"); this.rows.set(row.id, row); return { kind: "created", record: row }; } async completeReady(userId: string, reportId: string, document: unknown) { const row = this.rows.get(reportId); assert.ok(row && row.userId === userId && row.status === "generating"); const readyRow: PersonalReportRecord = { ...row, status: "ready", reportDocument: document as PersonalReportRecord["reportDocument"], completedAt: "2026-08-06T00:01:00.000Z", }; this.rows.set(reportId, readyRow); return readyRow; } async markFailed(userId: string, reportId: string, failureCode: string) { const row = this.rows.get(reportId); assert.ok(row && row.userId === userId); const failedRow: PersonalReportRecord = { ...row, status: "failed", failureCode: failureCode as PersonalReportRecord["failureCode"], completedAt: "2026-08-06T00:01:00.000Z", }; this.rows.set(reportId, failedRow); return failedRow; } async getOwnedById(userId: string, reportId: string) { const row = this.rows.get(reportId); return row && row.userId === userId ? row : null; } async deleteOwned(userId: string, reportId: string) { const row = this.rows.get(reportId); if (!row || row.userId !== userId) return false; this.rows.delete(reportId); return true; } } function baseDeps(overrides: Partial = {}): ReportCreateCoreDeps { const persistence = new MemoryPersistence(); return { requestUrl: "https://jyotisha.chat/api/reports", origin: "https://jyotisha.chat", allowedOrigins: [], userId: UUID_A, rawBody: { requestId: UUID_B, reportType: "personal_full", presentationMode: "default", themes: ["career", "marriage", "wealth", "timing"], }, profile: profileFixture(), checkSessionOwned: async () => true, checkChartProfileOwned: async () => true, featureEnabled: true, dailyLimit: 5, counts: { countGenerating: async () => 0, countCreatedToday: async () => 0, }, persistence, model: { id: "test-model" }, runWorkflow: async () => chartPayload(), createAgent: () => fakeAgent, skillSnapshot: SKILL_SNAPSHOT, now: () => new Date("2026-08-06T00:00:00.000Z"), ...overrides, }; } // --------------------------------------------------------------------------- // Executable route core behavior (no network, no model) // --------------------------------------------------------------------------- test("core create: 401 when not logged in", async () => { const response = await resolveReportCreate(baseDeps({ userId: null })); assert.equal(response.status, 401); }); test("core create: 403 on cross-origin", async () => { const response = await resolveReportCreate(baseDeps({ origin: "https://evil.example" })); assert.equal(response.status, 403); assert.equal(response.body.code, "report_resource_forbidden"); }); test("core create: accepts staging same-origin POST behind the trusted reverse proxy", async () => { const response = await resolveReportCreate({ ...baseDeps({ requestUrl: "http://staging.jyotisha.chat/api/reports", origin: "https://staging.jyotisha.chat", }), requestHeaders: new Headers({ host: "staging.jyotisha.chat", "x-forwarded-host": "staging.jyotisha.chat", "x-forwarded-proto": "https", }), }); assert.equal(response.status, 201); assert.equal(response.body.report && typeof response.body.report, "object"); }); test("core create: 400 on invalid payload", async () => { const response = await resolveReportCreate(baseDeps({ rawBody: { requestId: "not-a-uuid" } })); assert.equal(response.status, 400); assert.equal(response.body.code, "invalid_request"); }); test("core create: 422 profile_incomplete without a profile", async () => { const response = await resolveReportCreate(baseDeps({ profile: null })); assert.equal(response.status, 422); assert.equal(response.body.code, "profile_incomplete"); }); test("core create: 422 birth_time_not_usable without a concrete reported minute", async () => { const response = await resolveReportCreate(baseDeps({ profile: profileFixture({ birth_time_status: "reported", birth_time_source: "period_only", reported_birth_time: null, active_birth_time: null, }), })); assert.equal(response.status, 422); assert.equal(response.body.code, "birth_time_not_usable"); }); test("core create: 201 for a reported minute uses that clock and directional policy", async () => { const hours: number[] = []; const minutes: number[] = []; const capture: { input: unknown } = { input: null }; const response = await resolveReportCreate(baseDeps({ profile: profileFixture({ birth_time_status: "reported", birth_time_source: "hospital_record", reported_birth_time: "08:16:00", active_birth_time: "05:30:00", }), runWorkflow: async (input) => { hours.push(input.hour); minutes.push(input.minute); return chartPayload(); }, createAgent: () => capturingAgent(capture), })); assert.equal(response.status, 201); assert.deepEqual([...new Set(hours)], [8]); assert.deepEqual([...new Set(minutes)], [16]); const bundle = capture.input as ReportEvidenceBundleV2; assert.equal(bundle.subject.birthTimeStatus, "reported"); assert.equal(bundle.answerPolicy.birthTimePolicy, "reported_directional_only"); }); test("core create: 422 birth_time_not_usable for incomplete profile fields", async () => { const response = await resolveReportCreate(baseDeps({ profile: profileFixture({ latitude: null, longitude: null }), })); assert.equal(response.status, 422); assert.equal(response.body.code, "birth_time_not_usable"); }); test("core create: 403 when the session or chart profile is not owned", async () => { const response = await resolveReportCreate(baseDeps({ rawBody: { requestId: UUID_B, reportType: "personal_full", presentationMode: "default", themes: ["career"], sessionId: SESSION_ID, }, checkSessionOwned: async () => false, })); assert.equal(response.status, 403); assert.equal(response.body.code, "report_resource_forbidden"); }); test("core create: 403 when the feature is disabled", async () => { const response = await resolveReportCreate(baseDeps({ featureEnabled: false })); assert.equal(response.status, 403); assert.equal(response.body.code, "report_export_disabled"); }); test("core create: 409 request conflict for a different payload under the same requestId", async () => { const fingerprintA = computeRequestFingerprint({ reportType: "personal_full", presentationMode: "default", depth: "standard", themes: ["career", "marriage", "wealth", "timing"], sessionId: null, chartProfileId: null, }); const fingerprintB = computeRequestFingerprint({ reportType: "personal_full", presentationMode: "default", depth: "standard", themes: ["career"], sessionId: null, chartProfileId: null, }); assert.notEqual(fingerprintA, fingerprintB); const seeded = new MemoryPersistence([{ id: REPORT_ID, userId: UUID_A, sessionId: null, chartProfileId: null, requestId: UUID_B, requestFingerprint: fingerprintA, reportType: "personal_full", status: "ready", schemaVersion: "report_document.v1", presentationMode: "default", depth: "standard", requestedThemes: ["career", "marriage", "wealth", "timing"], reportDocument: { ok: true } as unknown as PersonalReportRecord["reportDocument"], calculationHash: "c".repeat(64), evidenceHash: "d".repeat(64), skillName: null, skillVersion: null, skillSourceCommit: null, skillSnapshotSha256: "a".repeat(64), failureCode: null, createdAt: "2026-08-06T00:00:00.000Z", updatedAt: "2026-08-06T00:00:00.000Z", completedAt: "2026-08-06T00:00:00.000Z", }]); const response = await resolveReportCreate(baseDeps({ rawBody: { requestId: UUID_B, reportType: "personal_full", presentationMode: "default", themes: ["career"] }, persistence: seeded, })); assert.equal(response.status, 409); assert.equal(response.body.code, "report_request_conflict"); }); test("core create: 200 replay for a ready row with the same fingerprint", async () => { const fingerprint = computeRequestFingerprint({ reportType: "personal_full", presentationMode: "default", depth: "standard", themes: ["career", "marriage", "wealth", "timing"], sessionId: null, chartProfileId: null, }); const seeded = new MemoryPersistence([{ id: REPORT_ID, userId: UUID_A, sessionId: null, chartProfileId: null, requestId: UUID_B, requestFingerprint: fingerprint, reportType: "personal_full", status: "ready", schemaVersion: "report_document.v1", presentationMode: "default", depth: "standard", requestedThemes: ["career", "marriage", "wealth", "timing"], reportDocument: { ok: true } as unknown as PersonalReportRecord["reportDocument"], calculationHash: "c".repeat(64), evidenceHash: "d".repeat(64), skillName: null, skillVersion: null, skillSourceCommit: null, skillSnapshotSha256: "a".repeat(64), failureCode: null, createdAt: "2026-08-06T00:00:00.000Z", updatedAt: "2026-08-06T00:00:00.000Z", completedAt: "2026-08-06T00:00:00.000Z", }]); const response = await resolveReportCreate(baseDeps({ persistence: seeded })); assert.equal(response.status, 200); assert.deepEqual(response.body.reportDocument, { ok: true }); }); test("core create: 409 when a generation is already in progress", async () => { const generating = new MemoryPersistence(); generating.rows.set("55555555-5555-4555-8555-555555555555", { id: "55555555-5555-4555-8555-555555555555", userId: UUID_A, sessionId: null, chartProfileId: null, requestId: "66666666-6666-4666-8666-666666666666", requestFingerprint: "e".repeat(64), reportType: "personal_full", status: "generating", schemaVersion: "report_document.v1", presentationMode: "default", depth: "standard", requestedThemes: [], reportDocument: null, calculationHash: null, evidenceHash: null, skillName: null, skillVersion: null, skillSourceCommit: null, skillSnapshotSha256: "a".repeat(64), failureCode: null, createdAt: "2026-08-06T00:00:00.000Z", updatedAt: "2026-08-06T00:00:00.000Z", completedAt: null, }); const response = await resolveReportCreate(baseDeps({ persistence: generating })); assert.equal(response.status, 409); assert.equal(response.body.code, "report_generation_in_progress"); }); test("core create: 429 at the daily limit", async () => { const response = await resolveReportCreate(baseDeps({ counts: { countGenerating: async () => 0, countCreatedToday: async () => 5 }, })); assert.equal(response.status, 429); assert.equal(response.body.code, "report_rate_limited"); }); test("core create: 502 model_unavailable when no model is configured", async () => { const persistence = new MemoryPersistence(); const response = await resolveReportCreate(baseDeps({ model: null, persistence, })); assert.equal(response.status, 502); assert.equal(response.body.code, "model_unavailable"); const row = persistence.rows.get(REPORT_ID); assert.equal(row?.status, "failed"); assert.equal(row?.failureCode, "model_unavailable"); }); test("core create: 502 calculation_unavailable when the workflow throws", async () => { const persistence = new MemoryPersistence(); const response = await resolveReportCreate(baseDeps({ persistence, runWorkflow: async () => { throw new Error("engine down"); }, })); assert.equal(response.status, 502); assert.equal(response.body.code, "calculation_unavailable"); assert.equal(persistence.rows.get(REPORT_ID)?.status, "failed"); }); test("core create: 502 when the workflow returns no usable chart", async () => { const response = await resolveReportCreate(baseDeps({ runWorkflow: async () => ({ success: false }), })); assert.equal(response.status, 502); assert.equal(response.body.code, "calculation_unavailable"); }); test("core create: 422 when the real evidence cannot support a report (fail closed)", async () => { const persistence = new MemoryPersistence(); const workflow = chartPayload() as Record; const chart = workflow.chart as Record; chart.houses = (chart.houses as unknown[]).slice(0, 6); const response = await resolveReportCreate(baseDeps({ persistence, runWorkflow: async () => workflow, })); assert.equal(response.status, 422); assert.equal(response.body.code, "calculation_unavailable"); assert.equal(persistence.rows.get(REPORT_ID)?.status, "failed"); }); test("core create: 201 ready with a document on the happy path", async () => { const persistence = new MemoryPersistence(); const response = await resolveReportCreate(baseDeps({ persistence })); assert.equal(response.status, 201); assert.ok(response.body.reportDocument); const row = persistence.rows.get(REPORT_ID); assert.equal(row?.status, "ready"); assert.ok(row?.reportDocument); assert.equal(row?.skillName, SKILL_SNAPSHOT.name); assert.equal(row?.skillVersion, SKILL_SNAPSHOT.version); assert.equal(row?.skillSnapshotSha256, SKILL_SNAPSHOT.sha256); assert.equal(row?.skillSourceCommit, SKILL_SNAPSHOT.sourceCommit); assert.equal(row?.reportDocument?.provenance.skillName, SKILL_SNAPSHOT.name); assert.equal(row?.reportDocument?.provenance.skillVersion, SKILL_SNAPSHOT.version); assert.equal(row?.reportDocument?.provenance.skillSnapshotSha256, SKILL_SNAPSHOT.sha256); assert.equal(row?.reportDocument?.provenance.skillSourceCommit, SKILL_SNAPSHOT.sourceCommit); }); test("core create: full report runs every requested theme and sends only bundle v2 to the agent", async () => { const requestedThemes = ["career", "marriage", "wealth", "timing"]; const workflowThemes: string[] = []; const capture: { input: unknown } = { input: null }; const response = await resolveReportCreate(baseDeps({ rawBody: { requestId: UUID_B, reportType: "personal_full", presentationMode: "default", themes: requestedThemes, }, runWorkflow: async (input) => { workflowThemes.push(input.theme); const workflow = chartPayload() as Record; const consumer = workflow.consumer_context as Record; consumer.route = input.theme; return workflow; }, createAgent: () => capturingAgent(capture), })); assert.equal(response.status, 201); assert.deepEqual(workflowThemes, requestedThemes); const bundle = capture.input as ReportEvidenceBundleV2; assert.equal(bundle.schemaVersion, "report_evidence_bundle.v2"); const coveredThemes = new Set([ ...bundle.claimCards.map((card) => card.theme), ...bundle.blockedSections.map((section) => section.theme), ]); for (const theme of requestedThemes) { assert.equal(coveredThemes.has(theme), true, `${theme} must have a claim card or blocked section`); } }); test("core create: accepted partial wealth evidence stays ready, blocks D2/D11 and leaks no raw internals", async () => { const capture: { input: unknown } = { input: null }; const persistence = new MemoryPersistence(); const response = await resolveReportCreate(baseDeps({ persistence, profile: profileFixture({ birth_time_status: "accepted" }), rawBody: { requestId: UUID_B, reportType: "personal_thematic", presentationMode: "default", themes: ["wealth"], }, runWorkflow: async (input) => { assert.equal(input.theme, "wealth"); const workflow = chartPayload() as Record; workflow.api_key = "sk-private-report-secret"; workflow.authorization = "Bearer private-token"; workflow.cookie = "session=private-cookie"; workflow.database_url = "postgres://private-db"; workflow.internal_path = "/Users/private/project/engine.py"; workflow.runtime_path = "/opt/jyotisha-production/private.py"; workflow.latitude = 39.9; workflow.longitude = 116.4; workflow.prompt = "raw hidden prompt"; workflow.traceback = "Traceback: raw tool failure"; workflow.calculation_profile = { ayanamsa: "AKIAIOSFODNN7EXAMPLE", node_mode: "prod-db-01", house_system: "internal-host-22", }; const chart = workflow.chart as Record; const dasha = chart.dasha as Record; (dasha.mahadashas as Record[]).push({ lord: "Moon", start: "39.9000", end: "116.4000", }); const consumer = workflow.consumer_context as Record; consumer.route = "wealth"; consumer.available_layers = ["D1", "Vimshottari"]; consumer.missing_route_layers = ["D2", "D11"]; consumer.hard_blockers = []; const answerPolicy = consumer.answer_policy as Record; answerPolicy.deterministic_claims_forbidden_for = ["timing", "prod-db-01"]; const machine = workflow.machine_evidence_packet as Record; machine.conflicts = [{ techniques: ["D1"], summary: "Bearer opaque-conflict at /Users/private/trace.py", }]; const sections = machine.sections as Record; sections.AKIAIOSFODNN7EXAMPLE = { status: "used", source_path: "modules.internal" }; return workflow; }, createAgent: () => capturingAgent(capture), })); assert.equal(response.status, 201); assert.equal(persistence.rows.get(REPORT_ID)?.status, "ready"); const bundle = capture.input as ReportEvidenceBundleV2; assert.equal(bundle.schemaVersion, "report_evidence_bundle.v2"); assert.equal(bundle.subject.birthTimeStatus, "accepted"); assert.equal(bundle.answerPolicy.birthTimePolicy, "accepted_directional_only"); assert.equal("candidateRange" in bundle, false); assert.ok(bundle.blockedSections.some((section) => section.theme === "wealth")); for (const technique of ["D2", "D11"]) { const receipt = bundle.executionLedger.find((entry) => entry.technique.toUpperCase() === technique); assert.ok(receipt, `${technique} must have an explicit non-execution receipt`); assert.equal(receipt.executed, false); assert.notEqual(receipt.status, "verified"); } const serialized = JSON.stringify(bundle); for (const forbidden of [ "sk-private-report-secret", "Bearer private-token", "private-cookie", "postgres://private-db", "/Users/private", "/opt/jyotisha-production", "39.9", "116.4", "raw hidden prompt", "Traceback: raw tool failure", "AKIAIOSFODNN7EXAMPLE", "prod-db-01", "internal-host-22", "opaque-conflict", ]) { assert.equal(serialized.includes(forbidden), false, `bundle leaked ${forbidden}`); } }); test("core create: durable queue returns 202 without running generation inline", async () => { const persistence = new MemoryPersistence(); let enqueueCalls = 0; let workflowCalls = 0; const response = await resolveReportCreate(baseDeps({ persistence, runWorkflow: async () => { workflowCalls += 1; return chartPayload(); }, jobs: { async enqueue(input) { enqueueCalls += 1; return { kind: "created" as const, job: { id: "55555555-5555-4555-8555-555555555555", userId: input.userId, requestId: input.requestId, requestFingerprint: input.requestFingerprint, status: "queued" as const, attemptCount: 0, maxAttempts: 3, leaseToken: null, leaseOwner: null, leaseAcquiredAt: null, leaseExpiresAt: null, heartbeatAt: null, nextAttemptAt: null, progressPhase: "queued", progressPercent: 0, lastErrorCode: null, lastErrorAt: null, startedAt: null, finishedAt: null, createdAt: "2026-08-06T00:00:00.000Z", updatedAt: "2026-08-06T00:00:00.000Z", }, }; }, }, })); assert.equal(response.status, 202); assert.equal(response.body.jobId, "55555555-5555-4555-8555-555555555555"); assert.equal((response.body.report as { status?: string }).status, "generating"); assert.equal(persistence.rows.get(REPORT_ID)?.status, "generating"); assert.equal(enqueueCalls, 1); assert.equal(workflowCalls, 0); }); test("core create: 422 report_guard_rejected when the agent output violates the guard", async () => { const persistence = new MemoryPersistence(); const violatingAgent: ReportAgentPort = { modelId: "test-model", async generate(bundle, plan): Promise { const output = agentOutputForBundle(bundle, plan); return { ...output, executiveSummary: { ...output.executiveSummary, summary: "你一定会患上心脏病。", }, thematicNarrative: output.thematicNarrative.map((section, index) => index === 0 ? { ...section, narrative: "你一定会患上心脏病。" } : section), }; }, }; const response = await resolveReportCreate(baseDeps({ persistence, createAgent: () => violatingAgent, })); assert.equal(response.status, 422); assert.equal(response.body.code, "report_guard_rejected"); assert.equal(persistence.rows.get(REPORT_ID)?.status, "failed"); }); test("core read: 401 without user, 404 for non-owned or missing reports", async () => { const persistence = new MemoryPersistence(); const readyRow = await createReadyRow(persistence); const unauthenticated = await resolveReportRead({ requestUrl: "https://jyotisha.chat/api/reports/x", origin: null, allowedOrigins: [], userId: null, reportId: REPORT_ID, persistence, validateReadyDocument: acceptAnyDocument, }); assert.equal(unauthenticated.status, 401); const otherUser = await resolveReportRead({ requestUrl: "https://jyotisha.chat/api/reports/x", origin: null, allowedOrigins: [], userId: UUID_B, reportId: REPORT_ID, persistence, validateReadyDocument: acceptAnyDocument, }); assert.equal(otherUser.status, 404); const missing = await resolveReportRead({ requestUrl: "https://jyotisha.chat/api/reports/x", origin: null, allowedOrigins: [], userId: UUID_A, reportId: "99999999-9999-4999-8999-999999999999", persistence, validateReadyDocument: acceptAnyDocument, }); assert.equal(missing.status, 404); assert.equal(missing.body.code, "report_not_found"); assert.equal(readyRow, true); }); test("core read: ready returns the document, generating returns status only", async () => { const persistence = new MemoryPersistence(); const row = await createReadyRow(persistence); const ready = await resolveReportRead({ requestUrl: "https://jyotisha.chat/api/reports/x", origin: null, allowedOrigins: [], userId: UUID_A, reportId: REPORT_ID, persistence, validateReadyDocument: acceptAnyDocument, }); assert.equal(ready.status, 200); assert.ok(ready.body.reportDocument); assert.equal(row, true); const generatingPersistence = new MemoryPersistence(); generatingPersistence.rows.set(REPORT_ID, { ...seedRecord(), status: "generating", reportDocument: null, completedAt: null, }); const generating = await resolveReportRead({ requestUrl: "https://jyotisha.chat/api/reports/x", origin: null, allowedOrigins: [], userId: UUID_A, reportId: REPORT_ID, persistence: generatingPersistence, validateReadyDocument: acceptAnyDocument, }); assert.equal(generating.status, 200); assert.equal("reportDocument" in generating.body, false); assert.equal((generating.body.report as { status: string }).status, "generating"); }); test("core read: rejects a polluted stored ready document via canonical re-validation", async () => { const persistence = new MemoryPersistence(); persistence.rows.set(REPORT_ID, { ...seedRecord(), reportDocument: { hacked: true } as unknown as PersonalReportRecord["reportDocument"], }); const response = await resolveReportRead({ requestUrl: "https://jyotisha.chat/api/reports/x", origin: null, allowedOrigins: [], userId: UUID_A, reportId: REPORT_ID, persistence, validateReadyDocument: (document) => { const parsed = safeParseServerReportDocument(document); return parsed.ok ? { ok: true, document: parsed.document } : { ok: false }; }, }); assert.equal(response.status, 422); assert.equal(response.body.code, "report_schema_invalid"); assert.equal("reportDocument" in response.body, false); assert.equal((response.body.report as { status: string }).status, "ready"); }); test("core read: returns a legitimate ready document after canonical re-validation", async () => { const persistence = new MemoryPersistence(); await createReadyRow(persistence); const response = await resolveReportRead({ requestUrl: "https://jyotisha.chat/api/reports/x", origin: null, allowedOrigins: [], userId: UUID_A, reportId: REPORT_ID, persistence, validateReadyDocument: (document) => { const parsed = safeParseServerReportDocument(document); return parsed.ok ? { ok: true, document: parsed.document } : { ok: false }; }, }); assert.equal(response.status, 200); assert.ok(response.body.reportDocument); }); test("core delete: owner-only, 200 ok for the owner and 404 otherwise", async () => { const persistence = new MemoryPersistence(); await createReadyRow(persistence); const otherUser = await resolveReportDelete({ requestUrl: "https://jyotisha.chat/api/reports/x", origin: null, allowedOrigins: [], userId: UUID_B, reportId: REPORT_ID, persistence, }); assert.equal(otherUser.status, 404); const owner = await resolveReportDelete({ requestUrl: "https://jyotisha.chat/api/reports/x", origin: null, allowedOrigins: [], userId: UUID_A, reportId: REPORT_ID, persistence, }); assert.equal(owner.status, 200); assert.deepEqual(owner.body, { ok: true }); assert.equal(persistence.rows.has(REPORT_ID), false); }); function acceptAnyDocument(document: unknown): { ok: true; document: unknown } | { ok: false } { return { ok: true, document }; } async function createReadyRow(persistence: MemoryPersistence): Promise { const fingerprint = computeRequestFingerprint({ reportType: "personal_full", presentationMode: "default", depth: "standard", themes: ["career", "marriage", "wealth", "timing"], sessionId: null, chartProfileId: null, }); const response = await resolveReportCreate(baseDeps({ persistence })); if (response.status !== 201) return false; const row = persistence.rows.get(REPORT_ID); assert.equal(row?.requestFingerprint, fingerprint); return true; } function seedRecord(): PersonalReportRecord { return { id: REPORT_ID, userId: UUID_A, sessionId: null, chartProfileId: null, requestId: UUID_B, requestFingerprint: "f".repeat(64), reportType: "personal_full", status: "ready", schemaVersion: "report_document.v1", presentationMode: "default", depth: "standard", requestedThemes: ["career", "marriage", "wealth", "timing"], reportDocument: { ok: true } as unknown as PersonalReportRecord["reportDocument"], calculationHash: "c".repeat(64), evidenceHash: "d".repeat(64), skillName: null, skillVersion: null, skillSourceCommit: null, skillSnapshotSha256: "a".repeat(64), failureCode: null, createdAt: "2026-08-06T00:00:00.000Z", updatedAt: "2026-08-06T00:00:00.000Z", completedAt: "2026-08-06T00:00:00.000Z", }; } // --------------------------------------------------------------------------- // Source-level production wiring checks // --------------------------------------------------------------------------- test("POST route uses dual clients: authenticated reads + admin persistence", () => { assert.match(createRoute, /createServerSupabaseClient\(\)/); assert.match(createRoute, /\.from\("profiles"\)/); assert.match(createRoute, /createAdminSupabaseClient\(\)/); assert.match(createRoute, /createSupabasePersonalReportService\(admin\)/); assert.match(createRoute, /createPersonalReportDataClient\(admin\)/); assert.doesNotMatch(createRoute, /resolveReportPersistencePort|resolveReportContractPort/); assert.doesNotMatch(createRoute, /ReportPersistenceUnavailableError|ReportContractUnavailableError/); assert.doesNotMatch(createRoute, /not wired yet|尚未就绪/); }); test("GET/DELETE use the authenticated client (least privilege) and the core handlers", () => { assert.match(itemRoute, /createServerSupabaseClient\(\)/); assert.match(itemRoute, /createSupabasePersonalReportService\(supabase\)/); assert.match(itemRoute, /resolveReportRead/); assert.match(itemRoute, /resolveReportDelete/); assert.doesNotMatch(itemRoute, /createAdminSupabaseClient/); }); test("route core enforces same-origin and never leaks raw exception text", () => { assert.match(coreSource, /checkSameOrigin/); assert.match(coreSource, /REPORT_STABLE_CODES\.resourceForbidden/); assert.match(createRoute, /requestHeaders:\s*request\.headers/); assert.match(itemRoute, /requestHeaders:\s*request\.headers/); assert.doesNotMatch(createRoute, /error\.message\)/); assert.doesNotMatch(itemRoute, /error\.message\)/); assert.doesNotMatch(createRoute, /\.stack/); assert.doesNotMatch(itemRoute, /\.stack/); }); test("POST route reads the daily limit from env and resolves a real skill snapshot", () => { assert.match(createRoute, /readPersonalReportDailyLimit\(process\.env\)/); assert.match(createRoute, /resolveSkillSnapshot\(\)/); assert.doesNotMatch(createRoute, /每日.*上限.*\d|PERSONAL_REPORT_DAILY_LIMIT.*\?\?\s*["']\d/); }); test("GET route re-validates stored ready documents through the canonical server parse", () => { assert.match(itemRoute, /safeParseServerReportDocument\(document\)/); assert.match(itemRoute, /validateReadyDocument/); assert.match(coreSource, /validateReadyDocument/); assert.match(coreSource, /canonical server parse/); assert.match(coreSource, /REPORT_STABLE_CODES\.schemaInvalid/); assert.doesNotMatch(coreSource, /client.*validation|validate.*client/i); }); test("POST route never generates HTML/PDF/base64 or local paths", () => { assert.doesNotMatch(createRoute, /window\.print|html2canvas|jsPDF|base64|\.pdf/); assert.doesNotMatch(createRoute, /sendFile|createWriteStream|\/opt\/|\/var\/|\/Users\//); }); test("POST enqueues durable work without Next.js after and GET lists metadata without report bodies", () => { assert.match(createRoute, /createSupabasePersonalReportJobService/); assert.match(createRoute, /jobs:\s*createSupabasePersonalReportJobService\(admin\)/); assert.doesNotMatch(createRoute, /\bafter\s*\(/); assert.doesNotMatch(createRoute, /deferGeneration/); assert.match(createRoute, /export async function GET\(\)/); assert.match(createRoute, /REPORT_LIST_COLUMNS/); const listColumns = createRoute.slice( createRoute.indexOf("const REPORT_LIST_COLUMNS"), createRoute.indexOf("function sanitizedErrorCode"), ); assert.doesNotMatch(listColumns, /report_document|calculation_hash|evidence_hash/); assert.doesNotMatch(createRoute, /STALE_GENERATION_MS|staleBefore/); }); test("stable error codes live in the dependency-free codes module", () => { for (const code of [ "profile_incomplete", "birth_time_not_usable", "report_generation_in_progress", "report_rate_limited", "calculation_unavailable", "model_unavailable", "report_schema_invalid", "report_guard_rejected", "report_not_found", "report_request_conflict", ]) { assert.ok(codesSource.includes(`"${code}"`), `missing stable code ${code}`); } // The codes module must stay free of heavy imports so route handlers that // only need codes never trace the generation/skill-snapshot logic. assert.doesNotMatch(codesSource, /node:fs|node:path|node:crypto|readdirSync|readFileSync/); assert.doesNotMatch(createRoute, /dangerouslySetInnerHTML/); }); test("GET route imports codes from the pure module, never the generation module", () => { assert.match(itemRoute, /personal-report-codes/); assert.doesNotMatch(itemRoute, /personal-report-generation/); assert.doesNotMatch(itemRoute, /resolveSkillSnapshot|buildReportEvidencePacket|canonicalSerialize/); assert.match(generationSource, /personal-report-codes/); }); test("generation pipeline re-validates with the canonical server parse after the guard", () => { assert.match(generationSource, /safeParseServerReportDocument\(guarded\.document\)/); assert.match(generationSource, /assembleReportDocument/); }); test("evidence hash is the canonical appendix hash, never a model self-report", () => { assert.match(generationSource, /computeEvidenceHash\(parsed\.document\.evidenceAppendix\)/); assert.match(generationSource, /computeEvidenceHash\(appendix\)/); }); test("skill snapshot resolution is registry-backed and fails closed", () => { assert.match(generationSource, /SkillSnapshotUnavailableError/); assert.match(generationSource, /resolveActiveSkillPackage/); assert.doesNotMatch(generationSource, /source-manifest\.json/); assert.doesNotMatch(generationSource, /skill_snapshot_unavailable.*digest/); }); test("generation delegates package verification to the immutable registry", () => { assert.doesNotMatch(generationSource, /node:fs|node:path|readdirSync|readFileSync/); assert.doesNotMatch(generationSource, /\bskillDirectory\b|\brepoRoot\b|turbopackIgnore/); assert.match(generationSource, /resolveActiveSkillPackage\("jyotish-personal-report"\)/); });