Repository navigation
Conversation
jussielo-amd
left a comment
There was a problem hiding this comment.
This is a genuinely careful piece of work — the commit-by-commit structure, the red-before/green-after discipline on each fix, and the mutation-testing pass (12/12 caught) are well above the median bar, and the atomic-writer test suite (permission preservation, symlink replacement, symlink-loop rejection, concurrent writers, a documented ENOSPC repro) is thorough. The e2e scenario for the unreadable-config case is also well built: it checks byte-for-byte file preservation, not just message wording.
Two things need to be resolved before this can merge, though:
windows-build-and-testis currently red:runtime_manifest_round_trips_through_the_registryandruntime_manifest_round_trips_for_utf8_install_rootsboth fail on Windows. Root cause looks like a gap in the new proptest generator —python_launcher/python_executableget arbitrary strings including a bare"/", but production code (normalize_runtime_path_text_for_host/_for_storage) normalizes path-like text to the host's native separator on save/load, so the round-trip isn't byte-for-byte on Windows for that input. Separately, the PR description still describesruntime_manifest_round_trips_through_the_registryas "ignored, Linux only," but its#[cfg_attr(unix, ignore...)]doesn't cover Windows, so it runs (and fails) there too — worth fixing the description along with the test.RocmCliConfig::load()still gates onpath.is_file(), which quietly returnsfalse(no error) for a directory sitting atconfig.json, and folds any other metadata error (permission denied, a symlink loop) into that samefalse. In both casesload()returnsOk(default())— the exact "unreadable config silently replaced with defaults" behavior this PR sets out to kill, just via a different trigger than the one it tests for.discover()correctly rejects a directory at that path (and it's tested), butload()itself doesn't, which also means the new doc comment claiming the two "agree" isn't quite true yet for this case.configured_managed_root_from_configalready has the right pattern next door (fs::read+ an explicitNotFoundmatch), so this looks like a small, local fix rather than a redesign.
Nothing else here is blocking. A couple of small things worth a look when you're back in this code: stage_file_for_atomic_publish's public API doesn't resolve a symlink target before staging the way the other two writer entry points do (not hit by any current caller, but worth a doc note or guard), and a test comment in config_steps.rs slightly overclaims that it "runs the advice as printed" when it actually reuses a pre-validated fixture value.
03a19dc to
14f68b5
Compare
|
Rebased onto main
On the review of
Verified locally on the new head:
On the old head, MI300X failed only therock-next-09, where vLLM on the ROCm 10 preview cannot load |
This reviewed 808aa9e. The head is now 14f68b5, rebased onto main. The Windows proptest findings were fixed by 808aa9e and 4a8cdd0. 11899ae makes load() treat only NotFound as absent, bad2b3a documents stage_file_for_atomic_publish's symlink behaviour, and 14f68b5 makes config-09 run the value the refusal printed. All required checks are green on 14f68b5.
14f68b5 to
33e125e
Compare
|
Rebased onto main Verified locally on the new head, all exiting 0:
|
33e125e to
2a40e18
Compare
|
Rebased again, onto main Verified locally on the new head, all exiting 0:
|
jussielo-amd
left a comment
There was a problem hiding this comment.
Builds on the prior review round (jussielo-amd, against 808aa9e). Both blockers from that round are fixed and verified against current HEAD (2a40e18), not just trusted from the dismissed state:
- The Windows proptest round-trip failures are fixed:
bd9835cdand31d9f196adjust the install-root generator to avoid names Windows normalizes away (trailing dot/space), and86d9cc4acompares the loaded manifest against the saved value in host path form instead of the raw value.windows-build-and-testis green on the current head (confirmed viagh pr checks). RocmCliConfig::load()(crates/rocm-core/src/lib.rs) no longer gates onpath.is_file().9e9fea64rewrote it tofs::read+ match onErrorKind::NotFound, so a directory atconfig.jsonor a symlink loop now fails the same wayAppPaths::discoveralready did, with a new regression test (lib.rs~7539) and a managed-Python-recording test for the same case intherock_persisted_state_props.rs.- The two minor items (stage_file_for_atomic_publish's missing symlink note, and the e2e step comment overclaiming it "ran the advice as printed") are also addressed:
b7144c7bdocuments the symlink behavior onstage_file_for_atomic_publish/publish_temp_file(crates/rocm-core/src/atomic_write.rs), and2a40e182makesconfig_steps.rs'sversion_with_suggested_data_dirstep actually readROCM_CLI_DATA_DIRout of the refusal's own stderr instead of reusing a value the test already knew.
I then did a fresh pass over the diff (atomic_write.rs, lib.rs's load/save/migration/discovery rewrite, the therock.rs/sandbox.rs writer deduplication, and the new proptest suites) looking for anything new. Nothing rises to a blocker:
resolve_symlink_target(atomic_write.rs:119) deliberately replaces a symlink's target rather than the link itself, so the link survives a save — this is the documented, intended behavior for a dotfiles-managedconfig.jsonand is covered bywrite_file_atomically_replaces_a_symlinks_target_and_keeps_the_linkandwrite_file_atomically_refuses_a_symlink_loop(atomic_write.rs:601,643), not a new overwrite hazard.- The managed-Python step's load error now propagating instead of being swallowed (therock.rs:5555, call sites 5709/5779) is exactly this PR's stated fix, and the PR description's "Blast radius" section already discloses that the
rocm install sdkmanaged-Python failure path has no e2e scenario and explains the unit/property-test coverage used instead, which satisfies the "state the gap" bar in AGENTS.md §3. - The legacy-TOML tick-refusal behavior (lib.rs ~2908) has a dedicated unit test,
migrate_legacy_dashboard_toml_refuses_an_invalid_tick_and_writes_nothing. save_runtime_manifest's non-atomic, non-transactional two-file write (therock.rs:6300) andwrite_active_runtime_marker's delete-then-rename (main.rs, untouched by this diff) are real but pre-existing and out of scope — the PR's own "Not in this PR" section already names the runtime registry manifest and the active-runtime marker as excluded.
Worth a follow-up, non-blocking: the fresh_paths/NEXT_DIR scratch-dir helper is duplicated across the three new proptest files rather than shared; configured_managed_root_from_config now does a full RocmCliConfig parse before falling back to its targeted Value lookup, and config.json ends up read/parsed twice per invocation (once in discovery, once in load); RocmCliConfig::save re-deserializes the whole struct just to catch a non-finite float. None of these affect correctness.
CI is green on 2a40e18 (windows-build-and-test, clippy, coverage, E2E consolidated, etc.); the three hardware-gated lanes (MI300X, Strix Halo WSL2/Windows) are still pending, consistent with how those lanes run in this repo.
33c61fe to
b69f6a7
Compare
|
Rebased onto main
Verified locally on the new head, all exiting 0:
|
b69f6a7 to
99d8992
Compare
|
Rebased onto main
Verified locally:
The one local Two things noticed but out of scope:
|
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 99d8992
This automation never files a GitHub approval, so no approving review will
appear here whatever the outcome — the merge decision stays with a human
reviewer.
Review — needs work
Full review of the whole change.
Implements #515 (the managed-Python wipe, data-dir guessing, non-atomic save and the unchecked tick migration it lists). It does not close the "other writes are not atomic" item, which the description defers explicitly.
Blocking
-
new · The recovery advice says the opposite of what most commands do with
ROCM_CLI_DATA_DIRset —crates/rocm-core/src/paths.rs:397-398,README.md:868-869,docs/manual-testing.md(new block)- What the text says: the error tells the user "commands that read your settings keep failing until the file is repaired". The README and manual-test text repeat this.
- What the code does: with
ROCM_CLI_DATA_DIRset, discovery never readsconfig.json. About 20 production call sites then doRocmCliConfig::load(&paths).unwrap_or_default()and carry on with default settings without a word. Examples:version(apps/rocm/src/main.rs:1572), the bare launcher and freeform request (:1680,:1688),examine(:2810),comfyui(:3786),apps/rocmd/src/service.rs:448,apps/rocmd/src/common.rs:32,engines/lemonade/src/process.rs:177. - The PR's own scenario relies on the opposite: config-09 runs
rocm versionwith the suggested folder and the file still broken, and expects success. - Which side governs: the code, because those callers are what actually runs.
- Confidence 85 · logic · Fix: say "commands that change your settings fail, and others run on default settings until the file is repaired". Or make the lenient loaders warn when they fall back.
-
new ·
legacy_toml_migration_round_trips_finite_valuescannot fail on what its name and doc claim —crates/rocm-core/tests/persisted_config_props.rs:327,:351-360- The shared property body treats
Err(_) | Ok(None)as a pass. - The "finite" generator includes
-1.0andf64::MAX, which this PR's migration now refuses. About 70% of cases therefore pass without migrating anything. - Mutation run: I made the tick check in
migrate_legacy_dashboard_toml_fromrefuse every tick (try_from_secs_f64(-1.0 - secs.abs())).cargo test -p rocm-core --test persisted_config_propsstayed green, and this property passed. - The doc comment ("finite ticks and arbitrary strings migrate and load back") is false of the test.
- Confidence 100 · logic (test quality) · Fix: generate only ticks
Duration::try_from_secs_f64accepts, requireOk(Some(_)), and compare the migrated ticks, not onlydefault_engine.
- The shared property body treats
-
new ·
docs/testing.mdsays every run prints areach:line; a plain run prints none —docs/testing.md:142-145- The counters are
eprintln!calls in passing#[test]s, and libtest captures that output. - Run:
cargo test -p rocm-core --test persisted_config_propsprinted zeroreachlines. - So the doc's safeguard ("a property whose reach is zero passed without testing anything") is invisible exactly where it matters, and nothing asserts any counter is non-zero.
- Confidence 100 · mechanical · Fix: assert the meaningful counters are greater than 0 after
runner.run, or add-- --nocaptureto the documented commands and say the line only shows then.
- The counters are
-
new · config-09's last Then cannot fail, and the scenario comment claims more than the steps do —
tests/e2e-cucumber/tests/e2e/config_steps.rs:333(moved_aside_settings_unchanged),tests/e2e-cucumber/features/config.feature:76- The last Then: the step renames the file to
config.json.brokenitself, and nothing inapps/orcrates/refers to that name. The assertion checks only the test's own rename. The behaviour the refusal promises for this way out — settings reset, data folder back to the default — is not observed. - The comment: "Each way out it offers is then taken, as printed" is also untrue:
- the repair route is never taken;
- no runtime is planted in the custom folder, so "lets commands find those runtimes" is never checked.
- Only the first Then's exact-string match guards the suggested value.
- Confidence 85 · logic (test quality) · Fix: after the move-aside, assert something the user can see, such as the resolved data folder or "no active runtime". Narrow the comment to the ways out actually taken, or plant a runtime and assert
versionreports it.
- The last Then: the step renames the file to
-
new ·
apps/rocmkeeps awindows-sysdependency that nothing uses —apps/rocm/Cargo.toml:58- Its only use at base was
ReplaceFileWintherock.rs, which this PR moves into rocm-core. A grep ofapps/rocmforwindows_sysnow finds nothing. - The PR removes the same dead dependency from rocmd but not from rocm.
- Confidence 85 · mechanical · Fix: drop the line and re-lock.
- Its only use at base was
-
new · On Windows,
config.jsonnow goes throughReplaceFileW, which has a documented failure that leaves noconfig.json—crates/rocm-core/src/atomic_write.rs:292-335,crates/rocm-core/src/lib.rs(save)- At base,
saveusedfs::rename. Its doc said the destination is never removed first, becauseloadreads a missing file as defaults. publish_temp_filenow callsReplaceFileWwhenever the destination exists, with no backup name, and returns the error without recovering.- Microsoft documents
ERROR_UNABLE_TO_MOVE_REPLACEMENT_2as "the file to be replaced still exists with a different name". The nextloadthen returns defaults, and the next save writes them: the loss this PR exists to prevent. - The
ReplaceFileWpath already existed for the active-runtime marker; this PR makesconfig.jsona new user of it. I argued this from the API documentation and the code path; I did not run it on Windows. - Confidence 70 · architectural · Fix: on that error, rename the displaced file back. Or use plain rename for
config.json. Restore the "not guaranteed" Windows notes insave's doc either way.
- At base,
-
new · New user-observable file behaviour has no scenario and no stated justification —
crates/rocm-core/src/atomic_write.rs(permission copy, symlink resolution), AGENTS.md §3- After
rocm config …, a0600config.jsonnow stays0600, and a symlinkedconfig.jsonkeeps its link. At base the link was replaced by a regular file. Both are visible in files the CLI writes. - Only unit tests cover them. AGENTS.md says a unit test "does NOT discharge this" and asks for a scenario, or for the PR text to say why there is none. The description lists these tests as "Unix-only by design" but does not address the scenario requirement.
- Confidence 85 · architectural · Fix: add a
config.featurescenario (Linux lane is fine, tagged accordingly), or justify the gap in the PR text.
- After
-
new · The description claims dependency changes and a deferral that the diff contradicts — PR description, "Dependencies" and "Not in this PR"
- It says proptest is "now used by
apps/rocmtoo" and "Cargo.lockgains only therocm→proptestedge". That edge is already on the base branch:apps/rocm/Cargo.tomlis untouched and the onlyCargo.lockchange is removing rocmd'swindows-sys. - It lists atomic writes for "the active-runtime marker (whose own temp-then-rename deletes the old file first)" as out of scope.
write_active_runtime_marker(apps/rocm/src/main.rs:8431-8452) already uses the shared atomic writer and never removes the destination first. - Confidence 100 · mechanical · Fix: narrow the description to what the diff does.
- It says proptest is "now used by
Non-blocking
- new · Wrong reason in the new module doc —
crates/rocm-core/src/atomic_write.rs:9-10- It says
ReplaceFileWis used on Windows "where a plain rename cannot replace an existing file".std::fs::renamedoes replace on Windows, as the basesavedoc said. - Confidence 85 · mechanical · Fix: state the real reason, which is keeping the replaced file's attributes and ACL.
- It says
- new · Stale comment in the new property —
crates/rocm-core/src/persisted_paths_props.rs:64- "An interrupted
RocmCliConfig::save(a plainfs::write)" no longer describessave, which was already temp-and-rename on base and is atomic and durable here. - Confidence 85 · mechanical · Fix: describe the case as a truncated file (hand edit, or one damaged before this change).
- "An interrupted
- new · The new Unix test duplicates an existing one —
crates/rocm-core/src/lib.rs:3890vs:4380config_save_replaces_the_file_instead_of_rewriting_it_in_placeproves the same property as the existing#[cfg(unix)] saving_the_config_replaces_the_file_rather_than_rewriting_it_in_place. Its leftovers check repeats:4349.- Its doc names the Windows twin as its counterpart, which hides that a Unix twin already exists.
- Confidence 85 · mechanical · Fix: fold the hard-link witness into the existing test.
- new · The commit trail should be squashed before merge
- new · "E2E tests (MI300X)" has not reported for this head. A check that has not reported is not a pass, so that lane's class of coverage is not established yet. "Skill checks" and "Regenerate manifests" were skipped; neither applies to the files this PR touches.
Decisions for the author
- Strict discovery, lenient settings loaders — tradeoff
- What happens now: for the same broken file, about 20 read-mostly commands error out when
ROCM_CLI_DATA_DIRis unset, and run silently on default settings when it is set.rocm examineandrocm versionnow stop exactly when a user is trying to find out what is wrong. - Current choice: it is safe and actionable.
- The alternative: read-only commands warn and fall back. That would make the env-var path consistent with the error text (see the first blocking finding).
- What happens now: for the same broken file, about 20 read-mostly commands error out when
- The test seam is public API — tradeoff
write_file_atomically_with_publishandpublish_temp_filearepub #[doc(hidden)]only sorocm's tests can inject a failing publish.- Current choice: simple, and documented in the code.
- The alternative: gate them behind a test-only feature, which keeps rocm-core's surface minimal.
- The writer now follows symlinks, and creates the parent of a dangling link's target — tradeoff
- Current choice: the dotfiles case works.
- Cost: for rocmd's artifact-cache marker, a dangling link planted in the data dir is now written through rather than replaced. Only the data dir's owner can plant one, so there is no privilege gain in the same-user model.
configured_managed_root_from_confighas a redundant strict fast path — non-blocking-improvement- The typed
RocmCliConfigparse atcrates/rocm-core/src/paths.rs:~338-347returns what the lenientValuepath below it returns for every inputloadaccepts. It adds a full typed deserialize to everydiscover. - Drop it, or say in its comment what it adds.
- The typed
Positive signals
recorded_root_in_damaged_confignames the folder only when the damaged bytes still hold a complete JSON string, and never guesses past a truncation.saveparses its own bytes back before publishing, so a valueloadwould reject can no longer replace a good file.- config-09 reads the suggested
ROCM_CLI_DATA_DIRfrom the refusal's own stderr, and its new helper points bothHOMEandUSERPROFILEinto the sandbox, so the default-folder fallback cannot reach the runner. - The managed-Python tests plant a stand-in
uvthat logs its calls, which proves the refusal happens before any download.
What this covered
- Read: every file in
prw-base...99d89921, completely. Also the callers ofAppPaths::discover,RocmCliConfig::load/saveandwrite_file_atomicallyacrossapps/,crates/andengines/, and the issue named in the description (#515: title, body, state). - How: a fan-out of four coupled-group passes, with Step 7's design questions run by a separate worker. I re-checked the surviving findings against the source.
- Runs:
- built and ran the
rocm-coretests in full; - ran
rocm's legacy-migration filter; - two mutations. The first deleted
instance_tickfrom the migration check and was caught bylegacy_tick_readers_agree_on_which_files_are_readable, so a suspected gap was dropped. The second, refuse-every-tick, survived, as described above.
- built and ran the
- Did not run:
- the prior-changes pass, because review comments on earlier PRs to these files were not fetched;
- anything on Windows (the
ReplaceFileWfinding is argued from documentation); - the e2e suite;
- full-workspace clippy.
- Not reconciled: this report is not reconciled against any discussion on the PR.
99d8992 to
7ae147d
Compare
|
Rebased onto main This push answers the automated review of Blocking
Non-blocking: the module doc reason and the stale comment in Decisions
Verified locally on the rebased head, all clean or passing:
|
7ae147d to
c9690e5
Compare
|
Rebased onto main
|
Adds proptest-based properties for state rocm persists across commands and upgrades: config.json save/load and legacy-TOML migration, the runtime registry, the startup update-check cache, the active-runtime marker, and how data-dir discovery reads config.json. Properties that hold run by default. Properties that currently fail are committed #[ignore]d with the defect in the reason, so they can be run with --ignored as reproducers and un-ignored with the fix: - a non-finite dashboard tick saves as JSON null, which load rejects - a legacy rocm-dash config.toml with nan/inf migrates into that state - record_managed_python_config replaces an unreadable config.json with defaults, dropping the active runtime and setup.therock_venv - AppPaths discovery reads a truncated config.json as "no managed root" and silently moves the data dir, while RocmCliConfig::load fails - a non-UTF-8 install_root is lossily rewritten into the registry even though the save then fails - unknown keys from a newer binary are dropped on the next save Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
rocmd carried its own copy of the stage-then-rename writer, so a fix to one copy would silently miss the other. rocmd already depends on rocm-core; use rocm_core::atomic_write and delete the copy. Move the writer's own tests (temp naming, concurrent writers, failed publish, cleanup after a failed rename, the ignored ENOSPC reproducer) from both binaries into rocm-core next to the code, merging the duplicates. With the tests local, the seams they used no longer need to be public: only write_file_atomically and stage_file_for_atomic_publish are API. write_file_atomically_with_publish and publish_temp_file stay public but hidden from docs, because the rocm binary's metadata-cache tests drive their own commit through them to make the rename fail or race. rocmd's copy differed in two ways that now change for it: a full disk during the write is reported through disk_space::map_write_error, and a failed publish names the destination. ReplaceFileW was rocmd's only use of windows-sys, so that dependency goes too; rocm-core declares its own. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
The writer replaced files atomically against concurrent readers, but not against a crash or power loss: neither the data nor the rename was flushed, so a crash could leave the new name pointing at an empty file — the truncated config.json this writer exists to prevent. Flush the staged file before publishing it, and on Unix flush the directory after the rename (best-effort: the file is already published, and some filesystems refuse a directory sync). On Windows, ReplaceFileW is called without REPLACEFILE_WRITE_THROUGH, which Microsoft documents as unsupported; the data is already flushed when it is staged. Replacing a file also changed what the user had set up: - the new file got default permissions, so a 0600 config.json came back as 0644. On Unix the staged file now takes the old one's mode first (ReplaceFileW already keeps attributes and ACL on Windows); - a config.json that is a symlink, as in a dotfiles checkout, was replaced by a plain file. The link chain is now followed and its target replaced, so the link survives. A loop is an error. The Windows arms were type-checked against x86_64-pc-windows-msvc in a scratch crate (proved live by injecting a type error), not run. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
Migration of the legacy rocm-dash TOML runs only while config.json does not exist. After a tick is refused, the first command that saves settings creates config.json, and from then on the legacy file is skipped silently — fixing the value later migrates nothing. Say so in the refusal (printed at startup as "skipped legacy dashboard config migration: ..."): fix the value before any command saves settings, or re-enter the dashboard settings by hand. The test asserts the warning together with the behaviour it describes: after a save, the broken file and the fixed one are both skipped and the legacy theme never arrives. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
…oducers Names the three proptest targets, what the reach: line means, how to run the #[ignore]d reproducers (which fail on purpose until their defect is fixed), and the out-of-space writer test that is ignored because it fills /dev/shm. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
plant_unreadable_config is only called by the two cfg(unix) ensure_managed_python tests, and the root-name strategy only pushes its non-UTF-8 option on unix. Windows builds the test binary with -D warnings, so the unused helper and the needless mut failed it. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
Windows strips a trailing space from a folder name when it creates it, so the spaced install root ' ROCm venvs ' came back without its trailing space and the manifest round-trip property failed on the Windows lane. Keep the inner and leading spaces there, which Windows stores as given. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
Loading a runtime manifest normalises its path text for the host (normalize_host_paths), so on Windows a python_launcher of "/" comes back as "\\". The round-trip properties compared against the raw saved value and failed only on the Windows lane; compare against the saved value in host form instead. On Linux the normalisation leaves these values unchanged, so the property is as strict there as before. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
Windows strips trailing dots from a folder name when it creates it, just as it strips trailing spaces, so a generated root like '_..' came back as '_' and the manifest round-trip properties failed on the Windows lane. Give such names a suffix there. The underlying install_root/pip_cache_dir mismatch is tracked in #551. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
This PR moves rocmd's copy of the atomic file writer, and the tests that covered it, into rocm_core::atomic_write. rocmd shrank from 7714 to 7500 measured lines and its line coverage from 75.36% to 75.11%; the coverage moved with the code to rocm-core rather than being lost. The new floor is the Coverage job's own measurement on this PR, in the form --bless writes. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
load() checked `is_file()` first and answered with defaults whenever it was false. That folds every way the path can fail to be a readable file into "no settings yet": a directory where the file should be, or a metadata error such as a symlink loop. Data-dir discovery already treats those as an error, so the two readers disagreed, and a caller that saved after load() would write defaults where the user's settings were expected. Read the file directly and treat only NotFound as absent, as discovery does. A directory at config.json now fails load(), and the managed Python step reports it with the same "left unchanged" message as for damaged contents instead of failing later on the rename. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
…link stage_file_for_atomic_publish takes its path as given, so publishing the staged file over a symbolic link replaces the link. Say so, and point at write_file_atomically for the case where the link must survive. publish_temp_file is public only as the test seam's default, so hide it from the docs like the seam itself. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
The step that follows the refusal's ROCM_CLI_DATA_DIR advice used the folder the scenario had planted. Read the value from the refusal's own stderr instead, so the scenario fails if the printed advice ever names a folder that does not find the runtimes. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
Windows clippy now runs in CI (#488). The two cfg(not(unix)) stubs in atomic_write.rs trip missing_const_for_fn; making only one arm const would split the two platforms' signatures, so allow it with the same comment #488 uses for its stubs. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
`RocmCliConfig::save` now publishes through `atomic_write`, so the cleanup of an unpublishable temp file is the writer's, its own test lives in `atomic_write.rs`, and the temp name is no longer `config.json.tmp-<ts>-<pid>`. Say so in the save tests that predate the move, and name the Windows file-identity test as the counterpart of the Unix-only hard-link check. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
The marker writer, its strict reader and the marker's fields moved out of main.rs into runtimes_cmd.rs, private to that module, so the check that enumerates which single-key drops split the two marker readers no longer compiled from `therock`'s persisted-state properties, which reached them through the crate root. Move it, unchanged in what it asserts, into a `persisted_state_props` module under runtimes_cmd's tests, where the private items are in reach. Keeping the module name keeps it selected by `cargo test -p rocm --bin rocm persisted_state_props`, the command docs/testing.md gives for these properties. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
Microsoft documents two ways ReplaceFileW, called without a backup name,
can fail after it has already moved the old file off the destination:
ERROR_UNABLE_TO_MOVE_REPLACEMENT ("the replaced file no longer exists
and the replacement file exists under its original name") and
ERROR_UNABLE_TO_MOVE_REPLACEMENT_2 ("the file to be replaced still
exists with a different name"). The writer then deleted its temp file,
the only remaining copy, and `load` read the missing config.json as "no
settings", which is the loss the writer exists to prevent.
On either code, retry once with a plain rename. If the publish still
fails and the destination that existed before is gone, keep the temp
file and name it in the error instead of deleting it. The Windows arm
is type-checked here only; the Windows lane is its first run.
Also restore save's "not guaranteed" notes for Windows, correct the
module doc's reason for ReplaceFileW (it keeps the replaced file's
attributes and ACL; a plain rename does replace on Windows), and fold
the hard-link witness of a duplicate Unix test into the existing
replace-not-rewrite test.
Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
The refusal said that with ROCM_CLI_DATA_DIR set, "commands that read your settings keep failing until the file is repaired". Most read-only commands load settings leniently and run on defaults instead; only the ones that change settings, and some others, still stop. None of the lenient loaders saves what it loaded, so the advice was wrong but not dangerous. Say what happens: any command that changes settings still stops, and the others either stop too or run on default settings. The README and the manual check say the same. config-09 now takes each claim it checks as printed: with the suggested folder the CLI keeps its data there and runs on default settings, and a setting change is refused with the file untouched; after moving the file aside, the CLI's data folder is no longer the recorded one. Its old last step only checked the test's own rename. config-10 and config-11 cover what saving now preserves, which only unit tests did: a 0600 settings file stays 0600, and a settings file that is a link stays a link while its target gets the new settings. Discovery also drops its typed parse ahead of the lenient one: for any file load accepts, both read the recorded folder the same way, so it only added a second deserialize. A test pins that agreement. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
legacy_toml_migration_round_trips_finite_values counted a refusal to migrate as a pass, and generated ticks (-1.0, f64::MAX) that the migration now refuses, so most cases migrated nothing; a migration that refused every tick stayed green. Generate only ticks Duration::try_from_secs_f64 accepts, require each case to migrate, and compare the migrated ticks, to within one ULP: serde_json is built without float_roundtrip, so config.json can read a float back one ULP off. docs/testing.md said every run prints a `reach:` line; the test harness captures it unless --nocapture is passed. Say so, and add it to the commands. Also correct a stale comment that still described save as a plain fs::write. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
Its only use was ReplaceFileW in therock.rs, which moved into rocm_core::atomic_write; rocmd's copy of the same dependency was already dropped. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
called atomic_write.rs the one file replacement rocm and rocmd use. Say why that writer stays separate rather than claim there is no other. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
The ASCII output guard treats files ending in `proptests.rs` as test code, the convention `providers/proptests.rs` and `examine_driver_proptests.rs` already follow. This file is only compiled under `#[cfg(test)]` (declared through `#[path]` in therock.rs), but its name hid that from the guard, which then flagged the non-ASCII install roots and GPU names its strategies generate on purpose. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
c9690e5 to
92b1f7e
Compare
|
Rebased onto
Verified locally, all green:
|
nowycondro
left a comment
There was a problem hiding this comment.
🤖 Automated review (Claude Code) · 92b1f7e
This automation never files a GitHub approval; the merge call stays with a human reviewer.
Scope and method
This PR already has two long, detailed review rounds (jussielo-amd at 808aa9e/2a40e182, siloteemu at 99d8992) that raised a substantial set of blocking findings. Per the batch instructions for this pass, I did not re-litigate that whole history. Instead I:
- Cloned the repo over HTTPS and checked out the PR head locally (
92b1f7edc046daac22d8a0edde635de197198564), confirmed it matches the PR's reported head SHA. - Diffed the last-reviewed commit (
99d89921) against the current head to isolate what changed since siloteemu's round (several commits plus three rebases ontomain, one of which relocatedRocmCliConfigintocrates/rocm-core/src/config.rsvia an unrelated landed PR, #666). - Read, line by line, each commit that responds to siloteemu's seven blocking findings: the Windows
ReplaceFileWpartial-failure fix (8342be1), the recovery-advice wording + redundant-typed-parse removal (32ba384), the migration round-trip property fix (3211ece), the unusedwindows-sysremoval (abd1f3e), the e2econfig-09/config-10/config-11scenario rewrite (26a3d08+ related), and the symlink-handling doc note (a4d2e51). - Re-read the core fix itself:
record_managed_python_config(apps/rocm/src/therock.rs:5624) now usesRocmCliConfig::load(paths).with_context(...)?instead ofunwrap_or_default(), and both call sites (therock.rs:5778, 5848) propagate with?instead of discarding the result — this is the central behavior change the PR claims and it checks out. - Cross-checked the author's own point-by-point rebuttal comments on the PR against the actual diffs (not just trusting the prose).
Findings
Blocking: none found. Every blocking item from the 99d89921 review round appears to have a corresponding fix in the commits since, and I verified each by reading the diff rather than trusting the commit message alone:
replace_file_windows(crates/rocm-core/src/atomic_write.rs) now detectsERROR_UNABLE_TO_MOVE_REPLACEMENT{,_2}and retries once as a plain rename; if that also fails, the temp file is kept and named in the error instead of deleted. A new unit test (a_publish_that_removes_the_destination_keeps_the_new_contents) pins the keep-path.legacy_toml_migration_round_trips_accepted_ticks(crates/rocm-core/tests/persisted_config_props.rs) now filters to ticksDuration::try_from_secs_f64accepts, requires every case to actually migrate (must_migrate: true), and compares migrated values (within 1 ULP, justified by serde_json lackingfloat_roundtrip) — this closes the "refuse every tick stays green" gap siloteemu demonstrated by mutation.apps/rocm/Cargo.toml's unusedwindows-sysis dropped andCargo.lockre-locked.- The e2e
config_steps.rssuggested_data_dirhelper now parses the actual suggested folder out of the refusal's own stderr (split_once("setting ROCM_CLI_DATA_DIR=")/split_once(" points commands")), rather than asserting against a value the test already planted — and I confirmed this parsing matches the current wording inunreadable_config_error(crates/rocm-core/src/paths.rs), i.e. it wasn't left stale after the wording changed in a later commit. - Discovery's redundant typed-parse fast path in
configured_managed_root_from_config(paths.rs) is removed, with a new regression test pinning that the lenient path alone agrees withloadon every loadable file.
I did not find anything new introduced in this round of changes that would rise to blocking. The bulk of the lib.rs diff churn versus 99d89921 is the unrelated RocmCliConfig file-split (#666) rebasing in underneath this PR, not new logic.
Non-blocking, low confidence (not independently verified further):
- siloteemu's prior non-blocking item about
ManagedServiceRecord::writeandwrite_active_runtime_markerstill using a plain write / delete-then-rename outside this PR's scope remains true at HEAD and is explicitly disclosed in the PR's "Not in this PR" section — flagging only so it isn't lost, not asking for action here. - The PR has 30 commits including several "fixup"-style commits patching tests added earlier in the same PR (per siloteemu's prior note); whether to squash before merge is a repo-convention / maintainer call, not a correctness issue.
What I could not verify
- No Rust toolchain available in this sandbox — I did not build, run
cargo test,clippy, or the e2e suite myself. All conclusions above are from static reading of the diff and tracing call sites, not execution. The author's own PR comments report local test/clippy/e2e runs at this head (and prior rebased heads) as green, including the specificconfig-e2e scenarios and the Windows-targetcargo check, which I did not independently reproduce. - I did not re-verify CI status/check results for the current head via
gh. - I relied on the author's rebase comments to confirm that the three intervening rebases (onto
fcc646e9etc.) carried the fixed logic over unchanged; I spot-checked this by diffing specific files/functions rather than diffing the entire tree commit-by-commit.
🤖 Generated with Claude Code
nowycondro
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 92b1f7e
Review — needs work
Full review of the whole change (21 files, d59f596f6e08297c207f008021eecb37defc3558...92b1f7ed), via an 8-way blind fan-out plus direct verification of every cross-worker disagreement.
Fixes GitHub issue #515 ("a corrupt config.json is silently overwritten with defaults when the CLI records its managed Python") and names Jira ROCMAI-532 (status: In Progress, same bug). The PR description's detailed, itemized account of the change (the three original bugs, the atomic-write move, the Windows caveats, the explicit "Not in this PR" list) matches what every review pass independently found in the code — no scope mismatch.
Blocking
- CI check "E2E tests (Strix Halo, Windows, current)" is failing on the reviewed commit — reported by
gh pr view 541 --json statusCheckRollup(job117421948601, run39041532923), re-confirmed immediately before this review was posted (head still92b1f7ed, PR still OPEN/non-draft).
This is not a generic CI nit: the PR description itself flags the Windows arm of the new atomic-write primitive (ReplaceFileWpublish, and the partial-replace recovery path) as "type-checked, not run" prior to this PR, and states "the requiredwindows-build-and-testlane is its first real run."windows-build-and-test(the build/unit-test lane) did pass, but the separate Strix Halo Windows E2E lane — which is where the real filesystem-levelReplaceFileWbehavior actually executes — is red. That is exactly the gap the author called out as unverified. A reasonable reviewer needs this resolved (or shown to be an unrelated, already-tracked flake with evidence) before merging a PR whose entire point is durable, correct atomic writes.
Confidence 100 · architectural · Fix: inspect the Strix Halo Windows E2E job's log and either fix the real failure in the Windows atomic-write/replace path, or if it's confirmed unrelated/flaky, say so on the PR with evidence and get it re-run green.
Non-blocking
- Newer, more durable
atomic_writeprimitive isn't used everywhere in the same crate —crates/rocm-core/src/lib.rs:502still defines the older, weakerwrite_atomic(no directory fsync, no symlink-target resolution, no Unix permission preservation), andcrates/rocm-core/src/automation.rs:223,368andcrates/rocm-core/src/service_record.rs:345still call it for automation audit logs/proposals and service manifests, instead of the newrocm_core::atomic_writemodule this PR introduces. Pre-existing, not introduced by this diff, and explicitly out of this PR's stated scope (the PR's "Not in this PR" list covers the runtime-registry/managed-Python manifests and the rocm-dash TUI config, but not these two). A reasonable follow-up for consistency, not a defect in what's here.
Confidence 70 · architectural · Fix: file a follow-up to migrateautomation.rs/service_record.rsontorocm_core::atomic_write, or note why the weaker primitive is acceptable there. - Cosmetic module-declaration placement inconsistency —
apps/rocm/src/therock.rs: the newpersisted_state_propsproptest child-module declaration sits near the bottom of the file, while the pre-existing, same-patternversion_proptestsdeclaration sits at the top. No effect on compilation or correctness.
Confidence 70 · mechanical · Fix: move the new declaration next toversion_proptestsfor consistency, if touching the file again. - Slightly weaker assertion in one e2e step —
tests/e2e-cucumber/tests/e2e/config_steps.rs, the "Unreadable settings file" /refuses_to_change_settingstep only assertsrc != 0and that stderr contains the bare substring"config.json", noticeably weaker than the multi-needle check used a few steps earlier in the same scenario (config-09). Plausibly deliberate — the richer message is already verified once in-scenario, and a comment notes repair is not exercised here — so this is flagged as a minor test-strength note rather than a defect.
Confidence 70 · logic · Fix: consider asserting the same multi-needle message check here too, for symmetry with the earlier step in the same scenario.
Decisions for the author
None — the one real tradeoff in this change (many more commands now hard-fail on a corrupt config.json instead of silently falling back to defaults) is already deliberated and justified at length in the PR description's "Blast radius" section, with the two escape hatches (ROCM_CLI_DATA_DIR, rocm diagnose) called out; nothing here needs further confirmation from the author.
Positive signals
- Mutation-tested fix discipline: the PR description states a full mutation pass over the final tree caught all 12 injected mutations, and multiple independent review passes confirmed specific tests fail when a specific branch is mutated or a fix is individually reverted (e.g.
managed_python_recording_never_replaces_an_unreadable_config_with_defaults, the per-call-siteensure_managed_python_reuse_path_fails_on_an_unreadable_config/..._install_path_fails_on_...pair) — not vacuous happy-path-only tests. - Clean, verified dedup: the stage-then-rename writer previously duplicated (with drift) between
apps/rocm/src/therock.rsandapps/rocmd/src/sandbox.rsis now one implementation incrates/rocm-core/src/atomic_write.rs; grepping both call sites for leftover inline rename/tempfile/ReplaceFileWlogic and for dangling references to the removed functions turned up nothing, and the correspondingwindows-sysdependency removals fromapps/rocm/Cargo.toml/apps/rocmd/Cargo.tomlline up exactly with what was deleted. - Honest, explicit disclosure of verification gaps — the PR description itself names which Windows-only paths were only type-checked before this PR (see the Blocking finding above), and a confirmed pre-existing, unrelated bug (
runtime_manifest_round_trips_through_the_registry, non-UTF-8 install root) is left#[ignore]'d with a comment explaining why, rather than hidden or worked around. - User-facing recovery messaging is itself test-verified: the new "cannot tell where rocm-cli keeps its data" error names the file, the recovered folder (only when determinable from the surviving bytes), and the exact
ROCM_CLI_DATA_DIR=workaround, and e2e scenarioconfig-09exercises that the suggested override actually works, that the file stays byte-for-byte unchanged throughout, and that moving the file aside resets to defaults.
Deployment notes
- User-facing behavior change, by design: many
rocm/rocmdcommands that previously tolerated an unreadableconfig.jsonby silently falling back to default paths/settings now hard-fail with a descriptive error unlessROCM_CLI_DATA_DIRis set. Anyone upgrading with an already-corruptedconfig.jsonwill see new failures post-upgrade. This is thoroughly documented in the PR body's "Blast radius" section and in the README changes, but is worth calling out explicitly in release notes since it changes observable CLI behavior for an existing (if buggy) state. apps/rocmdandapps/rocmdrop their directwindows-sysdependency (moved intorocm-core, which already carries it undercfg(target_os = "windows")); verified nothing else in either crate referenced it.
What this covered
Every file in the diff, read completely at the PR head, across 8 independent fan-out workers grouped by coupling (core atomic_write.rs/config.rs/paths.rs/lib.rs + their property tests; therock.rs + its proptest file; runtimes_cmd.rs; sandbox.rs + the three dependency-manifest files; the e2e-cucumber config suite; docs + coverage-floors.toml; AGENTS.md/CONTRIBUTING.md adherence; git history on the core files), plus my own direct verification of: the coverage-floors.toml change against xtask/src/coverage.rs's TOLERANCE constant and the file's own "do not edit by hand / re-bless" header (the one cross-worker disagreement — resolved as not a violation: TOLERANCE bounds runtime coverage vs. the recorded floor, not how much the floor file itself may change on a re-bless, and the PR's own commit list explicitly documents lowering it), the sibling rocm-dash-core::Config::load_default() (same load-defaults-then-save bug pattern but a different file/crate, no write-back path found, and explicitly listed in the PR's own "Not in this PR" section — correctly out of scope), GitHub issue #515 and Jira ROCMAI-532 (title/summary/status only), and the live PR/CI state (re-checked immediately before posting: head 92b1f7ed, OPEN, non-draft).
d59f596f6e08297c207f008021eecb37defc3558...92b1f7edc046daac22d8a0edde635de197198564.
Did not run: "prior changes" (review comments on earlier PRs touching these files) — not retrievable without reading GitHub PR discussion, which this review is barred from; substituted a best-effort local git log subject-line scan (found nothing beyond the already-cited, directly-relevant recent commits on config.rs/paths.rs). Did not independently execute the test suite or the failing CI job's logs — the job logs were not fetchable from this environment (proxy-blocked); relied on the CI status as reported by gh, per the method's instruction to take CI at face value. Not reconciled against any prior review or discussion thread on this PR — this is a fresh, first review.
jussielo-amd
left a comment
There was a problem hiding this comment.
Review — needs work
Full review of the current head (92b1f7ed) against merge-base fcc646e9. The branch was rebased since my earlier review at 2a40e182, so I re-reviewed the whole change rather than only what moved. Closes #515 as described.
Blocking
-
The managed-Python error advises "move it aside to start from default settings", which silently relocates a custom install's data dir. —
apps/rocm/src/therock.rs:5628
This error is reached whenAppPaths::discoversucceeded andRocmCliConfig::loadthen failed on some other field (e.g. a retypedchat_top_p).paths.rssays discovery still returns the recorded root in that case. Following the advice makesconfig.jsonmissing, so discovery silently falls back to the default data dir and a--prefixinstall's runtimes vanish, which is the harm this PR exists to prevent.paths.rs:395,README.md:950and the PR's own "blast radius B" section all say that moving the file aside needsROCM_CLI_DATA_DIR; this message does not. No test follows this advice to its result (AGENTS.md§3 asks for message and state to be asserted together).paths.rsand the README govern, since they describe the real effect.
Fix: drop the "move it aside" clause or reuse the discovery error's wording, and add a test asserting the message together with the data dir after the advice is followed. -
The description and a doc comment misstate what
savedid before this PR. —crates/rocm-core/src/config.rs:557-562, PR body ("saveusedfs::write(truncate, then write)", "Atomic, durable, faithful saves")
At the merge-basesavealready calledcrate::write_atomic(temp file, then rename): baseconfig.rs:559,lib.rs:502, and the base doc comment even says "destination is never removed first". A full disk mid-write therefore failed on the temp file and left the oldconfig.jsonalone. What this PR actually adds is fsync, mode preservation, symlink handling, the parse-back check and the shared writer. The doc comment now states false history, and several added tests (saving_the_config_replaces_it_without_leaving_a_temp_file_behind,a_config_save_that_cannot_publish_cleans_up_its_temp_file, the inode tests) pass on the base, so "every fix was shown red before" does not hold for this part.
Fix: narrow the description to what is new, and state only guarantees in the comment. -
Two docs disagree with the code after this change.
docs/architecture.md:94still sayswrite_atomicis shared byconfig.rs,automation.rsandservice_record.rs; line 77 of the same file andconfig.rs:587sayRocmCliConfig::saveno longer uses it. The code governs.docs/testing.md:157says each property prints areach:line;unknown_keys_survive_a_load_save_cycle,windows_path_normalization_is_idempotentandstartup_update_check_record_round_tripsprint none. The code governs.
AGENTS.md§5 requires claims repeated across docs to stay in sync. Fix: dropconfig.rsfrom line 94, and reword the sentence or add the counters.
-
The new Windows partial-replace recovery has no test. —
crates/rocm-core/src/atomic_write.rs:335-396
The classification ofERROR_UNABLE_TO_MOVE_REPLACEMENT/_2and the retry rename (fs::rename(replacement, path),:390) run in no test on any platform. The "keeps the new contents" message is tested by injecting a publish closure, which never reaches this code, and the description itself says it is "type-checked, not run".
Fix: pull the "is this a partial replace" check into a pure function and unit-test it; inject a rename failure to cover the retry. -
The last Then of
config-09claims more than it checks. —tests/e2e-cucumber/tests/e2e/config_steps.rs:475
"the CLI keeps its data in its default folder" isassert_ne!(data_dir, custom_rocm_folder), so any wrong data dir passes. "On default settings" (active runtime key: <unset>) is already true while the file is unreadable, so it cannot show that moving the file aside reset anything. The description says "the data dir is the default folder again", and the scenario title says it stops the CLI "hiding the user's runtimes", but no runtime is ever planted.
Fix: compare against<home>/.rocmand plant a runtime in the custom folder. -
E2E tests (Strix Halo, Windows, current)is red on this head (run 39041532923). Four scenarios fail: the PowerShellSetEnvironmentVariable/setxPATH step, two "resolved model" steps, andruntime-02"managed runtime folder path is not recursively nested". The same four failed on main's own runs 38911370206 and 38909090099 on 2026-10-16, and recent main runs are green; the newconfig-09passed on Windows. This looks inherited from an old base rather than caused by this change. The branch is 66 commits behind main, so please rebase and re-run.
Non-blocking
- Staged file is written at default permissions and chmod'ed afterwards —
atomic_write.rs:180-212. A0600config.jsonholding the dashboard token is briefly world-readable in its temp file, and a crash in the gap leaves aconfig.json.tmp-*that nothing sweeps. Still better than the base, which published at the umask default. Fix: create the temp file with the existing mode. - After a Windows partial replace there is no
config.json, andloadreads that as "no settings" —config.rs:567-576. Thesavedoc admits it; the next save (e.g. the managed-Python record) would write defaults over settings that survive only inconfig.json.tmp-*, which is this PR's own hazard in a rare path. The README sentence "an interrupted save leaves the previous file" (README.md:938) omits it. Fix: haveloadtreat a missingconfig.jsonwith a leftoverconfig.json.tmp-*beside it as unreadable, or add the caveat to the README. rocm dashpanics on tick values thatloadandsaveaccept —apps/rocm/src/dash.rs:54-56(Duration::from_secs_f64on a negative or overflowing*_tick_secs). Pre-existing, but it sits in the same validity question the PR settles for migration; thegpu_tick()accessors inconfig.rsalready guard it.
Decisions for the author
- A damaged
config.jsonnow stops almost everything unlessROCM_CLI_DATA_DIRis set, including all ofrocmd(tradeoff). One side: silent relocation of the data dir and writes to the wrong place. The other: a lockout ofexamine,uninstallandremote, with onlydiagnoseexempt. The description documents this; please confirm a lenient discovery with a warning for read-only commands was considered. - Lenient loaders still run on defaults without saying so (tradeoff), e.g.
apps/rocm/src/main.rs:7799andapps/rocmd/src/mcp.rs:597:gpu_snapshotwith an unreadable file runs with telemetry at its default even if the user set it off. They never save, so the file is safe, but nothing prints a notice. - A refused legacy-TOML migration is never retried (tradeoff): any later save creates
config.json, which closes the migration window for good. The refusal warns about this, but the command that prints it usually performs that save. - Property reach counters are printed, never asserted (non-blocking improvement).
docs/testing.mdconcedes a property with reach zero "passed without testing anything"; anassert!(reach > 0)after each run would make CI see it. unknown_keys_survive_a_load_save_cycleis#[ignore]d as a "by-design gap", whiledocs/testing.mdsays ignored reproducers are removed when fixed (non-blocking improvement). Track it as an issue or make it a real assertion; nothing runs--ignoredin CI.
Positive signals
app_paths_refuse_to_guess_the_data_dir_from_an_unreadable_config(paths.rs) asserts the advice and the resulting state together and drives each way out as printed.- The inode/hard-link witness test is a proper mutation check against an in-place rewrite.
- Using a directory at
config.jsonto produce a non-NotFound read error avoidschmodand root, so it works on every platform. - The "blast radius" section of the description lists what now refuses and why.
recorded_root_in_damaged_confignever guesses past a truncated string.
Deployment notes
- With
config.jsonunreadable andROCM_CLI_DATA_DIRunset, nearly everyrocmcommand and all ofrocmdnow stop with an error; a running daemon is unaffected until it restarts. - Every
config.jsonandrocmdwrite now fsyncs. Cargo.lockloseswindows-sysfromrocmandrocmd;rocm-coreenablesWin32_Storage_FileSystem.rocmd's coverage floor is lowered by hand incoverage-floors.toml.- The PR is 66 commits behind main; it is mergeable, but the Windows lane has not re-run against current main.
What this covered
Every changed file at 92b1f7ed (all 30 commits, 21 files), read in full, plus callers and consumers of the changed symbols, AGENTS.md, the README and docs diffs, issue #515, and the failing Windows E2E job log compared with main's runs on the same dates. I did not run a local build or tests; I relied on CI for compile and unit results (clippy, build-and-test, windows-build-and-test and the Linux E2E lanes pass). Not read in full: runtimes_cmd.rs test lines 1810-4296 and therock.rs tests outside the changed hunks. This review was not reconciled against the existing discussion on the PR.
Fixes #515.
Problem
RocmCliConfig::loadtreats a missingconfig.jsonas "use defaults" and an unreadable one as an error. Three code paths ignored that difference:python3-venv),record_managed_python_configloaded the config withunwrap_or_default(), added the interpreter and saved. Both callers ignored the result. An unreadable config was silently replaced with defaults, losing the active and default runtime, the TheRock folder, providers, permissions mode, engine preferences and dashboard chat settings. Every kind of damage did this: truncation, anulltick, a field with the wrong type, an out-of-range value.AppPaths::discoverreadssetup.therock_venvfrom the same file to find the data dir, and turned any read or parse error into "nothing recorded". A truncated config moved the data dir (registry, active-runtime marker, service records) back to the default without a message: a--prefixinstall's runtimes vanished, and new state landed in the wrong place.saveusedfs::write(truncate, then write), so a full disk mid-install left a truncated file. Non-finite dashboard ticks serialized as JSONnull, whichloadrejects, and legacy rocm-dash TOML migration copied them unchecked. rocm-dash's own reader already rejects such a file, so the two readers disagreed.What changes
savewrites through a stage-then-rename writer. The staged data is flushed before the rename; on Unix the directory is flushed after it. Bytes are parsed back before anything is written, so a valueloadwould reject fails the save and leaves the old file alone. The writer keeps the replaced file's Unix mode (a0600file stays0600). Ifconfig.jsonis a symlink (a dotfiles checkout, say), the link's target is replaced and the link survives. A symlink loop is an error.load. A missing file still means "nothing recorded". An unreadable one (unparseable, wrong shape, or an I/O error such as a permission problem) is an error. Whenloadaccepts the file, discovery reads it exactly asloaddoes. A valid-JSON file thatloadrejects for some other field still yields the recorded data dir;loadreports that field itself.loaditself draws the same line: only a missing file means defaults, and a directory atconfig.json, a permission error or a symlink loop is an error.Duration::try_from_secs_f64rejects (NaN, ±inf, negative, overflow) refuses the migration. The key is named, nothing is written and the TOML is untouched. Replacing the bad value with a default was rejected: rocm-cli would then honour a file the dashboard ignores. The refusal also warns that migration only runs whileconfig.jsondoesn't exist.The recovery advice is checked against what each step actually does
Example of the new discovery error, for a file cut off just after the folder:
The folder is named only when the damaged bytes still hold the complete string; a value cut off part-way is never guessed at. Unit tests follow each piece of advice and check the result:
ROCM_CLI_DATA_DIRfinds the same data dir the healthy file did;loadstill fails, and the lenient read-only loaders run on default settings without saving;Blast radius: what now refuses on a corrupt
config.jsonThis change makes more commands stop, on purpose. There are two cases.
A — the file can't be parsed or read (for example, truncated), and
ROCM_CLI_DATA_DIRis unset.AppPaths::discovernow errors, so every command that finds its paths that way stops with the message above:rocmitself:version, barerocm(launcher), freeform requests,setup,install,runtimes,config,engines,serve,services,comfyui,examine,storage,automations,uninstall,remote(serve/status/attach/stop),dash/chat/benchand the dashboard flows, and the engine shell.rocmd:run,supervise,status,bridge-snapshot,sandbox-run. They all go throughrun_cli's discovery. A daemon that is already running is unaffected until it restarts. The daemon starts therocmbinary withROCM_CLI_DATA_DIRset, so those child runs skip this check.rocmgetROCM_CLI_DATA_DIRfrom their parent, so they skip this check; started by hand without it, they stop too.rocm diagnose, startup logging, the legacy-uv-cache notice and the startup update check. These already treat a discovery error as "no paths".Before this change these commands carried on against the default data dir. Read-only answers came out wrong ("ROCm SDK: not detected" with a runtime installed in the custom folder). Writes landed in the wrong place: an install downloaded into the default dir, then failed at activation anyway.
B — the file parses but
loadrejects a field (for example,chat_top_p: 1.5). Discovery still works. Commands that already loaded strictly fail as before. The one new failure is the managed-Python step ofrocm install sdk, which used to wipe the file here (rocm updatealready loads the config strictly before reaching it).Why this is safer: every command that fails now either already failed on this file further in, or was running against a guessed data dir whose answers were wrong and whose writes went elsewhere. Stopping costs one command and names the file. Carrying on cost the user's settings or runtimes, silently. Two escape hatches remain:
ROCM_CLI_DATA_DIRkeeps the read-only commands that load settings leniently working, on default settings. No lenient loader saves, so none overwrites the file.rocm diagnosestill runs.The writer move
The stage-then-rename writer lived only in the
rocmbinary (therock.rs), with a near-identical copy inrocmd. It now lives inrocm_core::atomic_write. Both binaries use it androcmd's copy is gone (cargo xtask check-crate-edgespasses;rocmdalready depended onrocm-core). Main has since movedrocmd's copy intoapps/rocmd/src/sandbox.rs(#483); it is removed from there.Its tests moved with it, and duplicates were merged. Only
write_file_atomicallyandstage_file_for_atomic_publishare public API.write_file_atomically_with_publishandpublish_temp_filestay public but#[doc(hidden)], because therocmbinary's metadata-cache tests use them to make the final rename fail or race.On Windows, a
ReplaceFileWthat fails part-way (ERROR_UNABLE_TO_MOVE_REPLACEMENTor_2) can leave the destination gone. The writer retries once as a plain rename; if the destination is still gone, it keeps the temp file and names it in the error rather than deleting the only copy of the new contents.It is now the one general writer. #546 later added
write_atomicallyinapps/rocm/src/comfyui/source_swap.rs, which stays separate because its temp-file prefix is what the swap's recovery removes and it carries the crash tests' step hook;docs/architecture.mdsays so.Side effects for
rocmd's users: a full disk is now reported through the shared out-of-space message, and every write is flushed (a little slower, deliberately).Dependencies
No new dependency edges: proptest was already a dev-dependency of
apps/rocmon main.Cargo.lockonly loseswindows-sysfromrocmdandrocm, whose only use was their ownReplaceFileWcopy (cargo xtask manifest --checkpasses).rocm-coreturns on windows-sys'sWin32_Storage_FileSystemfeature forReplaceFileW.Commits
test: property-test persisted config, registry and marker state— proptest properties for persisted state. They were committed with#[ignore = "CONFIRMED BUG: …"]reproducers; the later commits un-ignore the ones they fix:config_save_never_writes_a_file_load_rejects,legacy_toml_migration_always_produces_a_loadable_config,data_dir_never_silently_relocates_when_config_is_unreadable,managed_python_recording_never_replaces_an_unreadable_config_with_defaults,every_corruption_class_is_wiped_by_managed_python_recording(renamedno_corruption_class_is_wiped_by_managed_python_recording),legacy_inf_tick_migration_then_python_bootstrap_loses_migrated_settings(rewritten aslegacy_inf_tick_never_reaches_config_json);runtime_manifest_round_trips_through_the_registry(a non-UTF-8 install folder is written lossily; ignored on Unix, and runs and passes on Windows, where such names cannot occur) andunknown_keys_survive_a_load_save_cycle(an older binary drops keys a newer one wrote).savethat refuses contentsloadwould reject.rocmandrocmd, tested in rocm-core.config.jsonexists.13–18. test: the property tests run on Windows (install roots Windows can store, the host's path form, unix-only helpers); clippy fixes; rocmd's coverage floor lowered for the writer it no longer owns.
loadtreats only a missingconfig.jsonas no settings.ROCM_CLI_DATA_DIRvalue the refusal printed, read from its stderr.runtimes_cmdafter ROCMAI-89: extract runtimes command and SDK-install-finalization clusters #653 made the marker functions private there.ROCM_CLI_DATA_DIRdoes to a broken settings file; config-09 checks each claim; config-10 and config-11; discovery's redundant typed parse dropped.reach:lines need--nocapture.rocm's unused windows-sys.Tests
Ok(None)", "callers back tolet _ =").config.jsonthat is a directory. That is a non-NotFound error on every platform, with nochmodor root needed.ensure_managed_pythonis driven end to end against a stand-inuvplanted where the managed copy lives (no download, no environment override). On the reuse path it must fail beforeuv python installruns; on the install path, before returning success. Both leave the file byte-for-byte. Unix only: the stand-ins are shell scripts, as in the existing resolver tests, so the Windows lane does not run them.config-09(@id:config-unreadable-settings-not-guessed, mock lane, every OS). It plants aconfig.jsoncut short after it recorded a custom folder, runsrocm versionwithoutROCM_CLI_DATA_DIR, and asserts:ROCM_CLI_DATA_DIR=value;config-10(@id:config-save-keeps-a-private-settings-file-private) andconfig-11(@id:config-save-keeps-a-linked-settings-file-linked), mock lane, Linux only (@requires-os:linux): a0600settings file stays0600after a save, and a settings file that is a link stays a link while its target gets the new setting.rocm install sdk. Reaching it needs a host whose system Python can't create a venv, plusuvand a Python download. It's covered by the unit and property tests above instead.What the Windows lane does and does not exercise
ensure_managed_python_*_fails_on_an_unreadable_configtests are#[cfg(unix)], because the stand-inuvand Python are shell scripts. The Windows lane therefore never runs the managed-Python caller tests; on Windows those callers are covered only by the shared record-function tests and the property tests, which do run there.ReplaceFileWpublish, and keeping permissions by relying onReplaceFileWrather than copying them) was type-checked againstx86_64-pc-windows-msvcin a scratch crate. A deliberately injected type error showed the check really compiles that code, but nothing executed it. The requiredwindows-build-and-testlane is its first real run. The same holds for the partial-replace recovery: it is type-checked, and the Windows lane is its first run.saving_the_config_replaces_the_file_rather_than_rewriting_it_in_placehas a Unix form (it watches the old file through a hard link and its inode) and a Windows form (GetFileInformationByHandle); the duplicate Unix writer test it overlapped with is folded into it.Docs
docs/manual-testing.md: an optional unreadable-settings check.docs/testing.md: how to run the persisted-state properties and the--ignoredreproducers.Not in this PR
rocm diagnose/rocm fixrecognise an unreadableconfig.json(separate issue; it belongs to the doctor-skill catalog).persist_chat_endpoint, which does the same load-defaults-then-save on the legacyconfig.toml(separate issue).