101 lines
6.5 KiB
TypeScript
101 lines
6.5 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
import { completeUsage } from "../src/lib/consultation-billing.ts";
|
|
|
|
const root = new URL("../", import.meta.url);
|
|
const rectificationRoute = readFileSync(new URL("src/app/api/rectification/agent/route.ts", root), "utf8");
|
|
const consultRoute = readFileSync(new URL("src/app/api/consult/route.ts", root), "utf8");
|
|
const packagesRoute = readFileSync(new URL("src/app/api/admin/packages/route.ts", root), "utf8");
|
|
|
|
function sourceBetween(source: string, start: string, end: string): string {
|
|
const startIndex = source.indexOf(start);
|
|
const endIndex = source.indexOf(end, startIndex + start.length);
|
|
assert.ok(startIndex >= 0, `missing start marker: ${start}`);
|
|
assert.ok(endIndex > startIndex, `missing end marker: ${end}`);
|
|
return source.slice(startIndex, endIndex);
|
|
}
|
|
|
|
test("Agentic rectification reuses one case-level usage authorization and the session-pinned model version", () => {
|
|
assert.match(rectificationRoute, /select\("id,messages,session_type,model_id,model_config_version,agentic_rectification_case_id"\)/);
|
|
assert.match(rectificationRoute, /resolveSessionLanguageModel\(\s*chatSession\.model_id,\s*chatSession\.model_config_version,?\s*\)/);
|
|
assert.match(rectificationRoute, /modelConfigVersion: selectedModel\.configVersion/);
|
|
assert.doesNotMatch(rectificationRoute, /loadLanguageModelCatalog|resolveLanguageModelFromCatalog|\bresolveLanguageModel\(|\bdefaultLanguageModel\(/);
|
|
assert.match(rectificationRoute, /const billingRequestPrefix = `rectification:case:\$\{caseId\}`/);
|
|
assert.match(rectificationRoute, /from\("usage_reservations"\)[\s\S]*\.eq\("feature_key", "rectification"\)[\s\S]*\.like\("request_id", `\$\{billingRequestPrefix\}%`\)/);
|
|
assert.match(rectificationRoute, /return reservations.length === 0[\s\S]*`\$\{billingRequestPrefix\}:retry:\$\{reservations.length\}`/);
|
|
assert.match(rectificationRoute, /authorizeUsage\(accounting, \{[\s\S]*requestId: billingRequestId/);
|
|
assert.match(rectificationRoute, /completeUsage\(accounting, userId, billingRequestId,/);
|
|
assert.match(rectificationRoute, /releaseUsage\(accounting, userId, billingRequestId,/);
|
|
});
|
|
|
|
test("free Agentic rectification turns bypass reservation, completion, and cancellation settlement", () => {
|
|
const reserveBilling = sourceBetween(rectificationRoute, "async reserve() {", "async complete(usage) {");
|
|
const completeBilling = sourceBetween(rectificationRoute, "async complete(usage) {", "async release() {");
|
|
const releaseBilling = sourceBetween(rectificationRoute, "async release() {", " };\n\n try {");
|
|
|
|
assert.match(rectificationRoute, /action: z\.enum\(\["opening", "message", "read_only"\]\)/);
|
|
assert.equal(rectificationRoute.match(/if \(action !== "message"\)/g)?.length, 3);
|
|
assert.match(reserveBilling, /if \(action !== "message"\) \{[\s\S]*return \{ success: true, status: 200 \};/);
|
|
assert.match(reserveBilling, /authorizeUsage\(/);
|
|
assert.match(completeBilling, /if \(action !== "message"\) return true;/);
|
|
assert.match(completeBilling, /completeUsage\(/);
|
|
assert.match(releaseBilling, /if \(action !== "message"\) return true;/);
|
|
assert.match(releaseBilling, /releaseUsage\(/);
|
|
});
|
|
|
|
|
|
test("standard consultation resolves and settles the session-pinned model version", () => {
|
|
assert.match(consultRoute, /sessionId: z\.string\(\)\.uuid\(\)/);
|
|
assert.match(consultRoute, /select\("id,model_id,model_config_version,session_type"\)/);
|
|
assert.match(consultRoute, /resolveSessionLanguageModel\(\s*chatSession\.model_id,\s*chatSession\.model_config_version,?\s*\)/);
|
|
assert.match(consultRoute, /actualModelId: selectedModel\.id/);
|
|
assert.match(consultRoute, /modelConfigVersion: selectedModel\.configVersion/);
|
|
});
|
|
|
|
test("standard consultation awaits real usage before durable response settlement", () => {
|
|
assert.doesNotMatch(consultRoute, /recordActualUsage|void usage\.then/);
|
|
assert.doesNotMatch(consultRoute, /inputTokens: 0,[\s\S]*outputTokens: 0,[\s\S]*costMicrousd: 0/);
|
|
assert.match(consultRoute, /async function usagePayload\(usage: Promise<\{ inputTokens\?: number; outputTokens\?: number \}>\)/);
|
|
assert.match(consultRoute, /const resolved = await usage;/);
|
|
assert.match(consultRoute, /const actualUsage = await usagePayload\(usage\);[\s\S]*p_actual_usage: actualUsage/);
|
|
assert.match(consultRoute, /function mergeUsage\(usages: Promise<Usage>\[\]\): Promise<Usage> \{[\s\S]*Promise\.all\(usages\)/);
|
|
assert.equal(consultRoute.match(/usages\.push\(result\.totalUsage\)/g)?.length, 2);
|
|
assert.equal(consultRoute.match(/usages\.push\(retried\.totalUsage\)/g)?.length, 2);
|
|
});
|
|
|
|
test("standard consultation forwards its stable reservation request as the usage event key", async () => {
|
|
const eventKey = "00000000-0000-4000-8000-000000000002";
|
|
const calls: Record<string, unknown>[] = [];
|
|
const accounting = {
|
|
async rpc(_rpcName: string, args: Record<string, unknown>) {
|
|
calls.push(args);
|
|
return {
|
|
data: { success: true, reservation_id: "00000000-0000-4000-8000-000000000003", credits: 9, error_code: null },
|
|
error: null,
|
|
};
|
|
},
|
|
};
|
|
const usage = { eventKey, actualModelId: "model", inputTokens: 1, outputTokens: 2, costMicrousd: 3, durationMs: 4 };
|
|
|
|
await completeUsage(accounting, "00000000-0000-4000-8000-000000000001", eventKey, usage);
|
|
await completeUsage(accounting, "00000000-0000-4000-8000-000000000001", eventKey, usage);
|
|
|
|
assert.deepEqual(calls.map((call) => (call.p_actual_usage as { eventKey: string }).eventKey), [eventKey, eventKey]);
|
|
assert.match(consultRoute, /eventKey: requestId,/);
|
|
assert.match(consultRoute, /const actualUsage = await usagePayload\(usage\);[\s\S]*p_actual_usage: actualUsage/);
|
|
assert.doesNotMatch(consultRoute, /eventKey:\s*(?:globalThis\.)?crypto\.randomUUID\(\)/);
|
|
});
|
|
|
|
test("legacy admin packages cannot mutate detached payment_packages", () => {
|
|
assert.doesNotMatch(packagesRoute, /payment_packages|queryAdminRows|insert into|update public/);
|
|
assert.match(packagesRoute, /export function GET\(request: Request\)/);
|
|
assert.match(packagesRoute, /const replacement = "\/api\/admin\/products"/);
|
|
assert.match(packagesRoute, /NextResponse\.redirect\(new URL\(replacement, request\.url\), 308\)/);
|
|
for (const method of ["POST", "PATCH", "DELETE"]) {
|
|
assert.match(packagesRoute, new RegExp(`export function ${method}\\(\\)`));
|
|
}
|
|
assert.match(packagesRoute, /status: 410/);
|
|
assert.match(packagesRoute, /error: "旧套餐写接口已停用,请使用统一商品管理。",[\s\S]*replacement,/);
|
|
});
|