发问准备可以并行,三个开关先保持关闭(BUG-1337~1339)
开流前的注销检查可以和读会话、解析模型同时进行;审核改到扣点前等待,不再挡住第一行;模型版本可以短缓存;两个领域可以一起算。本机没有数据库中位数,也没有带星盘网络库的镜像,所以默认仍是关闭、缓存 0、领域逐个算。 自测:tsc 0 错;lint 0 error;相关测试失败 0、取消 0。test:db 未跑。 Co-Authored-By: Grok (agent loop)
This commit is contained in:
co-authored by
Grok (agent loop)
parent
66cd60081f
commit
a0e223e20c
@@ -255,18 +255,23 @@ test("a turn that throws before producing any response ends the stream with a ge
|
||||
|
||||
test("the consult route opens the stream before subject reads, reservation, question append and classification", () => {
|
||||
const route = source("../src/app/api/consult/route.ts");
|
||||
const head = source("../src/lib/consult/turn-prep.ts");
|
||||
const openIndex = route.indexOf("const runTurn = async (): Promise<Response> =>");
|
||||
assert.ok(openIndex > 0);
|
||||
// Stays in front of the stream: auth, body, session existence and type, the session model.
|
||||
// 原值: 401、请求体、会话不存在、会话类型、模型已变这些字面量都在 route.ts 的 runTurn 之前。
|
||||
// 新值: 这些字面量在 turn-prep.ts;route.ts 在 runTurn 之前调用 loadConsultTurnHead 并返回失败响应。提示词拦截仍在 route.ts。
|
||||
// 原因: 开流前的独立读取收进同一个函数,两种开关的状态码仍在开流前返回。
|
||||
assert.ok(route.indexOf("loadConsultTurnHead") < openIndex);
|
||||
assert.ok(route.indexOf("if (!head.ok)") < openIndex);
|
||||
assert.ok(route.indexOf("if (blocksPromptExtraction(userControlledPrompt))") < openIndex);
|
||||
for (const marker of [
|
||||
"status: 401",
|
||||
'{ error: "出生资料或问题格式不正确", details: parsed.error.flatten() }',
|
||||
'{ error: "出生资料或问题格式不正确", details: error.flatten() }',
|
||||
'error: "咨询会话不存在"',
|
||||
'code: "session_not_consultation"',
|
||||
'error: "会话模型已经变化"',
|
||||
"if (blocksPromptExtraction(userControlledPrompt))",
|
||||
]) {
|
||||
assert.ok(route.indexOf(marker) < openIndex, `${marker} must answer with an HTTP status`);
|
||||
assert.ok(head.includes(marker), `${marker} stays an HTTP response before the stream`);
|
||||
}
|
||||
// Inside the stream: the slow part, and every rejection it can raise.
|
||||
for (const marker of [
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { register } from "node:module";
|
||||
import test from "node:test";
|
||||
|
||||
import { agentObservabilityEventSchema } from "../src/lib/agent-observability.ts";
|
||||
import { ConsultationInputBlockedError, consultPrepObservabilityEvent } from "../src/lib/consult/prep-log.ts";
|
||||
import { inputBlockedCopy, MODERATION_SELF_HARM_COPY } from "../src/lib/moderation/index.ts";
|
||||
import type { ModerationResult } from "../src/lib/moderation/types.ts";
|
||||
import { streamFirstResponse } from "../src/lib/stream-first-response.ts";
|
||||
|
||||
const blocked: ModerationResult = {
|
||||
verdict: "block",
|
||||
categories: ["self_harm"],
|
||||
ruleIds: ["lexicon"],
|
||||
providerId: "local",
|
||||
};
|
||||
|
||||
test("a blocked question records once and never reaches reserve", async () => {
|
||||
let reserves = 0;
|
||||
let records = 0;
|
||||
const pending = Promise.resolve(blocked);
|
||||
async function beforeReserve() {
|
||||
const result = await pending;
|
||||
if (result.verdict !== "pass") records += 1;
|
||||
if (result.verdict === "block") throw new ConsultationInputBlockedError(result);
|
||||
reserves += 1;
|
||||
}
|
||||
await assert.rejects(beforeReserve, (error: unknown) => {
|
||||
assert.ok(error instanceof ConsultationInputBlockedError);
|
||||
assert.equal(inputBlockedCopy(error.result), MODERATION_SELF_HARM_COPY);
|
||||
return true;
|
||||
});
|
||||
assert.equal(reserves, 0);
|
||||
assert.equal(records, 1);
|
||||
const route = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8");
|
||||
const blockedAt = route.indexOf('blocked?.verdict === "block"');
|
||||
const profileAt = route.indexOf("error instanceof ConsultationProfileTruthError");
|
||||
assert.ok(blockedAt > 0 && blockedAt < profileAt);
|
||||
});
|
||||
|
||||
test("moderation that takes 1.5s does not hold the first frame", async () => {
|
||||
let release: (result: ModerationResult) => void = () => {};
|
||||
const pending = new Promise<ModerationResult>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
const started = Date.now();
|
||||
const response = streamFirstResponse({
|
||||
requestId: "prep-frame",
|
||||
run: async () => {
|
||||
const result = await pending;
|
||||
return Response.json(
|
||||
{ error: "无法处理该请求", code: "content_blocked", message: inputBlockedCopy(result) },
|
||||
{ status: 400 },
|
||||
);
|
||||
},
|
||||
});
|
||||
const reader = response.body!.getReader();
|
||||
const first = await reader.read();
|
||||
assert.ok(Date.now() - started < 400, "first frame waited for moderation");
|
||||
assert.ok(first.value && first.value.byteLength > 0);
|
||||
setTimeout(() => release(blocked), 1_500);
|
||||
const chunks: string[] = [];
|
||||
const decoder = new TextDecoder();
|
||||
while (true) {
|
||||
const next = await reader.read();
|
||||
if (next.value) chunks.push(decoder.decode(next.value));
|
||||
if (next.done) break;
|
||||
}
|
||||
const events = chunks.join("").trim().split("\n").map((line) => JSON.parse(line));
|
||||
const rejected = events.find((event) => event.type === "run.failed");
|
||||
assert.equal(rejected.message, MODERATION_SELF_HARM_COPY);
|
||||
assert.equal(rejected.reason, "content_blocked");
|
||||
assert.equal(rejected.status, 400);
|
||||
});
|
||||
|
||||
test("prep observability accepts the closed schema and drops the question", () => {
|
||||
const question = "这句问题原文不该出现在日志里";
|
||||
const event = consultPrepObservabilityEvent({
|
||||
requestId: "req-prep",
|
||||
sessionId: "sess-prep",
|
||||
phases: [
|
||||
{ phase: "prep.model", durationMs: 3, status: "completed" },
|
||||
{ phase: "prep.moderation", durationMs: 12, status: "completed" },
|
||||
],
|
||||
});
|
||||
assert.deepEqual(agentObservabilityEventSchema.parse(event), event);
|
||||
assert.equal(JSON.stringify(event).includes(question), false);
|
||||
assert.equal(event.agentVersion, "consultation-prep-v1");
|
||||
});
|
||||
|
||||
test("readVersion cache skips a second database and DNS lookup inside the ttl", async () => {
|
||||
await register(new URL("./support/stub-server-only.mjs", import.meta.url).href, import.meta.url);
|
||||
let queries = 0;
|
||||
let dns = 0;
|
||||
const row = {
|
||||
model_id: "model-a",
|
||||
version: 4,
|
||||
label: "Model A",
|
||||
description: "fixture",
|
||||
provider_model: "demo",
|
||||
credit_cost: 1,
|
||||
is_default: false,
|
||||
provider_code: "demo",
|
||||
provider_type: "openai-compatible",
|
||||
base_url: "https://example.com/v1",
|
||||
encrypted_api_key: "enc",
|
||||
input_cost: 1,
|
||||
output_cost: 1,
|
||||
context_window: 8000,
|
||||
};
|
||||
const catalog = await import("../src/lib/model-catalog.ts");
|
||||
const flags = await import("../src/lib/consult/prep-flags.ts");
|
||||
catalog.setModelCatalogIoForTests({
|
||||
queryAdminRows: (async () => {
|
||||
queries += 1;
|
||||
if (queries === 3) throw new Error("database down");
|
||||
if (queries >= 4) return [];
|
||||
return [row];
|
||||
}) as typeof import("../src/lib/admin/database.ts").queryAdminRows,
|
||||
assertAllowedModelProviderUrl: (async (value: string) => {
|
||||
dns += 1;
|
||||
return { url: new URL(value), addresses: [] };
|
||||
}) as typeof import("../src/lib/epay/gateway-policy.ts").assertAllowedModelProviderUrl,
|
||||
decryptModelProviderApiKey: (() => "test-key") as typeof import("../src/lib/model-provider-policy.ts").decryptModelProviderApiKey,
|
||||
});
|
||||
process.env.MODEL_VERSION_CACHE_TTL_MS = "15000";
|
||||
try {
|
||||
catalog.invalidateLanguageModelCatalog();
|
||||
const first = await catalog.resolveSessionLanguageModel("model-a", 4);
|
||||
const second = await catalog.resolveSessionLanguageModel("model-a", 4);
|
||||
assert.equal(first?.id, "model-a");
|
||||
assert.equal(second, first);
|
||||
assert.equal(queries, 1);
|
||||
assert.equal(dns, 1);
|
||||
catalog.invalidateLanguageModelCatalog();
|
||||
await catalog.resolveSessionLanguageModel("model-a", 4);
|
||||
assert.equal(queries, 2);
|
||||
process.env.MODEL_VERSION_CACHE_TTL_MS = "0";
|
||||
await catalog.resolveSessionLanguageModel("model-a", 4);
|
||||
assert.ok(queries >= 3);
|
||||
const failed = await catalog.resolveSessionLanguageModel("model-a", 4);
|
||||
const empty = await catalog.resolveSessionLanguageModel("model-a", 4);
|
||||
assert.equal(failed, null);
|
||||
assert.equal(empty, null);
|
||||
const queriesAfterMiss = queries;
|
||||
await catalog.resolveSessionLanguageModel("model-a", 4);
|
||||
assert.ok(queries > queriesAfterMiss);
|
||||
} finally {
|
||||
flags.resetConsultPrepFlagWarningsForTests();
|
||||
delete process.env.MODEL_VERSION_CACHE_TTL_MS;
|
||||
catalog.invalidateLanguageModelCatalog();
|
||||
catalog.setModelCatalogIoForTests(null);
|
||||
}
|
||||
});
|
||||
|
||||
test("the title read starts after the question is saved and is awaited at the old use site", () => {
|
||||
const route = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8");
|
||||
const append = route.indexOf('accounting.rpc("append_consultation_question"');
|
||||
const title = route.indexOf('.select("title")');
|
||||
const classify = route.indexOf("await classifyConsultationTurn({");
|
||||
const awaited = route.indexOf("await titleRead");
|
||||
assert.ok(append > 0 && title > append && classify > title && awaited > classify);
|
||||
});
|
||||
@@ -0,0 +1,152 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { loadConsultTurnHead } from "../src/lib/consult/turn-prep.ts";
|
||||
|
||||
type User = { id: string };
|
||||
type Parsed = { sessionId: string; modelId: string };
|
||||
type Session = { model_id: string | null; model_config_version: number | null; session_type: string | null };
|
||||
type Model = { id: string };
|
||||
|
||||
function okSession(): Session {
|
||||
return { model_id: "m", model_config_version: 2, session_type: "consultation" };
|
||||
}
|
||||
|
||||
function deps(overrides: Record<string, unknown> = {}) {
|
||||
const calls = { deletion: 0, session: 0, model: 0 };
|
||||
const base = {
|
||||
parallel: false,
|
||||
getUser: async () => ({ data: { user: { id: "user-1" } as User }, error: null }),
|
||||
readPendingDeletion: async () => {
|
||||
calls.deletion += 1;
|
||||
return null;
|
||||
},
|
||||
readBody: async () => ({ success: true as const, data: { sessionId: "session-1", modelId: "m" } as Parsed }),
|
||||
readSession: async () => {
|
||||
calls.session += 1;
|
||||
return { data: okSession(), error: null };
|
||||
},
|
||||
resolveModel: async () => {
|
||||
calls.model += 1;
|
||||
return { id: "m" } as Model;
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
return { calls, deps: base };
|
||||
}
|
||||
|
||||
async function snapshot(response: Response) {
|
||||
return { status: response.status, body: await response.json() };
|
||||
}
|
||||
|
||||
async function both(overrides: Record<string, unknown> = {}) {
|
||||
const off = deps({ ...overrides, parallel: false });
|
||||
const on = deps({ ...overrides, parallel: true });
|
||||
const [left, right] = await Promise.all([
|
||||
loadConsultTurnHead(off.deps),
|
||||
loadConsultTurnHead(on.deps),
|
||||
]);
|
||||
assert.equal(left.ok, false);
|
||||
assert.equal(right.ok, false);
|
||||
if (left.ok || right.ok) return;
|
||||
assert.deepEqual(await snapshot(left.response), await snapshot(right.response));
|
||||
}
|
||||
|
||||
test("serial and parallel heads return the same status for each gate", async () => {
|
||||
const anonymous = deps({
|
||||
getUser: async () => ({ data: { user: null }, error: null }),
|
||||
});
|
||||
const missingUser = await loadConsultTurnHead({ ...anonymous.deps, parallel: false });
|
||||
assert.equal(missingUser.ok, false);
|
||||
if (!missingUser.ok) assert.equal(missingUser.response.status, 401);
|
||||
assert.equal(anonymous.calls.deletion, 0);
|
||||
|
||||
await both({
|
||||
readPendingDeletion: async () => Response.json({ error: "账号正在注销" }, { status: 423 }),
|
||||
readBody: async () => ({ success: false as const, error: { flatten: () => ({ formErrors: ["bad"] }) } }),
|
||||
});
|
||||
await both({
|
||||
readBody: async () => ({ success: false as const, error: { flatten: () => ({ formErrors: ["bad"] }) } }),
|
||||
});
|
||||
await both({ readSession: async () => ({ data: null, error: { message: "down" } }) });
|
||||
await both({ readSession: async () => ({ data: null, error: null }) });
|
||||
await both({
|
||||
readSession: async () => ({ data: { ...okSession(), session_type: "rectification" }, error: null }),
|
||||
});
|
||||
await both({
|
||||
readSession: async () => ({ data: { ...okSession(), model_id: "other" }, error: null }),
|
||||
});
|
||||
await both({ resolveModel: async () => null });
|
||||
});
|
||||
|
||||
test("a frozen account wins over a bad request body", async () => {
|
||||
const frozen = deps({
|
||||
parallel: true,
|
||||
readPendingDeletion: async () => Response.json({ error: "账号正在注销" }, { status: 423 }),
|
||||
readBody: async () => ({ success: false as const, error: { flatten: () => ({}) } }),
|
||||
});
|
||||
const head = await loadConsultTurnHead(frozen.deps);
|
||||
assert.equal(head.ok, false);
|
||||
if (!head.ok) {
|
||||
assert.equal(head.frozen, true);
|
||||
assert.equal(head.response.status, 423);
|
||||
}
|
||||
});
|
||||
|
||||
test("parallel head overlaps deletion with session and model", async () => {
|
||||
const delays = { deletion: 50, session: 80, model: 80 };
|
||||
async function run(parallel: boolean) {
|
||||
let now = 0;
|
||||
const waiters: Array<{ at: number; resolve: () => void }> = [];
|
||||
function sleep(ms: number) {
|
||||
return new Promise<void>((resolve) => {
|
||||
waiters.push({ at: now + ms, resolve });
|
||||
});
|
||||
}
|
||||
let deletionStarted = 0;
|
||||
let chainStarted = 0;
|
||||
let inFlight = 0;
|
||||
let maxInFlight = 0;
|
||||
let finished = false;
|
||||
async function occupy(ms: number, mark: () => void) {
|
||||
mark();
|
||||
inFlight += 1;
|
||||
maxInFlight = Math.max(maxInFlight, inFlight);
|
||||
await sleep(ms);
|
||||
inFlight -= 1;
|
||||
}
|
||||
const pending = loadConsultTurnHead({
|
||||
parallel,
|
||||
getUser: async () => ({ data: { user: { id: "user-1" } }, error: null }),
|
||||
readPendingDeletion: () => occupy(delays.deletion, () => { deletionStarted = now; }).then(() => null),
|
||||
readBody: async () => ({ success: true as const, data: { sessionId: "s", modelId: "m" } }),
|
||||
readSession: () => occupy(delays.session, () => { chainStarted = now; }).then(() => ({ data: okSession(), error: null })),
|
||||
resolveModel: () => occupy(delays.model, () => {}).then(() => ({ id: "m" })),
|
||||
}).then((head) => {
|
||||
finished = true;
|
||||
return head;
|
||||
});
|
||||
for (let guard = 0; guard < 40 && !finished; guard += 1) {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
if (!waiters.length) continue;
|
||||
waiters.sort((left, right) => left.at - right.at);
|
||||
now = waiters[0]!.at;
|
||||
const due: Array<() => void> = [];
|
||||
while (waiters.length && waiters[0]!.at <= now) due.push(waiters.shift()!.resolve);
|
||||
for (const resolve of due) resolve();
|
||||
}
|
||||
if (!finished) throw new Error("clock did not settle");
|
||||
const head = await pending;
|
||||
assert.equal(head.ok, true);
|
||||
return { elapsed: now, deletionStarted, chainStarted, maxInFlight };
|
||||
}
|
||||
const on = await run(true);
|
||||
const off = await run(false);
|
||||
assert.ok(on.elapsed < 230, `parallel elapsed ${on.elapsed}`);
|
||||
assert.equal(on.maxInFlight, 2);
|
||||
assert.equal(off.maxInFlight, 1);
|
||||
assert.equal(on.deletionStarted, on.chainStarted);
|
||||
assert.ok(off.chainStarted >= delays.deletion);
|
||||
assert.ok(off.elapsed > on.elapsed);
|
||||
});
|
||||
@@ -138,7 +138,7 @@ type PlanResult = Record<string, unknown> & {
|
||||
|
||||
async function runDomainPlan(
|
||||
domains: string[],
|
||||
runWorkflow: (theme: string) => ReturnType<typeof workflow>,
|
||||
runWorkflow: (theme: string) => ReturnType<typeof workflow> | Promise<ReturnType<typeof workflow>>,
|
||||
options: { now?: () => number; requestId?: string } = {},
|
||||
) {
|
||||
const state = createConsultationRuntimeState();
|
||||
@@ -623,6 +623,63 @@ test("the single-value domain form stays available to callers without the model
|
||||
assert.throws(() => canonicalDomainPlan({ theme: "prashna" }, {}), /unsupported_consultation_domain/);
|
||||
});
|
||||
|
||||
test("two domains stay in plan order when concurrency is 2 and the second finishes first", async () => {
|
||||
const previous = process.env.CONSULT_DOMAIN_CONCURRENCY;
|
||||
process.env.CONSULT_DOMAIN_CONCURRENCY = "2";
|
||||
const finished: string[] = [];
|
||||
try {
|
||||
const { result } = await runDomainPlan(["career", "marriage"], async (theme) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, theme === "career" ? 40 : 5));
|
||||
finished.push(theme);
|
||||
return workflow(theme);
|
||||
});
|
||||
assert.deepEqual(finished, ["marriage", "career"]);
|
||||
assert.deepEqual(result.domains, ["career", "marriage"]);
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.CONSULT_DOMAIN_CONCURRENCY;
|
||||
else process.env.CONSULT_DOMAIN_CONCURRENCY = previous;
|
||||
}
|
||||
});
|
||||
|
||||
test("a 429 from one parallel domain keeps workflow_rate_limited", async () => {
|
||||
const previous = process.env.CONSULT_DOMAIN_CONCURRENCY;
|
||||
process.env.CONSULT_DOMAIN_CONCURRENCY = "2";
|
||||
try {
|
||||
await assert.rejects(
|
||||
() => runDomainPlan(["career", "marriage"], async () => {
|
||||
throw new ConsultationWorkflowError("workflow_rate_limited", "busy");
|
||||
}),
|
||||
(error: unknown) => {
|
||||
assert.equal(consultationToolFailureCode(error), "workflow_rate_limited");
|
||||
return true;
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.CONSULT_DOMAIN_CONCURRENCY;
|
||||
else process.env.CONSULT_DOMAIN_CONCURRENCY = previous;
|
||||
}
|
||||
});
|
||||
|
||||
test("concurrency 1 still runs domains one at a time", async () => {
|
||||
const previous = process.env.CONSULT_DOMAIN_CONCURRENCY;
|
||||
process.env.CONSULT_DOMAIN_CONCURRENCY = "1";
|
||||
let active = 0;
|
||||
let maxActive = 0;
|
||||
try {
|
||||
await runDomainPlan(["career", "marriage"], async (theme) => {
|
||||
active += 1;
|
||||
maxActive = Math.max(maxActive, active);
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
active -= 1;
|
||||
return workflow(theme);
|
||||
});
|
||||
assert.equal(maxActive, 1);
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.CONSULT_DOMAIN_CONCURRENCY;
|
||||
else process.env.CONSULT_DOMAIN_CONCURRENCY = previous;
|
||||
}
|
||||
});
|
||||
|
||||
test("daily entrypoint ignores a model domain rewrite instead of executing it", async () => {
|
||||
const calls: string[] = [];
|
||||
const state = createConsultationRuntimeState();
|
||||
|
||||
@@ -239,8 +239,15 @@ test("a blocked answer is replaced in place and onComplete settles the notice",
|
||||
|
||||
test("routes check input before any charge and output before settlement", () => {
|
||||
const consult = read("../src/app/api/consult/route.ts");
|
||||
assert.ok(consult.indexOf('await moderate(visibleQuestion, "input"') > 0);
|
||||
assert.ok(consult.indexOf('await moderate(visibleQuestion, "input"') < consult.indexOf("reserve_consultation_usage"), "input check precedes the reservation");
|
||||
const prepare = read("../src/lib/consultation-route-service.ts");
|
||||
// 原值: 源码含 await moderate(visibleQuestion, "input" 且位置早于 reserve_consultation_usage
|
||||
// 新值: beforeReserve 里 await settleInputModeration(),而 prepare 里 beforeReserve 早于 input.reserve()
|
||||
// 原因: 审核从开流前挪到流内、预留前;开关关掉时仍在开流前 await moderate
|
||||
assert.match(consult, /await settleInputModeration\(\)/);
|
||||
assert.ok(consult.indexOf("await settleInputModeration()") < consult.indexOf("reserve_consultation_usage"));
|
||||
const beforeReserve = prepare.indexOf("input.beforeReserve");
|
||||
const reserveCall = prepare.indexOf("input.reserve()");
|
||||
assert.ok(beforeReserve > 0 && beforeReserve < reserveCall);
|
||||
assert.equal((consult.match(/moderateOutput: moderateAnswer/g) ?? []).length, 3, "every agentic answer stream is checked");
|
||||
assert.match(consult, /if \(!outputModerated\) await moderateAnswer\(reply\.text\);\n\s+if \(outputBlocked\) return await completeModeratedResponse\(usage\);/);
|
||||
assert.match(consult, /complete_consultation_moderated/);
|
||||
|
||||
@@ -45,9 +45,18 @@ test("catalog cache is short-lived and failure stays fail-closed without env fal
|
||||
test("session model resolution is async and pins published or retired database versions", () => {
|
||||
assert.match(catalog, /export async function resolveSessionLanguageModel/);
|
||||
assert.match(catalog, /c\.model_id=\$1 and v\.version=\$2 and v\.status in \('published','retired'\)/);
|
||||
assert.match(catalog, /return await readVersion\(modelId, configVersion\)/);
|
||||
// 原值: return await readVersion(modelId, configVersion)
|
||||
// 新值: const model = await readVersion(modelId, configVersion),只有非空结果写入短缓存
|
||||
// 原因: 绑版本的解析与目录共用 15 秒上限,失败和空结果不缓存
|
||||
assert.match(catalog, /const model = await readVersion\(modelId, configVersion\)/);
|
||||
assert.match(catalog, /if \(model && ttl > 0\) cache\.set\(key, \{ expiresAt: Date\.now\(\) \+ ttl, model \}\)/);
|
||||
assert.match(routes.consult, /await resolveSessionLanguageModel\(\s*chatSession\.model_id,\s*chatSession\.model_config_version/);
|
||||
assert.match(routes.consult, /if \(!sessionModel\)[\s\S]*status: 503/);
|
||||
// 原值: route.ts 里 if (!sessionModel) 之后是 status 503
|
||||
// 新值: turn-prep.ts 里 if (!sessionModel) 返回 modelUnavailable(),正文 status 503
|
||||
// 原因: 模型不可用仍是开流前的 503,文案随准备步骤一起搬走
|
||||
const head = readFileSync(new URL("../src/lib/consult/turn-prep.ts", import.meta.url), "utf8");
|
||||
assert.match(head, /if \(!sessionModel\) return \{ ok: false, response: modelUnavailable\(\) \}/);
|
||||
assert.match(head, /function modelUnavailable\(\)[\s\S]*status: 503/);
|
||||
assert.doesNotMatch(routes.consult, /languageModelConfigurationMessage/);
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
const read = (relativePath: string) => readFileSync(new URL(relativePath, import.meta.url), "utf8");
|
||||
const run = read("../src/hooks/use-consultation-run.ts");
|
||||
const sessions = read("../src/hooks/use-session-management.ts");
|
||||
const consultRoute = read("../src/app/api/consult/route.ts");
|
||||
const pageSource = read("../src/app/(app)/page.tsx");
|
||||
const sendFn = run.slice(run.indexOf("async function send("), run.indexOf('await fetch("/api/consult"'));
|
||||
|
||||
@@ -50,13 +49,16 @@ test("pending consult replay pointing at a rectification session is dropped", ()
|
||||
});
|
||||
|
||||
test("consult POST splits missing session from wrong session type", () => {
|
||||
assert.match(consultRoute, /if \(!chatSession\) \{[\s\S]*咨询会话不存在[\s\S]*status: 404/);
|
||||
assert.match(
|
||||
consultRoute,
|
||||
/if \(chatSession\.session_type !== "consultation"\) \{[\s\S]*session_not_consultation[\s\S]*status: 409/,
|
||||
);
|
||||
assert.match(consultRoute, /这是生时校正会话/);
|
||||
assert.match(consultRoute, /请在生时校正里继续。/);
|
||||
const head = readFileSync(new URL("../src/lib/consult/turn-prep.ts", import.meta.url), "utf8");
|
||||
// 原值: consult route 里 if (!chatSession) 返回 404,类型不对返回 session_not_consultation。
|
||||
// 新值: 同样两句在 turn-prep.ts,route 把读会话的结果交给它。
|
||||
// 原因: 开流前的会话判断收进准备函数,文案和状态码没变。
|
||||
assert.match(head, /if \(!session\.data\) return \{ ok: false, response: sessionMissing\(\) \}/);
|
||||
assert.match(head, /function sessionMissing\(\)[\s\S]*咨询会话不存在[\s\S]*status: 404/);
|
||||
assert.match(head, /function notConsultation\(\)[\s\S]*session_not_consultation[\s\S]*status: 409/);
|
||||
assert.match(head, /session_type !== "consultation"/);
|
||||
assert.match(head, /这是生时校正会话/);
|
||||
assert.match(head, /请在生时校正里继续。/);
|
||||
assert.equal(SESSION_NOT_CONSULTATION_CODE, "session_not_consultation");
|
||||
assert.match(run, /caught\.code === SESSION_NOT_CONSULTATION_CODE/);
|
||||
});
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export {};
|
||||
@@ -0,0 +1,8 @@
|
||||
const empty = new URL("./server-only-empty.mjs", import.meta.url).href;
|
||||
|
||||
export async function resolve(specifier, context, nextResolve) {
|
||||
if (specifier === "server-only") {
|
||||
return { url: empty, shortCircuit: true };
|
||||
}
|
||||
return nextResolve(specifier, context);
|
||||
}
|
||||
Reference in New Issue
Block a user