fix(rectification): show natal houses and lagna changes in a live right rail
The board was live data sitting under chat, so each new event looked like nothing changed. Keep candidate cards in the transcript and group 换升 by minute for the side panel. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -24,6 +24,14 @@ const agent = readFileSync(
|
||||
"utf8",
|
||||
);
|
||||
const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
|
||||
const board = readFileSync(
|
||||
new URL("../src/components/rectification-board.tsx", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const houseTable = readFileSync(
|
||||
new URL("../src/components/rectification-house-table.tsx", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const completedActivityReceipt = readFileSync(
|
||||
new URL("../src/components/completed-activity-receipt.tsx", import.meta.url),
|
||||
"utf8",
|
||||
@@ -224,7 +232,7 @@ test("rectification uses one case-level entitlement and the session-pinned model
|
||||
|
||||
test("Agentic rectification scrolls the conversation container as streamed messages grow", () => {
|
||||
assert.match(chat, /const conversation = useRef<HTMLElement>\(null\)/);
|
||||
assert.match(chat, /<section ref=\{conversation\} className="conversation"/);
|
||||
assert.match(chat, /<section ref=\{conversation\} className="conversation is-rectification"/);
|
||||
assert.match(chat, /const container = conversation\.current/);
|
||||
assert.match(chat, /top: container\.scrollHeight/);
|
||||
assert.match(chat, /\}, \[busy, candidateResult, error, messages, savedTime\]\);/);
|
||||
@@ -245,7 +253,7 @@ test("rectification keeps receipts for the varga sentence and hides Activity fro
|
||||
assert.match(chat, /showActivity=\{displayedMessage\.state === "thinking"\}/);
|
||||
assert.match(chat, /vargaSentenceFromMethods/);
|
||||
assert.match(chat, /vargaSentence=\{vargaSentence\}/);
|
||||
assert.match(chat, /RectificationHouseTableView/);
|
||||
assert.match(board, /RectificationHouseTableView/);
|
||||
assert.doesNotMatch(chat, /activeActivity/);
|
||||
assert.doesNotMatch(chat, /正在读取校正记录/);
|
||||
assert.doesNotMatch(chat, /<CompletedActivityReceipt/);
|
||||
@@ -259,9 +267,8 @@ test("rectification keeps receipts for the varga sentence and hides Activity fro
|
||||
chat.indexOf("{savedTime &&"),
|
||||
);
|
||||
const replyIndex = messageRender.indexOf("<ChatMessageRow");
|
||||
const houseIndex = messageRender.indexOf("<RectificationHouseTableView");
|
||||
assert.ok(replyIndex >= 0);
|
||||
assert.ok(houseIndex > replyIndex);
|
||||
assert.doesNotMatch(messageRender, /<RectificationHouseTableView/);
|
||||
assert.match(completedActivityReceipt, /<details className=/);
|
||||
assert.doesNotMatch(completedActivityReceipt, /<details[^>]*\sopen/);
|
||||
assert.match(completedActivityReceipt, /本轮完成 · \$\{stepLabels\.length\} 个步骤 · \$\{receipt\.methods\.length\} 项计算依据/);
|
||||
@@ -314,34 +321,33 @@ test("candidate state renders from the snapshot API and never from sentinels", (
|
||||
assert.match(chat, /isRecommendedRectificationCandidate/);
|
||||
assert.match(chat, /已采用/);
|
||||
assert.match(chat, /正在采用…/);
|
||||
assert.match(chat, /RectificationHouseTableView/);
|
||||
assert.match(chat, /className="rectification-snapshot"/);
|
||||
assert.match(chat, /RectificationBoard/);
|
||||
assert.match(board, /className="rectification-snapshot"/);
|
||||
assert.match(styles, /\.rectification-snapshot \{/);
|
||||
assert.match(styles, /\.rectification-house-table \{[^}]*border:\s*1px solid var\(--color-border\)/);
|
||||
assert.match(styles, /\.rectification-house-table \{[^}]*background:\s*var\(--color-canvas-soft\)/);
|
||||
assert.match(styles, /\.message-list \{[\s\S]*--assistant-content-inset: calc\(32px \+ var\(--space-3\)\)/);
|
||||
assert.match(styles, /\.rectification-message-wrap \.rectification-candidates \{[\s\S]*width: calc\(100% - var\(--assistant-content-inset\)\)/);
|
||||
assert.match(styles, /\.rectification-message-wrap \.rectification-candidates \{[\s\S]*margin-inline-start: var\(--assistant-content-inset\)/);
|
||||
assert.match(styles, /\.rectification-snapshot \{[\s\S]*width: calc\(100% - var\(--assistant-content-inset\)\)/);
|
||||
assert.match(styles, /\.rectification-snapshot \{[\s\S]*margin-inline-start: var\(--assistant-content-inset\)/);
|
||||
assert.match(styles, /\.rectification-workspace \{[\s\S]*grid-template-columns: minmax\(0, 1fr\) minmax\(18rem, 22\.5rem\)/);
|
||||
assert.match(styles, /\.rectification-workspace\.is-compact \{[\s\S]*grid-template-columns: minmax\(0, 1fr\)/);
|
||||
assert.doesNotMatch(styles, /\.rectification-snapshot \{[^}]*--assistant-content-inset/);
|
||||
assert.doesNotMatch(styles, /\.rectification-house-table \{[^}]*padding-inline-start:\s*var\(--assistant-content-inset\)/);
|
||||
assert.doesNotMatch(styles, /\.rectification-house-table \{[^}]*--assistant-content-inset/);
|
||||
assert.doesNotMatch(page, /rectification-workspace/);
|
||||
assert.match(page, /chat-panel\$\{rectificationSurfaceOpen \? " is-rectification" : ""\}/);
|
||||
});
|
||||
|
||||
|
||||
test("the natal house table is a live snapshot card outside the agent bubble", () => {
|
||||
const houseTable = readFileSync(
|
||||
new URL("../src/components/rectification-house-table.tsx", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
test("the natal house table is a live board beside the chat, not a message", () => {
|
||||
const messageLoop = chat.slice(
|
||||
chat.indexOf("{messages.map((message) => {"),
|
||||
chat.indexOf("{candidateResult?.houseTable &&"),
|
||||
);
|
||||
const snapshot = chat.slice(
|
||||
chat.indexOf("{candidateResult?.houseTable &&"),
|
||||
chat.indexOf("{savedTime &&"),
|
||||
);
|
||||
const snapshot = board.slice(
|
||||
board.indexOf("className=\"rectification-snapshot\""),
|
||||
board.indexOf("{scoringMinutes.length > 0 || displayMinutes.length > 0"),
|
||||
);
|
||||
const rowIndex = messageLoop.indexOf("<ChatMessageRow");
|
||||
const snapshotIndex = snapshot.indexOf("className=\"rectification-snapshot\"");
|
||||
const houseIndex = snapshot.indexOf("<RectificationHouseTableView");
|
||||
@@ -349,8 +355,15 @@ test("the natal house table is a live snapshot card outside the agent bubble", (
|
||||
assert.ok(snapshotIndex >= 0 && houseIndex > snapshotIndex);
|
||||
assert.doesNotMatch(snapshot, /<RectificationCandidateCards/);
|
||||
assert.doesNotMatch(snapshot, /当前可能的出生时间/);
|
||||
assert.doesNotMatch(messageLoop, /className="rectification-snapshot"/);
|
||||
assert.doesNotMatch(messageLoop, /<RectificationHouseTableView/);
|
||||
assert.match(chat, /className="conversation is-rectification"/);
|
||||
assert.match(chat, /<RectificationBoard/);
|
||||
assert.match(chat, /await loadCandidate\(\)\.then\(\(result\) => \{/);
|
||||
assert.match(houseTable, /aria-live="polite"/);
|
||||
assert.match(board, /aria-live="polite"/);
|
||||
assert.match(board, /groupWindowTransitions/);
|
||||
assert.match(board, /<dialog/);
|
||||
assert.match(chat, /RectificationBoardPeek/);
|
||||
assert.match(houseTable, /当前本命宫位/);
|
||||
assert.match(houseTable, /补充经历后会按新线索重算/);
|
||||
assert.doesNotMatch(houseTable, /<ChatMessageRow/);
|
||||
@@ -359,7 +372,7 @@ test("the natal house table is a live snapshot card outside the agent bubble", (
|
||||
test("time-selection cards appear under the latest settled agent bubble only after offer-candidates", () => {
|
||||
const messageLoop = chat.slice(
|
||||
chat.indexOf("{messages.map((message) => {"),
|
||||
chat.indexOf("{candidateResult?.houseTable &&"),
|
||||
chat.indexOf("{savedTime &&"),
|
||||
);
|
||||
const actionsIndex = messageLoop.indexOf("<ChatMessageActions");
|
||||
const cardsIndex = messageLoop.indexOf("<RectificationCandidateCards");
|
||||
@@ -437,8 +450,8 @@ test("the Agent prompt cannot offer candidates while asking for more evidence",
|
||||
test("adopted time offers a consultation handoff without unique-minute copy", () => {
|
||||
assert.match(chat, /用这个时间看盘/);
|
||||
assert.match(chat, /onStartConsultation/);
|
||||
assert.match(chat, /换升时刻/);
|
||||
assert.match(chat, /经历与大运对照/);
|
||||
assert.match(board, /换升时刻/);
|
||||
assert.match(board, /经历与大运对照/);
|
||||
assert.match(chat, /盘外对照/);
|
||||
assert.match(page, /startConsultationAfterRectification/);
|
||||
assert.match(page, /createSession\(modelCatalog\.defaultModelId\)/);
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
changedHouseNumbers,
|
||||
diffRectificationBoard,
|
||||
groupWindowTransitions,
|
||||
rectificationBoardPeekCopy,
|
||||
} from "../src/lib/rectification-board-model.ts";
|
||||
import { parseRectificationCandidateResult } from "../src/lib/rectification-candidate-result.ts";
|
||||
import type { WindowScanTransition } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
|
||||
|
||||
const CANDIDATE_ID = "88888888-8888-4888-8888-888888888881";
|
||||
const SECOND_CANDIDATE_ID = "88888888-8888-4888-8888-888888888882";
|
||||
const THIRD_CANDIDATE_ID = "88888888-8888-4888-8888-888888888883";
|
||||
|
||||
function transition(layer: WindowScanTransition["layer"], at: string): WindowScanTransition {
|
||||
return { layer, at, user_meaning: `${layer} 在 ${at} 发生变化` };
|
||||
}
|
||||
|
||||
function houses(lagna: string, seventh = lagna) {
|
||||
return Array.from({ length: 12 }, (_, index) => ({
|
||||
house: index + 1,
|
||||
sign: index === 6 ? seventh : lagna,
|
||||
occupants: index === 0 ? ["太阳"] : [],
|
||||
}));
|
||||
}
|
||||
|
||||
function snapshot(input: Readonly<{
|
||||
time: string;
|
||||
lagna: string;
|
||||
seventh?: string;
|
||||
transitions?: readonly WindowScanTransition[];
|
||||
}>) {
|
||||
return parseRectificationCandidateResult({
|
||||
resultId: "11111111-1111-4111-8111-111111111111",
|
||||
candidates: [
|
||||
{ candidateId: CANDIDATE_ID, rank: 1, time: input.time, relativeSupport: 34, tiedMinuteCount: 1 },
|
||||
{ candidateId: SECOND_CANDIDATE_ID, rank: 2, time: "05:08", relativeSupport: 33, tiedMinuteCount: 1 },
|
||||
{ candidateId: THIRD_CANDIDATE_ID, rank: 3, time: "05:09", relativeSupport: 33, tiedMinuteCount: 1 },
|
||||
],
|
||||
overallConfidence: "low",
|
||||
selectionAllowed: true,
|
||||
confirmationAllowed: false,
|
||||
representativeTime: input.time,
|
||||
selectedTime: null,
|
||||
selectionKind: null,
|
||||
decisionReceipt: {
|
||||
house_table: {
|
||||
time: input.time,
|
||||
lagna: input.lagna,
|
||||
houses: houses(input.lagna, input.seventh ?? input.lagna),
|
||||
},
|
||||
window_scan: {
|
||||
scanned: true,
|
||||
d9_lagna_count: 2,
|
||||
d10_lagna_count: 1,
|
||||
transitions: input.transitions ?? [],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
test("groups 换升时刻 by clock minute and splits scoring layers from display layers", () => {
|
||||
const grouped = groupWindowTransitions([
|
||||
transition("d11", "04:47"),
|
||||
transition("hora", "04:47"),
|
||||
transition("d24", "04:47"),
|
||||
transition("d10", "04:48"),
|
||||
transition("pranapada", "05:14"),
|
||||
transition("d9", "04:57"),
|
||||
]);
|
||||
|
||||
assert.deepEqual(grouped.map((row) => row.at), ["04:47", "04:48", "04:57", "05:14"]);
|
||||
assert.deepEqual(grouped[0], {
|
||||
at: "04:47",
|
||||
scoringLayers: ["d24", "d11"],
|
||||
displayLayers: ["hora"],
|
||||
});
|
||||
assert.deepEqual(grouped[1]?.scoringLayers, ["d10"]);
|
||||
assert.deepEqual(grouped[3], {
|
||||
at: "05:14",
|
||||
scoringLayers: [],
|
||||
displayLayers: ["pranapada"],
|
||||
});
|
||||
});
|
||||
|
||||
test("marks only houses whose sign or occupants actually changed", () => {
|
||||
const before = snapshot({ time: "04:45", lagna: "金牛座" })?.houseTable ?? null;
|
||||
const after = snapshot({ time: "04:48", lagna: "金牛座", seventh: "天秤座" })?.houseTable ?? null;
|
||||
assert.deepEqual(changedHouseNumbers(before, after), [7]);
|
||||
assert.deepEqual(changedHouseNumbers(null, after), []);
|
||||
});
|
||||
|
||||
test("announces the first board fill and later house or 换升 changes without dumping the table", () => {
|
||||
const first = snapshot({
|
||||
time: "04:45",
|
||||
lagna: "金牛座",
|
||||
transitions: [transition("d9", "04:57")],
|
||||
});
|
||||
const second = snapshot({
|
||||
time: "04:48",
|
||||
lagna: "金牛座",
|
||||
seventh: "天秤座",
|
||||
transitions: [transition("d9", "04:57"), transition("d10", "04:48")],
|
||||
});
|
||||
assert.ok(first && second);
|
||||
|
||||
const opened = diffRectificationBoard(null, first);
|
||||
assert.equal(opened.liveMessage, "当前本命宫位已按 04:45 排出,上升金牛座。");
|
||||
assert.deepEqual(opened.houseNumbers, []);
|
||||
assert.equal(opened.timeChanged, false);
|
||||
|
||||
const updated = diffRectificationBoard(first, second);
|
||||
assert.equal(updated.timeChanged, true);
|
||||
assert.deepEqual(updated.houseNumbers, [7]);
|
||||
assert.deepEqual(updated.transitionMinutes, ["04:48"]);
|
||||
assert.match(updated.liveMessage, /本命宫位已按 04:48 重算(上升 金牛座)。/);
|
||||
assert.match(updated.liveMessage, /第7宫已更新/);
|
||||
assert.match(updated.liveMessage, /换升时刻已更新/);
|
||||
assert.doesNotMatch(updated.liveMessage, /太阳|第1宫|第2宫|下面是本轮/);
|
||||
});
|
||||
|
||||
test("peek copy stays a control label, not a second dump of the house table", () => {
|
||||
const empty = rectificationBoardPeekCopy(null);
|
||||
assert.equal(empty.title, "当前盘面");
|
||||
assert.equal(empty.detail, "补充经历后会在这里更新");
|
||||
|
||||
const filled = rectificationBoardPeekCopy(snapshot({ time: "04:48", lagna: "金牛座" }));
|
||||
assert.equal(filled.title, "当前盘面");
|
||||
assert.equal(filled.detail, "04:48 · 上升金牛座");
|
||||
});
|
||||
Reference in New Issue
Block a user