fix(rectification): remove static interview fallbacks
This commit is contained in:
@@ -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,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user