fix: harden phase summary open questions
This commit is contained in:
@@ -129,6 +129,31 @@ test("phase v2 dispatches to PhaseSummaryViewer", () => {
|
||||
expect(screen.getByText("Riassunto della fase.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("phase v2 tolerates the malformed open question from the latest session", () => {
|
||||
render(
|
||||
<ArtifactView
|
||||
artifact={{
|
||||
kind: "phase",
|
||||
data: {
|
||||
schema_version: 2,
|
||||
phase: { id: "F6", num: 6, name: "Piano CTE" },
|
||||
summary: "Piano completato.",
|
||||
open_questions: [
|
||||
{
|
||||
label: "pazienti_finale restituisce 0 righe",
|
||||
question: "Verificare se la condizione temporale è troppo restrittiva",
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText("Verificare se la condizione temporale è troppo restrittiva"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("phase v1 (no schema_version) still renders via legacy markdown/structured path", () => {
|
||||
render(
|
||||
<ArtifactView
|
||||
|
||||
@@ -81,6 +81,22 @@ test("renders open_questions as a bullet list", () => {
|
||||
expect(screen.getByText("Serve confermare la finestra temporale?")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders a legacy structured open question safely", () => {
|
||||
const malformed = {
|
||||
...phase,
|
||||
open_questions: [
|
||||
{
|
||||
label: "pazienti_finale restituisce 0 righe",
|
||||
question: "Verificare se i filtri sono troppo restrittivi",
|
||||
},
|
||||
],
|
||||
} as unknown as PhaseSummaryV2;
|
||||
|
||||
render(<PhaseSummaryViewer phase={malformed} />);
|
||||
|
||||
expect(screen.getByText("Verificare se i filtri sono troppo restrittivi")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders a section with title only (no items key) without crashing", () => {
|
||||
const proseOnly: PhaseSummaryV2 = {
|
||||
schema_version: 2,
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { Badge } from "../components/ui/badge";
|
||||
import { MarkdownView } from "./MarkdownView";
|
||||
import { statusBadgeClass } from "./statusBadge";
|
||||
import type { PhaseCheck, PhaseSection, PhaseSectionItem, PhaseSummaryV2, PhaseTable } from "./artifactV2";
|
||||
import {
|
||||
phaseOpenQuestionText,
|
||||
type PhaseCheck,
|
||||
type PhaseSection,
|
||||
type PhaseSectionItem,
|
||||
type PhaseSummaryV2,
|
||||
type PhaseTable,
|
||||
} from "./artifactV2";
|
||||
|
||||
const CODE_CHIP = "rounded-md bg-muted px-1.5 py-0.5 font-mono text-xs text-foreground/90";
|
||||
|
||||
@@ -69,6 +76,12 @@ function TableRecap({ table }: { table: PhaseTable }) {
|
||||
}
|
||||
|
||||
export function PhaseSummaryViewer({ phase }: { phase: PhaseSummaryV2 }) {
|
||||
const openQuestions = Array.isArray(phase.open_questions)
|
||||
? (phase.open_questions as unknown[])
|
||||
.map(phaseOpenQuestionText)
|
||||
.filter((question): question is string => question !== null)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<h3 className="thot-label">
|
||||
@@ -101,11 +114,11 @@ export function PhaseSummaryViewer({ phase }: { phase: PhaseSummaryV2 }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{phase.open_questions && phase.open_questions.length > 0 && (
|
||||
{openQuestions.length > 0 && (
|
||||
<div>
|
||||
<h4 className="thot-label mb-1">Open questions</h4>
|
||||
<ul className="list-disc space-y-1 pl-5 text-sm text-foreground/90 marker:text-muted-foreground">
|
||||
{phase.open_questions.map((q, i) => (
|
||||
{openQuestions.map((q, i) => (
|
||||
<li key={i}>{q}</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -119,3 +119,18 @@ export interface PhaseSummaryV2 {
|
||||
tables?: PhaseTable[];
|
||||
open_questions?: string[];
|
||||
}
|
||||
|
||||
/** Defensive compatibility for malformed v2 artifacts persisted before the gate
|
||||
* enforced `open_questions: string[]`. New payloads stay strictly typed as strings. */
|
||||
export function phaseOpenQuestionText(value: unknown): string | null {
|
||||
if (typeof value === "string") return value.trim() || null;
|
||||
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
||||
|
||||
const record = value as Record<string, unknown>;
|
||||
for (const field of ["question", "label"]) {
|
||||
const text = record[field];
|
||||
if (typeof text === "string" && text.trim()) return text.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user