Skip to content

nvx-rs: add the scenario tests that replace the CLI transcripts - #825

Merged
Pedro Henrique Penna (ppenna) merged 8 commits into
devfrom
agents/issue-728-fix-logical-commits
Oct 25, 2026
Merged

Pedro Henrique Penna (ppenna) merged 8 commits into
devfrom
agents/issue-728-fix-logical-commits

Conversation

@ppenna

@ppenna Pedro Henrique Penna (ppenna) commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Part of #728: the first of its two pull requests, which adds the scenario tests. The second, stacked on this one, retires the transcripts.

nvx-rs/tests/fixtures/cli.json holds 315 recorded scenarios of the CLI, 313 for POSIX hosts and 312 for Windows, which nvx_rs_reproduces_the_recorded_scenarios replays byte for byte: each step's status, standard output and error, and files, once placeholders replace the root and volatile values. A change of a message rewrites transcripts by hand, as the error codes of #724 did in #812, while what callers rely on stays implicit in them. This pull request adds table-driven scenario tests in nvx-rs/tests/cli/ that check that contract directly: exit statuses; errors by their codes and stable fragments of their messages; the entries, records, and POSIX modes of state directories; and the arguments that OpenVMM receives. It also maps every recorded scenario, and every regression that #722 traces, to the tests that replace or guard it. The CLI does not change.

Changes

  • Harness (tests/cli/harness.rs): fresh artifact roots, Root::new() and Root::empty(), that end leftover fake OpenVMM processes when they drop; Outcome checks of successes, errors, and usage errors, each by its status, streams, exact code, and message fragments; snapshots of the tree below a root; and the splitting of >> lines with a POSIX shell-word splitter, or CommandLineToArgvW on Windows, for which the Windows dev-dependency on windows-sys enables Win32_UI_Shell. The replay and the other tests of tests/cli.rs use the same roots, and the replay checks that its fixture's tree is the harness's.
  • Tables:
    • usage.rs: 61 usage errors of both parsers, each with status 2, its argument-* code, and the option and value at fault, and help.
    • validation.rs: 129 refusals before any side effect, each with status 1, its code, its fragments, and the root unchanged, including which of several errors a command line reports first.
    • dry_run.rs: 72 dry runs, whose >> line must split into the options that OpenVMM's documentation prescribes for the case, and a property test that arbitrary --cmdline values split back out of the line.
    • one_shot.rs: 20 launches with the fake OpenVMM, checking exit statuses, output byte for byte, that the >> line splits into the argv that the fake recorded, the checked images, and outcome reports: the network policy that the case expects, host loopback and status code included, and the seven teardown steps that callers require, each successful.
    • lifecycle.rs: provision, start, exec, stop, and deprovision for 9 configurations, checking the configuration with its bindings, the runtime record and identity, the capability, POSIX modes, OpenVMM's options, and the guest's mappings; managed executions; and 10 refusals of lifecycle operations, each by its code. Together they cover the regressions of Track nvx.py managed sandbox bugs: PID-only liveness check wedges stop, deprovision, and start #369 and Track managed sandbox lifecycle bugs: on Windows, provision waits out its timeout for a lock file that refuses access for good #464, and the dry runs of Track managed sandbox bugs: --dry-run performs managed operations, and layered exec --cwd leaves PWD at / #373.
    • formats.rs, with hand-written configurations in tests/fixtures/state/: formats 1 and 7 start and exec, and formats 2 to 6 and the format 1 of nvx.py fail with format-unsupported before OpenVMM launches.
    • egress.rs: 9 policy files, including Track egress policy compiler bugs: scoped IPv6 validation, range fragmentation #462's scoped IPv6 range, against compile_policy_file's rules, or its refusal's code and message; artifacts.rs: a checkout and an NVX_OPENVMM override, in the order of Guest::ALL and the byte layout of json.dumps, whose ensure_ascii escapes it reproduces.
  • Mapping (tests/cli/scenarios.rs): each of the 315 scenarios maps to tests and table cases, and each regression that nvx-rs: retire the Python-parity golden tests and make Rust the reference #722 traces maps to the tests that guard against it. The PWD that Track managed sandbox bugs: --dry-run performs managed operations, and layered exec --cwd leaves PWD at / #373 left at / maps to sandbox_launch_runs_the_workload_itself_through_its_entry, a unit test of the guest agent's launch, and to the /usr/bin/env --cwd /tmp check that the managed-exec-config microVM scenario runs in a real guest. Track Rust sandbox exec bugs: async output collection bypasses timeouts #513 maps to async_wait_with_output_honors_the_workload_timeout of aci_edge_sandboxes. The crate's package lacks the sources of those last two, so the mapping test reads them at run time in a checkout. every_recorded_scenario_maps_to_a_test reads the cases from the tables themselves, and fails for an unmapped scenario, a mapping of a scenario that the fixture lacks, a duplicate, a test that does not exist, or a case that its test's table lacks or names twice. The mapping retires with the replay.
  • Documentation: the layout, Golden scenarios, and Testing sections of the crate's README, and doc/project-structure.md.

The eight commits are self-contained: each builds, lints, and passes its tests on Windows and Linux, and adds the helpers of the harness with their first users.

Differences from the issue

