Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions demos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,20 @@
`Network I/O: RX <n> KB TX <n> KB (last interval)`; it contains no `KB/s`.
- `tui_capture_2026-09-11.txt` — readable 110x50 text render of the same bytes (pyte 0.8.2).
- `http_capture_2026-09-11.txt` — real `GET /health`, `GET /metrics`, agent log, and local `export`
output from the built binary on a free loopback port. It also preserves the known remote-export
failure at this commit (see below).
output from the built binary on a free loopback port. It also preserves the historical
remote-export failure at commit `5dde108` (see below).
- `verification_2026-09-11.md` — build identity (commit, toolchain, binary SHA-256), exact capture
commands, artifact hashes, privacy review, and limitations.
- `verification_2026-09-11-remote-export.md` — CLI-boundary verification of the remote-export fix
on base `3dfa107`: success against a live `grainx agent` and controlled errors for unreachable,
malformed, and `http://127.0.0.1:0` endpoints, with no panic text in any capture.

Verified build: commit `5dde1082e5f8989793e79bd5130ec728dd0bde83`, binary SHA-256
`9dfd9cccdd5d4f2352d6aca527b0c3f8f75bda35f95aa1134d0bfaee98fdce00`, `rustc 1.94.1`.

Remote export is fixed on `main` at `3dfa107`; the current CLI evidence is in
`verification_2026-09-11-remote-export.md`.

### Reproduce the TUI capture

~~~bash
Expand Down Expand Up @@ -53,11 +59,16 @@ mkdir -p /tmp/grainx-export && cd /tmp/grainx-export

`network_rx_bytes` and `network_tx_bytes` in the JSON are per-interval deltas, not rates.

### Known failure at this commit
### Remote export status

`export --remote <url>` panics with `Cannot drop a runtime in a context where blocking is not allowed`.
The failure log is preserved in `http_capture_2026-09-11.txt`; local `export` works. This is a
pre-existing defect, documented rather than fixed so the demo stays bound to the captured commit.
- **Historical:** at `5dde108` (the 2026-09-11 capture commit), `export --remote <url>` panicked
with `Cannot drop a runtime in a context where blocking is not allowed`. The failure log is
preserved in `http_capture_2026-09-11.txt` as a defect record, not a current result.
- **Fixed:** `main` at `3dfa107` runs the blocking request on a dedicated OS thread.
`verification_2026-09-11-remote-export.md` shows exit 0 against a live `grainx agent`, exit 4
with controlled errors for unreachable, malformed, and `http://127.0.0.1:0` endpoints, and no
panic text. Automated CLI coverage is in `tests/cli_remote_export.rs`.
- Local `export` works at both commits.

## Historical captures (2026-09-06)

Expand Down
173 changes: 173 additions & 0 deletions demos/verification_2026-09-11-remote-export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# grainx remote-export CLI verification — 2026-09-11

This record proves the `grainx export --remote <url>` fix at the real CLI boundary on top of
`main` commit `3dfa107` (the commit that moved the blocking `reqwest` client to a dedicated OS
thread). It covers the success path, two controlled-failure paths, and the exact
`http://127.0.0.1:0` case that panicked before the fix. It is evidence for the listed build and
host only, not a compatibility claim.

## Build identity

| Field | Value |
| --- | --- |
| Source commit | `3dfa107f78b94812c080eae3b577c7cd6035cfdd` (`fix: build blocking remote metrics client outside the async runtime`) |
| Branch | `test/remote-export-cli` (branched from `origin/main` at the commit above) |
| Worktree at `cargo build` | clean (`git status --porcelain` = 0 paths) |
| Worktree at CLI capture | dirty count 1: untracked `tests/cli_remote_export.rs`; no changes under `src/` |
| Build command | `cargo build --locked` |
| Binary | `target/debug/grainx` (frozen copy used for captures: `grainx.used`) |
| Binary SHA-256 | `5e485df1469c1163db9b253794a30f7124b763969ea8abf39ae908584c7492b6` |
| Toolchain | `rustc 1.94.1 (e408947bf 2026-03-25)` / `cargo 1.94.1 (29ea6fb6a 2026-03-24)` |
| Host | macOS Apple Silicon (darwin arm64), UTC session date 2026-09-11 |

The demo captures below ran against a frozen copy (`grainx.used`) of the `cargo build --locked`
artifact, and both files hash to the value above. `cargo test` builds its own variant of the bin
target, so freezing the binary removes ambiguity about which bytes produced the captures.

## Historical failure (not a current result)

On 2026-09-11, at commit `5dde108`, the same CLI shape exited 101 with:

~~~text
thread 'main' (157332) panicked at .../tokio-1.53.1/src/runtime/blocking/shutdown.rs:51:21:
Cannot drop a runtime in a context where blocking is not allowed. ...
~~~

The full log is preserved in `demos/http_capture_2026-09-11.txt` and
`demos/verification_2026-09-11.md`. That capture is historical evidence of the defect; it is not
evidence of the fix.

## Captured CLI runs

