Skip to content

fix: require caller-chosen project for addNote support tickets - #46

Open
detail-app[bot] wants to merge 1 commit into
mainfrom
detail/bug-fix/fix-require-caller-chosen-project-for-addnote-supp-6de2de
Open

detail-app[bot] wants to merge 1 commit into
mainfrom
detail/bug-fix/fix-require-caller-chosen-project-for-addnote-supp-6de2de

Conversation

@detail-app

@detail-app detail-app Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Detail bug report: View on Detail

Bug

NoteResolver.addNote (@Authorized(ADMIN, MANAGER)) optionally files a Linear support ticket via createSupportTicket, which runs a per-(project, type) dedup guard and embeds the project's mentors/Slack/GitHub-issue/event/description in the ticket. The resolver had no projectId argument and passed student.projects[0] (loaded via a relation include with no orderBy/where/take) into both the dedup guard and the ticket body.

For a student on more than one project (reachable through the addProjectStudent mutation, which connects without disconnecting prior projects), this selects a project not chosen by the caller:

  • Silent misattribution — a Linear ticket is filed against the wrong project's context (mentors, Slack channel, GitHub issue, event), while the student is correctly named and no Linear URL/ID is returned in-band for review.
  • False dedup block — if the not-chosen project already has an open ticket of that type, createSupportTicket throws and the note is never persisted, silently dropping a legitimate first-time escalation for the manager's intended project (with a message indistinguishable from a real duplicate).

Fix

Make project selection caller-chosen and fail closed when ambiguous, mirroring SupportTicketResolver.createSupportTicket which already takes an explicit projectId:

  • Added an optional @Arg('projectId', () => String, { nullable: true }) projectId?: string to addNote.
  • Extracted the selection rule to a pure helper src/linear/selectProjectForSupportTicket.ts (following the existing src/linear/ pure-function convention):
    • projectId supplied → return the member's matching project, else throw Student is not a member of the specified project.
    • exactly one project → return it (unchanged designed-matching case)
    • more than one project and no projectId → throw Student is on multiple projects; projectId is required to file a support ticket. (distinct from the dedup message, so the manager knows to disambiguate rather than seeing a misleading duplicate error)
  • The resolver's outer if (supportTicketType && student.projects.length > 0) guard is preserved, so a student on zero projects still skips ticket filing and persists the note.

The dedup guard (hasExistingIssue, keyed by searchLabel(type, project) = sha256(project.id,type)) and the ticket's project-context block now both target the caller-chosen project.

Testing

  • Added offline unit tests for the pure helper (tests/testSelectProjectForSupportTicket.ts, node:test + node:assert/strict, matching the convention in tests/testSlackReporting.ts) covering: single-project (regression), multi-project fail-closed (order-independent), caller-chosen selection, invalid-projectId membership error, identical-instance return, and the empty-list contract. All 12 pass via node --test --require ts-node/register/transpile-only.
  • End-to-end verification of the resolver wiring against a fake Prisma and a fake Linear client (injected through the typedi Container) confirmed: caller-chosen projectId files against the chosen project's mentors/issue-URL/Slack; the dedup guard aims at the chosen project (a chosen project without an open ticket files while one with an open ticket blocks); multi-project with no projectId fails closed with the ambiguity error (not the dedup message) and persists neither ticket nor note; membership errors throw before any Linear call; and single-project, zero-project, and no-supportTicketType cases behave as before (note always persisted). This script was run during development but not versioned.
  • A live-DB GraphQL smoke (real type-graphql schema + real authChecker, seeded Postgres via prisma db push) confirmed the Linear-independent paths reproduce live: the ambiguity error and the membership error surface verbatim, omitting supportTicketType persists the note with no ticket, and a valid projectId proceeds past selection to the Linear boundary.
  • Could not fully verify the Linear-network paths: createSupportTicket/hasExistingIssue make a live Linear API call that rejected with Authentication required, not authenticated... against the dummy LINEAR_API_KEY in the test environment. Completing that round-trip (filing against the chosen project; a project-A open ticket blocking projectId=A but not projectId=B) requires real LINEAR_API_KEY/LINEAR_TEAM_ID/LINEAR_PROBLEM_LABEL_ID/LINEAR_BLOCKING_LABEL_ID. The per-chosen-project dedup behavior was otherwise covered by the fake-Linear end-to-end run above.
  • Routine checks: typecheck (npx tsc --skipLibCheck --noEmit) and the pre-existing tests/testSlackReporting.ts and src/automation/tasks/syncAlumniInteractions.test.ts all pass. (Note: npx eslint is currently broken in the repo due to an @typescript-eslint/parser v3 vs TypeScript 5.2 incompatibility — DeprecationError: 'originalKeywordKind' — and is not run by CI.)

Automatic Fixes PRs can be configured here.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant