merge: sync staging task sheet into cross-midnight gate fix
Preserve both the reviewed implementation and latest staging records. No production scoring changes beyond aa46da10.
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -90,7 +90,8 @@ 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;/);
|
||||
// 原值 await usage;新值等待咨询与分类 usage 合并;原因:分类成本也必须结算。
|
||||
assert.match(consultRoute, /const resolved = await mergeUsage\(\[usage, Promise\.resolve\(classificationUsage\)\]\);/);
|
||||
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\)/);
|
||||
// Former: three agentic first streams each had `usages.push(result.totalUsage)`.
|
||||
|
||||
@@ -70,7 +70,7 @@ test("shows honest agent activity states before and during streamed text", () =>
|
||||
assert.match(messageRowSource, /showActivity = message\.state !== "settled"/);
|
||||
assert.match(messageRowSource, /showThinkingPanel &&/);
|
||||
assert.match(messageRowSource, /showSpokenAnswer/);
|
||||
assert.match(messageRowSource, /<ChatMessageContent[\s\S]*text=\{message\.text\}[\s\S]*auditRows=\{message\.agentExecutionReceipt\?\.techniqueAuditTable\}/);
|
||||
assert.match(messageRowSource, /<ChatMessageContent[\s\S]*text=\{message\.text\}[\s\S]*auditRows=\{smalltalk \? undefined : message\.agentExecutionReceipt\?\.techniqueAuditTable\}/);
|
||||
assert.match(messageRowSource, /stackedThinkingAndAnswer/);
|
||||
assert.match(messageRowSource, /aria-label="回复"/);
|
||||
assert.match(messageRowSource, /className="message-stage-and-answer"/);
|
||||
|
||||
@@ -1015,7 +1015,8 @@ test("public stream filters private chunks and completes once", async () => {
|
||||
assert.equal(events.some((event) => event.type === "answer.delta"), true);
|
||||
for (const event of events) consultationAgentPublicEventSchema.parse(event);
|
||||
const completed = events.find((event) => event.type === "run.completed");
|
||||
assert.deepEqual(completed?.type === "run.completed" ? completed.receipt.workflow.domains : null, ["career"]);
|
||||
assert.ok(completed?.receipt, "a consultation completion still requires its execution receipt");
|
||||
assert.deepEqual(completed.receipt.workflow.domains, ["career"]);
|
||||
});
|
||||
|
||||
test("model answer text cannot forge a public Activity event", async () => {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import React from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { ChatMessageRow } from "../src/components/chat-message-row.tsx";
|
||||
import { settledChatMessageViews, streamingChatMessageView } from "../src/lib/chat-message-view.ts";
|
||||
|
||||
test("settled and streaming smalltalk render text without thinking, activity or evidence", () => {
|
||||
const settled = settledChatMessageViews([{ role: "assistant", text: "你好", responseKind: "smalltalk", thinkingText: "must stay hidden" }])[0]!;
|
||||
const streaming = streamingChatMessageView([{ role: "user", text: "你好" }], true, "你好", undefined, undefined, undefined, [], "smalltalk")!;
|
||||
for (const message of [settled, streaming]) {
|
||||
const html = renderToStaticMarkup(<ChatMessageRow message={message} />);
|
||||
assert.match(html, /你好/);
|
||||
assert.doesNotMatch(html, /consultation-thinking-report|agent-activity|must stay hidden|已完成|技法|正在分析/);
|
||||
}
|
||||
});
|
||||
test("classification wait does not invent a thinking step before any activity", () => {
|
||||
const message = streamingChatMessageView([{ role: "user", text: "你好" }], true, "", undefined, undefined, undefined, [])!;
|
||||
const html = renderToStaticMarkup(<ChatMessageRow message={message} />);
|
||||
assert.doesNotMatch(html, /consultation-thinking-report|正在处理|正在分析|已完成/);
|
||||
});
|
||||
@@ -0,0 +1,216 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { classifyConsultationTurn, consultationTurnSchema, smalltalkHistoryPair, type SmalltalkObservation } from "../src/lib/consultation-smalltalk.ts";
|
||||
import { streamSmalltalkResponse } from "../src/lib/stream-smalltalk-response.ts";
|
||||
import { consultationAgentPublicEventSchema } from "../src/lib/consultation-agent-events.ts";
|
||||
import { settledChatMessageViews, streamingChatMessageView } from "../src/lib/chat-message-view.ts";
|
||||
import { productConversationVoice, natalSpokenReportContract } from "../src/mastra/product-voice.ts";
|
||||
import type { ResolvedLanguageModel } from "../src/mastra/model.ts";
|
||||
import type { LanguageModelV2 } from "@ai-sdk/provider";
|
||||
|
||||
// Transport tests inject a generator; the adapter regression uses a local fake.
|
||||
// Neither resolves credentials nor calls a real provider.
|
||||
const model = { id: "fake-model", model: "openai/fake" } as ResolvedLanguageModel;
|
||||
const source = (path: string) => readFileSync(new URL(path, import.meta.url), "utf8");
|
||||
for (const question of ["你好", "在吗", "谢谢", "晚安", "哈哈", "我回来了"]) {
|
||||
test(`smalltalk fake transport: ${question}`, async () => {
|
||||
let calls = 0;
|
||||
assert.deepEqual(await classifyConsultationTurn({ model, question, history: [], generate: async () => {
|
||||
calls++; return { object: { kind: "smalltalk", reply: "在呢" } };
|
||||
} }), { kind: "smalltalk", reply: "在呢" });
|
||||
assert.equal(calls, 1);
|
||||
});
|
||||
}
|
||||
for (const question of ["你好,帮我看看事业", "?", "你在说什么鬼", "今年怎么样", ""]) {
|
||||
test(`consult fake transport: ${question || "blank"}`, async () => {
|
||||
assert.deepEqual(await classifyConsultationTurn({ model, question, history: [], generate: async () => ({ object: { kind: "consult" } }) }), { kind: "consult" });
|
||||
});
|
||||
}
|
||||
for (const object of [null, {}, { kind: "smalltalk", reply: "" }, { kind: "smalltalk", reply: "字".repeat(21) }, { kind: "smalltalk", reply: "你好。" }, { kind: "smalltalk", reply: "你好\n再见" }, { kind: "smalltalk", reply: "你好", extra: true }, { kind: "consult", reply: "你好" }]) {
|
||||
test(`strict schema fails open: ${JSON.stringify(object)}`, async () => {
|
||||
assert.equal(consultationTurnSchema.safeParse(object).success, false);
|
||||
assert.deepEqual(await classifyConsultationTurn({ model, question: "test", history: [], generate: async () => ({ object }) }), { kind: "consult" });
|
||||
});
|
||||
}
|
||||
test("default Mastra adapter uses one tool-free bounded model call and returns real usage", async () => {
|
||||
const calls: Parameters<LanguageModelV2["doGenerate"]>[0][] = [];
|
||||
const text = '{"kind":"smalltalk","reply":"你好"}';
|
||||
const usage = { inputTokens: 15, outputTokens: 10, totalTokens: 25 };
|
||||
const fake: LanguageModelV2 = {
|
||||
specificationVersion: "v2", provider: "fake", modelId: "fake", supportedUrls: {},
|
||||
async doGenerate(options) {
|
||||
calls.push(options);
|
||||
return { content: [{ type: "text", text }], finishReason: "stop", usage, warnings: [] };
|
||||
},
|
||||
async doStream(options) {
|
||||
calls.push(options);
|
||||
return { stream: new ReadableStream({ start(controller) {
|
||||
controller.enqueue({ type: "stream-start", warnings: [] });
|
||||
controller.enqueue({ type: "text-start", id: "1" });
|
||||
controller.enqueue({ type: "text-delta", id: "1", delta: text });
|
||||
controller.enqueue({ type: "text-end", id: "1" });
|
||||
controller.enqueue({ type: "finish", finishReason: "stop", usage });
|
||||
controller.close();
|
||||
} }) };
|
||||
},
|
||||
};
|
||||
const observations: SmalltalkObservation[] = [];
|
||||
const result = await classifyConsultationTurn({
|
||||
model: { ...model, model: fake }, question: "你好", history: [],
|
||||
onObservation: (observation) => observations.push(observation),
|
||||
});
|
||||
assert.deepEqual(result, { kind: "smalltalk", reply: "你好" });
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0]?.maxOutputTokens, 96);
|
||||
assert.equal(calls[0]?.tools?.length ?? 0, 0);
|
||||
assert.equal(observations[0]?.usage?.inputTokens, 15);
|
||||
assert.equal(observations[0]?.usage?.outputTokens, 10);
|
||||
});
|
||||
for (const scenario of ["invalid_schema", "bad_json", "provider_error"] as const) {
|
||||
test(`default Mastra adapter protects privacy and usage on ${scenario}`, async (t) => {
|
||||
const sentinel = "FICTIONAL_PRIVATE_SENTINEL";
|
||||
const text = scenario === "bad_json" ? sentinel : JSON.stringify({ kind: "smalltalk", reply: `${sentinel}。` });
|
||||
const usage = { inputTokens: 23, outputTokens: 7, totalTokens: 30 };
|
||||
const calls: Parameters<LanguageModelV2["doGenerate"]>[0][] = [];
|
||||
const fake: LanguageModelV2 = {
|
||||
specificationVersion: "v2", provider: "fake", modelId: "fake", supportedUrls: {},
|
||||
async doGenerate(options) {
|
||||
calls.push(options);
|
||||
if (scenario === "provider_error") throw new Error(sentinel);
|
||||
return { content: [{ type: "text", text }], finishReason: "stop", usage, warnings: [] };
|
||||
},
|
||||
async doStream(options) {
|
||||
calls.push(options);
|
||||
if (scenario === "provider_error") throw new Error(sentinel);
|
||||
return { stream: new ReadableStream({ start(controller) {
|
||||
controller.enqueue({ type: "stream-start", warnings: [] });
|
||||
controller.enqueue({ type: "text-start", id: "1" });
|
||||
controller.enqueue({ type: "text-delta", id: "1", delta: text });
|
||||
controller.enqueue({ type: "text-end", id: "1" });
|
||||
controller.enqueue({ type: "finish", finishReason: "stop", usage });
|
||||
controller.close();
|
||||
} }) };
|
||||
},
|
||||
};
|
||||
const logs: string[] = [];
|
||||
const observations: SmalltalkObservation[] = [];
|
||||
let result;
|
||||
try {
|
||||
for (const method of ["log", "info", "warn", "error", "debug"] as const) {
|
||||
t.mock.method(console, method, (...values: unknown[]) => { logs.push(JSON.stringify(values)); });
|
||||
}
|
||||
t.mock.method(process.stdout, "write", (chunk: unknown) => { logs.push(String(chunk)); return true; });
|
||||
t.mock.method(process.stderr, "write", (chunk: unknown) => { logs.push(String(chunk)); return true; });
|
||||
result = await classifyConsultationTurn({ model: { ...model, model: fake }, question: sentinel, history: [], onObservation: (o) => observations.push(o) });
|
||||
} finally { t.mock.restoreAll(); }
|
||||
assert.deepEqual(result, { kind: "consult" });
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0]?.maxOutputTokens, 96);
|
||||
assert.equal(calls[0]?.tools?.length ?? 0, 0);
|
||||
assert.equal(logs.join(" ").includes(sentinel), false, "SDK must not log private text");
|
||||
assert.equal(JSON.stringify(observations).includes(sentinel), false);
|
||||
if (scenario === "provider_error") {
|
||||
assert.equal(observations[0]?.outcome, "provider_error");
|
||||
assert.equal(observations[0]?.usage, undefined, "unknown usage must not become a fake zero");
|
||||
} else {
|
||||
assert.equal(observations[0]?.outcome, "invalid_output");
|
||||
assert.equal(observations[0]?.usage?.inputTokens, 23);
|
||||
assert.equal(observations[0]?.usage?.outputTokens, 7);
|
||||
}
|
||||
});
|
||||
}
|
||||
test("invalid JSON and provider errors fail open without leaking errors", async () => {
|
||||
for (const generate of [async () => ({ text: "not json" }), async () => { throw new Error("private provider payload"); }]) {
|
||||
const observations: SmalltalkObservation[] = [];
|
||||
assert.deepEqual(await classifyConsultationTurn({ model, question: "test", history: [], generate, onObservation: (o) => observations.push(o) }), { kind: "consult" });
|
||||
assert.equal(JSON.stringify(observations).includes("private"), false);
|
||||
}
|
||||
});
|
||||
test("timeout is bounded even when provider ignores abort, and late usage remains observable", async () => {
|
||||
let resolve!: (value: { object: unknown; usage: { inputTokens: number } }) => void;
|
||||
let signal: AbortSignal | undefined;
|
||||
const observations: SmalltalkObservation[] = [];
|
||||
const started = Date.now();
|
||||
const result = await classifyConsultationTurn({ model, question: "test", history: [], onObservation: (o) => observations.push(o), generate: async (_, s) => {
|
||||
signal = s; return new Promise((done) => { resolve = done; });
|
||||
} });
|
||||
assert.deepEqual(result, { kind: "consult" });
|
||||
assert.ok(Date.now() - started >= 2900 && Date.now() - started < 4500);
|
||||
assert.equal(signal?.aborted, true);
|
||||
assert.equal(observations[0]?.outcome, "timeout");
|
||||
resolve({ object: { kind: "smalltalk", reply: "你好" }, usage: { inputTokens: 12 } });
|
||||
await new Promise((done) => setImmediate(done));
|
||||
assert.equal(observations[1]?.late, true);
|
||||
assert.equal(observations[1]?.usage?.inputTokens, 12);
|
||||
});
|
||||
test("pre-cancelled input never calls the model or causes an unhandled rejection", async () => {
|
||||
const c = new AbortController(); c.abort();
|
||||
let calls = 0;
|
||||
assert.deepEqual(await classifyConsultationTurn({ model, question: "test", history: [], signal: c.signal, generate: async () => { calls++; return {}; } }), { kind: "consult" });
|
||||
assert.equal(calls, 0);
|
||||
await new Promise((done) => setImmediate(done));
|
||||
});
|
||||
test("input projects only visible question, name and last complete history pair", async () => {
|
||||
const history = [
|
||||
{ role: "user", text: "older", requestId: "old" }, { role: "assistant", text: "old reply", requestId: "old" },
|
||||
{ role: "user", text: "previous", requestId: "pair", birth: "excluded" },
|
||||
{ role: "assistant", text: "visible previous reply", requestId: "pair", thinkingText: "excluded", workflowReceipt: {} },
|
||||
{ role: "user", text: "incomplete" },
|
||||
];
|
||||
assert.deepEqual(smalltalkHistoryPair(history), [{ role: "user", text: "previous" }, { role: "assistant", text: "visible previous reply" }]);
|
||||
assert.deepEqual(smalltalkHistoryPair([{ role: "user", text: "unanswered" }]), []);
|
||||
assert.deepEqual(smalltalkHistoryPair([{ role: "user", text: "a", requestId: "a" }, { role: "assistant", text: "b", requestId: "b" }]), []);
|
||||
await classifyConsultationTurn({ model, question: "current", name: "虚构称呼", history, generate: async (content) => {
|
||||
assert.deepEqual(JSON.parse(content), { question: "current", name: "虚构称呼", history: smalltalkHistoryPair(history) });
|
||||
return { object: { kind: "consult" } };
|
||||
} });
|
||||
const classifier = source("../src/lib/consultation-smalltalk.ts");
|
||||
assert.doesNotMatch(classifier, /tools:|memory:|getJyotishAgent|skillBinding/);
|
||||
});
|
||||
test("smalltalk stream persists first and emits only answer plus receipt-free completion", async () => {
|
||||
let persisted = false;
|
||||
const response = streamSmalltalkResponse({ requestId: "fake", reply: "你好", complete: async () => { persisted = true; }, onError: async () => assert.fail() });
|
||||
const events = (await response.text()).trim().split("\n").map((line) => consultationAgentPublicEventSchema.parse(JSON.parse(line)));
|
||||
assert.equal(persisted, true);
|
||||
assert.deepEqual(events, [{ type: "answer.delta", text: "你好" }, { type: "run.completed", responseKind: "smalltalk" }]);
|
||||
assert.equal(consultationAgentPublicEventSchema.safeParse({ type: "run.completed" }).success, false);
|
||||
});
|
||||
test("failed free persistence never emits answer or completion", async () => {
|
||||
let cancelled = 0;
|
||||
const response = streamSmalltalkResponse({ requestId: "fake", reply: "你好", complete: async () => { throw new Error("failed"); }, onError: async () => { cancelled++; } });
|
||||
const text = await response.text();
|
||||
assert.equal(cancelled, 1);
|
||||
assert.match(text, /run.failed/);
|
||||
assert.doesNotMatch(text, /answer.delta|run.completed|你好/);
|
||||
});
|
||||
test("disconnect does not cancel the server-owned free completion", async () => {
|
||||
let finish!: () => void;
|
||||
let persisted = false;
|
||||
const response = streamSmalltalkResponse({ requestId: "fake", reply: "你好", complete: async () => { await new Promise<void>((done) => { finish = done; }); persisted = true; }, onError: async () => assert.fail() });
|
||||
await response.body!.cancel(); finish();
|
||||
await new Promise((done) => setImmediate(done));
|
||||
assert.equal(persisted, true);
|
||||
});
|
||||
test("smalltalk views never reconstruct an invented settled execution timeline", () => {
|
||||
assert.equal(settledChatMessageViews([{ role: "assistant", text: "你好", responseKind: "smalltalk" }])[0]?.timeline, undefined);
|
||||
assert.equal(streamingChatMessageView([{ role: "user", text: "你好" }], true, "你好", undefined, undefined, undefined, [], "smalltalk")?.responseKind, "smalltalk");
|
||||
assert.match(source("../src/components/chat-message-row.tsx"), /const quiet = smalltalk \|\| awaitingClassification/);
|
||||
assert.match(source("../src/lib/home-cloud-sync.ts"), /stored.responseKind === "smalltalk"/);
|
||||
});
|
||||
test("route gates all special entries before a single selected-model classifier and keeps tool contracts", () => {
|
||||
const route = source("../src/app/api/consult/route.ts");
|
||||
assert.match(route, /parsed.data.entrypoint === undefined\s*\? await classifyConsultationTurn/);
|
||||
assert.match(route, /classifyConsultationTurn\(\{\s*model: selectedModel,\s*question: visibleQuestion/);
|
||||
assert.equal((route.match(/requireTool: true/g) ?? []).length, 2);
|
||||
const smalltalk = route.slice(route.indexOf('if (turn.kind === "smalltalk")'), route.indexOf("const expectedTitle"));
|
||||
assert.match(smalltalk, /complete_consultation_free/);
|
||||
assert.doesNotMatch(smalltalk, /run-jyotish-|streamAgentResponse\(/);
|
||||
});
|
||||
test("all three agent voices share the exemption, natal domain skeleton stays mandatory", () => {
|
||||
assert.match(productConversationVoice, /只是打招呼、道谢、告别时,回一句话,不要套开场形状/);
|
||||
assert.doesNotMatch(natalSpokenReportContract, /Short chit-chat/);
|
||||
assert.match(natalSpokenReportContract, /One natal career\/wealth\/marriage\/family question still uses the opener-plus-skeleton/);
|
||||
const index = source("../src/mastra/index.ts");
|
||||
assert.ok((index.match(/\$\{productConversationVoice\}/g) ?? []).length >= 3);
|
||||
});
|
||||
@@ -66,6 +66,12 @@ test("general and window get voice without the natal Level 2 skeleton", () => {
|
||||
assert.match(general, /productConversationVoice/);
|
||||
assert.doesNotMatch(general, /natalSpokenReportContract/);
|
||||
assert.match(window, /productConversationVoice/);
|
||||
const exemption = "只是打招呼、道谢、告别时,回一句话,不要套开场形状";
|
||||
const sharedVoice = voice.slice(0, voice.indexOf("export const natalSpokenReportContract"));
|
||||
for (const instructions of [natal, general, window]) {
|
||||
// Expand only the shared voice literal; no runtime Skill filesystem is needed.
|
||||
assert.ok(instructions.replace("${productConversationVoice}", sharedVoice).includes(exemption));
|
||||
}
|
||||
assert.doesNotMatch(window, /natalSpokenReportContract/);
|
||||
assert.doesNotMatch(mastra, /const onboardingInstructions/);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
import pg from "pg";
|
||||
import { startPostgresFixture } from "./helpers/postgres-fixture.ts";
|
||||
|
||||
const runner = fileURLToPath(new URL("../scripts/db-migrate.mjs", import.meta.url));
|
||||
const docker = spawnSync("docker", ["version", "--format", "{{.Server.Version}}"], { stdio: "ignore" }).status === 0;
|
||||
const session = "99999999-9999-4999-8999-999999999971";
|
||||
const otherSession = "99999999-9999-4999-8999-999999999972";
|
||||
|
||||
test("free completion atomically persists, refunds exact credits, records actual cost and guards trust/idempotency", { skip: docker ? false : "docker unavailable" }, async () => {
|
||||
const fixture = startPostgresFixture();
|
||||
try {
|
||||
const migration = spawnSync(process.execPath, [runner], { encoding: "utf8", env: { ...process.env, SCHEMA_DATABASE_URL: fixture.connectionUrl("schema_owner", "schema-owner-test-password") } });
|
||||
assert.equal(migration.status, 0, migration.stderr);
|
||||
fixture.psqlAs("identity_runtime", "identity-runtime-test-password", `insert into identity.users(name,email,email_verified,email_verified_at) values ('Fictional Free','free@example.com',true,now()),('Fictional Other','other-free@example.com',true,now());`);
|
||||
const user = fixture.psql("select id from identity.users where email='free@example.com'");
|
||||
const other = fixture.psql("select id from identity.users where email='other-free@example.com'");
|
||||
fixture.psql(`update public.profiles set credits=20 where id='${user}';
|
||||
insert into public.chat_sessions(id,user_id,title,theme,model_id,messages,session_type,updated_at) values
|
||||
('${session}','${user}','test','general','test-model','[]','consultation',now()),
|
||||
('${otherSession}','${other}','test','general','test-model','[]','consultation',now());`);
|
||||
const reserve = (id: string) => fixture.psql(`select success from public.reserve_consultation_usage('${user}','${id}','${session}','test-model',3)`);
|
||||
const balance = () => fixture.psql(`select credits from public.profiles where id='${user}'`);
|
||||
const completeSql = (id: string, reply = "你好", owner = user, sid = session, usage = '{"inputTokens":23,"outputTokens":7,"costMicrousd":91,"actualModelId":"test-model","modelConfigVersion":1}') =>
|
||||
`select success::text || ':' || coalesce(error_code,'null') from public.complete_consultation_free('${owner}','${id}','${sid}','{"role":"assistant","text":"${reply}","responseKind":"smalltalk"}', '${usage}')`;
|
||||
for (const role of ["authenticated", "anon", "app_runtime"]) {
|
||||
assert.equal(fixture.psql(`select has_function_privilege('${role}','public.complete_consultation_free(uuid,text,uuid,jsonb,jsonb)','execute')`), "f");
|
||||
}
|
||||
assert.equal(fixture.psql("select has_function_privilege('service_role','public.complete_consultation_free(uuid,text,uuid,jsonb,jsonb)','execute')"), "t");
|
||||
assert.equal(reserve("free-1"), "t"); assert.equal(balance(), "17");
|
||||
assert.equal(fixture.psql(completeSql("free-1", "你好", other)), "f:request_missing");
|
||||
assert.equal(fixture.psql(completeSql("free-1", "你好", user, otherSession)), "f:request_missing");
|
||||
assert.equal(balance(), "17");
|
||||
assert.equal(fixture.psql(`set role service_role; ${completeSql("free-1")}`).split("\n").at(-1), "true:null");
|
||||
assert.equal(balance(), "20");
|
||||
assert.equal(fixture.psql(completeSql("free-1")), "true:null");
|
||||
assert.equal(fixture.psql(completeSql("free-1", "再见")), "f:response_conflict");
|
||||
assert.equal(balance(), "20");
|
||||
assert.equal(fixture.psql(`select jsonb_array_length(messages) from public.chat_sessions where id='${session}'`), "1");
|
||||
assert.equal(fixture.psql(`select status || ':' || (response_message->>'responseKind') from public.consultation_requests where user_id='${user}' and request_id='free-1'`), "completed:smalltalk");
|
||||
assert.equal(fixture.psql(`select input_tokens || ':' || output_tokens || ':' || cost_microusd || ':' || (metadata->>'freeCompletion') from public.usage_ledger where user_id='${user}' and request_id='free-1'`), "23:7:91:true");
|
||||
assert.equal(fixture.psql(`select count(*) || ':' || sum(amount) from public.credit_transactions where user_id='${user}' and request_id='free-1' and transaction_type='refund'`), "1:3");
|
||||
assert.equal(fixture.psql(`select status from public.usage_reservations where user_id='${user}' and request_id='free-1'`), "released");
|
||||
assert.equal(fixture.psql(`select success::text || ':' || coalesce(error_code,'null') from public.cancel_consultation_credit('${user}','free-1')`), "f:request_completed");
|
||||
|
||||
assert.equal(reserve("cancelled"), "t");
|
||||
fixture.psql(`select * from public.cancel_consultation_credit('${user}','cancelled')`);
|
||||
assert.equal(fixture.psql(completeSql("cancelled")), "f:request_cancelled"); assert.equal(balance(), "20");
|
||||
|
||||
assert.equal(reserve("paid"), "t");
|
||||
fixture.psql(`select * from public.complete_consultation_response('${user}','paid','${session}','{"role":"assistant","text":"paid consultation"}','{}')`);
|
||||
assert.equal(fixture.psql(completeSql("paid")), "f:response_conflict"); assert.equal(balance(), "17");
|
||||
assert.equal(fixture.psql(`select count(*) from public.credit_transactions where user_id='${user}' and request_id='paid' and transaction_type='refund'`), "0");
|
||||
|
||||
assert.equal(reserve("bad-usage"), "t");
|
||||
assert.throws(() => fixture.psql(completeSql("bad-usage", "你好", user, session, '{"inputTokens":-1}')));
|
||||
assert.equal(balance(), "14");
|
||||
assert.equal(fixture.psql(`select status from public.consultation_requests where user_id='${user}' and request_id='bad-usage'`), "reserved");
|
||||
assert.equal(fixture.psql(`select jsonb_array_length(messages) from public.chat_sessions where id='${session}'`), "2");
|
||||
// A successful retry after a transaction rollback refunds once and persists once.
|
||||
assert.equal(fixture.psql(completeSql("bad-usage")), "true:null"); assert.equal(balance(), "17");
|
||||
|
||||
// A subscription-shaped reservation has no credit debit/refund, and releases
|
||||
// the same quota counter (reserved/completed rows) while preserving cost.
|
||||
assert.equal(reserve("subscription-free"), "t");
|
||||
fixture.psql(`update public.profiles set credits=credits+3 where id='${user}';
|
||||
update public.usage_reservations set source='subscription',credit_amount=0 where user_id='${user}' and request_id='subscription-free';`);
|
||||
assert.equal(fixture.psql(completeSql("subscription-free")), "true:null");
|
||||
assert.equal(balance(), "17");
|
||||
assert.equal(fixture.psql(`select count(*) from public.credit_transactions where user_id='${user}' and request_id='subscription-free' and transaction_type='refund'`), "0");
|
||||
assert.equal(fixture.psql(`select count(*) from public.usage_reservations where user_id='${user}' and request_id='subscription-free' and status in ('reserved','completed')`), "0");
|
||||
|
||||
// Genuine concurrent connections: cancellation/free completion share one lock.
|
||||
assert.equal(reserve("race"), "t");
|
||||
const clients = [new pg.Client({ connectionString: fixture.connectionUrl("schema_owner", "schema-owner-test-password") }), new pg.Client({ connectionString: fixture.connectionUrl("schema_owner", "schema-owner-test-password") })];
|
||||
try {
|
||||
await Promise.all(clients.map((client) => client.connect()));
|
||||
await Promise.all([
|
||||
clients[0]!.query(completeSql("race")),
|
||||
clients[1]!.query(`select * from public.cancel_consultation_credit('${user}','race')`),
|
||||
]);
|
||||
assert.equal(balance(), "17");
|
||||
assert.equal(fixture.psql(`select count(*) from public.credit_transactions where user_id='${user}' and request_id='race' and transaction_type='refund'`), "1");
|
||||
assert.match(fixture.psql(`select status from public.consultation_requests where user_id='${user}' and request_id='race'`), /^(completed|cancelled)$/);
|
||||
} finally { await Promise.all(clients.map((client) => client.end())); }
|
||||
} finally { fixture.stop(); }
|
||||
});
|
||||
@@ -14,7 +14,8 @@ test("assistant messages fold the user-facing Technique Audit Table and never re
|
||||
assert.doesNotMatch(rowSource, /claimStatus=\{message\.techniqueTruth\}/);
|
||||
assert.doesNotMatch(rowSource, /workflowReceipt=\{message\.workflowReceipt\}/);
|
||||
assert.match(contentSource, /TechniqueAuditDisclosure/);
|
||||
assert.match(rowSource, /auditRows=\{message\.agentExecutionReceipt\?\.techniqueAuditTable\}/);
|
||||
// 原值总展示 receipt;新值寒暄明确无 auditRows,咨询保持原字段。
|
||||
assert.match(rowSource, /auditRows=\{smalltalk \? undefined : message\.agentExecutionReceipt\?\.techniqueAuditTable\}/);
|
||||
assert.match(panelSource, /Technique Audit Table/);
|
||||
assert.match(panelSource, /Workflow route:/);
|
||||
assert.match(panelSource, /Precise timing:/);
|
||||
|
||||
@@ -52,7 +52,8 @@ test("consult generates a title from the pre-RPC snapshot and guards with the po
|
||||
const afterAppend = sourceBetween(
|
||||
consultRoute,
|
||||
"if (!appendedQuestion.success) {",
|
||||
"const usageStartedAt = Date.now();",
|
||||
// 原值 usageStartedAt;新值 usagePayload;分类计时前移,标题仍须在免费早返之后。
|
||||
"async function usagePayload(",
|
||||
);
|
||||
const persist = sourceBetween(
|
||||
consultRoute,
|
||||
|
||||
Reference in New Issue
Block a user