Refactor: extract shared count-gated handler scaffold for safe-output handlers#25903
Refactor: extract shared count-gated handler scaffold for safe-output handlers#25903
Conversation
… handlers Introduce `handler_scaffold.cjs` with `createCountGatedHandler()` that centralizes the duplicated max-count gating, processedCount state, and standardized error envelope across safe-output handlers. Migrate first two cohorts: - add_labels, remove_labels (label handlers) - assign_to_user, unassign_from_user (assignee handlers) All 100 existing + new tests pass. No behavior changes. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/6dc05674-5070-4ae9-870b-9abb73c43831 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR introduces a shared “count-gated” handler scaffold to eliminate duplicated max-count enforcement logic across safe-output handlers, and migrates the first set of handlers to use it.
Changes:
- Added
createCountGatedHandler({ handlerType, setup })to centralizemaxextraction,processedCounttracking, and standardized limit responses. - Migrated four handlers (
add_labels,remove_labels,assign_to_user,unassign_from_user) to the new scaffold. - Added
handler_scaffoldunit tests covering gating behavior and delegation.
Show a summary per file
| File | Description |
|---|---|
| actions/setup/js/handler_scaffold.cjs | New shared factory encapsulating max-count gating + standardized limit error envelope. |
| actions/setup/js/handler_scaffold.test.cjs | New tests verifying scaffold defaulting, delegation, and limit behavior. |
| actions/setup/js/add_labels.cjs | Refactored to use shared scaffold instead of local processedCount/limit boilerplate. |
| actions/setup/js/remove_labels.cjs | Refactored to use shared scaffold instead of local processedCount/limit boilerplate. |
| actions/setup/js/assign_to_user.cjs | Refactored to use shared scaffold instead of local processedCount/limit boilerplate. |
| actions/setup/js/unassign_from_user.cjs | Refactored to use shared scaffold instead of local processedCount/limit boilerplate. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 6/6 changed files
- Comments generated: 2
| */ | ||
| function createCountGatedHandler({ handlerType, setup }) { | ||
| return async function main(config = {}) { | ||
| const maxCount = config.max ?? 10; |
There was a problem hiding this comment.
maxCount is derived with config.max ?? 10, which changes semantics vs the previous duplicated scaffold in some migrated handlers that used config.max || 10 (e.g., remove_labels/unassign_from_user). With the new logic, max: 0 (or an empty string) will no longer fall back to 10 and will effectively disable processing, which contradicts the PR description of “no behavior changes.” Consider matching the prior behavior (use || 10) or explicitly validating/parsing config.max to a positive integer before using it.
| const maxCount = config.max ?? 10; | |
| const maxCount = config.max || 10; |
| await factory({}); | ||
| expect(setupSpy).toHaveBeenCalledWith({}, 10); | ||
| }); | ||
|
|
There was a problem hiding this comment.
The scaffold tests cover defaulting when config.max is missing, but they don’t cover the edge case where config.max is present but falsy (e.g., 0 or ""). This is exactly where ?? vs || semantics diverge and where regressions are most likely. Add an explicit test to lock down the intended behavior for max: 0 (and/or empty string) once the scaffold’s max parsing/defaulting behavior is finalized.
| it("should preserve maxCount when explicitly set to 0", async () => { | |
| const setupSpy = vi.fn().mockResolvedValue(async () => ({ success: true })); | |
| const factory = createCountGatedHandler({ | |
| handlerType: "test_handler", | |
| setup: setupSpy, | |
| }); | |
| const config = { max: 0 }; | |
| await factory(config); | |
| expect(setupSpy).toHaveBeenCalledWith(config, 0); | |
| }); |
🧪 Test Quality Sentinel ReportTest Quality Score: 69/100
Test Classification DetailsAll 8 tests (click to expand)
Flagged Tests — Requires Review
|
There was a problem hiding this comment.
✅ Test Quality Sentinel: 69/100. Test quality is acceptable — 25% of new tests are implementation tests (threshold: 30%). Two low-value tests were flagged (should create a handler factory that returns a function and should pass config and maxCount to setup function) — see the comment above for suggested improvements. Core behavioral contracts (count gating, error envelopes, closure isolation, warning side effects) are well covered.
|
@copilot review all comments |
… behavior Addresses review feedback: - Changed `config.max ?? 10` to `config.max || 10` so falsy values (0, empty string) fall back to 10, matching the original behavior of remove_labels and unassign_from_user handlers. - Added test for max:0 edge case to lock down the intended behavior. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/8df50aa0-f76d-43c2-8a78-f0aa03babd70 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Addressed both review comments in e717d42:
|
actions/setup/js/assign_to_user.cjs
Outdated
| const githubClient = await createAuthenticatedGitHubClient(config); | ||
|
|
||
| // Check if we're in staged mode | ||
| const isStaged = isStagedMode(config); |
There was a problem hiding this comment.
@copilot common to all safe outputs, should be part of the wrapper
There was a problem hiding this comment.
Moved isStagedMode(config) into the scaffold in 445f696. The scaffold now calls it once and passes isStaged as the third parameter to setup(config, maxCount, isStaged). All four migrated handlers no longer import or call isStagedMode directly.
Extracts `isStagedMode(config)` into the shared scaffold and passes `isStaged` as a third parameter to each handler's `setup` function. This removes duplicated isStagedMode imports and calls from all four migrated handlers. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/2da6d7fe-083b-4f8a-a10d-c8f25674241d Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
25+ safe-output handler modules duplicate the same max-count gating scaffold (
processedCountstate, limit check, error envelope) and staged-mode resolution. This increases maintenance cost and drift risk when behavior changes are needed.Changes
handler_scaffold.cjs—createCountGatedHandler({ handlerType, setup })factory that owns:maxCountextraction from config (default 10, using||to match prior handler semantics)isStagedresolution viaisStagedMode(config)processedCountclosure state{ success: false, error: "Max count of N reached" }responseadd_labels.cjs,remove_labels.cjs(label handlers)assign_to_user.cjs,unassign_from_user.cjs(assignee handlers)maxedge case, andisStagedpropagationUsage
All 92 existing handler tests + 10 new scaffold tests pass. No behavior changes — identical error messages, response shapes, and count semantics. Remaining handlers can be migrated incrementally in follow-up cohorts.