An rsync command can exit zero, include --checksum, and still leave a newer destination file holding rejected bytes. That outcome is not evidence that the checksum request failed. It is evidence that the complete option set expressed a different policy from the operator's intended copy contract.
--checksum changes how rsync decides whether same-size files differ. --update adds another rule: skip an existing regular destination file when its modification time is newer than the source. The official option descriptions address different questions. A content-comparison request does not, by itself, authorize an overwrite that another transfer rule prevents.
This playbook makes that conflict reproducible with nine local cases, two equal-length strings, explicit whole-second modification times, independent ordinary files, captured process evidence, and literal SHA-256 oracles. The source is declared authoritative only for the laboratory; production owners must make that authority decision before any option repair. The acceptance target is not “rsync looked successful.” It is “the destination bytes equal the approved source bytes under the declared policy.”
The commissioning run reported PASS_9_CASES on Python 3.13.5, Linux 6.18.44 with glibc 2.41, and rsync 3.4.1 protocol 32. Every process returned zero, yet only four destinations matched the source-authoritative contract. That difference is the point of the test.
Choose which side is authoritative
Before changing an rsync command, write down which bytes are allowed to win. In this fixture, the approved source is the exact byte string release=approved\n. Each initially present destination contains the equal-length but rejected string release=rejected\n; R7 deliberately starts without a destination file. The laboratory explicitly authorizes replacing the destination with the source.
That authorization is not implied by the words “source” and “destination.” In a deployment mirror, the source may be an owner-approved signed release artifact. In a restore operation, the preserved recovery set may be authoritative. In a user workspace, however, a newer destination may contain legitimate edits that --update was intentionally chosen to protect. Removing --update before the owner resolves that policy can destroy valid work.
The operational acceptance target is therefore precise:
The selected source has reviewed provenance.
Replacement of the destination is authorized.
After the real command, the destination's bytes exactly match the approved source bytes.
Evidence captures the command, preconditions, process result, and post-run content.
A newer timestamp is not an acceptance oracle, and exit code zero is not an approval oracle. The broader discipline belongs to administration and backup responsibilities, but this page narrows the object of review to one local rsync argument vector and one literal file-content contract.
For this source-authoritative lab, a matching destination can receive ACCEPT_BOUNDED. A mismatch caused by contradictory selection flags receives REPAIR. Unknown authority, failed preconditions, or incomplete post-run evidence receives HOLD. RECOVER is reserved for an authorized restoration from an independent preserved source, not for guessing which current file looks newest.
Separate quick checks, checksums and update rules
Rsync’s default quick check and its --checksum option participate in transfer selection, but neither removes other transfer rules. The maintained official rsync manual, consulted on 2026-10-14, describes the default size-and-mtime comparison, checksum-based comparison for same-size files, and the separate receiver-newer skip rule. Its retrieved version identifies itself as 3.5.1. The version-tagged 3.4.1 manual documents these relevant rules for the executed baseline. The results below are observations of 3.4.1, not a 3.5.1 qualification.
Read those statements together. The option contract is not “checksum always wins.” It is “checksum changes one comparison, while update still expresses a receiver-newer protection policy.” The exact behavior comes from the complete selected option set.
The fixture also uses --times and --modify-window=0. The timestamp and itemization options distinguish mtime preservation, integer-second comparison, and change reporting. The authored times are exact integer-second values, so the comparisons do not depend on nanosecond differences. --itemize-changes provides change-summary evidence, including a plan under --dry-run; it does not certify the resulting bytes.
This scope intentionally differs from historical hard-link backup preservation. Every existing source and destination here is an independent ordinary file with nlink=1; no tested pair shares an inode. There is no --inplace, --link-dest, or older generation that can be mutated through an alias. The defect is non-action: a required overwrite is skipped. That linked article supplies a contrast in failure mode, not the normative option contract.
Keep transfer selection distinct from block reconstruction
The official project page How Rsync Works, consulted on 2026-10-14, is useful architectural context. It distinguishes file-list construction, the generator's file-level skip decision, and the sender/receiver work used to reconstruct selected files. The page is a broad historical explanation and explicitly trades some detail for accessibility, so the current manual remains authoritative for exact option behavior.
That separation prevents a category error. File-level selection and the mechanics of transferring selected bytes are different questions. This fixture does not measure block matching or an internal instruction sequence. In particular, the manual’s local whole-file default applies to these local paths without batch writing; this is not a demonstration of delta reconstruction. A checksum-related flag cannot be assumed to authorize every overwrite. Judge eligibility from the complete option set, then judge success from the resulting bytes.
Run the complete nine-case local fixture
Save the following file as rsync_selection_fixture.py, then run it with Python. It requires a locally installed rsync executable and writable disposable storage beside the script. The driver creates fresh directories, invokes rsync directly through an argument array, and sets timeout=20 on each copy call. Completed calls supply separate return-code, stdout and stderr fields. The case loop uses a failure-default JSON ledger; its startup and reporting limitations are explained below.
python rsync_selection_fixture.pyThe known-answer manifest has two 17-byte ASCII values. In the displayed literals, \n denotes one final line-feed byte, not a backslash followed by the letter n:
Name | Literal bytes | SHA-256 |
APPROVED | release=approved\n | e4a156141c7d98d9927986a129e8989b4cc09f0bd819c442d17fc7fea4420434 |
REJECTED | release=rejected\n | 4f6e94fe5cd8ae61a531091b52fd875aaf5480489fe5e9e510fc9d47b3824b79 |
The complete driver is:
"""Local disposable ordinary files only. No deletion, links, or remote endpoint."""
import hashlib
import json
import os
import platform
import shutil
import subprocess
import tempfile
from datetime import datetime, timezone
from pathlib import Path
SOURCE = b"release=approved\n"
OLD = b"release=rejected\n"
EPOCH_NS = 1_700_000_000 1_000_000_000
def require(ok, message):
if not ok:
raise RuntimeError(message)
def inspect(path):
if not path.exists():
return None
st = path.stat()
return {"bytes": path.read_bytes().decode("ascii"), "size": st.st_size,
"mtime_ns": st.st_mtime_ns, "device": st.st_dev,
"inode": st.st_ino, "nlink": st.st_nlink,
"sha256": hashlib.sha256(path.read_bytes()).hexdigest()}
def main():
executable = shutil.which("rsync")
require(executable is not None, "rsync is required")
source = Path(__file__).resolve()
directory = Path(tempfile.mkdtemp(prefix="rsync-select-", dir=source.parent))
report = {"fixture": "rsync-selection-v1", "status": "FAIL", "cases": [],
"started_utc": datetime.now(timezone.utc).isoformat(),
"python": platform.python_version(), "platform": platform.platform(),
"rsync": subprocess.run([executable, "--version"], check=True,
text=True, capture_output=True).stdout.splitlines()[0],
"source_sha256": hashlib.sha256(source.read_bytes()).hexdigest()}
# id, destination age, added flags, literal expected destination bytes.
specs = [
("R1", "equal", [], OLD),
("R2", "equal", ["--checksum"], SOURCE),
("R3", "newer", ["--update"], OLD),
("R4", "newer", ["--checksum", "--update"], OLD),
("R5", "newer", ["--checksum"], SOURCE),
("R6", "older", ["--checksum", "--update"], SOURCE),
("R7", "missing", ["--checksum", "--update"], SOURCE),
("R8", "newer", ["--ignore-times", "--update"], OLD),
("R9", "newer", ["--checksum", "--dry-run"], OLD),
]
try:
require(len(SOURCE) == len(OLD) and SOURCE != OLD, "Invalid byte oracle")
for identity, age, flags, expected in specs:
row = {"id": identity, "status": "FAIL", "destination_age": age,
"flags": flags, "expected_bytes": expected.decode("ascii")}
report["cases"].append(row)
root = directory / identity
src, dst = root / "source", root / "destination"
src.mkdir(parents=True); dst.mkdir()
s, d = src / "release.txt", dst / "release.txt"
s.write_bytes(SOURCE)
os.utime(s, ns=(EPOCH_NS, EPOCH_NS))
if age != "missing":
d.write_bytes(OLD)
delta = {"equal": 0, "newer": 120, "older": -120}[age]
stamp = EPOCH_NS + delta 1_000_000_000
os.utime(d, ns=(stamp, stamp))
require(d.stat().st_mtime_ns == stamp, "Filesystem changed requested timestamp")
require(d.stat().st_nlink == 1 and not os.path.samefile(s, d),
"Destination must be an independent ordinary file")
require(s.stat().st_mtime_ns == EPOCH_NS and s.stat().st_nlink == 1,
"Source timestamp/link precondition failed")
row["before"] = {"source": inspect(s), "destination": inspect(d)}
argv = [executable, "--recursive", "--times", "--modify-window=0",
"--itemize-changes", *flags, str(src) + "/", str(dst) + "/"]
row["argv"] = argv
result = subprocess.run(argv, capture_output=True, text=True, timeout=20)
row.update(returncode=result.returncode, stdout=result.stdout, stderr=result.stderr)
row["after"] = {"source": inspect(s), "destination": inspect(d)}
require(result.returncode == 0, identity + ": rsync execution failed")
require(s.read_bytes() == SOURCE, identity + ": source was changed")
require(d.read_bytes() == expected, identity + ": wrong destination bytes")
row["source_authoritative_contract_matches"] = d.read_bytes() == SOURCE
row["status"] = "PASS"
require(len(report["cases"]) == 9, "Incomplete ledger")
report["status"] = "PASS_9_CASES"
except Exception as error:
report["error"] = {"type": type(error).__name__, "message": str(error)}
raise
finally:
report["finished_utc"] = datetime.now(timezone.utc).isoformat()
path = directory / "results.json"
path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
print(path)
if name == "__main__":
main()Saved as UTF-8 with LF line endings and one final newline, the complete driver has SHA-256 5dfc0136626ea4ad5d680a0fb7f7dfc0191c1f8291c0dd1b34024d95acd3eaf3. This matches the script identity supplied with the commissioning evidence. The following table records that supplied execution, separately from the independently reproduced run described afterward:
Field | Supplied commissioning value |
Status | PASS_9_CASES |
Started UTC | 2026-10-14T06:41:42.137375+00:00 |
Finished UTC | 2026-10-14T06:41:42.573312+00:00 |
Python | 3.13.5 |
Platform | Linux-6.18.44-x86_64-with-glibc2.41 |
rsync | rsync version 3.4.1 protocol version 32 |
Driver SHA-256 | 5dfc0136626ea4ad5d680a0fb7f7dfc0191c1f8291c0dd1b34024d95acd3eaf3 |
The supplied timestamps identify one execution, not a performance measurement. An independent local execution of the byte-identical driver started at 2026-10-14T08:54:23.219959+00:00 and finished at 2026-10-14T08:54:23.646054+00:00. It recorded the same Python, platform and rsync versions and again produced PASS_9_CASES. The process-output examples and matrix below come from that independent run. They are observations under its stated conditions, not guarantees for another filesystem or command line.
The driver never invokes a shell or runs --delete. Its rsync operands are generated local directories, not remote endpoints or user-selected data trees. The tested files are freshly created and independent; no links or concurrent writers enter the setup. This is not a general-purpose path-safety validator. The printed JSON path identifies the disposable results directory. Retain results.json and the exact driver before cleanup when the run supports a review.
The argument array is visible in every completed case. shutil.which() resolves the rsync executable, and each row records that path in argv. The trailing slash on the source selects its directory contents. Python’s subprocess.run documentation explains the independently captured streams and timeout parameter. In this report, source_sha256 identifies the Python driver, whereas the nested file records contain the SHA-256 values for release.txt. Do not confuse code identity with approved artifact identity.
The failure-default design has limits visible in the source. Executable discovery, temporary-directory creation and the version probe occur before the try block, so failures there may leave no JSON report. The version probe has no timeout. A copy timeout reaches the handler, but this driver does not serialize its partial output; that row lacks completed-process fields. A failed JSON write can also prevent reporting. These failure paths were statically reviewed, not exercised by the nine successful subprocess calls. Missing evidence requires HOLD, never an inferred pass.
The temporary roots are also case-specific. R1 cannot leave a timestamp or destination file behind for R2, and a successful transfer in R5 cannot repair the input of R8. Independence turns the nine rows into controlled comparisons rather than a sequence whose later outcomes depend on earlier mutations.
Verify timestamps and file independence before comparing output
The fixture gives the source an mtime_ns of 1700000000000000000, corresponding to 2023-11-14T22:13:20+00:00. Equal destinations receive the same value. Newer destinations receive 1700000120000000000, exactly 120 seconds later, and older destinations receive 1699999880000000000, exactly 120 seconds earlier.
The Python 3.13 os.utime documentation, consulted on 2026-10-14, describes the nanosecond tuple and warns that a filesystem may not return the exact requested time. The driver therefore reads back st_mtime_ns and stops if the filesystem changed the authored value. It does not round a failed precondition into a presumed pass. These Python links refer to the maintained 3.13 documentation branch; the executed interpreter was specifically 3.13.5.
For every initially existing pair, the source and destination must each have nlink=1, and os.path.samefile() must be false. That function compares device and inode identity; the JSON records both fields. The ordinary-file assumption comes from controlled creation in a fresh directory without concurrent changes, not from treating link count alone as a symlink detector. Post-run identity is captured for inspection, including the destination created in R7. Failed setup checks require HOLD.
Make equal metadata conceal different contents
R1 is the quick-check trap. Source and destination are both 17 bytes and both have mtime_ns=1700000000000000000, but their contents differ. The base argument vector is:
rsync --recursive --times --modify-window=0 --itemize-changes SOURCE/ DESTINATION/No extra flag asks rsync to compare file content before selection. Under the documented default quick check, equal size and equal modification time allow the regular file to be skipped. R1 therefore leaves release=rejected\n in the destination. The command returns zero, stdout and stderr are empty in the controlled run, and the source remains unchanged.
R1 is a successful negative control because it proves the fixture can conceal a byte mismatch behind equal metadata. It is not an application success. Under the declared source-authoritative policy, its decision is REPAIR: the command's selection criteria are weaker than the acceptance contract.
R2 changes only one variable by adding --checksum. The same-size files now receive a content-based pre-transfer comparison. The mismatch is selected, rsync writes the approved bytes, and --times sets the destination modification time to the source value. The observed itemized line was >fc........ release.txt, while stderr remained empty. The destination digest becomes the approved SHA-256.
The independent digest comes from Python's documented hashlib.sha256() interface, consulted on 2026-10-14. It is an authored post-run byte oracle. It is not a statement about which checksum algorithm rsync negotiated internally, and it does not establish that the source was authorized. R2 earns ACCEPT_BOUNDED only because authority was declared separately and literal destination bytes match that source.
Give the destination a newer timestamp
R3 creates a fresh rejected destination and assigns its modification time 120 seconds after the source. It then adds --update to the base argument vector:
rsync --recursive --times --modify-window=0 --itemize-changes \
--update SOURCE/ DESTINATION/The documented update rule covers this regular-file comparison: an existing newer destination is skipped. R3 creates precisely that relationship. In the independent run, the rejected bytes and newer mtime remained unchanged, stdout and stderr were empty, and the process returned zero.
Nothing in that result says the rejected bytes are correct. The timestamp was manufactured by the fixture; it is not a trustworthy version number, approval signature, causal ordering record, or proof of user intent. The case isolates one fact: the selected policy protects a newer receiver file from replacement.
The pre-run and post-run evidence should be read together. Before rsync, the source contains approved bytes at the base timestamp and the destination contains rejected bytes at base plus 120 seconds. After rsync, both content and timestamps are unchanged. That is stronger evidence than “the file did not appear in output” because the driver verifies the actual paths and bytes.
Do not turn timestamp recency into content authority
A modification time can be a useful selection input without being a content-authority decision. In a restore, a destination may be newer because a damaged application rewrote it. In a deployment, it may be newer because a local process touched or reformatted it. In a user-owned directory, it may represent legitimate work that must be preserved. The same numerical relation supports different policies depending on ownership.
Treat “newer” as metadata, not a verdict. The data or release owner must decide whether source, destination, or an external manifest is authoritative. Until that decision is recorded, the correct action is HOLD, not removal of a protective option.
Add checksum without removing the skip policy
R4 is the central case. It creates its own independent pair with the same newer-destination relationship as R3, then adds both --checksum and --update to the base flags:
rsync --recursive --times --modify-window=0 --itemize-changes \
--checksum --update SOURCE/ DESTINATION/An operator looking at only --checksum may expect the different contents to force replacement. The actual option contract still includes the receiver-newer skip rule. In the controlled execution, R4 returns zero, emits no stdout or stderr, and leaves both the rejected bytes and the newer destination timestamp unchanged.
The careful statement is not that rsync “ignored” the checksum. The fixture does not instrument internal calls or prove an instruction order. Its observable result is that adding --checksum did not cancel --update: the newer destination was skipped. This agrees with the separate transfer-selection rules. The experiment does not claim that the receiver necessarily calculated a checksum for a file it skipped.
R3 and R4 form a controlled comparison: their relevant bytes and mtime relationship agree, while R4 adds --checksum. Both retain rejected contents. R4 and R5 provide the stronger repair comparison: both start with a newer rejected destination and request checksum selection, but only R4 includes --update. R5 transfers. By contrast, R2 and R4 differ in both destination age and flags, so that pair alone cannot isolate the skip rule.
Under the declared source-authoritative policy, R4 receives REPAIR: remove or otherwise resolve the incompatible --update rule after preserving evidence and confirming authorization. Under unknown authority, the same row would receive HOLD.
This distinction is especially important in copied runbooks. A command may have accumulated --update years earlier to protect interactive destination edits, while a new release process assumes the source is immutable and authoritative. Adding --checksum to address drift does not remove the older policy. The repair is therefore a contract review: identify why each selection flag exists, decide whether that reason still applies, and encode the resulting authority decision in the acceptance test.
Qualify an authorized source-authoritative replacement
R5 performs the narrow repair. It keeps --checksum and removes --update:
rsync --recursive --times --modify-window=0 --itemize-changes \
--checksum SOURCE/ DESTINATION/The destination is still 120 seconds newer and still contains rejected bytes before the run. Because the laboratory has already declared the source authoritative and approved replacement, there is no policy reason to protect the destination merely because its timestamp is later. R5 transfers the file, reports >fc.t...... release.txt in the qualifying environment, returns zero, and leaves stderr empty. The destination becomes release=approved\n, its SHA-256 matches the approved manifest, and its modification time is set to the source time because --times is present.
R5 is not a universal prescription to remove --update. It is a repair for one explicit contract: the reviewed source bytes must replace any differing destination bytes in this local scope. If a later destination edit has legitimate ownership, R5 would be destructive rather than corrective.
R6 and R7 are positive controls for the original combined flags. R6 gives the rejected destination an older timestamp and runs --checksum --update; the file transfers and matches the source. R7 starts with no destination file; the same flags create it. Those controls show that the command can transfer under conditions that do not trigger the newer-destination rule. They also prevent the false diagnosis that --update blocks all writes or that the fixture's paths are unwritable.
Preserve the rejected destination before production repair
The disposable laboratory is designed to overwrite its own rejected bytes. Production data deserves a different sequence. First record the mismatch, current metadata, ownership decision, and command line. Then preserve meaningful destination data in an independent location with a reviewed retention policy. Only after source provenance and replacement authority are confirmed should the corrected command run.
A flag change is not authorization. If the destination may contain user work, incident evidence, or the only copy of a revision, choose HOLD and obtain a data-owner decision. When recovery is authorized, use an independently preserved source; do not rely on the file that the disputed command is about to replace.
Test why ignore-times is not a policy override
R8 tests a tempting but incomplete repair. --ignore-times disables the default equal-size/equal-time quick check. The official manual actually uses the broad phrase “causing all files to be updated.” That phrase must be read alongside the independently documented --update rule; it is not an unconditional override of the complete option set. R8 tests the combination directly against a newer rejected destination:
rsync --recursive --times --modify-window=0 --itemize-changes \
--ignore-times --update SOURCE/ DESTINATION/The destination remains rejected. The process returns zero, and both captured output streams are empty in the controlled run. The negative control demonstrates that reconsidering files that would otherwise pass the quick check is not the same as withdrawing the receiver-newer protection rule.
This is another reason to review the full argument vector rather than search for a force-sounding flag. --ignore-times changes one selection criterion. --update still states that a newer receiver file should be skipped. Adding more aggressive comparison does not settle authority and does not necessarily make the file eligible for replacement.
Do not broaden this result into a survey of rsync overrides. The fixture tests one exact combination on local ordinary files. It does not evaluate --inplace, deletion behavior, remote transports, link handling, or metadata classes beyond the fields it records. For this source-authoritative contract, R8 receives REPAIR; for unknown authority, it receives HOLD.
Read a dry run without claiming a copy occurred
R9 removes --update, keeps --checksum, and adds --dry-run against the newer rejected destination:
rsync --recursive --times --modify-window=0 --itemize-changes \
--checksum --dry-run SOURCE/ DESTINATION/The manual’s dry-run description specifies a trial without making changes. In the independently reproduced case, stdout contains >fc.t...... release.txt, the same itemized form observed for R5, while stderr is empty and the return code is zero. The destination nevertheless remains release=rejected\n with its newer mtime. These are the captured values for this run, not fixed output requirements for every environment.
That is not a contradiction. The line is planning evidence, not copy proof. Compare R9 with R5: the argument vectors differ by --dry-run; both identify the file as a candidate, but only R5 writes approved bytes. A release gate that accepts itemized output without post-run content checks can report success before any mutation has occurred.
The exact stdout is supporting evidence, not a universal acceptance string. The fixture explicitly fixes file mtimes, not containing-directory mtimes. Because itemization includes attribute changes, another run can add directory-related lines without changing the release.txt byte result. Preserve the complete stream and inspect the file records rather than accepting or rejecting a copy solely from an expected number of output lines.
The contrast is conceptually related to independent CI status evidence, but this fixture deliberately contains no Bash pipeline, tee, pipefail, or PIPESTATUS. Python calls rsync directly and records the child process's return code and streams without an intervening logger process.
R9’s laboratory assertion passes because its expected destination is still rejected. Its application decision is HOLD: this plan has not produced the required bytes. A reviewed dry run can inform an owner’s authorization of the next step; the command itself does not grant that authorization. It cannot earn ACCEPT_BOUNDED for this mismatching destination.
Capture the command and logger outcomes separately
The report stores the exact argv array, integer return code, stdout, and stderr as separate fields. This matters even without a pipeline. A zero return code says the rsync invocation completed without a reported rsync error in that run; it does not say the source-authoritative manifest matched. Empty stdout can mean no itemized change, while nonempty stdout in a dry run can describe a plan rather than a write.
The report and each case start in FAIL; explicit assertions promote them to PASS. That design prevents unchecked cases from becoming successful records, but it does not guarantee that a report can always be written. Reject a missing or incomplete ledger rather than filling absent fields with reassuring defaults. This fixture has no separate logger process to test. The JSON write is another operation that must succeed before its evidence can be reviewed.
Reconcile all nine destinations with the literal manifest
The supplied commissioning result reports rejected bytes for R1, R3, R4, R8 and R9, and approved bytes for R2, R5, R6 and R7. The independent run identified above reproduced that mapping. Its nine return codes were zero and its nine stderr values were empty. Nonempty stdout entries below each ended with one newline; the table omits that final display newline. APPROVED and REJECTED refer to the exact literals and digests in the manifest.
Case | Age | Added flags | RC | Observed stdout | Expected / | Source-authoritative decision |
R1 | Equal | none | 0 | stdout empty | REJECTED -> REJECTED | REPAIR quick check is insufficient |
R2 | Equal | --checksum | 0 | >fc........ release.txt | APPROVED -> APPROVED | ACCEPT_BOUNDED |
R3 | Newer | --update | 0 | stdout empty | REJECTED -> REJECTED | REPAIR after authority approval |
R4 | Newer | --checksum | 0 | stdout empty | REJECTED -> REJECTED | REPAIR contradictory policy |
R5 | Newer | --checksum | 0 | >fc.t...... release.txt | APPROVED -> APPROVED | ACCEPT_BOUNDED |
R6 | Older | --checksum | 0 | >fc.t...... release.txt | APPROVED -> APPROVED | ACCEPT_BOUNDED |
R7 | Missing | --checksum | 0 | >f+++++++++ release.txt | APPROVED -> APPROVED | ACCEPT_BOUNDED |
R8 | Newer | --ignore-times | 0 | stdout empty | REJECTED -> REJECTED | REPAIR after authority approval |
R9 | Newer | --checksum | 0 | >fc.t...... release.txt | REJECTED -> REJECTED | HOLD plan only |
The table separates diagnostic test status from application acceptance. All nine cases pass because each matches its predeclared diagnostic oracle, including the controls expected to retain rejected bytes. PASS_9_CASES does not mean all nine destinations are releasable. The driver asserts return codes, source bytes and each expected destination value. It records, but does not assert every post-run timestamp, inode, link count or output string; those evidence fields need separate inspection.
This is the same discipline promoted by repeatable QA acceptance checks: define the oracle before execution, include counterexamples, and fail when evidence is absent. Here the oracle is deliberately narrow and literal. It is not “directory sizes look plausible,” “timestamps moved,” “stdout was reassuring,” or “the command returned zero.” It is the exact destination byte sequence and corresponding SHA-256, reconciled case by case.
R1 and R9 must both remain in the matrix even though their reasons differ. R1 is skipped because default quick-check metadata matches. R9 is selected in a trial but not written because dry-run forbids changes. Omitting either would let one evidence channel masquerade as another.
The matrix also demonstrates why every argument vector matters. The common base is /usr/bin/rsync --recursive --times --modify-window=0 --itemize-changes, followed by each row's added flags and fresh source/destination paths. A review of only the human shorthand “checksum copy” would miss the exact --update or --dry-run term that controls the outcome.
Separate byte preservation from a complete restore
The fixture establishes a bounded content claim for one small regular file. When a row receives ACCEPT_BOUNDED, the observed destination bytes match the approved source bytes, the source was not changed, the process evidence is present, and the authored timestamp/link preconditions held. That is useful evidence, but it is not a complete restore qualification.
The test does not prove destination permissions, ownership, ACLs, extended attributes, security labels, birth times, access times, sparse-file layout, or application-specific metadata. It does not prove a directory was published atomically. It does not establish that a set of files came from one consistent point in time. It excludes active writers, live databases, snapshots, remote transport, hard links, symlinks, --inplace, --link-dest, and --delete.
The archive-mode definition also limits broader conclusions. In the ordinary invocation discussed here, -a expands to -rlptgoD; it does not include -H, -A or -X. Replacing the explicit fixture flags with archive mode would not, by itself, establish hard-link, ACL or xattr preservation. Those properties are outside this laboratory and would need their own requirements and tests.
SHA-256 is a byte-comparison aid here. The digest lets the report compactly reconcile exact captured contents and detect accidental changes to the fixture source. It cannot authenticate an unknown source, prove who approved it, or make an untrusted artifact safe. Source provenance remains a human and system-of-record decision.
The assumptions are equally important: one local user, disposable directories, independent ordinary files, no concurrent writer, writable storage, and timestamps that survive read-back exactly. An observed result under those assumptions should not be extrapolated to remote rsync, a different filesystem, an active application, or a multi-file backup without a new protocol.
A complete restore review usually needs multiple acceptance layers: source identity and authorization; file-set completeness; bytes; required metadata; ordering or atomicity; application consistency; and a recovery rollback path. This article qualifies only the transfer-selection and destination-byte layer.
That boundary should appear in the change record itself. A statement such as “R5 passed” is too compressed for later review. A useful statement identifies the approved source digest, the exact destination path, the regular-file and no-writer assumptions, the rsync and Python versions, the real rather than dry-run command, and the properties that remain untested. Bounded language is not bureaucratic caution; it prevents a valid byte result from being reused as evidence for permissions, durability, consistency, or recovery objectives that the fixture never exercised.
Choose accept, repair, hold or recover
Use decisions that state both the evidence and its boundary.
ACCEPT_BOUNDED applies only when the owner has declared the source authoritative, replacement is authorized, all fixture preconditions pass, the real command returns successfully, and the destination bytes equal the approved source. In the nine cases, R2, R5, R6, and R7 qualify. The word “bounded” prevents that content result from being mistaken for a complete backup or restore certification.
REPAIR applies when authority is settled but the selected options contradict the required outcome. R1 needs content-sensitive selection because equal size and time conceal different bytes. R3, R4, and R8 need the newer-destination protection resolved because the source-authoritative policy permits replacement. Repair the reviewed argument vector, not just a copied shell snippet, and rerun the whole evidence protocol.
HOLD applies when source versus destination authority is unknown, source provenance is missing, destination data has not been preserved, timestamp or link preconditions fail, the ledger is incomplete, or only a dry run has occurred. R9 is a hold for application acceptance because it produces plan evidence without changing destination bytes. A technical hold is not a declaration that rsync is defective; it says the evidence is insufficient for the requested operational decision.
RECOVER applies when an authorized owner directs restoration from an independent preserved source. Recovery should use a known, retained artifact rather than rewriting the disputed current file and calling the new state history. Record the mismatch first, preserve relevant destination evidence, identify the approved recovery source, perform the controlled copy, and validate the resulting bytes against that source's manifest.
Do not repair the evidence until it says PASS
Keep the application oracle separate from the diagnostic oracle. R4 already has expected_bytes set to rejected, deliberately: its diagnostic assertion passes while source_authoritative_contract_matches remains false. Replacing the approved application requirement with whatever the destination currently contains would erase the defect. So would treating status="PASS" as release approval or dropping R4 and R9 from the ledger. Preserve both the expected counterexample and the independent approved-source requirement.
When the option contract changes, retain the original report showing the application mismatch, revise the authorized command, and generate a new report. Do not edit returned stdout, timestamps, hashes or expected values to resemble success. The application should pass because the resulting bytes meet the predeclared contract, not because that contract was rewritten around the observed output. The supplied nine-case driver remains unchanged so its diagnostic comparisons retain their meaning.
Assign the copy policy and regression triggers
A reliable rsync qualification has named ownership.
The data or release owner decides which side is authoritative, approves the exact source identity, and authorizes replacement of destination data. This role also defines whether a newer destination is expected local work, accidental drift, or a corrupted derivative. Administrators should not infer that decision from timestamps.
The system administrator or release engineer owns the executable path, option set, source and destination scope, preservation plan, and cleanup procedure. The reviewed acceptance object is the explicit argument array, not an informal description such as “run rsync with checksum.” This is where reviewed administration automation is relevant: repeatability comes from a reviewed configuration and evidence protocol, not from assuming automation makes policy correct.
The QA or operational reviewer owns the known-answer fixture, failure-default assertions, timestamp read-back, link-independence checks, and literal post-run oracles. The reviewer should verify that all nine rows exist and that negative controls pass for the intended reason while still failing the source-authoritative application contract.
Rerun the protocol when any of the following changes:
The rsync executable or version changes.
The Python harness changes; a changed source must have a new SHA-256 identity.
The filesystem, mount options, timestamp resolution, or storage layer changes.
The base flags, added flags, path form, or source/destination direction changes.
Links, special files, remote endpoints, active writers, or multiple files enter scope.
Required metadata expands beyond the byte and timestamp evidence captured here.
The authority policy changes, including a decision to protect destination edits.
Do not treat a syntax check as behavior evidence. Do not treat this local run as remote integration evidence. Do not treat one small-file content match as crash durability, filesystem persistence after power loss, or a consistent application backup. Each new property needs a corresponding test and oracle.
The fixture's generated root is safe to remove only after any required results.json and source file identity have been retained. Cleanup should target the exact printed temporary directory, not a wildcard guessed from the prefix. In a controlled review, evidence retention and cleanup are separate authorized actions.
For production adoption, convert the policy into a change record containing: authoritative side; approved source digest or release identity; protected destination-copy location; exact argv; expected selection behavior; required post-run byte checks; owner approvals; rollback or recovery source; and regression triggers. That record makes it possible to distinguish an intentional newer-destination protection from an accidental skip.
Develop the administration foundations behind recovery
This playbook depends on foundational administration skills: reading Linux file metadata, using explicit command-line arguments, troubleshooting selection rules, preserving evidence, and separating backup intent from restore proof. For structured practice in those foundations, review Refonte Learning’s System Administration Program. The provider lists Windows and Linux administration, command-line work, troubleshooting, virtualization, networking, backup/disaster recovery and applied projects, with a stated workload of 10–12 hours per week. Its page states that successful completion provides a Training Certificate and a Certificate of Internship, and describes a potential internship rather than guaranteeing an individual placement.
Broad administration study should be complemented by task-specific qualification. A course description cannot establish that a particular rsync command has copied the approved artifact, and this laboratory does not establish specialist coverage in any curriculum. Keep the operational standard explicit: decide authority first, run the reviewed command second, and compare the destination with the approved bytes rather than with the command’s reassuring appearance.
