test(runtime,cli): pin the ctx.api and flow-node undeclared-field-write refusals - #15370
Merged
os-litant merged 6 commits intoSep 4, 2026
Merged
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
…usals Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
…x-api-flow-node-undeclared-write-pins
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
…x-api-flow-node-undeclared-write-pins
Contributor
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
This was referenced Sep 4, 2026
os-litant
marked this pull request as ready for review
September 4, 2026 13:47
os-litant
deleted the
claude/issue-14241-ctx-api-flow-node-undeclared-write-pins
branch
September 4, 2026 14:10
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.
Fixes #14241
Pins only — no
packages/runtimeorpackages/clisource change, no lint-rule edit.The three lint messages #13858 rewrote (
hook-body-write-unknown-field,action-body-write-unknown-field, and the gatingflow-node-write-unknown-field,severity: 'error') now assert a runtime behaviour that nothing pinned: the harness thatmeasured it was a scratch and was deleted. The existing driver-split pin corroborates a
different call shape, so if the door moved for the
ctx.apior flow-node pathsspecifically, every gate would stay green while three author-facing sentences quietly went
wrong. This adds the two missing shapes.
What landed
packages/runtime/src/sandbox/undeclared-field-write-driver-split.integration.test.ts— thectx.apihalf, in the existing ruled file's own idiom: twoit.each(FAMILIES)blocks plus acontrol, run on both driver families. A hook body and an action body each issue
ctx.api.object('deal').update({ stagee: 'won' })against a seeded row. Each case asserts theADR-0112 envelope on the error that escapes —
code: 'INVALID_FIELD'andstatus: 400,never a bare
toThrow()— that it arrives as aSandboxErrorcarrying the payload across theVM boundary (the hop #3918 and #7867 each broke once), the refusal message verbatim, and that
nothing landed: the target row untouched, no shadow column, and the write that triggered the
body refused with it.
packages/cli/src/flow-node-undeclared-field-write.integration.test.ts(new) — the flow-nodehalf, through the real
AutomationEngine, the real builtin CRUD node executors, a realObjectQLand a realdriver-sqlon better-sqlite3.create_recordandupdate_recordeachfail the RUN; the step's error is pinned whole,
{ code: 'NODE_FAILURE', message: "create_record(deal) failed: Unknown field 'stagee' on object 'deal'" }— so the reader cansee that the door's
INVALID_FIELDis not what a run reports, only its message survives thefold. No row is created; the row under update keeps its values and grows no shadow column; and
the correctly named field riding in the same
fieldsmap does not land either. The door's ownINVALID_FIELD/ 400 envelope is asserted at the seam the node hands its payload to, which isthe only place in the flow chain where the code and the status are still observable.
The measurement was re-driven, not inherited
All six shapes were driven again in this PR rather than taken from #13858's report, and all six
agree with it:
INVALID_FIELD/ 400,Unknown field 'stagee' on object 'deal', nothingstored, no shadow column on the schemaless family. Nothing here contradicts the card.
Two things the card asked to be confirmed, and one deviation
The location claim holds.
packages/cliis the only package declaring@objectstack/service-automationand both driver families — checked across the elevenpackages that declare service-automation at all.
The flow half runs on one driver family, deliberately. The schemaless witness in this repo
is
@objectstack/driver-memory, whose every declaration is disposed of inscripts/driver-memory-census.ledger.jsonand gated bypnpm check:driver-memory-census(#6664, from #5704 / #5499). A new test consumer there is an unledgered arrival: admitting one
is a maintainer ruling and would additionally force rewriting the census sentence in both
existing ruled files, so it is not a test author's call. The CLI's own ledger entry records
that the CLI imports the driver nowhere, and it still does.
What replaces the second arm is stronger than a second sample, and is why this is not a
sacrifice: the lint prose's "identically on every datasource" is true because no driver is
reached, so the flow file counts every write verb the real driver is asked to perform and
asserts zero on each refusal case. A family split cannot arise below a door nothing gets
past. The schemaless persistence half, for the caller-payload shape a flow node's
fieldsmapliterally is, stays pinned by the ruled runtime file's existing caller-payload block — on both
families, including the no-shadow-column assertion. Both files say all of this in place.
The same constraint is why the
ctx.apihalf extends the existing ruled file instead oflanding in a sibling: a sibling would need its own
driver-memoryimport, i.e. the sameunledgered arrival.
check:driver-memory-censusis green.Verification
Ablation, to show the pins can fail rather than only that they pass. With the implementation
committed first,
stageewas declared on the fixture in both files — the one input the door'sverdict reads — the mutation was confirmed on disk by grep of the injected text plus a
git hash-objectcomparison against the HEAD blobs, and both files were re-run. No rebuild wasneeded or done: the mutated text is each test file's own fixture, and the subject under test
(
undeclaredWriteFieldErrorsin@objectstack/objectql) is unchanged indist, which wasbuilt beforehand.
Every refusal case went red on both families; only the controls stayed green, which is correct
— they do not depend on
stageebeing undeclared. The restore leg was proved, not assumed:git diff HEADempty, both blob hashes equal to their HEAD blobs, and the injected marker gone(grep count 0). The mutation script carried a
trap ... EXIT INT TERMwith absolute paths.Gate union derived at the final HEAD with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandson a cleantree — never a hand-written list — and re-derived after each merge with
main; the union wasidentical across all three derivations. 37 / 37 exit 0 at
1c8e95e5, each exit codecaptured before any pipe. Plus, off the derived union but directly relevant to this change:
pnpm check:driver-memory-censusgreen.One derived gate was genuinely moved by this change and was fixed rather than baselined:
check:query-options-erasurecounted the five newas anyoptions bags in test code(236 to 241 sites). They are ordinary
wherebags with no reason to be erased, so both filesnow read them through typed
EngineQueryOptionshelpers and the ratchet holds at its ceiling,236, with no baseline edit.
Also at
1c8e95e5:pnpm --filter @objectstack/runtime --filter @objectstack/cli run typecheckgreen, including both
check:test-typechecklegs — which matters here, becausepackages/runtime/tsconfig.jsonexcludes**/*.test.ts, sotsc --noEmitalone would havesaid nothing about the edited file;
tsconfig.test.jsonis the program that reads it.skip-changeset: this PR is test files only and publishes nothing from any package.Generated by Claude Code