refactor(server): share the addJob/updateJob schedule guards (#tech-debt) - #1084
Merged
Conversation
…ebt) addJob and updateJob each inlined the same two schedule guards — the invalid-cron throw and the enabled-without-schedule-and-without-continuation throw — with identical message templates and only the resolved label, schedule and continuation flag differing. Extract a module-private assertScheduleValid() taking those three already-resolved values plus `enabled`, so each caller keeps its own resolution policy local and only the two thrown messages are shared. Also tighten the two existing cron-rejection assertions to match the full message including the job label — a probe replacing `label` with a constant previously left the suite green. Behavior unchanged. enableJob's third validateCronExpression call is deliberately excluded: it precedes the cron check with a different guard and a different message, and follows it with validateCronInterval. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
JobService.addJobandJobService.updateJobeach inlined the same two schedule guards:if (schedule && !validateCronExpression(schedule)) throw \Job "${label}" has an invalid cron expression: "${schedule}"``if (enabled && !schedule && !continuationEnabled) throw \Job "${label}" needs a schedule or continuation enabled before it can be enabled.``Both message templates were byte-identical once the label and schedule were substituted. Extracted a module-private
assertScheduleValid({ label, schedule, enabled, continuationEnabled }).Why this is tech debt
Two copies of the same user-facing error text with no shared source. It has already drifted once in spirit — a third caller (
enableJob) carries a variant — and the whole point of the entry is preventing the two remaining copies from drifting apart.What the consolidated sites have in common — precisely
Both call sites pass already-resolved values, so each keeps its own resolution policy inline and unchanged:
addJobupdateJobinput.displayName?.trim() || input.nameinput.displayName ?? existing.nameschedulenextSchedule(falls back toexisting.schedule)input.continuationEnablednextContinuationEnabled(?? existing.continuationEnabled)input.enabledinput.enabledWhat is shared is exactly the two guard conditions and the two throw messages — nothing else moved. This is the same near-miss triage shape #1026 and #1032 used.
One statement reordering in
updateJob:nextContinuationEnabledis now computed before the cron check rather than between the two guards. It is a pure??expression with no side effects and nothing between the old and new positions mutates its inputs, so the observable throw order is unchanged.Deliberately excluded
enableJob(service.ts:813-827) — the thirdvalidateCronExpressioncall, with the identical invalid-cron message template. Left alone because it is a genuine near-miss, not a copy:!schedule && !job.continuationEnabled→"has no schedule configured; enable continuation first."), a different message from the shared one. Routing it through the helper would silently replace that curated message.validateCronInterval, which neither of the other two sites runs.Folding it in would be a behavior change, not a sweep.
Proof the sweep is identity-preserving
Reverting is degenerate (the helper would cease to exist), so eight defects were probed into the new code, each reverted after, against
apps/server/test/jobs/service.test.ts(38 tests):updateJobschedule →nulladdJobcontinuation →trueupdateJobcontinuation →trueaddJoblabel →"PROBE"The label probe passing was the one real gap: both cron-rejection tests asserted only on the substring
"invalid cron expression", never on the job label — and the label is exactly the parameter that differs between the two call sites. Tightened both assertions to the full message ('Job "bad-cron" has an invalid cron expression: "invalid cron"'and theupd-bad-cronequivalent). After that, both label probes (addJobandupdateJob) go red. No new test files; two assertions tightened.git diff --statconfirmed clean restore after every probe.Checks
pnpm run check✓pnpm run test— server 187 files passed / 1 skipped, web 1924 tests, browser-extension 60 ✓pnpm run test:e2e— 198 passed / 12 skipped, green first try (theagent-surfaces.spec.ts:680flake the last run saw did not recur)pnpm run finalize:webnot run — noapps/web/source files changedQuestion for the reviewer
The helper takes a 4-field options object rather than positional params. Labelled alternatives:
boolean | undefined, which are trivially transposable positionally and would transpose silently since both are optional booleans.assertScheduleValid(label, schedule, enabled, continuationEnabled), matching the three-positional-arg shape the backlog entry originally sketched. Shorter call sites, no object literal.Ruling 1308 ("keep explicit short prop lists") is about hook APIs at 5-7 fields and does not obviously govern a 4-param private function, so this is genuinely open.
Queued for the next run
resolveRepoRootx3 inapps/server/src—shared/git/worktree.ts:347andshared/github/pr.ts:167are near-identical 9-line file-private wrappers differing only in the thrown error class.🤖 Generated with Claude Code