fix(serenity): reject empty edit text and modernize edit-path tests#2875
Open
byteclimber wants to merge 3 commits into
Open
fix(serenity): reject empty edit text and modernize edit-path tests#2875byteclimber wants to merge 3 commits into
byteclimber wants to merge 3 commits into
Conversation
Fix 1: parseUpdatePromptBody accepted empty/whitespace text on PATCH (only rejected `undefined`), so an edit could reach renamePrompt with a blank prompt that then got classified/defaulted. Now trims and rejects empty text with a 400, mirroring the create path (normalizePromptInput) including the `|| ''` falsy coercion. Shared with the subworkspace twin, so both edit paths are covered. Fix 2: no behavior change. The always-reclassify on edit is intentional and unavoidable: the handler is not sent the old text and upstream has no GET-by-id, so it cannot detect a no-op text edit, and classification must run before the rename for failure-safety. Documented the rationale with a code comment; added a regression test that a rename returning is_updated:false still writes tags and publishes. Fix 3: modernized the stale handleUpdatePrompt tests (and the three edit-path twins in prompts-subworkspace.test.js) that still stubbed the retired delete+create flow. They failed at runtime and one referenced an undefined `createErr`. They now exercise the rename + replace-mode tag-write flow (409 collision, non-404 rename error, tag-write failure after rename, 404 promptNotFound plus its generic-Error negative). Added the intent tag to the recompute assertions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
This PR will trigger a patch release when merged. |
The feature branch left the dynamic-allocation headroom guard half-wired,
breaking type-check + build and a metering test:
- markets-subworkspace.js generateAndAttachPrompts: the caller threaded a
headroom guard as a 6th arg and the JSDoc documented it as required, but the
function never declared or used it (TS8024 + TS2554). Add the param and front
headroom.ensure({ prompts: texts.size }, { includeDrafted: true }) before the
metered createPromptsByIds write loop.
- prompts-subworkspace.js handleCreatePromptsSubworkspace: removed the redundant
pre-publish ensure that re-created a second headroom guard (no-shadow) and
issued a second getWorkspaceResources read. LLMO-6190 meters ONCE before the
write (the create is the metered write, not publish); the pre-write ensure
already reserves the whole batch, so the pre-publish read was a leftover.
- prompts-subworkspace.test.js: the dynamic-allocation test passed its options
object in the env positional slot instead of the 8th, so the guard was never
enabled; corrected to match the controller/signature.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
Author
|
Folded in the LLMO-6190 headroom base fix (was #2883, now closed) because the two are a mutual dependency — each PR's CI runs the full suite/lint, so neither could go green alone. This branch now also fixes:
Verified locally: type-check clean, lint clean, 223 serenity handler tests passing. |
Completes the LLMO-6190 create-prompts fronting: handleCreatePromptsSubworkspace now passes headroom.retryOnQuota as publishAffected's wrapPublish, so a disguised metered-405 on a project publish gets one bounded top-up+retry per project before being recorded as a failure (the JSDoc on publishAffected already documented this caller contract; the wiring was missing). Reuses the single guard created before the write — no extra guard, no extra read on the success path. Also fixes the stale positional call in the dynamic-allocation-fronting test (options were passed in the env slot instead of the 8th), matching the controller and the sibling test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
byteclimber
requested review from
MysticatBot
and removed request for
MysticatBot
July 22, 2026 09:23
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.
Summary
Three coupled edit-path (
PATCH prompt) fixes in the serenity handlers. Base isfeat/serenity-intent-classification, notmain.Fix 1 — Empty/whitespace text accepted on edit but rejected on create
parseUpdatePromptBodydidString(body.text)with no trim and only rejectedtext === undefined, so a PATCH withtext: ""or" "passed validation, reachedrenamePrompt, and got classified/defaulted into a blank prompt. It now trims and rejects empty text with a400 invalidRequest, mirroring the create path (normalizePromptInput) — including the|| ''coercion so a falsy non-string (null,0,false) is treated as empty too, matching create exactly.parseUpdatePromptBodyis shared withhandleUpdatePromptSubworkspace, so this one fix covers both edit paths.Fix 2 — Redundant reclassification on no-op text edits (no behavior change, by design)
handleUpdatePromptalways reclassifies from the new text and rewrites tags, even when the PATCH does not change the text. Decision after review: no behavior change. The handler is not sent the old text, and the upstream provider has no GET-by-id, so it cannot detect a no-op text edit without an API contract change (client sends old text) — deliberately out of scope. Classification must also run beforerenamePromptfor failure-safety (a classify failure leaves the prompt intact), sorenamePrompt'sis_updatedresult cannot gate it (it comes back too late). This PR documents the rationale with a code comment and adds a regression test proving anis_updated: false(no-op) rename still writes tags and publishes — guarding against a future "optimization" that would break the documented contract.Fix 3 — Modernize stale/broken edit-path tests
The
handleUpdatePromptblock had 7 stale tests (not 3) that still stubbed the retired delete+create flow (deletePromptsByIds/createPromptsByIds); they failed at runtime (transport.renamePrompt is not a function) and one referenced an undefinedcreateErr. Modernized them to the currentrenamePrompt+ replace-modeupdatePromptTagsByIdsflow: in-place edit, drops-falsy/malformed tagIds, 409 collision (throw, no tag write, no publish), non-404 rename error (throw, no tag write), tag-write failure after successful rename (throw, publish never fires, partial-mutation warn logged), and 404promptNotFound— plus a new negative test that a genericErrorwith.status=404does NOT map topromptNotFound. Also added the intent tag to the "recomputes the type tag on edit" assertion. The same stale-test class inprompts-subworkspace.test.js(3 edit-path twins) is fixed too, since this PR is the edit-path fix.Test evidence
prompts.test.js: green (was 8 failing → 0).prompts-subworkspace.test.js: edit-path tests green (3 fixed).npm run lintandnpm run docs:lintclean on the changed files; the changed source (prompts.js) passestype-check.Pre-existing branch breakage (NOT introduced here, out of scope)
The base branch
feat/serenity-intent-classificationis already red from the LLMO-6190 dynamic-allocation work, in files this PR does not touch. CI will show these regardless:eslint:src/support/serenity/handlers/prompts-subworkspace.js:251—headroomno-shadow.tsc(type-check):src/support/serenity/handlers/markets-subworkspace.js:217(stale@param headroom) and:537(Expected 5 arguments, but got 6— a real call-site arg mismatch tocreateHeadroomGuard).dynamic-allocation fronting — retryOnQuota wiring,prompts-subworkspace … dynamic-allocation ON …, andintent-classification.js — observability.These should be fixed on the feature branch (or by the LLMO-6190 owner) separately.
🤖 Generated with Claude Code