开流前的注销检查可以和读会话、解析模型同时进行;审核改到扣点前等待,不再挡住第一行;模型版本可以短缓存;两个领域可以一起算。本机没有数据库中位数,也没有带星盘网络库的镜像,所以默认仍是关闭、缓存 0、领域逐个算。 自测:tsc 0 错;lint 0 error;相关测试失败 0、取消 0。test:db 未跑。 Co-Authored-By: Grok (agent loop)
153 lines
5.7 KiB
TypeScript
153 lines
5.7 KiB
TypeScript
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);
|
|
});
|