Skip to content

test(http): pin byte-exact curl transport round-trips - #1019

Open
vernonstinebaker wants to merge 31 commits into
nullclaw:mainfrom
vernonstinebaker:test/http-curl-transport-integrity
Open

vernonstinebaker wants to merge 31 commits into
nullclaw:mainfrom
vernonstinebaker:test/http-curl-transport-integrity

Conversation

@vernonstinebaker

@vernonstinebaker vernonstinebaker commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Adds byte-exact integrity coverage for the HTTP curl transport: large payloads crossing the 8 KiB read buffer, a preserved request body, the Android fetchWithCurl entry point, and repeated transfers to catch state leaking between calls. These are transport invariants — a truncating or reordering reader still exits 0, so the existing suite could not catch it.

Why here

ProxyHttpClient.fetch routes every Android request through fetchWithCurl (the comptime builtin.abi == .android branch); other platforms keep using std.http. So:

  • the adapter is Android-only,
  • the curl executor underneath it is shared by every platform,
  • therefore these assertions run on Linux, macOS and Windows and flag a regression even where the adapter is never selected in production.

The coverage gap

Existing fixtures used 12-byte bodies ("transport-ok") against an 8 KiB read buffer, so a truncating or reordering reader still passed. Nothing round-tripped through fetchWithCurl at all — its only tests covered header validation and redirect policy, never a body.

The four tests

Test Guards against
round-trips 4/8/64 KiB marker payloads across the 8 KiB boundary truncation / partial reads
preserves a 48 KiB request body mangled request — the model answers a prompt it never received, which reads as scrambled output
round-trips through fetchWithCurl the Android entry point, which had zero body coverage
five sequential transfers with distinct payloads state leaking between transfers

Payloads use a position-sensitive pattern, so expectEqualSlices detects truncation, duplication and reordering.

Red proof

I injected a simulated truncation (compared against payload[0 .. len/2]) rather than trusting that a green test is a useful test:

error: 'curl transport round-trips payloads byte-exactly across the 8 KiB boundary' failed
error: 'curl transport preserves a large request body byte-exactly' failed
error: 'curl transport stays byte-exact across repeated calls' failed
Build Summary: 7388/7400 (9 skipped, 3 failed)

Reverted → green again. The 4th test uses the adapter's writer path, so it asserts independently.

Validation

  • Linux container: 7394/7408 (baseline was 7390/7404 — exactly +4)
  • macOS host: 7391/7400, repeated 3× green
  • zig fmt --check src/: clean
  • Test-only change: src/http_util.zig +223/-0

Note on --no-verify

This was pushed with --no-verify under AGENTS.md §8.2, after confirming the same suite green manually on both platforms. Reason: the repo's pre-push hook cannot pass from a worktree — git push exports GIT_DIR, the hook inherits it, and every test that spawns git (skills.installSkillFromGit ×7, workspace_audit ×1, +2) then operates on this repo instead of its temp fixture.

Reproduced deterministically: with GIT_DIR set the suite fails 7380/7400 (10 failed); without it, green. Verified separately that a push from a normal clone exports no GIT_*, while a push from a worktree exports GIT_DIR and GIT_PREFIX — and worktrees are this repo's documented workflow. That is a pre-existing bug in .githooks/pre-push, not caused by this change; happy to fix it in a separate PR.

vernonstinebaker and others added 29 commits October 4, 2026 20:27
Adds integrity coverage for the silent response corruption in nullclaw#1018, where
aarch64-linux-android returns scrambled or empty text with exit 0 — nothing
fails loudly, so the suite could not catch it.

The Android-only half is `ProxyHttpClient.fetchWithCurl` (selected by
`fetchWithCurl` at the `comptime builtin.abi == .android` branch); the curl
executor under it is shared by every platform, so these assertions run on
Linux, macOS and Windows and flag a regression even where the adapter is not
selected in production.

Existing fixtures used 12-byte bodies against an 8 KiB read buffer, so a
truncating reader still passed. Four tests close that:

- round-trips 4/8/64 KiB marker payloads across the 8 KiB boundary
- preserves a 48 KiB request body (a mangled request reads as scrambled output:
  the model answers a prompt it never received)
- round-trips through `fetchWithCurl`, the Android entry point, which had no
  body coverage at all
- repeats five transfers with distinct payloads to catch state leaking between
  transfers, the signature of the intermittent failure

Payloads use a position-sensitive pattern, so equality detects truncation,
duplication and reordering alike. Verified by injecting a simulated truncation:
three of the four tests fail loudly and pass again once reverted.

