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>
159 lines
7.6 KiB
TypeScript
159 lines
7.6 KiB
TypeScript
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");
|
||
});
|