Files
Jyotisha/frontend/tests/synastry-full-create-route.test.ts
T
jesse-uxGrok (agent loop)Cursor
57ee79cc5f fix(synastry): let the same pair start another report
A finished match used to reopen the old report. Starting again now writes a new one and charges again; a report still being written is reused.

Co-Authored-By: Grok (agent loop)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-21 23:08:21 +08:00

159 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import test from "node:test";
const frontendRoot = fileURLToPath(new URL("../", import.meta.url));
const otherId = "11111111-1111-4111-8111-111111111111";
const selfRow = {
name: "虚构甲",
birth_date: "1991-04-07",
reported_birth_time: "09:15:00",
birth_time_status: "reported",
latitude: 31.2,
longitude: 121.4,
timezone_offset: 8,
timezone_id: "Asia/Shanghai",
ayanamsa: "lahiri",
};
function runRoute(script: string): { status: number; body: Record<string, unknown>; charged: boolean } {
const result = spawnSync(process.execPath, [
"--experimental-test-module-mocks",
"--import",
"tsx",
"--input-type=module",
"--eval",
script,
], { cwd: frontendRoot, encoding: "utf8" });
assert.equal(result.status, 0, result.stderr || result.stdout);
const line = result.stdout.trim().split("\n").at(-1) ?? "{}";
return JSON.parse(line) as { status: number; body: Record<string, unknown>; charged: boolean };
}
function script(input: { product: boolean; same: boolean; missingOther: boolean; existing: "none" | "generating" }): string {
return String.raw`
import { mock } from "node:test";
import { pathToFileURL } from "node:url";
const moduleUrl = (path) => pathToFileURL(process.cwd() + "/" + path).href;
const selfRow = ${JSON.stringify(selfRow)};
const otherId = ${JSON.stringify(otherId)};
let charged = false;
mock.module("server-only", { namedExports: {} });
mock.module("@/mastra", { namedExports: { runConsultationWorkflow() { throw new Error("mastra"); } } });
mock.module("@/mastra/personal-report", { namedExports: {
PersonalReportAgentOutputError: class PersonalReportAgentOutputError extends Error {},
createPersonalReportAgent() { throw new Error("agent"); },
} });
mock.module("@/lib/product-access", { namedExports: { isProductEnabled: async () => ${input.product ? "true" : "false"} } });
mock.module("@/lib/consultation-billing", { namedExports: {
authorizeUsage() { charged = true; return { success: true }; },
completeUsage() { charged = true; },
releaseUsage() {},
} });
mock.module("@/lib/personal-report-service", { namedExports: {
createSupabasePersonalReportService: () => ({ getOwnedById: async () => null }),
createPersonalReportDataClient: () => ({ from: () => ({}) }),
} });
function query(table) {
const filters = [];
const q = {
select() { return q; },
eq(column, value) { filters.push([column, value]); return q; },
in(column, values) { filters.push([column, values]); return q; },
gte() { return q; },
is() { return q; },
or() { return q; },
order() { return q; },
limit() { return q; },
maybeSingle() {
if (table === "profiles") return Promise.resolve({ data: selfRow, error: null });
if (table === "chart_profiles") {
return Promise.resolve({ data: ${input.missingOther ? "null" : "{ ...selfRow, id: otherId, user_id: 'user-1', role: 'other' }"}, error: null });
}
if (table === "personal_reports") {
const wantsGenerating = filters.some(([column, value]) => column === "status" && (value === "generating" || (Array.isArray(value) && value.includes("generating"))));
if (${input.existing === "generating" ? "true" : "false"} && wantsGenerating) {
return Promise.resolve({ data: { id: "gen-1", request_id: "req-1", status: "generating", created_at: "2026-10-19T00:00:00Z" }, error: null });
}
return Promise.resolve({ data: null, error: null });
}
return Promise.resolve({ data: null, error: null });
},
then(resolve, reject) { return Promise.resolve({ data: [], error: null }).then(resolve, reject); },
};
return q;
}
const client = { auth: { getUser: async () => ({ data: { user: { id: "user-1" } }, error: null }) }, from: (table) => query(table) };
mock.module("@/lib/supabase/server", { namedExports: { createServerSupabaseClient: async () => client } });
mock.module("@/lib/supabase/admin", { namedExports: { createAdminSupabaseClient: () => client } });
const reports = await import(moduleUrl("src/app/api/reports/route.ts"));
const response = await reports.POST(new Request("https://staging.jyotisha.chat/api/reports", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
reportType: "synastry_full",
subjectA: "self",
subjectB: ${input.same ? '"self"' : "otherId"},
}),
}));
console.log(JSON.stringify({ status: response.status, body: await response.json(), charged }));
`;
}
test("the same person is rejected before billing", () => {
const result = runRoute(script({ product: true, same: true, missingOther: false, existing: "none" }));
assert.equal(result.status, 400);
assert.equal(result.body.code, "same_subject");
assert.equal(result.charged, false);
});
test("someone else's id is rejected before billing", () => {
const result = runRoute(script({ product: true, same: false, missingOther: true, existing: "none" }));
assert.equal(result.status, 409);
assert.equal(result.charged, false);
assert.match(JSON.stringify(result.body), /不会扣点/);
});
test("a generating pair is reused without a new reservation", () => {
// 原值:已有 ready 报告时 status 200、reused、不扣点
// 新值:只有 generating 复用并返回 202;ready 不再走这条
// 原因:K6 允许同一对人重新生成(BUG-1408)
const result = runRoute(script({ product: true, same: false, missingOther: false, existing: "generating" }));
assert.equal(result.status, 202);
assert.equal(result.body.reused, true);
assert.equal(result.charged, false);
});
test("a closed product switch rejects the report before billing", () => {
const result = runRoute(script({ product: false, same: false, missingOther: true, existing: "none" }));
assert.equal(result.status, 503);
assert.equal(result.body.code, "compatibility_product_disabled");
assert.equal(result.charged, false);
});
test("the english route stays closed when the product switch is off", () => {
const scriptText = String.raw`
import { mock } from "node:test";
import { pathToFileURL } from "node:url";
const moduleUrl = (path) => pathToFileURL(process.cwd() + "/" + path).href;
mock.module("server-only", { namedExports: {} });
mock.module("@/lib/product-access", { namedExports: { isProductEnabled: async () => false } });
mock.module("@/lib/supabase/server", { namedExports: {
createServerSupabaseClient: async () => ({ auth: { getUser: async () => ({ data: { user: { id: "user-1" } } }) } }),
} });
mock.module("@/lib/supabase/admin", { namedExports: { createAdminSupabaseClient: () => ({}) } });
mock.module("@/lib/personal-report-service", { namedExports: {
createSupabasePersonalReportService: () => ({ getOwnedById: async () => null }),
} });
const route = await import(moduleUrl("src/app/api/reports/[reportId]/english/route.ts"));
const response = await route.POST(new Request("https://staging.jyotisha.chat/api/reports/11111111-1111-4111-8111-111111111111/english", { method: "POST" }), {
params: Promise.resolve({ reportId: "11111111-1111-4111-8111-111111111111" }),
});
console.log(JSON.stringify({ status: response.status, body: await response.json(), charged: false }));
`;
const result = runRoute(scriptText);
assert.equal(result.status, 503);
assert.equal(result.body.code, "compatibility_product_disabled");
});