Files
Jyotisha/frontend/tests/rectification-agent-v6.test.ts
T

449 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import { readFileSync } from "node:fs";
import test from "node:test";
import {
diagnosticsSummarySchema,
normalizeQuestionOpportunity,
type DiagnosticsSummary,
type QuestionOpportunity,
type ValidatedDecision,
} from "../src/lib/rectification-agent/contracts.ts";
import { buildCandidateContrastPacket, buildQuestionOpportunities } from "../src/lib/rectification-agent/opportunity-builder.ts";
import { buildReasonerState } from "../src/lib/rectification-agent/reasoner-agent.ts";
import { candidateUpdateFor, realizePublicMessage, validateQuestionRealization } from "../src/lib/rectification-agent/renderer-agent.ts";
import { extractLifeEventEvidence, validatedModelAssistedEvidence } from "../src/lib/conversational-rectification/evidence-extractor.ts";
import type { CandidateSnapshot, LifeEventRevision, PendingEvidence, RectificationV4Turn } from "../src/lib/rectification-v4/contracts.ts";
import { reconcileV4Evidence } from "../src/lib/rectification-v4/extraction.ts";
const now = "2026-07-29T00:00:00.000Z";
const caseId = "00000000-0000-4000-8000-000000000601";
const snapshotId = "00000000-0000-4000-8000-000000000602";
function event(overrides: Partial<LifeEventRevision> = {}): LifeEventRevision {
return {
id: randomUUID(), eventId: randomUUID(), revision: 1, domain: "education", eventKind: "education_milestone",
subject: "self", relatedPerson: null, summary: "2016年大学入学", rawText: "2016年9月离家去外地上大学",
dateRange: { start: "2016-09-01", end: "2016-09-30", precision: "month", label: "2016年9月" },
scoreability: "scoreable", supersedesRevisionId: null, createdAt: now, ...overrides,
};
}
function turn(overrides: Partial<RectificationV4Turn> = {}): RectificationV4Turn {
return {
id: randomUUID(), caseId, caseVersion: 1, questionId: randomUUID(), questionDomain: "education",
questionTargetEventId: null, question: "请说一件时间比较确定的经历。", answer: "2016年9月离家去外地上大学。",
modelId: null, actionId: randomUUID(), createdAt: now, ...overrides,
};
}
function diagnostics(overrides: Partial<DiagnosticsSummary> = {}): DiagnosticsSummary {
return diagnosticsSummarySchema.parse({
id: randomUUID(), caseId, snapshotId, primaryClusterRetentionRate: .8, leaveOneEventOutRetentionRate: .8,
leaveOneDomainOutRetentionRate: .8, dateSensitivityRetentionRate: .8, neighborSupportMinutes: 8,
primarySecondaryMarginPercent: 14, clusterMassRatio: .7, unstableEventIds: [], mostDiscriminatingLayers: ["D9"],
eventDateSensitivity: [], candidateSplits: [], calculationHash: "d".repeat(64), createdAt: now, ...overrides,
});
}
function snapshot(range: readonly [string, string], overrides: Partial<CandidateSnapshot> = {}): CandidateSnapshot {
const [startTime, endTime] = range;
return {
id: randomUUID(), caseId, caseVersion: 3, evidenceSetHash: "e".repeat(64), calculationSpecHash: "c".repeat(64),
algorithmVersion: "rectification-v5-matrix-scoring-1",
candidates: [{ time: startTime, score: 10, supportingEventIds: [], conflictingEventIds: [] }],
clusters: [{ rank: 1, startTime, endTime, representativeTime: startTime, widthMinutes: 7, peakScore: 10, scoreMass: 1 }],
robustness: { neighborSupportMinutes: 8, leaveOneOutRetentionRate: .8, leaveOneDomainOutRetentionRate: .8, dateSensitivityRetentionRate: .8, calculationSpecHashMatched: true },
canConfirmExactMinute: false, canAcceptRange: true, gateReasons: [], createdAt: now, ...overrides,
};
}
function targetOpportunity(target: LifeEventRevision): QuestionOpportunity {
return {
contractVersion: "semantic-question-v2", opportunityId: randomUUID(), kind: "refine_event_date", domain: target.domain,
targetEventId: target.eventId, goal: `细化“${target.summary}”的日期。`, requestedFields: ["event_day"], anchors: [target.summary],
contextFacts: ["日期敏感性较高。"],
forbiddenMoves: ["switch_target_event", "ask_multiple_questions", "claim_exact_birth_minute", "invent_event", "invent_date", "expose_private_score", "expose_internal_id", "expose_technique_trace"],
fallbackPrompt: `关于“${target.summary}”,你还记得大概哪一天吗?`, reason: "日期敏感性诊断显示该事件可能改变候选排序。",
expectedInformationGain: .8, dateSensitivity: .8, candidateSplitRelevance: .5, domainCoverageGain: 0,
recallEase: .6, novelty: .8, repetitionPenalty: 0, privacyCost: .05, utility: .7, active: true,
};
}
function validated(opportunity: QuestionOpportunity): ValidatedDecision {
return {
decision: { action: "ask_question", opportunityId: opportunity.opportunityId, narrativeFocus: ["latest_event"] },
mode: "agent", validationIssues: [], selectedOpportunity: opportunity,
};
}
test("研究院实习被确定性提取为 career/self/month/scoreable", () => {
const [result] = extractLifeEventEvidence({ rawText: "2020 年 4 月去石油化工研究院实习做研究员。", sourceTurnId: randomUUID(), asOfDate: "2026-07-29" });
assert.ok(result);
assert.equal(result.domain, "career");
assert.equal(result.eventKind, "career_change");
assert.equal(result.subject, "self");
assert.equal(result.datePrecision, "month");
assert.equal(result.dateValue, "2020-04");
assert.equal(result.scoreability, "scoreable");
assert.equal(result.scoreable, true);
});
test("month 默认不细化,只有 retention 低于 .65 时才允许细化", () => {
const internship = event({ domain: "career", eventKind: "career_change", summary: "去石油化工研究院实习做研究员", rawText: "2020年4月去石油化工研究院实习做研究员", dateRange: { start: "2020-04-01", end: "2020-04-30", precision: "month", label: "2020年4月" } });
const coveredEvents = [
internship,
event({ domain: "education", eventKind: "education_milestone" }),
event({ domain: "relocation", eventKind: "relocation" }),
event({ domain: "relationship", eventKind: "relationship_change", relatedPerson: "partner" }),
event({ domain: "finance", eventKind: "finance_change" }),
event({ domain: "health_pressure", eventKind: "self_health_event" }),
];
const build = (summary: DiagnosticsSummary | null) => buildQuestionOpportunities({ caseId, events: coveredEvents, turns: [], snapshot: null, diagnostics: summary });
assert.equal(build(null).some((item) => item.kind === "refine_event_date"), false);
const sensitivity = (winnerRetentionRate: number, candidateClusterRetentionRate: number) => diagnostics({
eventDateSensitivity: [{ eventId: internship.eventId, declaredDateRange: { start: "2020-04-01", end: "2020-04-30", precision: "month" }, sampleDates: ["2020-04-01", "2020-04-30"], winnerRetentionRate, scoreVariance: 1, candidateClusterRetentionRate }],
});
const sensitiveOpportunities = build(sensitivity(.64, .1));
assert.equal(sensitiveOpportunities.some((item) => item.kind === "refine_event_date" && item.targetEventId === internship.eventId), true);
assert.doesNotMatch(sensitiveOpportunities.find((item) => item.kind === "refine_event_date")!.fallbackPrompt, /敏感性|排序|保持率|score/i);
assert.equal(build(sensitivity(.65, .65)).some((item) => item.kind === "refine_event_date"), false);
});
test("不知道或换方向不产生 pending,也不再生成同 target 机会", () => {
const target = event({ dateRange: { start: "2016-01-01", end: "2016-12-31", precision: "year", label: "2016年" } });
const answer = "记不清了,换一个吧。";
const result = reconcileV4Evidence({ caseId, answer, sourceTurnId: randomUUID(), asOfDate: "2026-07-29", existing: [target], targetEventId: target.eventId });
assert.equal(result.targetDisposition, "direction_change");
assert.deepEqual(result.pending, []);
const opportunities = buildQuestionOpportunities({ caseId, events: [target], turns: [turn({ questionTargetEventId: target.eventId, answer })], snapshot: null, diagnostics: null, targetDisposition: result.targetDisposition, retryTargetEventIds: [target.eventId] });
assert.equal(opportunities.some((item) => item.targetEventId === target.eventId), false);
assert.ok(opportunities.some((item) => item.kind === "ask_new_event"));
});
test("month-only answer refines the targeted year event", () => {
const target = event({
summary: "离家去外地上大学",
rawText: "2016 年离家去外地上大学",
dateRange: { start: "2016-01-01", end: "2016-12-31", precision: "year", label: "2016年" },
});
const result = reconcileV4Evidence({
caseId,
answer: "9 月",
sourceTurnId: randomUUID(),
asOfDate: "2026-07-31",
existing: [target],
targetEventId: target.eventId,
now: new Date(now),
});
assert.equal(result.targetDisposition, "resolved");
assert.equal(result.unansweredTargetEventId, null);
assert.deepEqual(result.pending, []);
assert.equal(result.revisions.length, 1);
assert.equal(result.revisions[0]?.eventId, target.eventId);
assert.equal(result.revisions[0]?.revision, 2);
assert.equal(result.revisions[0]?.dateRange.start, "2016-09-01");
assert.equal(result.revisions[0]?.dateRange.end, "2016-09-30");
assert.equal(result.revisions[0]?.dateRange.precision, "month");
});
test("无 target 的换方向表达也不会被记为 event_unparsed", () => {
const result = reconcileV4Evidence({ caseId, answer: "后来有一次搬家,但我记不清时间了,换一个吧。", sourceTurnId: randomUUID(), asOfDate: "2026-07-29", existing: [] });
assert.equal(result.targetDisposition, "direction_change");
assert.deepEqual(result.pending, []);
assert.equal(result.revisions.some((item) => item.scoreability === "scoreable"), false);
});
test("回答新 relocation 不覆盖 education target,原目标最多补问一次", () => {
const target = event({ dateRange: { start: "2016-01-01", end: "2016-12-31", precision: "year", label: "2016年" } });
const answer = "2018 年 8 月搬到北京。";
const assisted = validatedModelAssistedEvidence({
rawText: answer,
sourceTurnId: randomUUID(),
asOfDate: "2026-07-29",
extraction: { sourceSpan: "2018 年 8 月搬到北京", summary: "搬到北京", domain: "relocation", eventKind: "relocation", subject: "self", relatedPerson: null, dateText: "2018 年 8 月" },
});
assert.ok(assisted);
const result = reconcileV4Evidence({ caseId, answer, sourceTurnId: randomUUID(), asOfDate: "2026-07-29", existing: [target], targetEventId: target.eventId, assistedEvidence: [assisted] });
assert.equal(result.targetDisposition, "answered_other_event");
const relocation = result.revisions.find((item) => item.domain === "relocation");
assert.ok(relocation);
assert.notEqual(relocation.eventId, target.eventId);
assert.equal(result.revisions.some((item) => item.eventId === target.eventId), false);
const firstTurns = [turn({ questionTargetEventId: target.eventId, answer: "2018 年 8 月搬到北京。" })];
const first = buildQuestionOpportunities({ caseId, events: [target, relocation], turns: firstTurns, snapshot: null, diagnostics: null, targetDisposition: "answered_other_event", retryTargetEventIds: [target.eventId] });
assert.equal(first.filter((item) => item.kind === "resolve_event_conflict" && item.targetEventId === target.eventId).length, 1);
const second = buildQuestionOpportunities({ caseId, events: [target, relocation], turns: [...firstTurns, turn({ questionTargetEventId: target.eventId, answer: "2020年又搬到上海。" })], snapshot: null, diagnostics: null, targetDisposition: "answered_other_event", retryTargetEventIds: [target.eventId] });
assert.equal(second.some((item) => item.kind === "resolve_event_conflict" && item.targetEventId === target.eventId), false);
});
test("Renderer 对切换目标、多问题、出生分钟和内部信息统一回落锚定 fallback", () => {
const target = event({ summary: "2020年4月研究院实习" });
const opportunity = targetOpportunity(target);
const input = { latestAnswer: target.rawText, acceptedEvents: [target], pendingEvidence: [] as PendingEvidence[], snapshot: null, previousSnapshot: null, validated: validated(opportunity) };
const invalidQuestions = [
"你后来有没有搬家?",
"关于2020年4月研究院实习,你还记得具体月份吗?另外后来有没有换工作?",
"关于2020年4月研究院实习,你是不是05:13出生?",
"关于2020年4月研究院实习,opportunityId 是什么?",
"关于2020年4月研究院实习,snapshotId 是什么?",
"关于2020年4月研究院实习,请告诉我 score。",
"关于2020年4月研究院实习,D9 显示什么?",
"关于2020年4月研究院实习,tool call 返回什么?",
];
for (const question of invalidQuestions) {
assert.equal(validateQuestionRealization(question, opportunity).valid, false, question);
const message = realizePublicMessage({ acknowledgement: `你提到的是“${target.summary}”。`, candidateUpdate: null, limitation: null, question }, input);
assert.equal(message.question, opportunity.fallbackPrompt, question);
}
});
test("稳定候选范围相同不重复提示,实际变化才提示且不确认唯一分钟", () => {
const previous = snapshot(["05:00", "05:30"]);
const same = snapshot(["05:00", "05:30"], { clusters: [
{ rank: 1, startTime: "05:00", endTime: "05:30", representativeTime: "05:13", widthMinutes: 31, peakScore: 10, scoreMass: 1 },
{ rank: 2, startTime: "05:40", endTime: "05:45", representativeTime: "05:42", widthMinutes: 6, peakScore: 8, scoreMass: .4 },
] });
const changed = snapshot(["05:12", "05:18"]);
assert.equal(candidateUpdateFor({ snapshot: same, previousSnapshot: previous, decisionAction: "ask_question" }), null);
const offered = candidateUpdateFor({ snapshot: same, previousSnapshot: previous, decisionAction: "offer_candidate_range" });
assert.match(offered ?? "", /05:0005:30/);
assert.doesNotMatch(offered ?? "", /05:13|05:40|05:42|05:45/);
const update = candidateUpdateFor({ snapshot: changed, previousSnapshot: previous, decisionAction: "ask_question" });
assert.ok(update);
assert.match(update, /05:12.*05:18/);
assert.doesNotMatch(update, /唯一|确认.*分钟|代表分钟/);
assert.equal(changed.canConfirmExactMinute, false);
});
test("Builder 只提供领域中立的新事件降级契约,不根据测试语料选题", () => {
const education = event({
summary: "离家去外地上大学",
rawText: "2016年9月离家去外地上大学",
createdAt: "2026-07-29T01:00:00.000Z",
});
const career = event({
domain: "career",
eventKind: "career_change",
summary: "去研究院实习",
rawText: "2020年4月去研究院实习",
dateRange: { start: "2020-04-01", end: "2020-04-30", precision: "month", label: "2020年4月" },
createdAt: "2026-07-29T02:00:00.000Z",
});
const collect = (events: readonly LifeEventRevision[]) => buildQuestionOpportunities({
caseId, events, turns: [], snapshot: null, diagnostics: null,
}).filter((item) => item.kind === "ask_new_event");
const forward = collect([education, career]);
const reversed = collect([career, education]);
assert.equal(forward.length, 1);
assert.equal(forward[0]?.domain, "other");
assert.deepEqual(forward, reversed);
assert.match(forward[0]?.goal ?? "", /Agent|自主选择/);
assert.match(forward[0]?.fallbackPrompt ?? "", /愿意再讲一件/);
assert.doesNotMatch([forward[0]?.goal, forward[0]?.fallbackPrompt].join(" "), /复读|住校|搬家|关系确立|石油化工研究院|商业巡演/);
});
test("Renderer 不再用领域关键词限制 Agent 的新事件选题", () => {
const baseOpportunity = buildQuestionOpportunities({
caseId,
events: [event()],
turns: [],
snapshot: null,
diagnostics: null,
}).find((item) => item.kind === "ask_new_event");
assert.ok(baseOpportunity);
for (const question of [
"你是否有过获得长期资格或重要证书的经历,大概发生在什么时候?",
"有没有一次你公开发布长期创作项目的经历,大概是哪年哪月?",
"如果你愿意,哪次生活责任明显改变的时间你记得最清楚?",
]) {
assert.equal(validateQuestionRealization(question, baseOpportunity).valid, true, question);
}
assert.equal(validateQuestionRealization("请说说获得证书的时间?另外哪次创作发布最重要?", baseOpportunity).valid, false);
assert.equal(validateQuestionRealization("请根据 snapshotId 选择最高 score 的事件?", baseOpportunity).valid, false);
});
test("Agent 自主问题被保留,不会被服务器替换成领域 fallback", () => {
const latest = event({
domain: "career",
eventKind: "career_change",
summary: "去研究院实习",
rawText: "2020年4月去研究院实习",
dateRange: { start: "2020-04-01", end: "2020-04-30", precision: "month", label: "2020年4月" },
});
const opportunity = buildQuestionOpportunities({ caseId, events: [latest], turns: [], snapshot: null, diagnostics: null })
.find((item) => item.kind === "ask_new_event");
assert.ok(opportunity);
const question = "有没有一次你取得长期资格或身份变化的经历,大概发生在什么时候?";
const message = realizePublicMessage({
acknowledgement: "你提到的是 2020年4月去研究院实习。",
candidateUpdate: null,
limitation: null,
question,
}, {
latestAnswer: latest.rawText,
acceptedEvents: [latest],
pendingEvidence: [],
snapshot: null,
previousSnapshot: null,
validated: validated(opportunity),
});
assert.equal(message.question, question);
});
test("targetEventId 非空时只接受完整真实 anchor,不接受代词或四字片段", () => {
const target = event({ summary: "2020年4月研究院实习" });
const opportunity = targetOpportunity(target);
assert.equal(validateQuestionRealization("关于2020年4月研究院实习,你还记得具体日期吗?", opportunity).valid, true);
assert.equal(validateQuestionRealization("关于研究院实习,你还记得具体日期吗?", opportunity).valid, false);
assert.equal(validateQuestionRealization("关于这次经历,你还记得具体日期吗?", opportunity).valid, false);
});
test("Builder 和 Reasoner 按事件创建时间承接最近经历而不是 UUID 顺序", () => {
const older = event({
eventId: "ffffffff-ffff-4fff-8fff-ffffffffffff",
summary: "较早的研究院实习",
createdAt: "2026-07-29T01:00:00.000Z",
});
const newer = event({
eventId: "00000000-0000-4000-8000-000000000000",
domain: "relocation",
eventKind: "relocation",
summary: "刚提到的搬到北京",
rawText: "2018年8月搬到北京",
dateRange: { start: "2018-08-01", end: "2018-08-31", precision: "month", label: "2018年8月" },
createdAt: "2026-07-29T02:00:00.000Z",
});
const opportunities = buildQuestionOpportunities({ caseId, events: [newer, older], turns: [], snapshot: null, diagnostics: null });
const askNewEvent = opportunities.find((item) => item.kind === "ask_new_event");
assert.ok(askNewEvent);
assert.match(askNewEvent.fallbackPrompt, /刚提到的搬到北京/);
assert.doesNotMatch(askNewEvent.fallbackPrompt, /较早的研究院实习/);
const state = buildReasonerState({ snapshot: null, diagnostics: diagnostics(), opportunities, recentEvents: [newer, older] });
assert.equal(state.recentEvents.at(-1)?.summary, "刚提到的搬到北京");
});
test("Reasoner 状态包含最近语义上下文但不包含贡献矩阵", () => {
const latestEvent = event();
const opportunity = targetOpportunity(latestEvent);
const recentTurns = Array.from({ length: 7 }, (_, index) => turn({ answer: `回答${index}` }));
const pending: PendingEvidence = { id: randomUUID(), caseId, turnId: recentTurns.at(-1)!.id, rawText: "后来去了北京", reasonCode: "date_unresolved", targetEventId: null, resolvedEventId: null, createdAt: now, resolvedAt: null };
const state = buildReasonerState({ snapshot: snapshot(["05:12", "05:18"]), diagnostics: diagnostics(), opportunities: [opportunity], recentTurns, recentEvents: [latestEvent], currentTarget: latestEvent, targetDisposition: "unresolved", pendingEvidence: [pending], candidateRangeChanged: true });
assert.equal(state.latestAnswer, "回答6");
assert.equal(state.recentTurns.length, 6);
assert.equal(state.recentEvents[0]?.summary, latestEvent.summary);
assert.equal(state.targetDisposition, "unresolved");
assert.equal(state.opportunities[0]?.goal, opportunity.goal);
assert.equal(state.pendingEvidence.count, 1);
assert.doesNotMatch(JSON.stringify(state), /contribution(?:Matrix| matrix|_matrix)?/i);
});
test("模型辅助提取拒绝发明日期,接受原文连续日期并可进入 Event Ledger", () => {
const invented = validatedModelAssistedEvidence({
rawText: "大学毕业后去了北京。", sourceTurnId: randomUUID(), asOfDate: "2026-07-29",
extraction: { sourceSpan: "大学毕业后去了北京", summary: "大学毕业后去了北京", domain: "relocation", eventKind: "relocation", subject: "self", relatedPerson: null, dateText: "2020年7月" },
});
assert.equal(invented, null);
const normalizedButNotLiteral = validatedModelAssistedEvidence({
rawText: "2022年11月把生活重心挪到了成都。", sourceTurnId: randomUUID(), asOfDate: "2026-07-29",
extraction: { sourceSpan: "2022年11月把生活重心挪到了成都", summary: "把生活重心挪到了成都", domain: "relocation", eventKind: "relocation", subject: "self", relatedPerson: null, dateText: "2022年11月" },
});
assert.equal(normalizedButNotLiteral, null);
const rawText = "2022年11月把生活重心挪到了成都。";
const assisted = validatedModelAssistedEvidence({
rawText, sourceTurnId: randomUUID(), asOfDate: "2026-07-29",
extraction: { sourceSpan: "2022年11月把生活重心挪到了成都", summary: "把生活重心挪到了成都", domain: "relocation", eventKind: "relocation", subject: "self", relatedPerson: null, dateText: "2022年11月" },
});
assert.ok(assisted);
assert.equal(assisted.dateValue, "2022-11");
const reconciled = reconcileV4Evidence({ caseId, answer: rawText, sourceTurnId: randomUUID(), asOfDate: "2026-07-29", existing: [], assistedEvidence: [assisted] });
assert.ok(reconciled.revisions.some((item) => item.domain === "relocation" && item.scoreability === "scoreable"));
});
test("旧 prompt Opportunity 归一化为 semantic-question-v2", () => {
const legacyPrompt = "请说一件时间比较明确的经历。";
const normalized = normalizeQuestionOpportunity({ prompt: legacyPrompt, domain: "career", reason: "历史记录" });
assert.equal(normalized.contractVersion, "semantic-question-v2");
assert.equal(normalized.fallbackPrompt, legacyPrompt);
assert.equal(normalized.domain, "career");
assert.ok(normalized.opportunityId);
});
test("V6 迁移只更新未完成 Case 版本且不写 active_birth_time", () => {
const migration = readFileSync(new URL("../supabase/migrations/20260729010000_rectification_agent_v6_versions.sql", import.meta.url), "utf8");
assert.match(migration, /alter column skill_version set default 'birth-time-rectification-v6'/);
assert.match(migration, /alter column prompt_version set default 'rectification-agent-v6-1'/);
assert.match(migration, /where status in \('awaiting_answer', 'processing', 'paused'\)/);
assert.doesNotMatch(migration, /profiles\s*\.\s*active_birth_time|active_birth_time/i);
});
test("V8 Director migration advances only unfinished Agent cases", () => {
const migration = readFileSync(new URL("../supabase/migrations/20260731020000_rectification_director_v4_runtime.sql", import.meta.url), "utf8");
assert.match(migration, /alter column skill_version set default 'birth-time-rectification-v8'/);
assert.match(migration, /alter column prompt_version set default 'rectification-director-v4'/);
assert.match(migration, /where status in \('awaiting_answer', 'processing', 'paused'\)/);
assert.match(migration, /deployment_mode in \('v5_shadow', 'v5_agent'\)/);
assert.doesNotMatch(migration, /profiles\s*\.\s*active_birth_time|active_birth_time/i);
});
test("Candidate Contrast 暴露全部缺失证据事实,但不替 Agent 选定公开领域", () => {
const relationshipEnd = event({
domain: "relationship",
eventKind: "relationship_end",
summary: "一段关系结束",
rawText: "2024年7月一段关系结束",
dateRange: { start: "2024-07-01", end: "2024-07-31", precision: "month", label: "2024年7月" },
});
const currentSnapshot = snapshot(["05:10", "05:14"], {
canAcceptRange: false,
gateReasons: ["insufficient_candidate_separation"],
clusters: [
{ rank: 1, startTime: "05:10", endTime: "05:14", representativeTime: "05:13", widthMinutes: 5, peakScore: 10, scoreMass: .51 },
{ rank: 2, startTime: "05:16", endTime: "05:20", representativeTime: "05:17", widthMinutes: 5, peakScore: 9.9, scoreMass: .49 },
],
});
const splitDiagnostics = diagnostics({
candidateSplits: [{
leftCluster: { start: "05:10", end: "05:14" },
rightCluster: { start: "05:16", end: "05:20" },
techniqueLayers: ["D9", "vimshottari"],
eventIds: [relationshipEnd.eventId],
}],
});
const packet = buildCandidateContrastPacket({ events: [relationshipEnd], snapshot: currentSnapshot, diagnostics: splitDiagnostics });
assert.ok(packet?.missingEvidence.some((item) => item.eventKind === "relationship_start"));
assert.ok(packet?.missingEvidence.some((item) => item.eventKind === "relationship_change"));
const next = buildQuestionOpportunities({ caseId, events: [relationshipEnd], turns: [], snapshot: currentSnapshot, diagnostics: splitDiagnostics })
.find((item) => item.kind === "ask_new_event");
assert.ok(next);
assert.equal(next.domain, "other");
const fallbackWithoutAnchor = next.fallbackPrompt.replace("一段关系结束", "");
assert.doesNotMatch(fallbackWithoutAnchor, /关系|恋爱|分手|D9|05:1[037]/i);
assert.match(next.contextFacts.join(" "), /relationship_start/);
});
test("真实回放不会把大学和实习样例固化为下一问模板", () => {
const education = event({ summary: "离家去外地上大学", rawText: "2016年9月离家去外地上大学" });
const internship = event({
domain: "career",
eventKind: "career_change",
summary: "去石油化工研究院实习做研究员",
rawText: "2020年4月去石油化工研究院实习做研究员",
dateRange: { start: "2020-04-01", end: "2020-04-30", precision: "month", label: "2020年4月" },
});
const next = buildQuestionOpportunities({ caseId, events: [education, internship], turns: [], snapshot: null, diagnostics: null })
.find((item) => item.kind === "ask_new_event");
assert.ok(next);
assert.equal(next.domain, "other");
const fallbackWithoutAnchor = next.anchors.reduce((text, anchor) => text.replace(anchor, ""), next.fallbackPrompt);
assert.doesNotMatch(fallbackWithoutAnchor, /大学|实习|研究院|搬家|关系|工作|健康|财务/);
assert.equal(validateQuestionRealization("有没有一次你取得长期资格或公开发布作品的经历,大概发生在什么时候?", next).valid, true);
});