Linux 7394/7408, macOS 7391/7400, `zig fmt --check` clean.
vernonstinebaker and others added 2 commits October 6, 2026 16:21
The branch replaced the 974-line README with the single line
`# Not a skill` and carried a set of unrelated root fixtures
(`SKILL.md`, `skill.json`, `history.env`, `config.txt`,
`assets/payload.txt`, and fixture directories under `skills/`). None of
those paths exist on main, and the names match the `src/skills.zig`
workspace-audit test fixtures, so they were written into the working
tree by a test run rather than authored here.

Restored README to its merge-base content and removed the residue, so
the pull request is now only its own work: the byte-exact curl
transport round-trips in `src/http_util.zig`.

The underlying non-hermeticity is tracked in nullclaw#1029 and addressed in
nullclaw#1038. This branch should not be used to validate until those land.

Validation: `zig build test --summary all` 13/13 steps, 7391/7400
passed, 9 skipped, 0 failures, 0 leaks; `zig build -Doptimize=ReleaseSmall`
and `zig fmt --check src/` clean.
@vernonstinebaker

vernonstinebaker commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor Author

This branch was carrying committed test residue and is now MERGEABLE. No re-review was requested before, so treat this as a first look rather than a re-review.

What was wrong

The branch replaced the 974-line README with the single line # Not a skill, and carried a set of unrelated root fixtures: SKILL.md, skill.json, history.env, config.txt, assets/payload.txt, and fixture directories under skills/. None of those paths exist on main.

The names are the src/skills.zig workspace-audit test fixtures — good_skill, another_good, toml_only, existing_skill, flat-skill, unsafe, and the literal # Not a skill at src/skills.zig:4761 — so this was a test run writing into the working tree, not authored work.

This is byte-identical to the contamination in #1012, which is how I caught it: both branches had the same README.md 1/-974 signature. Fixing them together made the pattern obvious rather than a one-off.

What changed

088bec7d restores README to its merge-base content and removes the residue. The branch is then merged with current main, which also clears the CONFLICTING state. The PR is now a single file:

src/http_util.zig | 223 +++++++++

— just the byte-exact curl transport round-trips.

Why this happened — corrected

An earlier revision of this comment said the residue was a separate test-hygiene issue tracked in #1029/#1038. That was wrong, and #1038 is unrelated: it touches only src/config_paths.zig and fixes the cron/session leak into the real $HOME/.nullclaw.

The actual cause is #1020. Running the suite with GIT_DIR inherited, exactly as git push from a worktree exports it, makes the git-spawning tests operate on this repository rather than their fixture repos. They run git init/git add/commit and write their fixtures into the working tree:

error: 'skills.test.installSkillFromGit installs from local git repository' failed:  (and 2 more)
$ git log --oneline -3
d2e298e0 init
bf747d23 init
072f4ad5 init
$ git show d2e298e0:README.md
# Not a skill

That commit adds skills/unsafe/SKILL.md and carries the stub README — byte-identical to what this branch and #1012 both had. A squashed git add -A commit captures it, which is why only branches committed that way were affected.

#1021 fixes it (it unsets the inherited git environment before running the suite), verified against that exact condition: same worktree, same inherited GIT_DIR, suite green with HEAD unchanged and a clean git status. Until #1021 lands, any push from a worktree remains a corruption risk — which is also why this branch needed --no-verify.

Validation

zig build test --summary all 13/13 steps, 7391/7400 passed, 9 skipped, 0 failures, 0 leaks on the branch; 13/13 steps, 7494/7503 passed, 0 failures on the merge result. zig build -Doptimize=ReleaseSmall and zig fmt --check src/ clean in both.

vernonstinebaker added a commit to vernonstinebaker/nullclaw that referenced this pull request Oct 6, 2026
Extends the same hook that clears GIT_DIR (nullclaw#1020) so it also refuses to
let a push through when the test run left untracked files behind.

The suite can drop fixtures into the working tree. They are harmless
where they sit, but the next `git add -A` commits them silently. That
is how nullclaw#1012 and nullclaw#1019 each ended up carrying workspace-audit and
skills test fixtures -- `config.txt`, `history.env`, `SKILL.md`,
`skill.json`, `assets/payload.txt`, and fixture directories under
`skills/` -- along with a one-line `# Not a skill` stub that had
replaced the 974-line README. Both branches were single squashed
commits, which is the shape that captures whatever happens to be lying
around in the tree.

Deliberately limited to untracked ("??") entries. Modified tracked
files are nearly always work in progress, and blocking on those would
make the hook hostile to normal development; the residue problem is
specifically about files appearing from nowhere and being swept up by
an add-everything commit.

The check depends on the `unset` above it: with GIT_DIR still exported
from a worktree push, `git status` reports on the wrong repository.

Verified by running the hook directly: clean tree passes, an untracked
file fails with the offending paths listed, and a modified tracked file
still passes.

This branch has not been deployed

No deployments
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