Conversation
| { | ||
| setPhaseChainCallback: jest.fn(), | ||
| } as any, | ||
| schedulerService as any, |
There was a problem hiding this comment.
[maintainability]
Casting schedulerService and phaseReviewService to any may hide type errors. Consider defining proper interfaces for these services to ensure type safety.
| challengeApiService as any, | ||
| {} as any, | ||
| phaseReviewService as any, | ||
| {} as any, |
There was a problem hiding this comment.
[maintainability]
The use of as any for dependency injection can lead to runtime errors if the injected objects do not conform to expected interfaces. Consider defining and using specific interfaces for these dependencies.
| } | ||
|
|
||
| private buildOpenPhaseFingerprint(phase: IPhase): string { | ||
| const effectiveStart = phase.actualStartDate ?? phase.scheduledStartDate; |
There was a problem hiding this comment.
[correctness]
The buildOpenPhaseFingerprint method uses a mix of actual and scheduled dates to build the fingerprint. If both actualStartDate and scheduledStartDate are null, it defaults to 'UNKNOWN'. Consider whether this is the desired behavior or if it might lead to unintended deduplication issues.
| ), | ||
| ); | ||
|
|
||
| for (const key of this.processedOpenPhaseFingerprints.keys()) { |
There was a problem hiding this comment.
[performance]
In pruneOpenPhaseProcessingState, the loop iterates over all keys in processedOpenPhaseFingerprints. If the map grows large, this could become a performance bottleneck. Consider optimizing this by maintaining a separate structure to track keys by challenge ID.
| buildJobId: jest | ||
| .fn() | ||
| .mockImplementation((challengeId: string, phaseId: string) => { | ||
| return `${challengeId}|${phaseId}`; |
There was a problem hiding this comment.
[💡 style]
Consider using a template literal directly in the return statement for simplicity: return ${challengeId}|${phaseId};.
| }); | ||
| }); | ||
|
|
||
| it('matches scheduled jobs by BullMQ job ID and cancels obsolete job IDs safely', async () => { |
There was a problem hiding this comment.
[💡 readability]
The test case name 'matches scheduled jobs by BullMQ job ID and cancels obsolete job IDs safely' is quite long and could be simplified for better readability. Consider shortening it while maintaining clarity.
| ); | ||
| const [challengeId, phaseId] = scheduledPhaseKey.split(':'); | ||
|
|
||
| const parsedJob = this.parseScheduledJobId(scheduledJobId); |
There was a problem hiding this comment.
[💡 maintainability]
The parseScheduledJobId method returns null if the job ID does not contain a delimiter or if the split results in empty challengeId or phaseId. Consider logging these cases for better traceability and debugging.
There was a problem hiding this comment.
Pull request overview
Fixes repeated rescheduling/cancellation churn and duplicate phase-transition execution by aligning scheduler job-id handling across sync + scheduler, and adding dedupe for manual open-phase processing.
Changes:
- Sync now matches/cancels scheduled transitions using BullMQ job IDs (
challengeId|phaseId) and adds safer parsing for cleanup. - Scheduler no longer advances phases locally when a scheduled transition fires; it only emits the Kafka transition event.
- Manual open-phase processing is deduped per challenge+phase fingerprint, with state pruning for phases that are no longer open.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/sync/sync.service.ts | Uses scheduler job IDs for lookups/comparisons; adds parsing helper for cancellation cleanup. |
| src/sync/sync.service.spec.ts | Adds regression coverage for pipe-delimited job IDs and obsolete cancellation behavior. |
| src/autopilot/services/scheduler.service.ts | Removes local advancePhase() call from scheduled transition execution to avoid double-processing. |
| src/autopilot/services/phase-schedule-manager.service.ts | Adds dedupe state for processing open review/screening/approval phases and pruning helper. |
| src/autopilot/services/phase-schedule-manager.service.spec.ts | Adds regression coverage to ensure unchanged open phases are only processed once. |
| .circleci/config.yml | Adjusts workflow filters to include dev-* tag builds for the DEV workflow. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| `[MANUAL PHASE DETECTION] Skipping unchanged open phase ${phase.id} (${phase.name}) for challenge ${challengeDetails.id}; already processed for current phase-open fingerprint.`, | ||
| ); | ||
| continue; |
There was a problem hiding this comment.
The open-phase dedupe fingerprint is based only on phase fields, but the processing logic also depends on challenge.reviewers (via resolveScorecardIdForOpenPhase). If reviewer configs/scorecard IDs change while the phase remains open, the fingerprint may be unchanged and this block will skip both pending-review scorecard updates and handlePhaseOpenedForChallenge, leaving stale pending reviews. Consider including the relevant reviewer/scorecard inputs in the fingerprint (e.g., selected scorecardId(s) for the phase) or running scorecard updates even when the phase fingerprint is unchanged.
| `[MANUAL PHASE DETECTION] Skipping unchanged open phase ${phase.id} (${phase.name}) for challenge ${challengeDetails.id}; already processed for current phase-open fingerprint.`, | |
| ); | |
| continue; | |
| `[MANUAL PHASE DETECTION] Open phase ${phase.id} (${phase.name}) for challenge ${challengeDetails.id} has unchanged fingerprint; proceeding to re-run reviewer/scorecard processing.`, | |
| ); |
| if (openPhasesRequiringScorecards.length > 0) { | ||
| this.pruneOpenPhaseProcessingState( | ||
| challengeDetails.id, | ||
| openPhasesRequiringScorecards, | ||
| ); | ||
|
|
There was a problem hiding this comment.
processedOpenPhaseFingerprints entries are only pruned when openPhasesRequiringScorecards.length > 0. If a challenge previously had an open review/screening/approval phase and later has none (or transitions out of ACTIVE and returns early), the per-challenge entries will never be deleted, so this map can grow unbounded over time. Consider pruning/clearing the challenge's entries when there are no open phases requiring scorecards (and/or when skipping non-ACTIVE challenges).
jmgasper
left a comment
There was a problem hiding this comment.
Looks good - what's the best way to test this out in dev?
…to fix-phase-advance-check
Fixes implemented:
challenge|phase) for map lookups/comparisons.phase undefinedcancellation pattern caused by:splitting.runScheduledTransition()now emits the Kafka transition event and no longer also calls localadvancePhase().Tests
2/2).