237 lines
12 KiB
TypeScript
237 lines
12 KiB
TypeScript
/**
|
||
* 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);
|
||
});
|