Public: anyone can read this; the crew can change it.

propose refuses an option that is not a string, rather than coercing it #41

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

ledger/quorum:ledger/17-propose-refuses-an-option-that-is-not-a → main 1 commit fork

  • Merged. ledger/17-propose-refuses-an-option-that-is-not-a was merged into main as b601973.

Review

Sign in to review this change.
3 files changed +24 −2
README.md +3 −1
@@ -112,7 +112,9 @@ it, or `off`. At boot the app prints one line per misconfiguration — the name
112112 of the variable and what it costs, never its value.
113113
114114 A proposal is a title (1–120 characters), a paragraph (1–2000) and two to
115 eight options, each labelled, each different. Its author or an admin can
115 eight options, each labelled, each different. Every option is a line of text:
116 a JSON or MCP caller that sends a number, an object or a list in that array is
117 refused rather than having it coerced into a label. Its author or an admin can
116118 withdraw it while it is open, once.
117119
118120 `rule` says how it will be decided. The proposer picks it when they open the
src/actions.test.ts +16
@@ -82,6 +82,7 @@ test("a proposal has two to eight distinct, labelled options", () => {
8282 ["a blank label", ["Yes", " "]],
8383 ["a label of eighty-one characters", ["Yes", "x".repeat(81)]],
8484 ["nothing at all", []],
85 ["an option that is not a string", [{}, ["x"]]],
8586 ];
8687 for (const [what, options] of bad) {
8788 const r = propose(db, alice, { ...good, options } as typeof good);
@@ -97,6 +98,21 @@ test("a proposal has two to eight distinct, labelled options", () => {
9798 expect(eight.options.length).toBe(8);
9899 });
99100
101 test("an option that is not a line of text is named, not coerced", () => {
102 const r = propose(db, alice, { ...good, options: [{}, ["x"]] } as never);
103 expect(r.ok).toBe(false);
104 if (!r.ok) {
105 expect(r.error).toBe("invalid");
106 expect(r.message).toBe("Every option is a line of text.");
107 }
108 // Said before the count and the duplicates: the shape is the first thing
109 // wrong, and a coerced label is never what the caller meant.
110 for (const options of [[{}], [null, null], [1, 1]]) {
111 const one = propose(db, alice, { ...good, options } as never);
112 expect(!one.ok && one.message).toBe("Every option is a line of text.");
113 }
114 });
115
100116 test("a title is at most a hundred and twenty characters, a paragraph two thousand", () => {
101117 for (const input of [
102118 { title: "" },
src/actions.ts +5 −1
@@ -190,7 +190,11 @@ export function propose(
190190 "invalid",
191191 `A proposal has ${OPTIONS_MIN} to ${OPTIONS_MAX} options, one per line.`,
192192 );
193 const labels = input.options.map((label) => String(label ?? "").trim());
193 // A caller that sends an object or a list where a line of text belongs is
194 // told so, rather than having it coerced into a label nobody wrote.
195 if (input.options.some((label) => typeof label !== "string"))
196 return fail("invalid", "Every option is a line of text.");
197 const labels = input.options.map((label) => String(label).trim());
194198 if (labels.length < OPTIONS_MIN || labels.length > OPTIONS_MAX)
195199 return fail(
196200 "invalid",

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs