refactor(pm): generalize PM integration layer for plug-and-play extensibility#1109
refactor(pm): generalize PM integration layer for plug-and-play extensibility#1109
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
nhopeatall
left a comment
There was a problem hiding this comment.
Summary
Well-structured incremental refactor that adds generic PM abstractions alongside existing per-provider code. The approach of coexisting with backward-compatible fields is sound. A few design observations worth discussing before this pattern is extended further.
Architecture & Design
-
[SHOULD_FIX]
pmIdentifierderivation duplicates provider-key knowledge: The mapping of provider to identifier key name (trello->boardId,jira->projectKey,linear->teamId) now exists in three places:configRepository.ts(PM_IDENTIFIER_KEYS),router/config.ts(inline if/else chain at lines 108-114), and theconfigMapper.ts(activePmConfigpriority chain). When a new provider is added, all three locations must be updated in lockstep. Consider extracting thePM_IDENTIFIER_KEYSmap to a shared location (e.g.,src/pm/constants.ts) and reusing it inrouter/config.tsto derivepmIdentifiergenerically viapmConfig[PM_IDENTIFIER_KEYS[pmType]]. -
[SHOULD_FIX]
resolveWebhookSecretfallback assumes Trello for unknown providers: WithWebhookPlatformopened from a closed union tostring(Step 9), any unrecognized provider string now falls through to the Trello-specificapi_secretcredential resolution (line 99 ofcredentials.ts). A new PM provider would silently get the wrong credential. The function should have an explicit default that returnsnullfor unknown providers, with Trello handled in its ownifbranch. This is the kind of bug the closed union was protecting against.
Code Issues
Should Fix
src/pm/config.ts:105-119— The fallback branch ingetCostFieldId()aftergetPMConfig()is dead code. SincegetPMConfig()itself falls back to the per-provider fields whenpmConfigis not set, it will return a value whenever any PM config exists — making the per-provider fallback (lines 112-119) unreachable. This isn't a bug (the result is correct either way), but dead code is confusing and should be removed or documented with a comment explaining it's defensive.
Questions
- The new
findProjectByPMIdentifierandloadProjectConfigByPMIdentifierfunctions are exported but have no callers in this PR. Are these intended to be consumed in a follow-up PR? If so, it might be worth mentioning in the PR description which step(s) will consume them.
🕵️ claude-code · claude-opus-4-6 · run details
…, remove dead code - Extract PM_IDENTIFIER_KEYS to src/pm/constants.ts so configRepository.ts and router/config.ts share a single source of truth for the provider→identifier-key mapping; adding a new provider now requires only one edit. - Replace the inline if/else chain in loadProjectConfig() with a generic lookup via PM_IDENTIFIER_KEYS[pmType] to derive pmIdentifier. - Fix resolveWebhookSecret() fallback: unknown providers now return null explicitly instead of silently falling through to the Trello api_secret credential, restoring the safety the closed union previously provided. - Remove unreachable per-provider fallback branches from getCostFieldId(): getPMConfig() already handles the backward-compat fallback internally, making the post-guard branches dead code. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
nhopeatall
left a comment
There was a problem hiding this comment.
Summary
LGTM — Well-structured incremental refactoring that adds generic PM infrastructure alongside existing per-provider code for backward compatibility. All CI checks pass, test coverage is solid, and the changes are additive (no existing behavior is modified).
Notes
Architecture & Design
The approach of laying down generic infrastructure (pmConfig, pmIdentifier, PM_IDENTIFIER_KEYS, PMJob, unified cache/lookup functions) while keeping per-provider code intact is the right strategy for a gradual migration. The PM_IDENTIFIER_KEYS constant centralizes the provider→key mapping, and new providers only need to add an entry there.
A few observations (not blocking):
-
pmConfigobject sharing inconfigMapper.ts:activePmConfigis a direct cast of the input provider config object (same reference), while the per-provider fields (project.trello, etc.) get new copies viabuildTrelloConfig()etc. This isn't a bug because Zod validation creates fresh objects downstream, but it's a subtle inconsistency. A shallow clone ({ ...jiraConfig }) would make the intent clearer. -
WebhookPlatformtype widening: Opening this from a closed union tostringachieves the plug-and-play goal, but it also means typos in platform strings won't be caught at compile time. The risk is minimal since all existing call sites use literal strings, but consider keeping the known values as a documented union (type BuiltInPlatform = ...) alongside the openstringif type safety becomes a concern later. -
resolveWebhookSecretbehavior change (credentials.ts): The old code had an implicit fallback to Trello'sapi_secretfor unknown providers — the new code correctly returnsnull. This is a good fix that eliminates a potential security issue where an unknown provider would silently get Trello credentials.
Code Quality
- SQL query in
pmIdentifierWhereClausecorrectly parameterizes bothproviderandkeyvia drizzle'ssqltemplate tag — no injection risk - Cache key format
${provider}:${identifier}properly isolates entries by provider, and theinvalidate()method correctly clears the new cache getPMConfig()fallback chain is correct and matches the existingpm.typederivation logicgetCostFieldId()refactoring is behaviorally equivalent to the old per-provider branching- Worker
case 'pm'dispatch correctly validates the provider viapmRegistry.getOrNull()before callingprocessPMWebhook() - New functions (
findProjectByPMIdentifier,loadProjectConfigByPMIdentifier) follow existing patterns — caching forfindProject*, no caching forloadProjectConfig* - Tests are well-structured with good edge case coverage (negative cache, provider isolation, invalidation)
🕵️ claude-code · claude-opus-4-6 · run details
|
@aaight add proper tests to hit the coverage target |
…erage target Adds unit tests for the four files flagged by Codecov as under-covered: - tests/unit/worker-entry.test.ts: LinearJob + PMJob dispatch via dispatchJob(), including the pmRegistry lookup path and the unknown-provider error branch - tests/unit/config/provider.test.ts: findProjectByPMIdentifier() (cache hit, miss, negative cache) and loadProjectConfigByPMIdentifier() (delegates to DB) - tests/unit/db/repositories/configRepository.test.ts: findProjectByPMIdentifierFromDb() and findProjectWithConfigByPMIdentifierFromDb() for trello/jira/linear providers and the unknown-provider sql-false path - tests/unit/router/platformClients.test.ts: resolveWebhookSecret() direct tests for all five provider branches (github, jira, sentry, linear, trello) and the unknown-provider null-return branch added in Step 9 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Added tests to cover the four files Codecov flagged as under-covered. All 138 affected tests pass; full suite now has 8053 tests green. What was added:
🕵️ claude-code · claude-sonnet-4-6 · run details |
nhopeatall
left a comment
There was a problem hiding this comment.
Summary
LGTM — Well-structured incremental refactor that adds generic PM abstractions alongside existing per-provider code, maintaining full backward compatibility. All CI checks pass, test coverage is thorough (34+ new test cases), and the implementation is consistent with existing codebase patterns.
Notes
Architecture: The layered approach is sound — each step (generic pmConfig field, unified PM lookup, getPMConfig() accessor, pmIdentifier in router config, PMJob union member, open WebhookPlatform type) builds incrementally and coexists with per-provider code. This avoids a big-bang migration while making the path clear for new providers to plug in.
Key observations verified:
pmIdentifierWhereClausecorrectly passes the JSONB key as a bind parameter — PostgreSQL's->>operator accepts parameterized text on the RHS, so this works identically to the hardcoded per-provider queries.mapProjectRowprecedence foractivePmConfig(jira > linear > trello) matches thepmTypederivation order — no inconsistency.loadProjectConfigByPMIdentifiercorrectly delegates directly to DB without caching, matching the pattern of all otherloadProjectConfigBy*functions.resolveWebhookSecretnow explicitly handles Trello as a named branch instead of the previous fall-through default, and returnsnullfor unknown providers — a defensive improvement over the old behavior of silently using Trello credentials for unknown platforms.getPMConfig()backward-compat fallback correctly derives from per-provider typed fields whenpmConfigis not populated (test fixtures, legacy projects).- The
PMJobworker dispatch properly validates againstpmRegistryand provides a clear error message with registered providers listed.
No blocking or should-fix issues found.
🕵️ claude-code · claude-opus-4-6 · run details
Summary
Implements the core generalization steps of the PM integration refactor to eliminate per-provider branching and make the layer truly plug-and-play.
Card: https://trello.com/c/cW0No7p6/606-refactor-pm-integration-layer-for-plug-and-play-extensibility
Changes
Step 1 — Generic
pmConfigfield (schema.ts,configMapper.ts): AddspmConfig: z.record(z.unknown()).optional()toProjectConfigSchemaand populates it inmapProjectRow()from the active provider's integration config. Coexists with per-provider fields (trello,jira,linear) for backward compatibility.Step 2 — Unified PM lookup (
configRepository.ts,configCache.ts,provider.ts): AddsfindProjectByPMIdentifierFromDb(provider, identifier)andfindProjectWithConfigByPMIdentifierFromDb()using a generic SQL query keyed byPM_IDENTIFIER_KEYS(boardId/projectKey/teamId). AddsgetProjectByPMIdentifier/setProjectByPMIdentifierunified cache inConfigCache. ExportsfindProjectByPMIdentifier()andloadProjectConfigByPMIdentifier()fromprovider.ts.Step 3 — Generic
getPMConfig()accessor (pm/config.ts): AddsgetPMConfig(project)returning the genericpmConfigfield when populated, falling back to per-provider typed fields for backward compat. UpdatesgetCostFieldId()to use this generic path.Step 4 —
pmIdentifierfield inRouterProjectConfig(router/config.ts): AddspmIdentifier?: stringtoRouterProjectConfigand populates it inloadProjectConfig()from the active provider's config (boardId/projectKey/teamId). Adapters can now route bypmIdentifierinstead of per-provider fields.Step 7 — Unified
PMJobtype (router/queue.ts,worker-entry.ts): AddsPMJob { type: 'pm', source: string, ... }to theCascadeJobunion. AddsPMJobDatatoworker-entry.tsand handlescase 'pm':dispatch viapmRegistry.getOrNull(source)→processPMWebhook(). Per-provider job types are retained for Redis backward compat.Step 9 — Open
WebhookPlatformtype (webhookVerification.ts,credentials.ts): ChangesWebhookPlatformfrom a closed union tostringand updatesresolveWebhookSecretparameter type accordingly, allowing new PM providers to plug in without touching these files.Test plan
tsc --noEmit) — no errorstests/unit/pm/config.test.ts—getPMConfig()tests (7 new cases)tests/unit/db/repositories/configMapper.test.ts—pmConfigpopulation tests (5 new cases)tests/unit/config/configCache.test.ts—projectByPMIdentifiercache tests (7 new cases)tests/unit/router/config.test.ts—pmIdentifierfield in RouterProjectConfigtests/unit/router/queue-pm-job.test.ts—PMJobtype and discriminated union tests (5 new cases)🤖 Generated with Claude Code
🕵️ claude-code · claude-sonnet-4-6 · run details