merge origin/staging into report chart render branch
Keep BUG-607 after 602–606 and retain both 09-09 changelog entries. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,13 +4,16 @@ import test from "node:test";
|
||||
|
||||
import {
|
||||
GENERIC_COLLECT_QUESTION,
|
||||
OPENING_COLLECT_DOMAINS,
|
||||
RECTIFICATION_USER_COPY,
|
||||
REPRESENTATIVE_MINUTE_DISCLAIMER,
|
||||
accidentCaseHardcodedTurns,
|
||||
containsBoundarySemantics,
|
||||
engineMeaningToDisplayCopy,
|
||||
isAcceptableOpeningBody,
|
||||
listUserVisibleCopy,
|
||||
nonConvergingRangeNarration,
|
||||
openingSpokenBody,
|
||||
stopReasonPrefix,
|
||||
} from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import { MACHINE_VOICE_LEXICON } from "../src/lib/rectification-agentic/v9/agent-voice-lexicon.ts";
|
||||
@@ -114,6 +117,7 @@ test("delivery range narration puts the stop reason before the progress clause",
|
||||
assert.ok(listUserVisibleCopy().includes(RECTIFICATION_USER_COPY.uncertaintyStop));
|
||||
assert.ok(listUserVisibleCopy().includes(RECTIFICATION_USER_COPY.tiedFirstStop));
|
||||
assert.ok(listUserVisibleCopy().includes(RECTIFICATION_USER_COPY.collectDeclinedAck));
|
||||
assert.ok(listUserVisibleCopy().includes(RECTIFICATION_USER_COPY.collectSkippedAck));
|
||||
assert.ok(listUserVisibleCopy().includes(RECTIFICATION_USER_COPY.probePoolExhaustedStop));
|
||||
assert.ok(listUserVisibleCopy().includes(RECTIFICATION_USER_COPY.insufficientEventsGate));
|
||||
assert.equal(stopReasonPrefix("probe_pool_exhausted"), RECTIFICATION_USER_COPY.probePoolExhaustedStop);
|
||||
@@ -127,10 +131,15 @@ test("question stem ownership stays on set-focus spokenPrompt, not a slot or a s
|
||||
assert.match(agent, /用 rectification-set-focus 的 spokenPrompt 写出服务端给你的下一问/);
|
||||
assert.match(agent, /题干会作为同一条消息的下一段自动出现/);
|
||||
assert.match(agent, /开场轮:先 set-focus 写采集题的 spokenPrompt/);
|
||||
assert.match(agent, /不要举大学、工作、搬家的例子/);
|
||||
assert.match(agent, /每轮正文 2-4 句/);
|
||||
// 旧:不要举大学、工作、搬家的例子 / 每轮正文 2-4 句 / 接下来我们继续
|
||||
// 新:开场点六类、证据轮一句复述
|
||||
// 原因:BUG-604 / BUG-606
|
||||
assert.match(agent, /升学、第一份工作、搬家、恋爱结婚、家里的大事、生病受伤/);
|
||||
assert.match(agent, /证据轮正文只写一句复述/);
|
||||
assert.match(agent, /记下了:2016 年 9 月入学、2020 年 6 月毕业/);
|
||||
assert.match(agent, /正文不得断言界面当前状态/);
|
||||
assert.match(agent, /接下来我们继续/);
|
||||
assert.doesNotMatch(agent, /每轮正文 2-4 句/);
|
||||
assert.doesNotMatch(agent, /不要举大学、工作、搬家的例子/);
|
||||
assert.doesNotMatch(agent, /题干与选项完全由结构化槽位和 UI 承担/);
|
||||
assert.doesNotMatch(agent, /collect_spoken 题干由服务器接在同一条正文末尾/);
|
||||
assert.doesNotMatch(agent, /自然过渡到界面上的下一步/);
|
||||
@@ -256,3 +265,18 @@ test("skill forbids computing D9/D10 signs from transition clocks", () => {
|
||||
assert.match(skill, new RegExp(sentence.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
||||
assert.match(comparison, new RegExp(sentence.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
||||
});
|
||||
|
||||
test("opening body lists domains without years and the stem no longer lists year examples", () => {
|
||||
const body = openingSpokenBody(["04:45", "05:15"]);
|
||||
assert.equal(isAcceptableOpeningBody(body), true);
|
||||
assert.ok(OPENING_COLLECT_DOMAINS.filter((domain) => body.includes(domain)).length >= 5);
|
||||
assert.doesNotMatch(body, /(?:19|20)\d{2}/);
|
||||
assert.match(body, /最后给区间和代表分钟,不给精确到秒/);
|
||||
assert.equal(GENERIC_COLLECT_QUESTION, "先说你最容易想起的一两件,年月大概就行。");
|
||||
assert.doesNotMatch(GENERIC_COLLECT_QUESTION, /比如/);
|
||||
assert.ok(listUserVisibleCopy().includes(RECTIFICATION_USER_COPY.collectSkippedAck));
|
||||
assert.ok(listUserVisibleCopy().includes(RECTIFICATION_USER_COPY.firstDatedCollectInvite));
|
||||
assert.doesNotMatch(listUserVisibleCopy().join("\n"), /还有吗|还能想起别的吗/);
|
||||
assert.ok(MACHINE_VOICE_LEXICON.includes("领先"));
|
||||
assert.ok(MACHINE_VOICE_LEXICON.includes("落后"));
|
||||
});
|
||||
|
||||
@@ -74,7 +74,11 @@ test("shows honest agent activity states before and during streamed text", () =>
|
||||
assert.match(messageRowSource, /stackedThinkingAndAnswer/);
|
||||
assert.match(messageRowSource, /aria-label="回复"/);
|
||||
assert.match(messageRowSource, /className="message-stage-and-answer"/);
|
||||
assert.match(messageRowSource, /vargaSentence=\{showThinkingPanel \? null : vargaSentence\}/);
|
||||
// 旧:vargaSentence 在气泡正文,折叠思考时才藏
|
||||
// 新:正文不再渲染方法句;展开的活动时间线保留
|
||||
// 原因:BUG-606 决策 3(a)
|
||||
assert.doesNotMatch(messageRowSource, /vargaSentence=\{showThinkingPanel \? null : vargaSentence\}/);
|
||||
assert.match(messageRowSource, /<ConsultationRunTimeline[\s\S]*vargaSentence=\{vargaSentence\}/);
|
||||
assert.match(globalStyles, /\.message-stage-and-answer/);
|
||||
assert.match(globalStyles, /\.message-stage-and-answer \{[\s\S]*gap: var\(--space-2\)/);
|
||||
assert.match(globalStyles, /\.agent-activity-status \+ \.message-answer/);
|
||||
|
||||
@@ -706,7 +706,10 @@ test("rectification Agent output stays natural and keeps tool execution silent",
|
||||
assert.match(strategy, /(?:无需|不要求)结束、暂停或保存进度/);
|
||||
assert.match(strategy, /不要一进场就出 A\/B\/C\/D/);
|
||||
assert.match(agent, /用 rectification-set-focus 的 spokenPrompt 写出服务端给你的下一问/);
|
||||
assert.match(agent, /正文只做承接(2-4 句),不提问、不复述题干、不预告选项/);
|
||||
// 旧:正文只做承接(2-4 句)→ 新:正文只做承接,证据轮一句复述
|
||||
// 原因:BUG-606 每轮只留一句话,不再给 2-4 句额度
|
||||
assert.match(agent, /正文只做承接,不提问、不复述题干、不预告选项/);
|
||||
assert.match(agent, /证据轮正文只写一句复述/);
|
||||
});
|
||||
|
||||
test("clear current-turn events go through the batch evidence service", () => {
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
import { applyHoldoutAnswer, buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts";
|
||||
import { applyChoiceWithoutEvidence } from "../src/lib/rectification-agentic/v9/inference-adapter.ts";
|
||||
import { RECTIFICATION_TERMINATION_COPY, ADOPT_OUTCOMES } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
|
||||
import { containsBoundarySemantics, RECTIFICATION_USER_COPY } from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import { containsBoundarySemantics, RECTIFICATION_USER_COPY, withFirstDatedCollectInvite } from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import {
|
||||
evidenceLedgerFingerprint,
|
||||
parseV9CaseDossier,
|
||||
@@ -729,7 +729,7 @@ test("clicking A applies the choice without invoking a language model", async ()
|
||||
start: "2015-01-01",
|
||||
end: "2015-12-31",
|
||||
});
|
||||
assert.match(applied.narration, /已记录你的选择/);
|
||||
assert.match(applied.narration, /已记录,/);
|
||||
// 原值: 旁白含 05:00–05:10 或「眼下更像 05:00」,且不再问家人
|
||||
// 新值: 下一问是家人采集
|
||||
// 原因: BUG-559 无同域带年月锚点的 varga_style 卡被丢掉;账本只有事业,下一问按方法轮转到家人
|
||||
@@ -809,7 +809,7 @@ test("answering a discriminator persists the next dated card so GET still has a
|
||||
const persistedPrompt = schema.choice?.prompt;
|
||||
assert.ok(persistedPrompt);
|
||||
assert.notEqual(applied.narration, persistedPrompt);
|
||||
assert.match(applied.narration, /已记录你的选择/);
|
||||
assert.match(applied.narration, /已记录,/);
|
||||
assert.doesNotMatch(applied.narration, /2014/);
|
||||
const turn = accounting.calls.find((call) => call.fn === "append_agentic_rectification_turn");
|
||||
assert.equal(turn?.args.p_assistant_message, applied.narration);
|
||||
@@ -978,18 +978,23 @@ test("answering the last discriminator persists a year-locked family collect foc
|
||||
const schema = setFocus.args.p_expected_answer_schema as { choice?: unknown; prompt?: string; collect?: boolean };
|
||||
assert.equal(schema.choice, undefined);
|
||||
assert.equal(schema.collect, true);
|
||||
// 旧:prompt 含 2021 → 新:口语等于 USER_COLLECT_QUESTION.family → 决策 7 采集题不带年份前缀
|
||||
assert.equal(schema.prompt, RECTIFICATION_USER_COPY.collectQuestionByDomain.family);
|
||||
// 旧:prompt 等于 USER_COLLECT_QUESTION.family
|
||||
// 新:第一道逐领域采集题干末尾加一次「想到别的也可以一起说」
|
||||
// 原因:BUG-604;年份仍不进采集题干
|
||||
assert.equal(
|
||||
schema.prompt,
|
||||
withFirstDatedCollectInvite(RECTIFICATION_USER_COPY.collectQuestionByDomain.family),
|
||||
);
|
||||
assert.match(schema.prompt ?? "", /家人|结婚|添丁|住院/);
|
||||
assert.equal(applied.nextInterviewPersisted, true);
|
||||
assert.equal(applied.nextChoiceReady, false);
|
||||
assert.equal(shouldContinueAfterStructuredChoice(applied.nextAction, applied), false);
|
||||
assert.match(applied.narration, /已记录你的选择/);
|
||||
assert.match(applied.narration, /已记录,/);
|
||||
assert.doesNotMatch(applied.narration, /2021/);
|
||||
assert.doesNotMatch(applied.narration, /点选/);
|
||||
assert.doesNotMatch(applied.narration, /D24/);
|
||||
const turn = accounting.calls.find((call) => call.fn === "append_agentic_rectification_turn");
|
||||
assert.match(String(turn?.args.p_assistant_message ?? ""), /已记录你的选择/);
|
||||
assert.match(String(turn?.args.p_assistant_message ?? ""), /已记录,/);
|
||||
assert.doesNotMatch(String(turn?.args.p_assistant_message ?? ""), /2021/);
|
||||
assert.equal(
|
||||
accounting.calls.some((call) => (
|
||||
@@ -1047,7 +1052,7 @@ test("keeps the applied answer when narration persistence fails", async () => {
|
||||
assert.equal(applied.applied, true);
|
||||
assert.equal(applied.status, "applied");
|
||||
assert.equal(applied.narrationPersisted, false);
|
||||
assert.match(applied.narration, /已记录你的选择/);
|
||||
assert.match(applied.narration, /已记录,/);
|
||||
});
|
||||
|
||||
test("stop_and_review does not write an inference transition", async () => {
|
||||
@@ -1301,9 +1306,12 @@ test("a scoring choice narrates cluster movement and range change", () => {
|
||||
credibleBefore: ["04:31", "05:07"],
|
||||
credibleAfter: ["04:31", "04:39"],
|
||||
});
|
||||
assert.match(narrowed, /领先/);
|
||||
assert.match(narrowed, /落后/);
|
||||
assert.match(narrowed, /范围从 04:31–05:07 收到 04:31–04:39/);
|
||||
// 旧:领先/落后 +「范围从 04:31–05:07 收到 04:31–04:39」
|
||||
// 新:已记录,范围收到 A–B。不变则「范围没变」。变宽写「变为」。
|
||||
// 原因:BUG-606 决策 3(b),旁白不再讲哪段领先落后
|
||||
assert.equal(narrowed, "已记录,范围收到 04:31–04:39。");
|
||||
assert.doesNotMatch(narrowed, /领先|落后/);
|
||||
assert.doesNotMatch(narrowed, /范围从 /);
|
||||
const unchanged = composeChoiceNarration({
|
||||
optionId: "A",
|
||||
scoring: true,
|
||||
@@ -1313,6 +1321,14 @@ test("a scoring choice narrates cluster movement and range change", () => {
|
||||
credibleAfter: ["04:31", "04:39"],
|
||||
});
|
||||
assert.match(unchanged, /范围没变/);
|
||||
const widened = composeChoiceNarration({
|
||||
optionId: "A",
|
||||
scoring: true,
|
||||
appliedInference: true,
|
||||
credibleBefore: ["04:45", "04:50"],
|
||||
credibleAfter: ["04:40", "05:00"],
|
||||
});
|
||||
assert.equal(widened, "已记录,范围变为 04:40–05:00。");
|
||||
const unsure = composeChoiceNarration({
|
||||
optionId: "D",
|
||||
scoring: true,
|
||||
@@ -1353,8 +1369,12 @@ test("answering A narrates the credible range even when the search window stays
|
||||
optionId: "A",
|
||||
expectedRevision: previous.revision,
|
||||
});
|
||||
assert.match(applied.narration, /范围从 04:31–05:07 收到 04:31–04:31/);
|
||||
// 旧:整段旁白等于「已记录,范围收到 04:31–04:31。」
|
||||
// 新:旁白以该句开头;若下一问未能单独落库,服务端仍可能把采集题干接在后面
|
||||
// 原因:BUG-606 决策 3(b) 锁的是点选旁白,不是下一问是否挂上同一条消息
|
||||
assert.match(applied.narration, /^已记录,范围收到 04:31–04:31。/);
|
||||
assert.doesNotMatch(applied.narration, /范围没变/);
|
||||
assert.doesNotMatch(applied.narration, /领先|落后/);
|
||||
const source = readFileSync(new URL("../src/lib/rectification-agentic/v9/answer-choice.ts", import.meta.url), "utf8");
|
||||
assert.match(source, /credibleBefore:\s*previous\.credible_range/);
|
||||
assert.match(source, /credibleAfter:\s*applied\.state\.credible_range/);
|
||||
|
||||
@@ -307,4 +307,40 @@ test("session_outcome gates adopt cards independently of leaked can_adopt", () =
|
||||
assert.equal(canShowRectificationReadonlyRange(adopted), false);
|
||||
});
|
||||
|
||||
test("parses inference marks from decisionReceipt.inference_state.candidates", () => {
|
||||
const times = [
|
||||
"04:45", "04:47", "04:49", "04:51", "04:52", "04:53", "04:55", "04:57", "04:59",
|
||||
] as const;
|
||||
const result = parseRectificationCandidateResult({
|
||||
...camelCaseSnapshot,
|
||||
candidates: [
|
||||
{ candidateId: CANDIDATE_ID, rank: 1, time: "04:51", relativeSupport: 40, tiedMinuteCount: 3 },
|
||||
{ candidateId: SECOND_CANDIDATE_ID, rank: 2, time: "04:53", relativeSupport: 38, tiedMinuteCount: 3 },
|
||||
],
|
||||
credible_range: ["04:51", "04:53"],
|
||||
decisionReceipt: {
|
||||
...camelCaseSnapshot.decisionReceipt,
|
||||
inference_state: {
|
||||
candidates: times.map((clock) => ({
|
||||
time: clock,
|
||||
status: clock === "04:51" || clock === "04:53" ? "active" : "eliminated",
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.ok(result);
|
||||
assert.ok(result.inferenceMarks);
|
||||
assert.equal(result.candidates.length, 2);
|
||||
assert.deepEqual(result.inferenceMarks.map((mark) => mark.time), [...times]);
|
||||
assert.equal(result.inferenceMarks.filter((mark) => mark.eliminated).length, 7);
|
||||
assert.equal(result.inferenceMarks.find((mark) => mark.time === "04:51")?.eliminated, false);
|
||||
assert.equal(result.inferenceMarks.find((mark) => mark.time === "04:45")?.eliminated, true);
|
||||
});
|
||||
|
||||
test("inference marks are empty when inference_state is absent", () => {
|
||||
const result = parseRectificationCandidateResult(camelCaseSnapshot);
|
||||
assert.ok(result);
|
||||
assert.deepEqual(result.inferenceMarks, []);
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
parseV9CaseDossier,
|
||||
RectificationToolServiceError,
|
||||
} from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
||||
import { GENERIC_COLLECT_QUESTION, USER_COLLECT_QUESTION } from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import { GENERIC_COLLECT_QUESTION, USER_COLLECT_QUESTION, withFirstDatedCollectInvite } from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import { turnQuestionKind } from "../src/lib/rectification-agentic/v9/turn-question.ts";
|
||||
import { createRectificationV9Tools } from "../src/mastra/rectification-v9-tools.ts";
|
||||
import {
|
||||
@@ -186,7 +186,10 @@ test("family collect spoken stem has no year prefix while probe_year stays dated
|
||||
});
|
||||
assert.equal(plan.next_followup?.domain, "family");
|
||||
assert.equal(plan.next_followup?.probe_year, 2021);
|
||||
assert.equal(spokenFollowupForUser(plan.next_followup), USER_COLLECT_QUESTION.family);
|
||||
// 旧:口语等于 USER_COLLECT_QUESTION.family
|
||||
// 新:第一道逐领域采集题干末尾加一次「想到别的也可以一起说」
|
||||
// 原因:BUG-604 只报一件也走既有采集,不插「还有吗」追问轮
|
||||
assert.equal(spokenFollowupForUser(plan.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.family));
|
||||
});
|
||||
|
||||
test("four scoreable events skip holdout for domains already in the ledger", () => {
|
||||
@@ -582,13 +585,13 @@ test("career discriminator yes covers career collect without ledger career evide
|
||||
});
|
||||
assert.equal(unanswered.next_followup?.domain, "career");
|
||||
assert.equal(unanswered.next_followup?.intent, "collect_method_evidence");
|
||||
assert.equal(spokenFollowupForUser(unanswered.next_followup), USER_COLLECT_QUESTION.career);
|
||||
assert.equal(spokenFollowupForUser(unanswered.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.career));
|
||||
|
||||
const yes = careerAnswerPlan("yes");
|
||||
assert.notEqual(yes.next_followup?.domain, "career");
|
||||
assert.equal(yes.next_followup?.domain, "education");
|
||||
assert.equal(yes.next_followup?.intent, "collect_method_evidence");
|
||||
assert.equal(spokenFollowupForUser(yes.next_followup), USER_COLLECT_QUESTION.education);
|
||||
assert.equal(spokenFollowupForUser(yes.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.education));
|
||||
assert.equal(
|
||||
yes.methods.find((item) => item.method_id === "d10_career")?.status,
|
||||
"covered",
|
||||
@@ -600,17 +603,17 @@ test("career discriminator yes covers career collect without ledger career evide
|
||||
|
||||
const no = careerAnswerPlan("no");
|
||||
assert.equal(no.next_followup?.domain, "career");
|
||||
assert.equal(spokenFollowupForUser(no.next_followup), USER_COLLECT_QUESTION.career);
|
||||
assert.equal(spokenFollowupForUser(no.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.career));
|
||||
|
||||
const unsure = careerAnswerPlan("unsure");
|
||||
assert.equal(unsure.next_followup?.domain, "career");
|
||||
assert.equal(spokenFollowupForUser(unsure.next_followup), USER_COLLECT_QUESTION.career);
|
||||
assert.equal(spokenFollowupForUser(unsure.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.career));
|
||||
});
|
||||
|
||||
test("career discriminator yes does not invent coverage from a semantic_key prefix", () => {
|
||||
const plan = careerAnswerPlan("yes", { eventProbes: [] });
|
||||
assert.equal(plan.next_followup?.domain, "career");
|
||||
assert.equal(spokenFollowupForUser(plan.next_followup), USER_COLLECT_QUESTION.career);
|
||||
assert.equal(spokenFollowupForUser(plan.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.career));
|
||||
});
|
||||
|
||||
test("ledger-classified career probe answers do not cover career collect", () => {
|
||||
@@ -623,7 +626,7 @@ test("ledger-classified career probe answers do not cover career collect", () =>
|
||||
}],
|
||||
});
|
||||
assert.equal(plan.next_followup?.domain, "career");
|
||||
assert.equal(spokenFollowupForUser(plan.next_followup), USER_COLLECT_QUESTION.career);
|
||||
assert.equal(spokenFollowupForUser(plan.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.career));
|
||||
});
|
||||
|
||||
const THIRD_CANDIDATE_ID = "88888888-8888-4888-8888-888888888883";
|
||||
|
||||
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import { composeCollectSpokenAssistantText, detachCollectSpokenAssistantText, stripQuestionSentences } from "../src/lib/rectification-agentic/v9/collect-prompt.ts";
|
||||
import { composeCollectSpokenAssistantText, detachCollectSpokenAssistantText, stripQuestionSentences, trimEvidenceTurnBody } from "../src/lib/rectification-agentic/v9/collect-prompt.ts";
|
||||
import { attachQuestionsToTurns } from "../src/lib/rectification-agentic/v9/turn-question.ts";
|
||||
import { GENERIC_COLLECT_QUESTION, RECTIFICATION_USER_COPY, USER_COLLECT_QUESTION } from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import { CASE_ID, FOCUS_ID, TURN_ID } from "./rectification-v9-test-support.ts";
|
||||
@@ -121,3 +121,13 @@ test("attachQuestionsToTurns leaves body unchanged when the turn has no focus",
|
||||
assert.equal(turns[0]?.question, null);
|
||||
});
|
||||
|
||||
test("trimEvidenceTurnBody keeps the first sentence and strips value judgments", () => {
|
||||
const three = trimEvidenceTurnBody("记下了:2016 年 9 月入学。这对校正很有帮助。接下来我们继续。");
|
||||
assert.equal(three, "记下了:2016 年 9 月入学。");
|
||||
assert.doesNotMatch(three, /很有帮助|很有价值|很有分量|特别有用/);
|
||||
const judged = trimEvidenceTurnBody("记下了:2020 年 6 月毕业,特别有用。");
|
||||
assert.doesNotMatch(judged, /特别有用/);
|
||||
const two = trimEvidenceTurnBody("记下了:2016 年 9 月入学。\n\n范围收到 04:50–05:10。");
|
||||
assert.equal(two, "记下了:2016 年 9 月入学。\n\n范围收到 04:50–05:10。");
|
||||
});
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9
|
||||
import {
|
||||
RECTIFICATION_USER_COPY,
|
||||
USER_COLLECT_QUESTION,
|
||||
withFirstDatedCollectInvite,
|
||||
} from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import {
|
||||
ATTEMPT_ID,
|
||||
@@ -462,8 +463,8 @@ function rpcDossier(decision: DecisionDossier, activeFocus?: Record<string, unkn
|
||||
});
|
||||
}
|
||||
|
||||
test("skill version is 10.0.18 after the delivery UI simplify bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.18");
|
||||
test("skill version is 10.0.19 after the delivery UI simplify bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.19");
|
||||
});
|
||||
|
||||
test("revision 5 with uncovered relatives asks the dated family collect, not a yearless D12 card", () => {
|
||||
@@ -486,7 +487,10 @@ test("revision 5 with uncovered relatives asks the dated family collect, not a y
|
||||
// 旧:year_label 含 2021 → 新:采集口语不带年份前缀,year_label 不进 followup
|
||||
// 原因:决策 7;probe_year 仍锁定 dated 家庭采集,不出无年份 D12 卡
|
||||
assert.equal(plan.next_followup?.year_label, undefined);
|
||||
assert.equal(spokenFollowupForUser(plan.next_followup), USER_COLLECT_QUESTION.family);
|
||||
// 旧:口语等于 USER_COLLECT_QUESTION.family
|
||||
// 新:第一道逐领域采集题干末尾加一次「想到别的也可以一起说」
|
||||
// 原因:BUG-604 单事件开场不插追问轮
|
||||
assert.equal(spokenFollowupForUser(plan.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.family));
|
||||
assert.equal(plan.dropped_probes.some((item) => (
|
||||
item.semantic_key.startsWith("varga.d12") && item.reason === "yearless_ungrounded_contrast"
|
||||
)), true);
|
||||
@@ -1295,9 +1299,12 @@ test("has_new_dated_event continues into the agent after applying the answer", (
|
||||
answerChoice.indexOf("export async function persistCollectDenialTurn"),
|
||||
answerChoice.indexOf("export function isStalePreAdoptFocus"),
|
||||
);
|
||||
assert.match(persistDenial, /composeCollectSpokenAssistantText\(RECTIFICATION_USER_COPY\.collectDeclinedAck/);
|
||||
assert.match(persistDenial, /const ack = input\.applied\.ack/);
|
||||
assert.match(persistDenial, /composeCollectSpokenAssistantText\(ack,/);
|
||||
assert.match(persistDenial, /linkFocusAskedTurn/);
|
||||
assert.match(persistDenial, /streamText = hasNextStem \? RECTIFICATION_USER_COPY\.collectDeclinedAck/);
|
||||
// 旧:streamText 写死 collectDeclinedAck → 新:用 applied.ack,跳过走 collectSkippedAck
|
||||
// 原因:BUG-605 「记不清」回执与「没有」不同
|
||||
assert.match(persistDenial, /streamText = hasNextStem \? ack/);
|
||||
const persistApplied = answerChoice.slice(answerChoice.indexOf("async function persistApplied"));
|
||||
assert.match(persistApplied, /command\.deferFollowup !== true/);
|
||||
assert.equal(shouldContinueAgentForDatedEvent({
|
||||
|
||||
@@ -359,7 +359,7 @@ test("holdout not_ready forbids unique-minute copy and still blocks confirm", as
|
||||
assert.match(agentSource, /不得宣称唯一出生分钟/);
|
||||
assert.doesNotMatch(agentSource, /±2 分钟/);
|
||||
assert.equal(PUBLIC_RECTIFICATION_TOOLS.length, 14);
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.18");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.19");
|
||||
|
||||
const accounting = fakeAccounting({
|
||||
...receiptHandlers,
|
||||
|
||||
@@ -673,7 +673,10 @@ test("nonterminal invariant 3: answer_choice response waits until incident fallb
|
||||
// 保留语义:answer_choice 出口仍必须先把家人采集题干写入 focus/turn,再返回 HTTP。
|
||||
assert.equal(decision.canAdopt, true);
|
||||
assert.ok(currentQuestion?.prompt);
|
||||
assert.equal(currentQuestion.prompt, "家里如果有结婚、添丁或住院这类事,记得大概哪年就行。");
|
||||
// 旧:家人采集题干原句
|
||||
// 新:第一道逐领域采集题干末尾加一次「想到别的也可以一起说」
|
||||
// 原因:BUG-604 单事件开场不插追问轮
|
||||
assert.equal(currentQuestion.prompt, "家里如果有结婚、添丁或住院这类事,记得大概哪年就行。想到别的也可以一起说。");
|
||||
const setFocus = accounting.calls.find((call) => call.fn === "set_agentic_rectification_conversation_focus");
|
||||
assert.ok(setFocus);
|
||||
assert.equal(setFocus.args.p_intent, "collect_method_evidence");
|
||||
|
||||
@@ -199,12 +199,12 @@ test("delivery report gives 04:53 D10 as Cancer instead of letting the model inf
|
||||
assert.match(report.markdown, /04:53 \| .*巨蟹座/);
|
||||
});
|
||||
|
||||
test("skill 10.0.18 forbids computing varga signs from transition times", () => {
|
||||
test("skill 10.0.19 forbids computing varga signs from transition times", () => {
|
||||
const skillDir = fileURLToPath(new URL("../../skills/jyotish-birth-time-rectification", import.meta.url));
|
||||
const skill = readFileSync(`${skillDir}/SKILL.md`, "utf8");
|
||||
const comparison = readFileSync(`${skillDir}/references/candidate-comparison.md`, "utf8");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.18");
|
||||
assert.match(skill, /^version: 10\.0\.18$/m);
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.19");
|
||||
assert.match(skill, /^version: 10\.0\.19$/m);
|
||||
assert.match(skill, new RegExp(SKILL_SIGN_SENTENCE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
||||
assert.match(comparison, new RegExp(SKILL_SIGN_SENTENCE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
parseWindowScan,
|
||||
} from "../src/lib/rectification-agentic/v9/varga-observations.ts";
|
||||
import { WINDOW_SCAN_DISPLAY_LAYER_ORDER } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
|
||||
import { GENERIC_COLLECT_QUESTION, USER_COLLECT_QUESTION } from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import { GENERIC_COLLECT_QUESTION, USER_COLLECT_QUESTION, withFirstDatedCollectInvite } from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts";
|
||||
import { buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts";
|
||||
import { decideRectification } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
|
||||
@@ -1454,9 +1454,9 @@ test("rescore failure does not fail the evidence write", async () => {
|
||||
assert.ok(result.rescore.error_code);
|
||||
});
|
||||
|
||||
test("public tool surface stays at 14 and new cases bind 10.0.18", () => {
|
||||
test("public tool surface stays at 14 and new cases bind 10.0.19", () => {
|
||||
assert.equal(PUBLIC_RECTIFICATION_TOOLS.length, 14);
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.18");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.19");
|
||||
const deprecated = resolveExactSkillPackage(
|
||||
"jyotish-birth-time-rectification",
|
||||
"10.0.2",
|
||||
@@ -2381,7 +2381,10 @@ test("family collect attaches the collection-probe year instead of a yearless D2
|
||||
// 原因:决策 7,年份只用于计分与去重,不进采集题干
|
||||
assert.equal(plan.next_followup?.year_label, undefined);
|
||||
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /varga\.d24/);
|
||||
assert.equal(spokenFollowupForUser(plan.next_followup), USER_COLLECT_QUESTION.family);
|
||||
// 旧:口语等于 USER_COLLECT_QUESTION.family
|
||||
// 新:第一道逐领域采集题干末尾加一次「想到别的也可以一起说」
|
||||
// 原因:BUG-604 单事件开场不插追问轮
|
||||
assert.equal(spokenFollowupForUser(plan.next_followup), withFirstDatedCollectInvite(USER_COLLECT_QUESTION.family));
|
||||
});
|
||||
|
||||
test("already-open yearless family card is abandoned instead of kept as a scoring frame", () => {
|
||||
|
||||
@@ -576,8 +576,8 @@ function warnLines(run: () => Promise<unknown> | unknown) {
|
||||
}).then((result) => ({ result, lines }));
|
||||
}
|
||||
|
||||
test("skill version is 10.0.18 after the delivery UI simplify bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.18");
|
||||
test("skill version is 10.0.19 after the delivery UI simplify bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.19");
|
||||
});
|
||||
|
||||
test("USER_COLLECT_QUESTION no longer has an other fallback", () => {
|
||||
|
||||
@@ -335,7 +335,7 @@ test("narrator failure still leaves the choice applied and does not ask the user
|
||||
scoring: true,
|
||||
appliedInference: true,
|
||||
});
|
||||
assert.match(applied, /已记录你的选择/);
|
||||
assert.match(applied, /已记录,范围没变/);
|
||||
assert.doesNotMatch(applied, /请再选一次|重新回答/);
|
||||
const stopped = composeChoiceNarration({
|
||||
optionId: "stop",
|
||||
|
||||
@@ -213,9 +213,9 @@ test("read-case evidence context keeps day labels and confirm does not rewrite d
|
||||
assert.equal("p_occurred_from" in confirmCall.args, false);
|
||||
});
|
||||
|
||||
test("new-case skill identity is 10.0.18 and the prompt prefers batch ingest", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.18");
|
||||
assert.match(skill, /^version: 10\.0\.18$/m);
|
||||
test("new-case skill identity is 10.0.19 and the prompt prefers batch ingest", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.19");
|
||||
assert.match(skill, /^version: 10\.0\.19$/m);
|
||||
assert.match(skill, /不要对同一句用户消息里的多件事件逐条 propose\+confirm/);
|
||||
assert.match(agentSource, /新事件走 rectification-record-evidence-batch/);
|
||||
assert.doesNotMatch(agentSource, /分别调用 rectification-propose-evidence 和 rectification-confirm-evidence/);
|
||||
|
||||
@@ -184,8 +184,8 @@ const CANDIDATE_IDS = [
|
||||
"88888888-8888-4888-8888-888888888882",
|
||||
] as const;
|
||||
|
||||
test("skill version is 10.0.18 after the delivery UI simplify bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.18");
|
||||
test("skill version is 10.0.19 after the delivery UI simplify bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.19");
|
||||
});
|
||||
|
||||
test("nineteen-row ledger opens the training gate with four scoreable domains", () => {
|
||||
|
||||
@@ -11,7 +11,10 @@ import {
|
||||
buildNextUserAction,
|
||||
spokenFollowupForUser,
|
||||
} from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||||
import { USER_COLLECT_QUESTION } from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import {
|
||||
USER_COLLECT_QUESTION,
|
||||
withFirstDatedCollectInvite,
|
||||
} from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import {
|
||||
RECTIFICATION_QUESTION_RETRY_LIMIT,
|
||||
rectificationQuestionGapState,
|
||||
@@ -204,6 +207,9 @@ test("family collect spoken stem equals USER_COLLECT_QUESTION.family while probe
|
||||
assert.equal(plan.next_followup?.probe_year, 2021);
|
||||
assert.equal(plan.next_followup?.choice_frame, null);
|
||||
const spoken = spokenFollowupForUser(plan.next_followup);
|
||||
assert.equal(spoken, USER_COLLECT_QUESTION.family);
|
||||
// 旧:口语等于 USER_COLLECT_QUESTION.family
|
||||
// 新:第一道逐领域采集题干末尾加一次「想到别的也可以一起说」
|
||||
// 原因:BUG-604 只报一件也走既有采集,不插「还有吗」追问轮
|
||||
assert.equal(spoken, withFirstDatedCollectInvite(USER_COLLECT_QUESTION.family));
|
||||
assert.doesNotMatch(spoken ?? "", /^\d{4} 年前后,/);
|
||||
});
|
||||
|
||||
@@ -296,6 +296,8 @@ test("collect denial persists ack+stem then binds asked_turn_id; stream is ack o
|
||||
userMessage: "没有",
|
||||
applied: {
|
||||
narration: prompt,
|
||||
ack: RECTIFICATION_USER_COPY.collectDeclinedAck,
|
||||
closeStatus: "declined",
|
||||
nextInterviewPersisted: true,
|
||||
nextChoiceReady: false,
|
||||
focus: {
|
||||
@@ -341,6 +343,8 @@ test("collect denial without a next stem still writes a turn and does not invent
|
||||
userMessage: "没有",
|
||||
applied: {
|
||||
narration: RECTIFICATION_USER_COPY.collectDeclinedAck,
|
||||
ack: RECTIFICATION_USER_COPY.collectDeclinedAck,
|
||||
closeStatus: "declined",
|
||||
nextInterviewPersisted: false,
|
||||
nextChoiceReady: false,
|
||||
focus: null,
|
||||
|
||||
@@ -426,8 +426,8 @@ function rpcDossier(decision: DecisionDossier) {
|
||||
});
|
||||
}
|
||||
|
||||
test("skill version is 10.0.18 after the delivery UI simplify bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.18");
|
||||
test("skill version is 10.0.19 after the delivery UI simplify bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.19");
|
||||
});
|
||||
|
||||
test("pre-fix dual-exit constant is gone; range narration carries numbers and the disclaimer", () => {
|
||||
|
||||
@@ -517,10 +517,11 @@ test("zero-evidence opening persists the server-owned first question", async ()
|
||||
// 新值: collect:other:collect_method_evidence
|
||||
// 原因: 开场仍用 domain=other,GENERIC 题干;稳定 id 不得含 unknown
|
||||
assert.equal(stableFollowupQuestionId(followup), "collect:other:collect_method_evidence");
|
||||
// 旧:开场 domain=other 后查表落到补问兜底句 → 新:method_coverage + other 且非 retry 用 GENERIC_COLLECT_QUESTION → 保留 开场题由服务端出、带具体例子
|
||||
// 旧:开场 GENERIC 带「比如哪年上的大学…」→ 新:题干只邀请一两件,领域清单在开场正文
|
||||
// 原因:BUG-604
|
||||
assert.equal(spokenFollowupForUser(followup), GENERIC_COLLECT_QUESTION);
|
||||
assert.doesNotMatch(spokenFollowupForUser(followup) ?? "", /^也可以再/);
|
||||
assert.match(spokenFollowupForUser(followup) ?? "", /比如/);
|
||||
assert.match(spokenFollowupForUser(followup) ?? "", /先说你最容易想起的一两件/);
|
||||
const expected = GENERIC_COLLECT_QUESTION;
|
||||
|
||||
const accounting = fakeAccounting({
|
||||
@@ -606,15 +607,15 @@ test("exhaustion other fallback and other retry keep the after-evidence copy", (
|
||||
assert.equal(spokenFollowupForUser(retry), null);
|
||||
});
|
||||
|
||||
test("GENERIC_COLLECT_QUESTION keeps concrete opening examples", () => {
|
||||
test("GENERIC_COLLECT_QUESTION invites one or two memories without listing years", () => {
|
||||
const source = readFileSync(new URL("../src/lib/rectification-agentic/user-copy.ts", import.meta.url), "utf8");
|
||||
assert.match(GENERIC_COLLECT_QUESTION, /比如/);
|
||||
assert.ok(
|
||||
[...GENERIC_COLLECT_QUESTION.matchAll(/上的大学|换的工作|搬的家/g)].length >= 2,
|
||||
GENERIC_COLLECT_QUESTION,
|
||||
);
|
||||
// 旧:「…开始就好——比如哪年上的大学、哪年换的工作…」→ 新:加「哪年搬的家」→ 保留 开场题必须带具体例子、由服务端出
|
||||
assert.match(source, /GENERIC_COLLECT_QUESTION = "从你最容易想起来的一件事开始就好/);
|
||||
// 旧:从你最容易想起来的一件事开始就好——比如哪年上的大学、哪年换的工作、哪年搬的家
|
||||
// 新:先说你最容易想起的一两件,年月大概就行。领域清单在开场正文。
|
||||
// 原因:BUG-604 决策 1,题干不再列年份例子
|
||||
assert.equal(GENERIC_COLLECT_QUESTION, "先说你最容易想起的一两件,年月大概就行。");
|
||||
assert.doesNotMatch(GENERIC_COLLECT_QUESTION, /比如/);
|
||||
assert.doesNotMatch(GENERIC_COLLECT_QUESTION, /(?:19|20)\d{2}/);
|
||||
assert.match(source, /GENERIC_COLLECT_QUESTION = "先说你最容易想起的一两件,年月大概就行。"/);
|
||||
});
|
||||
|
||||
test("degraded spoken collect does not keep discriminator identity or block a later card", async () => {
|
||||
|
||||
@@ -10,8 +10,15 @@ import {
|
||||
stableFollowupQuestionId,
|
||||
} from "../src/lib/rectification-agentic/v9/server-focus.ts";
|
||||
import { projectTurnDecision } from "../src/lib/rectification-agentic/v9/turn-decision.ts";
|
||||
import { spokenCollectFallbackFollowup } from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||||
import type { MethodFollowup } from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||||
import {
|
||||
declinedCollectDomains,
|
||||
nextDatedCollectFollowup,
|
||||
reverseVerifyRemainingForAdopt,
|
||||
spokenFollowupForUser,
|
||||
spokenCollectFallbackFollowup,
|
||||
type MethodFollowup,
|
||||
type MethodFollowupEvidence,
|
||||
} from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||||
import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts";
|
||||
import {
|
||||
CASE_ID,
|
||||
@@ -28,6 +35,8 @@ import {
|
||||
import { parseV9CaseDossier as parseDossier } from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
||||
|
||||
import { USER_COLLECT_QUESTION } from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import type { DiscriminatingEventProbe } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
|
||||
import { isCollectDeclineUtterance, isCollectSkipUtterance } from "../src/lib/rectification-agentic/v9/turn-intent-classifier.ts";
|
||||
|
||||
const RELATIONSHIP_PROMPT = USER_COLLECT_QUESTION.relationship;
|
||||
|
||||
@@ -85,8 +94,8 @@ function collectPersistResult(overrides: {
|
||||
};
|
||||
}
|
||||
|
||||
test("skill version is 10.0.18 after the delivery UI simplify bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.18");
|
||||
test("skill version is 10.0.19 after the delivery UI simplify bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.19");
|
||||
});
|
||||
|
||||
test("cases current_question remains the submit contract, not a visual slot", () => {
|
||||
@@ -107,6 +116,19 @@ test("cases current_question remains the submit contract, not a visual slot", ()
|
||||
const wrap = chat.slice(chat.indexOf("className=\"composer-wrap\""), chat.indexOf("className=\"composer-footer\""));
|
||||
assert.doesNotMatch(wrap, /rectification-collect-stop/);
|
||||
assert.doesNotMatch(wrap, /CHOICE_STOP_LABEL/);
|
||||
// 旧:输入框上方无按钮(先这样已删)
|
||||
// 新:collect_spoken 且未 busy 时两个 44px 次要按钮「没有」「记不清」
|
||||
// 原因:BUG-605;不得复用 rectification-step-state
|
||||
assert.match(wrap, /rectification-collect-replies/);
|
||||
assert.match(wrap, /rectification-collect-reply/);
|
||||
assert.match(wrap, /send\("message", "没有"\)/);
|
||||
assert.match(wrap, /send\("message", "记不清"\)/);
|
||||
assert.match(wrap, />[\s\n]*没有[\s\n]*</);
|
||||
assert.match(wrap, />[\s\n]*记不清[\s\n]*</);
|
||||
assert.doesNotMatch(wrap, /rectification-step-state/);
|
||||
assert.match(chat, /collectSpokenPrompt && !busy && !readonly && regeneratingMessageKey === null/);
|
||||
const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
|
||||
assert.match(styles, /\.rectification-collect-reply \{[\s\S]*min-height:\s*44px/);
|
||||
assert.equal(parseRectificationChoiceCard(COLLECT_GET_QUESTION), null);
|
||||
});
|
||||
|
||||
@@ -403,3 +425,103 @@ test("agent route keeps question ownership in the server Case projection", () =>
|
||||
assert.match(agentRun, /askedTurnId: turnId/);
|
||||
assert.doesNotMatch(agentRun, /当前可询问范围/);
|
||||
});
|
||||
|
||||
test("没有 and 记不清 short-circuit collect_spoken without a model run", () => {
|
||||
assert.equal(isCollectDeclineUtterance("没有"), true);
|
||||
assert.equal(isCollectDeclineUtterance("没有。"), true);
|
||||
assert.equal(isCollectSkipUtterance("记不清"), true);
|
||||
assert.equal(isCollectSkipUtterance("记不清!"), true);
|
||||
assert.equal(isCollectDeclineUtterance("记不清"), false);
|
||||
assert.equal(isCollectSkipUtterance("没有"), false);
|
||||
const route = readFileSync(new URL("../src/app/api/rectification/agent/route.ts", import.meta.url), "utf8");
|
||||
const collectStart = route.lastIndexOf("} else if (focus && isCollectFocusSchema(focus.expectedAnswerSchema))");
|
||||
const collectBranch = route.slice(collectStart);
|
||||
const collectFocus = collectBranch.slice(0, collectBranch.indexOf("classifyRectificationTurnIntent"));
|
||||
assert.match(collectFocus, /isCollectDeclineUtterance/);
|
||||
assert.match(collectFocus, /isCollectSkipUtterance/);
|
||||
assert.match(collectFocus, /status: isCollectSkipUtterance\(userMessage\) \? "skipped" : "declined"/);
|
||||
assert.match(collectFocus, /persistCollectDenialTurn/);
|
||||
assert.doesNotMatch(collectFocus, /runV9AgentTurn/);
|
||||
});
|
||||
|
||||
test("skipped collect domains are not re-asked this session; reverse-verify may still touch them", () => {
|
||||
const skippedFamily = [{
|
||||
target_domain: "family",
|
||||
status: "skipped",
|
||||
intent: "collect_method_evidence",
|
||||
}];
|
||||
const declined = declinedCollectDomains(skippedFamily);
|
||||
assert.equal(declined.has("family"), true);
|
||||
const next = nextDatedCollectFollowup([], declined);
|
||||
assert.notEqual(next?.domain, "family");
|
||||
const familyProbe: DiscriminatingEventProbe = {
|
||||
year: 2018,
|
||||
year_label: "2018 年前后",
|
||||
domain: "family",
|
||||
event_family: "家里的大事",
|
||||
source: "dasha_boundary",
|
||||
tracks: ["vimshottari", "narayana"],
|
||||
tracks_agree: true,
|
||||
unique_minute_claim: false,
|
||||
user_meaning: "时间范围锁定 2018 年。",
|
||||
role: "distinguish",
|
||||
information_gain: 1.1,
|
||||
semantic_key: "family.2018.dasha_boundary",
|
||||
candidate_split_hash: "family.2018.dasha_boundary",
|
||||
candidate_ids: ["05:00", "05:20"],
|
||||
expected_outcomes: [
|
||||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:20"] },
|
||||
{ answer_class: "no", supports: ["05:20"], conflicts: ["05:00"] },
|
||||
],
|
||||
choice_kind: "existence",
|
||||
};
|
||||
const remaining = reverseVerifyRemainingForAdopt({
|
||||
eventProbes: [familyProbe],
|
||||
evidence: [],
|
||||
declinedTopics: skippedFamily,
|
||||
});
|
||||
assert.equal(remaining.some((probe) => probe.domain === "family"), true);
|
||||
});
|
||||
|
||||
test("single confirmed domain opening goes to dated collect without a chase-more turn", () => {
|
||||
const evidence: MethodFollowupEvidence[] = [
|
||||
{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-09-01", occurredTo: null },
|
||||
];
|
||||
const next = nextDatedCollectFollowup(evidence, new Set());
|
||||
assert.ok(next);
|
||||
assert.notEqual(next.domain, "education");
|
||||
assert.equal(next.domain, "family");
|
||||
const spoken = spokenFollowupForUser(next);
|
||||
assert.ok(spoken);
|
||||
assert.match(spoken, /家里/);
|
||||
assert.match(spoken, /想到别的也可以一起说/);
|
||||
assert.doesNotMatch(spoken, /还有吗|别的吗|还能想起别的吗/);
|
||||
assert.doesNotMatch(spoken, /先说你最容易想起的一两件/);
|
||||
const later = nextDatedCollectFollowup(evidence, new Set(), new Set(), { askedDatedCollect: true });
|
||||
assert.equal(later?.domain, "family");
|
||||
assert.doesNotMatch(spokenFollowupForUser(later) ?? "", /想到别的也可以一起说/);
|
||||
const tools = readFileSync(new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url), "utf8");
|
||||
assert.match(tools, /recordV10EvidenceBatch/);
|
||||
assert.match(tools, /result\.acceptedCount > 0[\s\S]*autoRescoreAfterEvidenceChange/);
|
||||
const copy = readFileSync(new URL("../src/lib/rectification-agentic/user-copy.ts", import.meta.url), "utf8");
|
||||
const agent = readFileSync(new URL("../src/mastra/agentic-rectification.ts", import.meta.url), "utf8");
|
||||
const agentRun = readFileSync(new URL("../src/lib/rectification-agentic/v9/agent-run.ts", import.meta.url), "utf8");
|
||||
assert.doesNotMatch(copy, /还有吗|还能想起别的吗/);
|
||||
assert.doesNotMatch(agent, /还有吗|还能想起别的吗/);
|
||||
assert.doesNotMatch(agentRun, /还有吗|还能想起别的吗/);
|
||||
});
|
||||
|
||||
test("three confirmed domains from one message are not re-asked by dated collect", () => {
|
||||
const evidence: MethodFollowupEvidence[] = [
|
||||
{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-09-01", occurredTo: null },
|
||||
{ status: "confirmed", domain: "career", datePrecision: "month", occurredFrom: "2018-07-01", occurredTo: null },
|
||||
{ status: "confirmed", domain: "relocation", datePrecision: "year", occurredFrom: "2023-01-01", occurredTo: null },
|
||||
];
|
||||
const next = nextDatedCollectFollowup(evidence, new Set());
|
||||
assert.notEqual(next?.domain, "education");
|
||||
assert.notEqual(next?.domain, "career");
|
||||
assert.notEqual(next?.domain, "relocation");
|
||||
const tools = readFileSync(new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url), "utf8");
|
||||
assert.match(tools, /recordV10EvidenceBatch/);
|
||||
assert.match(tools, /result\.acceptedCount > 0[\s\S]*autoRescoreAfterEvidenceChange/);
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import { parseRectificationCandidateResult } from "../src/lib/rectification-candidate-result.ts";
|
||||
import {
|
||||
buildRectificationTimeline,
|
||||
formatClockMinutes,
|
||||
@@ -86,7 +87,8 @@ test("the band is placed against the axis and never escapes it", () => {
|
||||
assert.equal(Math.round(view.bandWidthPercent), 7);
|
||||
assert.ok(view.bandStartPercent + view.bandWidthPercent <= 100);
|
||||
assert.equal(view.rangeLabel, "05:07–05:09");
|
||||
assert.equal(view.widthLabel, "2 分钟");
|
||||
// Inclusive of both ends: 05:07, 05:08, 05:09. Exclusive difference was "2 分钟".
|
||||
assert.equal(view.widthLabel, "3 分钟");
|
||||
|
||||
// A range reported wider than the window is clamped rather than overflowing.
|
||||
const overflowing = buildRectificationTimeline({
|
||||
@@ -100,7 +102,33 @@ test("the band is placed against the axis and never escapes it", () => {
|
||||
assert.equal(overflowing.bandWidthPercent, 100);
|
||||
});
|
||||
|
||||
test("candidate marks are binary and the range boundary counts as inside", () => {
|
||||
test("candidate marks follow inference status, not band position", () => {
|
||||
const view = buildRectificationTimeline({
|
||||
searchWindow: declaredWindow,
|
||||
credibleRange: ["05:07", "05:09"],
|
||||
candidateTimes: ["05:07", "05:08", "05:09"],
|
||||
inferenceMarks: [
|
||||
{ time: "05:06", eliminated: true },
|
||||
{ time: "05:07", eliminated: false },
|
||||
{ time: "05:08", eliminated: true },
|
||||
{ time: "05:09", eliminated: false },
|
||||
{ time: "05:10", eliminated: true },
|
||||
],
|
||||
stage: "minute",
|
||||
});
|
||||
assert.ok(view);
|
||||
// 05:08 sits inside the band but is eliminated → hollow. Position vs band is not the judge.
|
||||
assert.deepEqual(view.marks.map((mark) => [mark.key, mark.state]), [
|
||||
["m306", "out"],
|
||||
["m307", "in"],
|
||||
["m308", "out"],
|
||||
["m309", "in"],
|
||||
["m310", "out"],
|
||||
]);
|
||||
assert.equal(view.marks.every((mark) => mark.state === "in" || mark.state === "out"), true);
|
||||
});
|
||||
|
||||
test("without inference marks every engine candidate is solid", () => {
|
||||
const view = buildRectificationTimeline({
|
||||
searchWindow: declaredWindow,
|
||||
credibleRange: ["05:07", "05:09"],
|
||||
@@ -108,12 +136,7 @@ test("candidate marks are binary and the range boundary counts as inside", () =>
|
||||
stage: "minute",
|
||||
});
|
||||
assert.ok(view);
|
||||
assert.deepEqual(view.marks.map((mark) => mark.state), ["out", "in", "in", "in", "out"]);
|
||||
// Closed interval: both ends of the range are in, not on the fence.
|
||||
assert.equal(view.marks[1]?.state, "in");
|
||||
assert.equal(view.marks[3]?.state, "in");
|
||||
// No third state exists to grade confidence with.
|
||||
assert.equal(view.marks.every((mark) => mark.state === "in" || mark.state === "out"), true);
|
||||
assert.deepEqual(view.marks.map((mark) => mark.state), ["in", "in", "in", "in", "in"]);
|
||||
});
|
||||
|
||||
test("marks outside the window are dropped and duplicates collapse", () => {
|
||||
@@ -135,11 +158,11 @@ test("block scan draws the window as the range and no minute marks", () => {
|
||||
stage: "block_scan",
|
||||
});
|
||||
assert.ok(view);
|
||||
// Saying "23 小时 59 分" here is the truth: nothing has been ruled out yet.
|
||||
// Inclusive 00:00–23:59 is a full day. Exclusive difference was "23 小时 59 分".
|
||||
assert.equal(view.bandStartPercent, 0);
|
||||
assert.equal(view.bandWidthPercent, 100);
|
||||
assert.equal(view.rangeLabel, "00:00–23:59");
|
||||
assert.equal(view.widthLabel, "23 小时 59 分");
|
||||
assert.equal(view.widthLabel, "24 小时");
|
||||
assert.deepEqual(view.marks, []);
|
||||
});
|
||||
|
||||
@@ -176,7 +199,8 @@ test("widening the window rescales the axis and keeps the band inside it", () =>
|
||||
// The widened range still fits: an axis pinned to the opening window would not hold it.
|
||||
assert.ok(after.bandStartPercent >= 0);
|
||||
assert.ok(after.bandStartPercent + after.bandWidthPercent <= 100);
|
||||
assert.equal(after.widthLabel, "24 分钟");
|
||||
// 04:56–05:20 inclusive is 25 minutes. Exclusive difference was "24 分钟".
|
||||
assert.equal(after.widthLabel, "25 分钟");
|
||||
});
|
||||
|
||||
test("a window that crosses midnight stays monotonic", () => {
|
||||
@@ -191,11 +215,126 @@ test("a window that crosses midnight stays monotonic", () => {
|
||||
assert.equal(view.axisStartLabel, "23:00");
|
||||
assert.equal(view.axisEndLabel, "03:59");
|
||||
assert.equal(view.rangeLabel, "00:30–01:30");
|
||||
assert.equal(view.widthLabel, "1 小时");
|
||||
assert.deepEqual(view.marks.map((mark) => mark.state), ["out", "in", "out"]);
|
||||
// Inclusive 00:30–01:30 is 61 minutes. Exclusive difference was "1 小时".
|
||||
assert.equal(view.widthLabel, "1 小时 1 分");
|
||||
assert.deepEqual(view.marks.map((mark) => mark.state), ["in", "in", "in"]);
|
||||
assert.ok(view.marks.every((mark) => mark.percent >= 0 && mark.percent <= 100));
|
||||
});
|
||||
|
||||
test("widthLabel counts both clock ends, matching the delivery-report inclusive minute shape", () => {
|
||||
const label = (
|
||||
searchWindow: readonly [string, string],
|
||||
credibleRange: readonly [string, string] | null,
|
||||
) => buildRectificationTimeline({
|
||||
searchWindow,
|
||||
credibleRange,
|
||||
candidateTimes: [],
|
||||
stage: credibleRange ? "minute" : "block_scan",
|
||||
})?.widthLabel;
|
||||
|
||||
assert.equal(label(["04:45", "05:15"], ["04:51", "04:59"]), "9 分钟");
|
||||
assert.equal(label(["04:45", "05:15"], ["05:07", "05:09"]), "3 分钟");
|
||||
assert.equal(label(["04:45", "05:15"], ["04:51", "04:53"]), "3 分钟");
|
||||
assert.equal(label(["04:45", "05:15"], ["05:07", "05:07"]), "1 分钟");
|
||||
assert.equal(label(["00:00", "23:59"], null), "24 小时");
|
||||
});
|
||||
|
||||
test("eliminated inference minutes stay as hollow dots and keep stable keys", () => {
|
||||
const searchWindow = ["04:45", "05:15"] as const;
|
||||
const credibleRange = ["04:51", "04:53"] as const;
|
||||
const times = [
|
||||
"04:45", "04:47", "04:49", "04:51", "04:52", "04:53", "04:55", "04:57", "04:59",
|
||||
] as const;
|
||||
const marksFor = (active: ReadonlySet<string>) => times.map((time) => ({
|
||||
time,
|
||||
eliminated: !active.has(time),
|
||||
}));
|
||||
|
||||
const first = buildRectificationTimeline({
|
||||
searchWindow,
|
||||
credibleRange,
|
||||
candidateTimes: ["04:51", "04:53"],
|
||||
inferenceMarks: marksFor(new Set(["04:51", "04:53"])),
|
||||
stage: "minute",
|
||||
});
|
||||
assert.ok(first);
|
||||
assert.equal(first.marks.length, 9);
|
||||
assert.equal(first.marks.filter((mark) => mark.state === "out").length, 7);
|
||||
assert.equal(first.marks.filter((mark) => mark.state === "in").length, 2);
|
||||
const firstKeys = first.marks.map((mark) => mark.key);
|
||||
|
||||
const second = buildRectificationTimeline({
|
||||
searchWindow,
|
||||
credibleRange: ["04:53", "04:53"],
|
||||
candidateTimes: ["04:53"],
|
||||
inferenceMarks: marksFor(new Set(["04:53"])),
|
||||
stage: "minute",
|
||||
});
|
||||
assert.ok(second);
|
||||
assert.equal(second.marks.length, 9);
|
||||
assert.equal(second.marks.filter((mark) => mark.state === "out").length, 8);
|
||||
assert.equal(second.marks.filter((mark) => mark.state === "in").length, 1);
|
||||
assert.deepEqual(second.marks.map((mark) => mark.key), firstKeys);
|
||||
});
|
||||
|
||||
test("timeline chat wiring reads parsed inferenceMarks and never the raw receipt", () => {
|
||||
const timelineBlock = chat.slice(
|
||||
chat.indexOf("const timelineView = buildRectificationTimeline"),
|
||||
chat.indexOf("const persistedOfferKey"),
|
||||
);
|
||||
assert.match(timelineBlock, /inferenceMarks:\s*candidateResult\?\.inferenceMarks/);
|
||||
assert.doesNotMatch(timelineBlock, /decisionReceipt|inference_state/);
|
||||
});
|
||||
|
||||
test("parsed inference_state marks reach the timeline even when engine candidates are only the active minutes", () => {
|
||||
const activeId = "88888888-8888-4888-8888-888888888881";
|
||||
const secondId = "88888888-8888-4888-8888-888888888882";
|
||||
const times = [
|
||||
"04:45", "04:47", "04:49", "04:51", "04:52", "04:53", "04:55", "04:57", "04:59",
|
||||
] as const;
|
||||
const result = parseRectificationCandidateResult({
|
||||
resultId: "11111111-1111-4111-8111-111111111111",
|
||||
candidates: [
|
||||
{ candidateId: activeId, rank: 1, time: "04:51", relativeSupport: 40, tiedMinuteCount: 3 },
|
||||
{ candidateId: secondId, rank: 2, time: "04:53", relativeSupport: 38, tiedMinuteCount: 3 },
|
||||
],
|
||||
overallConfidence: "low",
|
||||
selectionAllowed: false,
|
||||
canAdopt: false,
|
||||
confirmationAllowed: false,
|
||||
representativeTime: "04:53",
|
||||
selectedTime: null,
|
||||
selectionKind: null,
|
||||
credibleRange: ["04:51", "04:53"],
|
||||
decisionReceipt: {
|
||||
inference_state: {
|
||||
candidates: times.map((time) => ({
|
||||
time,
|
||||
status: time === "04:51" || time === "04:53" ? "active" : "eliminated",
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.ok(result);
|
||||
assert.ok(result.inferenceMarks);
|
||||
assert.equal(result.candidates.length, 2);
|
||||
assert.equal(result.inferenceMarks.length, 9);
|
||||
assert.equal(result.inferenceMarks.filter((mark) => mark.eliminated).length, 7);
|
||||
|
||||
const view = buildRectificationTimeline({
|
||||
searchWindow: declaredWindow,
|
||||
credibleRange: result.credibleRange,
|
||||
candidateTimes: result.candidates.map((candidate) => candidate.time),
|
||||
inferenceMarks: result.inferenceMarks,
|
||||
stage: "minute",
|
||||
});
|
||||
assert.ok(view);
|
||||
assert.equal(view.marks.length, 9);
|
||||
assert.equal(view.marks.filter((mark) => mark.state === "out").length, 7);
|
||||
assert.equal(view.marks.filter((mark) => mark.state === "in").length, 2);
|
||||
assert.equal(view.widthLabel, "3 分钟");
|
||||
});
|
||||
|
||||
test("duration reads as clock language, not raw minutes", () => {
|
||||
assert.equal(timelineDurationLabel(2), "2 分钟");
|
||||
assert.equal(timelineDurationLabel(59), "59 分钟");
|
||||
|
||||
@@ -19,11 +19,17 @@ import {
|
||||
createRectificationV9ReadOnlyTools,
|
||||
createRectificationV9Tools,
|
||||
} from "../src/mastra/rectification-v9-tools.ts";
|
||||
import { runV9AgentTurn, type V9AgentRunOptions } from "../src/lib/rectification-agentic/v9/agent-run.ts";
|
||||
import { persistV9Candidate } from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
||||
import { buildOpeningBrief, runV9AgentTurn, type V9AgentRunOptions } from "../src/lib/rectification-agentic/v9/agent-run.ts";
|
||||
import { parseV9CaseDossier, persistV9Candidate } from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
||||
import { RECTIFICATION_SKILL_NAME, RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts";
|
||||
import { composeCollectSpokenAssistantText } from "../src/lib/rectification-agentic/v9/collect-prompt.ts";
|
||||
import { GENERIC_COLLECT_QUESTION, RECTIFICATION_USER_COPY } from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import {
|
||||
GENERIC_COLLECT_QUESTION,
|
||||
OPENING_COLLECT_DOMAINS,
|
||||
RECTIFICATION_USER_COPY,
|
||||
isAcceptableOpeningBody,
|
||||
openingSpokenBody,
|
||||
} from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import {
|
||||
CASE_ID,
|
||||
CANDIDATE_RANGE,
|
||||
@@ -68,7 +74,8 @@ test("system prompt carries only high-priority boundaries, never the method copy
|
||||
assert.match(prompt, /思考用简体中文写在思维链/);
|
||||
assert.match(prompt, /对用户说的话必须自己写在正文里/);
|
||||
assert.match(prompt, /用 rectification-set-focus 的 spokenPrompt 写出服务端给你的下一问/);
|
||||
assert.match(prompt, /每轮正文 2-4 句/);
|
||||
// 旧:每轮正文 2-4 句 → 新:证据轮正文只写一句复述 → BUG-606 决策 3
|
||||
assert.match(prompt, /证据轮正文只写一句复述/);
|
||||
assert.match(prompt, /「先这样」由服务器/);
|
||||
assert.match(prompt, /collection_progress/);
|
||||
assert.match(prompt, /不得写「范围在收窄」/);
|
||||
@@ -88,11 +95,11 @@ test("system prompt carries only high-priority boundaries, never the method copy
|
||||
test("agent pins the dedicated rectification skill and its fixed version", () => {
|
||||
assert.equal(RECTIFICATION_V9_SKILL_NAME, "jyotish-birth-time-rectification");
|
||||
assert.equal(basename(RECTIFICATION_V9_SKILL_PATH), RECTIFICATION_V9_SKILL_NAME);
|
||||
assert.ok(RECTIFICATION_V9_PACKAGE_PATH.endsWith("skills/jyotish-birth-time-rectification/versions/10.0.18"));
|
||||
assert.ok(RECTIFICATION_V9_PACKAGE_PATH.endsWith("skills/jyotish-birth-time-rectification/versions/10.0.19"));
|
||||
assert.notEqual(RECTIFICATION_V9_SKILL_PATH, RECTIFICATION_V9_PACKAGE_PATH);
|
||||
assert.equal(realpathSync(RECTIFICATION_V9_SKILL_PATH), RECTIFICATION_V9_PACKAGE_PATH);
|
||||
assert.equal(RECTIFICATION_SKILL_NAME, "jyotish-birth-time-rectification");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.18");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.19");
|
||||
});
|
||||
|
||||
test("step budgets are bounded per action with a hard ceiling", () => {
|
||||
@@ -239,7 +246,13 @@ test("agent receives the exact server-owned case id for tool calls", async () =>
|
||||
const openingPrompt = JSON.stringify(observedMessages);
|
||||
assert.match(openingPrompt, new RegExp(CASE_ID));
|
||||
assert.match(openingPrompt, /服务端 opening brief/);
|
||||
assert.match(openingPrompt, /不要要求一次说完/);
|
||||
// 旧:不要要求一次说完、不要举大学/工作/搬家的例子
|
||||
// 新:窗口 + 六类领域 + 一条消息可以报多件
|
||||
// 原因:BUG-604 决策 1,重算按轮计费
|
||||
assert.match(openingPrompt, /一条消息可以报多件/);
|
||||
assert.match(openingPrompt, /升学、第一份工作、搬家、恋爱结婚、家里的大事、生病受伤/);
|
||||
assert.match(openingPrompt, /04:50–05:10|当前搜索窗口/);
|
||||
assert.match(openingPrompt, /intake 声明的不确定档/);
|
||||
assert.match(openingPrompt, /spokenPrompt 写出当前采集题/);
|
||||
assert.match(openingPrompt, /不要提问/);
|
||||
assert.doesNotMatch(openingPrompt, /接在正文末尾并写入聊天历史/);
|
||||
@@ -392,13 +405,17 @@ test("collect_spoken stem is persisted on the same turn via asked_turn_id, not a
|
||||
});
|
||||
const result = await runV9AgentTurn(options);
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.answerText, greeting);
|
||||
// 旧:模型一句招呼原样落库 → 新:开场正文必须过六类领域验收,不合格则换成模板
|
||||
// 原因:BUG-604 开场 ≤4 句、≥5 类、不含年份
|
||||
const spoken = openingSpokenBody(["04:50", "05:10"]);
|
||||
assert.equal(isAcceptableOpeningBody(result.answerText), true);
|
||||
assert.equal(result.answerText, spoken);
|
||||
const replaced = emitted.find((event) => (
|
||||
event.type === "answer.delta" && (event as { replace?: unknown }).replace === true
|
||||
)) as { text?: string } | undefined;
|
||||
assert.notEqual(replaced?.text, composeCollectSpokenAssistantText(greeting, GENERIC_COLLECT_QUESTION));
|
||||
const finalize = accounting.calls.find((call) => call.fn === "finalize_agentic_rectification_turn");
|
||||
assert.equal(finalize?.args.p_assistant_message, greeting);
|
||||
assert.equal(finalize?.args.p_assistant_message, spoken);
|
||||
assert.equal(
|
||||
accounting.calls.some((call) => (
|
||||
call.fn === "set_agentic_rectification_conversation_focus"
|
||||
@@ -785,3 +802,52 @@ test("agent-run uses collectHandoff when the owned-focus body is only a question
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.answerText, RECTIFICATION_USER_COPY.collectHandoff);
|
||||
});
|
||||
|
||||
test("buildOpeningBrief names the search window, intake source, and six domains", () => {
|
||||
const dossier = parseV9CaseDossier(dossierFixture({
|
||||
candidateRange: { start_time: "04:45", end_time: "05:15" },
|
||||
evidence: [],
|
||||
evidenceCount: 0,
|
||||
}));
|
||||
assert.ok(dossier);
|
||||
const brief = buildOpeningBrief(dossier);
|
||||
assert.match(brief, /当前搜索窗口:04:45–05:15/);
|
||||
assert.match(brief, /intake 声明的不确定档/);
|
||||
for (const domain of OPENING_COLLECT_DOMAINS) {
|
||||
assert.match(brief, new RegExp(domain));
|
||||
}
|
||||
assert.match(brief, /先说你最容易想起的一两件,年月大概就行/);
|
||||
assert.doesNotMatch(brief, /不要要求一次说完/);
|
||||
assert.doesNotMatch(brief, /不要举大学、工作、搬家的例子/);
|
||||
assert.doesNotMatch(brief, /(?:19|20)\d{2}/);
|
||||
});
|
||||
|
||||
test("evidence persist keeps the first sentence when the model writes three", async () => {
|
||||
const accounting = fakeAccounting({
|
||||
...receiptHandlers,
|
||||
get_agentic_rectification_case_dossier: () => dossierFixture(),
|
||||
append_agentic_rectification_turn: () => ({ turn_id: TURN_ID }),
|
||||
finalize_agentic_rectification_turn: () => ({ turn_id: TURN_ID, status: "completed", idempotent: false }),
|
||||
});
|
||||
const { options } = runOptions({
|
||||
action: "evidence",
|
||||
accounting: accounting.client,
|
||||
buildAgent: async () => fakeAgentStream([
|
||||
chunk("start"),
|
||||
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
|
||||
chunk("tool-result", { toolName: "rectification-read-case" }),
|
||||
chunk("text-delta", {
|
||||
text: "记下了:2016 年 9 月入学、2020 年 6 月毕业。这对校正很有帮助。接下来我们继续。",
|
||||
}),
|
||||
chunk("finish"),
|
||||
]) as never,
|
||||
});
|
||||
const result = await runV9AgentTurn(options);
|
||||
assert.equal(result.ok, true);
|
||||
assert.match(result.answerText, /^记下了:2016 年 9 月入学、2020 年 6 月毕业。/);
|
||||
assert.doesNotMatch(result.answerText, /很有帮助|很有价值|很有分量|特别有用/);
|
||||
assert.doesNotMatch(result.answerText, /接下来我们继续/);
|
||||
const finalize = accounting.calls.find((call) => call.fn === "finalize_agentic_rectification_turn");
|
||||
assert.match(String(finalize?.args.p_assistant_message ?? ""), /^记下了:2016 年 9 月入学、2020 年 6 月毕业。/);
|
||||
assert.doesNotMatch(String(finalize?.args.p_assistant_message ?? ""), /很有帮助|特别有用/);
|
||||
});
|
||||
|
||||
@@ -95,9 +95,9 @@ test("terminal transitions are one-way and evidence writes stop at terminal", ()
|
||||
|
||||
test("the active rectification skill pins the v10 identity and lives in the right directory", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_NAME, "jyotish-birth-time-rectification");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.18");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.19");
|
||||
assert.match(skill, /^---\nname: jyotish-birth-time-rectification/m);
|
||||
assert.match(skill, /^version: 10\.0\.18$/m);
|
||||
assert.match(skill, /^version: 10\.0\.19$/m);
|
||||
assert.match(skill, /至多一个主问题且唯一来源:[\s\S]*不得自行提出、复述、改写或预告问题/);
|
||||
for (const reference of references) {
|
||||
const content = readFileSync(`${skillDirectory}/references/${reference}`, "utf8");
|
||||
|
||||
@@ -209,7 +209,7 @@ test("open RPC passes the pinned skill and server-derived baseline only", async
|
||||
session_id: SESSION_ID,
|
||||
status: "draft",
|
||||
should_start_opening: true,
|
||||
skill_version: "10.0.18",
|
||||
skill_version: "10.0.19",
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -247,11 +247,11 @@ test("open RPC passes the pinned skill and server-derived baseline only", async
|
||||
});
|
||||
assert.equal(response.disposition, "created");
|
||||
assert.equal(response.shouldStartOpening, true);
|
||||
assert.equal(response.skillVersion, "10.0.18");
|
||||
assert.equal(response.skillVersion, "10.0.19");
|
||||
const openCall = accounting.calls.find((call) => call.fn === "open_agentic_rectification_case_v2");
|
||||
assert.ok(openCall);
|
||||
assert.equal(openCall.args.p_skill_name, "jyotish-birth-time-rectification");
|
||||
assert.equal(openCall.args.p_skill_version, "10.0.18");
|
||||
assert.equal(openCall.args.p_skill_version, "10.0.19");
|
||||
assert.equal(openCall.args.p_user_id, "user-1");
|
||||
// The server derives the baseline; the request never carries it from the browser.
|
||||
assert.equal("birth_date" in openCall.args, false);
|
||||
|
||||
@@ -1404,11 +1404,14 @@ test("Chinese interview planning stays on reasoning-delta; the spoken answer is
|
||||
});
|
||||
const result = await runV9AgentTurn(options);
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.answerText, spoken);
|
||||
assert.deepEqual(
|
||||
emitted.filter((event) => event.type === "answer.delta"),
|
||||
[{ type: "answer.delta", text: spoken }],
|
||||
);
|
||||
// 旧:模型正文含追问原样落库 → 新:证据轮超过两句只留首句
|
||||
// 原因:BUG-606 服务端裁剪,不靠提示词
|
||||
const recap = "职业类型已经记下。";
|
||||
assert.equal(result.answerText, recap);
|
||||
const spokenDeltas = emitted.filter((event) => event.type === "answer.delta");
|
||||
assert.equal(spokenDeltas[0]?.text, spoken);
|
||||
assert.equal(spokenDeltas.at(-1)?.text, recap);
|
||||
assert.equal((spokenDeltas.at(-1) as { replace?: boolean } | undefined)?.replace, true);
|
||||
const publicText = JSON.stringify(emitted);
|
||||
assert.doesNotMatch(publicText, /occupation_note/);
|
||||
assert.doesNotMatch(publicText, /让我继续访谈/);
|
||||
@@ -1575,11 +1578,13 @@ test("model terminal text-delta is the reply even when Case evidence is availabl
|
||||
});
|
||||
const result = await runV9AgentTurn(options);
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.answerText, spoken);
|
||||
assert.deepEqual(
|
||||
emitted.filter((event) => event.type === "answer.delta"),
|
||||
[{ type: "answer.delta", text: spoken }],
|
||||
);
|
||||
// 旧:模型正文含入职追问原样落库 → 新:证据轮超过两句只留首句
|
||||
// 原因:BUG-606 服务端裁剪
|
||||
const recap = "职业已经记下。";
|
||||
assert.equal(result.answerText, recap);
|
||||
const spokenDeltas = emitted.filter((event) => event.type === "answer.delta");
|
||||
assert.equal(spokenDeltas[0]?.text, spoken);
|
||||
assert.equal(spokenDeltas.at(-1)?.text, recap);
|
||||
assert.doesNotMatch(result.answerText, /已经记下:/);
|
||||
assert.doesNotMatch(result.answerText, /2016年9月离家去北京开始工作/);
|
||||
});
|
||||
|
||||
@@ -379,3 +379,21 @@ test("choice option focus ring stays inside the button and does not stack on the
|
||||
assert.doesNotMatch(card, /firstChoiceRef/);
|
||||
assert.doesNotMatch(card, /\.focus\(/);
|
||||
});
|
||||
|
||||
test("vargaSentence stays in expanded activity rows, not the spoken bubble", () => {
|
||||
const content = readFileSync(new URL("../src/components/chat-message-content.tsx", import.meta.url), "utf8");
|
||||
const activity = readFileSync(new URL("../src/components/agent-activity-status.tsx", import.meta.url), "utf8");
|
||||
const timeline = readFileSync(new URL("../src/components/consultation-run-timeline.tsx", import.meta.url), "utf8");
|
||||
const row = readFileSync(new URL("../src/components/chat-message-row.tsx", import.meta.url), "utf8");
|
||||
// 旧:ChatMessageContent 渲染 message-varga-sentence
|
||||
// 新:气泡正文不再带方法句;活动记录展开区保留
|
||||
// 原因:BUG-606 决策 3(a)
|
||||
assert.doesNotMatch(content, /vargaSentence/);
|
||||
assert.doesNotMatch(content, /message-varga-sentence/);
|
||||
assert.match(activity, /vargaSentence/);
|
||||
assert.match(activity, /VargaTraceStep/);
|
||||
assert.match(timeline, /vargaSentence/);
|
||||
assert.match(timeline, /inert=\{open \? undefined : true\}/);
|
||||
assert.match(row, /<ConsultationRunTimeline[\s\S]*vargaSentence=\{vargaSentence\}/);
|
||||
assert.doesNotMatch(row, /vargaSentence=\{showThinkingPanel \? null : vargaSentence\}/);
|
||||
});
|
||||
|
||||
@@ -417,7 +417,9 @@ test("agent instructions do not assert that the next question is already on scre
|
||||
const agent = readFileSync(new URL("../src/mastra/agentic-rectification.ts", import.meta.url), "utf8");
|
||||
const chat = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8");
|
||||
assert.match(agent, /正文不得断言界面当前状态/);
|
||||
assert.match(agent, /接下来我们继续/);
|
||||
// 旧:用「接下来我们继续」垫句 → 新:题干作为同一条消息下一段出现,不再垫过渡句
|
||||
// 原因:BUG-606 每轮一句,开场/证据轮不再写「接下来我们继续」
|
||||
assert.match(agent, /题干会作为同一条消息的下一段自动出现/);
|
||||
assert.doesNotMatch(agent, /自然过渡到界面上的下一步/);
|
||||
assert.match(chat, /await loadCaseSnapshot\(\)/);
|
||||
assert.match(chat, /mergeTurnQuestions/);
|
||||
|
||||
@@ -85,8 +85,8 @@ test("checked-in registry verifies hashed product packages and leaves consult on
|
||||
[
|
||||
{
|
||||
name: "jyotish-birth-time-rectification",
|
||||
version: "10.0.18",
|
||||
sha256: "ad209a7824d7eb65f35efd7d6245e9a3752a8b32972de0fbafb751691e86a1ad",
|
||||
version: "10.0.19",
|
||||
sha256: "a68885d3cf2110ee456bff65210c60e6bef4da4ea45e2a790a5462fd6d7307c8",
|
||||
},
|
||||
{
|
||||
name: "jyotish-personal-report",
|
||||
|
||||
Reference in New Issue
Block a user