Skip to content

fix(tests): write migration output back to the file that was read - #354

Open
kaceper11 wants to merge 1 commit into
simion:mainfrom
kaceper11:fix/settings-writeback-straddle
Open

kaceper11 wants to merge 1 commit into
simion:mainfrom
kaceper11:fix/settings-writeback-straddle

Conversation

@kaceper11

Copy link
Copy Markdown
Contributor

What this fixes

An intermittent full-suite failure simion saw while verifying #353:

agent_hooks::tests::a_clones_status_line_is_read_from_its_own_config_dir fails ~once per cargo test run, passes alone, pre-existing on main.

The mechanism

with_scratch_data_dir serializes tests that redirect TERMIC_DATA_DIR, but TERMIC_DATA_DIR is process-global: tests that read global_dir()-derived paths without the lock can still straddle a neighbour's scratch window.

That alone is a known hazard — but it produces writes through one specific path: the migration write-backs.

Both load_settings_in and load_projects_in resolve the record file once for the read, run their migrations, and then call save_*_in(id, …) — which re-resolves global_dir() a second time at write time. If a scratch window opens in the gap, the write lands in a directory the load never read from:

  • Foreign load_settings_inner resolves env → real/fixture path, reads a legacy-shaped settings.json, sets migrated → write-back re-resolves → a scratch window just opened → the migrated copy lands over that scratch's settings.json, wiping the agent the victim test just saved → its next-claude lookup falls back to ~/.claude → assert fails.
  • Reversed the same way: a load that resolved inside a scratch can write the migrated fixture into the developer's real data dir if the window closes mid-write.

Why not lock the accessor instead

Gating global_dir() (or the settings accessors) on DATA_DIR_LOCK deadlocks: PORT_ALLOC_LOCK is held across task-create flows that call load_settings_inner, while scratch closures call task-creation paths that need PORT_ALLOC_LOCK.

The fix

Both write-backs now write to the path already resolved for the read (save_settings_at(&f, …) / save_projects_at(&f, …)), which is what their own comments already claim — "the write has to land in the profile the records came from". save_settings_in/save_projects_in keep their signatures for explicit saves.

Honest caveats

  • I could not reproduce the flake locally (6 clean full-suite runs); it is ~1-in-N upstream. This removes the one mechanism found by auditing every set_var/save_settings_*/global_dir writer that can inject a foreign settings.json into a live scratch without an unlocked writer call.
  • Residual, unaddressed: unlocked readers still see whatever TERMIC_DATA_DIR points at mid-window (wrong data for their asserts — the "different test each time" signature from the comment in test_support.rs). Closing that needs the accessor gating ruled out above, or dropping the process-global seam entirely.

Test plan

  • cargo test — 1216 pass on this branch
  • Race window itself is not unit-testable without a seam between resolution and write

load_settings_in and load_projects_in resolve the record file for the
read, then re-resolve it through global_dir() for the write-back.
TERMIC_DATA_DIR is process-global and tests run in threads, so a
with_scratch_data_dir window can flip it between the two resolutions:
the migrated write lands in a settings.json/projects.json the load
never read from. That clobbers the scratch a neighbouring test just
wrote (the intermittent agent_hooks::...status_line failure), and
flipped the other way drops test content into the developer's real
data dir.

Keep the already-resolved path for the write-back; the callers' doc
comments already promise the write lands where the records came from.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@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.

1 participant