Skip to content

Add JsonReadHelpers and WslShellQuoting canonical helpers (additive)#994

Open
bkudiess wants to merge 1 commit into
mainfrom
bkudiess-json-read-helpers-and-wsl-quoting
Open

Add JsonReadHelpers and WslShellQuoting canonical helpers (additive)#994
bkudiess wants to merge 1 commit into
mainfrom
bkudiess-json-read-helpers-and-wsl-quoting

Conversation

@bkudiess

Copy link
Copy Markdown
Collaborator

Summary

Additive PR 2 of the architecture refactor. It establishes two canonical, single-source helpers that were previously copy-pasted across the codebase, with thorough tests and ledger updates.

  • JsonReadHelpers (src/OpenClaw.Shared/JsonReadHelpers.cs, internal static) — the canonical home for the non-nullable, fallback-returning family of JsonElement coercion helpers: GetString, TryGetString, GetInt/GetLong/GetDouble, GetArrayLength, FirstNonEmpty. All accessors are total (never throw) — a missing property, JSON null, wrong JsonValueKind, or non-object parent yields the documented fallback, folding in the defensive GetStringSafe non-object guard.
  • WslShellQuoting (src/OpenClaw.Shared/WslShellQuoting.cs, internal static) — POSIX/WSL single-quote quoting using the '\'' idiom, kept deliberately separate from the cmd.exe/PowerShell ShellQuoting (which doubles single quotes — wrong and injection-unsafe for WSL command lines). Two distinct operations: EscapePosixSingleQuoteInner (escape-only) and QuotePosixSingleQuote (fully-wrapped token).
  • Proof-of-adoption routing: OpenClawGatewayClient.ParseUsageCost now uses JsonReadHelpers.GetInt (3 sites); the duplicate private GetInt is removed. Behavior-identical — those payloads are always JSON objects.
  • Ledger (docs/ARCHITECTURE.md): json-read-helpers and wsl-posix-quoting moved plannedauthoritative with real behavioral guard tests; single-source-owners table and the SetupSteps "extract-toward" guidance updated.

PR 2 is additive only. The SetupSteps.cs POSIX-quoting call-site migration is intentionally deferred to PR 3. (SetupSteps currently has two same-named ShellEscape methods with divergent wrap semantics — exactly the divergence WslShellQuoting retires.)

Scope of the canonical contract (intentionally narrow)

JsonReadHelpers canonicalizes the non-nullable fallback-returning family only. It is not a drop-in for every private JSON helper — several existing helpers have deliberately different contracts and must not be blindly routed through it (each needs a call-site guard or a dedicated variant). This is documented in the type's XML doc and the ledger residue:

  • Models.GetInt/GetLong return int?/long? (null sentinel = missing); Models.GetLong has no double→long fallback (canonical truncates instead).
  • WorkspaceFilesModel.GetLong rejects negatives (>= 0); canonical accepts negatives.
  • WindowsNodeClient.TryGetString treats empty/whitespace as absent; canonical returns true for "" (present).
  • Mxc.MxcAvailability.FirstNonEmpty trims; SetupSteps.FirstNonEmpty trims + returns "no output"; canonical is verbatim / null.
  • Some older getters throw on a non-object parent; canonical is total.

Validation

$env:OPENCLAW_REPO_ROOT=(Get-Location).Path set for all runs (linked worktree).

Command Result
.\build.ps1 ✅ all 5 projects, warning-clean (TreatWarningsAsErrors=true)
dotnet test .\tests\OpenClaw.Shared.Tests\OpenClaw.Shared.Tests.csproj 2863 passed, 0 failed, 31 skipped
dotnet test .\tests\OpenClaw.Tray.Tests\OpenClaw.Tray.Tests.csproj 1717 passed, 0 failed

ArchitectureLedgerConsistencyTests (ledger format + guard-test existence) and GatewayProtocolDriftTests (unaffected by the routing) are green.

Real behavior proof

Proof profile: pure helper. No UI/MCP/node-capability/gateway surface is touched, so the proof is the automated unit tests that directly exercise the changed behavior:

  • JsonReadHelpersTests — missing property, JSON null, wrong kind, valid string/number, empty-string-vs-null, empty array, int64 clamp to int range, fractional→long truncation, fallback params, non-object parent, and FirstNonEmpty selection/verbatim/whitespace/null-array.
  • WslShellQuotingTests — empty→'', whitespace, embedded/single/multiple quotes, newlines, multi-line fragments, $(...), backticks, URLs, JSON arrays, Windows backslash paths, already-quoted input, a quote-breakout payload (x'; rm -rf / #), the escape-only-vs-wrapped invariant, the no-bare-unescaped-quote invariant, and a guard asserting divergence from PowerShell quoting.

Rubber-duck review

Adversarial dual-model review (GPT-5.5 + Claude Opus 4.8) was run on the diff, and the coordinator independently ran a second dual-model pass:

  • No blocking findings. Both passes confirmed the GetInt routing is behavior-identical, WslShellQuoting is POSIX-correct/injection-safe, the hand-computed '\'' test expectations are correct, and the ledger is well-formed with existing guard tests.
  • Hardening applied: the canonical FirstNonEmpty would NullReferenceException on a single bare null argument (latent, unreachable today, matched the pre-existing helpers) → added a values is null guard + regression test.
  • Contract-narrowing resolution (consensus): the docs/ledger no longer claim the helper is the "union of all private helpers"; the authoritative scope is narrowed to the non-nullable fallback-returning family, with the divergent variants (null-sentinel / non-negative / whitespace-absent / trimming) explicitly listed as separate and not-to-be-blindly-migrated.
  • Nullable-flow fix: TryGetString's out param now carries [NotNullWhen(true)].
  • Doc accuracy fix: GetInt's XML doc clarifies that numbers within long range but beyond int are clamped, while numbers beyond long range return the fallback (behavior unchanged from the deleted private helper).

Additive PR 2 of the architecture refactor. Introduces two single-source
helpers previously copy-pasted across the codebase, with thorough tests and
ledger updates. No runtime behavior change beyond one behavior-identical
call-site routing.

- JsonReadHelpers: canonical, total (never-throwing) JsonElement coercion for
  the non-nullable, fallback-returning helper family (GetString, TryGetString,
  GetInt/GetLong/GetDouble, GetArrayLength, FirstNonEmpty). Docs explicitly
  scope out the divergent null-sentinel / non-negative / whitespace-absent /
  trimming variants that must not be blindly migrated.
- WslShellQuoting: POSIX/WSL single-quote quoting (the '\'' idiom), deliberately
  separate from the cmd/PowerShell ShellQuoting. Escape-only and fully-wrapped
  are two distinct operations.
- OpenClawGatewayClient.ParseUsageCost: route 3 GetInt sites to
  JsonReadHelpers.GetInt; remove the duplicate private GetInt.
- docs/ARCHITECTURE.md: move json-read-helpers and wsl-posix-quoting from
  planned to authoritative with behavioral guard tests; narrow the ledger
  scope to the non-nullable fallback family.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@bkudiess bkudiess changed the title PR 2: Add JsonReadHelpers and WslShellQuoting canonical helpers (additive) Add JsonReadHelpers and WslShellQuoting canonical helpers (additive) Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant