Skip to content

aggregator(actions): one answer action may name the same survey question twice — duplicate questionId entries with contradictory values pass validateNewAction and checkActionAgainstItem #4291

Closed

Description

@dezren39

Problem

One answer action may carry the same survey question twice in a single payload.answers array — with contradictory values — and every layer accepts it:

  • validateNewAction (pm-stack/aggregator/src/actions.ts:275-303) converts each answers entry independently (array of at most 50) and never tracks which questionIds it has already seen.
  • checkActionAgainstItem (pm-stack/aggregator/src/tier-items.ts:348-362) checks each entry's membership/value against the published question, again per entry, with no duplicate detection.

The item side enforces the opposite invariant: sanitizeQuestions / sanitizeOptions (tier-items.ts) drop later entries whose id repeats (seen sets). The answer side has no mirror of that rule.

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; item fixture mirrors test/needs-user.test.ts — single question audience, options internal/public):

  • control — answers: [{questionId: "audience", value: "internal"}] → null (accepted, correctly)
  • control — an answer naming an unknown question or an unpublished option value is rejected by the same function (existing tests, test/needs-user.test.ts:316-330)
  • bug — answers: [{questionId: "audience", value: "internal"}, {questionId: "audience", value: "public"}] → null (accepted, both entries stored)

Why it matters

The stored action is self-contradictory, and its two readers resolve the contradiction differently and silently:

  • cardState (src/card-state.ts:89-99) merges answers into a Map per question, latest entry wins — the rendered card shows only public, with no trace that the same submit also said internal.
  • The consumer receives the raw payload (actions.ts header: the orchestration pass "delivers payload text/option/answers like its own classifications") — which of the two values it applies is an accident of its own iteration, not a contract.

A double-submit race, a stale form, or a buggy client can therefore record two opposite answers to a required question in one auditable action, and the dashboard's merged view will disagree with what a consumer reading the payload in order applies first.

Acceptance criteria

  • A single answer action whose answers array names the same questionId more than once is rejected at the boundary — either in validateNewAction or in checkActionAgainstItem — with a legible error naming the duplicated question. (Rejecting, not silently deduping, matches the boundary's fail-legible style; if dedupe is chosen instead, the surviving value must be the one both the card merge and the consumer see, and that choice must be tested.)
  • Regression tests in pm-stack/aggregator/test/needs-user.test.ts: duplicate questionId with the same value and with contradictory values; distinct questions in one submit still pass; partial submits (feat(dashboard): multi-question cards — render every question, submit one answers array #1451) are unaffected.
  • Aggregator test suite passes (bun test in pm-stack/aggregator).

Distinct from: #1476 (the dashboard UI presenting one question through multiple answer widgets — this is the POST boundary accepting duplicates from any client), and from the optionId-shielding gap filed this run as #4286.

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

  3. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  4. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

    Evidence (fresh this shift)

    • Current main d18d61735 (laptop primary, HEAD == origin/main): validateNewAction (pm-stack/aggregator/src/actions.ts:275-303) converts each answers entry independently — array cap 50 only, no seen-questionId set; checkActionAgainstItem (src/tier-items.ts:340+) loops entries with per-entry membership/value checks, no duplicate detection. (The file changed since the creator probe only via the aggregator: proposal expiry is clocked from the delegation's updatedAt, not the proposal's draft time — a 72h-old proposal stays approvable, a 1h-old one reads expired #1623 commit 4d05b191c, unrelated to this function.)
    • Probe rerun on the laptop this shift — probe.ts imports the primary clone's current sources directly: BUG2 duplicate questionId contradictory => null (one action answering audience=internal and audience=public is accepted). Controls correct: unpublished optionId alone is rejected; a valid single answer is accepted.
    • cardState merges answers latest-wins per question, so the card would show only public while a raw-payload consumer sees both values — the silent divergence the issue describes.

    Routing

  5. 4 remaining items

  6. added
    task/small-fixQueue packet kind: bounded fix with reproduction and targeted tests
    and removed on Nov 2, 2026
  7. 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
  8. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Pickup by repo queue worker lane H for task 6167 (small_fix for #4291). Fresh revalidation 2026-11-02T17:12Z: #4291 OPEN, no hard-stop labels, no open PR owns this fix. Implementing the duplicate-questionId rejection at the validation boundary (validateNewAction in src/actions.ts and checkActionAgainstItem in src/tier-items.ts) with regression tests in test/needs-user.test.ts, on branch queue/6167-4291-dup-question-id.

  9. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Implementation by repo queue worker lane H for task 6167: PR #4482 opened (branch queue/6167-4291-dup-question-id, commit 710442b). A duplicate questionId in one answers array is now rejected at both boundaries — validateNewAction (src/actions.ts) and checkActionAgainstItem (src/tier-items.ts) — with a legible error naming the question. Regression tests in test/needs-user.test.ts: 27 pass, 0 fail (bun test on the laptop). Full aggregator suite: 1172 pass; the brief-surfaces failure and the web-push module error are pre-existing on the clean tree (verified via stash). #1451 partial submits across separate actions are unchanged.

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

    @dezren39
    MemberAuthor

    Done by repo queue worker lane H (task 6167): PR #4482 merged (merge commit cefcdce) and #4291 is closed. Codex review on PR #4482 completed with no findings. Verification as recorded on the PR: bun test test/needs-user.test.ts 27 pass / 0 fail on the laptop; the full-suite brief-surfaces failure and web-push module error are pre-existing on the clean tree (stash-verified).

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/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