BUG-915:BUG-912 首版只在盖戳那一刻把自建探针注入内存 state,答题 / GET /
idle 三处读持久化 receipt 都找不到它 → 点选与打字回答报 stale_probe、GET 无卡、
刚落库的焦点每次 idle persist 被 superseded。新增
withOwnedDistinguishProbes(state, receipt) 按 window_scan.transitions + state
候选确定性重建(照 withNakshatraBoundaryProbe 的既有模式),接入盖戳、点选答题、
打字答题、GET 投影、idle 过期五处,并加源码契约测试钉住。重建探针的 source 为
owned_varga_style,像 nakshatra_boundary 一样从 contrast packet 与 event 探针池
排除,避免答题写回 state 后它们变成可问的题。
产品决策(任务书 §1d,选项 b):分盘风格题不再作为 distinguish_candidates 计分题。
分层判别题只在引擎给出该领域带年份事件探针时以「某年前后有没有…」出题并按该探针
计分;没有事件探针时不出题,计划直接走下一条线。删除 attachVargaDistinguishIdentity
与 withFollowupOwnedProbe,保留 buildVargaDistinguishFields / conflictProbeFromFollowup
供重建与将来选项 (a)。
BUG-917:最新助手消息带未答点选而 GET 无卡时,缺口态改走 unavailable 修复出口,
不再与「再说一件带年月的事」同屏;superseded 且未作答的焦点不再渲染成灰色选项。
BUG-916:年月阶段 host 前置挪到会话校验之后,phase 复用 answer.host_fallback。
测试改走生产路径(inferenceForPersistedAnswer / choiceCardFromCaseDossier /
persistedDistinguishFocusStale),删掉手工把自建探针塞进 state 的 fixture。
tsc 0 错;lint 0 error / 116 warning;npm test 3421 条 / 31 红,失败清单与基线
c32f81e7 逐条一致;next build --webpack 后 / 仍 ○ Static,产物 JS gzip
1,496,339 → 1,496,408(+0.005%);pytest rectification 定向 63 绿,快速门 Python
段 798 绿。Skill 未 bump。
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
494 lines
18 KiB
TypeScript
494 lines
18 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import test from "node:test";
|
||
import { readFileSync } from "node:fs";
|
||
|
||
import { buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts";
|
||
import {
|
||
applyChoiceWithoutEvidence,
|
||
previousInferenceFromReceipt,
|
||
stampChoiceSchemaWithProbe,
|
||
} from "../src/lib/rectification-agentic/v9/inference-adapter.ts";
|
||
|
||
import {
|
||
buildMethodFollowupPlan,
|
||
yearlessPersonalityCanAsk,
|
||
type MethodFollowup,
|
||
} from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||
import { choiceCardFromCaseDossier } from "../src/lib/rectification-agentic/v9/interview-state.ts";
|
||
import { rectificationFollowupCatalog } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
|
||
import {
|
||
inferenceForPersistedAnswer,
|
||
persistedDistinguishFocusStale,
|
||
} from "../src/lib/rectification-agentic/v9/answer-choice.ts";
|
||
import { expectedAnswerSchemaFor } from "../src/lib/rectification-agentic/v9/server-focus.ts";
|
||
import { buildChoiceFrame } from "../src/lib/rectification-agentic/v9/choice-card.ts";
|
||
import { buildRangeDelivery } from "../src/lib/rectification-agentic/v9/divergence-panel.ts";
|
||
import { parseWindowScan } from "../src/lib/rectification-agentic/v9/varga-observations.ts";
|
||
import {
|
||
buildVargaDistinguishFields,
|
||
withOwnedDistinguishProbes,
|
||
} from "../src/lib/rectification-agentic/v9/varga-distinguish-probe.ts";
|
||
import { rectificationQuestionGapState } from "../src/lib/rectification-surface-state.ts";
|
||
import { FOCUS_ID } from "./rectification-v9-test-support.ts";
|
||
|
||
const OWNED_KEY = "d9_relationship:relationship_style";
|
||
const QUESTION_ID = "d9_relationship:relationship_style:score";
|
||
|
||
const DATED = [
|
||
{
|
||
status: "confirmed",
|
||
domain: "health_pressure",
|
||
datePrecision: "month",
|
||
occurredFrom: "2019-03-01",
|
||
occurredTo: "2019-03-31",
|
||
},
|
||
{
|
||
status: "confirmed",
|
||
domain: "career",
|
||
datePrecision: "year",
|
||
occurredFrom: "2018-01-01",
|
||
occurredTo: "2018-12-31",
|
||
},
|
||
{
|
||
status: "confirmed",
|
||
domain: "education",
|
||
datePrecision: "year",
|
||
occurredFrom: "2016-01-01",
|
||
occurredTo: "2016-12-31",
|
||
},
|
||
{
|
||
status: "confirmed",
|
||
domain: "occupation",
|
||
datePrecision: "unknown",
|
||
occurredFrom: null,
|
||
occurredTo: null,
|
||
},
|
||
];
|
||
|
||
const D24 = {
|
||
id: "contrast:varga.d24.白羊座|金牛座|双子座|巨蟹座|狮子座|处女座",
|
||
semantic_key: "varga.d24.白羊座|金牛座|双子座|巨蟹座|狮子座|处女座",
|
||
candidate_split_hash: "set:varga.d24.白羊座|金牛座|双子座|巨蟹座|狮子座|处女座",
|
||
domain: "education",
|
||
year: 0,
|
||
question: "学业对照",
|
||
candidate_ids: ["04:48", "04:59"],
|
||
expected_outcomes: [
|
||
{ answer_class: "yes" as const, supports: ["04:48"], conflicts: ["04:59"] },
|
||
{ answer_class: "weak_yes" as const, supports: ["04:59"], conflicts: ["04:48"] },
|
||
{ answer_class: "no" as const, supports: [], conflicts: [] },
|
||
{ answer_class: "unsure" as const, supports: [], conflicts: [] },
|
||
],
|
||
information_gain: 0.9,
|
||
source: "engine",
|
||
choice_kind: "event_quality" as const,
|
||
};
|
||
|
||
/** Engine window scan as it arrives on the decision receipt. */
|
||
const WINDOW_SCAN_RECEIPT = {
|
||
scanned: true,
|
||
d9_lagna_count: 2,
|
||
d10_lagna_count: 1,
|
||
d9_candidates_differ: true,
|
||
d10_candidates_differ: false,
|
||
d9_sign_names: ["天蝎座", "天秤座"],
|
||
d10_sign_names: ["巨蟹座"],
|
||
transitions: [
|
||
{ layer: "d9", at: "04:53", from_sign: "天蝎座", to_sign: "天秤座" },
|
||
{ layer: "d10", at: "04:40", from_sign: "巨蟹座", to_sign: "巨蟹座" },
|
||
],
|
||
};
|
||
|
||
const WINDOW = parseWindowScan(WINDOW_SCAN_RECEIPT)!;
|
||
|
||
const CANDIDATES = [
|
||
{ id: "04:48", time: "04:48" },
|
||
{ id: "04:53", time: "04:53" },
|
||
{ id: "04:59", time: "04:59" },
|
||
];
|
||
|
||
/** The persisted state: engine probes only, exactly as the receipt stores it. */
|
||
function persistedState() {
|
||
return buildInferenceState({
|
||
range_start: "04:48",
|
||
range_end: "05:07",
|
||
candidates: [
|
||
{ id: "04:48", time: "04:48", relative_support: 44 },
|
||
{ id: "04:53", time: "04:53", relative_support: 35 },
|
||
{ id: "04:59", time: "04:59", relative_support: 21 },
|
||
],
|
||
events: Array.from({ length: 5 }, (_, index) => ({
|
||
id: `e${index}`,
|
||
domain: "health",
|
||
year: 2019,
|
||
precision: "month" as const,
|
||
})),
|
||
probes: [D24],
|
||
});
|
||
}
|
||
|
||
function receipt() {
|
||
return { inference_state: persistedState(), window_scan: WINDOW_SCAN_RECEIPT };
|
||
}
|
||
|
||
/**
|
||
* A D9 style focus as it was persisted before the 2026-09-17 (b) decision:
|
||
* its identity is the layer's own key, and its probe lives nowhere but the
|
||
* receipt's window scan.
|
||
*/
|
||
function legacyD9Followup(): MethodFollowup {
|
||
const fields = buildVargaDistinguishFields({
|
||
methodId: "d9_relationship",
|
||
askTheme: "relationship_style",
|
||
transitions: WINDOW.transitions,
|
||
candidates: CANDIDATES,
|
||
});
|
||
assert.ok(fields);
|
||
const followup = {
|
||
method_id: "d9_relationship",
|
||
intent: "distinguish_candidates",
|
||
ask_theme: "relationship_style",
|
||
domain: "relationship",
|
||
kind_hint: "relationship_change",
|
||
user_prompt_hint: "当前候选在关系主题上仍分不开。",
|
||
source: "varga_observation",
|
||
must_not_label: false,
|
||
choice_frame: null,
|
||
...fields,
|
||
} as MethodFollowup;
|
||
const frame = buildChoiceFrame(followup, {
|
||
evidence: DATED,
|
||
probes: [{
|
||
year: 0,
|
||
year_label: "当前这几个候选",
|
||
domain: "relationship",
|
||
event_family: "开始认真关系、分手或结婚",
|
||
source: "dasha_activation",
|
||
tracks: ["vimshottari", "narayana"],
|
||
tracks_agree: true,
|
||
unique_minute_claim: false,
|
||
user_meaning: followup.user_prompt_hint,
|
||
role: "distinguish",
|
||
phase: "candidate_discriminator",
|
||
information_gain: followup.information_gain,
|
||
semantic_key: followup.semantic_key,
|
||
candidate_split_hash: followup.candidate_split_hash,
|
||
candidate_ids: followup.candidate_ids,
|
||
expected_outcomes: followup.expected_outcomes,
|
||
choice_kind: "varga_style" as const,
|
||
style_options: followup.style_options,
|
||
}],
|
||
});
|
||
assert.ok(frame);
|
||
return { ...followup, choice_frame: frame };
|
||
}
|
||
|
||
function persistedSchema(): Record<string, unknown> {
|
||
const followup = legacyD9Followup();
|
||
const schema = expectedAnswerSchemaFor(
|
||
followup.choice_frame!,
|
||
QUESTION_ID,
|
||
receipt(),
|
||
followup,
|
||
);
|
||
assert.ok(schema);
|
||
return schema;
|
||
}
|
||
|
||
test("stamping rebuilds the owned probe from the receipt instead of borrowing D24 (BUG-912/915)", () => {
|
||
const state = persistedState();
|
||
const borrowed = stampChoiceSchemaWithProbe({
|
||
choice: {
|
||
options: [
|
||
{ key: "A", answer_class: "yes" },
|
||
{ key: "B", answer_class: "weak_yes" },
|
||
{ key: "C", answer_class: "no" },
|
||
{ key: "D", answer_class: "unsure" },
|
||
],
|
||
},
|
||
}, state, QUESTION_ID);
|
||
assert.notEqual(borrowed.probe_id, D24.id);
|
||
|
||
// The persisted receipt itself never carries the owned probe.
|
||
const stored = previousInferenceFromReceipt(receipt());
|
||
assert.ok(stored);
|
||
assert.equal(stored.probes.some((probe) => probe.semantic_key === OWNED_KEY), false);
|
||
const rebuilt = withOwnedDistinguishProbes(stored, receipt());
|
||
assert.ok(rebuilt?.probes.some((probe) => probe.semantic_key === OWNED_KEY));
|
||
|
||
const schema = persistedSchema();
|
||
assert.equal(schema.semantic_key, OWNED_KEY);
|
||
assert.equal(schema.probe_id, `probe:${OWNED_KEY}`);
|
||
assert.notEqual(String(schema.probe_id ?? ""), D24.id);
|
||
});
|
||
|
||
test("the production answer path applies a persisted owned-probe choice (BUG-915)", () => {
|
||
const schema = persistedSchema();
|
||
const answerInput = {
|
||
choiceKey: "C",
|
||
schema,
|
||
questionId: QUESTION_ID,
|
||
domain: "relationship",
|
||
classifiedFrom: "choice" as const,
|
||
};
|
||
// Without the rebuild the same schema is unanswerable: that was the P0.
|
||
const withoutRebuild = applyChoiceWithoutEvidence(previousInferenceFromReceipt(receipt())!, answerInput);
|
||
assert.equal(withoutRebuild.applied, false);
|
||
assert.equal(withoutRebuild.reason, "stale_probe");
|
||
|
||
const applied = applyChoiceWithoutEvidence(inferenceForPersistedAnswer(receipt())!, answerInput);
|
||
assert.equal(applied.reason, "applied");
|
||
assert.equal(applied.applied, true);
|
||
assert.equal(applied.state.answered_probes.at(-1)?.semantic_key, OWNED_KEY);
|
||
assert.ok(!applied.state.answered_probes.some((row) => row.probe_id === D24.id));
|
||
});
|
||
|
||
test("GET projects the persisted D9 card from the receipt alone (BUG-915)", () => {
|
||
const schema = persistedSchema();
|
||
const card = choiceCardFromCaseDossier({
|
||
evidence: DATED,
|
||
conversationSummary: {
|
||
activeFocus: {
|
||
id: FOCUS_ID,
|
||
questionId: QUESTION_ID,
|
||
intent: "distinguish_candidates",
|
||
targetDomain: "relationship",
|
||
targetKind: null,
|
||
expectedAnswerSchema: schema,
|
||
},
|
||
declinedSkippedTopics: [],
|
||
},
|
||
latestResult: {
|
||
resultId: "result-1",
|
||
decisionReceipt: receipt(),
|
||
selectionAllowed: false,
|
||
confirmationAllowed: false,
|
||
candidates: [
|
||
{ time: "04:48", rank: 1, relativeSupport: 44 },
|
||
{ time: "04:53", rank: 2, relativeSupport: 35 },
|
||
{ time: "04:59", rank: 3, relativeSupport: 21 },
|
||
],
|
||
},
|
||
case: { acceptedTime: null, status: "active", stage: "minute" },
|
||
});
|
||
assert.ok(card, "GET must render the persisted focus");
|
||
assert.equal(card.question_id, QUESTION_ID);
|
||
assert.equal(card.options.length, 4);
|
||
});
|
||
|
||
test("idle persist does not supersede a focus whose probe is rebuilt (BUG-915)", () => {
|
||
const schema = persistedSchema();
|
||
const focus = { intent: "distinguish_candidates", expectedAnswerSchema: schema };
|
||
// The raw receipt state has no owned probe: that is what superseded the
|
||
// fresh focus on every idle persist.
|
||
assert.equal(persistedDistinguishFocusStale(previousInferenceFromReceipt(receipt()), focus), true);
|
||
assert.equal(persistedDistinguishFocusStale(inferenceForPersistedAnswer(receipt()), focus), false);
|
||
|
||
// A changed candidate set still expires the focus (BUG-559 stays).
|
||
const movedReceipt = {
|
||
window_scan: WINDOW_SCAN_RECEIPT,
|
||
inference_state: buildInferenceState({
|
||
range_start: "04:48",
|
||
range_end: "05:07",
|
||
candidates: [
|
||
{ id: "04:48", time: "04:48", relative_support: 60 },
|
||
{ id: "04:59", time: "04:59", relative_support: 40 },
|
||
],
|
||
events: persistedState().events,
|
||
probes: [D24],
|
||
}),
|
||
};
|
||
assert.equal(persistedDistinguishFocusStale(inferenceForPersistedAnswer(movedReceipt), focus), true);
|
||
});
|
||
|
||
test("every state read that can touch an owned probe rebuilds it (BUG-915 source contract)", () => {
|
||
const sites = [
|
||
"src/lib/rectification-agentic/v9/server-focus.ts",
|
||
"src/lib/rectification-agentic/v9/interview-state.ts",
|
||
];
|
||
for (const site of sites) {
|
||
const source = readFileSync(new URL(`../${site}`, import.meta.url), "utf8");
|
||
assert.match(source, /withOwnedDistinguishProbes\(/, site);
|
||
}
|
||
const answerChoice = readFileSync(
|
||
new URL("../src/lib/rectification-agentic/v9/answer-choice.ts", import.meta.url),
|
||
"utf8",
|
||
);
|
||
// The tap answer and the idle supersede check both read the rebuilt state.
|
||
assert.match(answerChoice, /const previous = inferenceForPersistedAnswer\(receipt\);/);
|
||
assert.match(answerChoice, /const inferenceForStale = withOwnedDistinguishProbes\(/);
|
||
const tools = readFileSync(new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url), "utf8");
|
||
// The typed answer goes through the same helper as the tap.
|
||
assert.match(tools, /const previous = inferenceForPersistedAnswer\(/);
|
||
});
|
||
|
||
const REL_2020_PROBE = {
|
||
year: 2020,
|
||
year_label: "2020 年前后",
|
||
domain: "relationship" as const,
|
||
event_family: "开始认真关系、分手或结婚",
|
||
source: "dasha_activation" as const,
|
||
tracks: ["vimshottari", "narayana"] as const,
|
||
tracks_agree: true,
|
||
unique_minute_claim: false as const,
|
||
user_meaning: "2020 年感情线",
|
||
role: "distinguish" as const,
|
||
phase: "candidate_discriminator" as const,
|
||
information_gain: 0.5,
|
||
semantic_key: "relationship.2020",
|
||
candidate_split_hash: "set:relationship.2020",
|
||
candidate_ids: ["04:48", "04:53", "04:59"],
|
||
expected_outcomes: [
|
||
{ answer_class: "yes" as const, supports: ["04:48"], conflicts: ["04:53", "04:59"] },
|
||
{ answer_class: "weak_yes" as const, supports: ["04:53"], conflicts: ["04:48", "04:59"] },
|
||
{ answer_class: "no" as const, supports: ["04:59"], conflicts: ["04:48", "04:53"] },
|
||
{ answer_class: "unsure" as const, supports: [], conflicts: [] },
|
||
],
|
||
choice_kind: "existence" as const,
|
||
};
|
||
|
||
/** Every dated collect line already closed, as in the 2026-09-17 real run. */
|
||
const CLOSED_COLLECT = [
|
||
"family",
|
||
"education",
|
||
"finance",
|
||
"relocation",
|
||
"health_pressure",
|
||
"career",
|
||
"relationship",
|
||
].map((domain) => ({
|
||
question_id: `collect:targeted:${domain}`,
|
||
target_kind: `targeted:${domain}`,
|
||
domain,
|
||
status: "declined",
|
||
intent: "collect_method_evidence",
|
||
}));
|
||
|
||
function planWithD9Split(input: { askedProbeKeys: readonly string[] }) {
|
||
return buildMethodFollowupPlan({
|
||
evidence: DATED,
|
||
observations: [
|
||
{ layer: "d9", candidates_differ: true, ask_theme: "relationship_style" },
|
||
{ layer: "d10", candidates_differ: false, ask_theme: null },
|
||
],
|
||
windowScan: WINDOW,
|
||
inferenceState: persistedState(),
|
||
sessionOutcome: "discriminate_candidates",
|
||
precisionStage: "d9_refine",
|
||
eventProbes: [REL_2020_PROBE],
|
||
askedProbeKeys: input.askedProbeKeys,
|
||
closedCollectFocuses: CLOSED_COLLECT,
|
||
topCandidateTimes: ["04:48", "04:53", "04:59"],
|
||
candidateRanges: CANDIDATES.map((row) => ({ id: row.id, time: row.time })),
|
||
});
|
||
}
|
||
|
||
test("a varga style question is never a scoring discriminator (2026-09-17 decision b)", () => {
|
||
// The engine's relationship probe is already asked, so nothing dates this
|
||
// line: before the decision this is where 「你更接近哪一种相处方式」 came from.
|
||
const plan = planWithD9Split({ askedProbeKeys: ["relationship.2020"] });
|
||
const next = plan.next_followup;
|
||
assert.notEqual(next?.semantic_key, OWNED_KEY);
|
||
assert.notEqual(next?.choice_kind, "varga_style");
|
||
assert.doesNotMatch(next?.choice_frame?.prompt ?? "", /更接近哪一种/);
|
||
assert.equal(
|
||
next?.intent === "distinguish_candidates"
|
||
&& (next.source === "varga_observation" || next.source === "precision_stage"),
|
||
false,
|
||
JSON.stringify({ intent: next?.intent, source: next?.source, key: next?.semantic_key }),
|
||
);
|
||
assert.ok(
|
||
plan.dropped_probes.some((row) => (
|
||
row.semantic_key === "d9_relationship" && row.reason === "frameless_distinguish"
|
||
)),
|
||
JSON.stringify(plan.dropped_probes),
|
||
);
|
||
});
|
||
|
||
test("the D9 line is still asked when the engine dates it (2026-09-17 decision b)", () => {
|
||
const plan = planWithD9Split({ askedProbeKeys: [] });
|
||
const next = plan.next_followup;
|
||
assert.equal(next?.method_id, "d9_relationship");
|
||
assert.equal(next?.intent, "distinguish_candidates");
|
||
assert.equal(next?.semantic_key, REL_2020_PROBE.semantic_key);
|
||
assert.equal(next?.choice_kind, "existence");
|
||
assert.match(next?.choice_frame?.prompt ?? "", /2020/);
|
||
});
|
||
|
||
test("a rebuilt owned probe never becomes an askable discriminator (decision b)", () => {
|
||
// Answering an old style focus writes the rebuilt probes into the receipt.
|
||
const answered = withOwnedDistinguishProbes(previousInferenceFromReceipt(receipt()), receipt())!;
|
||
const owned = answered.probes.find((probe) => probe.semantic_key === OWNED_KEY);
|
||
assert.ok(owned);
|
||
const catalog = rectificationFollowupCatalog(
|
||
{ resultId: "result-1", decisionReceipt: { ...receipt(), inference_state: answered } },
|
||
DATED,
|
||
);
|
||
assert.equal(catalog.eventProbes.some((probe) => probe.semantic_key === OWNED_KEY), false);
|
||
assert.equal(
|
||
(catalog.contrastPacket?.probes ?? []).some((probe) => probe.semanticKey === OWNED_KEY),
|
||
false,
|
||
);
|
||
});
|
||
|
||
test("a dead tap card never shares the screen with the collect-wait placeholder (BUG-917)", () => {
|
||
const base = {
|
||
liveQuestionVisible: false,
|
||
questionMissing: true,
|
||
questionLoadFailed: false,
|
||
busy: false,
|
||
readonly: false,
|
||
regenerating: false,
|
||
snapshotLoaded: true,
|
||
resumableCase: true,
|
||
retryAttempts: 0,
|
||
collectWaiting: true,
|
||
};
|
||
assert.equal(rectificationQuestionGapState(base), "collect_waiting");
|
||
assert.equal(
|
||
rectificationQuestionGapState({ ...base, unansweredDeadChoice: true }),
|
||
"unavailable",
|
||
);
|
||
const chat = readFileSync(
|
||
new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url),
|
||
"utf8",
|
||
);
|
||
assert.match(chat, /unansweredDeadChoice: unansweredDeadChoiceOnMessages,/);
|
||
assert.match(chat, /latestSettledAssistant\.question\.status === "superseded"/);
|
||
const entry = readFileSync(
|
||
new URL("../src/components/rectification-message-entry.tsx", import.meta.url),
|
||
"utf8",
|
||
);
|
||
assert.match(entry, /question\.status === "superseded" && !question\.answer_option/);
|
||
});
|
||
|
||
test("five dated events do not hold for yearless personality (BUG-913 closed_by_design)", () => {
|
||
assert.equal(yearlessPersonalityCanAsk({ datedCount: 5, candidatesSeparated: false }), false);
|
||
assert.equal(yearlessPersonalityCanAsk({ datedCount: 0, candidatesSeparated: false, topCandidateTimes: ["04:48", "04:59"] }), true);
|
||
});
|
||
|
||
test("nakshatra opposite poles are not both lifted as shared traits (BUG-914)", () => {
|
||
const earlier = "希望两边都能说得过去";
|
||
const later = "必要时会直接选边";
|
||
const delivery = buildRangeDelivery({
|
||
inference: persistedState(),
|
||
publicCandidates: [
|
||
{ candidateId: "a", time: "04:48" },
|
||
{ candidateId: "b", time: "04:53" },
|
||
{ candidateId: "c", time: "04:59" },
|
||
],
|
||
credibleRange: ["04:48", "04:59"],
|
||
representativeTime: "04:59",
|
||
nakshatraBoundary: {
|
||
near_boundary: true,
|
||
user_meaning: `A:${earlier}。B:${later}。`,
|
||
options: [
|
||
{ key: "A", time_bias: "earlier", traits: [earlier] },
|
||
{ key: "B", time_bias: "later", traits: [later] },
|
||
],
|
||
},
|
||
});
|
||
const shared = delivery.shared_traits.join(" ");
|
||
assert.equal(shared.includes(earlier) && shared.includes(later), false);
|
||
});
|