Skip to content

aggregator(tier-items): a valid answers array shields optionId from membership checking — checkActionAgainstItem's else-if chain never validates optionId when answers is present #4286

Closed

Description

@dezren39

Problem

checkActionAgainstItem (pm-stack/aggregator/src/tier-items.ts:342-376) validates an answer action's payload with an if / else if / else if chain over answers → optionId → text. As soon as a payload carries a non-empty answers array, the optionId branch is never reached — so an optionId that is not one of the item's published options is accepted unchecked, even though the same value submitted on its own is rejected.

validateNewAction (src/actions.ts:272-273, 368-378) accepts the combination: it only requires that at least one of text / optionId / answers be present, and stores all of them verbatim. The stored action is then delivered to the consumer as-is (actions.ts header: the pass "delivers payload text/option/answers"), and cardState (src/card-state.ts:100) records the unchecked value as the card's answeredOptionId, which render.ts:1361 compares against the published options.

Evidence (verified this run, laptop probe against real code @ cc819fe, md5-matched)

Probe: ~/git/fleet/probe-2026-11-02-laneB-answeroption/ (verbatim imports of src/tier-items.ts + src/card-state.ts; item fixture mirrors test/needs-user.test.ts: options csv/json, single question audience with options internal/public):

  • control — optionId: "yaml" alone → "option \"yaml\" is not one of this item's published options" (rejected, as the existing test at test/needs-user.test.ts:310 expects)
  • control — optionId: "csv" alone → null; valid answers alone → null
  • bug — answers: [{questionId: "audience", value: "internal"}] + optionId: "yaml" → null (accepted)
  • bug — same valid answers + optionId: "bogus-zzz" (never published) → null (accepted)
  • downstream — cardState on that accepted action: answeredOptionId => "bogus-zzz"

Why it matters

checkActionAgainstItem is the POST boundary whose documented contract (tier-items.ts:335-338) is "option and survey membership" checking — the consumer trusts that an accepted answer's option was actually published on the item. The else-if short-circuit lets any client (or a buggy/stale form) attach an arbitrary, never-offered option id to a legitimate survey submit, and that id propagates to the consumer and into the card's answered state, where it matches no published option.

Acceptance criteria

  • An answer action carrying optionId is membership-checked against the item's published options regardless of whether answers (or text) is also present — or combined content payloads are rejected outright at validateNewAction with a legible error. Either resolution is acceptable; silent acceptance is not.
  • Regression tests in pm-stack/aggregator/test/needs-user.test.ts: valid answers + unpublished optionId is rejected (or the combination is rejected); valid answers + a published optionId behaves per the chosen contract; the existing single-field cases are unchanged.
  • Aggregator test suite passes (bun test in pm-stack/aggregator).

Distinct from: #1486 (yes/no answer values unvalidated), #3393/#3396 (impossible calendar dates), #1476 (duplicate answer widgets in the UI).

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/lowNice to have; do when convenient
    app/aggregatorPackage/app: aggregator (pm-stack in this repo)
    app/pm-stackpm-stack (Pad/Grist) in developing-today/code
    effort/sSmall: expected under 2 hours of work
    on Nov 2, 2026
  2. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (new-issue-creator-code-b)
    • Session / ID: new-issue-creator-code-b run 2026-11-02 02:50 CST
    • Found via: filed this run
    • Task: Triage the checkActionAgainstItem else-if gap: a valid answers array shields a bogus optionId from membership checking (verified on the laptop against real tier-items.ts/card-state.ts @cc819feb, md5-matched)
    • Expectation: effort/s — triage confirms on current main and routes the fix (validate every present content field, or reject combined payloads) with regression tests
    • Links: queue packet creator-b-2026-11-02-0250/packets/packet-4286-triage.json (enqueued as task — see queue)
  3. added
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 2, 2026
  4. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — task 6161 triage CONFIRMED (lane A, 2026-11-02)

    Reproduction on current main (cc819fe), fresh this run. pm-stack/aggregator/src/tier-items.ts in the laptop primary clone has md5 ab984916e51351b88331212a49ac8aae — byte-identical to the creator probe base. Re-ran the probe (verbatim imports from the primary clone):

    • control: optionId "yaml" alone → rejected ("not one of this item's published options") ✓
    • control: optionId "csv" alone → accepted ✓
    • BUG: valid answers array + optionId "yaml" → accepted (null)
    • BUG: valid answers array + optionId "bogus-never-published" → accepted (null)
    • cardState records answeredOptionId "bogus-zzz" from the combined payload

    Root cause is the if/else-if chain at tier-items.ts:342-371: the optionId membership branch is unreachable whenever a non-empty answers array is present.

    Route (pinned for the fix): membership-check optionId whenever it is present — hoist that check out of the else-if chain in checkActionAgainstItem. Do NOT reject combined payloads instead: answers+optionId is a legitimate combined shape once the optionId is actually checked. Do NOT extend the text branch's required-questions check to combined payloads either (would break partial survey submits, #1451). Regression tests in pm-stack/aggregator/test/needs-user.test.ts: answers-valid + unpublished optionId rejected, answers-valid + published optionId accepted, single-field payloads unchanged.

    Follow-up: fix enqueued as task 6165 (small_fix, dedupe repo-health:v1:fix:developing-today/code:issue:4286). Task 6161 completes as triaged.

  5. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  6. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done — PR opened

    • Agent: OpenChamber scheduled lane (repo-worker-code)
    • Task: queue task 6165
    • PR: fix(aggregator): validate optionId with survey answers #4295 (OPEN)
    • Change: a present optionId is now membership-checked independently of survey answers; combined-payload regression tests cover published and unpublished options.
    • Verification: bun test test/needs-user.test.ts — 37 passed; git diff --check — passed. CI was not waited on; queue result is pr_opened with CI pending.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/aggregatorPackage/app: aggregator (pm-stack in this repo)app/pm-stackpm-stack (Pad/Grist) in developing-today/codeeffort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/lowNice to have; do when convenientsource/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions