Legacy backup recovery overwrites newer source and deletes the backup without conflict detection

Open
#34 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 2 days

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
Half a day
Newbie friendliness
35/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
python
Domain
testing-qa

Research direction

Start with restore_backups in src/mutator/engine.py (around lines 17-31), which writes and unlinks files under target/mutator-backup. Run the reproduction probe from the issue to see the overwrite and deletion. The call site is in src/mutator/cli.py (around lines 486-488), and tests/test_engine.py has the existing backup test. Done means newer source survives, both versions are kept, a conflict is reported, and a separate test covers a verified interrupted mutant.

Written by the indexing model from the issue text.

Description

Summary

restore_backups treats every target/mutator-backup file as authoritative. A stale backup overwrites legitimate newer source without comparison or confirmation, then the backup is deleted. Non-scan CLI runs invoke this recovery automatically when source files are selected.

Verified on main at 830ec7e9c39664d753f8699015bc796df67727b9 with Python 3.14.8. No package installation is required for this reproduction.

Minimal reproduction

Run with Python 3.11+ and Git. This uses only disposable fixture files and does not install or invoke the mutator launcher.

work=$(mktemp -d)
git clone https://github.com/unclebob/mutator.git "$work/mutator"
cd "$work/mutator"
git checkout 830ec7e9c39664d753f8699015bc796df67727b9
python3 -B - <<'PY'
"""Run from a mutator checkout using Python 3.11+; standard library only."""
import ast
import tempfile
from pathlib import Path

path = Path('src/mutator/engine.py')
tree = ast.parse(path.read_text())
module = ast.parse('from __future__ import annotations')
module.body += [n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name == 'restore_backups']
g = {'Path': Path}
exec(compile(module, str(path), 'exec'), g)
with tempfile.TemporaryDirectory() as directory:
    root = Path(directory)
    source = root / 'calc.py'
    backup = root / 'target' / 'mutator-backup' / 'calc.py'
    backup.parent.mkdir(parents=True)
    backup.write_bytes(b'def answer():\n    return 1\n')
    # New legitimate code, not an in-place mutant left by an interrupted run.
    source.write_bytes(b'def answer():\n    return 42\n\ndef added():\n    return 7\n')
    newer = source.read_bytes()
    restored = g['restore_backups'](root)
    assert restored == [source]
    assert source.read_bytes() != newer
    assert source.read_bytes() == b'def answer():\n    return 1\n'
    assert not backup.exists()
    print('newer source overwritten with stale backup; backup deleted; no conflict reported')
PY

This standard-library probe executes the actual restore_backups function extracted with ast. All files are disposable fixtures inside TemporaryDirectory. It calls the recovery function directly rather than launching the CLI.

Observed output

newer source overwritten with stale backup; backup deleted; no conflict reported

Expected behavior

Do not overwrite legitimate newer source automatically. Preserve both versions on an ambiguous recovery and report a conflict, or require an explicit recovery decision backed by enough state to identify the interrupted mutant.

Actual behavior

A source file with a newer function and an added function is replaced with the stale backup; the added function disappears. The backup is unlinked and no conflict is reported.

Regression test

Place stale original bytes in target/mutator-backup and legitimate newer code at the corresponding source path. Invoke recovery and assert the newer source survives, both versions remain available, and the conflict is reported. Retain a separate test for an explicitly verified interrupted mutant.

The reproduction asserts the currently observed defect. A regression test should invert the defective-behavior assertions to enforce the expected behavior.

Source

Related #20 concerns final cleanup after current worker execution. This defect occurs before mutation execution in legacy restore_backups, so removing _restore_if_dirty does not fix it. No genuine duplicate found.

Dominant language
Python
Stars
112
Forks
53
PR merge metrics
No merged PRs in 30d

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from unclebob/mutator

All issues in unclebob/mutator

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.