Files
Jyotisha/frontend/tests/personal-report-api.test.ts
T
Jesse_Chen 17430a1f21
Staging Backend Quality Gate / validate (push) Successful in 12m13s
Staging Backend Quality Gate / publish (push) Has been cancelled
fix(reports): trust verified proxy origin
2026-08-09 00:54:55 +08:00

839 lines
30 KiB
TypeScript

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 { 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 { 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(): PersonalReportAgentOutput {
return {
executiveSummary: {
headline: "综合盘面以事业发展为主线",
summary: "事业结构稳定,财富与婚恋需结合分盘审慎解读。",
priorities: ["先聚焦职业方向"],
},
thematicNarrative: [
{
id: "career",
title: "事业",
narrative: "事业层面以十宫与 D10 结构为主,方向性判断稳定。",
actions: ["在稳定领域深耕"],
caveats: [],
claimStatus: "single_system_inference",
evidenceRefs: ["ev-audit-2"],
},
],
};
}
const fakeAgent: ReportAgentPort = {
modelId: "test-model",
async generate() {
return agentOutput();
},
};
const SKILL_SNAPSHOT = { sha256: "a".repeat(64), sourceCommit: "b".repeat(40) };
function profileFixture(overrides: Record<string, unknown> = {}) {
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<string, PersonalReportRecord>();
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,
requestedThemes: input.requestedThemes ?? [],
reportDocument: null,
calculationHash: null,
evidenceHash: null,
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<CreateGeneratingResult> {
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> = {}): 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 for reported status", async () => {
const response = await resolveReportCreate(baseDeps({
profile: profileFixture({ birth_time_status: "reported", active_birth_time: "05:30:00" }),
}));
assert.equal(response.status, 422);
assert.equal(response.body.code, "birth_time_not_usable");
});
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",
themes: ["career", "marriage", "wealth", "timing"],
sessionId: null,
chartProfileId: null,
});
const fingerprintB = computeRequestFingerprint({
reportType: "personal_full",
presentationMode: "default",
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",
requestedThemes: ["career", "marriage", "wealth", "timing"],
reportDocument: { ok: true } as unknown as PersonalReportRecord["reportDocument"],
calculationHash: "c".repeat(64),
evidenceHash: "d".repeat(64),
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",
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",
requestedThemes: ["career", "marriage", "wealth", "timing"],
reportDocument: { ok: true } as unknown as PersonalReportRecord["reportDocument"],
calculationHash: "c".repeat(64),
evidenceHash: "d".repeat(64),
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",
requestedThemes: [],
reportDocument: null,
calculationHash: null,
evidenceHash: 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<string, unknown>;
const chart = workflow.chart as Record<string, unknown>;
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);
});
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(): Promise<PersonalReportAgentOutput> {
return {
executiveSummary: {
headline: "综合盘面",
summary: "结构稳定。",
priorities: [],
},
thematicNarrative: [{
id: "career",
title: "事业",
narrative: "你必定会胜诉。",
actions: [],
caveats: [],
claimStatus: "single_system_inference",
evidenceRefs: ["ev-audit-2"],
}],
};
},
};
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<boolean> {
const fingerprint = computeRequestFingerprint({
reportType: "personal_full",
presentationMode: "default",
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",
requestedThemes: ["career", "marriage", "wealth", "timing"],
reportDocument: { ok: true } as unknown as PersonalReportRecord["reportDocument"],
calculationHash: "c".repeat(64),
evidenceHash: "d".repeat(64),
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("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 fails closed without a real source", () => {
assert.match(generationSource, /SkillSnapshotUnavailableError/);
assert.match(generationSource, /source-manifest\.json/);
assert.doesNotMatch(generationSource, /skill_snapshot_unavailable.*digest/);
});
test("generation module has no filesystem/path scanning (static manifest import only)", () => {
assert.doesNotMatch(generationSource, /node:fs|node:path|readdirSync|readFileSync/);
assert.doesNotMatch(generationSource, /\bskillDirectory\b|\brepoRoot\b|turbopackIgnore/);
assert.match(generationSource, /source-manifest\.json/);
});