Files
Jyotisha/frontend/tests/consult-prep-gate-20261013.test.ts
T
jesse-uxandGrok (agent loop) a0e223e20c 发问准备可以并行,三个开关先保持关闭(BUG-1337~1339)
开流前的注销检查可以和读会话、解析模型同时进行;审核改到扣点前等待,不再挡住第一行;模型版本可以短缓存;两个领域可以一起算。本机没有数据库中位数,也没有带星盘网络库的镜像,所以默认仍是关闭、缓存 0、领域逐个算。

自测:tsc 0 错;lint 0 error;相关测试失败 0、取消 0。test:db 未跑。

Co-Authored-By: Grok (agent loop)
2026-10-14 07:23:58 +08:00

165 lines
6.7 KiB
TypeScript

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);
});