Skip to content

fix(router): use event.projectId in Linear resolveProject (closes #1332 miss)#1337

Merged
zbigniewsobiecki merged 1 commit into
devfrom
fix/linear-router-resolveProject-shadow
May 11, 2026
Merged

fix(router): use event.projectId in Linear resolveProject (closes #1332 miss)#1337
zbigniewsobiecki merged 1 commit into
devfrom
fix/linear-router-resolveProject-shadow

Conversation

@zbigniewsobiecki
Copy link
Copy Markdown
Member

Summary

PR #1332 fixed only half the bug. parseWebhook correctly selects the right cascade project (by team + Linear Project scope) and embeds its id on the LinearParsedEvent extension. But the very next call in the router pipeline — adapter.resolveProject(event) at src/router/adapters/linear.ts:227-230discards that selection and re-looks up by teamId with the same .find() first-match pattern, returning the first cascade project in the array. In production, that's cascade — even though parseWebhook had correctly chosen ucho for MNG-638.

Live evidence (2026-05-11): webhook ddcee404 body had data.projectId: '7108c72e-...' (ucho's scope); run d58cd8ab dispatched with projectId=cascade, cloned mongrel-intelligence/cascade, agent correctly identified the workspace/work-item mismatch and filed a friction report. The friction-report path worked end-to-end during a real routing incident — a secondary signal that PRs #1305 / #1311 / #1313 are paying off.

Fix

resolveProject now uses event.projectId (the cascade project id parseWebhook already determined) when present, falling back to teamId lookup only for legacy callers (existing unit tests that construct bare `ParsedWebhookEvent` without the Linear extension).

Comprehensive audit of .find() shadow sites

Site Pattern Risk Action
linear.ts:89 parseWebhook .filter() + candidate selection Fixed in #1332
linear.ts:229 resolveProject .find by teamId Active bug — fixed here This PR
webhookVerification.ts:300 Linear signature .find by teamId None — Linear webhook secrets are team-scoped, all cascade candidates share the same secret None
config/provider.ts:60 findProjectByLinearTeamId DB find + cache Dead code (only test callers) None
JIRA / Trello / GitHub adapter .find sites by projectKey / boardId / repo Naturally unique per cascade project; no operator config triggers ambiguity None

Linear is the only platform where a single backend discriminator (team) can map to multiple cascade projects (each scoped to a different Linear Project within that team).

Test plan

  • npm run lint + typecheck clean
  • All 9307 unit tests pass
  • Extended describe('resolveProject', () => { ... }) block with multi-cascade-project-per-team sub-describe:
    • Returns ucho when event.projectId='ucho' regardless of array order
    • Returns cascade when event.projectId='cascade'
    • Returns null when projectId points at no configured cascade project (fail-closed)
    • Falls back to teamId lookup when event lacks the projectId extension (legacy compat — current bare-event tests still pass)
  • Post-deploy E2E: user re-moves MNG-638 to Todo → webhook routes to ucho (not cascade); implementation agent fires on the ucho project; worker clones `zbigniewsobiecki/ucho`.

Out of scope (defer)

  • JIRA / Trello / GitHub adapter .find()s — same pattern but operators don't typically configure two cascade projects against the same JIRA project key / Trello board / GitHub repo. Safe today; track as a future hardening.
  • Surfacing actual drop reason in webhooklogs show — separate diagnostic gap.

🤖 Generated with Claude Code

… miss)

PR #1332 fixed `parseWebhook` to select the right cascade project by team
+ Linear-Project scope, but missed `resolveProject` at the same file
(line 227-230) which re-looks up by `event.projectIdentifier` (the
teamId) and uses the same `.find()` first-match pattern.

Live evidence from 2026-05-11 prod: webhook ddcee404 body had
`data.projectId: 7108c72e-...` (ucho scope); run d58cd8ab dispatched
with projectId=cascade, cloned mongrel-intelligence/cascade. Agent
correctly identified the workspace/work-item mismatch and filed a
friction report.

Fix: resolveProject uses event.projectId (cascade project id
parseWebhook already determined) when present, falling back to teamId
lookup only for legacy bare-event callers.

Audit of all .find() shadow sites — only Linear resolveProject needed
fixing. webhookVerification.ts Linear lookup is harmless because Linear
webhook secrets are team-scoped. findProjectByLinearTeamId is dead code.
JIRA / Trello / GitHub adapters have naturally unique-per-cascade-project
discriminators.

Tests: extended resolveProject describe block with multi-cascade-project-
per-team sub-describe covering: returns ucho regardless of array order,
returns cascade for cascade events, null fail-closed when projectId is
unconfigured, and legacy bare-event teamId-fallback compat. All 49
LinearRouterAdapter tests pass; full suite (9307 unit tests) clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zbigniewsobiecki zbigniewsobiecki merged commit 91cd4a9 into dev May 11, 2026
8 checks passed
@codecov
Copy link
Copy Markdown

codecov Bot commented May 11, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Collaborator

@nhopeatall nhopeatall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - resolveProject now preserves the Cascade project selected by Linear parseWebhook, which fixes the same-team/multiple-Linear-Project routing miss without changing the legacy bare-event fallback. The added unit coverage exercises both scoped project ids, fail-closed behavior for stale ids, and fallback lookup; CI is green.

🕵️ codex · gpt-5.5 · run details

zbigniewsobiecki added a commit that referenced this pull request May 11, 2026
Third bug in the Linear router-shadow chain. PR #1332 fixed parseWebhook;
PR #1337 fixed resolveProject; this PR fixes the WORKER-side
re-resolution that overrode both router fixes.

Flow before this PR:

  1. Router parseWebhook → selects ucho (correct after #1332)
  2. Router resolveProject → uses event.projectId (correct after #1337)
  3. Router buildJob → embeds projectId=ucho in the LinearJob ✅
  4. Job enqueued
  5. Worker picks up job, calls processLinearWebhook(payload, ...)
     — WITHOUT forwarding jobData.projectId ❌
  6. processPMWebhook calls integration.lookupProject(event.projectIdentifier)
     — which does loadProjectConfigByLinearTeamId(teamId) and returns
     the FIRST cascade project sharing the team. Returns "cascade"
     instead of "ucho". ❌

Live evidence 2026-05-11 after deploys at 09:55Z and 11:30:58Z (both
router fixes live): webhook ddcee404 had data.projectId 7108c72e (ucho
scope), webhook log showed projectId=ucho with "Coalesced dispatch
scheduled". But the resulting run 5aa8f137 had projectId=cascade and
cloned mongrel-intelligence/cascade. Worker re-resolved.

Fix: pass jobData.projectId through the chain:
worker-entry → process{Trello,Jira,Linear}Webhook → processPMWebhook
as a new `preferredProjectId` parameter. processPMWebhook now uses
loadProjectConfigById(preferredProjectId) when set, falling back to
integration.lookupProject for legacy callers that do not pass it.

Trello / JIRA threading is defensive (their discriminators are
naturally unique per cascade project so no current shadow), but the
symmetric signature keeps the architecture consistent and future-
proofs against multi-cascade-project-per-discriminator configs.

Tests:

- tests/unit/pm/webhook-handler.test.ts: new describe block
  "preferredProjectId (router-selected project)" with 4 scenarios:
  uses loadProjectConfigById when set, withCredentials receives the
  router-selected project.id, falls back to lookupProject when unset,
  fail-closes when preferredProjectId resolves to nothing.
- tests/unit/worker-entry.test.ts: updated 9 existing call-shape
  assertions to include the new projectId trailing arg.

Full suite (9311 unit tests) clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

2 participants