fix: speed up rectification start
This commit is contained in:
@@ -1353,3 +1353,18 @@
|
||||
- 防复发:数据库 JSON 合同的日期格式只维护一组精度分支;新增生产迁移时必须执行真实 PostgreSQL 函数调用,而不是只做 SQL 文本断言。
|
||||
- 相关记录:BUG-067、BUG-070
|
||||
- 修复版本:本次修复提交(生产已向前迁移)
|
||||
|
||||
## BUG-072 | 生时校正首轮在零事件时执行全范围扫描并阻塞输入
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-07-25
|
||||
- 最近更新:2026-07-25
|
||||
- 影响面:首页生时校正入口、`start` 编排、首次会话持久化与咨询问题 handoff
|
||||
- 用户现象:从首页进入生时校正后长时间停留在加载状态;未知时间或宽时段资料最明显,首问出现后输入框也可能继续等待会话同步。
|
||||
- 触发条件:新建生时校正 case,尤其声明时间为全天未知或宽时段;首页没有待转交咨询问题时也会读取 durable handoff。
|
||||
- 根因:`start` 在尚无人生事件时仍构建 Technical Packet,按声明范围逐分钟重排,随后再调用叙事模型生成固定性质的首问;前端收到首轮后又串行等待 `persistSession()`,并且直接首页入口无条件读取 durable handoff。
|
||||
- 修复:`start` 只按已声明时间范围创建确定性首轮和待验证候选,不做分钟扫描或模型生成;第一条有效事件回答后才进入原技术计算与叙事链。首页先展示首轮并开放输入,再后台同步会话;只有存在本地或显式待转交问题时才读取 durable handoff。
|
||||
- 验证:Orchestrator、入口、客户端、路由、持久化与同会话写入队列聚焦测试 144/144 通过;目标 ESLint 与 `git diff --check` 通过。覆盖首轮不扫描/不生成叙事、先展示后按序后台持久化,以及无 handoff 时跳过 durable 读取。
|
||||
- 防复发:零证据首轮不得执行分钟级技术计算或模型调用;非关键会话同步不得阻塞已持久化 case 的首轮交互;可选 handoff 读取必须由实际 handoff 状态触发。
|
||||
- 相关记录:BUG-055、BUG-065、BUG-067
|
||||
- 修复版本:待提交(本地可测)
|
||||
|
||||
+23
-19
@@ -2087,10 +2087,11 @@ export default function Home() {
|
||||
: resumableSession ?? createSession(modelCatalog.defaultModelId, "birth_time_rectification");
|
||||
const reusingRectificationSession = canReuseSourceRectificationSession
|
||||
|| resumableSession !== null;
|
||||
const localHandoff = rectificationQuestionHandoff.current.peek();
|
||||
const requestedQuestion = pendingConsultationQuestion
|
||||
?? (reusingRectificationSession
|
||||
? null
|
||||
: rectificationQuestionHandoff.current.peek()?.question)
|
||||
: localHandoff?.question)
|
||||
?? null;
|
||||
rectificationOpenInFlight.current = true;
|
||||
setDraft("");
|
||||
@@ -2116,7 +2117,9 @@ export default function Home() {
|
||||
try {
|
||||
let turn: ConversationalRectificationTurn;
|
||||
if (!resumeTarget) {
|
||||
const durable = await durableRectificationQuestionHandoff.current.load();
|
||||
const durable = requestedQuestion !== null || localHandoff !== null
|
||||
? await durableRectificationQuestionHandoff.current.load()
|
||||
: null;
|
||||
if (durable && durable.status !== "consumed") {
|
||||
turn = durable.turn;
|
||||
} else {
|
||||
@@ -2226,26 +2229,27 @@ export default function Home() {
|
||||
? rectificationSession.updatedAt
|
||||
: timestamp(),
|
||||
};
|
||||
let sessionSyncFailed = false;
|
||||
if (!reusingRectificationSession) {
|
||||
try {
|
||||
await persistSession(boundSession, "create");
|
||||
} catch {
|
||||
sessionSyncFailed = true;
|
||||
}
|
||||
setSessions((current) => [boundSession, ...current.filter((session) => session.id !== boundSession.id)]);
|
||||
} else if (boundSession !== rectificationSession) {
|
||||
updateSession(rectificationSession.id, () => boundSession);
|
||||
try {
|
||||
await persistSession(boundSession);
|
||||
} catch {
|
||||
sessionSyncFailed = true;
|
||||
}
|
||||
}
|
||||
setRectificationInitialTurn(turn);
|
||||
setRectificationOpeningAssistantText("");
|
||||
synchronizeRectificationQuestion(turn, sourceSession);
|
||||
setComposerNotice(sessionSyncFailed ? "校正已经开始,但会话关联暂时未同步到云端。" : "");
|
||||
setComposerNotice("");
|
||||
if (!reusingRectificationSession) {
|
||||
setSessions((current) => [boundSession, ...current.filter((session) => session.id !== boundSession.id)]);
|
||||
void rectificationPersistence.current.enqueue(
|
||||
boundSession.id,
|
||||
() => persistSession(boundSession, "create"),
|
||||
).catch(() => {
|
||||
setComposerNotice("校正已经开始,但会话关联暂时未同步到云端。");
|
||||
});
|
||||
} else if (boundSession !== rectificationSession) {
|
||||
updateSession(rectificationSession.id, () => boundSession);
|
||||
void rectificationPersistence.current.enqueue(
|
||||
boundSession.id,
|
||||
() => persistSession(boundSession),
|
||||
).catch(() => {
|
||||
setComposerNotice("校正已经开始,但会话关联暂时未同步到云端。");
|
||||
});
|
||||
}
|
||||
} catch (caught) {
|
||||
const message = caught instanceof Error
|
||||
? caught.message
|
||||
|
||||
@@ -559,6 +559,89 @@ function midpointOfRange(range: Readonly<{ startTime: string; endTime: string }>
|
||||
return clock(Math.round((start + end) / 2));
|
||||
}
|
||||
|
||||
function declaredRange(input: DeclaredBirthInput): Readonly<{ startTime: string; endTime: string }> {
|
||||
if (input.source === "period_only") {
|
||||
return {
|
||||
early_morning: { startTime: "04:00", endTime: "07:59" },
|
||||
morning: { startTime: "08:00", endTime: "11:59" },
|
||||
afternoon: { startTime: "12:00", endTime: "17:59" },
|
||||
evening: { startTime: "18:00", endTime: "22:59" },
|
||||
late_night: { startTime: "23:00", endTime: "03:59" },
|
||||
}[input.reportedPeriod];
|
||||
}
|
||||
if (input.source === "unknown") return { startTime: "00:00", endTime: "23:59" };
|
||||
if (input.source === "legacy_import" && !input.reportedTime) {
|
||||
return input.reportedPeriod
|
||||
? declaredRange({ ...input, source: "period_only", reportedPeriod: input.reportedPeriod })
|
||||
: { startTime: "00:00", endTime: "23:59" };
|
||||
}
|
||||
if (!input.reportedTime) throw new ConversationalRectificationError("profile_incomplete");
|
||||
const minute = (value: string) => {
|
||||
const [hour = 0, part = 0] = value.split(":").map(Number);
|
||||
return hour * 60 + part;
|
||||
};
|
||||
const clock = (value: number) => {
|
||||
const normalized = ((value % 1_440) + 1_440) % 1_440;
|
||||
return `${String(Math.floor(normalized / 60)).padStart(2, "0")}:${String(normalized % 60).padStart(2, "0")}`;
|
||||
};
|
||||
return {
|
||||
startTime: clock(minute(input.reportedTime) - (input.uncertaintyBeforeMinutes ?? 2)),
|
||||
endTime: clock(minute(input.reportedTime) + (input.uncertaintyAfterMinutes ?? 2)),
|
||||
};
|
||||
}
|
||||
|
||||
function openingRectificationState(input: {
|
||||
readonly caseId: string;
|
||||
readonly declaredBirthInput: DeclaredBirthInput;
|
||||
readonly pendingConsultationQuestion: string | null;
|
||||
}) {
|
||||
const range = declaredRange(input.declaredBirthInput);
|
||||
const representativeTime = midpointOfRange(range);
|
||||
const calculationVersion = "rectification-opening-v1";
|
||||
const turn = conversationalRectificationTurnSchema.parse({
|
||||
caseId: input.caseId,
|
||||
journeyProtocol: "conversational-evidence-v3",
|
||||
status: "active",
|
||||
turnVersion: 0,
|
||||
narrative: "我们先从一件时间最明确、影响比较大的真实经历开始。请只说一件,并告诉我大约发生在哪一年、哪一月?",
|
||||
candidate: {
|
||||
status: "pending_validation",
|
||||
representativeTime,
|
||||
rangeStart: range.startTime,
|
||||
rangeEnd: range.endTime,
|
||||
},
|
||||
technicalReceipt: {
|
||||
calculationVersion,
|
||||
stableLayers: [],
|
||||
sensitiveLayers: [],
|
||||
candidateDifferenceRefs: [],
|
||||
},
|
||||
evidenceRequest: {
|
||||
domains: ["career", "education", "relocation", "relationship"],
|
||||
datePrecision: "month_preferred",
|
||||
freeTextAllowed: true,
|
||||
prompt: "请说一件已经发生、时间比较明确的重要经历,并告诉我大约是哪一年、哪一月?",
|
||||
followUp: { kind: "new_event", evidenceId: null },
|
||||
},
|
||||
evidenceRecap: [],
|
||||
actions: ["answer", "pause", "abandon"],
|
||||
pendingConsultationQuestion: input.pendingConsultationQuestion,
|
||||
});
|
||||
const privateCandidate = privateCandidateSchema.parse({
|
||||
resultId: null,
|
||||
representativeTime,
|
||||
rangeStart: range.startTime,
|
||||
rangeEnd: range.endTime,
|
||||
calculationVersion,
|
||||
workingState: { phase: "collecting_evidence", iteration: 0, notes: [] },
|
||||
});
|
||||
return {
|
||||
turn,
|
||||
privateCandidate,
|
||||
validationReceipt: transitionReceipt("deterministic-rectification-opening"),
|
||||
};
|
||||
}
|
||||
|
||||
type CorrectionResetReason =
|
||||
| "needs_clarification"
|
||||
| "non_scoreable"
|
||||
@@ -1193,33 +1276,10 @@ export function createConversationalRectificationService(
|
||||
});
|
||||
reserved = reservation.billingState === "reserved";
|
||||
if (reserved) lastTelemetryOutcome = { billingState: "unknown", caseStatus: null };
|
||||
const computed = await ports.buildTechnicalPacket({
|
||||
userId,
|
||||
const opening = openingRectificationState({
|
||||
caseId,
|
||||
asOfDate: ports.asOfDate(),
|
||||
declaredBirthInput: declared.data,
|
||||
privateCandidate: null,
|
||||
evidence: [],
|
||||
});
|
||||
const gatedPacket = confirmationGatedPacket(computed.packet, 0, 0);
|
||||
const narrative = await generateRectificationNarrative({
|
||||
phase: "first",
|
||||
packet: gatedPacket,
|
||||
generator: ports.narrativeGenerator,
|
||||
});
|
||||
const privateCandidate = privateCandidateFromPacket({
|
||||
packet: gatedPacket,
|
||||
resultId: null,
|
||||
iteration: 0,
|
||||
forceCollecting: true,
|
||||
});
|
||||
const firstTurn = turnFromNarrative({
|
||||
caseId,
|
||||
turnVersion: 0,
|
||||
pendingConsultationQuestion: command.pendingConsultationQuestion ?? null,
|
||||
packet: gatedPacket,
|
||||
narrative,
|
||||
evidence: [],
|
||||
declaredBirthInput: declared.data,
|
||||
});
|
||||
const created = await ports.store.createCaseWithFirstTurn({
|
||||
userId,
|
||||
@@ -1229,9 +1289,9 @@ export function createConversationalRectificationService(
|
||||
revisionOfCaseId: profile.revisionOfCaseId,
|
||||
pendingConsultationQuestion: command.pendingConsultationQuestion ?? null,
|
||||
declaredBirthInput: declared.data,
|
||||
firstTurn,
|
||||
validationReceipt: narrative.validationReceipt,
|
||||
privateCandidate,
|
||||
firstTurn: opening.turn,
|
||||
validationReceipt: opening.validationReceipt,
|
||||
privateCandidate: opening.privateCandidate,
|
||||
});
|
||||
observeCase(created, "unknown");
|
||||
await ports.billing.complete({
|
||||
|
||||
@@ -140,6 +140,32 @@ test("homepage birth-time card opens its dedicated session before the first turn
|
||||
assert.match(source, /rectificationSurfaceOpen && \(!visibleRectificationTurn && rectificationError \? \([\s\S]*?<ConversationalBirthTimeRectification[\s\S]*?initialTurn=\{visibleRectificationTurn\}[\s\S]*?openingAssistantText=\{rectificationOpeningAssistantText\}/);
|
||||
});
|
||||
|
||||
test("the first rectification turn becomes interactive before session persistence finishes", () => {
|
||||
const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
|
||||
const start = source.indexOf("async function openBirthTimeRectification");
|
||||
const end = source.indexOf("function handleConversationalRectificationTurn", start);
|
||||
const handler = source.slice(start, end);
|
||||
const turnVisible = handler.indexOf("setRectificationInitialTurn(turn)");
|
||||
const backgroundPersist = handler.indexOf("void rectificationPersistence.current.enqueue(");
|
||||
|
||||
assert.ok(turnVisible >= 0);
|
||||
assert.ok(backgroundPersist > turnVisible);
|
||||
assert.match(handler, /void rectificationPersistence\.current\.enqueue\([\s\S]*?\(\) => persistSession\([\s\S]*?\.catch\(\(\) => \{[\s\S]*?校正已经开始,但会话关联暂时未同步到云端。/);
|
||||
});
|
||||
|
||||
test("a direct homepage start skips the durable handoff read when no question was handed off", () => {
|
||||
const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
|
||||
const start = source.indexOf("async function openBirthTimeRectification");
|
||||
const end = source.indexOf("function handleConversationalRectificationTurn", start);
|
||||
const handler = source.slice(start, end);
|
||||
|
||||
assert.match(handler, /const localHandoff = rectificationQuestionHandoff\.current\.peek\(\)/);
|
||||
assert.match(
|
||||
handler,
|
||||
/const durable = requestedQuestion !== null \|\| localHandoff !== null\s*\? await durableRectificationQuestionHandoff\.current\.load\(\)\s*:\s*null/,
|
||||
);
|
||||
});
|
||||
|
||||
test("rectification cards render only inside the active rectification session", () => {
|
||||
const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
|
||||
|
||||
|
||||
@@ -211,6 +211,7 @@ type MutableCase = {
|
||||
};
|
||||
|
||||
function harness(options: {
|
||||
readonly createFailure?: Error;
|
||||
readonly packetFailure?: Error;
|
||||
readonly packetFailureFromBuild?: number;
|
||||
readonly completeFailures?: number;
|
||||
@@ -330,6 +331,7 @@ function harness(options: {
|
||||
async createCaseWithFirstTurn(input) {
|
||||
events.push("create");
|
||||
mutations.push("create");
|
||||
if (options.createFailure) throw options.createFailure;
|
||||
return replay(input.actionId, input, () => {
|
||||
const row = stored({
|
||||
userId: input.userId,
|
||||
@@ -508,18 +510,30 @@ async function start(value: ReturnType<typeof harness>, pendingConsultationQuest
|
||||
});
|
||||
}
|
||||
|
||||
test("start validates profile, reads server price, reserves, computes, saves, then charges", async () => {
|
||||
test("start creates a deterministic opening without scanning or narrative generation", async () => {
|
||||
const value = harness();
|
||||
const turn = await start(value);
|
||||
|
||||
assert.deepEqual(value.events, ["profile", "price", "reserve:9", "packet", "narrative", "create", "complete"]);
|
||||
assert.deepEqual(value.events, ["profile", "price", "reserve:9", "create", "complete"]);
|
||||
assert.equal(value.counts().packetBuilds, 0);
|
||||
assert.equal(turn.caseId, startActionId);
|
||||
assert.equal(turn.turnVersion, 0);
|
||||
assert.equal(turn.pendingConsultationQuestion, "我的工作何时变化?");
|
||||
assert.deepEqual(turn.technicalReceipt.sensitiveLayers, ["D9", "D10"]);
|
||||
assert.equal(turn.candidate.status, "pending_validation");
|
||||
assert.equal(turn.candidate.representativeTime, "05:20");
|
||||
assert.equal(turn.candidate.rangeStart, "04:50");
|
||||
assert.equal(turn.candidate.rangeEnd, "05:50");
|
||||
assert.deepEqual(turn.technicalReceipt, {
|
||||
calculationVersion: "rectification-opening-v1",
|
||||
stableLayers: [],
|
||||
sensitiveLayers: [],
|
||||
candidateDifferenceRefs: [],
|
||||
});
|
||||
assert.equal(turn.evidenceRequest?.followUp?.kind, "new_event");
|
||||
assert.equal(JSON.stringify(turn).includes("candidateWeights"), false);
|
||||
assert.equal(value.cases.get(startActionId)?.row.revisionOfCaseId, priorCaseId);
|
||||
assert.equal(value.cases.get(startActionId)?.row.baselineActiveTime, "04:58");
|
||||
assert.equal(value.cases.get(startActionId)?.row.validationReceipts[0]?.modelId, "deterministic-rectification-opening");
|
||||
});
|
||||
|
||||
test("start rejects a client price before profile, billing, or calculation", async () => {
|
||||
@@ -533,11 +547,11 @@ test("start rejects a client price before profile, billing, or calculation", asy
|
||||
|
||||
test("every post-reservation failure releases exactly once and never leaks its cause", async () => {
|
||||
const raw = "SQL model browser secret detail";
|
||||
const value = harness({ packetFailure: new Error(raw) });
|
||||
const value = harness({ createFailure: new Error(raw) });
|
||||
await assert.rejects(start(value), (error: unknown) => error instanceof ConversationalRectificationError
|
||||
&& error.code === "service_unavailable" && !error.message.includes(raw));
|
||||
assert.equal(value.counts().releaseCount, 1);
|
||||
assert.deepEqual(value.events, ["profile", "price", "reserve:9", "packet", "release"]);
|
||||
assert.deepEqual(value.events, ["profile", "price", "reserve:9", "create", "release"]);
|
||||
});
|
||||
|
||||
test("a start retry settles an existing reservation without reserving or computing again", async () => {
|
||||
@@ -550,7 +564,7 @@ test("a start retry settles an existing reservation without reserving or computi
|
||||
|
||||
assert.equal(replayed.status, "active");
|
||||
assert.equal(value.cases.get(startActionId)?.row.billingState, "charged");
|
||||
assert.deepEqual(value.counts(), { packetBuilds: 1, reserveCount: 1, releaseCount: 1 });
|
||||
assert.deepEqual(value.counts(), { packetBuilds: 0, reserveCount: 1, releaseCount: 1 });
|
||||
assert.deepEqual(value.events.slice(-3), ["profile", "price", "complete"]);
|
||||
});
|
||||
|
||||
@@ -568,7 +582,7 @@ test("a duplicate start with the same declared birth input reuses the unfinished
|
||||
assert.deepEqual(replayed, first);
|
||||
assert.deepEqual(value.counts(), countsBeforeRetry);
|
||||
assert.deepEqual(value.events, [
|
||||
"profile", "price", "reserve:9", "packet", "narrative", "create", "complete",
|
||||
"profile", "price", "reserve:9", "create", "complete",
|
||||
"profile", "price",
|
||||
]);
|
||||
});
|
||||
@@ -595,7 +609,7 @@ test("a duplicate start with changed declared birth input remains a conflict", a
|
||||
}), (error: unknown) => error instanceof ConversationalRectificationError
|
||||
&& error.code === "action_conflict");
|
||||
assert.equal(value.counts().reserveCount, 1);
|
||||
assert.equal(value.counts().packetBuilds, 1);
|
||||
assert.equal(value.counts().packetBuilds, 0);
|
||||
});
|
||||
|
||||
test("a settlement failure releases its created case once and cannot replay as success", async () => {
|
||||
@@ -607,7 +621,7 @@ test("a settlement failure releases its created case once and cannot replay as s
|
||||
|
||||
await assert.rejects(start(value), (error: unknown) => error instanceof ConversationalRectificationError
|
||||
&& error.code === "billing_failed");
|
||||
assert.deepEqual(value.counts(), { packetBuilds: 1, reserveCount: 1, releaseCount: 1 });
|
||||
assert.deepEqual(value.counts(), { packetBuilds: 0, reserveCount: 1, releaseCount: 1 });
|
||||
});
|
||||
|
||||
test("clear historical evidence is extracted, scored, narrated, recapped, and atomically saved", async () => {
|
||||
@@ -621,7 +635,7 @@ test("clear historical evidence is extracted, scored, narrated, recapped, and at
|
||||
answer: "2018年6月毕业,2019年7月开始第一份工作,2020年3月去外地工作,2022年8月结婚",
|
||||
});
|
||||
|
||||
assert.equal(value.counts().packetBuilds, 2);
|
||||
assert.equal(value.counts().packetBuilds, 1);
|
||||
assert.equal(turn.status, "confirming");
|
||||
assert.equal(turn.candidate.status, "ready_for_confirmation");
|
||||
assert.equal(turn.turnVersion, 1);
|
||||
@@ -685,7 +699,7 @@ test("evidence corrections are append-only while recap and scoring use only the
|
||||
domain: "career",
|
||||
isCorrection: true,
|
||||
}]);
|
||||
assert.deepEqual(value.packetEvidenceCounts, [0, 1, 1, 1]);
|
||||
assert.deepEqual(value.packetEvidenceCounts, [1, 1, 1]);
|
||||
});
|
||||
|
||||
test("uses Agent semantic classification when a single event falls through the deterministic keywords", async () => {
|
||||
@@ -732,7 +746,7 @@ test("repeated evidence remains auditable but identical date-domain-semantics sc
|
||||
const stored = value.cases.get(startActionId)?.row.eventEvidence ?? [];
|
||||
assert.equal(stored.length, 2, "both user submissions remain in the audit ledger");
|
||||
assert.equal(repeated.evidenceRecap.length, 2);
|
||||
assert.deepEqual(value.packetEvidenceCounts, [0, 1, 1]);
|
||||
assert.deepEqual(value.packetEvidenceCounts, [1, 1]);
|
||||
assert.deepEqual(value.packetEvidenceIds.at(-1), [firstId]);
|
||||
assert.equal(repeated.status, "active");
|
||||
assert.equal(repeated.actions.includes("confirm"), false);
|
||||
@@ -771,13 +785,13 @@ test("an unclear correction immediately retires the wrong fact and stays retired
|
||||
value.cases.get(startActionId)?.row.privateCandidate.scoredHistoricalEvidence ?? [],
|
||||
[],
|
||||
);
|
||||
assert.deepEqual(value.packetEvidenceCounts, [0, 1, 0]);
|
||||
assert.deepEqual(value.packetEvidenceCounts, [1, 0]);
|
||||
|
||||
const later = await value.service.answer(userId, {
|
||||
type: "answer", caseId: startActionId, actionId: thirdAnswerActionId,
|
||||
turnVersion: 2, answer: "2022年3月搬家",
|
||||
});
|
||||
assert.deepEqual(value.packetEvidenceCounts, [0, 1, 0, 1]);
|
||||
assert.deepEqual(value.packetEvidenceCounts, [1, 0, 1]);
|
||||
assert.equal(later.evidenceRecap.some((item) => item.id === wrongId), false);
|
||||
assert.equal(later.evidenceRecap.some((item) => item.id === unclear?.id), true);
|
||||
});
|
||||
@@ -838,7 +852,7 @@ test("every non-confirmable correction rescans the declared range and withdraws
|
||||
});
|
||||
|
||||
test("a narrative fallback cannot discard a confirmation candidate produced by a valid correction", async () => {
|
||||
const value = harness({ invalidNarrativeFromGeneration: 3 });
|
||||
const value = harness({ invalidNarrativeFromGeneration: 2 });
|
||||
await start(value, null);
|
||||
await value.service.answer(userId, {
|
||||
type: "answer", caseId: startActionId, actionId: answerActionId,
|
||||
@@ -944,7 +958,6 @@ test("ordinary new evidence continues incrementally from the current candidate r
|
||||
});
|
||||
|
||||
assert.deepEqual(value.packetPrivateCandidates, [
|
||||
null,
|
||||
{ rangeStart: "04:50", rangeEnd: "05:50", resultId: null },
|
||||
{ rangeStart: "05:16", rangeEnd: "05:20", resultId: null },
|
||||
]);
|
||||
@@ -999,7 +1012,7 @@ test("generic date uncertainty does not suppress clear historical evidence", asy
|
||||
|
||||
assert.equal(turn.status, "active");
|
||||
assert.equal(turn.candidate.status, "pending_validation");
|
||||
assert.equal(value.counts().packetBuilds, 2);
|
||||
assert.equal(value.counts().packetBuilds, 1);
|
||||
assert.ok(value.events.includes("score-packet"));
|
||||
assert.ok((value.cases.get(startActionId)?.row.eventEvidence ?? [])
|
||||
.some((item) => item.eventSummary.includes("毕业") && item.scoreable === true));
|
||||
@@ -1617,7 +1630,7 @@ test("the next evidence request moves past a domain the user already answered",
|
||||
});
|
||||
|
||||
test("a rejected intermediate narrative returns a retryable error without saving a template turn", async () => {
|
||||
const value = harness({ invalidNarrativeFromGeneration: 2 });
|
||||
const value = harness({ invalidNarrativeFromGeneration: 1 });
|
||||
await start(value, null);
|
||||
|
||||
await assert.rejects(value.service.answer(userId, {
|
||||
@@ -1660,8 +1673,8 @@ test("one through three supported events save and narrate before the fourth accu
|
||||
assert.doesNotMatch(turn.narrative, /当前累计|本轮已纳入|本轮区分重点|下一步:/);
|
||||
}
|
||||
|
||||
assert.equal(value.counts().packetBuilds, 5);
|
||||
assert.equal(value.events.filter((event) => event === "narrative").length, 5);
|
||||
assert.equal(value.counts().packetBuilds, 4);
|
||||
assert.equal(value.events.filter((event) => event === "narrative").length, 4);
|
||||
});
|
||||
|
||||
test("intermediate narrative receives the complete active event ledger", async () => {
|
||||
@@ -1827,7 +1840,7 @@ test("family evidence remains stored and public without changing its domain", as
|
||||
dateLabel: "2020-07",
|
||||
domain: "family",
|
||||
}]);
|
||||
assert.deepEqual(value.packetEvidenceCounts, [0, 0]);
|
||||
assert.deepEqual(value.packetEvidenceCounts, [0]);
|
||||
assert.equal(turn.status, "active");
|
||||
});
|
||||
|
||||
@@ -1860,7 +1873,7 @@ test("three valid events plus pre-birth evidence wait until a later valid fourth
|
||||
assert.equal(preBirth?.extractionStatus, "needs_clarification");
|
||||
assert.equal(stored?.eventEvidence.length, 5);
|
||||
assert.equal(latest?.evidenceRecap.length, 5);
|
||||
assert.deepEqual(value.packetEvidenceCounts, [0, 1, 2, 2, 3, 4]);
|
||||
assert.deepEqual(value.packetEvidenceCounts, [1, 2, 2, 3, 4]);
|
||||
});
|
||||
|
||||
test("vague, future, and unmatched answers stay conversational and never score", async () => {
|
||||
@@ -1879,7 +1892,7 @@ test("vague, future, and unmatched answers stay conversational and never score",
|
||||
answer,
|
||||
...(domain ? { domain } : {}),
|
||||
});
|
||||
assert.equal(value.counts().packetBuilds, 2);
|
||||
assert.equal(value.counts().packetBuilds, 1);
|
||||
assert.equal(turn.status, "active");
|
||||
assert.match(turn.narrative, /哪一年|哪一月|年月|已发生|已经发生|换个方向|未来/);
|
||||
assert.doesNotMatch(turn.narrative, /好的,我们不沿用不符合你的方向|已保存这段描述|我已保存你的原话|这条更正已保存/);
|
||||
@@ -1891,7 +1904,7 @@ test("vague, future, and unmatched answers stay conversational and never score",
|
||||
test("a non-scoring packet failure responds to the current turn instead of replaying the prior agent message", async () => {
|
||||
const value = harness({
|
||||
packetFailure: new Error("synthetic packet outage"),
|
||||
packetFailureFromBuild: 2,
|
||||
packetFailureFromBuild: 1,
|
||||
});
|
||||
const initial = await start(value, null);
|
||||
|
||||
@@ -1947,7 +1960,7 @@ test("a lost-response retry replays the saved answer without rescoring or regene
|
||||
const before = [...value.events];
|
||||
const replayed = await value.service.answer(userId, command);
|
||||
assert.deepEqual(replayed, first);
|
||||
assert.equal(value.counts().packetBuilds, 2);
|
||||
assert.equal(value.counts().packetBuilds, 1);
|
||||
assert.deepEqual(value.events, before);
|
||||
});
|
||||
|
||||
@@ -1990,7 +2003,7 @@ test("regenerate rewrites only the current narrative and preserves evidence, sco
|
||||
});
|
||||
|
||||
test("a failed regenerate preserves the prior turn, evidence, candidate, and billing", async () => {
|
||||
const value = harness({ readyAfterEvidenceCount: 99, invalidNarrativeFromGeneration: 3 });
|
||||
const value = harness({ readyAfterEvidenceCount: 99, invalidNarrativeFromGeneration: 2 });
|
||||
await start(value, null);
|
||||
const answered = await value.service.answer(userId, {
|
||||
type: "answer",
|
||||
@@ -2044,7 +2057,7 @@ test("overlapping identical answers converge on the first receipt despite differ
|
||||
|
||||
assert.deepEqual(second, first);
|
||||
assert.equal(value.cases.get(startActionId)?.row.eventEvidence.length, 2);
|
||||
assert.equal(value.events.filter((event) => event === "narrative").length, 3);
|
||||
assert.equal(value.events.filter((event) => event === "narrative").length, 2);
|
||||
assert.equal(value.mutations.filter((mutation) => mutation === "saveTurn").length, 2);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user