Files
Jyotisha/frontend/tests/chat-session-write.test.ts
T
Jesse_Chen 10ae149c58
Independent Staging Quality Gate / validate (push) Successful in 7m41s
Independent Staging Quality Gate / publish (push) Successful in 9m39s
fix(consult): check the evidence gate against the route the answer is on
七条路由的证据门都不是自己的:route_requirements 的键写成 relationship/finance,而
路由名是 marriage/wealth,另有 5 条路由压根没有条目,全部静默落到 general 的门。
missingLayers: [] 因此不表示证据齐备,只表示没检查过——婚姻的 UL 与财富的 D2 从未
进入检查。

同一函数另有两处判据也没接到权威来源。7 块正则用问题文本重猜领域,而领域早已由模型
声明并写进 route_packet,一句写作「情感」而非表里「感情」的提问在 marriage 路由上完全
拿不到性别解读边界。timing_layers_ready 读的是 missing_route_layers,该列表只装本路由
要求的层,于是对任何不要求 narayana_dasha 的路由恒为真,精确应期在该层根本没算出来时
也照样放行。三处的失败方向都是静默放宽,因此没有任何人报错。

三处都接回权威来源:10 条路由逐条显式列出必需层(层名限定为证据包真实构建的 section,
所以 wealth 不要求引擎不产出的 D11)、领域边界按 route 查表、出生时间边界从矫正闸门的
effective_accuracy 与 Lagna 敏感度派生、就绪判断直接读 section 状态。唯一保留文本探测
的是「用户有没有要一个具体日期」——服务端对此没有权威来源,改为 timing/annual 路由结构
性携带、文本仅作叠加,一次措辞漏判不再能把信号清零。

另外把 skillReferenceReadCount 暴露为回执的 skill.referenceReads(必填)与可观测日志的
skillReferenceReads。它此前数完即丢,而 skill_read 按设计不记成 runtime step,因此「模型
有没有真的翻开方法文档」在运行结束后无处可查。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-18 11:50:28 +08:00

106 lines
4.4 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { chatSessionWriteSchema, writeChatSession, type ChatSessionWrite } from "../src/lib/chat-session-write-contract.ts";
const sessionId = "11111111-1111-4111-8111-111111111111";
const values = {
title: "事业方向",
theme: "career",
model_id: "gpt-test",
messages: [{ role: "user", text: "你好" }],
session_type: "consultation",
rectification_case_id: null,
updated_at: "2026-07-22T00:00:00.000Z",
} satisfies ChatSessionWrite;
test("chat session schema preserves the safe agent execution receipt", () => {
const receipt = {
runId: "run-1",
runtime: "mastra-agentic" as const,
skill: { name: "jyotish-vedic-astrology" as const, loaded: true, referenceReads: 0 },
steps: [{ sequence: 1, kind: "skill" as const, name: "jyotish-vedic-astrology", status: "completed" as const }],
workflow: { route: "multi-domain", status: "ready", preciseTiming: "allowed", missingLayers: [], domains: ["general", "timing"] },
techniqueTruth: "verified",
};
const workflowReceipt = {
route: "multi-domain",
status: "ready",
preciseTiming: "allowed",
missingLayers: [],
domains: ["general", "timing"] as const,
};
const parsed = chatSessionWriteSchema.parse({
...values,
messages: [{ role: "assistant", text: "回答", workflowReceipt, agentExecutionReceipt: receipt }],
});
assert.deepEqual(parsed.messages[0]?.workflowReceipt, workflowReceipt);
assert.deepEqual(parsed.messages[0]?.agentExecutionReceipt, receipt);
});
test("chat session writes use same-origin API instead of browser-to-Supabase requests", async () => {
const calls: Array<{ url: string; init?: RequestInit }> = [];
await writeChatSession(sessionId, values, "update", async (url, init) => {
calls.push({ url: String(url), init });
return Response.json({ ok: true });
});
assert.equal(calls.length, 1);
assert.equal(calls[0]?.url, `/api/sessions/${sessionId}`);
assert.equal(calls[0]?.init?.method, "PATCH");
assert.equal(calls[0]?.init?.credentials, "same-origin");
});
test("transient Load failed is retried and never exposed as raw browser copy", async () => {
let attempts = 0;
await assert.rejects(
writeChatSession(sessionId, values, "update", async () => {
attempts += 1;
throw new TypeError("Load failed");
}),
(error: unknown) => error instanceof Error
&& error.message === "网络暂时不可用,云端记录尚未更新",
);
assert.equal(attempts, 2);
});
test("owner or validation failures are not retried", async () => {
let attempts = 0;
await assert.rejects(
writeChatSession(sessionId, values, "update", async () => {
attempts += 1;
return Response.json({ error: "聊天记录不存在或已被删除" }, { status: 404 });
}),
/聊天记录不存在或已被删除/,
);
assert.equal(attempts, 1);
});
test("session API owns create and update while answer UI keeps sync failures out of reply errors", () => {
const page = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
const collectionRoute = readFileSync(new URL("../src/app/api/sessions/route.ts", import.meta.url), "utf8");
const itemRoute = readFileSync(new URL("../src/app/api/sessions/[id]/route.ts", import.meta.url), "utf8");
assert.match(page, /writeChatSession\(session\.id, values, mode\)/);
assert.doesNotMatch(page, /云端同步失败.*回答仍保留在当前页面/);
assert.match(collectionRoute, /export async function POST/);
assert.match(itemRoute, /export async function PATCH/);
assert.match(itemRoute, /\.eq\("user_id", user\.id\)/);
});
test("self-hosted staging bootstrap reads profile and sessions through same-origin APIs", () => {
const page = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
const accountRoute = readFileSync(new URL("../src/app/api/account/route.ts", import.meta.url), "utf8");
assert.doesNotMatch(page, /createBrowserSupabaseClient/);
assert.match(page, /fetch\("\/api\/account"/);
assert.match(page, /fetch\("\/api\/sessions"/);
assert.match(page, /writeChatSession\(initialSession\.id,[\s\S]*?"create"\)/);
assert.match(page, /fetch\(`\/api\/sessions\/\$\{encodeURIComponent\(sessionId\)\}`/);
assert.match(accountRoute, /AUTH_PROVIDER\?\.trim\(\) === "self-hosted"/);
assert.match(accountRoute, /profile,/);
assert.doesNotMatch(accountRoute, /rectificationCase/);
});