fix: read strictly before a merge-on-write so a failed read cannot wipe the file - #456
Merged
Merged
Conversation
… read (#446) RED contract for #446. A failed read (any OSError but the file being absent) at the four merge-on-write sites must raise and leave the file byte-identical: roster.record_occurrence, sessions.write_session_meta, the voice-mapping route's own read, and absorb_session on both sides. The lenient read paths stay lenient, and a torn file still reads as empty.
model=koishi/qwen3.8-flash-next-mtp gate='python3 -m pytest tests/test_merge_on_write_strict_read.py tests/test_roster.py tests/test_session_maintenance_absorb.py tests/test_routes_diarize.py tests/test_voices.py && ruff check tapscribe tests && ruff format --check tapscribe tests'
model=koishi/qwen3.8-flash-next-mtp gate='python3 -m pytest tests/test_merge_on_write_strict_read.py tests/test_roster.py tests/test_session_maintenance_absorb.py tests/test_routes_diarize.py tests/test_voices.py && ruff check tapscribe tests && ruff format --check tapscribe tests'
…pe the file (#446) A merge-on-write over a lenient read treated any OSError as an empty file. One transient read failure then replaced a roster or a session's meta with the caller's partial update. The merge paths now use a strict loader that raises on every OSError other than a missing file, and the public readers stay lenient. Applies the code-review and simplify fixes on top of the ralph iterations.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A merge-on-write read a JSON file, merged a partial update into it and wrote the whole file back. Its read treated every
OSErroras an empty file. One transient read failure (EACCES, EMFILE, EIO) then replaced the whole file with the partial update.Each merge path now reads through a strict loader: a missing or torn file still reads as empty, and every other
OSErrorraises, so the write does not happen. The public readers stay lenient, so a bad file never crashes a poll tick. The pattern is the onevoices.pyuses since #438.Sites
roster.record_occurrencereads through the newload_roster.read_rosterstays lenient.sessions.write_session_metareads through the newload_session_meta, over a strict_read_json_strictthat holds the containment check.read_session_metaand_read_json_or_nonestay lenient.routes/people.py) reads the voices map strictly, so a failed read cannot write back a map holding only the new key. TheOSErrorpropagates as a 500 before either the meta orpeople.jsonis written.session_maintenance.absorb_sessiondoes every strict read of both sessions' roster and meta ahead of the WAV moves, so a refusal leaves nothing half-done and the source folder is not deleted.strip_meta.load_strip_meta,voices.load_voices) read strictly with the other fold inputs, so they no longer overwrite a target file that failed to read;config_store.read_json_strictis the one strict JSON read the loaders share, and it treats a path that is not a regular file as absent (opening a FIFO would block);session_is_emptytreats a meta it cannot read as not empty, because that meta may hold a label and prune deletes on "empty".tests/e2e/test_dashboard_ui.pyreadstextContent, notinnerText, because Chromium reports an emptyinnerTextundercontent-visibility: auto.Behaviour change to know about
A
/tapopen on a session whose roster cannot be read now fails, instead of silently resetting the roster. A bridge's reconnect retries into it until the file is readable again. That is the intended trade: the roster is the only record of a recording's full identity and its multi-personmode.Tests
tests/test_merge_on_write_strict_read.pypins every site: a failed read raises and leaves the file byte-identical, an absent or torn file still reads as empty, and the lenient readers never raise.pytest tests --ignore=tests/e2e,ruff checkandruff format --checkpass, and so does CI with CodeQL.Closes #446
Implemented by the local model in the delegation experiment, then verified and reviewed by Claude.