Gates

  • Coverage (run 40028577794, at 7c36bfd, whose CLI sources the head shares): the replay, the CLI scenarios family of scripts/nvx_rs_coverage.py, reaches no line or branch of production code beyond the tests outside the families, on Linux or Windows. What only it reaches among all the tests, 6 lines and 2 branches on Linux and 1 branch on Windows, depends on timing and random inputs: busy-wait loops of the guest's unit tests in src/guest/process.rs and src/guest/sys/process.rs, and a bound in src/protocol/exec.rs that the protocol's property tests reach by chance. Earlier runs attribute such lines to other families.
  • Mutation, Linux (the replay and the other tests of --test cli, at 7c36bfd, on src/host/cli/**/*.rs): both runs evaluate the same 791 mutants. The replay catches or times out on 483 of them, and the other tests on 538, every one of the 483 among them. One shard of the second run lost its runner, in this run and in an earlier one: mutants of Parser::parse and State::consume_option make nvx-rs loop and allocate without bound, which exhausts a 16 GB runner before cargo-mutants' timeout. That shard's 50 mutants ran locally with cargo-mutants 27.1.0, four test threads as on the runner, and 1 GiB of address space per process, so that such a process aborts instead.
  • Mutation, Windows (the replay and the other tests of --test cli, at 630cfa1, on the files with platform-specific code, src/host/cli/{mod,output,session}.rs and src/host/cli/sandbox/*.rs): both runs evaluate the same 505 mutants, 72 of them unviable in both. The replay catches or times out on 268 of them, and the other tests on 309, every one of the 268 among them. 43 that the replay catches time out with the other tests instead: in each such run, at least 4 tests had already failed, and only start_hands_openvmm_the_images_that_it_checked or a_run_passes_openvmm_no_handle_of_its_caller, both older than this pull request, were still waiting when cargo-mutants' timeout ended it.

The head's tests differ from those of 7c36bfd only by the review fixes, which add checks and drop none, and from those of 630cfa1 only by the mapping test, which exercises no production code, and a comment, so the mutants that they catch can only be more.

Validation

  • Windows, Rust 1.95.0, at each commit: rustfmt; Clippy with -D warnings for all targets and features and for the library without default features; and the cli tests. At the head: the full test suite with all features, and cargo +1.89 check --all-targets --all-features.
  • Linux (WSL), Rust 1.95.0, at each commit, from a git archive of the commit: rustfmt, both Clippy configurations, and the cli tests. At the head: the full test suite with all features, and the Rust 1.89 check.
  • Package: the crate that cargo package packs, unpacked outside the repository, passes cargo check --all-targets --features cli,testing and the cli tests on Windows.
  • Non-ASCII root: the artifacts test passes with TMP or TMPDIR below nvx728-José-😀, on Windows and Linux.
  • Python: the Markdown link check and the suite of the validate-nvx action. On this machine, 18 subtests of test_setup_azure.py and 3 of a host-loopback script test fail through the local Git Bash; they run shell scripts that this change does not touch.
  • Left to CI: the macOS check, and the hypervisor, OpenVMM, and benchmark lanes.

Copilot AI balanced review requested due to automatic review settings October 23, 2026 21:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The mapping guard can accept missing replacement cases, and stale-runtime assertion failures can leak the fake OpenVMM process.

2 open issues
What changed in this PR

This PR adds structured CLI scenario tests for nvx-rs, preparing to retire recorded transcripts under #728 while retaining the replay and leaving production behavior unchanged.

Changes:

  • Adds a shared harness and table-driven checks for CLI output, validation, launch arguments, and sandbox state.
  • Adds configuration-format fixtures and mappings from recorded scenarios to replacement tests.
  • Updates test documentation and enables Windows command-line parsing support.
File Description
nvx-rs/​tests/​fixtures/​state/​format-7.json Current export-plan configuration fixture.
nvx-rs/​tests/​fixtures/​state/​format-6.json Legacy file-share configuration fixture.
nvx-rs/​tests/​fixtures/​state/​format-5.json Legacy share-policy configuration fixture.
nvx-rs/​tests/​fixtures/​state/​format-4.json Legacy multiple-share configuration fixture.
nvx-rs/​tests/​fixtures/​state/​format-3.json Legacy caller-owned share fixture.
nvx-rs/​tests/​fixtures/​state/​format-2.json Legacy read-only share fixture.
nvx-rs/​tests/​fixtures/​state/​format-1.json Supported basic configuration fixture.
nvx-rs/​tests/​fixtures/​state/​format-1-nvx-py.json Python configuration fixture without image bindings.
nvx-rs/​tests/​fixtures/​state/​format-1-network.json Supported network configuration fixture.
nvx-rs/​tests/​cli/​validation.rs Checks validation failures leave the root unchanged.
nvx-rs/​tests/​cli/​usage.rs Checks parser errors and help output.
nvx-rs/​tests/​cli/​openvmm.rs Defines launch-argument and report assertions.
nvx-rs/​tests/​cli/​one_shot.rs Checks launches, output, exit statuses, and reports.
nvx-rs/​tests/​cli/​harness.rs Shares roots, snapshots, process cleanup, and output parsing.
nvx-rs/​tests/​cli/​formats.rs Checks supported and rejected configuration formats.
nvx-rs/​tests/​cli/​egress.rs Checks policy compilation and refusals.
nvx-rs/​tests/​cli/​artifacts.rs Checks artifact paths and guest ordering.
nvx-rs/​tests/​cli.rs Registers scenario modules and shares the harness with existing tests.
nvx-rs/​README.md Documents scenario tests and transcript retirement.
nvx-rs/​Cargo.toml Enables Windows shell parsing for tests.
doc/​project-structure.md Updates the test-layout description.

Review effort: Balanced

Comment thread nvx-rs/tests/cli/lifecycle.rs Outdated
Comment thread nvx-rs/tests/cli/scenarios.rs Outdated
@ppenna
Pedro Henrique Penna (ppenna) force-pushed the agents/issue-728-fix-logical-commits branch from 927430e to 7c36bfd Compare October 23, 2026 21:40
Copilot AI balanced review requested due to automatic review settings October 23, 2026 21:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The replacement outcome-report assertions omit required schema and value checks needed to preserve transcript coverage.

3 open issues

Review effort: Balanced

Comment thread nvx-rs/tests/cli/openvmm.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The #513 mapping claims regression coverage from a CLI test that cannot exercise the affected asynchronous collection path.

2 open issues
2 resolved since last review

Review effort: Balanced

Comment thread nvx-rs/tests/cli/scenarios.rs Outdated
@ppenna
Pedro Henrique Penna (ppenna) force-pushed the agents/issue-728-fix-logical-commits branch from 20cc199 to 7456b6d Compare October 23, 2026 22:12
Copilot AI balanced review requested due to automatic review settings October 23, 2026 22:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No blocking findings remain in the review, and these test-focused changes retain the existing transcript replay and interruption coverage without changing production behavior.

1 open issue
3 resolved since last review

Review effort: Balanced

tests/cli.rs built its artifact roots for itself: the replay of
tests/fixtures/cli.json populated each root from the tree that the
fixture records, and populated_root() gave the other tests a root with
the same tree. The scenario tests that are to replace the replay
(#728) need such roots in modules of their own, without the fixture
that they retire.

tests/cli/harness.rs now builds them. Root::new() writes the build
provenance, layers, scratch image, shares, and report directory of a
test root, and installs the fake OpenVMM as its OpenVMM, and
Root::empty() returns an empty root for the replay to populate. Either
ends, when it drops, the fake OpenVMM processes that the runtime
records and launch journals below it name. Root::command() runs
nvx-rs --root ROOT in the root, without the variables that change its
output or its OpenVMM and without standard input, and
Root::snapshot() records each path below the root with its kind, its
POSIX mode, and a digest of its content.

The replay checks that the tree that its fixture records is the one
that Root::new() builds, apart from the fake OpenVMM that both
install, so that the two cannot drift apart while both exist, and the
tests that called populated_root() call Root::new().
fixture_roots_link_the_built_executable is now
roots_link_the_built_fake_openvmm. A root's path must hold no comma,
which --layer and --mount use as a separator; only the replay, whose
recordings quote no part of the root, still needs a path without
spaces or other special characters.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The replay of tests/fixtures/cli.json checks every error of nvx-rs
against a transcript that Python recorded, byte for byte: the usage
text, the message, and the files that each step leaves. A change of a
message breaks those transcripts, as the error codes of #724 did, which
#812 added to them by hand, while what callers rely on, the exit
status, the code of the error, the option or value that it names, and
that a refused command changes nothing, stays implicit in them (#728).

tests/cli/usage.rs checks from a table that each of 61 command lines
that the parser of nvx-rs sandbox or nvx-rs run refuses exits with
status 2, writes nothing to standard output, and writes the usage of
its parser and one error, argument-unrecognized, argument-missing, or
argument-invalid, that names the option, and the value if any, at
fault. Help prints the usage and the options of either command and
exits with status 0, after an operation or options, and before an
invalid value too.

tests/cli/validation.rs checks from a table of 129 cases each check
that refuses a command line before anything changes: options outside
their operation, environments and environment files, egress policy
files, network options, layers, scratch images, distro manifests,
state directories, shares and their policies, owners, and targets,
kernel command-line tokens and the share of the command line's budget
that shares take, restores and memory, the artifacts that nvx-rs run
needs, before and at launch, and which of several errors a command
line reports first. Each case builds the root that it needs, then must
exit with status 1, write nothing to standard output and one error
with the case's code, such as unsupported, artifact-missing, or io,
whose message holds the case's fragments, and leave every file below
the root as it found it. Cases for Windows alone cover shares that the
caller owns.

The harness's Outcome holds how a run ended and checks a success, an
error, or a usage error by its status, its streams, the code of its
error, and stable fragments of its message, such as option names,
values, and paths: callers tell errors apart by their codes and such
fragments, since the wording of a message may change. Root::nvx_rs()
runs nvx-rs with the placeholders {root}, {root/PATH}, and {exe} of
its arguments replaced, and in_parallel() checks the cases of a table
on four threads.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A dry run prints the OpenVMM command that a run would launch, after
">> ", and launches nothing. The replay compares that line with the one
that Python's shlex.join or subprocess.list2cmdline rendered for the
recorded command, character for character, so it pins how each
argument is quoted rather than that the line splits back into the
arguments that OpenVMM would receive (#728).

tests/cli/dry_run.rs checks 72 dry runs of nvx-rs sandbox run and
nvx-rs run from a table. Each must succeed, leave the root as it found
it, and print a line that splits, as the host splits a command line,
into OpenVMM's program and options, each with its values in order,
which must be those that the case derives from the minimal one-shot
run, the cold boot, or the restore that OpenVMM's documentation
describes. The cases cover layers, scratch images, workloads,
environments, shared directories and files with their access and
policies, egress policies, network options, kernel command lines,
memory, processors, guests, snapshots, and the paths that nvx-rs run
shows relative to the root. A property test runs nvx-rs run --dry-run
with 64 arbitrary kernel command lines, with quotes, backslashes, and,
on POSIX hosts, newlines, and checks that the line splits back into
the --cmdline value that it was given.

tests/cli/openvmm.rs parses OpenVMM command lines into their options,
and builds the options of the minimal one-shot run, the cold boot, and
the restore, the kernel command-line tokens of the sandbox profile and
its default workload, and the nvx_share= tokens and child names of
shared directories and files. The harness splits the >> line as a
POSIX shell splits words, and on Windows with CommandLineToArgvW, for
which the Windows dev-dependency on windows-sys enables
Win32_UI_Shell.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A one-shot run prints its OpenVMM command, runs OpenVMM on the
terminal, and exits with OpenVMM's status, which a one-shot sandbox's
OpenVMM takes from its workload. The replay compared what each run
printed with Python's transcript, so what OpenVMM received was
implicit in the >> line, and the images that a launch handed it went
unchecked (#728).

tests/cli/one_shot.rs runs 20 launches of nvx-rs sandbox run and
nvx-rs run with the fake OpenVMM from a table. Each case checks the
exit status, a workload's or OpenVMM's; the output after the >> line,
byte for byte, and standard error, exactly; that the >> line splits
into the very arguments that the fake OpenVMM recorded when it
accepted them, with the options that the case expects; that the
sandbox's layer and scratch image reach OpenVMM through files that the
launch checked, and that OpenVMM read them whole; and that an outcome
report that the case asks for follows OpenVMM's schema, with the
network policy that the case expects, its host loopback among it, and
with every step of the teardown that callers require, successful. The
launches of nvx-rs run cover cold boots, every boot option, restores,
missing snapshots, outcome reports, shares, an OpenVMM that refuses
its options, and a relative NVX_OPENVMM.

tests/cli/openvmm.rs now checks the sandbox blocks of a launch, the
images that the fake OpenVMM read through them, and outcome reports,
and the harness reads the arguments that the fake OpenVMM records and
other JSON documents below the root.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ppenna
Pedro Henrique Penna (ppenna) force-pushed the agents/issue-728-fix-logical-commits branch from 7456b6d to 630cfa1 Compare October 23, 2026 23:16
Copilot AI balanced review requested due to automatic review settings October 23, 2026 23:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A compile-time include of sibling-crate source prevents packaged CLI tests from compiling independently of the repository checkout.

1 open issue
1 resolved since last review

Review effort: Balanced

Comment thread nvx-rs/tests/cli/scenarios.rs Outdated
@ppenna

Pedro Henrique Penna (ppenna) commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor Author

Windows mutation results, as the Gates section promised. Run 40038822590 ran the replay alone, and run 40038833994 every other test of --test cli. Both ran at 630cfa1, on src/host/cli/{mod,output,session}.rs and src/host/cli/sandbox/*.rs.

Outcome Replay Other tests
Caught 258 256
Timeout 10 53
Missed 165 124
Unviable 72 72
  • Both runs evaluate the same 505 mutants. No mutant is caught or timed out only by the replay, while 41 are caught or timed out only by the other tests.
  • 43 mutants that the replay catches time out with the other tests instead. In each such run, at least 4 tests had already failed when cargo-mutants' timeout ended it. The only tests still waiting were start_hands_openvmm_the_images_that_it_checked, a_run_passes_openvmm_no_handle_of_its_caller, or both; neither is new in this pull request.
  • Both runs time out on the other 10. Only one of them, OneShot::poll -> Ok(None), times out before any test fails: with it, a one-shot run never sees OpenVMM exit.

The head's tests differ from 630cfa1's only in the mapping test, which exercises no production code, and a comment.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The #373 regression mapping points to a shell case that cannot detect incorrect inherited PWD in a non-shell workload.

1 open issue
1 resolved since last review

Review effort: Balanced

Comment thread nvx-rs/tests/cli/scenarios.rs Outdated
The replay ran each lifecycle of a managed sandbox as recorded steps,
and compared their output and the files that they left after it
replaced process IDs, devices, file IDs, times, and random identifiers
with placeholders, so it pinned the layout of every record without
checking what the record says (#728).

tests/cli/lifecycle.rs provisions, starts, executes in, stops, and
deprovisions a sandbox of each configuration of a table: minimal, with
one share, a share that the caller owns on POSIX hosts, several
shares, share policies, a shared file, network and launch options, a
proxy-only network, and relative and nested state directories. It
checks the entries and POSIX modes of the state directory; the
configuration that provision records, with bindings that verify
against the root's distro layer and scratch image; the runtime record
of a start, whose process runs and whose runtime identity holds the
digests of the root's artifacts and the versions of nvx::contract; the
control capability; the options with which the start launched OpenVMM
and the mappings that its guest received; an execution; the outcome
report of the stop; and that a second start and stop behave as the
first did.

A table of executions checks that exec exits with the workload's status
and passes its output through: environments, working directories, among
them a shell's PWD, timeouts, among them that of a workload that writes
until its timeout, missing programs, and outcome reports. A refused
execution fails with the code of its refusal, such as exec-refused or
outcome-report-exists, changes nothing but the fake OpenVMM's log of
sessions, and a marker in guest memory shows that its workload never
ran. A table of refusals covers, each by its code, operations without a
sandbox, those that a running sandbox refuses, a failed start, a start
without OpenVMM, a malformed configuration, a state path that is not a
directory or holds a foreign file, dry runs of lifecycle operations
(#373), a recycled process ID (#369), and concurrent starts (#464). The
check of a recycled process ID holds the OpenVMM whose record exec
clears in a guard that kills it when the check fails, since a root ends
only the processes that the records below it name.

tests/cli/openvmm.rs builds the options of a managed start and checks
its control endpoint, and the harness lists directories, finds the
records of the fake OpenVMM, reads POSIX modes, and replaces the
placeholders of JSON documents.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The replay started only the sandboxes that the build under test had
just provisioned, so it read only the configuration formats that the
build writes. start reads formats 1 and 7, and must refuse formats 2
to 6 of earlier builds, and the configurations that nvx.py wrote
without bindings, before it launches OpenVMM (#728).

tests/fixtures/state/ holds a hand-written config.json of each format:
1, which this build writes for a sandbox without shares, with launch
options and with a network; 7, with an export plan for a shared
directory with policies and a shared file; 2 to 6, in the shapes of
the builds that wrote them; and the format 1 of nvx.py.
tests/cli/formats.rs provisions a sandbox in a root, replaces its
configuration with each fixture, whose placeholders name the paths of
the root, the hypervisor of this host, and the bindings of the root's
distro layer and scratch image, and checks that formats 1 and 7 start
with the options that they configure, that OpenVMM received the root's
images through checked files, and that exec runs in them, and that the
others fail with format-unsupported and the error that their format
earns, and leave the root as it was.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ppenna
Pedro Henrique Penna (ppenna) force-pushed the agents/issue-728-fix-logical-commits branch from baa8c97 to cfcc540 Compare October 24, 2026 00:21
Copilot AI balanced review requested due to automatic review settings October 24, 2026 00:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The artifact-formatting assertion rejects correct CLI output when temporary directory paths contain non-ASCII characters.

1 open issue
1 resolved since last review

Review effort: Balanced

Comment thread nvx-rs/tests/cli/artifacts.rs Outdated
nvx-rs egress compile prints the native rules into which the library
lowers an egress policy file, and nvx-rs artifacts the paths of a
root's artifacts. The replay compared both with what Python's
functions printed when they were recorded, so a deliberate change of
the lowering, or of the guests that the library knows, needed a new
recording by hand (#728).

tests/cli/egress.rs runs egress compile on nine policy files, empty,
allowing, allowing and denying, invalid JSON, an invalid rule, an
unknown field, a missing file, a directory, and a scoped IPv6 range
(#462), and compares what it prints with the JSON of the rules of
nvx::host::network::policy::compile_policy_file, and its errors with the
code and the message of the library's refusal, as well as with the code
that the case expects. tests/cli/artifacts.rs checks that artifacts
prints, for a checkout and for an NVX_OPENVMM override, the root,
OpenVMM, the guest kernel, and every guest in the order of
nvx::host::artifacts::Guest::ALL, with its initramfs, package manifest,
and distro layer and manifest, or null, in the exact layout of
json.dumps(value, indent=2, sort_keys=True), which escapes every
character outside printable ASCII, such as those of a root below a
non-ASCII temporary directory.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
#728 retires tests/fixtures/cli.json and its replay once the scenario
tests check what each scenario checked. tests/cli/scenarios.rs makes
that claim checkable: it maps each of the 315 scenarios of the
fixture's POSIX and Windows sections to the tests and table cases that
check what it checked, and each regression that #722 traces, #369,
#373, #462, #464, and #513, to the tests that guard against it.
every_recorded_scenario_maps_to_a_test fails for a scenario that maps
to no test, for a mapping of a scenario that the fixture lacks, for a
scenario that maps twice, for a test that does not exist, and for a
case that the table of its test lacks or names twice. It reads the
cases from the tables themselves, so a case cannot pass for one whose
name only appears elsewhere in a test's source: the tables of the
scenario tests, and the names of their cases, are visible to it. The
mapping retires with the replay.

The PWD that #373 left at / for a workload in a working directory is
not the CLI's to check: a shell repairs it, and the fake OpenVMM
emulates the guest agent that sets it. For it, the mapping names the
unit test of the sandbox agent's launch, which plans the environment
of /bin/echo in a working directory, and the check of the
managed-exec-config microVM scenario, which runs /usr/bin/env there in
a real guest. For #513, it names the test of the asynchronous API of
aci_edge_sandboxes. The published package of nvx lacks the sources of
those last two, so the mapping test reads them at run time, in a
checkout, which SOURCE-MANIFEST.json marks at its root, and fails
there if it cannot; from the package, the CLI tests still compile,
and the mapping test checks the other mappings.

The README describes the scenario tests in its Testing section, the
mapping at the end of Golden scenarios, and tests/cli/ and the
configurations of tests/fixtures/state/ in its layout, as
doc/project-structure.md does.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ppenna
Pedro Henrique Penna (ppenna) force-pushed the agents/issue-728-fix-logical-commits branch from cfcc540 to e7be5d7 Compare October 24, 2026 00:48
Copilot AI balanced review requested due to automatic review settings October 24, 2026 00:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No unresolved blocking issues remain in these test-focused changes, which preserve production behavior and retain the existing transcript replay.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

@ppenna
Pedro Henrique Penna (ppenna) merged commit 3e80c7c into dev Oct 25, 2026
44 checks passed
@ppenna
Pedro Henrique Penna (ppenna) deleted the agents/issue-728-fix-logical-commits branch October 25, 2026 01:52
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 25, 2026
tests/fixtures/cli.json held the 315 scenarios that scripts recorded
from the Python implementations of nvx.py sandbox and nvx.py run, and
nvx_rs_reproduces_the_recorded_scenarios replayed them byte for byte
once placeholders replaced the root and volatile values. The scenario
tests of tests/cli/ check what callers rely on in their stead (#728):
every scenario, and every regression that #722 traces, maps to them;
on Linux and Windows, the replay reaches no line or branch of
production code beyond the tests outside its family; and cargo-mutants
finds no mutant of src/host/cli that the replay catches and the other
tests of the CLI miss.

The fixture goes, with the replay, its normalization and population
helpers, and the mapping of tests/cli/scenarios.rs, whose tables of
cases are private again, as is Root::empty(), which only Root::new()
calls now. .gitattributes no longer marks the fixture as generated,
and the test of the guest binaries' source identity names another file
of tests/fixtures, root-tokens.json, which holds the cases of a root's
boot tokens rather than behavior recorded from Python, so that no
retirement of #722 removes it. The README describes the scenario tests
in a section of their own, which replaces Golden scenarios and records
how they replaced the transcripts, and its Python parity and Testing
sections point to it.

#828, which merged after #825 added the scenario tests, changed two
messages that they check, and recorded the change in the transcripts
alone: an unknown field of an egress policy is quoted as Rust quotes
it, "other" where Python wrote 'other', and a policy file that cannot
be read is named as it was given, ./ included, where Python normalized
its path. The tests of egress compile and of the refusals of run expect
these messages, which the transcripts no longer check.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 25, 2026
Retire the Python-backed scalar compatibility layer of #730 in favor of
native sandbox values while preserving the existing wire contracts.

- Remove the public `Int` and `IntError` exports; use `Option<u64>` for
  the memory limit and `Option<u32>` for the process limit, including
  the bound that leaves room for the guest's init process.
- Share ASCII-digit-guarded `FromStr` parsing for UID and GID, the CLI
  resource limits, and the limits that sandbox configurations persist,
  which they keep writing as JSON integers.
- Use the canonical native `Uuid` parser while preserving tokens and
  all valid plan and configuration wire encodings.
- Reject malformed limits at the CLI usage stage with clear native
  errors: `--memory-max` and `--pids-max` fail with `argument-invalid`
  and exit status 2, naming the accepted ASCII decimal range, instead
  of Python's `invalid int value` and the later `--pids-max must be
  positive` refusal with exit status 1.
- Quote layer and persisted-plan values with Rust debug formatting.
- Remove the now-unused Python integer arithmetic, keep only the
  test-only historical Python UUID oracle, and narrow the visibility of
  the remaining private integer helpers.
- Update the scenario tests that replaced the CLI transcripts (#825,
  #830) to match: the usage table expects the double-quoted layer UUID
  and roles and the native `--memory-max` message, and the
  `--pids-max 0` case moves from the validation table, whose cases exit
  with status 1, to the usage table.
- Document the input contract and the API change in the README, whose
  Python parity section no longer lists the sandbox, nor UUIDs among
  the values that it checks as Python does; #828 already removed the
  egress policy compiler from that section.

Validation on Windows, rebased on dev: 702 library tests and 51 CLI
tests pass with all features, and `cargo fmt --all --check` passes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 25, 2026
Retire the Python JSON and surrogate behavior from the sandbox metadata
checks for #730. Parse bounded environment files directly as
Vec<String> with serde_json and validate ordinary UTF-8 KEY=VALUE
strings. Read distro manifests as strict JSON, require the integer
format 1, and refuse non-finite numbers and unpaired surrogate escapes,
even in unused fields. Preserve the file, entry, and byte limits, the
binding of the artifact name and digest, the file seals, and the
independent refusal of systemd.

Quote sandbox values and paths with Rust debug formatting, and keep the
native JSON and I/O causes in diagnostics. Move the now-unused Python
JSON lookup and UTF-8 encode-oracle helpers out of production code; the
unrelated historical Python family keeps its test-only oracles. Update
the focused checks of tests/cli.rs that name these values and paths;
the transcripts of tests/fixtures/cli.json, which #830 retired, no
longer record them. Document the accepted-input and diagnostic changes.

Update the scenario tests that replaced the transcripts (#825, #830) to
match. The validation table expects the double-quoted --mount modes and
targets, and the serde_json causes of environment files that hold an
object or a non-string entry. Its environment-file cases name the file
as nvx-rs now quotes it: Root::expand, in the shared harness, gains the
{root:?} and {root/RELATIVE:?} placeholders, which quote a path as
Rust's Debug quotes it, so that the fragments match the escaped
backslashes of Windows paths as well as POSIX paths.

Validation on Windows, rebased on dev: 703 library tests and 51 CLI
tests pass with all features, `cargo fmt --all --check` passes, and
Clippy passes with warnings denied for all targets with all features
and for the library without default features.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 25, 2026
Complete the kernel-token part of #730 with the existing
protocol::cmdline Linux lexer rather than Python's whitespace
splitting. Refuse control characters, Unicode whitespace, quotes, and
the bytes that Linux treats as whitespace, including the 0xa0 inside a
UTF-8 sequence, in entrypoints, workload arguments, and guest targets,
including the target of the single directory share of run.

Check reserved names after Linux's quote splitting and its aliasing of
hyphens and underscores, and also in the view of the guest's shell
bootstrap, which splits the expanded command line on IFS whitespace
without interpreting quotes, so that a quoted value such as
extra="ignored nvx_memory_max=max ignored" cannot override a configured
entrypoint, memory limit, process limit, or share; ordinary quoted
caller values remain accepted. Trim the caller's command line once,
before both reservation checks and rendering, so that the checks see
exactly the text that is rendered and leading Unicode whitespace, such
as U+2003 EM SPACE, cannot hide a reserved name. Confirm that Linux can
still see every generated root parameter, so that -- and unmatched
quotes cannot hide the appended boot settings. Keep the guest root
parser and the exact 1024-byte budgets for both share-delivery modes
and adapter plans.

Broaden the properties to quoted and dashed reserved inputs, leading
Unicode whitespace, and the known separator bytes, add explicit
hidden-root and quoted-override regressions for both share-delivery
modes, and make the host-path unit tests native-only. Update the
documentation, and the validation table of the scenario tests that
replaced the CLI transcripts (#825, #830): a sandbox argument with a
space is now refused for its whitespace rather than its spaces.

Build the Python compatibility code that only the CLI and the
historical tests use for them alone, so that the default-feature
library has no dead code, and add `cargo clippy --lib -- -D warnings`
to check-nvx-rs so that it stays so. Gate the Python split and strip
oracles to tests, and the integer, codec, and Unicode-digit helpers to
the cli feature and tests. Since #828 moved the egress policy compiler
to serde_json, and the previous commit the sandbox metadata readers,
only the CLI's state files use the Python JSON reader and Python
strings, so gate the json and text modules whole, as their dumps,
shell, and value siblings are. The README's Python parity section now
credits the nvx-rs CLI, their only remaining user, with the whitespace,
int, and json.loads semantics, and the module documentation of the JSON
reader no longer cites the retired egress and distro-manifest readers.

Validation on Windows, rebased on dev: 706 library tests and 51 CLI
tests pass with all features, and 630 library tests with default
features; `cargo fmt --all --check`, Clippy with warnings denied for
all targets with all features, for the default-feature library, and
for the library without default features, rustdoc with warnings
denied, and the Markdown link check pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 25, 2026
Retire the Python-backed scalar compatibility layer of #730 in favor of
native sandbox values while preserving the existing wire contracts.

- Remove the public `Int` and `IntError` exports; use `Option<u64>` for
  the memory limit and `Option<u32>` for the process limit, including
  the bound that leaves room for the guest's init process.
- Share ASCII-digit-guarded `FromStr` parsing for UID and GID, the CLI
  resource limits, and the limits that sandbox configurations persist,
  which they keep writing as JSON integers.
- Use the canonical native `Uuid` parser while preserving tokens and
  all valid plan and configuration wire encodings.
- Reject malformed limits at the CLI usage stage with clear native
  errors: `--memory-max` and `--pids-max` fail with `argument-invalid`
  and exit status 2, naming the accepted ASCII decimal range, instead
  of Python's `invalid int value` and the later `--pids-max must be
  positive` refusal with exit status 1.
- Quote layer and persisted-plan values with Rust debug formatting.
- Remove the now-unused Python integer arithmetic, keep only the
  test-only historical Python UUID oracle, and narrow the visibility of
  the remaining private integer helpers.
- Update the scenario tests that replaced the CLI transcripts (#825,
  #830) to match: the usage table expects the double-quoted layer UUID
  and roles and the native `--memory-max` message, and the
  `--pids-max 0` case moves from the validation table, whose cases exit
  with status 1, to the usage table.
- Document the input contract and the API change in the README, whose
  Python parity section no longer lists the sandbox, nor UUIDs among
  the values that it checks as Python does; #828 already removed the
  egress policy compiler from that section.

Validation on Windows, rebased on dev: 702 library tests and 51 CLI
tests pass with all features, and `cargo fmt --all --check` passes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 25, 2026
Retire the Python JSON and surrogate behavior from the sandbox metadata
checks for #730. Parse bounded environment files directly as
Vec<String> with serde_json and validate ordinary UTF-8 KEY=VALUE
strings. Read distro manifests as strict JSON, require the integer
format 1, and refuse non-finite numbers and unpaired surrogate escapes,
even in unused fields. Preserve the file, entry, and byte limits, the
binding of the artifact name and digest, the file seals, and the
independent refusal of systemd.

Read distro manifests with the strict JSON reader of egress policy
files that #828 added, which host::network::policy::json now shares as
read_document, with the name that its errors give the root of the
document. serde_json's own Value depends on the features of serde_json,
which another crate of a build may enable: with arbitrary_precision, it
reads an object that holds a number as that number, so that a format of
{"$serde_json::private::Number": "1"} would pass the integer check, and
it accepts numbers beyond the range of a double in unused fields. The
shared reader reads both as it does without the feature. It also
refuses an object that repeats a field, as `manifest has duplicate
field "format"` or `packages[0] has duplicate field "name"`, and arrays
and objects nested more than MAX_JSON_DEPTH, 32, deep, and a manifest
that is not UTF-8 now fails as such.

Quote sandbox values and paths with Rust debug formatting, and keep the
native JSON and I/O causes in diagnostics. Move the now-unused Python
JSON lookup and UTF-8 encode-oracle helpers out of production code; the
unrelated historical Python family keeps its test-only oracles. Update
the focused checks of tests/cli.rs that name these values and paths;
the transcripts of tests/fixtures/cli.json, which #830 retired, no
longer record them. Document the accepted-input and diagnostic changes.

Update the scenario tests that replaced the transcripts (#825, #830) to
match. The validation table expects the double-quoted --mount modes and
targets, and the serde_json causes of environment files that hold an
object or a non-string entry. Its environment-file cases name the file
as nvx-rs now quotes it: Root::expand, in the shared harness, gains the
{root:?} and {root/RELATIVE:?} placeholders, which quote a path as
Rust's Debug quotes it, so that the fragments match the escaped
backslashes of Windows paths as well as POSIX paths.

The manifest tests refuse that object-valued format as a mismatch, and
refuse numbers beyond a double, repeated fields at the root and in a
package, nesting beyond the limit, and a manifest that is not UTF-8. A
test of the shared reader simulates arbitrary_precision, as its other
tests do, and checks that the object stays an object there, and
another that read_document names the root of a document in its errors.

Validation on Windows, rebased on dev: 705 library tests and 51 CLI
tests pass with all features, `cargo fmt --all --check` passes, Clippy
passes with warnings denied for all targets with all features, and the
Markdown link check passes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 25, 2026
Complete the kernel-token part of #730 with the existing
protocol::cmdline Linux lexer rather than Python's whitespace
splitting. Refuse control characters, Unicode whitespace, quotes, and
the bytes that Linux treats as whitespace, including the 0xa0 inside a
UTF-8 sequence, in entrypoints, workload arguments, and guest targets,
including the target of the single directory share of run.

Check reserved names after Linux's quote splitting and its aliasing of
hyphens and underscores, and also in the view of the guest's shell
bootstrap, which splits the expanded command line on IFS whitespace
without interpreting quotes, so that a quoted value such as
extra="ignored nvx_memory_max=max ignored" cannot override a configured
entrypoint, memory limit, process limit, or share; ordinary quoted
caller values remain accepted. Trim the caller's command line once,
before both reservation checks and rendering, so that the checks see
exactly the text that is rendered and leading Unicode whitespace, such
as U+2003 EM SPACE, cannot hide a reserved name. Confirm that Linux can
still see every generated root parameter, so that -- and unmatched
quotes cannot hide the appended boot settings. Keep the guest root
parser and the exact 1024-byte budgets for both share-delivery modes
and adapter plans.

Broaden the properties to quoted and dashed reserved inputs, leading
Unicode whitespace, and the known separator bytes, add explicit
hidden-root and quoted-override regressions for both share-delivery
modes, and make the host-path unit tests native-only. Update the
documentation, and the validation table of the scenario tests that
replaced the CLI transcripts (#825, #830): a sandbox argument with a
space is now refused for its whitespace rather than its spaces.

Build the Python compatibility code that only the CLI and the
historical tests use for them alone, so that the default-feature
library has no dead code, and add `cargo clippy --lib -- -D warnings`
to check-nvx-rs so that it stays so. Gate the Python split and strip
oracles to tests, and the integer, codec, and Unicode-digit helpers to
the cli feature and tests. Since #828 moved the egress policy compiler
to serde_json, and the previous commit the sandbox metadata readers,
only the CLI's state files use the Python JSON reader and Python
strings, so gate the json and text modules whole, as their dumps,
shell, and value siblings are. The README's Python parity section now
credits the nvx-rs CLI, their only remaining user, with the whitespace,
int, and json.loads semantics, and the module documentation of the JSON
reader no longer cites the retired egress and distro-manifest readers.

Validation on Windows, rebased on dev: 708 library tests and 51 CLI
tests pass with all features, and 632 library tests with default
features; `cargo fmt --all --check`, Clippy with warnings denied for
all targets with all features, for the default-feature library, and
for the library without default features, rustdoc with warnings
denied, and the Markdown link check pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

2 participants