Fix magic-link verify URLs in email and related create-flow QA #70

Merged
an.di merged 4 commits from adilallo/fix/QaFeedback into main 2026-09-01 16:09:31 +00:00
20 changed files with 531 additions and 36 deletions
Showing only changes of commit 511c3efb4c - Show all commits
@@ -1078,6 +1078,7 @@ export function FinalReviewChipEditModal({
<Create
isOpen={isOpen && !addCustomWizardOpen}
onClose={handleModalClose}
onBack={handleModalClose}
backdropVariant="blurredYellow"
headerContent={headerContent}
showNextButton={true}
@@ -80,19 +80,21 @@ function DecisionApproachEditFieldsComponent({
onChange={(v) => patch("stepByStepInstructions", v)}
disabled={readOnly}
/>
<IncrementerBlock
label={t.sectionHeadings.consensusLevel}
helpIcon={false}
value={value.consensusLevel}
min={CONSENSUS_LEVEL_MIN}
max={CONSENSUS_LEVEL_MAX}
step={CONSENSUS_LEVEL_STEP}
onChange={(next) => patch("consensusLevel", next)}
formatValue={(v) => `${v}%`}
decrementAriaLabel="Decrease consensus level"
incrementAriaLabel="Increase consensus level"
disabled={readOnly}
/>
{value.consensusLevel !== undefined ? (
<IncrementerBlock
label={t.sectionHeadings.consensusLevel}
helpIcon={false}
value={value.consensusLevel}
min={CONSENSUS_LEVEL_MIN}
max={CONSENSUS_LEVEL_MAX}
step={CONSENSUS_LEVEL_STEP}
onChange={(next) => patch("consensusLevel", next)}
formatValue={(v) => `${v}%`}
decrementAriaLabel="Decrease consensus level"
incrementAriaLabel="Increase consensus level"
disabled={readOnly}
/>
) : null}
<ModalTextAreaField
label={t.sectionHeadings.objectionsDeadlocks}
value={value.objectionsDeadlocks}
+5 -1
View File
@@ -85,7 +85,11 @@ export type DecisionApproachDetailEntry = {
applicableScope: string[];
selectedApplicableScope: string[];
stepByStepInstructions: string;
consensusLevel: number;
/**
* Catalog presets always set this. User-authored custom cards omit it until
* the author adds a proportion field or edits consensus in the facet form.
*/
consensusLevel?: number;
objectionsDeadlocks: string;
};
@@ -14,8 +14,8 @@ export interface CommunityRuleEntry {
/** Plain text; split on blank lines into paragraphs when rendering. */
body: string;
/**
* When set, rendered as Figma-style label + body stacks. If non-empty, takes
* precedence over {@link body} for main content (body may be empty).
* Figma-style label + body stacks (facet sections, wizard fields). Shown
* after {@link body} when both are present.
*/
blocks?: CommunityRuleLabeledBlock[];
}
@@ -45,7 +45,7 @@ function CommunityRuleView({
<TextBlock
key={entryIndex}
title={entry.title}
body={hasBlocks ? undefined : entry.body}
body={entry.body}
rows={hasBlocks ? entry.blocks : undefined}
/>
);
@@ -94,9 +94,10 @@ function TextBlockView({
>
<p className={`${ENTRY_TITLE_CLASS} w-full min-w-0`}>{title}</p>
<div className="flex min-w-0 flex-col gap-3">
{body.trim().length > 0 ? <ParagraphGroup text={body} /> : null}
{hasRows
? rows!.map((row, i) => <LabeledRowView key={i} row={row} />)
: body.trim().length > 0 && <ParagraphGroup text={body} />}
: null}
</div>
</div>
);
+41 -2
View File
@@ -16,7 +16,10 @@ import {
publishedMethodDisplayLabel,
} from "./finalReviewChipPresets";
import { isDocumentEntry } from "./documentEntryGuards";
import { replaceMethodSectionsWithMethodSelections } from "./ruleSectionsFromMethodSelections";
import {
replaceMethodSectionsWithMethodSelections,
withoutUnpublishedDecisionConsensus,
} from "./ruleSectionsFromMethodSelections";
import { templateCategoryToGroupKey } from "./templateReviewMapping";
export { isDocumentEntry } from "./documentEntryGuards";
@@ -78,21 +81,25 @@ export type PublishedMethodSelections = {
id: string;
label: string;
sections: CommunicationMethodDetailEntry;
supportText?: string;
}>;
membership?: Array<{
id: string;
label: string;
sections: MembershipMethodDetailEntry;
supportText?: string;
}>;
decisionApproaches?: Array<{
id: string;
label: string;
sections: DecisionApproachDetailEntry;
supportText?: string;
}>;
conflictManagement?: Array<{
id: string;
label: string;
sections: ConflictManagementDetailEntry;
supportText?: string;
}>;
};
@@ -247,6 +254,14 @@ function pickMethodIds(
return derived;
}
function publishedRowSupportText(
id: string,
meta: CreateFlowState["customMethodCardMetaById"],
): string | undefined {
const t = meta?.[id]?.supportText?.trim();
return t && t.length > 0 ? t : undefined;
}
/**
* Merge `selected*MethodIds` with any saved `{group}MethodDetailsById`
* overrides authored on the final-review screen. Preset defaults from the
@@ -270,6 +285,10 @@ export function buildMethodSelectionsForDocument(
out.communication = commIds.map((id) => {
const preset = communicationPresetFor(id);
const override = state.communicationMethodDetailsById?.[id];
const supportText = publishedRowSupportText(
id,
state.customMethodCardMetaById,
);
return {
id,
label: publishedMethodDisplayLabel(
@@ -278,6 +297,7 @@ export function buildMethodSelectionsForDocument(
state.customMethodCardMetaById,
),
sections: override ? { ...preset, ...override } : preset,
...(supportText ? { supportText } : {}),
};
});
}
@@ -290,6 +310,10 @@ export function buildMethodSelectionsForDocument(
out.membership = memIds.map((id) => {
const preset = membershipPresetFor(id);
const override = state.membershipMethodDetailsById?.[id];
const supportText = publishedRowSupportText(
id,
state.customMethodCardMetaById,
);
return {
id,
label: publishedMethodDisplayLabel(
@@ -298,6 +322,7 @@ export function buildMethodSelectionsForDocument(
state.customMethodCardMetaById,
),
sections: override ? { ...preset, ...override } : preset,
...(supportText ? { supportText } : {}),
};
});
}
@@ -310,6 +335,11 @@ export function buildMethodSelectionsForDocument(
out.decisionApproaches = daIds.map((id) => {
const preset = decisionApproachPresetFor(id);
const override = state.decisionApproachDetailsById?.[id];
const supportText = publishedRowSupportText(
id,
state.customMethodCardMetaById,
);
const merged = override ? { ...preset, ...override } : preset;
return {
id,
label: publishedMethodDisplayLabel(
@@ -317,7 +347,11 @@ export function buildMethodSelectionsForDocument(
id,
state.customMethodCardMetaById,
),
sections: override ? { ...preset, ...override } : preset,
sections: withoutUnpublishedDecisionConsensus(
{ ...merged },
state.customMethodCardFieldBlocksById?.[id],
) as DecisionApproachDetailEntry,
...(supportText ? { supportText } : {}),
};
});
}
@@ -330,6 +364,10 @@ export function buildMethodSelectionsForDocument(
out.conflictManagement = cmIds.map((id) => {
const preset = conflictManagementPresetFor(id);
const override = state.conflictManagementDetailsById?.[id];
const supportText = publishedRowSupportText(
id,
state.customMethodCardMetaById,
);
return {
id,
label: publishedMethodDisplayLabel(
@@ -338,6 +376,7 @@ export function buildMethodSelectionsForDocument(
state.customMethodCardMetaById,
),
sections: override ? { ...preset, ...override } : preset,
...(supportText ? { supportText } : {}),
};
});
}
+9 -6
View File
@@ -106,7 +106,7 @@ export function membershipPresetFor(id: string): MembershipMethodDetailEntry {
};
}
/** Default consensus level used when presets omit a value (see DecisionApproachesScreen). */
/** Default consensus level used when a **catalog** preset omits a value. */
export const DECISION_CONSENSUS_LEVEL_DEFAULT = 75;
export function decisionApproachPresetFor(
@@ -114,19 +114,22 @@ export function decisionApproachPresetFor(
): DecisionApproachDetailEntry {
const method = findMethod(decisionApproachesMessages, id);
const s = method?.sections ?? {};
return {
const entry: DecisionApproachDetailEntry = {
corePrinciple: asString(s.corePrinciple),
applicableScope: asStringArray(s.applicableScope),
selectedApplicableScope: [],
stepByStepInstructions: asString(s.stepByStepInstructions),
consensusLevel: asNumberClamped(
objectionsDeadlocks: asString(s.objectionsDeadlocks),
};
if (method) {
entry.consensusLevel = asNumberClamped(
s.consensusLevel,
0,
100,
DECISION_CONSENSUS_LEVEL_DEFAULT,
),
objectionsDeadlocks: asString(s.objectionsDeadlocks),
};
);
}
return entry;
}
export function conflictManagementPresetFor(
+9 -2
View File
@@ -195,12 +195,19 @@ function mapFacetPrefillToWizardFieldBlocks(
prefill.headings.stepByStepInstructions,
prefill.draft.stepByStepInstructions,
),
{
);
if (
typeof prefill.draft.consensusLevel === "number" &&
facetPrefillHasContent(prefill)
) {
blocks.push({
kind: "proportion",
id: "facet-consensusLevel",
blockTitle: prefill.headings.consensusLevel,
defaultPercent: clampPercent(prefill.draft.consensusLevel),
},
});
}
blocks.push(
textBlock(
"facet-objectionsDeadlocks",
prefill.headings.objectionsDeadlocks,
@@ -22,6 +22,7 @@ function customMethodCardMetaFromPublishedSelections(
| Array<{
id: string;
label: string;
supportText?: string;
}>
| undefined,
) => {
@@ -32,7 +33,9 @@ function customMethodCardMetaFromPublishedSelections(
if (methodLabelFor(groupKey, id).length > 0) continue;
const label = typeof row.label === "string" ? row.label.trim() : "";
if (!label) continue;
meta[id] = { label, supportText: "" };
const supportText =
typeof row.supportText === "string" ? row.supportText : "";
meta[id] = { label, supportText };
}
};
absorb("communication", ms.communication);
+76 -6
View File
@@ -78,6 +78,8 @@ export function labeledBlocksFromCustomMethodCardFieldBlocks(
export type CommunityRuleEntryFromChipOptions = {
consensusLevelKey?: string;
customFieldBlocks?: CustomMethodCardFieldBlock[];
/** Wizard step-2 policy description (`customMethodCardMetaById.supportText`). */
supportText?: string;
};
/** Canonical `categoryName` strings for method groups in published documents. */
@@ -195,8 +197,64 @@ export function communityRuleEntryFromMethodChip(
? labeledBlocksFromCustomMethodCardFieldBlocks(options.customFieldBlocks)
: [];
const blocks = [...presetBlocks, ...wizardBlocks];
if (blocks.length === 0) return null;
return { title, body: "", blocks };
const description = nonEmptyTrimmed(options?.supportText);
if (blocks.length === 0) {
if (!description) return null;
return { title, body: description };
}
return {
title,
body: description ?? "",
blocks,
};
}
function decisionApproachHasPublishableFacetCopy(
sections: Record<string, unknown>,
): boolean {
return Boolean(
nonEmptyTrimmed(sections.corePrinciple) ||
nonEmptyTrimmed(sections.stepByStepInstructions) ||
nonEmptyTrimmed(sections.objectionsDeadlocks) ||
formatScopePayload(sections.applicableScope) ||
formatScopePayload(sections.selectedApplicableScope),
);
}
/**
* Catalog methods publish their consensus figure. User-authored custom cards
* often seed `75` with empty facet copy skip that unless the author actually
* filled decision sections. Wizard field blocks (including proportion) are the
* source of truth when present.
*/
function shouldPublishDecisionConsensusLevel(
sections: Record<string, unknown>,
customFieldBlocks?: CustomMethodCardFieldBlock[],
): boolean {
if (
typeof sections.consensusLevel !== "number" ||
Number.isNaN(sections.consensusLevel)
) {
return false;
}
if (customFieldBlocks && customFieldBlocks.length > 0) {
return false;
}
return decisionApproachHasPublishableFacetCopy(sections);
}
/** Drop seeded / wizard-superseded `consensusLevel` before publish or hydrate. */
export function withoutUnpublishedDecisionConsensus(
sections: Record<string, unknown>,
customFieldBlocks?: CustomMethodCardFieldBlock[],
): Record<string, unknown> {
if (shouldPublishDecisionConsensusLevel(sections, customFieldBlocks)) {
return sections;
}
if (!("consensusLevel" in sections)) return sections;
const next = { ...sections };
delete next.consensusLevel;
return next;
}
export function sectionFromCommunication(
@@ -209,6 +267,7 @@ export function sectionFromCommunication(
const sec = m.sections as unknown as Record<string, unknown>;
const e = communityRuleEntryFromMethodChip(m.label, sec, COMM_LABELS, {
customFieldBlocks: customFieldBlocksById?.[m.id],
supportText: m.supportText,
});
if (e) entries.push(e);
}
@@ -227,6 +286,7 @@ export function sectionFromMembership(
const sec = m.sections as unknown as Record<string, unknown>;
const e = communityRuleEntryFromMethodChip(m.label, sec, MEM_LABELS, {
customFieldBlocks: customFieldBlocksById?.[m.id],
supportText: m.supportText,
});
if (e) entries.push(e);
}
@@ -254,10 +314,19 @@ export function sectionFromDecision(
);
if (scope) merged.applicableScope = scope;
delete merged.selectedApplicableScope;
const e = communityRuleEntryFromMethodChip(m.label, merged, DEC_LABELS, {
consensusLevelKey: "consensusLevel",
customFieldBlocks: customFieldBlocksById?.[m.id],
});
const e = communityRuleEntryFromMethodChip(
m.label,
withoutUnpublishedDecisionConsensus(
merged,
customFieldBlocksById?.[m.id],
),
DEC_LABELS,
{
consensusLevelKey: "consensusLevel",
customFieldBlocks: customFieldBlocksById?.[m.id],
supportText: m.supportText,
},
);
if (e) entries.push(e);
}
return entries.length > 0
@@ -281,6 +350,7 @@ export function sectionFromConflict(
delete merged.selectedApplicableScope;
const e = communityRuleEntryFromMethodChip(m.label, merged, CM_LABELS, {
customFieldBlocks: customFieldBlocksById?.[m.id],
supportText: m.supportText,
});
if (e) entries.push(e);
}
+1 -1
View File
@@ -54,7 +54,7 @@ const decisionApproachDetailEntrySchema = z.object({
applicableScope: z.array(z.string().max(2000)).max(50),
selectedApplicableScope: z.array(z.string().max(2000)).max(50),
stepByStepInstructions: z.string().max(8000),
consensusLevel: z.number().int().min(0).max(100),
consensusLevel: z.number().int().min(0).max(100).optional(),
objectionsDeadlocks: z.string().max(8000),
});
+22
View File
@@ -50,4 +50,26 @@ describe("CommunityRule", () => {
);
expect(screen.getByText("How proposals pass")).toBeInTheDocument();
});
it("renders entry body together with labeled blocks", () => {
render(
<CommunityRule
sections={[
{
categoryName: "Decision-making",
entries: [
{
title: "Forum proposals",
body: "Anyone can start a thread.",
blocks: [{ label: "Quorum", body: "60%" }],
},
],
},
]}
/>,
);
expect(screen.getByText("Anyone can start a thread.")).toBeInTheDocument();
expect(screen.getByText("Quorum")).toBeInTheDocument();
expect(screen.getByText("60%")).toBeInTheDocument();
});
});
+19
View File
@@ -519,6 +519,25 @@ describe("FinalReviewScreen — chip detail modal", () => {
).not.toBeInTheDocument();
});
it("closes the chip edit modal when Back is pressed", async () => {
render(
<FinalReviewWithStateProbe
onState={() => {}}
initial={{
title: "Oak Park Commons",
selectedCommunicationMethodIds: ["signal"],
}}
/>,
);
fireEvent.click(await screen.findByRole("button", { name: "Signal" }));
const dialog = await screen.findByRole("dialog");
fireEvent.click(within(dialog).getByRole("button", { name: "Back" }));
await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
});
});
/**
+13
View File
@@ -45,4 +45,17 @@ describe("TextBlock", () => {
"/api/uploads/aaaaaaaa-bbbb-4ccc-dddd-eeeeeeeeeeee",
);
});
it("renders body paragraphs above labeled rows when both are set", () => {
render(
<TextBlock
title="Forum proposals"
body="Anyone can start a thread."
rows={[{ label: "Quorum", body: "60%" }]}
/>,
);
expect(screen.getByText("Anyone can start a thread.")).toBeInTheDocument();
expect(screen.getByText("Quorum")).toBeInTheDocument();
expect(screen.getByText("60%")).toBeInTheDocument();
});
});
+49
View File
@@ -249,6 +249,55 @@ describe("buildPublishPayload — methodSelections", () => {
expect(entry.sections.corePrinciple.length).toBeGreaterThan(0);
});
it("does not seed 75% consensus on a custom decision-making card", () => {
const customId = "00000000-0000-4000-8000-000000000170";
const r = buildPublishPayload({
title: "T",
selectedDecisionApproachIds: [customId],
customMethodCardMetaById: {
[customId]: {
label: "Forum proposals",
supportText: "Anyone can start a thread.",
},
},
decisionApproachDetailsById: {
[customId]: {
corePrinciple: "",
applicableScope: [],
selectedApplicableScope: [],
stepByStepInstructions: "",
consensusLevel: 75,
objectionsDeadlocks: "",
},
},
});
expect(r.ok).toBe(true);
if (!r.ok) return;
const ms = r.document.methodSelections as {
decisionApproaches?: Array<{
supportText?: string;
sections: { consensusLevel?: number };
}>;
};
expect(ms.decisionApproaches?.[0]?.supportText).toBe(
"Anyone can start a thread.",
);
expect(ms.decisionApproaches?.[0]?.sections.consensusLevel).toBeUndefined();
});
it("keeps catalog decision consensus in methodSelections", () => {
const r = buildPublishPayload({
title: "T",
selectedDecisionApproachIds: ["lazy-consensus"],
});
expect(r.ok).toBe(true);
if (!r.ok) return;
const ms = r.document.methodSelections as {
decisionApproaches?: Array<{ sections: { consensusLevel?: number } }>;
};
expect(ms.decisionApproaches?.[0]?.sections.consensusLevel).toBe(100);
});
it("merges override on top of preset for the selected method", () => {
const r = buildPublishPayload({
title: "T",
@@ -3,6 +3,7 @@ import {
buildMethodCardWizardInitialValues,
coreValueDetailsFromWizardFieldBlocks,
facetDetailsToWizardFieldBlocks,
overlayFacetPrefillValues,
} from "../../lib/create/methodCardWizardPrefill";
import type { CustomMethodCardFieldBlock } from "../../lib/create/customMethodCardFieldBlocks";
@@ -219,6 +220,49 @@ describe("facetDetailsToWizardFieldBlocks", () => {
});
});
it("omits a consensus proportion when the draft has no consensus level", () => {
const blocksOut = facetDetailsToWizardFieldBlocks({
group: "decisionApproaches",
draft: {
corePrinciple: "Momentum.",
applicableScope: [],
selectedApplicableScope: [],
stepByStepInstructions: "Post a deadline.",
objectionsDeadlocks: "",
},
headings: {
corePrinciple: "Core Principle",
applicableScope: "Applicable Scope",
stepByStepInstructions: "Step-by-Step Instructions",
consensusLevel: "Consensus Level",
objectionsDeadlocks: "Objections & Deadlocks",
},
});
expect(blocksOut.find((b) => b.kind === "proportion")).toBeUndefined();
});
it("does not inject a seeded 75% proportion onto empty custom drafts", () => {
const next = overlayFacetPrefillValues([], {
group: "decisionApproaches",
draft: {
corePrinciple: "",
applicableScope: [],
selectedApplicableScope: [],
stepByStepInstructions: "",
consensusLevel: 75,
objectionsDeadlocks: "",
},
headings: {
corePrinciple: "Core Principle",
applicableScope: "Applicable Scope",
stepByStepInstructions: "Step-by-Step Instructions",
consensusLevel: "Consensus Level",
objectionsDeadlocks: "Objections & Deadlocks",
},
});
expect(next).toEqual([]);
});
it("maps core value meaning and signals onto text blocks", () => {
expect(
facetDetailsToWizardFieldBlocks({
@@ -314,6 +314,37 @@ describe("createFlowStateFromPublishedRule", () => {
});
});
it("hydrates wizard supportText from published methodSelections", () => {
const customId = "b7c0a9f3-0000-4000-8000-000000000002";
const partial = createFlowStateFromPublishedRule({
id: "rule-custom-desc",
title: "C",
summary: "",
document: {
methodSelections: {
decisionApproaches: [
{
id: customId,
label: "Forum proposals",
supportText: "Anyone can start a thread.",
sections: {
corePrinciple: "",
applicableScope: [],
selectedApplicableScope: [],
stepByStepInstructions: "",
objectionsDeadlocks: "",
},
},
],
},
},
});
expect(partial.customMethodCardMetaById?.[customId]).toEqual({
label: "Forum proposals",
supportText: "Anyone can start a thread.",
});
});
it("sets sections to [] even when methodSelections is missing (edit hydrate)", () => {
const partial = createFlowStateFromPublishedRule({
id: "rule-2",
@@ -248,4 +248,35 @@ describe("parsePublishedDocumentForCommunityRuleDisplay", () => {
},
]);
});
it("shows custom decision description without a seeded 75% consensus row", () => {
const customId = "00000000-0000-4000-8000-000000000170";
const out = parsePublishedDocumentForCommunityRuleDisplay({
sections: [],
methodSelections: {
decisionApproaches: [
{
id: customId,
label: "Forum proposals",
supportText: "Anyone can start a thread.",
sections: {
corePrinciple: "",
applicableScope: [],
selectedApplicableScope: [],
stepByStepInstructions: "",
consensusLevel: 75,
objectionsDeadlocks: "",
},
},
],
},
});
const decision = out.find((s) => s.categoryName === "Decision-making");
expect(decision?.entries).toEqual([
{
title: "Forum proposals",
body: "Anyone can start a thread.",
},
]);
});
});
@@ -0,0 +1,156 @@
import { describe, expect, it } from "vitest";
import {
communityRuleEntryFromMethodChip,
sectionFromDecision,
withoutUnpublishedDecisionConsensus,
} from "../../lib/create/ruleSectionsFromMethodSelections";
const emptyDecisionSections = {
corePrinciple: "",
applicableScope: [] as string[],
selectedApplicableScope: [] as string[],
stepByStepInstructions: "",
consensusLevel: 75,
objectionsDeadlocks: "",
};
describe("withoutUnpublishedDecisionConsensus", () => {
it("keeps catalog consensus when facet copy is present", () => {
const next = withoutUnpublishedDecisionConsensus({
corePrinciple: "Momentum.",
consensusLevel: 100,
});
expect(next.consensusLevel).toBe(100);
});
it("drops seeded consensus on empty custom facet copy", () => {
const next = withoutUnpublishedDecisionConsensus({ ...emptyDecisionSections });
expect(next.consensusLevel).toBeUndefined();
});
it("drops keyed consensus when wizard field blocks exist", () => {
const next = withoutUnpublishedDecisionConsensus(
{ corePrinciple: "Momentum.", consensusLevel: 75 },
[
{
kind: "proportion",
id: "p1",
blockTitle: "Quorum",
defaultPercent: 60,
},
],
);
expect(next.consensusLevel).toBeUndefined();
});
});
describe("communityRuleEntryFromMethodChip", () => {
it("publishes supportText as body when there are no labeled blocks", () => {
expect(
communityRuleEntryFromMethodChip(
"Our process",
{ corePrinciple: "" },
{ corePrinciple: "Core Principle" },
{ supportText: " Members propose in the forum. " },
),
).toEqual({
title: "Our process",
body: "Members propose in the forum.",
});
});
it("keeps supportText alongside wizard blocks", () => {
const entry = communityRuleEntryFromMethodChip(
"Our process",
{ corePrinciple: "" },
{ corePrinciple: "Core Principle" },
{
supportText: "How we decide.",
customFieldBlocks: [
{
kind: "text",
id: "b1",
blockTitle: "Notes",
placeholderText: "Post in #governance.",
},
],
},
);
expect(entry).toEqual({
title: "Our process",
body: "How we decide.",
blocks: [{ label: "Notes", body: "Post in #governance." }],
});
});
});
describe("sectionFromDecision", () => {
it("omits seeded 75% and shows the policy description on a custom card", () => {
const section = sectionFromDecision([
{
id: "00000000-0000-4000-8000-000000000001",
label: "Forum proposals",
supportText: "Anyone can start a thread; silence after a week is consent.",
sections: { ...emptyDecisionSections },
},
]);
expect(section?.categoryName).toBe("Decision-making");
expect(section?.entries).toEqual([
{
title: "Forum proposals",
body: "Anyone can start a thread; silence after a week is consent.",
},
]);
});
it("still emits catalog consensus when facet copy is present", () => {
const section = sectionFromDecision([
{
id: "lazy-consensus",
label: "Lazy Consensus",
sections: {
corePrinciple: "Silence is consent.",
applicableScope: [],
selectedApplicableScope: [],
stepByStepInstructions: "Post a deadline.",
consensusLevel: 100,
objectionsDeadlocks: "Any member can block.",
},
},
]);
const blocks = section?.entries[0]?.blocks ?? [];
expect(blocks).toContainEqual({
label: "Consensus Level",
body: "100%",
});
});
it("does not duplicate consensus when a wizard proportion block exists", () => {
const section = sectionFromDecision(
[
{
id: "00000000-0000-4000-8000-000000000002",
label: "Custom vote",
supportText: "We vote in person.",
sections: {
...emptyDecisionSections,
consensusLevel: 75,
},
},
],
{
"00000000-0000-4000-8000-000000000002": [
{
kind: "proportion",
id: "q",
blockTitle: "Quorum",
defaultPercent: 60,
},
],
},
);
const blocks = section?.entries[0]?.blocks ?? [];
expect(blocks).toEqual([{ label: "Quorum", body: "60%" }]);
expect(section?.entries[0]?.body).toBe("We vote in person.");
});
});