Files
Jyotisha/frontend/tests/rectification-nearest-boundary-opening-20261012.test.ts
T

237 lines
12 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.
/**
* BUG-1307: the opening says how close the recorded minute is to the nearest
* chart change. Question order stays off unless the env flag is "on".
*/
import assert from "node:assert/strict";
import test from "node:test";
import { buildOpeningBrief } from "../src/lib/rectification-agentic/v9/agent-run-messages.ts";
import { orderDiscriminatorsForNearestBoundary } from "../src/lib/rectification-agentic/v9/method-followup.ts";
import { parseV9CaseDossier } from "../src/lib/rectification-agentic/v9/tool-service.ts";
import { fetchOpeningBoundary, openingFactsFromBoundaryPayload, resolveOpeningBoundaryFacts } from "../src/lib/rectification-agentic/opening-boundary.ts";
import { scanCaseSegments } from "../src/lib/rectification-agentic/v9/segment-scan.ts";
import {
nearestBoundaryFromTransitions,
nearestBoundaryProbesEnabled,
orderByBoundaryDomains,
topicsForLayers,
} from "../src/lib/rectification-agentic/nearest-boundary.ts";
import {
OPENING_BODY_JARGON,
isAcceptableOpeningBody,
openingBoundarySentence,
openingSpokenBody,
} from "../src/lib/rectification-agentic/user-copy.ts";
import { dossierFixture } from "./rectification-v9-test-support.ts";
const RANGE = ["04:45", "05:15"] as const;
const PLAIN = "我们来把你的出生时间缩小到更准的范围,现在先在 04:45–05:15 之间找。做法很简单:你说几件人生里的大事和大概年月,我拿去和星盘对照。";
const BANNED = ["秒", "记录时间错了", "记录时间不准", "已确认", ...OPENING_BODY_JARGON];
function assertPlain(text: string): void {
for (const word of BANNED) assert.equal(text.includes(word), false, word);
assert.doesNotMatch(text, /[??]/);
assert.ok(text.split(/(?<=。)/).filter(Boolean).length <= 3, text);
}
test("default opening stays two sentences", () => {
assert.equal(openingSpokenBody(RANGE), PLAIN);
assert.equal(openingSpokenBody(null), "我们来把你的出生时间缩小到更准的范围。做法很简单:你说几件人生里的大事和大概年月,我拿去和星盘对照。");
assert.equal(openingBoundarySentence({ kind: "period_only" }), null);
assert.equal(openingBoundarySentence({ kind: "unavailable" }), null);
assert.equal(openingSpokenBody(RANGE, null, { kind: "period_only" }), PLAIN);
assert.equal(isAcceptableOpeningBody(PLAIN, RANGE), true);
});
test("near, under a minute, stable, and only the birth chart", () => {
const near = openingSpokenBody(RANGE, null, { kind: "near", distanceMinutes: 4, topics: ["父母", "工作", "<script>"] });
assert.match(near, /差 4 分钟,两边不一样的是父母、工作。/);
assert.doesNotMatch(near, /接下来先问这几件|script/);
const close = openingSpokenBody(RANGE, null, { kind: "near", distanceMinutes: 0, topics: ["婚恋"], askFirst: true });
assert.match(close, /不到 1 分钟,两边不一样的是婚恋,接下来先问这几件。/);
const stable = openingBoundarySentence({ kind: "stable" });
assert.equal(stable, "你填的时间附近这些盘都不换,先从经历开始。");
const onlyBirth = openingSpokenBody(RANGE, null, { kind: "near", distanceMinutes: 2, topics: [] });
assert.match(onlyBirth, /差 2 分钟。/);
assert.doesNotMatch(onlyBirth, /两边不一样/);
for (const text of [near, close, openingSpokenBody(RANGE, null, { kind: "stable" }), onlyBirth]) {
assertPlain(text);
assert.equal(isAcceptableOpeningBody(text, RANGE), true);
assert.equal(isAcceptableOpeningBody(PLAIN, RANGE, null, { kind: "stable" }), false);
}
assert.equal(isAcceptableOpeningBody(`${near}再补一句。`, RANGE), false);
});
test("d1 change is named in the same sentence, and no d1 change keeps today's sentence", () => {
const changed = openingFactsFromBoundaryPayload({
found: true,
distance_minutes: 4,
layers: [{ layer: "d1" }, { layer: "d12" }, { layer: "d10" }],
}, false);
assert.equal(changed.d1Changed, true);
assert.deepEqual(changed.topics, ["工作", "父母"]);
assert.equal(openingBoundarySentence(changed), "你填的时间离最近会换的地方差 4 分钟,这里连本命上升都换,两边不一样的是工作、父母。");
const onlyBirth = openingFactsFromBoundaryPayload({
found: true,
distance_minutes: 0,
layers: [{ layer: "d1" }],
}, true);
assert.equal(onlyBirth.d1Changed, true);
assert.deepEqual(onlyBirth.topics, []);
assert.equal(onlyBirth.askFirst, false);
assert.equal(openingBoundarySentence(onlyBirth), "你填的时间离最近会换的地方不到 1 分钟,这里连本命上升都换。");
assert.equal(
openingBoundarySentence({ kind: "near", distanceMinutes: 2, topics: [], d1Changed: true, askFirst: true }),
"你填的时间离最近会换的地方差 2 分钟,这里连本命上升都换。",
);
const asked = openingBoundarySentence({ kind: "near", distanceMinutes: 2, topics: ["婚恋"], d1Changed: true, askFirst: true });
assert.equal(asked, "你填的时间离最近会换的地方差 2 分钟,这里连本命上升都换,两边不一样的是婚恋,接下来先问这几件。");
const unchanged = openingFactsFromBoundaryPayload({
found: true,
distance_minutes: 4,
layers: [{ layer: "d12" }, { layer: "d10" }],
}, false);
assert.equal(unchanged.d1Changed, false);
assert.equal(openingBoundarySentence(unchanged), "你填的时间离最近会换的地方差 4 分钟,两边不一样的是工作、父母。");
assert.equal(
openingBoundarySentence({ kind: "near", distanceMinutes: 0, topics: ["婚恋"] }),
"你填的时间离最近会换的地方不到 1 分钟,两边不一样的是婚恋。",
);
assert.equal(openingBoundarySentence({ kind: "stable" }), "你填的时间附近这些盘都不换,先从经历开始。");
assert.equal(openingBoundarySentence({ kind: "period_only" }), null);
assert.equal(openingBoundarySentence({ kind: "unavailable" }), null);
const askedFacts = { kind: "near" as const, distanceMinutes: 2, topics: ["婚恋"], d1Changed: true, askFirst: true };
for (const facts of [changed, onlyBirth, askedFacts, unchanged]) {
const text = openingSpokenBody(RANGE, null, facts);
assertPlain(text);
assert.equal(isAcceptableOpeningBody(text, RANGE, null, facts), true);
assert.equal(text.includes("D1"), false);
assert.equal(text.split(/(?<=。)/).filter(Boolean).length, 3);
}
assert.match(openingSpokenBody(RANGE, null, changed), /这里连本命上升都换,两边不一样的是工作、父母。/);
});
test("topics come from the layer allowlist, not the payload string", () => {
assert.deepEqual(topicsForLayers(["d12", "d10", "d1", "kp1", "nope"]), ["工作", "父母"]);
const facts = openingFactsFromBoundaryPayload({
found: true,
distance_minutes: 0,
at: "19:15",
topics: ["已确认秒"],
layers: [
{ layer: "d12", topic: "记录时间错了" },
{ layer: "kp1", topic: "工作" },
],
}, false);
assert.equal(facts.kind, "near");
assert.deepEqual(facts.topics, ["父母"]);
assert.equal(facts.askFirst, false);
assert.equal(openingFactsFromBoundaryPayload({ found: false, reason: "no_transition" }, true).kind, "stable");
});
test("question order is a switch and defaults off", () => {
assert.equal(nearestBoundaryProbesEnabled(undefined), false);
assert.equal(nearestBoundaryProbesEnabled("on"), true);
const rows = [
{ id: "career", eventProbe: { domain: "career" }, contrastProbe: null },
{ id: "family", eventProbe: { domain: "family" }, contrastProbe: null },
{ id: "money", eventProbe: null, contrastProbe: { domain: "finance" } },
];
const scan = { transitions: [
{ layer: "d12", at: "19:16", from_sign: "白羊座", to_sign: "金牛座" },
{ layer: "kp1", at: "19:15", from_sign: "白羊座", to_sign: "金牛座" },
] };
const boundary = nearestBoundaryFromTransitions(scan.transitions, "19:15");
assert.equal(boundary.distanceMinutes, 1);
assert.deepEqual(boundary.domains, ["family"]);
const input = { windowScan: scan, reportedTime: "19:15", birthTimeSource: "approximate" };
assert.equal(orderDiscriminatorsForNearestBoundary(rows, input), rows);
const asked = orderDiscriminatorsForNearestBoundary(rows, { ...input, probeFlag: "on" });
assert.deepEqual(asked.map((row) => row.id), ["family", "career", "money"]);
assert.equal(orderDiscriminatorsForNearestBoundary(rows, { ...input, probeFlag: "on", birthTimeSource: "period_only" }), rows);
assert.equal(orderDiscriminatorsForNearestBoundary(rows, {
...input,
probeFlag: "on",
windowScan: { transitions: [{ layer: "d12", at: "19:30", from_sign: "白羊座", to_sign: "金牛座" }] },
}), rows);
const same = orderByBoundaryDomains(rows, ["family"], (row) => row.eventProbe?.domain ?? row.contrastProbe?.domain ?? "");
assert.equal(same[0]?.id, "family");
});
test("period_only and a missing clock do not scan", async () => {
let called = 0;
const loadCompute = async () => {
called += 1;
throw new Error("should not scan");
};
assert.equal((await resolveOpeningBoundaryFacts({
birthTimeSource: "period_only",
reportedBirthTime: "19:15",
candidateRange: { start_time: "19:00", end_time: "19:30" },
loadCompute,
})).kind, "period_only");
assert.equal((await resolveOpeningBoundaryFacts({
birthTimeSource: null,
reportedBirthTime: null,
candidateRange: null,
loadCompute,
})).kind, "unavailable");
assert.equal(called, 0);
const dossier = parseV9CaseDossier(dossierFixture());
assert.ok(dossier);
const brief = buildOpeningBrief(dossier);
assert.match(brief, /正文两句大白话/);
assert.doesNotMatch(brief, /服务端另有一句/);
const withSentence = buildOpeningBrief(dossier, null, "你填的时间附近这些盘都不换,先从经历开始。");
assert.match(withSentence, /原样放在这两句之间/);
assert.match(brief, /正文不用大运/);
});
test("opening and segment scans send the snapshot ayanamsa", async (t) => {
const bodies: Array<Record<string, unknown>> = [];
t.mock.method(globalThis, "fetch", async (_endpoint: unknown, init?: RequestInit) => {
const body = JSON.parse(String(init?.body)) as {
start_at: string;
end_at: string;
ayanamsa?: string;
boundary_charts?: unknown;
};
bodies.push(body);
const start = Date.parse(`${body.start_at}:00Z`);
const end = Date.parse(`${body.end_at}:00Z`);
const width = (end - start) / 60000 + 1;
const minutes = Array.from({ length: width }, (_, offset) => {
const stamp = new Date(start + offset * 60000).toISOString().slice(0, 16);
return { offset, date: stamp.slice(0, 10), time: stamp.slice(11), signs: { D1: 1, D9: 2, D10: 3 } };
});
return Response.json({
start_at: body.start_at,
end_at: body.end_at,
ayanamsa: body.ayanamsa,
node_mode: "mean",
step_minutes: 1,
window_minutes: width,
minutes,
nearest_boundary: { found: false, reason: "no_transition" },
});
});
const base = {
birth_date: "2000-06-14",
latitude: 40,
longitude: -74,
timezone_offset: -4,
reported_birth_time: "09:10",
};
const range = { start_time: "09:00", end_time: "09:06" };
await fetchOpeningBoundary({ snapshot: { ...base, ayanamsa: "lahiri" }, range, recordedTime: "09:10" });
await scanCaseSegments({ snapshot: { ...base, ayanamsa: "lahiri" }, range, targets: ["D1", "D9", "D10"] });
await fetchOpeningBoundary({ snapshot: base, range, recordedTime: "09:10" });
await scanCaseSegments({ snapshot: base, range, targets: ["D1", "D9", "D10"] });
assert.equal(bodies.length, 4);
assert.deepEqual(bodies.map((body) => body.ayanamsa), ["lahiri", "lahiri", "raman", "raman"]);
assert.ok(Array.isArray(bodies[0]?.boundary_charts));
assert.equal(bodies[1]?.boundary_charts, undefined);
assert.equal(bodies[0]?.node_mode, undefined);
assert.equal(bodies[1]?.node_mode, undefined);
});