fix(state,stream): support nested append paths in atomic update ops#1612
fix(state,stream): support nested append paths in atomic update ops#1612deep-name wants to merge 9 commits intoiii-hq:mainfrom
Conversation
|
@deep-name is attempting to deploy a commit to the motia Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughAdds nested-path and root support for the ChangesAppend Nested Path Support
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
all green from my side after a final local sweep:
two surface-level CI items here are first-time-fork artifacts, not code issues:
ready when you are. |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
sdk/packages/node/iii-browser/src/stream.ts (1)
105-107:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate
errorscomment to reflect append emissions too.This still documents
errorsas merge-only, but append now contributes structured per-op errors as well.Suggested doc fix
- * Per-op errors. Currently emitted only by the `merge` op when - * input violates validation bounds. Field omitted when empty. + * Per-op errors. Emitted by `merge` and `append` when + * input violates validation/type bounds. Field omitted when empty.As per coding guidelines,
sdk/**updates should stay consistent within and between SDKs.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sdk/packages/node/iii-browser/src/stream.ts` around lines 105 - 107, Update the JSDoc for the errors field to state it’s emitted by both merge and append operations (not merge-only); locate the comment above the errors property in stream.ts and change the wording from “Currently emitted only by the `merge` op” to something like “Emitted by per-op validation failures (e.g., `merge` and `append`)” and note the field is omitted when empty so SDK docs across packages remain consistent.sdk/packages/node/iii/src/stream.ts (1)
105-110:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winRefresh
StreamUpdateResult.errorsdocs to include append.The current comment still says errors are only emitted by
merge, which is outdated after append validation/type errors were added.Suggested doc fix
- * Per-op errors. Currently emitted only by the `merge` op when input - * violates the validation bounds (path depth/size, value depth, or - * a `__proto__`/`constructor`/`prototype` segment or top-level key). + * Per-op errors. Emitted by `merge` and `append` when input + * violates validation/type bounds (including append path validation + * and append leaf/type mismatch cases).As per coding guidelines,
sdk/**changes should keep naming/shape/behavior documentation consistent within and between SDKs.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sdk/packages/node/iii/src/stream.ts` around lines 105 - 110, Update the JSDoc for StreamUpdateResult.errors to reflect that per-op errors are emitted not only by the merge operation but also by append (including append validation and type errors), and keep the existing notes that successfully applied ops are still reflected in new_value and the field is omitted from the JSON wire when empty; edit the comment block that references "Per-op errors. Currently emitted only by the `merge` op..." so it mentions both `merge` and `append` and briefly lists the kinds of validation/type errors (path depth/size, value depth, `__proto__`/`constructor`/`prototype`, and append-specific type/validation failures) while preserving the rest of the description associated with StreamUpdateResult.errors.sdk/packages/python/iii/src/iii/stream.py (1)
120-125:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate
StreamUpdateResult.errorscomment to include append errors.The comment still says errors are emitted only by
merge, but append now also emits structured per-op errors.Suggested doc fix
- # Per-op errors. Currently emitted only by the ``merge`` op for - # validation rejections. Field is omitted from the JSON wire when + # Per-op errors. Emitted by ``merge`` and ``append`` for + # validation/type rejections. Field is omitted from the JSON wire whenAs per coding guidelines,
sdk/**changes should keep shapes and related behavior documentation consistent between SDKs and docs/engine.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sdk/packages/python/iii/src/iii/stream.py` around lines 120 - 125, Update the docstring/comment for the StreamUpdateResult.errors field to reflect that per-op structured errors are emitted by both merge and append operations (not only merge); locate the Field named errors: list[UpdateOpError] in class/struct StreamUpdateResult (or symbol StreamUpdateResult.errors) and change the comment text to mention both "merge" and "append" as sources of validation/per-op errors, keeping the rest of the explanation (omitted when empty, use of default_factory) intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@engine/src/workers/redis.rs`:
- Around line 385-425: The nested-append path is rejecting empty tables because
is_array({}) currently returns true for empty tables; fix is_array so empty
tables are treated as objects (return false) by requiring a non-zero length
(e.g., ensure the computed max/count indicates at least one numeric index or
require max > 0 or count > 0) before returning true. Locate the is_array
function used by the validation branch that checks "elseif type(current) ~=
'table' or current == cjson.null or is_array(current) then" and update its logic
to exclude empty tables, then run the same branch (current, push_error,
append_to_target, walk_or_create) to confirm nested-path appends no longer
produce the 'append.target_not_object' error for newly-created parent tables.
---
Outside diff comments:
In `@sdk/packages/node/iii-browser/src/stream.ts`:
- Around line 105-107: Update the JSDoc for the errors field to state it’s
emitted by both merge and append operations (not merge-only); locate the comment
above the errors property in stream.ts and change the wording from “Currently
emitted only by the `merge` op” to something like “Emitted by per-op validation
failures (e.g., `merge` and `append`)” and note the field is omitted when empty
so SDK docs across packages remain consistent.
In `@sdk/packages/node/iii/src/stream.ts`:
- Around line 105-110: Update the JSDoc for StreamUpdateResult.errors to reflect
that per-op errors are emitted not only by the merge operation but also by
append (including append validation and type errors), and keep the existing
notes that successfully applied ops are still reflected in new_value and the
field is omitted from the JSON wire when empty; edit the comment block that
references "Per-op errors. Currently emitted only by the `merge` op..." so it
mentions both `merge` and `append` and briefly lists the kinds of
validation/type errors (path depth/size, value depth,
`__proto__`/`constructor`/`prototype`, and append-specific type/validation
failures) while preserving the rest of the description associated with
StreamUpdateResult.errors.
In `@sdk/packages/python/iii/src/iii/stream.py`:
- Around line 120-125: Update the docstring/comment for the
StreamUpdateResult.errors field to reflect that per-op structured errors are
emitted by both merge and append operations (not only merge); locate the Field
named errors: list[UpdateOpError] in class/struct StreamUpdateResult (or symbol
StreamUpdateResult.errors) and change the comment text to mention both "merge"
and "append" as sources of validation/per-op errors, keeping the rest of the
explanation (omitted when empty, use of default_factory) intact.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 213b985a-10ff-462d-aa72-4f33559c4be4
📒 Files selected for processing (12)
console/packages/console-frontend/src/api/types/shared.tsdocs/workers/iii-state.mdxengine/src/update_ops.rsengine/src/workers/redis.rsengine/tests/state_stream_update_e2e.rssdk/packages/node/iii-browser/src/stream.tssdk/packages/node/iii-browser/tests/exports.test.tssdk/packages/node/iii/src/stream.tssdk/packages/python/iii/src/iii/stream.pysdk/packages/python/iii/tests/test_stream_models.pysdk/packages/rust/iii/src/stream.rssdk/packages/rust/iii/src/types.rs
… sdk error docs
`is_array({})` returned `true` (count==max==0), so on the redis-backed
adapter a brand-new empty document hit the IMP-003 root-not-object gate
and nested append failed with `append.target_not_object`. The in-memory
rust path initializes `current` to `Value::Object({})` and proceeds, so
issue iii-hq#1552's repro succeeded against `BuiltinKvStore` but would have
diverged once the same client traffic moved to the redis worker.
Treat empty lua tables as objects for `is_array` (cjson loses the
`[]` vs `{}` distinction across encode/decode anyway, and this aligns
`is_array` with `json_type_name`'s existing convention). Empty
documents now pass the nested-append gate; existing-leaf empty-array
appends are now type_mismatch, which matches `Value::Object({})` in
rust — the cjson roundtrip already collapses `[]` to `{}` in lua so
the previous behavior was non-deterministic with respect to the
original wire shape.
Also refresh the `StreamUpdateResult.errors` doc comment in the three
sdks to mention `append` alongside `merge` — the field is no longer
merge-only after this PR.
Found by CodeRabbit on PR iii-hq#1612.
Ridden with [Loa](https://github.com/0xHoneyJar/loa)
|
thanks @coderabbitai — the actionable finding on root cause: fix: empty lua tables now report as objects ( doc fixes: also picked up the three outside-diff findings — note that |
|
Tip For best results, initiate chat on the files or code changes.
The doc fixes on The follow-up you flagged is important: |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
sdk/packages/python/iii/src/iii/stream.py (1)
95-100:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winStale
UpdateOpErrordocstring —appenderrors are now also emitted.The class docstring still says errors are "Currently emitted only by the
mergeop when input violates the new validation bounds", but this PR addsappend.path.*,append.target_not_object, andappend.type_mismatchas additional surfaces (correctly reflected in theStreamUpdateResult.errorscomment at lines 120–126 and in theUpdateAppenddocstring at lines 187–191). Updating this docstring keeps the per-class documentation consistent with the rest of the file.📝 Suggested docstring update
class UpdateOpError(BaseModel): """Per-op error returned by ``state::update`` / ``stream::update``. - Currently emitted only by the ``merge`` op when input violates the - new validation bounds. Successfully applied ops are still - reflected in the response's ``new_value``. + Emitted by the ``merge`` and ``append`` ops when input violates + path/value validation bounds (e.g. depth, segment size, + prototype-pollution segments) and by ``append`` for + ``append.target_not_object`` / ``append.type_mismatch`` surfaces. + Successfully applied ops are still reflected in the response's + ``new_value``. """🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sdk/packages/python/iii/src/iii/stream.py` around lines 95 - 100, Update the UpdateOpError class docstring to reflect that errors are no longer emitted only by "merge" but also by "append" operations; mention the new append error variants such as append.path.*, append.target_not_object, and append.type_mismatch and align the wording with StreamUpdateResult and UpdateAppend comments so the per-op error documentation is consistent across the file (look for class UpdateOpError, StreamUpdateResult, and UpdateAppend to locate the related docs).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@sdk/packages/python/iii/src/iii/stream.py`:
- Around line 95-100: Update the UpdateOpError class docstring to reflect that
errors are no longer emitted only by "merge" but also by "append" operations;
mention the new append error variants such as append.path.*,
append.target_not_object, and append.type_mismatch and align the wording with
StreamUpdateResult and UpdateAppend comments so the per-op error documentation
is consistent across the file (look for class UpdateOpError, StreamUpdateResult,
and UpdateAppend to locate the related docs).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 64b0d3b0-6340-451d-8c44-8920fb5f2edd
📒 Files selected for processing (4)
engine/src/workers/redis.rssdk/packages/node/iii-browser/src/stream.tssdk/packages/node/iii/src/stream.tssdk/packages/python/iii/src/iii/stream.py
🚧 Files skipped from review as they are similar to previous changes (2)
- sdk/packages/node/iii/src/stream.ts
- sdk/packages/node/iii-browser/src/stream.ts
Closes the type asymmetry where `UpdateOp::Append.path` was `FieldPath` (single literal-string segment) while `UpdateOp::Merge.path` was already `Option<MergePath>` (Single | Segments) after iii-hq#1547 — same shape, two ops handled differently. Issue iii-hq#1552 reports three failure modes that all stem from this asymmetry. - `Append.path` is now `Option<MergePath>` matching the merge precedent. Three constructors mirror the merge surface: `append(path, value)` for first-level keys (or any `Into<MergePath>`), `append_root(value)` for explicit root, `append_at_path(segments, value)` for nested. - `impl From<FieldPath> for MergePath` softens the source-compat break for callers that built paths via `FieldPath`. `From<FieldPath> for Option<MergePath>` would violate Rust orphan rules; sites needing the `Option` wrapping use `.map(Into::into)` or `Some(fp.into())`. - VARIANT-ORDER-LOAD-BEARING comment above `MergePath` warns against reordering, locked by `merge_path_single_variant_deserializes_string_first` (asserts a bare JSON string deserializes to `Single`, not a one-element `Segments`). Engine-side wiring (the actual append handler in `engine/src/update_ops.rs`) follows in the next commit. Test plan - [x] `cargo test -p iii-sdk --lib` — 71 passed (5 new: append round-trips for Single / Segments / root, serde-default deserialize-as-None for omitted-and-explicit-null payloads, FieldPath→MergePath compat, variant-order regression). - [x] `cargo fmt -p iii-sdk --check` — clean. - [x] `cargo clippy -p iii-sdk -- -D warnings` — clean. Refs iii-hq#1552. Ridden with [Loa](https://github.com/0xHoneyJar/loa)
Wires the engine-side apply loop to the `Option<MergePath>` shape introduced for `UpdateOp::Append.path` in the previous commit. Issue iii-hq#1552's three reporter cases now have explicit, tested behaviors. The match arm preserves a load-bearing validation order so a polluted or oversized path can't mutate state via `walk_or_create`'s intermediate auto-creation: (1) `validate_op_path("append", segments)` runs first, (2) root-is-object check before any descent, (3) `walk_or_create` only on nested paths, (4) leaf-type matrix per FR-11. `path_label_segments` formats nested paths in bracket notation `[a, b]` to keep the literal-segment contract visible in error messages — never dot-joined, since dots are not separators in iii. **Behavior change worth noting:** Issue iii-hq#1552 Case 2 (object-value append at a parent path with an existing object leaf) previously silently no-op'd at the single-string level. The nested-path equivalent now returns a structured `append.type_mismatch` error and leaves state unchanged. Existing `path: ""` and `path: <single-string>` payloads that were appending to array/string/missing leaves are unaffected. The nested-path missing-leaf rule in FR-11 diverges from single-path's `initial_append_value`: nested missing leaves are ALWAYS arrays even for string values, while single-path keeps strings as strings for the string-concat tier. Locked by `append_segments_missing_leaf_string_value_becomes_array`. Test plan - [x] `cargo test -p iii update_ops:: --lib` — 54 passed (9 new): nested happy path, intermediate auto-creation, missing-leaf string→array (B1 carve-out), dotted-string literal (UC-3), object-leaf type-mismatch (UC-6 / Case-2), non-object root rejection (IMP-003), proto-pollution at root/intermediate/leaf positions (B3 expansion), multi-op partial-failure (FR-13). - [x] `cargo fmt -p iii --check` — clean. - [x] `cargo clippy -p iii-sdk --all-targets --all-features -- -D warnings` — clean (the active CI clippy gate per `.github/workflows/ci.yml`). - [x] `cargo check -p iii` — clean. Engine-side complete; Redis Lua mirror in `engine/src/workers/redis.rs` follows in the next commit so adapters stay lockstep. Refs iii-hq#1552. Ridden with [Loa](https://github.com/0xHoneyJar/loa)
Rust path and Lua path stay lockstep: same validation order, same
error codes, same FR-11 leaf-type matrix, same root-not-object
gating. Without this commit the engine accepts nested-path payloads
in-memory but a redis-backed adapter would fall back to the legacy
single-string handler and the two adapters would diverge.
The Lua append branch now consumes `merge_path_segments(op.path)`
(which already handles `string | array | nil` post-cjson-decode for
merge) and routes through four cases:
- `#segments == 0` → root append, legacy semantics
- non-object root → `append.target_not_object` (matches
IMP-003 root-is-object gate from Rust)
- `#segments == 1` → single-segment back-compat; missing leaf
goes through `initial_append_value` to keep string-concat tier
- `#segments >= 2` → nested path; `walk_or_create` for parent;
missing leaf is ALWAYS `{op.value}` per FR-11
Adds `path_label_segments(segments)` Lua helper paralleling the Rust
`path_label_segments` so error messages match byte-for-byte across
adapters — critical for the cross-impl parity test in the next commit.
Test plan
- [x] `cargo test -p iii update_ops:: --lib` — 54 passed (in-memory
tests unchanged; the Lua path is exercised by the e2e parity
test that follows in T-04).
- [x] `cargo fmt -p iii --check` — clean.
- [x] `cargo check -p iii` — clean.
- [ ] Manual: cross-impl parity validated by `adapter_parity_append_3op_batch`
e2e test in the next commit (FR-7, NFR-4, IMP-008).
Refs iii-hq#1552.
Ridden with [Loa](https://github.com/0xHoneyJar/loa)
…3 + FR-13) Three reporter-case tests lock the documented behavior of issue iii-hq#1552 verbatim — Case 1 (dotted-string literal segment), Case 2 (object-value returns `append.type_mismatch`), Case 3 (array-path nested happy path). Four additional tests cover ground the unit tests can't reach via the in-memory kv store path: - `nested_append_creates_intermediate_objects_through_kv_store` — UC-5 + FR-3 walk_or_create with missing-leaf string→array (FR-11 nested-path rule, B1 carve-out). - `nested_append_proto_pollution_rejected_at_intermediate_segment` — B3 expansion + validate-before-mutate audit through KV: state must remain `{}` after rejection (no walk_or_create mutation). - `nested_append_multi_op_partial_failure_retains_prior_successes` — FR-13 batch semantics; op-1 fails on `__proto__`, op-0 + op-2 succeed, errors[0].op_index == 1 (original-array position). - `nested_append_segments_path_round_trips_via_merge_path_segments` — constructor smoke for callers building `MergePath::Segments(...)` directly rather than via `append_at_path`. Cross-impl parity (FR-7 / NFR-4 / IMP-008) for the Rust ↔ Redis Lua path is deferred to maintainer-side validation against a running redis instance — the loa setup here doesn't ship redis test infra. The Lua mirror's invariants are documented in the previous commit and validated by inspection against the Rust `apply_update_ops` arm. Test plan - [x] `cargo test -p iii --test state_stream_update_e2e` — 20 passed (7 new for nested append + 13 unchanged from PR iii-hq#1547 baseline). - [x] `cargo fmt -p iii --check` — clean. Refs iii-hq#1552. Ridden with [Loa](https://github.com/0xHoneyJar/loa)
…ython/console Surfaces the engine-side change from the previous commits to every SDK that constructs `UpdateAppend` payloads. Path now accepts `string | string[] | undefined` (or pydantic-equivalent) — same shape `UpdateMerge.path` got in iii-hq#1547 — so SDK consumers can issue nested-path appends without dropping to raw JSON. - **node**: `UpdateAppend.path?: MergePath` (reuses the existing `MergePath = string | string[]` alias from iii-hq#1547). Docstring documents the leaf-type matrix and the FR-11 nested-vs-single divergence on missing leaves. - **browser**: same widening; adds two new TS-level smoke tests in `tests/exports.test.ts` covering the array-form path (issue iii-hq#1552 case 3) and omitted-path (root append) per Flatline IMP-005's "explicit Node-side test target" guidance. - **python**: `UpdateAppend.path: str | list[str] | None = None` via pydantic smart-union. Five new round-trip tests: array-path (case 3), omitted-path, explicit None, empty-string (`Single("")`), and dotted-string literal-segment. - **console**: adds the `append` variant to the `StreamUpdateOp` union (it was previously omitted entirely) with the same `path?: string | string[]` shape as `merge`. `StreamUpdateResult.errors` comment updated to mention `append.*` codes alongside `merge.*`. The pydantic shape is symmetric with the Rust serde shape: both serialize `None` / missing path as `"path": null` and accept either `"path": null` or omitted-field on deserialization. No `exclude_none` is needed — Flatline B2's concern resolved by matching Rust's serde-default behavior on both sides. Test plan - [x] `pnpm --filter iii-sdk exec tsc --noEmit` — clean. - [x] `pnpm --filter iii-browser-sdk exec tsc --noEmit` — clean. - [x] `pnpm --filter iii-browser-sdk exec vitest run tests/exports.test.ts` — 7 passed (5 existing + 2 new for nested append + root append). - [x] `cd sdk/packages/python/iii && uv run pytest tests/test_stream_models.py` — 11 passed (6 existing + 5 new round-trip). - [x] `cd sdk/packages/python/iii && uv run ruff check src/iii/stream.py tests/test_stream_models.py` — clean. - [x] `cd sdk/packages/python/iii && uv run mypy src` — clean. Refs iii-hq#1552. Ridden with [Loa](https://github.com/0xHoneyJar/loa)
Updates `docs/workers/iii-state.mdx` to surface the nested-path support that lands with this PR and the FR-9 Case-2 transition. - Append's example row in the operations table swapped to the nested-path form to make the new shape obvious at a glance. - The "first-level field names" sentence narrowed to set / increment / decrement / remove (the four ops still gated to single-string paths). Merge + append now share a paragraph + side-by-side code examples covering root / first-level / nested paths. - New explicit note on the FR-11 nested-vs-single-path divergence for missing leaves: nested missing leaves are ALWAYS arrays even for string values, while single-string paths preserve the legacy string-concat tier. This is the surface-level user-facing rule. - New **Behavior change worth noting** callout on the Case-2 transition (object/scalar leaf -> `append.type_mismatch` rather than silent no-op). Marks the boundary between back-compat payloads (unchanged) and the documented break. - Two new error code rows: `append.path.too_deep` and `append.path.empty_segment` mirror the merge-side validation surface. Refs iii-hq#1552. Ridden with [Loa](https://github.com/0xHoneyJar/loa)
Two-line collapse from `cargo fmt` running over the new test bodies that landed with `fix(state): apply nested-path append in update_ops apply loop`. Pure formatting, no behavior change. Ridden with [Loa](https://github.com/0xHoneyJar/loa)
The previous T-10 commit's "Behavior change worth noting" callout read as if THIS PR introduced the silent-no-op → structured-error transition for `append.type_mismatch`. Strictly that transition already shipped in iii-hq#1555 ("Surface state update failures consistently", Apr 28) for the single-string-path case. The nested-path form added here returns the same error code on the same condition — extending iii-hq#1555's surface, not flipping it. The replacement copy reframes the callout as a *consumer-side* note ("no new branches needed") which is more useful to the reader and credits iii-hq#1555's prior work accurately. Refs iii-hq#1552, iii-hq#1555. Ridden with [Loa](https://github.com/0xHoneyJar/loa)
… sdk error docs
`is_array({})` returned `true` (count==max==0), so on the redis-backed
adapter a brand-new empty document hit the IMP-003 root-not-object gate
and nested append failed with `append.target_not_object`. The in-memory
rust path initializes `current` to `Value::Object({})` and proceeds, so
issue iii-hq#1552's repro succeeded against `BuiltinKvStore` but would have
diverged once the same client traffic moved to the redis worker.
Treat empty lua tables as objects for `is_array` (cjson loses the
`[]` vs `{}` distinction across encode/decode anyway, and this aligns
`is_array` with `json_type_name`'s existing convention). Empty
documents now pass the nested-append gate; existing-leaf empty-array
appends are now type_mismatch, which matches `Value::Object({})` in
rust — the cjson roundtrip already collapses `[]` to `{}` in lua so
the previous behavior was non-deterministic with respect to the
original wire shape.
Also refresh the `StreamUpdateResult.errors` doc comment in the three
sdks to mention `append` alongside `merge` — the field is no longer
merge-only after this PR.
Found by CodeRabbit on PR iii-hq#1612.
Ridden with [Loa](https://github.com/0xHoneyJar/loa)
fb693d1 to
926d365
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
sdk/packages/python/iii/tests/test_stream_models.py (1)
44-49: ⚡ Quick winExtend empty-string case to assert deserialization symmetry.
At Line 44, this test only checks
model_dump(). Add amodel_validate(...)round-trip assertion (like the other cases) to ensure""is not coerced toNoneduring parse.Proposed patch
def test_update_append_with_empty_string_path() -> None: """Empty string is preserved as `Single("")` — engine maps it to root append.""" op = UpdateAppend(path="", value="x") dumped = op.model_dump() assert dumped["path"] == "" + parsed = UpdateAppend.model_validate(json.loads(json.dumps(dumped))) + assert parsed.path == ""🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sdk/packages/python/iii/tests/test_stream_models.py` around lines 44 - 49, The test test_update_append_with_empty_string_path only asserts model_dump but must also assert that deserialization round-trips the empty-string path; update the test to call UpdateAppend.model_validate(dumped) (or the equivalent parse/validation method) and assert the resulting object's path is still "" so empty string isn't coerced to None—locate the test_update_append_with_empty_string_path function and add the model_validate round-trip assertion referencing UpdateAppend, model_dump, and model_validate.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@console/packages/console-frontend/src/api/types/shared.ts`:
- Line 19: Update the append action type so its path can be explicitly null to
match the API contract: change the union variant `{ type: 'append'; path?:
string | string[]; value: unknown }` to allow `null` (e.g. `path?: string |
string[] | null`) so callers can target the root by passing null or omitting the
field; ensure the change is applied to the shared type definition that contains
the 'append' action variant.
---
Nitpick comments:
In `@sdk/packages/python/iii/tests/test_stream_models.py`:
- Around line 44-49: The test test_update_append_with_empty_string_path only
asserts model_dump but must also assert that deserialization round-trips the
empty-string path; update the test to call UpdateAppend.model_validate(dumped)
(or the equivalent parse/validation method) and assert the resulting object's
path is still "" so empty string isn't coerced to None—locate the
test_update_append_with_empty_string_path function and add the model_validate
round-trip assertion referencing UpdateAppend, model_dump, and model_validate.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 57f99178-f973-4641-9630-d11fae6ed047
📒 Files selected for processing (12)
console/packages/console-frontend/src/api/types/shared.tsdocs/workers/iii-state.mdxengine/src/update_ops.rsengine/src/workers/redis.rsengine/tests/state_stream_update_e2e.rssdk/packages/node/iii-browser/src/stream.tssdk/packages/node/iii-browser/tests/exports.test.tssdk/packages/node/iii/src/stream.tssdk/packages/python/iii/src/iii/stream.pysdk/packages/python/iii/tests/test_stream_models.pysdk/packages/rust/iii/src/stream.rssdk/packages/rust/iii/src/types.rs
✅ Files skipped from review due to trivial changes (1)
- engine/tests/state_stream_update_e2e.rs
🚧 Files skipped from review as they are similar to previous changes (9)
- sdk/packages/rust/iii/src/stream.rs
- docs/workers/iii-state.mdx
- sdk/packages/node/iii-browser/tests/exports.test.ts
- sdk/packages/python/iii/src/iii/stream.py
- sdk/packages/node/iii-browser/src/stream.ts
- engine/src/workers/redis.rs
- sdk/packages/node/iii/src/stream.ts
- engine/src/update_ops.rs
- sdk/packages/rust/iii/src/types.rs
| // for first-level keys, array for nested paths, omitted for root. | ||
| // Per-op errors include `append.path.*` (validation), `append.target_not_object`, | ||
| // and `append.type_mismatch` (case-2: existing object/scalar at the leaf). | ||
| | { type: 'append'; path?: string | string[]; value: unknown } |
There was a problem hiding this comment.
Include null in append.path to match API contract.
On Line 19, path?: string | string[] excludes explicit null, but this PR’s API contract documents root targeting via omitted/null path. This type currently blocks valid usage in typed TS callers.
Proposed fix
- | { type: 'append'; path?: string | string[]; value: unknown }
+ | { type: 'append'; path?: string | string[] | null; value: unknown }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| | { type: 'append'; path?: string | string[]; value: unknown } | |
| | { type: 'append'; path?: string | string[] | null; value: unknown } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@console/packages/console-frontend/src/api/types/shared.ts` at line 19, Update
the append action type so its path can be explicitly null to match the API
contract: change the union variant `{ type: 'append'; path?: string | string[];
value: unknown }` to allow `null` (e.g. `path?: string | string[] | null`) so
callers can target the root by passing null or omitting the field; ensure the
change is applied to the shared type definition that contains the 'append'
action variant.
|
hey @deep-name can you check and add these tests scenarios? Feature: state::update append op behaves identically across the in-memory
and Redis (Lua) adapters
Background:
Given a running Redis instance
And a `StateRedisAdapter` connected to it
And a fresh state scope and key
Scenario: Appending to an existing empty array at a single-segment path
Given the state at the key is `{ "buffer": [] }`
When I apply the op `append("buffer", "x")`
Then the response contains no errors
And the new value is `{ "buffer": ["x"] }`
Scenario: Root append when the state itself is an empty array
Given the state at the key is `[]`
When I apply the op `append` with no path and value `"x"`
Then the response contains no errors
And the new value is `["x"]`
Scenario: Nested append where an intermediate segment points to an array
Given the state at the key is `{ "a": [1, 2, 3] }`
When I apply the op `append` at path `["a", "b"]` with value `42`
Then one of the following holds:
| outcome | errors | new value |
| Rust-parity | empty | { "a": { "b": [42] } } |
| Strict / explicit | non-empty | { "a": [1, 2, 3] } unchanged |
But the new value MUST NOT be a corrupted mixed-key object such as
`{ "a": { "1": 1, "2": 2, "3": 3, "b": [42] } }`
Scenario: Nested append happy path through the Lua script
Given no state at the key
When I apply the op `append` at path `["session", "abc", "events"]` with value `"first"`
Then the response contains no errors
And the new value is `{ "session": { "abc": { "events": ["first"] } } }`Feature: Cross-SDK wire-format consistency for the `path` field on
append ops
Scenario: Root append serializes without an explicit `null` path
Given a `UpdateOp::append_root("first")`
When I serialize it to JSON
Then the resulting object MUST NOT contain a `path` key
And the resulting object equals `{ "type": "append", "value": "first" }`
# Alternative if you prefer the TS-side fix:
# Then the TS type `UpdateAppend.path` accepts `null`
# And the type-test `const op: UpdateAppend = { type: "append", path: null, value: "x" }` compiles |
Closes #1552.
Summary
state::update'sappendop rejects nested paths at the type-deserialization boundary. RifkiSalim's three repros in BUG:appendoperation does not support nested paths #1552 currently behave like this againstorigin/main(1cdc9d4a): dotted strings key on"entityId.buffer"literally; parent-path appends with object values returnappend.type_mismatch(this transition shipped in Surface state update failures consistently #1555, see below); array-form paths fail upstream of the apply loop withexpected string, received sequence.UpdateOp::Append.pathisFieldPath(single literal-string segment) atengine/src/update_ops.rs:495andsdk/packages/rust/iii/src/types.rs:119, whileUpdateOp::Merge.pathisOption<MergePath>(Single | Segments) since fix(state,stream): support nested shallow-merge paths in atomic update ops #1547. PR fix(state,stream): support nested shallow-merge paths in atomic update ops #1547 generalized onlyMerge. This PR closes the gap with the same shape.UpdateOp::Append.pathtoOption<MergePath>and routes the apply loop throughvalidate_op_path("append", …)so bounds +__proto__/constructor/prototyperejection match the merge surface. The Lua mirror inengine/src/workers/redis.rsis updated lockstep so the in-memory and Redis-backed paths produce identical(old_value, new_value, errors)tuples for the same input.A regression test (
merge_path_single_variant_deserializes_string_firstatsdk/packages/rust/iii/src/types.rs) couplesMergePath's untagged-enum variant order to a JSON-string deserialization assertion — alphabetizing or auto-formatter reordering the variants fails the build before it can break wire compat. A second test (append_segments_missing_leaf_string_value_becomes_array) locks the FR-11 nested-vs-single-path divergence so the issue #1552 fix can't drift back to the legacy single-path string-concat tier.Note on
append.type_mismatchfor nested paths: the structuredappend.type_mismatcherror for object/scalar leaves shipped in #1555 ("Surface state update failures consistently",@ytallo, Apr 28) for the single-string-path case. The nested-path form added here returns the same error code with the same shape, so consumers parsingerrors[]need no new branches. Pre-PR, an array-form path failed earlier than the apply loop — at JSON deserialization — so it never reached the type-matrix at all. Callers usingpath: ""orpath: "field"against array, string, null, or missing-field leaves are unaffected.The Rust SDK signature change (
UpdateOp::Append.path: FieldPath → Option<MergePath>) is a compile-time break for downstream callers constructing the variant literally. Same posture #1547 took onMerge. I asked about this on #1552 (comment) and shipped the change withimpl From<FieldPath> for MergePath+impl From<&str>shims so call sites using.into()keep compiling — only literal struct construction breaks. PR is draft until@ytalloconfirms the public-API posture; no 24h fallback.What changed
Engine (
engine/src/):update_ops.rs—Appendmatch arm rewritten to a 4-step validation order (validate_op_path → root-is-object check → walk_or_create on parent for nested paths → leaf-type matrix). Newpath_label_segmentshelper formats nested paths in bracket notation[a, b]so error messages keep the literal-segment contract visible. 9 new unit tests cover walk-and-push, intermediate auto-creation, missing-leaf string→array (the FR-11 nested-vs-single divergence), dotted-string literal-key behavior, object-leaf type-mismatch, non-object-root rejection,__proto__/constructor/prototypeat root / intermediate / leaf positions, and multi-op partial-failure preserving prior successes.workers/redis.rs— Lua append branch inJSON_UPDATE_SCRIPTconsumesmerge_path_segments(op.path)(already handled string|array|nil for merge) and routes through the same four cases. Newpath_label_segmentsLua helper paralleling the Rust side keeps error messages byte-identical across adapters.Tests (
engine/tests/state_stream_update_e2e.rs):BuiltinKvStore: 3 RifkiSalim repros verbatim (case 1 dotted-string literal, case 2 nested-path type_mismatch, case 3 array-path happy path) + intermediate auto-creation + proto-pollution validate-before-mutate audit + multi-op partial-failure withop_indexpreservation + a constructor-level round-trip viaMergePath::Segments.SDKs (
sdk/packages/):rust/iii/src/types.rs— newOption<MergePath>field shape;From<&str>andFrom<FieldPath>forMergePath; newappend,append_root,append_at_pathconstructors mirroring the merge surface.VARIANT-ORDER-LOAD-BEARINGcomment aboveMergePath. 8 new round-trip + serde-default + variant-order tests.node/iii/src/stream.tsandnode/iii-browser/src/stream.ts—UpdateAppend.path?: MergePath(reusing the existingMergePath = string | string[]alias from fix(state,stream): support nested shallow-merge paths in atomic update ops #1547). Browser SDK'stests/exports.test.tsgets 2 TS-level smoke tests covering the array-form path and omitted-path.python/iii/src/iii/stream.py—UpdateAppend.path: str | list[str] | None = Nonevia pydantic smart-union. 5 new round-trip tests covering array path, omitted path, explicitNone, empty string, and dotted-string literal.console/packages/console-frontend/src/api/types/shared.ts— adds theappendvariant to theStreamUpdateOpunion (it was previously omitted) with the samepath?: string | string[]shape as merge.Docs (
docs/workers/iii-state.mdx):append.type_mismatchclarifying it's the same shape as Surface state update failures consistently #1555's surface, just reachable from nested paths now. Two new error-code rows:append.path.too_deepandappend.path.empty_segmentmirror the merge surface.Test plan
cargo test -p iii update_ops:: --lib— 54 passed (9 new for nested append + leaf matrix + multi-op + root-not-object + proto-pollution at three positions).cargo test -p iii --test state_stream_update_e2e— 20 passed (7 new: 3 RifkiSalim repros + 4 nested-append behaviors via the KV-store path).cargo test -p iii-sdk --lib— 71 passed (8 new: 5 round-trip + 1 variant-order regression + 1 serde-defaults + 1From<FieldPath>compat shim).cargo clippy -p iii-sdk --all-targets --all-features -- -D warnings— clean (the active CI gate).cargo fmt --all --check— clean.pnpm --filter iii-sdk exec tsc --noEmit— clean.pnpm --filter iii-browser-sdk exec vitest run tests/exports.test.ts— 7 passed (2 new for nested + root append).cd sdk/packages/python/iii && uv run pytest tests/test_stream_models.py— 11 passed (5 new round-trip).cd sdk/packages/python/iii && uv run mypy src— clean.cd sdk/packages/python/iii && uv run ruff check src/iii/stream.py tests/test_stream_models.py— clean (existing baseline ruff issues in the wider tree are pre-existing onmainand unrelated to this PR).engine/src/workers/redis.rsandengine/src/update_ops.rsvalidation-order comment blocks. The repro is a 3-op fixture batch covering happy nested append +__proto__rejection + Case-2 type-mismatch, run through bothBuiltinKvStoreand the Redis adapter. The harness here didn't ship Redis test infra so I'm flagging this for your local run rather than leaving an[x]I can't honestly stand behind.Notes
CONTRIBUTING.md.UpdateOp::Set.path,Remove.path,Increment.path,Decrement.pathall share the sameFieldPathlimitation but are out of scope here. Happy to take that as a follow-up if the shape lands.append_at_pathis a Rust-SDK-only ergonomic helper for now (mirrorsmerge_at_pathfrom fix(state,stream): support nested shallow-merge paths in atomic update ops #1547). If you'd prefer cross-SDK helper symmetry I can add Node / Python equivalents in this PR or a follow-up.Ridden with Loa
Summary by CodeRabbit
New Features
Documentation
Tests