All runs used the frozen binary on loopback only; no internet access is required. The success run
used a real `grainx agent` process as the server; the malformed run used
`python3 -m http.server` serving a syntactically invalid `/metrics` body.

Result summary:

| Case | Exit code | Output files | Panic text |
| --- | --- | --- | --- |
| Success against live `grainx agent` | 0 | `r.json` (2045 bytes), `r.csv` (987 bytes) | none |
| Unreachable closed loopback port | 4 | none | none |
| Malformed `/metrics` JSON (HTTP 200) | 4 | none | none |
| `http://127.0.0.1:0` (historical panic repro) | 4 | none | none |

### Success

~~~bash
port=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1]); s.close()')
target/debug/grainx agent --bind 127.0.0.1 --port "$port" &
target/debug/grainx export --remote "http://127.0.0.1:$port" --json r.json --csv r.csv
~~~

Observed stdout: empty. Observed stderr:

~~~text
Exported stats to r.json and r.csv
~~~

Both files parse. The JSON keeps byte counts (per-interval deltas) and does not rename them to
rates:

~~~text
json network_rx_bytes=3072 network_tx_bytes=2048
json keys containing 'rate': []
system,network_rx_bytes,3072
system,network_tx_bytes,2048
~~~

(Values are per-run deltas from the agent and vary between runs; the field names and integer-byte
semantics do not.)

### Unreachable endpoint

The port number comes from a bind/drop probe and is not listening:

~~~bash
target/debug/grainx export --remote "http://127.0.0.1:$closed_port" --json r.json --csv r.csv
~~~

Observed exit code: `4`. stderr:

~~~text
Error: remote metrics: remote metrics: failed to fetch remote metrics from http://127.0.0.1:50320/metrics: error sending request for url (http://127.0.0.1:50320/metrics)
~~~

No `r.json` was written.

### Malformed response

The server returned HTTP 200 with body `{"cpu_usage_percent": "not a snapshot"}` at `/metrics`:

~~~bash
printf '%s' '{"cpu_usage_percent": "not a snapshot"}' > badserver/metrics
python3 -m http.server --bind 127.0.0.1 "$bad_port" --directory badserver &
target/debug/grainx export --remote "http://127.0.0.1:$bad_port" --json r.json --csv r.csv
~~~

Observed exit code: `4`. stderr:

~~~text
Error: remote metrics: remote metrics: invalid metrics JSON: error decoding response body
~~~

No `r.json` or `r.csv` was written.

### Port zero (exact historical repro)

~~~bash
target/debug/grainx export --remote http://127.0.0.1:0 --json r.json --csv r.csv
~~~

Observed exit code: `4`. stderr:

~~~text
Error: remote metrics: remote metrics: failed to fetch remote metrics from http://127.0.0.1:0/metrics: error sending request for url (http://127.0.0.1:0/metrics)
~~~

No `r.json` was written, and no `panicked` or `Cannot drop a runtime` text appears in any capture.

## Automated CLI coverage

`tests/cli_remote_export.rs` spawns the compiled binary via `env!("CARGO_BIN_EXE_grainx")` against
a minimal `std::net::TcpListener` server (no extra dependencies, no internet). Four tests:

1. `remote_export_succeeds_against_a_live_http_server` — exit 0; JSON deserializes as
`StatsSnapshot`; `network_rx_bytes`/`network_tx_bytes` equal the served byte counts; no
`network_*_rate` keys; CSV contains the same byte-count rows and process section.
2. `remote_export_reports_controlled_error_when_endpoint_unreachable` — exit 4, controlled
message, no panic text, no output files.
3. `remote_export_reports_controlled_error_on_malformed_response` — exit 4, `invalid metrics
JSON`, no panic text, no output files.
4. `remote_export_to_port_zero_reports_controlled_error` — the exact historical repro.

## Repo checks

Run on the same commit with the committed lockfile:

~~~text
cargo fmt --check PASS
cargo check --locked --all-targets PASS
cargo clippy --locked --all-targets -- -D warnings PASS
cargo test --locked PASS
~~~

`cargo test --locked` result: 45 library tests, 2 `src/main.rs` tests, 4 `cli_remote_export`
tests, 13 `integration_tests` tests — 64 passed, 0 failed, 0 ignored; 0 doctests.

## Remaining limits

- The automated tests bind `127.0.0.1` only. No TLS, proxy, redirect, auth, non-200 status, slow
endpoint, or large-body coverage is claimed here.
- The test server answers one request with `Connection: close`; it is a fixture, not a
conformance suite.
- The malformed case covers schema-invalid JSON in an HTTP 200 body. Non-200 handling
(`remote metrics request failed with status ...`) is implemented but not asserted at the CLI
boundary in this record.
- The success capture is a real host snapshot at one instant; values are samples, not benchmarks.
Process names, disk labels, OS/kernel versions, and uptime appear in the generated files and
were not committed.
- `cargo test` builds a slightly different bin artifact than `cargo build`, so the frozen binary
hash above identifies the captured artifact, not necessarily every local `cargo test` run.
- Windows behavior is not tested; CI runs the test suite on ubuntu-latest and an MSRV
`cargo check` on Rust 1.88.
Loading
Loading