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 { 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 { 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 { 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 { 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:00–05: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); });