fix(workflow): keep cancelled rows as entities so their GraphQL getters resolve - #35
Merged
fantasywind merged 2 commits intoSep 7, 2026
Conversation
…rs resolve `cancelAdhocDirective` spread the loaded directive into a plain object before saving it. The saved value is returned straight to the resolver, and `AdhocDirective.targetValueJson` is a non-nullable field backed by a prototype getter, so the spread dropped the getter and the mutation answered `INTERNAL_SERVER_ERROR` — after the cancellation had already been committed. To the approver the withdraw looks like it failed while the row is in fact `CANCELLED`, so the natural reaction is to retry a directive that is already gone. Reproduced end to end against a deployed instance: the toast reads "Internal server error" and the GraphQL response carries `path: ["cancelAdhocDirective", "targetValueJson"]`, yet the directive reads back as `CANCELLED`. `cancelApprovalInstance` has the same shape and returns an entity with five getter-backed JSON fields, so any caller selecting one of them hits the same failure. Its client wrapper happens to select only `id` and `state`, which is why it has not surfaced yet; the API is reachable by other consumers, so it is fixed here too. Only the directive path carries a regression test. The instance repository stub re-wraps whatever it is handed via `Object.assign(createApprovalInstance(), …)`, so it restores the prototype the production code had dropped and a test written against it passes either way — verified by reverting the fix and watching it stay green. Closing that gap means changing the shared stub, which is left out of a bug fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018STtLMQTp1GVCYFp9GPSfg
… given The mock rebuilt every saved row as `Object.assign(createApprovalInstance(), entity)`, so it handed back an `ApprovalInstanceEntity` even when the caller had spread the row into a plain object — the exact regression the cancel paths guard against was invisible to the suite. `save` now keeps the prototype it was handed, and `findOne` rehydrates, which is what the database actually does: a row read back is always an entity, and only `save` sees the shape the caller passed. Adds the missing `cancelApprovalInstance` case. Both cancel tests were checked by reverting each fix in turn; each one fails alone. 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 breaks today
Withdrawing a pending countersign answers
Internal server error, while thedirective is in fact cancelled. Reproduced end to end on a deployed instance:
Reading the directive back afterwards returns
status: CANCELLED. The writelands; only the reply fails. The approver sees a red toast and retries a
directive that no longer exists.
Why
cancelAdhocDirectivespread the loaded row before saving it:AdhocDirective.targetValueJsonis@Field(() => String)backed by aprototype getter (
adhoc-directive.entity.ts). The spread produces a plainobject, the getter is gone, the non-nullable field cannot resolve, and Apollo
reports it as
INTERNAL_SERVER_ERROR.The sibling writes in this service already avoid it —
requestAdhocCountersignand
configureAdhocNotificationboth go throughrepository.create(...), whichkeeps the prototype.
Scope
Only two call sites both spread-save and return the result to a resolver:
cancelAdhocDirectiveAdhocDirectivecancelApprovalInstanceApprovalInstanceThe other spread-saves in this file either return
voidor hand back are-fetched entity, so they are left alone.
cancelApprovalInstancehas notsurfaced because its client wrapper selects only
idandstate, but themutation is reachable by any consumer, so it is fixed here as well.
The fix uses
Object.assign(new XEntity(), row, { … }), already the idiom inthis file (
workflow-engine.service.tsuses it forTaskEntityin threeplaces).
Tests
returns a cancelled ad-hoc directive that still resolves its GraphQL gettersfails on
mainwith:and passes with the fix. Full suite: 37 suites / 460 tests green,
pnpm typecheckandpnpm lintclean (the 5 lint warnings are pre-existing and inother files).
No regression test for
cancelApprovalInstance. The instance repositorystub re-wraps its argument via
Object.assign(createApprovalInstance(), entity),restoring the prototype the production code had dropped, so such a test passes
with or without the fix — I confirmed that by reverting the fix and watching it
stay green, then deleted the test rather than ship a vacuous one. Making it
testable means teaching the shared stub to preserve what it is given, which
felt out of scope for a bug fix but is worth a look.
Affected versions
The spread has been there since
cancelAdhocDirectivewas introduced in1f56dbb(2026-06-05); 24 released tags carry it, up to and includingv0.13.3. There is no "just upgrade" path.🤖 Generated with Claude Code
https://claude.ai/code/session_018STtLMQTp1GVCYFp9GPSfg