fix(web): keep consultation thinking above one spoken answer
Per-domain thinking trees were interleaved with sliced analysis, so a finished reply still looked like unfinished checklists. One collapsed thinking panel and one full body restores the reading order. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5387,4 +5387,20 @@
|
||||
- 复发自:BUG-309(validate 跳过 `next build`,publish 才暴露类型错误)
|
||||
- 修复版本:`707327ef`
|
||||
|
||||
## BUG-356 | 咨询思考与正文交错,完成步骤仍显示「还有 N 项」
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-08-23
|
||||
- 最近更新:2026-08-23
|
||||
- 影响面:咨询页 `ConsultationThinkingReport`、思考步骤树、回答 Markdown 列表、技法审计折叠
|
||||
- 用户现象:思考与「分析」看起来像同一段;领域步骤树插在正文中间和文末;已完成步骤仍显示空心圆「还有 N 项」;行运/适合推进等并列项是挤在一起的段落,技法表与「本轮技法」重复。
|
||||
- 触发条件:带 `thinking.section` 的本命咨询回答,尤其模型未按领域 H2 切开、或步骤超过 4 项时。
|
||||
- 根因:报告按每个 thinking section 交错渲染思考+切片正文。`visibleThinkingSteps` 把第 5 项起收成「还有 N 项」且固定空心圆。模型用「标签:解释」段落而不是列表。正文已贴审计表时仍再叠一层折叠表。
|
||||
- 修复:一轮只保留一个可折叠「思考」和一个完整「分析」。结算后未写出的步骤标为完成并展开全部步骤。并列「标签:解释」提升为 Markdown 列表。正文已有完整审计表时不再重复折叠副本。组答要求并列项用 bullet list。
|
||||
- 验证:`frontend/tests/consultation-thinking-plan.test.ts`、`frontend/tests/chat-definition-lists.test.ts`、`frontend/tests/chat-stream-layout.test.ts`、`frontend/tests/consultation-technique-audit.test.ts`
|
||||
- 防复发:咨询思考不得按领域把正文切片后反复插入步骤树。完成态不得用空心圆「还有 N 项」代替未展示步骤。正文已含完整技法表时不得再渲染折叠副本。
|
||||
- 相关记录:BUG-354
|
||||
- 复发自:BUG-354(按领域交错思考/分析,步骤树截断)
|
||||
- 修复版本:
|
||||
|
||||
|
||||
|
||||
@@ -859,22 +859,34 @@ button:disabled { cursor: default; opacity: .45; }
|
||||
}
|
||||
.consultation-thinking-report {
|
||||
display: grid;
|
||||
gap: var(--space-5);
|
||||
gap: var(--space-6);
|
||||
}
|
||||
.consultation-report-block {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
.consultation-step-tree__group + .consultation-step-tree__group {
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
.consultation-report-analysis__label {
|
||||
margin: 0 0 var(--space-2);
|
||||
color: var(--color-ink);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
.consultation-report-analysis {
|
||||
min-width: 0;
|
||||
}
|
||||
.consultation-report-analysis .message-answer {
|
||||
margin-top: 0;
|
||||
}
|
||||
.consultation-report-analysis .message-markdown {
|
||||
color: var(--color-ink-strong);
|
||||
}
|
||||
.message-markdown ul.markdown-list,
|
||||
.message-markdown ol.markdown-list {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
margin: var(--space-3) 0 var(--space-5);
|
||||
padding-inline-start: 1.35em;
|
||||
}
|
||||
.message-markdown .markdown-list > li {
|
||||
padding-block: 0;
|
||||
padding-inline-start: 6px;
|
||||
}
|
||||
.message-markdown .markdown-list > li + li {
|
||||
margin-top: 0;
|
||||
}
|
||||
.rectification-message-entry { min-width: 0; }
|
||||
.message-actions {
|
||||
display: flex;
|
||||
@@ -1947,7 +1959,11 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
|
||||
.conversation:not(.is-empty):not(.is-rectification) .message-markdown ul,
|
||||
.conversation:not(.is-empty):not(.is-rectification) .message-markdown ol {
|
||||
margin: 4px 0 var(--space-5);
|
||||
margin: var(--space-3) 0 var(--space-5);
|
||||
}
|
||||
|
||||
.conversation:not(.is-empty):not(.is-rectification) .message-markdown .markdown-list {
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.conversation:not(.is-empty):not(.is-rectification) .markdown-table {
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import ReactMarkdown, { type Components } from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
import { promoteDefinitionLists } from "@/lib/chat-definition-lists";
|
||||
|
||||
const markdownComponents: Components = {
|
||||
a: ({ children, href, ...props }) => (
|
||||
<a {...props} href={href} rel="noreferrer" target="_blank">
|
||||
@@ -25,7 +27,7 @@ export function renderChatMarkdown(text: string) {
|
||||
remarkPlugins={[remarkGfm]}
|
||||
skipHtml
|
||||
>
|
||||
{text}
|
||||
{promoteDefinitionLists(text)}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ export function ChatMessageContent({
|
||||
const split = splitSpokenAnswerAndTechniqueAudit(text);
|
||||
const rows = resolveTechniqueAuditRows(split, auditRows);
|
||||
const spoken = split.spoken;
|
||||
const showFoldedAudit = rows.length > 0 && split.rows.length === 0;
|
||||
|
||||
return (
|
||||
<div className="message-answer">
|
||||
@@ -67,7 +68,7 @@ export function ChatMessageContent({
|
||||
</div>
|
||||
) : null}
|
||||
{vargaSentence ? <p className="message-varga-sentence">{vargaSentence}</p> : null}
|
||||
{rows.length > 0 ? <TechniqueAuditDisclosure rows={rows} /> : null}
|
||||
{showFoldedAudit ? <TechniqueAuditDisclosure rows={rows} /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,25 +5,9 @@ import { ThinkingStepTree } from "@/components/thinking-step-tree";
|
||||
import type { TechniqueAuditRow } from "@/lib/consultation-agent-events";
|
||||
import {
|
||||
applyThinkingSectionProgress,
|
||||
REPORT_HEADING,
|
||||
splitAnswerByHeadings,
|
||||
type PublicThinkingSection,
|
||||
} from "@/lib/consultation-thinking-plan";
|
||||
|
||||
function analysisForSection(
|
||||
section: PublicThinkingSection,
|
||||
preamble: string,
|
||||
slices: Record<string, string>,
|
||||
isFirst: boolean,
|
||||
): string {
|
||||
const own = slices[section.heading] ?? "";
|
||||
const wrap = section.id === "close" ? (slices[REPORT_HEADING.wrap] ?? "") : "";
|
||||
const lead = isFirst || section.id === "foundation" || section.id === "answer" || section.id === "window"
|
||||
? preamble
|
||||
: "";
|
||||
return [lead, own, wrap].filter((part) => part.trim()).join("\n\n");
|
||||
}
|
||||
|
||||
export function ConsultationThinkingReport({
|
||||
sections,
|
||||
answer,
|
||||
@@ -43,44 +27,40 @@ export function ConsultationThinkingReport({
|
||||
auditRows?: readonly TechniqueAuditRow[];
|
||||
vargaSentence?: string | null;
|
||||
}>) {
|
||||
const progressed = applyThinkingSectionProgress(sections, answer);
|
||||
const headings = [
|
||||
...progressed.map((section) => section.heading),
|
||||
REPORT_HEADING.wrap,
|
||||
];
|
||||
const { preamble, slices } = splitAnswerByHeadings(answer, headings);
|
||||
const hasAnswer = Boolean(answer.trim());
|
||||
const progressed = applyThinkingSectionProgress(sections, answer, {
|
||||
settled: !live && hasAnswer,
|
||||
});
|
||||
const activeIndex = progressed.findIndex((section) => (
|
||||
section.steps.some((step) => step.status === "active")
|
||||
));
|
||||
|
||||
return (
|
||||
<div className="consultation-thinking-report">
|
||||
{progressed.map((section, index) => {
|
||||
const active = section.steps.some((step) => step.status === "active");
|
||||
const analysis = analysisForSection(section, preamble, slices, index === 0);
|
||||
const last = index === progressed.length - 1;
|
||||
return (
|
||||
<section className="consultation-report-block" key={section.id}>
|
||||
<ThinkingStepTree
|
||||
caption="思考"
|
||||
intent={section.title}
|
||||
steps={section.steps}
|
||||
live={live && active}
|
||||
liveLabel={live && active ? liveLabel : undefined}
|
||||
liveState={liveState}
|
||||
startedAt={live && active ? startedAt : undefined}
|
||||
defaultOpen={active || (live && index === 0 && !answer.trim())}
|
||||
/>
|
||||
{analysis.trim() ? (
|
||||
<div className="consultation-report-analysis">
|
||||
<h3 className="consultation-report-analysis__label">分析</h3>
|
||||
<ChatMessageContent
|
||||
text={analysis}
|
||||
auditRows={last ? auditRows : undefined}
|
||||
vargaSentence={last ? vargaSentence : null}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
<ThinkingStepTree
|
||||
caption="思考"
|
||||
groups={progressed.map((section, index) => ({
|
||||
id: section.id,
|
||||
intent: section.title,
|
||||
steps: section.steps,
|
||||
live: live && index === activeIndex,
|
||||
}))}
|
||||
revealAll
|
||||
live={live && !hasAnswer}
|
||||
liveLabel={live && !hasAnswer ? liveLabel : undefined}
|
||||
liveState={liveState}
|
||||
startedAt={live && !hasAnswer ? startedAt : undefined}
|
||||
defaultOpen={live && !hasAnswer}
|
||||
/>
|
||||
{hasAnswer ? (
|
||||
<section className="consultation-report-analysis" aria-label="分析">
|
||||
<ChatMessageContent
|
||||
text={answer}
|
||||
auditRows={auditRows}
|
||||
vargaSentence={vargaSentence}
|
||||
/>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,11 +59,68 @@ function StepMarker({
|
||||
return <span className="agent-thinking-marker is-pending" aria-hidden="true" />;
|
||||
}
|
||||
|
||||
type ThinkingStepGroup = Readonly<{
|
||||
id: string;
|
||||
intent?: string;
|
||||
steps: readonly PublicThinkingStep[];
|
||||
live?: boolean;
|
||||
}>;
|
||||
|
||||
function StepList({
|
||||
steps,
|
||||
revealAll,
|
||||
live,
|
||||
liveLabel,
|
||||
liveState,
|
||||
startedAt,
|
||||
}: Readonly<{
|
||||
steps: readonly PublicThinkingStep[];
|
||||
revealAll: boolean;
|
||||
live: boolean;
|
||||
liveLabel?: string;
|
||||
liveState?: OrbState;
|
||||
startedAt?: number;
|
||||
}>) {
|
||||
const visible = revealAll ? { visible: steps, hiddenCount: 0 } : visibleThinkingSteps(steps);
|
||||
const hiddenDone = visible.hiddenCount > 0
|
||||
&& steps.slice(visible.visible.length).every((step) => step.status === "done");
|
||||
return (
|
||||
<ol className="agent-thinking-timeline">
|
||||
{visible.visible.map((step) => (
|
||||
<li
|
||||
className={`agent-thinking-step is-${step.status}`}
|
||||
key={step.id}
|
||||
>
|
||||
<StepMarker status={step.status} liveState={liveState} />
|
||||
<span>{step.label}</span>
|
||||
</li>
|
||||
))}
|
||||
{visible.hiddenCount > 0 ? (
|
||||
<li className={`agent-thinking-step is-more${hiddenDone ? " is-done" : ""}`}>
|
||||
<StepMarker status={hiddenDone ? "done" : "pending"} />
|
||||
<span>还有 {visible.hiddenCount} 项</span>
|
||||
</li>
|
||||
) : null}
|
||||
{live && liveLabel ? (
|
||||
<li className="agent-thinking-step is-live">
|
||||
<StepMarker status="live" liveState={liveState} />
|
||||
<span className="agent-activity-status__live" role="status">
|
||||
<span key={liveLabel} className="agent-activity-status__text">{liveLabel}</span>
|
||||
{startedAt ? <ActivityElapsed key={startedAt} startedAt={startedAt} /> : null}
|
||||
</span>
|
||||
</li>
|
||||
) : null}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
|
||||
export function ThinkingStepTree({
|
||||
caption,
|
||||
intent,
|
||||
steps,
|
||||
steps = [],
|
||||
groups,
|
||||
hiddenCount,
|
||||
revealAll = false,
|
||||
live = false,
|
||||
liveLabel,
|
||||
liveState = "working",
|
||||
@@ -72,8 +129,10 @@ export function ThinkingStepTree({
|
||||
}: Readonly<{
|
||||
caption?: string;
|
||||
intent?: string;
|
||||
steps: readonly PublicThinkingStep[];
|
||||
steps?: readonly PublicThinkingStep[];
|
||||
groups?: readonly ThinkingStepGroup[];
|
||||
hiddenCount?: number;
|
||||
revealAll?: boolean;
|
||||
live?: boolean;
|
||||
liveLabel?: string;
|
||||
liveState?: OrbState;
|
||||
@@ -81,36 +140,28 @@ export function ThinkingStepTree({
|
||||
defaultOpen?: boolean;
|
||||
}>) {
|
||||
const [userOpen, setUserOpen] = useState<boolean | null>(null);
|
||||
const visible = hiddenCount === undefined ? visibleThinkingSteps(steps) : { visible: steps, hiddenCount };
|
||||
const groupItems = groups ?? [{
|
||||
id: "default",
|
||||
intent,
|
||||
steps,
|
||||
live,
|
||||
}];
|
||||
const showAll = revealAll || hiddenCount === 0;
|
||||
const body = (
|
||||
<>
|
||||
{intent ? <p className="consultation-step-tree__intent">{intent}</p> : null}
|
||||
<ol className="agent-thinking-timeline">
|
||||
{visible.visible.map((step) => (
|
||||
<li
|
||||
className={`agent-thinking-step is-${step.status}`}
|
||||
key={step.id}
|
||||
>
|
||||
<StepMarker status={step.status} liveState={liveState} />
|
||||
<span>{step.label}</span>
|
||||
</li>
|
||||
))}
|
||||
{visible.hiddenCount > 0 ? (
|
||||
<li className="agent-thinking-step is-more">
|
||||
<span className="agent-thinking-marker is-pending" aria-hidden="true" />
|
||||
<span>还有 {visible.hiddenCount} 项</span>
|
||||
</li>
|
||||
) : null}
|
||||
{live && liveLabel ? (
|
||||
<li className="agent-thinking-step is-live">
|
||||
<StepMarker status="live" liveState={liveState} />
|
||||
<span className="agent-activity-status__live" role="status">
|
||||
<span key={liveLabel} className="agent-activity-status__text">{liveLabel}</span>
|
||||
{startedAt ? <ActivityElapsed key={startedAt} startedAt={startedAt} /> : null}
|
||||
</span>
|
||||
</li>
|
||||
) : null}
|
||||
</ol>
|
||||
{groupItems.map((group) => (
|
||||
<div className="consultation-step-tree__group" key={group.id}>
|
||||
{group.intent ? <p className="consultation-step-tree__intent">{group.intent}</p> : null}
|
||||
<StepList
|
||||
steps={group.steps}
|
||||
revealAll={showAll}
|
||||
live={Boolean(group.live)}
|
||||
liveLabel={group.live ? liveLabel : undefined}
|
||||
liveState={liveState}
|
||||
startedAt={group.live ? startedAt : undefined}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
const FENCE = /(```[\s\S]*?```)/;
|
||||
const DEFINITION_LINE = /^(.{2,80}?)[::](.+)$/u;
|
||||
|
||||
function isSkippableLine(line: string): boolean {
|
||||
return /^(#{1,6}\s|>\s|[-*+]\s|\d+[.)]\s|\|)/.test(line) || line.startsWith("```");
|
||||
}
|
||||
|
||||
function isDefinitionLine(block: string): boolean {
|
||||
const trimmed = block.trim();
|
||||
if (!trimmed || trimmed.includes("\n") || isSkippableLine(trimmed)) return false;
|
||||
const match = DEFINITION_LINE.exec(trimmed);
|
||||
if (!match) return false;
|
||||
const body = match[2]?.trim() ?? "";
|
||||
return body.length >= 4;
|
||||
}
|
||||
|
||||
function toListItem(block: string): string {
|
||||
const trimmed = block.trim();
|
||||
const match = DEFINITION_LINE.exec(trimmed);
|
||||
if (!match) return trimmed;
|
||||
return `- **${match[1].trim()}**:${match[2].trim()}`;
|
||||
}
|
||||
|
||||
function promoteProse(text: string): string {
|
||||
const blocks = text.split(/\n{2,}/);
|
||||
const out: string[] = [];
|
||||
let index = 0;
|
||||
while (index < blocks.length) {
|
||||
if (isDefinitionLine(blocks[index] ?? "")) {
|
||||
let end = index;
|
||||
while (end < blocks.length && isDefinitionLine(blocks[end] ?? "")) end += 1;
|
||||
if (end - index >= 2) {
|
||||
out.push(blocks.slice(index, end).map((block) => toListItem(block)).join("\n"));
|
||||
index = end;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const lines = (blocks[index] ?? "").split("\n").map((line) => line.trim()).filter(Boolean);
|
||||
if (lines.length >= 2 && lines.every(isDefinitionLine)) {
|
||||
out.push(lines.map((line) => toListItem(line)).join("\n"));
|
||||
} else {
|
||||
out.push(blocks[index] ?? "");
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
return out.join("\n\n");
|
||||
}
|
||||
|
||||
export function promoteDefinitionLists(text: string): string {
|
||||
if (!text.includes(":") && !text.includes(":")) return text;
|
||||
return text.split(FENCE).map((chunk, index) => (
|
||||
index % 2 === 1 ? chunk : promoteProse(chunk)
|
||||
)).join("");
|
||||
}
|
||||
@@ -170,8 +170,15 @@ export function windowConsultationThinkingPlan(): PublicThinkingSection[] {
|
||||
export function applyThinkingSectionProgress(
|
||||
sections: readonly PublicThinkingSection[],
|
||||
answerText: string,
|
||||
options?: { settled?: boolean },
|
||||
): PublicThinkingSection[] {
|
||||
if (sections.length === 0) return [];
|
||||
if (options?.settled && answerText.trim()) {
|
||||
return sections.map((section) => ({
|
||||
...section,
|
||||
steps: section.steps.map((item) => ({ ...item, status: "done" as const })),
|
||||
}));
|
||||
}
|
||||
const present = new Set(
|
||||
[...answerText.matchAll(/^##\s+(.+?)\s*$/gm)].map((match) => match[1]?.trim() ?? ""),
|
||||
);
|
||||
@@ -234,6 +241,7 @@ export function consultationSpokenHeadingRule(kind: "natal" | "general" | "windo
|
||||
if (kind === "natal") {
|
||||
return [
|
||||
`Write the spoken answer with these exact Markdown H2 headings in order: ## ${REPORT_HEADING.foundation}, then ## {the Chinese label of each executed domain in tool order, such as 事业 / 财富 / 关系}, then ## ${REPORT_HEADING.audit}, then ## ${REPORT_HEADING.wrap}.`,
|
||||
"Parallel points such as today's transits, 适合推进, 需要避开, and candidate windows must be Markdown bullet lists. Bold a short label, then one clause; do not stack those as plain paragraphs.",
|
||||
activity,
|
||||
secrets,
|
||||
].join(" ");
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { promoteDefinitionLists } from "../src/lib/chat-definition-lists.ts";
|
||||
|
||||
test("consecutive label-colon paragraphs become a markdown list", () => {
|
||||
const promoted = promoteDefinitionLists([
|
||||
"行运焦点:",
|
||||
"",
|
||||
"月亮行运三分本命太阳(orb 0.29°):情绪与精力契合。",
|
||||
"",
|
||||
"天王星行运三分本命月亮(orb 0.75°):突发灵感可能冒出来。",
|
||||
"",
|
||||
"海王星行运对本命月亮(orb 0.87°):注意信息模糊。",
|
||||
].join("\n"));
|
||||
assert.match(promoted, /^- \*\*月亮行运三分本命太阳(orb 0\.29°)\*\*:情绪与精力契合。$/m);
|
||||
assert.match(promoted, /^- \*\*天王星行运三分本命月亮(orb 0\.75°)\*\*:突发灵感可能冒出来。$/m);
|
||||
assert.match(promoted, /行运焦点:/);
|
||||
assert.equal(promoted.includes("- **行运焦点**:"), false);
|
||||
});
|
||||
|
||||
test("code fences and existing lists are left alone", () => {
|
||||
const source = [
|
||||
"```",
|
||||
"标签:不要动",
|
||||
"```",
|
||||
"",
|
||||
"- 已有列表:保持原样",
|
||||
].join("\n");
|
||||
assert.equal(promoteDefinitionLists(source), source);
|
||||
});
|
||||
@@ -94,7 +94,11 @@ test("shows honest agent activity states before and during streamed text", () =>
|
||||
assert.match(pageSource, /activeStreamingSections/);
|
||||
assert.match(messageRowSource, /ConsultationThinkingReport/);
|
||||
assert.match(reportSource, /caption="思考"/);
|
||||
assert.match(reportSource, /分析/);
|
||||
assert.match(reportSource, /aria-label="分析"/);
|
||||
assert.match(reportSource, /revealAll/);
|
||||
assert.match(reportSource, /groups=\{progressed\.map/);
|
||||
assert.doesNotMatch(reportSource, /analysisForSection|splitAnswerByHeadings/);
|
||||
assert.match(globalStyles, /\.markdown-list/);
|
||||
assert.match(activitySource, /思考过程/);
|
||||
assert.match(pageSource, /event\.type === "run\.failed"/);
|
||||
assert.match(pageSource, /event\.code === "answer_truncated"/);
|
||||
|
||||
@@ -104,6 +104,7 @@ test("the folded audit control stays closed and never remounts the internal pane
|
||||
assert.doesNotMatch(disclosureSource, /EvidenceAuditPanel/);
|
||||
assert.doesNotMatch(disclosureSource, /techniqueTruth|workflowReceipt|rawReceipt/);
|
||||
assert.match(contentSource, /splitSpokenAnswerAndTechniqueAudit/);
|
||||
assert.match(contentSource, /showFoldedAudit = rows\.length > 0 && split\.rows\.length === 0/);
|
||||
assert.match(contentSource, /TechniqueAuditDisclosure/);
|
||||
assert.match(toolsSource, /ctx\.state\.techniqueAuditTable = normalizeTechniqueAuditRows/);
|
||||
assert.match(routeSource, /techniqueAuditTable: state\.techniqueAuditTable/);
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
applyThinkingSectionProgress,
|
||||
consultationContinuePrompt,
|
||||
consultationReportHeadings,
|
||||
consultationSpokenHeadingRule,
|
||||
natalConsultationThinkingPlan,
|
||||
REPORT_HEADING,
|
||||
splitAnswerByHeadings,
|
||||
@@ -78,6 +79,16 @@ test("answer split follows the required heading order", () => {
|
||||
assert.match(slices[REPORT_HEADING.wrap] ?? "", /合同条款/);
|
||||
});
|
||||
|
||||
test("settled answers mark leftover thinking sections done", () => {
|
||||
const sections = natalConsultationThinkingPlan({ domains: ["career"] });
|
||||
const progressed = applyThinkingSectionProgress(
|
||||
sections,
|
||||
"今日行运已经写完,但没有领域二级标题。",
|
||||
{ settled: true },
|
||||
);
|
||||
assert.equal(progressed.every((section) => section.steps.every((step) => step.status === "done")), true);
|
||||
});
|
||||
|
||||
test("progress marks the current heading active and completed ones done", () => {
|
||||
const sections = natalConsultationThinkingPlan({ domains: ["career"] });
|
||||
const progressed = applyThinkingSectionProgress(sections, "## 统一参数与原始结构\n岁差。\n");
|
||||
@@ -91,3 +102,7 @@ test("continue prompt asks to resume after the last complete heading", () => {
|
||||
assert.match(prompt, /不要重复已写出的段落/);
|
||||
assert.match(prompt, /不要写思考过程清单/);
|
||||
});
|
||||
|
||||
test("natal spoken answers must use markdown lists for parallel points", () => {
|
||||
assert.match(consultationSpokenHeadingRule("natal"), /Markdown bullet lists/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user