fix(rectification): remove static interview fallbacks
This commit is contained in:
@@ -116,7 +116,8 @@ test("failed server turns retain their receipt state without parsing activity fr
|
||||
test("candidate acceptance copy never presents adoption as confirmation", () => {
|
||||
assert.match(chatSource, /当前可能的出生时间/);
|
||||
assert.match(chatSource, /采用不等于确认出生时间/);
|
||||
assert.match(chatSource, /当前排盘时间(代表性时间,本会话不确认唯一分钟)/);
|
||||
assert.match(chatSource, /rectification-candidate-badge">已采用/);
|
||||
assert.match(chatSource, /已确认校正时间/);
|
||||
assert.doesNotMatch(chatSource, /本轮校正已收口|当前排盘时间(代表性时间|本会话以代表性时间收口/);
|
||||
assert.doesNotMatch(chatSource, /candidateResult\.confirmationAllowed \? "确认校正时间"/);
|
||||
});
|
||||
|
||||
@@ -398,8 +398,8 @@ test("completed Agent replies restore feedback, copy and safe in-place regenerat
|
||||
|
||||
test("candidate state renders from the snapshot API and never from sentinels", () => {
|
||||
assert.match(chat, /当前可能的出生时间/);
|
||||
assert.match(chat, /本会话以代表性时间收口,不确认唯一分钟/);
|
||||
assert.match(chat, /可以先采用,也可以继续补充事件或改选/);
|
||||
assert.doesNotMatch(chat, /本会话以代表性时间收口,不确认唯一分钟/);
|
||||
assert.doesNotMatch(chat, /当前排盘时间(代表性时间,本会话不确认唯一分钟)/);
|
||||
assert.match(chat, /相对支持度不是统计概率/);
|
||||
assert.match(chat, /改选为此时间/);
|
||||
assert.doesNotMatch(chat, /disabled=\{Boolean\(candidateResult\.selectedTime\)/);
|
||||
@@ -616,8 +616,12 @@ test("rectification Agent output stays natural and keeps tool execution silent",
|
||||
assert.match(agent, /本轮做了什么/);
|
||||
assert.match(agent, /完成凭证完全由服务端公开 Activity\/receipt 展示/);
|
||||
assert.match(agent, /禁止只说记下了、会话会保留、以后再继续/);
|
||||
assert.match(agent, /没有 \/ 没有了 \/ 没有其它 \/ 想不起来了.*只关闭当前 focus 或当前领域/);
|
||||
assert.match(agent, /结束校正 \/ 不想继续 \/ 直接给结果 \/ 就到这里.*on_user_stop/);
|
||||
assert.match(agent, /根据用户自然语言语义区分“当前问题没有证据”“停止整个证据收集”和“恢复继续校正”/);
|
||||
assert.match(agent, /rectification-read-case 传 resume=true/);
|
||||
assert.doesNotMatch(route, /action === "message" && caseStatus === "paused"/);
|
||||
assert.match(agent, /前者调用 rectification-resolve-focus/);
|
||||
assert.match(agent, /全局停止则调用 rectification-stop-and-review/);
|
||||
assert.doesNotMatch(agent, /USER_STOP_PATTERN|USER_STOP_NEGATION_PATTERN/);
|
||||
assert.match(agent, /skill_verification_report/);
|
||||
assert.match(agent, /D9\/D10 类型对照/);
|
||||
assert.doesNotMatch(agent, /分盘句和宫位表由界面展示/);
|
||||
@@ -677,7 +681,7 @@ test("adopted time offers a consultation handoff without unique-minute copy", ()
|
||||
assert.match(page, /startConsultationAfterRectification/);
|
||||
assert.match(page, /createSession\(modelCatalog\.defaultModelId\)/);
|
||||
assert.match(agent, /start_consultation/);
|
||||
assert.match(agent, /本会话以代表性时间收口,不确认唯一分钟/);
|
||||
assert.doesNotMatch(agent, /本会话以代表性时间收口|本轮校正已收口/);
|
||||
});
|
||||
|
||||
test("stream failures remove empty assistant placeholders", () => {
|
||||
|
||||
@@ -187,7 +187,7 @@ test("adopted minute recasts the house table and keeps technique audit collapsed
|
||||
technique_audit_table: [
|
||||
{ technique: "D1 本命盘", status: "executed", note: "本轮已按该分钟重算本命宫位。" },
|
||||
{ technique: "KP 宫头", status: "blocked", note: "KP 宫头本轮未计算。" },
|
||||
{ technique: "唯一分钟确认", status: "blocked", note: "本会话以代表性时间收口,不确认唯一分钟。" },
|
||||
{ technique: "唯一分钟确认", status: "blocked", note: "当前确认门未通过。" },
|
||||
],
|
||||
precision_stage: {
|
||||
current: "d4_refine",
|
||||
|
||||
@@ -17,12 +17,19 @@ import { buildMethodFollowupPlan, projectRectificationChoiceCard } from "../src/
|
||||
import { choiceCardFromCaseDossier } from "../src/lib/rectification-agentic/v9/interview-state.ts";
|
||||
import type { DiscriminatingEventProbe } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
|
||||
|
||||
const DYNAMIC_STYLE_OPTIONS = [
|
||||
{ label: "明确发生且时间吻合", answer_class: "yes" },
|
||||
{ label: "发生过但程度较弱", answer_class: "weak_yes" },
|
||||
{ label: "明确没有发生", answer_class: "no" },
|
||||
{ label: "这段记不清楚", answer_class: "unsure" },
|
||||
] as const;
|
||||
|
||||
const SAMPLE_COPY = {
|
||||
prompt: "2016 年前后,有没有明显搬家、离乡或长期异地?",
|
||||
option_a: "是,大概就在那段时间",
|
||||
option_b: "有类似,但年份不对或不够重大",
|
||||
option_c: "没有明显发生",
|
||||
option_d: "不记得 / 不确定",
|
||||
option_a: DYNAMIC_STYLE_OPTIONS[0].label,
|
||||
option_b: DYNAMIC_STYLE_OPTIONS[1].label,
|
||||
option_c: DYNAMIC_STYLE_OPTIONS[2].label,
|
||||
option_d: DYNAMIC_STYLE_OPTIONS[3].label,
|
||||
} as const;
|
||||
|
||||
const FOCUS_ID = "abababab-abab-4bab-8bab-abababababab";
|
||||
@@ -44,6 +51,7 @@ const MOVE_PROBE: DiscriminatingEventProbe = {
|
||||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:20"] },
|
||||
{ answer_class: "no", supports: ["05:20"], conflicts: ["05:00"] },
|
||||
],
|
||||
style_options: DYNAMIC_STYLE_OPTIONS,
|
||||
};
|
||||
|
||||
test("choice frames ask one biographical event from a server probe, not competing charts", () => {
|
||||
@@ -70,12 +78,13 @@ test("choice frames ask one biographical event from a server probe, not competin
|
||||
probes: [MOVE_PROBE],
|
||||
},
|
||||
);
|
||||
assert.ok(frame);
|
||||
assert.equal(frame.period, "2016 年前后");
|
||||
assert.match(frame.why, /年份锁定/);
|
||||
assert.match(frame.why, /请写成/);
|
||||
assert.doesNotMatch(frame.why, /^2016 年前后,有没有明显/);
|
||||
assert.match(frame.option_a_hint, /是,大概就在那段时间/);
|
||||
assert.match(frame.option_b_hint, /年份不对或不够重大/);
|
||||
assert.equal(frame.option_a_hint, DYNAMIC_STYLE_OPTIONS[0].label);
|
||||
assert.equal(frame.option_b_hint, DYNAMIC_STYLE_OPTIONS[1].label);
|
||||
assert.doesNotMatch(frame.option_a_hint + frame.option_b_hint, /更像哪一件|可能性/);
|
||||
assert.doesNotMatch(frame.why, /更像哪一件/);
|
||||
assert.equal(/外貌|疤痕|胎记/.test(frame.option_a_hint + frame.option_b_hint), false);
|
||||
@@ -93,7 +102,9 @@ test("server-owned card names the event family, not a generic 有没有这件事
|
||||
},
|
||||
{ probes: [MOVE_PROBE] },
|
||||
);
|
||||
const copy = serverOwnedChoiceCopy(frame);
|
||||
assert.ok(frame);
|
||||
if (!frame) assert.fail("expected dynamic choice frame");
|
||||
const copy = serverOwnedChoiceCopy(frame!);
|
||||
assert.ok(copy);
|
||||
assert.match(copy.prompt, /搬家、离乡或长期异地/);
|
||||
assert.doesNotMatch(copy.prompt, /有没有这件事/);
|
||||
@@ -116,6 +127,7 @@ test("the visible card uses Agent copy for A/B/C/D, not the server hint", () =>
|
||||
}],
|
||||
probes: [MOVE_PROBE],
|
||||
});
|
||||
assert.ok(frame);
|
||||
const card = mergeChoiceCard(frame, SAMPLE_COPY);
|
||||
assert.equal(card?.prompt, SAMPLE_COPY.prompt);
|
||||
assert.equal(card?.options[0]?.label, SAMPLE_COPY.option_a);
|
||||
@@ -133,14 +145,20 @@ test("holdout messages are prefixed and do not count as scoring quotes", () => {
|
||||
domain: "family",
|
||||
user_prompt_hint: "unused",
|
||||
},
|
||||
{ scoring: false },
|
||||
{
|
||||
scoring: false,
|
||||
probes: [{
|
||||
...MOVE_PROBE,
|
||||
domain: "family",
|
||||
event_family: "家人结婚、添丁或住院等重要变动",
|
||||
user_meaning: "盘外核对:只核对家人事件,不计入候选评分。",
|
||||
}],
|
||||
},
|
||||
);
|
||||
assert.ok(frame);
|
||||
const card = mergeChoiceCard(frame, {
|
||||
...SAMPLE_COPY,
|
||||
prompt: "家人这条线还没用过,有没有一件记得大概年份的变化?",
|
||||
option_a: "是,大概就在那段时间",
|
||||
option_b: "有类似,但年份不对或不够重大",
|
||||
option_c: "没有明显发生",
|
||||
option_d: "不记得 / 不确定",
|
||||
});
|
||||
assert.ok(card);
|
||||
const message = choiceCardUserMessage(card, "A");
|
||||
@@ -169,11 +187,13 @@ test("reverse-verify after adopt scores the predicted event and invites 改选 o
|
||||
tracks: ["vimshottari", "narayana"],
|
||||
tracks_agree: true,
|
||||
unique_minute_claim: false,
|
||||
user_meaning: "年份锁定 2018 年前后。",
|
||||
user_meaning: "按当前采用时间核对 2018 年前后的事业变化;若不吻合可以改选。",
|
||||
role: "reverse_verify",
|
||||
style_options: DYNAMIC_STYLE_OPTIONS,
|
||||
}],
|
||||
},
|
||||
);
|
||||
assert.ok(frame);
|
||||
assert.equal(frame.period, "2018 年前后");
|
||||
assert.equal(frame.scoring, true);
|
||||
assert.match(frame.why, /按当前采用时间核对/);
|
||||
@@ -198,60 +218,56 @@ test("life period only reuses years from the same domain", () => {
|
||||
], "relocation"), "那段时间");
|
||||
});
|
||||
|
||||
test("known-event quality probe uses the recorded year, not a discriminator split", () => {
|
||||
const frame = buildChoiceFrame({
|
||||
method_id: "d5_education",
|
||||
ask_theme: "education_style",
|
||||
domain: "education",
|
||||
user_prompt_hint: "unused",
|
||||
choice_kind: "event_quality",
|
||||
}, {
|
||||
evidence: [{
|
||||
status: "confirmed",
|
||||
domain: "education",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2015-06-01",
|
||||
occurredTo: null,
|
||||
}],
|
||||
probes: [{
|
||||
year: 2015,
|
||||
year_label: "2015 年前后",
|
||||
domain: "education",
|
||||
event_family: "高考或重要考试发挥明显失常、压力很大",
|
||||
source: "known_event_quality",
|
||||
tracks: ["vimshottari", "narayana"],
|
||||
tracks_agree: true,
|
||||
unique_minute_claim: false,
|
||||
user_meaning: "年份锁定 2015 年前后。已有相关经历。请按事件家族改写成自然语言。不得改年份。",
|
||||
role: "clarify",
|
||||
phase: "event_clarification",
|
||||
choice_kind: "event_quality",
|
||||
}],
|
||||
});
|
||||
assert.equal(frame.period, "2015 年前后");
|
||||
assert.equal(frame.choice_kind, "event_quality");
|
||||
assert.doesNotMatch(`${frame.why}${frame.option_a_hint}${frame.option_b_hint}`, /更像哪一件/);
|
||||
});
|
||||
|
||||
test("age-band fallback uses birth date, not another event's year", () => {
|
||||
const frame = buildChoiceFrame({
|
||||
test("choice frame requires a real non-empty event family", () => {
|
||||
const missingFamily = { ...MOVE_PROBE, event_family: undefined } as unknown as DiscriminatingEventProbe;
|
||||
assert.equal(buildChoiceFrame({
|
||||
method_id: "d4_home",
|
||||
ask_theme: "home_change",
|
||||
domain: "relocation",
|
||||
user_prompt_hint: "unused",
|
||||
}, {
|
||||
birthDate: "1997-08-08",
|
||||
evidence: [{
|
||||
status: "confirmed",
|
||||
domain: "education",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2016-01-01",
|
||||
occurredTo: null,
|
||||
}],
|
||||
});
|
||||
assert.equal(frame.period, "2018 年前后");
|
||||
assert.match(frame.option_a_hint, /是,大概就在那段时间/);
|
||||
assert.doesNotMatch(frame.period, /2016/);
|
||||
}, { probes: [missingFamily] }), null);
|
||||
assert.equal(buildChoiceFrame({
|
||||
method_id: "d4_home",
|
||||
ask_theme: "home_change",
|
||||
domain: "relocation",
|
||||
user_prompt_hint: "unused",
|
||||
}, { probes: [{ ...MOVE_PROBE, event_family: " " }] }), null);
|
||||
});
|
||||
|
||||
test("choice frame rejects incomplete, duplicate-class, illegal, and duplicate-label style options", () => {
|
||||
const build = (style_options: DiscriminatingEventProbe["style_options"]) => buildChoiceFrame({
|
||||
method_id: "d4_home",
|
||||
ask_theme: "home_change",
|
||||
domain: "relocation",
|
||||
user_prompt_hint: "unused",
|
||||
}, { probes: [{ ...MOVE_PROBE, style_options }] });
|
||||
|
||||
assert.equal(build(DYNAMIC_STYLE_OPTIONS.slice(0, 3)), null);
|
||||
assert.equal(build([
|
||||
...DYNAMIC_STYLE_OPTIONS,
|
||||
{ label: "另一种明确发生", answer_class: "yes" },
|
||||
]), null);
|
||||
assert.equal(build([
|
||||
{ ...DYNAMIC_STYLE_OPTIONS[0], label: "外貌更接近第一种" },
|
||||
...DYNAMIC_STYLE_OPTIONS.slice(1),
|
||||
]), null);
|
||||
assert.equal(build([
|
||||
DYNAMIC_STYLE_OPTIONS[0],
|
||||
{ ...DYNAMIC_STYLE_OPTIONS[1], label: DYNAMIC_STYLE_OPTIONS[0].label },
|
||||
DYNAMIC_STYLE_OPTIONS[2],
|
||||
DYNAMIC_STYLE_OPTIONS[3],
|
||||
]), null);
|
||||
});
|
||||
|
||||
test("style options without a real event probe do not generate a card", () => {
|
||||
assert.equal(buildChoiceFrame({
|
||||
method_id: "d10_career",
|
||||
ask_theme: "career_style",
|
||||
domain: "career",
|
||||
user_prompt_hint: "unused",
|
||||
choice_kind: "varga_style",
|
||||
style_options: DYNAMIC_STYLE_OPTIONS,
|
||||
}), null);
|
||||
});
|
||||
|
||||
test("agent choice copy rejects appearance, scars, clock times, and incomplete C/D", () => {
|
||||
@@ -323,13 +339,14 @@ test("GET hides a legacy known-event quality focus and resumes evidence collecti
|
||||
year: 2016,
|
||||
year_label: "2016 年前后",
|
||||
domain: "education",
|
||||
event_family: "高考或重要考试发挥明显失常、压力很大",
|
||||
event_family: "升学结果或学习环境出现明显变化",
|
||||
source: "known_event_quality",
|
||||
tracks: ["vimshottari", "narayana"],
|
||||
tracks_agree: true,
|
||||
unique_minute_claim: false,
|
||||
user_meaning: "已知事件质量",
|
||||
role: "clarify",
|
||||
style_options: DYNAMIC_STYLE_OPTIONS,
|
||||
phase: "event_clarification",
|
||||
choice_kind: "event_quality",
|
||||
semantic_key: "education.2016",
|
||||
@@ -432,6 +449,7 @@ test("GET card stays hidden on an empty ledger even if the Agent already wrote c
|
||||
unique_minute_claim: false,
|
||||
user_meaning: "年份锁定 2018 年前后。事件家族:认真关系进入、结束或关系观明显转变。请写成一句自然语言是/否题。不得改年份。",
|
||||
role: "distinguish",
|
||||
style_options: DYNAMIC_STYLE_OPTIONS,
|
||||
information_gain: 0.4,
|
||||
candidate_ids: ["05:00", "05:20"],
|
||||
expected_outcomes: [
|
||||
@@ -636,6 +654,7 @@ test("GET reverse-verify card still appears after a time is accepted", () => {
|
||||
unique_minute_claim: false,
|
||||
user_meaning: "年份锁定 2018 年前后。请写成一句自然语言,问是否入职或职责加重。",
|
||||
role: "reverse_verify",
|
||||
style_options: DYNAMIC_STYLE_OPTIONS,
|
||||
}],
|
||||
activeFocus: {
|
||||
id: FOCUS_ID,
|
||||
@@ -650,10 +669,10 @@ test("GET reverse-verify card still appears after a time is accepted", () => {
|
||||
});
|
||||
|
||||
test("choice card user messages expose A/B/C/D as a leading key", () => {
|
||||
assert.equal(parseChoiceKeyFromUserMessage("C. 没有明显发生"), "C");
|
||||
assert.equal(parseChoiceKeyFromUserMessage("D、不记得 / 不确定"), "D");
|
||||
assert.equal(parseChoiceKeyFromUserMessage(`${HOLDOUT_MESSAGE_PREFIX}:B. 有类似但年份不对`), "B");
|
||||
assert.equal(parseChoiceKeyFromUserMessage("没有明显发生"), null);
|
||||
assert.equal(parseChoiceKeyFromUserMessage(`C. ${DYNAMIC_STYLE_OPTIONS[2].label}`), "C");
|
||||
assert.equal(parseChoiceKeyFromUserMessage(`D、${DYNAMIC_STYLE_OPTIONS[3].label}`), "D");
|
||||
assert.equal(parseChoiceKeyFromUserMessage(`${HOLDOUT_MESSAGE_PREFIX}:B. ${DYNAMIC_STYLE_OPTIONS[1].label}`), "B");
|
||||
assert.equal(parseChoiceKeyFromUserMessage(DYNAMIC_STYLE_OPTIONS[2].label), null);
|
||||
});
|
||||
|
||||
test("GET choice_card stays after coverage when remaining minutes still split on D24", () => {
|
||||
@@ -716,6 +735,28 @@ test("GET choice_card stays after coverage when remaining minutes still split on
|
||||
],
|
||||
decisionReceipt: {
|
||||
propose_allowed: true,
|
||||
discriminating_event_probes: [{
|
||||
year: 2017,
|
||||
year_label: "2017 年前后",
|
||||
domain: "education",
|
||||
event_family: "学习方向或升学环境出现明显变化",
|
||||
source: "dasha_activation",
|
||||
tracks: ["vimshottari", "narayana"],
|
||||
tracks_agree: true,
|
||||
unique_minute_claim: false,
|
||||
user_meaning: "年份锁定 2017 年前后,核对学习方向或升学环境变化。",
|
||||
role: "distinguish",
|
||||
phase: "candidate_discriminator",
|
||||
information_gain: 0.16,
|
||||
semantic_key: "varga.d24.05:00/05:06|05:07",
|
||||
candidate_split_hash: "varga.d24.05:00/05:06|05:07",
|
||||
candidate_ids: ["05:00", "05:06", "05:07"],
|
||||
expected_outcomes: [
|
||||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:06", "05:07"] },
|
||||
{ answer_class: "no", supports: ["05:06", "05:07"], conflicts: ["05:00"] },
|
||||
],
|
||||
style_options: DYNAMIC_STYLE_OPTIONS,
|
||||
}],
|
||||
window_scan: {
|
||||
scanned: true,
|
||||
d9_lagna_count: 1,
|
||||
@@ -845,6 +886,7 @@ test("dynamic option labels keep the A/B/C/D answer-class contract", () => {
|
||||
{ label: "发生过但程度较弱", answer_class: "weak_yes" },
|
||||
],
|
||||
}, { probes: [MOVE_PROBE] });
|
||||
assert.ok(frame);
|
||||
const copy = serverOwnedChoiceCopy(frame);
|
||||
assert.ok(copy);
|
||||
assert.equal(copy.option_a, "明确发生且时间吻合");
|
||||
@@ -853,78 +895,6 @@ test("dynamic option labels keep the A/B/C/D answer-class contract", () => {
|
||||
assert.equal(copy.option_d, "这段记不清楚");
|
||||
});
|
||||
|
||||
test("remaining D10 style card uses type-table labels instead of existence", () => {
|
||||
const frame = buildChoiceFrame({
|
||||
method_id: "d10_career",
|
||||
ask_theme: "career_style",
|
||||
domain: "career",
|
||||
user_prompt_hint: "unused",
|
||||
choice_kind: "varga_style",
|
||||
style_options: [
|
||||
{ label: "照顾、情感", answer_class: "yes", sign: "巨蟹座" },
|
||||
{ label: "领导、表演", answer_class: "weak_yes", sign: "狮子座" },
|
||||
{ label: "细致、服务", answer_class: "no", sign: "处女座" },
|
||||
],
|
||||
});
|
||||
const copy = serverOwnedChoiceCopy(frame);
|
||||
assert.ok(copy);
|
||||
assert.equal(frame.choice_kind, "varga_style");
|
||||
assert.match(copy.prompt, /长期工作更接近哪一类/);
|
||||
assert.doesNotMatch(copy.prompt, /有没有明显/);
|
||||
assert.equal(copy.option_a, "照顾、情感");
|
||||
assert.equal(copy.option_b, "领导、表演");
|
||||
assert.equal(copy.option_c, "细致、服务");
|
||||
});
|
||||
|
||||
test("two-way remaining D9 puts 都不像 on C", () => {
|
||||
const frame = buildChoiceFrame({
|
||||
method_id: "d9_relationship",
|
||||
ask_theme: "relationship_style",
|
||||
domain: "relationship",
|
||||
user_prompt_hint: "unused",
|
||||
choice_kind: "varga_style",
|
||||
style_options: [
|
||||
{ label: "情感丰富、家庭导向", answer_class: "yes", sign: "巨蟹座" },
|
||||
{ label: "骄傲、戏剧性、领导欲", answer_class: "weak_yes", sign: "狮子座" },
|
||||
],
|
||||
});
|
||||
const copy = serverOwnedChoiceCopy(frame);
|
||||
assert.ok(copy);
|
||||
assert.match(copy.prompt, /相处/);
|
||||
assert.match(copy.option_c, /都不像/);
|
||||
assert.match(copy.option_d, /不确定/);
|
||||
});
|
||||
|
||||
test("known exam quality card keeps the recorded year and quality mapping", () => {
|
||||
const frame = buildChoiceFrame({
|
||||
method_id: "d5_education",
|
||||
ask_theme: "education_style",
|
||||
domain: "education",
|
||||
user_prompt_hint: "unused",
|
||||
choice_kind: "event_quality",
|
||||
}, {
|
||||
probes: [{
|
||||
year: 2015,
|
||||
year_label: "2015 年前后",
|
||||
domain: "education",
|
||||
event_family: "高考或重要考试发挥明显失常、压力很大",
|
||||
source: "known_event_quality",
|
||||
tracks: ["vimshottari", "narayana"],
|
||||
tracks_agree: true,
|
||||
unique_minute_claim: false,
|
||||
user_meaning: "年份锁定 2015 年前后。已有相关经历。请按事件家族改写成自然语言。不得改年份。",
|
||||
role: "clarify",
|
||||
phase: "event_clarification",
|
||||
choice_kind: "event_quality",
|
||||
}],
|
||||
});
|
||||
const copy = serverOwnedChoiceCopy(frame);
|
||||
assert.ok(copy);
|
||||
assert.equal(frame.period, "2015 年前后");
|
||||
assert.equal(frame.choice_kind, "event_quality");
|
||||
assert.doesNotMatch(copy.prompt, /有没有/);
|
||||
});
|
||||
|
||||
test("GET card prompt keeps the agent's year-locked question", () => {
|
||||
const fallback = {
|
||||
prompt: "2018 年前后 · 认真关系进入、结束或关系观明显转变",
|
||||
@@ -964,6 +934,7 @@ test("GET card prompt keeps the agent's year-locked question", () => {
|
||||
unique_minute_claim: false,
|
||||
user_meaning: "年份锁定 2018 年前后。",
|
||||
role: "reverse_verify",
|
||||
style_options: DYNAMIC_STYLE_OPTIONS,
|
||||
}],
|
||||
activeFocus: {
|
||||
id: FOCUS_ID,
|
||||
@@ -979,60 +950,7 @@ test("GET card prompt keeps the agent's year-locked question", () => {
|
||||
assert.doesNotMatch(card.prompt, /有没有明显认真关系进入/);
|
||||
});
|
||||
|
||||
test("GET without agent speech shows the lock label, not a yes/no template", () => {
|
||||
const fallback = {
|
||||
prompt: "2018 年前后 · 认真关系进入、结束或关系观明显转变",
|
||||
option_a: SAMPLE_COPY.option_a,
|
||||
option_b: SAMPLE_COPY.option_b,
|
||||
option_c: SAMPLE_COPY.option_c,
|
||||
option_d: SAMPLE_COPY.option_d,
|
||||
};
|
||||
const card = projectRectificationChoiceCard({
|
||||
evidence: [{
|
||||
status: "confirmed",
|
||||
domain: "education",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2016-01-01",
|
||||
occurredTo: null,
|
||||
}, {
|
||||
status: "confirmed",
|
||||
domain: "relationship",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2017-01-01",
|
||||
occurredTo: null,
|
||||
}, {
|
||||
status: "confirmed",
|
||||
domain: "career",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2019-01-01",
|
||||
occurredTo: null,
|
||||
}],
|
||||
eventProbes: [{
|
||||
year: 2018,
|
||||
year_label: "2018 年前后",
|
||||
domain: "relationship",
|
||||
event_family: "认真关系进入、结束或关系观明显转变",
|
||||
source: "dasha_activation",
|
||||
tracks: ["vimshottari", "narayana"],
|
||||
tracks_agree: true,
|
||||
unique_minute_claim: false,
|
||||
user_meaning: "年份锁定 2018 年前后。",
|
||||
role: "reverse_verify",
|
||||
}],
|
||||
activeFocus: {
|
||||
id: FOCUS_ID,
|
||||
intent: "distinguish_candidates",
|
||||
targetDomain: "relationship",
|
||||
targetKind: "relationship_change",
|
||||
expectedAnswerSchema: { choice: fallback, probe_id: "relationship.2018" },
|
||||
},
|
||||
});
|
||||
assert.ok(card);
|
||||
assert.equal(card.prompt, fallback.prompt);
|
||||
assert.doesNotMatch(card.prompt, /有没有/);
|
||||
});
|
||||
|
||||
test("career event_quality does not reuse the exam underperformance copy", () => {
|
||||
test("career event_quality uses the supplied dynamic labels", () => {
|
||||
const frame = buildChoiceFrame({
|
||||
method_id: "d10_career",
|
||||
ask_theme: "career_style",
|
||||
@@ -1051,21 +969,22 @@ test("career event_quality does not reuse the exam underperformance copy", () =>
|
||||
unique_minute_claim: false,
|
||||
user_meaning: "年份锁定 2020 年前后。已有相关经历。请写成一句自然语言,问入职、升职或职责明显加重有没有发生过。不得改年份。",
|
||||
role: "distinguish",
|
||||
style_options: DYNAMIC_STYLE_OPTIONS,
|
||||
choice_kind: "event_quality",
|
||||
}],
|
||||
});
|
||||
assert.ok(frame);
|
||||
const copy = serverOwnedChoiceCopy(frame);
|
||||
assert.ok(copy);
|
||||
assert.match(copy.prompt, /2020 年前后/);
|
||||
assert.match(copy.prompt, /入职、升职或职责明显加重/);
|
||||
assert.doesNotMatch(copy.prompt, /高考|失常|压力很大/);
|
||||
assert.doesNotMatch(copy.prompt, /有没有/);
|
||||
assert.match(copy.prompt, /·/);
|
||||
assert.equal(copy.option_a, "是,大概就在那段时间");
|
||||
assert.equal(copy.option_a, DYNAMIC_STYLE_OPTIONS[0].label);
|
||||
assert.doesNotMatch(copy.option_a, /失常/);
|
||||
});
|
||||
|
||||
test("dasha existence cards lock the year and family, not a yes/no template", () => {
|
||||
test("dasha existence cards lock the event and use supplied dynamic labels", () => {
|
||||
const frame = buildChoiceFrame({
|
||||
method_id: "d4_home",
|
||||
ask_theme: "home_change",
|
||||
@@ -1073,11 +992,12 @@ test("dasha existence cards lock the year and family, not a yes/no template", ()
|
||||
user_prompt_hint: "unused",
|
||||
choice_kind: "existence",
|
||||
}, { probes: [MOVE_PROBE] });
|
||||
assert.ok(frame);
|
||||
const copy = serverOwnedChoiceCopy(frame);
|
||||
assert.ok(copy);
|
||||
assert.match(copy.prompt, /2016 年前后/);
|
||||
assert.match(copy.prompt, /搬家、离乡或长期异地/);
|
||||
assert.doesNotMatch(copy.prompt, /有没有明显/);
|
||||
assert.match(copy.option_a, /是,大概就在那段时间/);
|
||||
assert.match(copy.option_c, /没有明显发生/);
|
||||
assert.equal(copy.option_a, DYNAMIC_STYLE_OPTIONS[0].label);
|
||||
assert.equal(copy.option_c, DYNAMIC_STYLE_OPTIONS[2].label);
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@ import test from "node:test";
|
||||
|
||||
import {
|
||||
SEALED_MINUTE_HOLDOUT,
|
||||
UNIQUE_MINUTE_CLOSED_COPY,
|
||||
buildConfirmationGate,
|
||||
readVedastroMinuteSensitiveStatus,
|
||||
sessionOutcomeFromGate,
|
||||
@@ -104,13 +103,14 @@ test("VedAstro not_evaluated is not described as fail and still forbids confirma
|
||||
assert.equal(vedastro.status, "not_evaluated");
|
||||
assert.notEqual(vedastro.status, "fail");
|
||||
assert.notEqual(vedastro.status, "failed");
|
||||
assert.match(vedastro.user_meaning, /不等于失败|不是失败|尚未跑通/);
|
||||
assert.doesNotMatch(vedastro.user_meaning, /失败了|校验失败/);
|
||||
assert.match(vedastro.user_meaning, /尚未执行/);
|
||||
assert.doesNotMatch(vedastro.user_meaning, /执行失败|校验失败/);
|
||||
assert.equal(adjacent.status, "passed");
|
||||
assert.equal(holdout.status, "not_ready");
|
||||
assert.equal(gate.confirmation_allowed, false);
|
||||
assert.equal(gate.unique_minute_path, "closed_at_representative");
|
||||
assert.ok(holdout.user_meaning.includes(UNIQUE_MINUTE_CLOSED_COPY));
|
||||
assert.match(holdout.user_meaning, /未达发布门槛/);
|
||||
assert.match(holdout.user_meaning, /不能确认唯一分钟/);
|
||||
assert.equal(uniqueMinutePath(false), "closed_at_representative");
|
||||
assert.equal(uniqueMinutePath(true), "awaiting_user_consent");
|
||||
});
|
||||
@@ -152,7 +152,7 @@ test("a 25-minute plateau stays confirmation-blocked as an indistinguishable ran
|
||||
proposeAllowed: true,
|
||||
confirmationAllowed: false,
|
||||
interviewOpen: false,
|
||||
}).user_meaning, /本会话以代表性时间收口,不确认唯一分钟/);
|
||||
}).user_meaning, /可以采用代表性候选作当前排盘/);
|
||||
assert.equal(sessionOutcomeFromGate({
|
||||
selectionAllowed: true,
|
||||
confirmationAllowed: false,
|
||||
@@ -198,15 +198,15 @@ test("holdout not_ready forbids unique-minute copy and still blocks confirm", as
|
||||
assert.match(skill, /confirmation_gate/);
|
||||
assert.match(skill, /not_evaluated/);
|
||||
assert.match(skill, /session_outcome=adopt_representative/);
|
||||
assert.match(skill, /本会话以代表性时间收口,不确认唯一分钟/);
|
||||
assert.doesNotMatch(skill, /本会话以代表性时间收口|本轮校正已收口/);
|
||||
assert.match(skill, /unique_minute_path=closed_at_representative/);
|
||||
assert.doesNotMatch(skill, /还不能确认唯一分钟/);
|
||||
assert.match(candidateComparison, /confirmation_gate/);
|
||||
assert.match(agentSource, /confirmation_gate/);
|
||||
assert.match(agentSource, /session_outcome=adopt_representative/);
|
||||
assert.doesNotMatch(agentSource, /±2 分钟/);
|
||||
assert.equal(PUBLIC_RECTIFICATION_TOOLS.length, 13);
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.12");
|
||||
assert.equal(PUBLIC_RECTIFICATION_TOOLS.length, 14);
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.13");
|
||||
|
||||
const accounting = fakeAccounting({
|
||||
...receiptHandlers,
|
||||
|
||||
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import { buildMethodFollowupPlan, buildNextUserAction, conversationalSessionOutcome, isOfferBlockingFollowup, latestUserStoppedCollecting } from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||||
import { buildMethodFollowupPlan, buildNextUserAction, conversationalSessionOutcome, isOfferBlockingFollowup } from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||||
import { trainingScoreableGate } from "../src/lib/rectification-agentic/v9/evidence-model.ts";
|
||||
import {
|
||||
askedKeysFromLedgerEvidence,
|
||||
@@ -14,9 +14,16 @@ import {
|
||||
} 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 { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts";
|
||||
import { readVedastroMinuteSensitiveStatus } from "../src/lib/rectification-agentic/v9/confirmation-gate.ts";
|
||||
import { authoritativeCandidateProjection } from "../src/lib/rectification-agentic/v9/inference-adapter.ts";
|
||||
import { createRectificationV9Tools, latestResultToolProjection } from "../src/mastra/rectification-v9-tools.ts";
|
||||
import { PUBLIC_RECTIFICATION_TOOLS } from "../src/lib/rectification-agentic/v9/public-receipt.ts";
|
||||
import { RectificationToolServiceError } from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
||||
import {
|
||||
candidateRangeFingerprint,
|
||||
evidenceLedgerFingerprint,
|
||||
parseV9CaseDossier,
|
||||
RectificationToolServiceError,
|
||||
} from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
||||
import {
|
||||
resolveExactSkillPackage,
|
||||
} from "../src/lib/skill-package-registry.ts";
|
||||
@@ -42,6 +49,13 @@ const FAMILY_ID = "44444444-4444-4444-8444-444444444443";
|
||||
const CAREER_ID = "44444444-4444-4444-8444-444444444444";
|
||||
const UNIQUE_MINUTE_COPY = /±5 分钟确定性/;
|
||||
|
||||
const DYNAMIC_STYLE_OPTIONS = [
|
||||
{ label: "明确发生且时间吻合", answer_class: "yes" as const },
|
||||
{ label: "发生过但程度较弱", answer_class: "weak_yes" as const },
|
||||
{ label: "明确没有发生", answer_class: "no" as const },
|
||||
{ label: "不记得这段经历", answer_class: "unsure" as const },
|
||||
];
|
||||
|
||||
const CLASSIC_COVERAGE = [
|
||||
{ status: "confirmed", domain: "education", datePrecision: "year" as const, occurredFrom: "2016-01-01", occurredTo: null },
|
||||
{ status: "confirmed", domain: "relationship", datePrecision: "year" as const, occurredFrom: "2018-01-01", occurredTo: null },
|
||||
@@ -94,6 +108,7 @@ const CAREER_CONFLICT_PROBE = {
|
||||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:20"] },
|
||||
{ answer_class: "no", supports: ["05:20"], conflicts: ["05:00"] },
|
||||
],
|
||||
style_options: DYNAMIC_STYLE_OPTIONS,
|
||||
};
|
||||
|
||||
const EDUCATION_QUALITY_PROBE = {
|
||||
@@ -111,6 +126,7 @@ const EDUCATION_QUALITY_PROBE = {
|
||||
choice_kind: "event_quality" as const,
|
||||
information_gain: 0,
|
||||
semantic_key: "education.2016",
|
||||
style_options: DYNAMIC_STYLE_OPTIONS,
|
||||
};
|
||||
|
||||
const CAREER_QUALITY_PROBE = {
|
||||
@@ -128,6 +144,7 @@ const CAREER_QUALITY_PROBE = {
|
||||
choice_kind: "event_quality" as const,
|
||||
information_gain: 0,
|
||||
semantic_key: "career.2020",
|
||||
style_options: DYNAMIC_STYLE_OPTIONS,
|
||||
};
|
||||
|
||||
const ENGINE_SCORE = {
|
||||
@@ -866,6 +883,172 @@ test("accepted batch evidence triggers server rescore without offering adoption"
|
||||
}
|
||||
});
|
||||
|
||||
test("cached candidates retry a failed VedAstro validation without recomputing ranking", async () => {
|
||||
const validationResponse = {
|
||||
status: "passed",
|
||||
can_confirm_exact_minute: true,
|
||||
event_validation: {
|
||||
search_events_primary_supports_local_winner: true,
|
||||
},
|
||||
minute_sensitive_validation: {
|
||||
status: "passed",
|
||||
},
|
||||
};
|
||||
const restore = stubEngine(validationResponse);
|
||||
try {
|
||||
const compute = computeFixture();
|
||||
const evidence = [educationEvidence];
|
||||
const evidenceFingerprint = evidenceLedgerFingerprint([{
|
||||
id: educationEvidence.id,
|
||||
sourceTurnId: educationEvidence.source_turn_id,
|
||||
subject: educationEvidence.subject,
|
||||
eventKind: educationEvidence.event_kind,
|
||||
domain: educationEvidence.domain,
|
||||
occurredFrom: educationEvidence.occurred_from,
|
||||
occurredTo: educationEvidence.occurred_to,
|
||||
datePrecision: educationEvidence.date_precision,
|
||||
summary: educationEvidence.summary,
|
||||
status: educationEvidence.status,
|
||||
supersedesEvidenceId: educationEvidence.supersedes_evidence_id,
|
||||
createdAt: educationEvidence.created_at,
|
||||
}]);
|
||||
const rangeFingerprint = candidateRangeFingerprint(
|
||||
compute.candidate_range,
|
||||
compute.baseline_profile_fingerprint,
|
||||
);
|
||||
const cached = {
|
||||
...candidateSnapshotFixture({
|
||||
representativeTime: "05:02",
|
||||
evidenceLedgerFingerprint: evidenceFingerprint,
|
||||
decisionReceipt: {
|
||||
inference_state: {
|
||||
algorithm_version: "rectification-inference-v1",
|
||||
candidate_set_id: "04:55-05:02:04:55,05:02",
|
||||
revision: 1,
|
||||
phase: "discrimination",
|
||||
result_status: "credible_range",
|
||||
range_start: "04:55",
|
||||
range_end: "05:02",
|
||||
candidates: [
|
||||
{ id: CANDIDATE_ID, time: "05:02", cluster_range: ["05:02", "05:02"], prior_score: 58, posterior_score: 58, probability: 0.58, status: "active", rank: 1, strong_conflict_count: 0 },
|
||||
{ id: SECOND_CANDIDATE_ID, time: "04:55", cluster_range: ["04:55", "04:55"], prior_score: 42, posterior_score: 42, probability: 0.42, status: "active", rank: 2, strong_conflict_count: 0 },
|
||||
],
|
||||
events: [],
|
||||
probes: [],
|
||||
answered_probes: [],
|
||||
rounds: [],
|
||||
entropy: 0.98,
|
||||
representative_time: "05:02",
|
||||
credible_range: ["04:55", "05:02"],
|
||||
},
|
||||
gates: {
|
||||
exact_confirmation: {
|
||||
external_validation_status: "failed",
|
||||
vedastro_event_validation: {
|
||||
status: "failed",
|
||||
search_events_primary_supports_local_winner: false,
|
||||
can_confirm_exact_minute: false,
|
||||
failure: { code: "timeout" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
candidate_range_fingerprint: rangeFingerprint,
|
||||
};
|
||||
const refreshedReceipt = {
|
||||
...cached.decision_receipt,
|
||||
gates: {
|
||||
exact_confirmation: {
|
||||
external_validation_status: "passed",
|
||||
vedastro_event_validation: {
|
||||
status: "passed",
|
||||
search_events_primary_supports_local_winner: true,
|
||||
can_confirm_exact_minute: true,
|
||||
failure: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const parsedCached = parseV9CaseDossier(dossierFixture({ evidence, latestResult: cached }));
|
||||
assert.equal(parsedCached?.latestResult?.selectionAllowed, true);
|
||||
assert.equal(parsedCached?.latestResult?.evidenceLedgerFingerprint, evidenceFingerprint);
|
||||
assert.equal(parsedCached?.latestResult?.candidateRangeFingerprint, rangeFingerprint);
|
||||
assert.equal(readVedastroMinuteSensitiveStatus(parsedCached?.latestResult?.decisionReceipt), "failed");
|
||||
assert.equal(authoritativeCandidateProjection(parsedCached!.latestResult!).candidates.length, 2);
|
||||
const accounting = fakeAccounting({
|
||||
...receiptHandlers,
|
||||
get_agentic_rectification_case_dossier: () => dossierFixture({
|
||||
evidence,
|
||||
latestResult: cached,
|
||||
}),
|
||||
get_agentic_rectification_case_compute: () => compute,
|
||||
refresh_agentic_rectification_vedastro_validation: () => ({
|
||||
result_id: RESULT_ID,
|
||||
decision_receipt: refreshedReceipt,
|
||||
}),
|
||||
set_agentic_rectification_conversation_focus: (_fn, args) => ({
|
||||
focus: {
|
||||
id: FOCUS_ID,
|
||||
case_id: CASE_ID,
|
||||
question_id: args.p_question_id,
|
||||
intent: args.p_intent,
|
||||
target_evidence_id: args.p_target_evidence_id,
|
||||
target_domain: args.p_target_domain,
|
||||
target_kind: args.p_target_kind,
|
||||
expected_answer_schema: args.p_expected_answer_schema,
|
||||
status: "active",
|
||||
asked_at: "2026-08-27T00:00:00.000Z",
|
||||
resolved_at: null,
|
||||
},
|
||||
idempotent: false,
|
||||
}),
|
||||
});
|
||||
const tools = createRectificationV9Tools({
|
||||
userId: USER_ID,
|
||||
caseId: CASE_ID,
|
||||
turnId: TURN_ID,
|
||||
userMessage: "继续比较",
|
||||
accounting: accounting.client as never,
|
||||
});
|
||||
const result = await (tools["rectification-compare-candidates"] as unknown as {
|
||||
execute(input: unknown): Promise<{
|
||||
cached: boolean;
|
||||
candidates: Array<{ time: string; relativeSupport: number }>;
|
||||
confirmation_gate: { blockers: Array<{ id: string; status: string }> };
|
||||
}>;
|
||||
}).execute({ caseId: CASE_ID });
|
||||
|
||||
const refreshCall = accounting.calls.find((call) =>
|
||||
call.fn === "refresh_agentic_rectification_vedastro_validation"
|
||||
);
|
||||
assert.ok(refreshCall, JSON.stringify(accounting.calls.map((call) => call.fn)));
|
||||
assert.deepEqual(refreshCall.args.p_validation, {
|
||||
status: "passed",
|
||||
search_events_primary_supports_local_winner: true,
|
||||
can_confirm_exact_minute: true,
|
||||
failure: null,
|
||||
});
|
||||
assert.equal(refreshCall.args.p_minute_sensitive_status, "passed");
|
||||
assert.equal(
|
||||
accounting.calls.some((call) => call.fn === "persist_agentic_rectification_candidate_v2"),
|
||||
false,
|
||||
);
|
||||
assert.equal(result.cached, true);
|
||||
assert.deepEqual(result.candidates.map((item) => [item.time, item.relativeSupport]), [
|
||||
["05:02", 58],
|
||||
["04:55", 42],
|
||||
]);
|
||||
assert.ok(result.confirmation_gate, JSON.stringify(result));
|
||||
assert.equal(
|
||||
result.confirmation_gate.blockers.find((gate) => gate.id === "vedastro_minute_sensitive")?.status,
|
||||
"passed",
|
||||
);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("evidence batch returns the persisted choice prompt as open_question", async () => {
|
||||
const restore = stubEngine({
|
||||
...ENGINE_SCORE,
|
||||
@@ -892,6 +1075,7 @@ test("evidence batch returns the persisted choice prompt as open_question", asyn
|
||||
{ answer_class: "yes", supports: ["04:50"], conflicts: ["05:20"] },
|
||||
{ answer_class: "no", supports: ["05:20"], conflicts: ["04:50"] },
|
||||
],
|
||||
style_options: DYNAMIC_STYLE_OPTIONS,
|
||||
choice_kind: "existence",
|
||||
}],
|
||||
},
|
||||
@@ -1033,9 +1217,9 @@ test("rescore failure does not fail the evidence write", async () => {
|
||||
assert.ok(result.rescore.error_code);
|
||||
});
|
||||
|
||||
test("public tool surface stays at 13 and new cases bind 10.0.12", () => {
|
||||
assert.equal(PUBLIC_RECTIFICATION_TOOLS.length, 13);
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.12");
|
||||
test("public tool surface stays at 14 and new cases bind 10.0.13", () => {
|
||||
assert.equal(PUBLIC_RECTIFICATION_TOOLS.length, 14);
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.13");
|
||||
const deprecated = resolveExactSkillPackage(
|
||||
"jyotish-birth-time-rectification",
|
||||
"10.0.2",
|
||||
@@ -1253,6 +1437,7 @@ test("accepted representative time reverse-verifies predicted events then hands
|
||||
unique_minute_claim: false as const,
|
||||
user_meaning: "年份锁定 2018 年前后。请写成一句自然语言,问是否入职或职责加重。",
|
||||
role: "reverse_verify" as const,
|
||||
style_options: DYNAMIC_STYLE_OPTIONS,
|
||||
};
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: [{
|
||||
@@ -1287,7 +1472,7 @@ test("accepted representative time reverse-verifies predicted events then hands
|
||||
assert.equal(plan.next_followup?.method_id, "reverse_verify");
|
||||
assert.equal(plan.next_followup?.domain, "career");
|
||||
assert.equal(plan.next_followup?.choice_frame?.scoring, true);
|
||||
assert.match(plan.next_followup?.choice_frame?.why ?? "", /按当前采用时间核对/);
|
||||
assert.match(plan.next_followup?.choice_frame?.why ?? "", /2018 年前后/);
|
||||
assert.equal(plan.deferred_followup, null);
|
||||
assert.equal(plan.next_followup?.source, "reverse_verify");
|
||||
const action = buildNextUserAction({
|
||||
@@ -1770,18 +1955,7 @@ test("coverage-complete tie with no remaining split offers a provisional range",
|
||||
assert.equal(action.id, "offer_provisional_range");
|
||||
});
|
||||
|
||||
test("only explicit rectification exit language is a global user stop", () => {
|
||||
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "没有" }]), false);
|
||||
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "没有了" }]), false);
|
||||
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "想不起来了" }]), false);
|
||||
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "结束校正" }]), true);
|
||||
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "我不想继续了" }]), true);
|
||||
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "直接给结果吧" }]), true);
|
||||
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "就到这里" }]), true);
|
||||
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "我不是不想继续,只是想先补工作" }]), false);
|
||||
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "不要直接给结果,我还想补一件事" }]), false);
|
||||
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "还没到这里结束,我继续说" }]), false);
|
||||
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "还有一件升学" }]), false);
|
||||
test("structured paused state ends evidence collection without parsing user copy", () => {
|
||||
assert.equal(conversationalSessionOutcome({
|
||||
selectionAllowed: true,
|
||||
proposeAllowed: true,
|
||||
@@ -2015,6 +2189,7 @@ test("accepted time reverse-verifies an uncovered year in a covered domain", ()
|
||||
unique_minute_claim: false,
|
||||
user_meaning: "年份锁定 2018 年前后。请写成一句自然语言,问是否入职或职责加重。",
|
||||
role: "reverse_verify",
|
||||
style_options: DYNAMIC_STYLE_OPTIONS,
|
||||
}],
|
||||
});
|
||||
assert.equal(plan.next_followup?.source, "reverse_verify");
|
||||
@@ -2112,19 +2287,12 @@ test("offer-candidates refuses while method coverage remains", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("user stop with selection_allowed may offer the escape hatch", async () => {
|
||||
test("paused case with selection_allowed may offer the escape hatch", async () => {
|
||||
const accounting = fakeAccounting({
|
||||
...receiptHandlers,
|
||||
get_agentic_rectification_case_dossier: () => dossierFixture({
|
||||
status: "paused",
|
||||
evidence: [educationEvidence],
|
||||
turns: [{
|
||||
id: TURN_ID,
|
||||
role: "user",
|
||||
text: "结束校正",
|
||||
status: "completed",
|
||||
created_at: "2026-08-12T10:00:00.000Z",
|
||||
completed_at: "2026-08-12T10:00:05.000Z",
|
||||
}],
|
||||
latestResult: candidateSnapshotFixture({
|
||||
selectionAllowed: true,
|
||||
representativeTime: "04:48",
|
||||
@@ -2178,6 +2346,40 @@ test("user stop with selection_allowed may offer the escape hatch", async () =>
|
||||
call.fn === "transition_agentic_rectification_case_status"
|
||||
&& call.args.p_status === "candidate_ready"
|
||||
),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("paused case resumes only when the Agent explicitly requests it", async () => {
|
||||
let reads = 0;
|
||||
const accounting = fakeAccounting({
|
||||
...receiptHandlers,
|
||||
get_agentic_rectification_case_dossier: () => dossierFixture({
|
||||
status: reads++ === 0 ? "paused" : "collecting_evidence",
|
||||
evidence: [],
|
||||
}),
|
||||
transition_agentic_rectification_case_status: (_fn, args) => ({
|
||||
case_id: CASE_ID,
|
||||
status: args.p_status,
|
||||
idempotent: false,
|
||||
}),
|
||||
});
|
||||
const tools = createRectificationV9Tools({
|
||||
userId: USER_ID,
|
||||
caseId: CASE_ID,
|
||||
turnId: TURN_ID,
|
||||
userMessage: "继续校正",
|
||||
accounting: accounting.client as never,
|
||||
});
|
||||
const projection = await (tools["rectification-read-case"] as unknown as {
|
||||
execute(input: unknown): Promise<{ status: string }>;
|
||||
}).execute({ caseId: CASE_ID, resume: true });
|
||||
assert.equal(projection.status, "collecting_evidence");
|
||||
assert.equal(
|
||||
accounting.calls.some((call) =>
|
||||
call.fn === "transition_agentic_rectification_case_status"
|
||||
&& call.args.p_status === "collecting_evidence"
|
||||
),
|
||||
true,
|
||||
);
|
||||
});
|
||||
@@ -2287,4 +2489,3 @@ test("offer-candidates allows a 34/33/33 tie after method coverage when remainin
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -206,9 +206,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.12 and the prompt prefers batch ingest", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.12");
|
||||
assert.match(skill, /^version: 10\.0\.12$/m);
|
||||
test("new-case skill identity is 10.0.13 and the prompt prefers batch ingest", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.13");
|
||||
assert.match(skill, /^version: 10\.0\.13$/m);
|
||||
assert.match(skill, /不要对同一句用户消息里的多件事件逐条 propose\+confirm/);
|
||||
assert.match(agentSource, /当前轮新事件一律走 rectification-record-evidence-batch/);
|
||||
assert.doesNotMatch(agentSource, /分别调用 rectification-propose-evidence 和 rectification-confirm-evidence/);
|
||||
|
||||
@@ -5,8 +5,16 @@ import { buildInferenceState } from "../src/lib/rectification-agentic/core/build
|
||||
import { persistServerOwnedFocus, shouldSkipDiscriminatorFollowup, stableFollowupQuestionId } from "../src/lib/rectification-agentic/v9/server-focus.ts";
|
||||
import { buildChoiceFrame, serverOwnedChoiceCopy } from "../src/lib/rectification-agentic/v9/choice-card.ts";
|
||||
import type { MethodFollowup } from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||||
import type { EventProbeStyleOption } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
|
||||
import { fakeAccounting, CASE_ID, FOCUS_ID, USER_ID, activeFocusFixture } from "./rectification-v9-test-support.ts";
|
||||
|
||||
const DYNAMIC_STYLE_OPTIONS = [
|
||||
{ label: "明确发生且时间吻合", answer_class: "yes" },
|
||||
{ label: "发生过但程度较弱", answer_class: "weak_yes" },
|
||||
{ label: "明确没有发生", answer_class: "no" },
|
||||
{ label: "这段记不清楚", answer_class: "unsure" },
|
||||
] as const;
|
||||
|
||||
function discriminatorFollowup(overrides: Partial<MethodFollowup> = {}): MethodFollowup {
|
||||
const frame = buildChoiceFrame({
|
||||
method_id: "dasha_events",
|
||||
@@ -18,17 +26,19 @@ function discriminatorFollowup(overrides: Partial<MethodFollowup> = {}): MethodF
|
||||
year: 2016,
|
||||
year_label: "2016 年前后",
|
||||
domain: "education",
|
||||
event_family: "高考或重要考试发挥明显失常",
|
||||
event_family: "升学结果或学习环境出现明显变化",
|
||||
source: "dasha_activation",
|
||||
tracks: ["vimshottari", "narayana"],
|
||||
tracks_agree: true,
|
||||
unique_minute_claim: false,
|
||||
user_meaning: "2016 年前后高考或重要考试发挥明显失常",
|
||||
user_meaning: "2016 年前后升学结果或学习环境出现明显变化",
|
||||
role: "distinguish",
|
||||
information_gain: 0.4,
|
||||
semantic_key: "education:2016",
|
||||
style_options: DYNAMIC_STYLE_OPTIONS,
|
||||
}],
|
||||
});
|
||||
assert.ok(frame);
|
||||
return {
|
||||
method_id: "dasha_events",
|
||||
intent: "distinguish_candidates",
|
||||
@@ -51,6 +61,107 @@ function discriminatorFollowup(overrides: Partial<MethodFollowup> = {}): MethodF
|
||||
};
|
||||
}
|
||||
|
||||
function invalidStyleFollowup(
|
||||
styleOptions: readonly EventProbeStyleOption[] | undefined,
|
||||
eventFamily = "升学结果或学习环境出现明显变化",
|
||||
): MethodFollowup {
|
||||
const valid = discriminatorFollowup();
|
||||
return {
|
||||
...valid,
|
||||
choice_frame: buildChoiceFrame({
|
||||
method_id: "dasha_events",
|
||||
ask_theme: "dated_event",
|
||||
domain: "education",
|
||||
user_prompt_hint: "ask",
|
||||
}, {
|
||||
probes: [{
|
||||
year: 2016,
|
||||
year_label: "2016 年前后",
|
||||
domain: "education",
|
||||
event_family: eventFamily,
|
||||
source: "dasha_activation",
|
||||
tracks: ["vimshottari", "narayana"],
|
||||
tracks_agree: true,
|
||||
unique_minute_claim: false,
|
||||
user_meaning: "2016 年前后升学结果或学习环境出现明显变化",
|
||||
role: "distinguish",
|
||||
information_gain: 0.4,
|
||||
semantic_key: "education:2016",
|
||||
style_options: styleOptions,
|
||||
}],
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async function assertInvalidChoiceSkipped(followup: MethodFollowup) {
|
||||
const accounting = fakeAccounting({}, {
|
||||
fallback: () => {
|
||||
throw new Error("invalid choice frame must not call the database");
|
||||
},
|
||||
});
|
||||
const result = await persistServerOwnedFocus({
|
||||
accounting: accounting.client,
|
||||
userId: USER_ID,
|
||||
caseId: CASE_ID,
|
||||
activeFocus: null,
|
||||
decisionReceipt: null,
|
||||
followup,
|
||||
});
|
||||
assert.equal(result.status, "skipped");
|
||||
assert.equal(accounting.calls.length, 0);
|
||||
}
|
||||
|
||||
test("complete dynamic event options persist a server-owned focus", async () => {
|
||||
const followup = discriminatorFollowup({ source: "precision_stage", information_gain: 0.2 });
|
||||
const accounting = fakeAccounting({
|
||||
set_agentic_rectification_conversation_focus: (_fn, args) => ({
|
||||
id: FOCUS_ID,
|
||||
case_id: CASE_ID,
|
||||
question_id: args.p_question_id,
|
||||
intent: args.p_intent,
|
||||
target_evidence_id: null,
|
||||
target_domain: args.p_target_domain,
|
||||
target_kind: args.p_target_kind,
|
||||
expected_answer_schema: args.p_expected_answer_schema,
|
||||
status: "active",
|
||||
asked_at: "2026-08-27T00:00:00.000Z",
|
||||
resolved_at: null,
|
||||
idempotent: false,
|
||||
}),
|
||||
});
|
||||
const result = await persistServerOwnedFocus({
|
||||
accounting: accounting.client,
|
||||
userId: USER_ID,
|
||||
caseId: CASE_ID,
|
||||
activeFocus: null,
|
||||
decisionReceipt: null,
|
||||
followup,
|
||||
});
|
||||
assert.equal(result.status, "created");
|
||||
assert.equal(accounting.calls.length, 1);
|
||||
assert.deepEqual(result.focus?.expectedAnswerSchema.choice, {
|
||||
prompt: "2016 年前后 · 升学结果或学习环境出现明显变化",
|
||||
option_a: DYNAMIC_STYLE_OPTIONS[0].label,
|
||||
option_b: DYNAMIC_STYLE_OPTIONS[1].label,
|
||||
option_c: DYNAMIC_STYLE_OPTIONS[2].label,
|
||||
option_d: DYNAMIC_STYLE_OPTIONS[3].label,
|
||||
});
|
||||
});
|
||||
|
||||
test("missing, duplicate, incomplete, or illegal dynamic options skip focus persistence", async () => {
|
||||
await assertInvalidChoiceSkipped(invalidStyleFollowup(undefined));
|
||||
await assertInvalidChoiceSkipped(invalidStyleFollowup(DYNAMIC_STYLE_OPTIONS.slice(0, 3)));
|
||||
await assertInvalidChoiceSkipped(invalidStyleFollowup([
|
||||
...DYNAMIC_STYLE_OPTIONS,
|
||||
{ label: "另一种明确发生", answer_class: "yes" },
|
||||
]));
|
||||
await assertInvalidChoiceSkipped(invalidStyleFollowup([
|
||||
{ ...DYNAMIC_STYLE_OPTIONS[0], label: "08:12 左右发生" },
|
||||
...DYNAMIC_STYLE_OPTIONS.slice(1),
|
||||
]));
|
||||
await assertInvalidChoiceSkipped(invalidStyleFollowup(DYNAMIC_STYLE_OPTIONS, " "));
|
||||
});
|
||||
|
||||
test("does not ask an already-answered discriminator probe again", async () => {
|
||||
const followup = discriminatorFollowup();
|
||||
const active = activeFocusFixture({
|
||||
|
||||
@@ -33,6 +33,7 @@ const EXACT_TOOL_KEYS = [
|
||||
"rectification-offer-candidates",
|
||||
"rectification-accept-candidate",
|
||||
"rectification-confirm-birth-time",
|
||||
"rectification-stop-and-review",
|
||||
"rectification-close-case",
|
||||
] as const;
|
||||
|
||||
@@ -113,20 +114,21 @@ const validInputs: Record<(typeof EXACT_TOOL_KEYS)[number], Record<string, unkno
|
||||
candidateId: CANDIDATE_ID,
|
||||
consentQuote: "就用05:02",
|
||||
},
|
||||
"rectification-stop-and-review": { caseId: CASE_ID },
|
||||
"rectification-close-case": {
|
||||
caseId: CASE_ID,
|
||||
reason: "completed_by_user",
|
||||
},
|
||||
};
|
||||
|
||||
test("V10 exposes exactly the 13 allowlisted rectification tool keys", () => {
|
||||
test("V10 exposes exactly the 14 allowlisted rectification tool keys", () => {
|
||||
const tools = toolsUnderTest();
|
||||
assert.deepEqual(Object.keys(tools), [...EXACT_TOOL_KEYS]);
|
||||
assert.deepEqual([...PUBLIC_RECTIFICATION_TOOLS], [...EXACT_TOOL_KEYS]);
|
||||
assert.equal(new Set(Object.keys(tools)).size, 13);
|
||||
assert.equal(new Set(Object.keys(tools)).size, 14);
|
||||
});
|
||||
|
||||
test("all 13 tool input schemas accept their minimal contract and reject unknown fields", () => {
|
||||
test("all 14 tool input schemas accept their minimal contract and reject unknown fields", () => {
|
||||
const tools = toolsUnderTest() as unknown as Record<string, ToolSchema>;
|
||||
for (const toolName of EXACT_TOOL_KEYS) {
|
||||
const schema = tools[toolName]?.inputSchema;
|
||||
|
||||
@@ -62,7 +62,7 @@ test("system prompt carries only high-priority boundaries, never the method copy
|
||||
assert.match(prompt, /不可分区间/);
|
||||
assert.match(prompt, /confirmation_gate/);
|
||||
assert.match(prompt, /session_outcome=adopt_representative/);
|
||||
assert.match(prompt, /本会话以代表性时间收口,不确认唯一分钟/);
|
||||
assert.doesNotMatch(prompt, /本会话以代表性时间收口|本轮校正已收口/);
|
||||
assert.match(prompt, /unique_minute_path=closed_at_representative/);
|
||||
assert.match(prompt, /next_user_action/);
|
||||
assert.match(prompt, /rectification-offer-candidates/);
|
||||
@@ -105,11 +105,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.12"));
|
||||
assert.ok(RECTIFICATION_V9_PACKAGE_PATH.endsWith("skills/jyotish-birth-time-rectification/versions/10.0.13"));
|
||||
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.12");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.13");
|
||||
});
|
||||
|
||||
test("step budgets are bounded per action with a hard ceiling", () => {
|
||||
@@ -142,6 +142,7 @@ test("no tool accepts event arrays, birth data or candidate ranges as input", ()
|
||||
"rectification-offer-candidates",
|
||||
"rectification-accept-candidate",
|
||||
"rectification-confirm-birth-time",
|
||||
"rectification-stop-and-review",
|
||||
"rectification-close-case",
|
||||
];
|
||||
for (const name of toolNames) {
|
||||
|
||||
@@ -94,9 +94,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.12");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.13");
|
||||
assert.match(skill, /^---\nname: jyotish-birth-time-rectification/m);
|
||||
assert.match(skill, /^version: 10\.0\.12$/m);
|
||||
assert.match(skill, /^version: 10\.0\.13$/m);
|
||||
for (const reference of references) {
|
||||
const content = readFileSync(`${skillDirectory}/references/${reference}`, "utf8");
|
||||
assert.ok(content.length > 0, `${reference} must be non-empty`);
|
||||
|
||||
@@ -349,13 +349,13 @@ test("engine http failures surface as safe engine errors, never raw stack traces
|
||||
}
|
||||
});
|
||||
|
||||
test("vedastro-validate maps pass/fail and never grants unique-minute confirmation", async () => {
|
||||
test("vedastro-validate propagates the engine minute confirmation signal into the combined gate input", async () => {
|
||||
const restore = stubEngine({
|
||||
success: true,
|
||||
endpoint: "rectification_v5_vedastro_validate",
|
||||
status: "pass",
|
||||
passed: true,
|
||||
can_confirm_exact_minute: false,
|
||||
can_confirm_exact_minute: true,
|
||||
minute_sensitive_validation: { status: "pass", discriminated: true },
|
||||
event_validation: {
|
||||
status: "pass",
|
||||
@@ -370,7 +370,7 @@ test("vedastro-validate maps pass/fail and never grants unique-minute confirmati
|
||||
candidateTimes: ["04:50", "04:51"],
|
||||
});
|
||||
assert.equal(result.status, "passed");
|
||||
assert.equal(result.canConfirmExactMinute, false);
|
||||
assert.equal(result.canConfirmExactMinute, true);
|
||||
assert.equal(result.searchEventsSupportsLocalWinner, true);
|
||||
const merged = mergeVedastroValidateIntoReceipt(
|
||||
{ ...DECISION_RECEIPT, gates: { exact_confirmation: { external_validation_status: "passed" } } },
|
||||
@@ -389,11 +389,13 @@ test("vedastro-validate maps pass/fail and never grants unique-minute confirmati
|
||||
}
|
||||
});
|
||||
|
||||
test("vedastro-validate timeout stays not_evaluated and is not described as fail", async () => {
|
||||
const previous = globalThis.fetch;
|
||||
globalThis.fetch = (async () => {
|
||||
throw new Error("timeout");
|
||||
}) as unknown as typeof fetch;
|
||||
test("vedastro-validate cannot pass the minute gate when the overall validation failed", async () => {
|
||||
const restore = stubEngine({
|
||||
status: "failed",
|
||||
can_confirm_exact_minute: true,
|
||||
minute_sensitive_validation: { status: "passed" },
|
||||
event_validation: { search_events_primary_supports_local_winner: true },
|
||||
});
|
||||
try {
|
||||
const result = await runV9VedastroValidate({
|
||||
baselineBirthSnapshot: SNAPSHOT,
|
||||
@@ -401,9 +403,55 @@ test("vedastro-validate timeout stays not_evaluated and is not described as fail
|
||||
events: toEngineEvents(EVIDENCE),
|
||||
candidateTimes: ["04:50", "04:51"],
|
||||
});
|
||||
assert.equal(result.status, "not_evaluated");
|
||||
assert.notEqual(result.status, "failed");
|
||||
assert.equal(result.status, "failed");
|
||||
assert.equal(result.minuteSensitiveStatus, "failed");
|
||||
assert.equal(result.canConfirmExactMinute, false);
|
||||
const merged = mergeVedastroValidateIntoReceipt(
|
||||
{ ...DECISION_RECEIPT, gates: { exact_confirmation: { external_validation_status: "not_evaluated" } } },
|
||||
result,
|
||||
);
|
||||
const exact = (merged.gates as { exact_confirmation: Record<string, unknown> }).exact_confirmation;
|
||||
assert.equal(exact.external_validation_status, "failed");
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("vedastro-validate keeps safe timeout, HTTP, and invalid-response failure codes", async () => {
|
||||
const input = {
|
||||
baselineBirthSnapshot: SNAPSHOT,
|
||||
candidateRange: RANGE,
|
||||
events: toEngineEvents(EVIDENCE),
|
||||
candidateTimes: ["04:50", "04:51"] as const,
|
||||
};
|
||||
const previous = globalThis.fetch;
|
||||
try {
|
||||
globalThis.fetch = (async () => {
|
||||
throw new DOMException("secret upstream detail", "TimeoutError");
|
||||
}) as unknown as typeof fetch;
|
||||
const timeout = await runV9VedastroValidate(input);
|
||||
assert.equal(timeout.status, "failed");
|
||||
assert.deepEqual(timeout.failure, { code: "timeout" });
|
||||
assert.doesNotMatch(JSON.stringify(timeout.raw), /secret upstream detail/);
|
||||
|
||||
globalThis.fetch = (async () => ({
|
||||
ok: false,
|
||||
status: 503,
|
||||
json: async () => ({ error: "private upstream URL and token" }),
|
||||
})) as unknown as typeof fetch;
|
||||
const requestFailure = await runV9VedastroValidate(input);
|
||||
assert.equal(requestFailure.status, "failed");
|
||||
assert.deepEqual(requestFailure.failure, { code: "engine_request_failed" });
|
||||
assert.doesNotMatch(JSON.stringify(requestFailure.raw), /private upstream URL|token/);
|
||||
|
||||
globalThis.fetch = (async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => null,
|
||||
})) as unknown as typeof fetch;
|
||||
const invalid = await runV9VedastroValidate(input);
|
||||
assert.equal(invalid.status, "failed");
|
||||
assert.deepEqual(invalid.failure, { code: "engine_invalid_response" });
|
||||
} finally {
|
||||
globalThis.fetch = previous;
|
||||
}
|
||||
|
||||
@@ -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.12",
|
||||
skill_version: "10.0.13",
|
||||
};
|
||||
}
|
||||
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.12");
|
||||
assert.equal(response.skillVersion, "10.0.13");
|
||||
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.12");
|
||||
assert.equal(openCall.args.p_skill_version, "10.0.13");
|
||||
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);
|
||||
|
||||
@@ -13,6 +13,19 @@ const migration = readFileSync(
|
||||
const dbMigrationsCopy = fileURLToPath(
|
||||
new URL("../db/migrations/20260812010000_agentic_rectification_v9_runtime.sql", import.meta.url),
|
||||
);
|
||||
const vedastroRetryMigration = readFileSync(
|
||||
new URL(
|
||||
"../supabase/migrations/20260827020000_rectification_vedastro_validation_retry.sql",
|
||||
import.meta.url,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
const vedastroRetryMigrationCopy = fileURLToPath(
|
||||
new URL(
|
||||
"../db/migrations/20260827020000_rectification_vedastro_validation_retry.sql",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
|
||||
test("v9 runtime migration sorts after the newest business migration and stays unique", () => {
|
||||
assert.ok(
|
||||
@@ -1409,3 +1422,95 @@ test("candidate freshness trigger uses only calculation inputs", () => {
|
||||
/after update of[^;]*birth_place_label|after update of[^;]*active_birth_time/,
|
||||
);
|
||||
});
|
||||
|
||||
test("VedAstro retry migration is forward-only, unique and registers the stop tool", () => {
|
||||
assert.match(vedastroRetryMigration, /^begin;[\s\S]*^commit;$/m);
|
||||
assert.equal(
|
||||
existsSync(vedastroRetryMigrationCopy),
|
||||
false,
|
||||
"business migration must not be copied into frontend/db/migrations (BUG-127/BUG-144)",
|
||||
);
|
||||
assert.match(
|
||||
vedastroRetryMigration,
|
||||
/agentic_rectification_tool_receipts_tool_name_check check \([\s\S]*'rectification-stop-and-review'/,
|
||||
);
|
||||
const receiptRpc = vedastroRetryMigration.slice(
|
||||
vedastroRetryMigration.indexOf(
|
||||
"create or replace function public.insert_agentic_rectification_tool_receipt(",
|
||||
),
|
||||
vedastroRetryMigration.indexOf(
|
||||
"create or replace function public.refresh_agentic_rectification_vedastro_validation(",
|
||||
),
|
||||
);
|
||||
assert.match(receiptRpc, /p_tool_name not in \([\s\S]*'rectification-stop-and-review'/);
|
||||
assert.match(receiptRpc, /security definer\s+set search_path = ''/);
|
||||
assert.match(
|
||||
receiptRpc,
|
||||
/revoke all on function public\.insert_agentic_rectification_tool_receipt\([\s\S]*from public, anon, authenticated/,
|
||||
);
|
||||
assert.match(
|
||||
receiptRpc,
|
||||
/grant execute on function public\.insert_agentic_rectification_tool_receipt\([\s\S]*to service_role/,
|
||||
);
|
||||
});
|
||||
|
||||
test("VedAstro retry RPC is service-role only and validates current result ownership", () => {
|
||||
assert.match(
|
||||
vedastroRetryMigration,
|
||||
/create or replace function public\.refresh_agentic_rectification_vedastro_validation\(/,
|
||||
);
|
||||
assert.match(vedastroRetryMigration, /security definer\s+set search_path = ''/);
|
||||
assert.match(
|
||||
vedastroRetryMigration,
|
||||
/revoke all on function public\.refresh_agentic_rectification_vedastro_validation\([\s\S]*from public, anon, authenticated/,
|
||||
);
|
||||
assert.match(
|
||||
vedastroRetryMigration,
|
||||
/grant execute on function public\.refresh_agentic_rectification_vedastro_validation\([\s\S]*to service_role/,
|
||||
);
|
||||
assert.match(vedastroRetryMigration, /where id = p_case_id and user_id = p_user_id/);
|
||||
assert.match(
|
||||
vedastroRetryMigration,
|
||||
/where id = p_result_id\s+and user_id = p_user_id\s+and case_id = p_case_id/,
|
||||
);
|
||||
assert.match(vedastroRetryMigration, /evidence_ledger_fingerprint is distinct from p_evidence_ledger_fingerprint/);
|
||||
assert.match(vedastroRetryMigration, /candidate_range_fingerprint is distinct from p_candidate_range_fingerprint/);
|
||||
assert.match(vedastroRetryMigration, /newer\.created_at > v_result\.created_at/);
|
||||
assert.match(vedastroRetryMigration, /v_profile\.birth_date is distinct from/);
|
||||
assert.match(vedastroRetryMigration, /v_profile\.declared_window_start is distinct from/);
|
||||
assert.match(vedastroRetryMigration, /agentic_rectification_candidate_profile_changed/);
|
||||
});
|
||||
|
||||
test("VedAstro retry RPC accepts only safe validation fields and mutates only the receipt", () => {
|
||||
const refreshRpc = vedastroRetryMigration.slice(
|
||||
vedastroRetryMigration.indexOf(
|
||||
"create or replace function public.refresh_agentic_rectification_vedastro_validation(",
|
||||
),
|
||||
);
|
||||
assert.match(
|
||||
refreshRpc,
|
||||
/jsonb_typeof\(p_validation -> 'failure'\) is distinct from 'object'[\s\S]*jsonb_typeof\(p_validation -> 'failure'\) is distinct from 'null'/,
|
||||
);
|
||||
assert.match(
|
||||
refreshRpc,
|
||||
/v_failure_code not in \('timeout', 'engine_request_failed', 'engine_invalid_response', 'unknown'\)/,
|
||||
);
|
||||
assert.match(
|
||||
refreshRpc,
|
||||
/v_exact := jsonb_set\(v_exact, '\{vedastro_event_validation\}', v_safe_validation, true\)/,
|
||||
);
|
||||
assert.match(
|
||||
refreshRpc,
|
||||
/p_minute_sensitive_status = 'passed' and v_status <> 'passed'/,
|
||||
);
|
||||
assert.match(
|
||||
refreshRpc,
|
||||
/v_exact := jsonb_set\(v_exact, '\{external_validation_status\}', to_jsonb\(v_minute_sensitive_status\), true\)/,
|
||||
);
|
||||
const update = refreshRpc.slice(
|
||||
refreshRpc.indexOf("update public.agentic_rectification_results"),
|
||||
refreshRpc.indexOf("return jsonb_build_object("),
|
||||
);
|
||||
assert.match(update, /set decision_receipt = v_receipt,\s+updated_at = pg_catalog\.now\(\)/);
|
||||
assert.doesNotMatch(update, /candidates\s*=|inference_state\s*=|selected_time\s*=|selected_candidate_id\s*=/);
|
||||
});
|
||||
|
||||
@@ -85,8 +85,8 @@ test("checked-in registry verifies hashed product packages and leaves consult on
|
||||
[
|
||||
{
|
||||
name: "jyotish-birth-time-rectification",
|
||||
version: "10.0.12",
|
||||
sha256: "cc4510111f43f109b8483e3fd9699dbd15ef77f14a45ba1149697412075800be",
|
||||
version: "10.0.13",
|
||||
sha256: "fa38ce8529a497d24101c841371e067d00904ba6a091120496da178fa9df40e2",
|
||||
},
|
||||
{
|
||||
name: "jyotish-personal-report",
|
||||
|
||||
Reference in New Issue
Block a user