Skip to content

grist(update-choice-options): a non-list choices value is silently shredded or crashes the tool — a string choice becomes one-character choices and the read-back verification passes the corruption #6588

Closed

Description

@dezren39

grist(update-choice-options): a non-list choices value is silently shredded or crashes the tool — a string choice becomes one-character choices and the read-back verification passes the corruption

Evidence (verified this run, laptop @ 9123103 == origin/main)

pm-stack/grist/update-choice-options.py extends a Grist Choice column's options. It reads the column's widgetOptions JSON, merges additions, PATCHes the result back, and verifies by read-back. Nothing checks the shape of what came back:

  • merge_choices (:45) does choices = list(merged.get("choices") or []) at :53. If the stored choices is a string, list() shreds it into characters. If it is a list containing non-strings, they pass through unchecked.
  • get_widget_options (:90) returns json.loads(raw) at :95 verbatim — if the stored widgetOptions JSON parses to a non-object (a string, a list, a number), that value flows straight into merge_choices, where dict(widget_options) raises a raw ValueError. run() (:165) catches only TimeoutError, so the tool dies with a traceback instead of its documented clean exit codes (0 ok / 1 API-or-verification failure / 2 usage).
  • The read-back verification in main (:147) compares read-back choices against the merged value — so a corrupted merge verifies as good. It never compares against the original choices' shape.

Probe (laptop, ~/git/fleet/probe-grist-choices/probe.py, real module imported from the primary tree):

  • Control: merge_choices({"choices": ["code", "infra"]}, ["id"]) → ["code", "infra", "id"] ✔
  • merge_choices({"choices": "code"}, ["id"]) → ['c', 'o', 'd', 'e', 'id'] — the existing choice code is destroyed and replaced with four one-character choices; main would PATCH this and the read-back check at :147 would pass it.
  • merge_choices(json.loads('"justastring"'), ["x"]) → uncaught ValueError: dictionary update sequence element #0 has length 1; 2 is required (traceback out of run(), which catches only TimeoutError).
  • merge_choices({"choices": [1, None, True]}, ["id"]) → [1, None, True, "id"] — non-string choices pass through and would be written back as JSON 1 / null / true choices.

Why it matters

This tool is the supported path for extending Choice columns (#1287): it writes to a live Grist doc and its only safety net is the read-back verification — which compares against the possibly-corrupted merge, not the original data. A widgetOptions value whose choices was written by anything else (a Grist UI change, a manual edit, an older/newer Grist version's serialization, a ChoiceList vs Choice confusion) in a non-list shape is silently destroyed on the next routine --add, with the tool reporting success. The non-object case separately breaks the tool's exit-code contract with a raw traceback.

Acceptance criteria

  • merge_choices (or its caller) refuses a widgetOptions that is not a JSON object, and a choices value that is not a list of strings, with a clear named error — never a shredded/coerced write and never a raw traceback.
  • The refusal happens before any PATCH is sent; the stored column is untouched on a shape failure.
  • The CLI surfaces shape failures through its documented exit-code contract (a clean message on stderr and a non-zero exit, like the timeout path in run()).
  • Tests in pm-stack/grist/test_update_choice_options.py pin: string choices, dict choices, non-object widgetOptions, and a list containing non-string entries — plus the existing controls (normal append, idempotence, empty options, duplicate additions, input not mutated) unchanged.
  • python3 -m unittest test_update_choice_options green in pm-stack/grist (laptop or CI).

Activity

  1. developing-today commented on Nov 6, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 6, 2026
  3. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator (Lane B), agent slug new-issue-creator-code-b
    • Session / ID: cron job new-issue-creator-code-b, run 2026-11-06 11:50 CST
    • Found via: filed this run
    • Task: Triage the non-list choices shape defect in pm-stack/grist/update-choice-options.py — a string choices value is shredded by list() and PATCHed back with read-back verification passing, and a non-object widgetOptions crashes with a raw ValueError traceback
    • Expectation: effort/s — done when shape validation refuses non-object widgetOptions and non-list-of-strings choices before any PATCH, with tests pinning the probe cases
    • Links: probe ~/git/fleet/probe-grist-choices on the laptop (real module @ 9123103)
  4. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 6, 2026
  5. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · triage: CONFIRMED

  6. 23 remaining items

  7. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  8. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    Progress from repo-worker-code-i: picked up: script and existing tests read on current main; fix = WidgetOptionsError shape validation in merge_choices + get_widget_options, run() maps it to exit 1 before any PATCH; next: laptop worktree + implementation

  9. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    Progress from repo-worker-code-i: pr_opened: PR for issue #6588 opened (branch queue/fallback-6588-grist-choices-shape, commit fc49b8d); unittest 15/15 green on laptop, pre-fix suite fails (1 failure + 8 errors); next: PR checks + Codex, then merge under standing criteria

  10. added a commit that references this issue on Nov 7, 2026
  11. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

  12. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    Released by repo-worker-code-i (outcome: done). Outcome: fixed_merged. PR #6719 merged (merge commit ae31c5d); issue #6588 auto-closed. update-choice-options.py now validates stored widgetOptions/choices shape via check_widget_options/WidgetOptionsError before any PATCH (string choices refused, not shredded; non-object and invalid-JSON refused by name), surfaced as clean stderr + exit 1 via run(). Tests 15/15 green on laptop; pre-fix suite fails (1 failure + 8 errors). Codex zero findings on PR #6719.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    app/gristpm-stack/gristkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobstatus/triagedTriaged and routed (see agent note)status:donetask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions