9958e00abc
Rectification stays optional. Reported minutes can consult and generate reports; date-plus-period uses a declared window instead of a midpoint or 00:00. Updates BUG-341. Co-authored-by: Cursor <cursoragent@cursor.com>
1130 lines
41 KiB
TypeScript
1130 lines
41 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 {
|
|
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> = {}): 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<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,
|
|
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<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 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<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);
|
|
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<string, unknown>;
|
|
const consumer = workflow.consumer_context as Record<string, unknown>;
|
|
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<string, unknown>;
|
|
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<string, unknown>;
|
|
const dasha = chart.dasha as Record<string, unknown>;
|
|
(dasha.mahadashas as Record<string, unknown>[]).push({
|
|
lord: "Moon",
|
|
start: "39.9000",
|
|
end: "116.4000",
|
|
});
|
|
const consumer = workflow.consumer_context as Record<string, unknown>;
|
|
consumer.route = "wealth";
|
|
consumer.available_layers = ["D1", "Vimshottari"];
|
|
consumer.missing_route_layers = ["D2", "D11"];
|
|
consumer.hard_blockers = [];
|
|
const answerPolicy = consumer.answer_policy as Record<string, unknown>;
|
|
answerPolicy.deterministic_claims_forbidden_for = ["timing", "prod-db-01"];
|
|
const machine = workflow.machine_evidence_packet as Record<string, unknown>;
|
|
machine.conflicts = [{
|
|
techniques: ["D1"],
|
|
summary: "Bearer opaque-conflict at /Users/private/trace.py",
|
|
}];
|
|
const sections = machine.sections as Record<string, unknown>;
|
|
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<PersonalReportAgentOutput> {
|
|
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<boolean> {
|
|
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"\)/);
|
|
});
|