feat(confidence-engine): clarify understanding during Done refresh
This commit is contained in:
@@ -24,6 +24,50 @@
|
|||||||
animation-delay: 0.16s;
|
animation-delay: 0.16s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── CU skeleton overlay during synthesis refresh ─────────────── */
|
||||||
|
|
||||||
|
.cu-skeleton-overlay {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cu-skeleton-lines {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cu-skeleton-line {
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #e5e7eb;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Striped shimmer that travels left → right through each bar */
|
||||||
|
.cu-skeleton-line::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
105deg,
|
||||||
|
transparent 0%,
|
||||||
|
transparent 8px,
|
||||||
|
rgba(255, 255, 255, 0.45) 8px,
|
||||||
|
rgba(255, 255, 255, 0.45) 16px,
|
||||||
|
transparent 16px,
|
||||||
|
transparent 24px
|
||||||
|
);
|
||||||
|
animation: cuSkeletonShimmer 1.6s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cuSkeletonShimmer {
|
||||||
|
0% { transform: translateX(-100%); }
|
||||||
|
100% { transform: translateX(100%); }
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
[style*="animation:spin"] {
|
[style*="animation:spin"] {
|
||||||
animation: none !important;
|
animation: none !important;
|
||||||
@@ -32,4 +76,8 @@
|
|||||||
.investigation-card {
|
.investigation-card {
|
||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cu-skeleton-line::after {
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -394,7 +394,7 @@ function FocusedQuestionBody({
|
|||||||
|
|
||||||
// ── Persistent navigation controls (overlay-level, outside content grid) ──
|
// ── Persistent navigation controls (overlay-level, outside content grid) ──
|
||||||
|
|
||||||
function FocusedWorkspaceNavigation({ nodeId, doneForNow, isDoneForNowActive, isProcessing }) {
|
function FocusedWorkspaceNavigation({ nodeId, doneForNow, isDoneForNowActive, isProcessing, onImmediateGraphChange }) {
|
||||||
const canDoneForNow = Boolean(isDoneForNowActive) && !isProcessing;
|
const canDoneForNow = Boolean(isDoneForNowActive) && !isProcessing;
|
||||||
return (
|
return (
|
||||||
<div className="mt-6 flex items-center justify-end border-t border-gray-200 pt-5">
|
<div className="mt-6 flex items-center justify-end border-t border-gray-200 pt-5">
|
||||||
@@ -1303,6 +1303,7 @@ export default function ReasoningWorkspace({
|
|||||||
scenario,
|
scenario,
|
||||||
status,
|
status,
|
||||||
updateStatus,
|
updateStatus,
|
||||||
|
cuSynthesisLoading,
|
||||||
currentUnderstanding: propUnderstanding,
|
currentUnderstanding: propUnderstanding,
|
||||||
result,
|
result,
|
||||||
answer,
|
answer,
|
||||||
@@ -1317,8 +1318,12 @@ export default function ReasoningWorkspace({
|
|||||||
onUpdateFindingProposition,
|
onUpdateFindingProposition,
|
||||||
/* ── v0.49 — done-for-now promotion callback ───────── */
|
/* ── v0.49 — done-for-now promotion callback ───────── */
|
||||||
onSummaryUpdate,
|
onSummaryUpdate,
|
||||||
|
/* ── immediate graph transition (Done acknowledged before async) ── */
|
||||||
|
onImmediateGraphChange,
|
||||||
/* ── canonical graph-replacement seam (future Re-open) ── */
|
/* ── canonical graph-replacement seam (future Re-open) ── */
|
||||||
onSituationGraphChange,
|
onSituationGraphChange,
|
||||||
|
/* ── test init seam (no effect → immediate state) ───────── */
|
||||||
|
initialPostAnalyseStatus,
|
||||||
}) {
|
}) {
|
||||||
const [investigationHistory, setInvestigationHistory] = useState([]);
|
const [investigationHistory, setInvestigationHistory] = useState([]);
|
||||||
const turnCounter = useRef(0);
|
const turnCounter = useRef(0);
|
||||||
@@ -1338,7 +1343,7 @@ export default function ReasoningWorkspace({
|
|||||||
|
|
||||||
// ── RTO.29D — post-Analyse initial reflection surface ─────────
|
// ── RTO.29D — post-Analyse initial reflection surface ─────────
|
||||||
const [initialReflectionActive, setInitialReflectionActive] = useState(false);
|
const [initialReflectionActive, setInitialReflectionActive] = useState(false);
|
||||||
const [postAnalyseStatus, setPostAnalyseStatus] = useState(null);
|
const [postAnalyseStatus, setPostAnalyseStatus] = useState(initialPostAnalyseStatus ?? null);
|
||||||
|
|
||||||
// Capture the current selected question at submit time (not from a stale ref)
|
// Capture the current selected question at submit time (not from a stale ref)
|
||||||
const capturePendingTurn = (selectedQuestion, answerText) => {
|
const capturePendingTurn = (selectedQuestion, answerText) => {
|
||||||
@@ -1790,11 +1795,21 @@ export default function ReasoningWorkspace({
|
|||||||
{/* Current Understanding + Situation — independent vertical flow */}
|
{/* Current Understanding + Situation — independent vertical flow */}
|
||||||
<div className="flex gap-3 items-start flex-wrap">
|
<div className="flex gap-3 items-start flex-wrap">
|
||||||
{/* Current Understanding — prominent orienting surface */}
|
{/* Current Understanding — prominent orienting surface */}
|
||||||
<div className={`rounded-xl border-[2.5px] border-teal-300/70 bg-gradient-to-b from-teal-50/60 to-white px-8 pt-7 pb-8 shadow-sm flex-1 min-w-0 ${hasGraph ? 'lg:max-w-2xl' : ''}`}>
|
<div className={`rounded-xl border-[2.5px] border-teal-300/70 bg-gradient-to-b from-teal-50/60 to-white px-8 pt-7 pb-8 shadow-sm flex-1 min-w-0 relative ${hasGraph ? 'lg:max-w-2xl' : ''}`}>
|
||||||
<h2 className="mb-4 text-[11px] font-bold tracking-[.18em] uppercase text-teal-700/60">
|
<h2 className="mb-4 text-[11px] font-bold tracking-[.18em] uppercase text-teal-700/60">
|
||||||
Current Understanding
|
Current Understanding
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-lg leading-relaxed text-gray-800">{propUnderstanding}</p>
|
<p className="text-lg leading-relaxed text-gray-800">{propUnderstanding}</p>
|
||||||
|
{cuSynthesisLoading && (
|
||||||
|
<div className="absolute inset-0 cu-skeleton-overlay rounded-xl overflow-hidden flex flex-col items-center justify-center bg-gray-50" role="status" aria-live="polite">
|
||||||
|
<p className="text-sm font-semibold text-teal-800 mb-6 relative z-10 tracking-wide">Clarifying your current understanding…</p>
|
||||||
|
<div className="cu-skeleton-lines w-full max-w-lg px-8 pb-8" aria-hidden="true">
|
||||||
|
{Array.from({ length: 7 }, (_, i) => (
|
||||||
|
<div key={i} className={`cu-skeleton-line mb-3 ${['w-[94%]', 'w-[87%]', 'w-[96%]', 'w-[72%]', 'w-[90%]', 'w-[82%]', 'w-[64%]'][i % 7]}`} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Situation panel during initial reflection */}
|
{/* Situation panel during initial reflection */}
|
||||||
@@ -1990,7 +2005,7 @@ export default function ReasoningWorkspace({
|
|||||||
|
|
||||||
{/* Current Understanding — independent row, full-width of left area (cols 1-2) */}
|
{/* Current Understanding — independent row, full-width of left area (cols 1-2) */}
|
||||||
{propUnderstanding && hasCurrentSummaryCondition && postAnalyseStatus !== "success" && (
|
{propUnderstanding && hasCurrentSummaryCondition && postAnalyseStatus !== "success" && (
|
||||||
<div className="lg:row-start-1 lg:col-span-full rounded-xl border-[2.5px] border-teal-300/70 bg-gradient-to-b from-teal-50/60 to-white px-8 pt-7 pb-8 shadow-sm">
|
<div className="lg:row-start-1 lg:col-span-full rounded-xl border-[2.5px] border-teal-300/70 bg-gradient-to-b from-teal-50/60 to-white px-8 pt-7 pb-8 shadow-sm relative">
|
||||||
<CurrentUnderstandingCard currentSummary={graph?.currentSummary || result?.updatedSituationGraph?.currentSummary} plainLanguage={propUnderstanding} />
|
<CurrentUnderstandingCard currentSummary={graph?.currentSummary || result?.updatedSituationGraph?.currentSummary} plainLanguage={propUnderstanding} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -2211,7 +2226,18 @@ export default function ReasoningWorkspace({
|
|||||||
nodeId={focusedPresentationItemId}
|
nodeId={focusedPresentationItemId}
|
||||||
doneForNow={() => {
|
doneForNow={() => {
|
||||||
if (processingStep === "active") return;
|
if (processingStep === "active") return;
|
||||||
/* ── v0.49 — promote eligible focused findings into Current Understanding ─── */
|
/* ── Immediate transition: resolve target node BEFORE async — cuSynthesisLoading also set ─── */
|
||||||
|
if (onImmediateGraphChange && focusedPresentationItemId) {
|
||||||
|
const currentGraph = result?.situationGraph;
|
||||||
|
if (currentGraph) {
|
||||||
|
const resolvedIds = new Set(currentGraph.resolvedNodeIds || []);
|
||||||
|
resolvedIds.add(focusedPresentationItemId);
|
||||||
|
onImmediateGraphChange({
|
||||||
|
...currentGraph,
|
||||||
|
resolvedNodeIds: Array.from(resolvedIds),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
onSummaryUpdate?.(focusedPresentationItemId);
|
onSummaryUpdate?.(focusedPresentationItemId);
|
||||||
setDoneForNowIds((prev) => [...prev, focusedPresentationItemId]);
|
setDoneForNowIds((prev) => [...prev, focusedPresentationItemId]);
|
||||||
setFocusedAnswer("");
|
setFocusedAnswer("");
|
||||||
@@ -2221,6 +2247,7 @@ export default function ReasoningWorkspace({
|
|||||||
}}
|
}}
|
||||||
isDoneForNowActive={Boolean(getFocusedInvestigation()?.question?.trim())}
|
isDoneForNowActive={Boolean(getFocusedInvestigation()?.question?.trim())}
|
||||||
isProcessing={processingStep === "active"}
|
isProcessing={processingStep === "active"}
|
||||||
|
onImmediateGraphChange={onImmediateGraphChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -281,6 +281,7 @@ export default function ScenarioForm() {
|
|||||||
const [updateResult, setUpdateResult] = useState(null);
|
const [updateResult, setUpdateResult] = useState(null);
|
||||||
const [lastSubmittedAnswer, setLastSubmittedAnswer] = useState("");
|
const [lastSubmittedAnswer, setLastSubmittedAnswer] = useState("");
|
||||||
const [currentUnderstanding, setCurrentUnderstanding] = useState(null);
|
const [currentUnderstanding, setCurrentUnderstanding] = useState(null);
|
||||||
|
const [cuSynthesisLoading, setCuSynthesisLoading] = useState(false);
|
||||||
const [mockScenario, setMockScenario] = useState("");
|
const [mockScenario, setMockScenario] = useState("");
|
||||||
const [hideFacilitatorOnLanding, setHideFacilitatorOnLanding] = useState(false);
|
const [hideFacilitatorOnLanding, setHideFacilitatorOnLanding] = useState(false);
|
||||||
|
|
||||||
@@ -357,16 +358,29 @@ export default function ScenarioForm() {
|
|||||||
* Authoritative graph reconsideration triggered by "Done for now"
|
* Authoritative graph reconsideration triggered by "Done for now"
|
||||||
* activity boundary. Delegates to the exported executeEpisodeDone pipeline.
|
* activity boundary. Delegates to the exported executeEpisodeDone pipeline.
|
||||||
*/
|
*/
|
||||||
async function handleDoneForNowPromotion(targetNodeId) {
|
async function handleDoneForNowPromotion(targetNodeId, onImmediateGraphUpdate) {
|
||||||
if (!targetNodeId || !findings?.length) return;
|
if (!targetNodeId || !findings?.length) return;
|
||||||
|
|
||||||
// In-flight guard: exactly-once enforcement
|
// In-flight guard: exactly-once enforcement
|
||||||
if (doneInProgressRef.current) return;
|
if (doneInProgressRef.current) return;
|
||||||
doneInProgressRef.current = true;
|
doneInProgressRef.current = true;
|
||||||
|
|
||||||
|
/* ── Immediate client transition — before awaiting async work ── */
|
||||||
|
const preDoneGraph = result?.situationGraph;
|
||||||
|
if (preDoneGraph && onImmediateGraphUpdate) {
|
||||||
|
const immediateResolvedIds = new Set(preDoneGraph.resolvedNodeIds || []);
|
||||||
|
immediateResolvedIds.add(targetNodeId);
|
||||||
|
const immediateGraph = {
|
||||||
|
...preDoneGraph,
|
||||||
|
resolvedNodeIds: Array.from(immediateResolvedIds),
|
||||||
|
};
|
||||||
|
onImmediateGraphUpdate(immediateGraph);
|
||||||
|
}
|
||||||
|
|
||||||
|
setCuSynthesisLoading(true);
|
||||||
try {
|
try {
|
||||||
const doneResult = await executeEpisodeDone({
|
const doneResult = await executeEpisodeDone({
|
||||||
resultSituationGraph: result?.situationGraph,
|
resultSituationGraph: preDoneGraph,
|
||||||
targetNodeId,
|
targetNodeId,
|
||||||
focusedContributions: focusedContributions ?? [],
|
focusedContributions: focusedContributions ?? [],
|
||||||
findings,
|
findings,
|
||||||
@@ -388,6 +402,7 @@ export default function ScenarioForm() {
|
|||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
doneInProgressRef.current = false;
|
doneInProgressRef.current = false;
|
||||||
|
setCuSynthesisLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,14 +437,9 @@ export default function ScenarioForm() {
|
|||||||
const currentGraph = result?.situationGraph;
|
const currentGraph = result?.situationGraph;
|
||||||
if (!currentGraph) return;
|
if (!currentGraph) return;
|
||||||
|
|
||||||
const completeNextFindings = normalizeFindings([
|
|
||||||
...(findings ?? []),
|
|
||||||
...newFindingsDelta,
|
|
||||||
]);
|
|
||||||
|
|
||||||
void synthesizeFromFindings(fetch, {
|
void synthesizeFromFindings(fetch, {
|
||||||
situationGraph: currentGraph,
|
situationGraph: currentGraph,
|
||||||
findings: completeNextFindings,
|
findings: normalizeFindings([...(findings ?? []), ...newFindingsDelta]),
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
if (res.ok && res.data?.currentUnderstanding) {
|
if (res.ok && res.data?.currentUnderstanding) {
|
||||||
setCurrentUnderstanding(res.data.currentUnderstanding);
|
setCurrentUnderstanding(res.data.currentUnderstanding);
|
||||||
@@ -794,6 +804,7 @@ export default function ScenarioForm() {
|
|||||||
scenario={scenario}
|
scenario={scenario}
|
||||||
status={status}
|
status={status}
|
||||||
updateStatus={updateStatus}
|
updateStatus={updateStatus}
|
||||||
|
cuSynthesisLoading={cuSynthesisLoading}
|
||||||
currentUnderstanding={currentUnderstanding}
|
currentUnderstanding={currentUnderstanding}
|
||||||
result={{
|
result={{
|
||||||
...(result || {}),
|
...(result || {}),
|
||||||
@@ -814,6 +825,8 @@ export default function ScenarioForm() {
|
|||||||
onUpdateFindingProposition={updateFindingProposition}
|
onUpdateFindingProposition={updateFindingProposition}
|
||||||
/* ── v0.49 — done-for-now promotion seam ─────────── */
|
/* ── v0.49 — done-for-now promotion seam ─────────── */
|
||||||
onSummaryUpdate={handleDoneForNowPromotion}
|
onSummaryUpdate={handleDoneForNowPromotion}
|
||||||
|
/* ── immediate graph transition (Done acknowledged before async) ── */
|
||||||
|
onImmediateGraphChange={(nextGraph) => setResult((prev) => ({ ...(prev ?? {}), situationGraph: nextGraph }))}
|
||||||
/* ── canonical graph-replacement seam (future Re-open) ── */
|
/* ── canonical graph-replacement seam (future Re-open) ── */
|
||||||
onSituationGraphChange={(nextGraph) => setResult((prev) => ({ ...(prev ?? {}), situationGraph: nextGraph }))}
|
onSituationGraphChange={(nextGraph) => setResult((prev) => ({ ...(prev ?? {}), situationGraph: nextGraph }))}
|
||||||
onRestart={() => {
|
onRestart={() => {
|
||||||
|
|||||||
@@ -144,6 +144,20 @@ coherent user-facing explanation
|
|||||||
|
|
||||||
A separate LLM call here remains appropriate because this operation serves presentation/coherence, **not** authoritative episode interpretation. Desired presentation direction: short, clear, scannable, plain language, minimal repetition. Do not redesign or tune the CU prompt now.
|
A separate LLM call here remains appropriate because this operation serves presentation/coherence, **not** authoritative episode interpretation. Desired presentation direction: short, clear, scannable, plain language, minimal repetition. Do not redesign or tune the CU prompt now.
|
||||||
|
|
||||||
|
### Done-for-now interaction (current bounded contract)
|
||||||
|
|
||||||
|
`Done for now` is user-owned and has immediate visible effect — the engine does not decide whether enough evidence has been gathered.
|
||||||
|
|
||||||
|
On clicking **Done for now**:
|
||||||
|
|
||||||
|
1. **Question parks immediately** under "Questions we have clarified" with `Clarified` status + Re-open button
|
||||||
|
2. **Focused investigation workspace closes** without waiting for async pipeline
|
||||||
|
3. **Current Understanding loading begins immediately** — skeleton overlay appears while the async pipeline runs
|
||||||
|
4. Skeleton spans: episode reconsideration → graph application → CU synthesis
|
||||||
|
5. **CU refresh completes the investigation checkpoint** — new CU replaces skeleton when ready
|
||||||
|
|
||||||
|
The skeleton overlay uses strong paragraph-style bars with varied widths and a left→right shimmer, centred status message ("Clarifying your current understanding…"), and an opaque background that fully obscures old CU content until synthesis succeeds or fails.
|
||||||
|
|
||||||
### Current Understanding refresh invariant
|
### Current Understanding refresh invariant
|
||||||
|
|
||||||
Reconstruct Current Understanding when canonical meaning or the eligible evidence set changes. Do **not** reconstruct it merely because investigation/question status changes.
|
Reconstruct Current Understanding when canonical meaning or the eligible evidence set changes. Do **not** reconstruct it merely because investigation/question status changes.
|
||||||
|
|||||||
@@ -2147,4 +2147,96 @@ describe("ReasoningWorkspace UI", () => {
|
|||||||
expect(nextResult.selectedQuestion.question).toBe("What denominator is being used for the complaint rate?");
|
expect(nextResult.selectedQuestion.question).toBe("What denominator is being used for the complaint rate?");
|
||||||
expect(nextResult.diagnostics.modelName).toBe("test");
|
expect(nextResult.diagnostics.modelName).toBe("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── CU synthesis loading overlay (wired to visible PostAnalyse CU path) ─────────────────────────────
|
||||||
|
|
||||||
|
// Uses renderToStaticMarkup + ReasoningWorkspace.initialPostAnalyseStatus test init prop
|
||||||
|
// to exercise Path A (initial-reflection CU with shimmer overlay) without effect lifecycle.
|
||||||
|
// initialPostAnalyseStatus="success" lets us render the same card that useEffect produces
|
||||||
|
// in production, proving cuSynthesisLoading reaches the actually visible CU card.
|
||||||
|
|
||||||
|
function createCUOverlayProps(cuSynthesisLoading) {
|
||||||
|
return {
|
||||||
|
status: "success",
|
||||||
|
updateStatus: "idle",
|
||||||
|
initialPostAnalyseStatus: "success",
|
||||||
|
currentUnderstanding: "We have identified a key question to investigate further.",
|
||||||
|
cuSynthesisLoading,
|
||||||
|
result: makeWorkspaceResult({
|
||||||
|
situationGraph: {
|
||||||
|
...makeWorkspaceResult().situationGraph,
|
||||||
|
activeUnknownNodeId: null,
|
||||||
|
nodes: makeWorkspaceResult().situationGraph.nodes.map((n) =>
|
||||||
|
n.id === "n-unknown" ? { ...n, status: "resolved", value: "the denominator is total units sold" } : n,
|
||||||
|
),
|
||||||
|
resolvedNodeIds: ["n-unknown"],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
answer: "",
|
||||||
|
setAnswer: vi.fn(),
|
||||||
|
onAnswerSubmit: vi.fn(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
it("does NOT show CU synthesis overlay when cuSynthesisLoading is false", () => {
|
||||||
|
const html = renderToStaticMarkup(<ReasoningWorkspace {...createCUOverlayProps(false)} />);
|
||||||
|
|
||||||
|
// The visible PostAnalyse CU card renders on Path A (postAnalyseStatus === "success")
|
||||||
|
expect(html).toContain("Current Understanding");
|
||||||
|
// But cu-skeleton-overlay class MUST NOT be present
|
||||||
|
expect(html).not.toContain("cu-skeleton-overlay");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows CU skeleton overlay on the ACTUAL VISIBLE PostAnalyse CU card when cuSynthesisLoading is true", () => {
|
||||||
|
const html = renderToStaticMarkup(<ReasoningWorkspace {...createCUOverlayProps(true)} />);
|
||||||
|
|
||||||
|
// ── Prove the skeleton reaches Path A (the authoritative visible CU) ──────────
|
||||||
|
// initialPostAnalyseStatus="success" puts ReasoningWorkspace in the same state
|
||||||
|
// as after useEffect fires → Path A renders (initial-reflection CU).
|
||||||
|
// The skeleton overlay MUST appear there — this assertion would fail if cuSynthesisLoading
|
||||||
|
// were wired only to the old grid path (which is on Path B).
|
||||||
|
|
||||||
|
// role="status" proves the overlay's accessible status container renders
|
||||||
|
expect(html).toContain("role=\"status\"");
|
||||||
|
// "Clarifying your current understanding…" message visible
|
||||||
|
expect(html).toContain("Clarifying your current understanding…");
|
||||||
|
// The skeleton overlay class is present on Path A (not dead on Path B)
|
||||||
|
expect(html).toContain("cu-skeleton-overlay");
|
||||||
|
// Exactly 7 skeleton line placeholders (strong skeleton requirement)
|
||||||
|
// cu-skeleton-lines parent class contains cu-skeleton-line as prefix substring,
|
||||||
|
// so count = 7 bars + 1 parent = 8 occurrences → split gives 9 parts
|
||||||
|
expect(html.split("cu-skeleton-line").length).toBe(9);
|
||||||
|
// Old CU text remains structurally available behind the overlay
|
||||||
|
expect(html).toContain("We have identified a key question to investigate further.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("proves skeleton is on Path A not the grid CU by verifying PostAnalyse CU header when cuSynthesisLoading=true", () => {
|
||||||
|
const html = renderToStaticMarkup(<ReasoningWorkspace {...createCUOverlayProps(true)} />);
|
||||||
|
|
||||||
|
// The PostAnalyse visible CU header and skeleton are present
|
||||||
|
expect(html).toContain("Current Understanding");
|
||||||
|
expect(html).toContain("cu-skeleton-overlay");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("proves skeleton visible alongside clarified question — pending state does not block graph transition", () => {
|
||||||
|
// ── Pending-state assertion: cuSynthesisLoading=true AND resolved node coexist ─
|
||||||
|
// The overlay (skeleton) must be visible WHILE the canonical graph already
|
||||||
|
// reflects the clarification. This proves the immediate move is NOT gated by
|
||||||
|
// async reconsideration completion.
|
||||||
|
const html = renderToStaticMarkup(<ReasoningWorkspace {...createCUOverlayProps(true)} />);
|
||||||
|
|
||||||
|
// Skeleton present → CU being regenerated
|
||||||
|
expect(html).toContain("cu-skeleton-overlay");
|
||||||
|
// Target question already in Questions we have clarified (resolvedNodeIds)
|
||||||
|
expect(html).toContain("Questions we have clarified");
|
||||||
|
// Question "Complaint rate denominator" moved into the resolved section
|
||||||
|
expect(html).toContain("Complaint rate denominator");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("proves skeleton loading surface blocks old CU — opaque gray background", () => {
|
||||||
|
const html = renderToStaticMarkup(<ReasoningWorkspace {...createCUOverlayProps(true)} />);
|
||||||
|
|
||||||
|
// The overlay uses a strong bg-gray-50 blocking surface (not transparent)
|
||||||
|
expect(html).toContain("bg-gray-50");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user