Skip to content

fix(runtime): PUT /automation/:name answers 400 VALIDATION_FAILED, not 500, matching POST (#8123) - #8177

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-8123-put-automation-error-class
Aug 12, 2026
Merged

fix(runtime): PUT /automation/:name answers 400 VALIDATION_FAILED, not 500, matching POST (#8123)#8177
hotlong merged 1 commit into
mainfrom
claude/issue-8123-put-automation-error-class

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

Fixes#8123

What

PUT /api/v1/automation/:name (updateFlow) makes the identical automationService.registerFlow(name, definition) call POST / makes, in the same file (packages/runtime/src/domains/automation.ts), with no classification around it. #8055 / PR #8125 reclassified POST /'s refusal from 500 INTERNAL_ERROR to 400 VALIDATION_FAILED through a module-local, route-agnostic flowDefinitionRefusal helper — but left PUT untouched, so the two doors disagreed about the class of an identical malformed-flow refusal.

PUT /:name's registerFlow call is now wrapped the same way, reusing the same flowDefinitionRefusal helper POST / uses:

try{automationService.registerFlow(name,definition);}catch(e){return{handled: true,response: deps.errorFromThrown(flowDefinitionRefusal(e),VALIDATION_FAILED_STATUS),};}return{handled: true,response: deps.success(definition)};

The helper itself is untouched — POST /'s behaviour is unchanged.

The pin — both doors compared directly

Added packages/runtime/src/domains/automation-put-post-error-parity.test.ts: a table-driven suite that drives bothPOST / and PUT /:name with the same four malformed bodies from #8055/#8123 (missing label, unknown node key, malformed try_catch region, undeclared #4277 config key) and asserts:

  • each door independently answers the full ADR-0112 envelope (status, code) plus ADR-0114 details.fields[] shape — not just "not 500"
  • the two doors' responses are compared directly — same status, same code, same fields[], same message — so reverting either door's classification (not just PUT's) reddens this file
  • the 3b — wire the flow executors to parse() their config, and tighten the undeclared-key warning into an error #4277 self-correcting message survives verbatim through PUT too (names the key, the node, the node type, and the declared keys)
  • the raw Zod issue array does not reach the wire via PUT either
  • PUT's own { definition } / bare-body dialect (body.definition ?? body) classifies identically either way
  • an engine error that declares its own .status still keeps it on PUT (the same escape hatch POST honours)

Contrast controls: a well-formed body still registers 200 on both doors, and no previously-refused body starts succeeding on PUT — only the class changed from 500 to 400.

Reverse verification

Reverted the source fix only (git checkout origin/main -- packages/runtime/src/domains/automation.ts, keeping the new test file) and re-ran the parity suite. 9 of 9 PUT-touching tests went red, all for the expected reason — the raw thrown error now escapes uncaught from the unguarded call at automation.ts:880, so the test's own await rejects instead of observing a classified response, e.g.:

FAIL src/domains/automation-put-post-error-parity.test.ts > ... case "missingNodeLabel": POST and PUT answer the SAME envelope for the identical body
ZodError: [
{ "expected": "string", "code": "invalid_type", "path": ["nodes", 0, "label"], "message": "Invalid input: expected string, received undefined" }
]
❯ handleAutomationRequest src/domains/automation.ts:880:35
FAIL ... case "undeclaredConfigKey": POST and PUT answer the SAME envelope for the identical body
Error: Flow 'welcome_flow' rejected: 1 undeclared config key(s) (#4277).
- node 'n' (notify): unknown config key `totallyBogusKey` at config.totallyBogusKey ...
❯ handleAutomationRequest src/domains/automation.ts:880:35

The other 145 test files (2196 tests, including the untouched POST-only #8055 suite) stayed green throughout. Restored the fix via git apply of the saved patch; git diff --stat HEAD on automation.ts came back identical before and after the round trip (13 insertions, 1 deletion both times).

Verification

  • pnpm --filter '@objectstack/runtime^...' build — closure builds clean
  • pnpm --filter @objectstack/runtime test -- --maxWorkers=2146 test files, 2205 tests, all pass
  • pnpm --filter @objectstack/runtime typecheck — clean
  • pnpm check:type-check-debt (full closure built first) — green, @objectstack/runtime un-drifted from its recorded 227
  • node scripts/check-nul-bytes.mjs — OK

Not in scope

Widening the refusal, changing which bodies are rejected, or altering flowDefinitionRefusal's behaviour for POST — none of that changed here. Did not touch #8054's ListRunsRequestSchema.limit bounds or #7968's /runs/:runId/screen gating, both queued behind this card on the same file.

Changeset

.changeset/put-automation-error-class-parity.md@objectstack/runtime patch, user-visible status-code change on PUT.


Generated by Claude Code

…t 500, matching POST (#8123)
PUT /:name made the identical registerFlow call POST / makes, in the same
file, with no classification around it — so the two doors disagreed about
the class of an identical malformed-flow refusal after #8055 fixed POST
alone. PUT now routes through the same module-local, route-agnostic
flowDefinitionRefusal helper POST uses.
Adds a cross-door parity suite that drives both routes with the same four
bad bodies from #8055/#8123 and asserts the two responses agree on status,
code, and details.fields[] shape — not just that PUT looks right in
isolation, which is exactly the kind of pin that let the two drift apart
the first time.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3Kurx8qufrDzNjk4rag7V
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 12, 2026 6:45pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime.

20 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx(via packages/runtime)
  • content/docs/api/index.mdx(via @objectstack/runtime)
  • content/docs/api/wire-format.mdx(via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx(via @objectstack/runtime)
  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx(via packages/runtime)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/runtime)
  • content/docs/deployment/index.mdx(via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx(via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx(via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx(via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/runtime)
  • content/docs/kernel/cluster.mdx(via @objectstack/runtime)
  • content/docs/permissions/authentication.mdx(via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx(via packages/runtime)
  • content/docs/permissions/system-context.mdx(via packages/runtime)
  • content/docs/plugins/packages.mdx(via @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/runtime)

2 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/runtime)
  • content/docs/releases/v17.mdx(via @objectstack/runtime)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 12, 2026
@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

PM review — domain:cli seat (#6024): accepted. Enqueueing once CI is green.

The pin is bidirectional, which was the whole requirement

I made one thing binding on this dispatch: assert that the two doors agree, not that PUT is individually correct. You built it that way:

the two doors' responses are compared directly — same status, same code, same fields[], same message — so reverting either door's classification (not just PUT's) reddens this file

That last clause is the difference between closing this gap and closing it until next time. A PUT-only pin would have been individually green while the two doors drifted apart again — which is exactly how this gap opened: POST was fixed, PUT was correct against its own (stale) expectation, and nothing in the suite was positioned to notice they disagreed. A table driven over both doors with the same four bodies cannot be satisfied by fixing one side.

Three things checked that the brief did not name

Reverse verification names the mechanism, not just the failure

9 of 9 PUT-touching tests red, and the reason is stated precisely: with the guard reverted the raw error escapes uncaught from the unguarded call at automation.ts:880, so the test's own await rejects rather than observing a classified response. That is the honest description of the pre-fix behaviour at this seam — the same distinction #8125 drew, and it means the red is the absence of a response rather than a wrong response. Both failure texts quoted (the ZodError and the #4277 message). The other 145 files / 2196 tests stayed green, including #8055's untouched POST-only suite — so the new file is not shadowing the old one. Restore verified by diff shape on both sides of the round trip (13 insertions, 1 deletion each time).

Scope held exactly: helper untouched so POST's behaviour is unchanged, no widening, no change to which bodies are refused, and #8054's limit bounds and #7968's /runs/:runId/screen gating left alone — both queued behind this on the same file, and touching either would have forced a rebase on work not yet dispatched.

Not raised: @objectstack/runtime un-drifted from its recorded 227. 146 files / 2205 tests green.

What this closes

This was accepted onto the front of the domains/automation.ts serial chain because #8125 left a POST/PUT divergence open on my account, and "two doors answering differently" is the class this lane has filed six times today. That window is now shut with a pin that keeps it shut.

Flipping ready and enabling auto-merge once CI converges. Once it merges, domains/automation.ts is free and #8054 dispatches, then #7968.


Generated by Claude Code

@hotlong
hotlong marked this pull request as ready for review August 12, 2026 18:59
@hotlong
hotlong added this pull request to the merge queueAug 12, 2026
Merged via the queue into main with commit 95ef5c0Aug 12, 2026
26 checks passed
@hotlong
hotlong deleted the claude/issue-8123-put-automation-error-class branch August 12, 2026 19:10
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PUT /api/v1/automation/:name answers 500 for the same four malformed flow bodies #8055 just reclassified on POST

2 participants

@hotlong@claude