A complete candidate file is ready. The destination pathname already exists. GNU mv returns zero. Yet the old destination remains untouched, and the candidate is still sitting at its source pathname.
That state is not hypothetical in the bounded GNU coreutils behavior examined here. With GNU mv --update=none, skipping an existing destination does not itself induce failure, so exit status alone cannot answer the operational question that matters: did this particular complete file actually move to the exact pathname the release procedure required? The GNU 9.4 manual documents --update=none as a policy that skips existing destination files without making the skip a failure.
The answer requires three independent observations: the process result, the exact destination state, and the source state after the invocation. This article keeps that question deliberately narrow. It covers one synthetic ordinary file, one owned temporary directory tree, one writer, GNU coreutils 9.4, and a Python standard-library controller. It does not evaluate application activation, crash durability, remote filesystems, concurrent publication, incomplete build generation, timestamp selection, archive extraction, or cross-filesystem moves.
The supplied reference execution was a synthetic preparation run on October 31, 2026, not an incident report. Its purpose is to turn mv behavior into reviewable evidence rather than infer success from a return code.
01. Declare What a Completed Move Must Leave Behind
Start with a contract that exists before mv runs. In this fixture, candidate.bin is a complete authored file containing the literal bytes ARTIFACT=new\n. The intended destination is the exact pathname release.bin. Where a pre-existing destination is required for a collision control, it contains ARTIFACT=old\n.
The required-move contract is intentionally stronger than “the destination eventually contains the right bytes.” Acceptance requires all of the following after the invocation: candidate.bin no longer exists; release.bin exists as a regular file rather than a directory; and release.bin contains the authored NEW bytes. In the same-filesystem fixture, the destination's device/inode pair is also compared with the pre-move source as corroborating evidence that the same local filesystem object was renamed. Python documents st_ino, st_dev, and st_nlink as stat-result fields describing inode identity within a device, device identity, and hard-link count respectively.
That contract assumes the candidate was already complete. It deliberately says nothing about how an upstream packaging process chose the file. If that is the unresolved question, treat it separately as a release-file selection before the move problem rather than weakening the move acceptance test.
The fixture also requires the candidate's st_nlink to be one before each case. That makes the constructed source an independent ordinary file for this experiment; it is not a general artifact-identity scheme.
Source Consumption and Content Reuse Are Different Contracts
An organization may legitimately use a different rule: if an existing destination already has identical content, perhaps no namespace move is necessary. That can be a sound idempotent content policy when declared in advance.
It is not the contract tested here. M03 deliberately constructs two independent files with equal bytes. If both paths remain afterward, a checksum-only rule could say “desired content is present,” while the required-move rule must say “source consumption did not occur.” Neither answer is inherently universal; the error is silently changing policies after seeing the result. Content equality can support reuse. It does not prove that candidate.bin was moved.
02. Pin the Executable Before Interpreting Its Status
Before interpreting a GNU mv return code, identify the executable whose behavior you are qualifying. The fixture resolves mv from os.defpath, resolves that pathname to its final path, runs --version, and requires the first version line to be exactly mv (GNU coreutils) 9.4 before any test mutation occurs.
Every invocation uses the resolved executable directly rather than passing a shell command string. Python's subprocess.run() waits for the command and returns a CompletedProcess; with capture_output=True, stdout and stderr are captured separately, and an explicit env mapping replaces the ordinary inherited environment for the child. The fixture also supplies a ten-second timeout, though the reference run did not exercise a timeout failure.
The supplied preparation run records CPython 3.12.14 on Linux-6.18.57-x86_64-with-glibc2.39, UID 0, /usr/bin/mv, and GNU coreutils 9.4. The measured mv binary SHA-256 was 31be03602835c3a6d9b0b11ab1fe52e99bfe91638a3f3b151a8bb0d9d1998b41. The controller source digest supplied with the reference execution is fce5db6544da106446af565c276c003fdce226aec27959e57597e023273d5f6e.
Those hashes answer different questions. The source digest identifies the controller bytes; the executable digest identifies the measured binary; per-file hashes in the JSON supplement literal byte comparisons. None grants permission to publish software, proves another operator reran the experiment, or establishes that an application consumed a pathname.
The Python 3.12 documentation retrieved for this research identified itself as 3.12.16, while the measured interpreter was 3.12.14. That distinction should remain visible rather than being flattened into “Python 3.12.”
Keep the 9.4 Observation Separate From the 9.5 Release Note
GNU's version history matters because mv -n status behavior changed. The retained 9.3 NEWS entry says cp -n and mv -n began issuing an error diagnostic when skipping a file, corresponding to the nonzero status introduced in coreutils 9.2. The same 9.3 release introduced --update=none, which skips existing destination files without affecting status.
Coreutils 9.5 then reverted mv -n to the pre-9.2 behavior of skipping existing destinations without failure and introduced --update=none-fail, which diagnoses an existing destination and exits with failure.
That 9.5 information is documentary evidence, not a measured 9.5 execution here. M04 and M05 are 9.4 observations. Copying their expected return codes into a ledger for another binary without requalification would erase the very version dependency the controls are designed to expose.
03. Separate Collision Policy From the Exact Destination
There are two different questions hidden in a seemingly simple move command.
The first is collision policy: what should happen if an entry already occupies the destination pathname? In GNU coreutils 9.4, --update=none means existing destination files are not replaced, and skipping one does not itself induce failure. The manual's whichUpdate definition explicitly separates none, older, and all policies.
The second is destination interpretation: does the final operand name the exact target entry, or can it be interpreted as a directory into which the source basename is placed? GNU documents -T, or --no-target-directory, as preventing special treatment of the final operand when it is a directory or a symlink to a directory. Without -T, mv source dest can become a move to dest/source when dest is a directory.
These controls are independent. -T establishes how the final operand is interpreted. It does not compel replacement when another option, such as --update=none, permits an existing file to be skipped.
That distinction also keeps this workflow separate from rsync transfer-selection checks. There is no timestamp-authority matrix here and no synchronization quick-check. The question is source consumption plus exact local destination state after one move invocation.
Keep the Laboratory on One Owned Filesystem
All cases live under one TemporaryDirectory. Python documents that context-managed temporary directories and their contents are removed when the context completes normally.
Every move adds --no-copy. GNU documents that option as requesting failure rather than copy-and-remove fallback when a rename cannot occur because the destination filesystem differs.
That is a constraint, not a cross-filesystem experiment. No cross-filesystem case was run. The resulting claim therefore stays local: one owned filesystem tree, synthetic files, no concurrent writer, no symlink-security test, no metadata-preservation guarantee, and no persistence or interruption claim.
04. Run the Complete Ten-Case Experiment
Save the following source as cloud_mv_fixture.py in an ordinary writable working directory outside an existing project. The source accepts no external pathname. It creates its own disposable workspace, uses only the Python standard library, and expects GNU coreutils 9.4 before it mutates any case.
"""Owned GNU mv collision and exact-destination experiment; no external paths."""
import hashlib
import json
import os
import platform
import shutil
import stat
import subprocess
import sys
import tempfile
from datetime import datetime, timezone
from pathlib import Path
NEW = b"ARTIFACT=new\n"
OLD = b"ARTIFACT=old\n"
SENTINEL = b"OWNED-SENTINEL\n"
ENV = {"PATH": os.defpath, "LC_ALL": "C", "LANG": "C", "TZ": "UTC"}
def require(condition, message):
if not condition:
raise RuntimeError("HOLD: " + message)
def sha(data):
return hashlib.sha256(data).hexdigest()
def inventory(root):
result = {}
for path in sorted(root.rglob("*")):
info = path.lstat()
require(stat.S_ISREG(info.st_mode) or stat.S_ISDIR(info.st_mode),
"unexpected file type")
row = {"kind": "file" if stat.S_ISREG(info.st_mode) else "directory",
"device": info.st_dev, "inode": info.st_ino, "nlink": info.st_nlink}
if row["kind"] == "file":
data = path.read_bytes()
row.update(bytes_hex=data.hex(), sha256=sha(data), size=len(data))
result[str(path.relative_to(root))] = row
return result
def expected_inventory(mapping):
return {name: {"kind": "directory" if value is None else "file",
**({} if value is None else {"bytes_hex": value.hex()})}
for name, value in mapping.items()}
def verify_inventory(actual, expected):
observed = {name: {key: value for key, value in item.items()
if key in ("kind", "bytes_hex")}
for name, item in actual.items()}
require(observed == expected_inventory(expected), "unexpected path set or bytes")
def same_object(a, b):
return (a["device"], a["inode"]) == (b["device"], b["inode"])
def move_postcondition(after, source, destination):
target = after.get(destination)
return source not in after and target is not None and target["kind"] == "file" \
and target["bytes_hex"] == NEW.hex()
def main(report):
require(os.name == "posix", "POSIX environment required")
executable = shutil.which("mv", path=os.defpath)
require(executable is not None, "GNU mv required")
executable = str(Path(executable).resolve())
def invoke(args):
result = subprocess.run([executable, args], env=ENV, stdin=subprocess.DEVNULL,
capture_output=True, text=True, timeout=10)
return {"argv": result.args, "returncode": result.returncode,
"stdout": result.stdout, "stderr": result.stderr}
version = invoke(["--version"])
require(version["returncode"] == 0, "version probe failed")
require(version["stdout"].splitlines()[0] == "mv (GNU coreutils) 9.4",
"fixture baseline is GNU coreutils 9.4")
report["environment"].update(mv_version=version["stdout"].splitlines()[0],
mv_path=executable, mv_sha256=sha(Path(executable).read_bytes()))
report["version_probe"] = version
report["oracles"] = {"NEW": NEW.hex(), "OLD": OLD.hex(), "SENTINEL": SENTINEL.hex()}
with tempfile.TemporaryDirectory(prefix="owned-mv-contract-") as temporary:
root = Path(temporary)
def record(case_id, work, flags, expected, zero, moved, decision,
source="candidate.bin", destination="release.bin"):
before = inventory(work)
source_before = before.get(source)
if source_before:
require(source_before["kind"] == "file" and source_before["nlink"] == 1,
"source must be an independent ordinary file")
require(source_before["bytes_hex"] == NEW.hex(), "source oracle mismatch")
require(source_before["device"] == work.stat().st_dev, "filesystem mismatch")
command = invoke(["--no-copy", flags, "--", str(work / source), str(work / destination)])
after = inventory(work)
require((command["returncode"] == 0) == zero, case_id + " unexpected exit class")
verify_inventory(after, expected)
matches = move_postcondition(after, source, destination)
require(matches == moved, case_id + " incorrect acceptance decision")
if moved:
require(source_before is not None and same_object(source_before, after[destination]),
case_id + " moved object identity mismatch")
if source in after and source_before:
require(after[source] == source_before, case_id + " retained source changed")
if destination in before and destination in after and not moved:
require(after[destination] == before[destination], case_id + " existing destination changed")
row = {"case": case_id, "before": before, "command": command, "after": after,
"move_contract_matches": matches, "decision": decision, "pass": True}
report["cases"].append(row)
return row
def setup(case_id, destination=None, source=True, directory=False):
work = root / case_id
work.mkdir()
if source:
(work / "candidate.bin").write_bytes(NEW)
if directory:
(work / "release.bin").mkdir()
(work / "release.bin/sentinel.txt").write_bytes(SENTINEL)
elif destination is not None:
(work / "release.bin").write_bytes(destination)
return work
exact_none = ["-T", "--update=none"]
record("M01_absent", setup("M01"), exact_none,
{"release.bin": NEW}, True, True, "ACCEPT_LOCAL_MOVE")
collision = setup("M02", OLD)
rejected = record("M02_collision", collision, exact_none,
{"candidate.bin": NEW, "release.bin": OLD}, True, False, "HOLD_COLLISION")
equal = record("M03_equal_bytes_collision", setup("M03", NEW), exact_none,
{"candidate.bin": NEW, "release.bin": NEW}, True, False, "HOLD_NOT_MOVED")
require(not same_object(equal["before"]["candidate.bin"], equal["before"]["release.bin"]),
"equal bytes must be different objects")
record("M04_n_94", setup("M04", OLD), ["-T", "-n"],
{"candidate.bin": NEW, "release.bin": OLD}, False, False, "HOLD_COLLISION_VERSION_94")
record("M05_none_fail_unsupported", setup("M05", OLD), ["-T", "--update=none-fail"],
{"candidate.bin": NEW, "release.bin": OLD}, False, False, "HOLD_UNSUPPORTED_OPTION")
record("M06_implicit_directory", setup("M06", directory=True), [],
{"release.bin": None, "release.bin/sentinel.txt": SENTINEL,
"release.bin/candidate.bin": NEW}, True, False, "HOLD_WRONG_DESTINATION_TYPE")
record("M07_exact_directory_rejected", setup("M07", directory=True), ["-T"],
{"candidate.bin": NEW, "release.bin": None,
"release.bin/sentinel.txt": SENTINEL}, False, False, "HOLD_WRONG_DESTINATION_TYPE")
record("M08_explicit_replacement", setup("M08", OLD), ["-T", "--update=all"],
{"release.bin": NEW}, True, True, "ACCEPT_OWNED_REPLACEMENT")
record("M09_missing_source", setup("M09", OLD, source=False), exact_none,
{"release.bin": OLD}, False, False, "HOLD_MISSING_SOURCE")
recovered = record("M10_reviewed_new_name", collision, exact_none,
{"release.bin": OLD, "release-reviewed.bin": NEW}, True, True,
"ACCEPT_LOCAL_NEW_NAME", destination="release-reviewed.bin")
require(recovered["after"]["release.bin"] == rejected["after"]["release.bin"],
"recovery changed the original collided destination")
require(len(report["cases"]) == 10, "incomplete case inventory")
require(not root.exists(), "temporary workspace survived normal cleanup")
report.update(status="PASS_10_CASES", owned_workspace_removed=True)
if name == "__main__":
report = {"status": "FAIL", "started_utc": datetime.now(timezone.utc).isoformat(),
"source_sha256": sha(Path(__file__).read_bytes()), "cases": [],
"environment": {"python": sys.version, "platform": platform.platform(),
"uid": os.getuid(), "controlled_env": ENV}}
try:
main(report)
except Exception as error:
report["failure"] = {"type": type(error).__name__, "message": str(error)}
report["finished_utc"] = datetime.now(timezone.utc).isoformat()
print(json.dumps(report, indent=2))
sys.exit(0 if report["status"] == "PASS_10_CASES" else 1)Run it with the installed Python interpreter:
python cloud_mv_fixture.py > cloud_mv_results.jsonInspect the shell process status immediately, then inspect the JSON. A qualifying 9.4 run must report PASS_10_CASES, contain exactly M01 through M10, and report owned_workspace_removed: true. A different GNU mv version intentionally produces HOLD before the test cases mutate their work directories; removing that guard and pretending the old ledger still applies would defeat the qualification.
The controller uses literal NEW, OLD, and sentinel byte oracles rather than deriving the expected answer from the candidate being judged. Before and after each invocation, it inventories the complete relative pathname set. For every entry it records type, device, inode, link count, and, for regular files, literal bytes, size, and SHA-256. Path.lstat() is appropriate to the inventory because Python's lstat interface returns stat information without following a symbolic link, although this fixture rejects any entry that is not a regular file or directory.
Each invocation records argv, return code, stdout, and stderr. The process result is therefore evidence about the command that ran; move_postcondition() separately judges the filesystem contract. This separation is important because a zero return code is neither discarded nor promoted into a stronger statement than it can support.
The temporary files disappear after ordinary context cleanup, while the redirected JSON remains outside the fixture tree and contains the before/after inventories. Python's TemporaryDirectory contract explains normal context-exit cleanup, but that documentation does not turn cleanup into durable evidence preservation or crash recovery.
The supplied preparation execution ran from 2026-10-31T07:16:50.015452+00:00 through 2026-10-31T07:16:50.064948+00:00 and returned PASS_10_CASES. If you run the fixture elsewhere, keep that output as a separately labeled execution and report the actual interpreter, platform, mv identity, and source digest. Temporary directory suffixes and inode numbers are expected to differ. A digest can identify code bytes; it cannot prove that a historical run occurred.
05. Establish the Absent-Destination Baseline
M01 is the positive baseline. candidate.bin exists with NEW bytes, release.bin is absent, and the command uses -T --update=none plus the common --no-copy and -- separator. The supplied ledger records return code zero.
Afterward, candidate.bin is absent and release.bin is a regular file containing NEW. The destination's device/inode pair equals the pre-move source's pair. Within this bounded same-filesystem fixture, those observations agree with the required local rename outcome. Python documents that a nonzero inode identifies a file for a given device value, which is why the pair is useful corroboration here rather than inode alone.
M01 matters because negative controls are only useful if the experiment can also demonstrate the intended operation. It shows that the same controller, executable, workspace construction, and postcondition can accept a straightforward absent-destination move.
This says nothing about whether NEW is a valid application release. The candidate's completeness is an experiment precondition, not something established by mv. Problems such as partial output after a failed build belong upstream. Here the bytes are deliberately fixed before the move begins.
06. Inspect a Successful Collision Without Changing Either File
M02 changes one condition from M01: release.bin already exists with OLD bytes. The collision policy remains -T --update=none.
Three observations must remain separate. First, the reference command returns zero. Second, candidate.bin still exists afterward with the same recorded attributes and NEW bytes. Third, release.bin remains the same OLD file recorded before the invocation. That is exactly the distinction GNU 9.4 documents for --update=none: an existing destination can be skipped without the skip producing failure.
A zero status therefore answers only the command's own status convention. It does not establish the independent required-move contract. In M02, the controller assigns HOLD_COLLISION.
The same reasoning applies if captured stdout is empty. Lack of output cannot supply missing source-consumption evidence. Operationally, the useful response is to retain the candidate and preserve the collision evidence until the responsible owner selects an authorized path: keep it, give it a reviewed distinct name, or replace the destination under an explicit replacement decision.
Equal Bytes Still Do Not Establish a Move
M03 makes the collision more deceptive. Before mv runs, candidate.bin and release.bin are two independent files, both containing NEW. Their device/inode pairs differ, which confirms that the equal content did not come from two names for the same constructed object.
After --update=none, both files remain. Status is zero, but the source has not been consumed. The required-move decision is therefore HOLD_NOT_MOVED.
A checksum-only gate could deliberately accept this state under a content-reuse contract. That would be a different policy, not evidence that the move occurred. M03 exists precisely to prevent “same bytes” from being silently substituted for “source moved to the required pathname.”
07. Detect the Nested Destination Created by Directory Interpretation
M06 demonstrates a second reason why status plus source disappearance is insufficient.
Its pre-state contains candidate.bin and an existing directory named release.bin, with release.bin/sentinel.txt inside it. The invocation deliberately omits -T. GNU's documented ordinary destination behavior allows a final directory operand to receive the source under its basename.
The supplied result returns zero and consumes the source pathname, but the complete after-inventory is:
release.bin: directory
release.bin/sentinel.txt: original sentinel
release.bin/candidate.bin: NEW regular file
Nothing is missing from mv's own interpretation, but the declared contract required release.bin itself to be the regular NEW file. M06 is therefore HOLD_WRONG_DESTINATION_TYPE.
M07 repeats the directory shape with -T. This time GNU is told not to treat the final operand as a target directory. The supplied 9.4 result is nonzero; the source remains, the release.bin directory remains, and its sentinel remains. The recorded failure is the refusal to overwrite a directory with a non-directory.
That pair clarifies what -T does. It removes ambiguity about the role of the final operand; it is not an overwrite instruction. GNU's own target-directory discussion explains -T in terms of final-operand interpretation and notes how it prevents a successful command from unexpectedly placing a source inside a directory.
The fixture's complete relative path inventory is what makes M06 detectable. A narrow check such as “source disappeared” would have incorrectly accepted it.
08. Classify the Failed Controls Before Choosing a Repair
M04, M05, and M09 all return one in the supplied 9.4 ledger. They are not the same failure.
M04 uses -T -n against an existing OLD destination. Under the measured GNU coreutils 9.4 binary, the destination is retained, the source is retained, status is one, and stderr contains a not-replacing diagnostic. The 9.3 NEWS history explains why: coreutils 9.2 had changed -n skipping to induce nonzero status, and 9.3 added a diagnostic corresponding to that status.
That matters because the 9.4 Texinfo description says -n does not overwrite an existing file and uses wording about silently failing. The measured stderr and the release-history note are better evidence for the exact 9.4 observation than treating that wording as proof of silence.
M05 uses -T --update=none-fail on 9.4. Status is also one, but the option value is unsupported by this binary; the invocation rejects it as an invalid update argument and leaves both files unchanged. GNU 9.5 NEWS is explicit that --update=none-fail was introduced in 9.5.
M09 is different again. The source was never created. --update=none does not transform a missing source into a collision; the command reports the missing source and leaves OLD untouched.
So a generic rule such as “nonzero means collision” is unusable. Diagnosis requires argv, executable version, captured stderr, and the after-state together. A rerun should follow the classified cause, not the number alone.
For a future 9.5 baseline, the release notes say mv -n again skips an existing destination without failure and --update=none-fail provides diagnosed, failing skips. That is a qualification target for 9.5, not evidence that this 9.4 fixture executed those semantics.
09. Compare Owned Replacement With a Reviewed New Name
M08 and M10 show two possible owned responses to a collision, but they represent different policy decisions.
M08 is a separate synthetic case. It starts with NEW at candidate.bin and OLD at release.bin, then uses -T --update=all. GNU 9.4 documents all as the default replacement policy when no update restriction is selected: existing destination files are replaced.
The reference ledger records zero, source absence, and NEW at the exact release.bin pathname. Device/inode corroboration also follows the source object into the destination in this same-filesystem case. The decision is ACCEPT_OWNED_REPLACEMENT.
That case is evidence about what happened after an explicitly selected replacement policy. It is not permission to overwrite M02's destination. Authorization remains an operational decision outside the command.
M10 takes a different route and intentionally depends on M02. Instead of destroying the collided OLD entry, it reuses M02's retained candidate.bin and names a reviewed distinct destination, release-reviewed.bin. With -T --update=none, the new destination is absent, so the candidate moves successfully there.
The resulting state contains OLD at the original release.bin, NEW at release-reviewed.bin, and no remaining candidate.bin. The controller marks that ACCEPT_LOCAL_NEW_NAME.
This resolves the local namespace collision only. It does not establish that an application reads release-reviewed.bin, that a deployment pointer was changed, or that anybody authorized a broader release. If the operational requirement is specifically “install at release.bin,” then M10 is evidence that the candidate has been preserved under a reviewed name, not evidence that the application-level destination requirement was met.
Preserve the Original Collision During New-Name Recovery
M10 includes a direct equality check between the original M02 collision state and the post-M10 state of release.bin:
recovered["after"]["release.bin"] == rejected["after"]["release.bin"]
That check makes the recovery decision reviewable. The original collided destination was not rewritten while the retained candidate was moved to its new name.
This is not rollback. Nothing deleted is being restored. It is not activation. No consumer is redirected. It is not proof that the new name is suitable for production use.
Where the required application pathname cannot change, a reviewed alternate name may simply preserve the candidate while the owner remains on HOLD. The filesystem evidence should not be promoted into a release decision it was never designed to make.
10. Reconcile the Ten Outcomes Into One Evidence Ledger
The supplied reference execution produced the following compact ledger. Every move invocation includes --no-copy and -- before the two absolute owned paths.
Case | Principal control | Status | Observed state | Decision |
M01 | -T --update=none; destination absent | 0 | NEW at exact destination; source absent | ACCEPT_LOCAL_MOVE |
M02 | Same policy; OLD destination exists | 0 | OLD and NEW source unchanged | HOLD_COLLISION |
M03 | Same policy; independent destination already equals NEW | 0 | Both equal-content files remain | HOLD_NOT_MOVED |
M04 | -T -n on 9.4 | 1 | Collision retained; diagnostic recorded | HOLD_COLLISION_VERSION_94 |
M05 | -T --update=none-fail on 9.4 | 1 | Unsupported value; both files unchanged | HOLD_UNSUPPORTED_OPTION |
M06 | No -T; destination is a populated directory | 0 | NEW moved inside release.bin/candidate.bin | HOLD_WRONG_DESTINATION_TYPE |
M07 | -T; same directory condition | 1 | Source and directory contents retained | HOLD_WRONG_DESTINATION_TYPE |
M08 | -T --update=all; owned OLD destination | 0 | Exact destination replaced with NEW; source absent | ACCEPT_OWNED_REPLACEMENT |
M09 | -T --update=none; source absent | 1 | OLD destination retained | HOLD_MISSING_SOURCE |
M10 | Reuse M02 source; reviewed distinct destination | 0 | New name holds NEW; source absent; original OLD unchanged | ACCEPT_LOCAL_NEW_NAME |
PASS_10_CASES means the controller observed every expected control outcome. It does not mean all ten moves were accepted. Only M01, M08, and M10 satisfy the declared local move postcondition for the destination each case names.
That distinction is valuable in automation. A negative control can pass the experiment by correctly proving that the operation should be held. “PASS” belongs to the test oracle; “ACCEPT” or “HOLD” belongs to the operational decision.
Evidence should also remain attached to each invocation. M02 and M10 reuse one directory sequentially, so the final directory state alone cannot reconstruct the pre-M10 collision history. The JSON preserves each case's before inventory, command record, and after inventory. Do not infer historical state backward from the final pathname set when the controller has already captured it directly.
11. State What Local Move Acceptance Leaves Unproven
A verified local namespace move proves much less than a completed release.
The experiment can establish, within its controlled tree, that a complete regular source disappeared and the exact requested destination became a regular file containing the expected bytes. In successful local cases it can also corroborate continuity through device/inode identity. Python's stat documentation supports the meaning of those fields, but it does not turn them into permanent artifact identifiers across arbitrary environments.
The experiment does not show that a consumer opened the new pathname, that an application process reloaded it, or that a release authority approved it. Those are different controls.
It also does not establish crash persistence. No fsync protocol, interruption test, power-loss simulation, or durable journal was exercised. The JSON report is emitted only after ordinary TemporaryDirectory cleanup succeeds. Its retained before/after records are useful review evidence, but the temporary filesystem objects themselves have been removed under the normal context-manager path.
UID 0 is another boundary rather than a strength. The supplied run happened as UID 0, so no permission-denial conclusion follows. There was no permission rejection control.
Likewise, --no-copy does not prove cross-filesystem behavior was tested. GNU documents that the flag requests a diagnostic rather than copy-and-remove fallback when a rename cannot cross filesystems, but this fixture never creates that condition.
Consumer publication and staged ingestion belong to another workflow. Refonte's staged archive extraction and publication article addresses a different boundary involving extraction and publication state. This move experiment intentionally does not import that architecture.
12. Turn a Collision Into an Owned Operational Decision
Once a move is held, the next step is not a blind retry. Use a short owned procedure that keeps the evidence and the authority decision separate:
Preserve the command record, version identity, stdout/stderr, source state, destination state, and complete owned path inventory.
Confirm which pathname is the complete candidate, which exact destination the contract names, and whether source consumption is required.
Classify what happened: policy skip, destination interpreted as a directory, unsupported option, missing source, or another evidenced failure.
Obtain the appropriate decision under established team authority: retain the collision, use a reviewed distinct pathname, or authorize replacement of the owned destination.
Perform only the selected operation, then recheck source absence or retention, exact destination type, bytes, and any required preservation of the original collision.
Keep the resulting evidence with the invocation so later reviewers do not have to reconstruct state from memory.
This procedure distinguishes an implementation correction from permission to overwrite legitimate work. Discovering that --update=none skipped a destination is not authorization to rerun with --update=all. Discovering that the final operand was a directory may justify fixing destination interpretation, but it does not authorize deletion of that directory.
The safe operational outcome for incomplete ownership or unclear policy is HOLD. That is not an error state to suppress; it is the explicit point where filesystem observation stops and an accountable decision begins.
Assign the Hold and the Release Decision to Their Owners
The producing owner is responsible for the candidate's integrity and completeness. The delivery maintainer owns the namespace contract: which source and exact destination are intended, which collision policy is appropriate, and what evidence is retained. The release authority owns permission to publish or replace according to the organization's process.
Those roles can belong to the same person in a small environment, but the decisions remain conceptually distinct.
A zero status does not confer release authority. A matching SHA-256 does not confer overwrite permission. A reviewed alternate pathname does not prove application activation. If ownership is unclear or evidence is incomplete, keep the operation on HOLD until the missing decision is supplied by the party authorized to make it.
13. Requalify the Contract When the Runtime or Policy Changes
A bounded procedure should be adopted only when its assumptions are explicit. At minimum, qualification should establish that the intended mv executable is identified; the selected options are supported by that version; the candidate is already complete; the expected destination entry type is known; the paths are inside the owned scope; collision evidence is retained; and the post-state is checked against source consumption plus the exact destination contract.
Version compatibility is especially important for mv no-clobber behavior. GNU's release history shows that -n status semantics changed in 9.2 and changed again in 9.5, while --update=none-fail first appears in the 9.5 release notes.
Do not “upgrade” an old evidence ledger by editing expected statuses until it becomes green. A changed binary, flag policy, or acceptance rule deserves a newly labeled baseline with its own executable identity, source digest, observations, and review date.
The same applies to policy changes. If a team decides equal-content reuse is acceptable, document a new contract that intentionally accepts M03-like states. Do not reinterpret a historical required-move result as though source consumption had never been required.
Report retention also belongs to the qualification design. The supplied fixture intentionally leaves its JSON outside the cleaned temporary tree, but it does not provide durable storage, signing, tamper evidence, or a retention system. Those are organizational evidence-management choices, not implied capabilities of mv or TemporaryDirectory.
This discipline keeps future investigations simple: reviewers can see which binary was measured, which behavior was documentary rather than executed, which postcondition was required, and which authority approved any replacement or alternate pathname.
14. Build Reviewable Linux Delivery Habits
A single-file move is small enough to expose habits that scale well: resolve the actual executable, control inputs that matter, preserve argv and streams, separate command status from filesystem state, and turn ambiguous outcomes into explicit HOLD decisions rather than optimistic assumptions. These are the same kinds of evidence-oriented habits that strengthen broader system-administration troubleshooting skills.
For readers developing those foundations more systematically, Refonte Learning's DevOps Engineering programme lists a three-month format at 12–14 hours per week and covers Linux fundamentals and scripting, Git/GitHub, CI/CD, Docker/Kubernetes, Terraform, cloud platforms, monitoring/logging, and a capstone. Its current admissions text requires applicants to be working toward a bachelor's or higher-level degree, and it describes virtual internship opportunities rather than guaranteeing a specific external placement.
The page states that successful completion earns a Training Certificate and a Certificate of Internship. It does not establish that this exact GNU mv laboratory or version-specific collision matrix is part of the curriculum, so those claims should not be inferred.
The practical lesson remains narrower than any tool list: automation becomes reviewable when a command's documented behavior, the measured runtime, the exact pathname contract, and the authority to change state are kept separate.
