{
  "part": 7,
  "title": "Full-overwrite of an existing target (target.allowFullOverwrite)",
  "reset": "standard",
  "steps": [
    { "id": "7.1", "title": "Force a full send situation",
      "_comment": "A full send uses zfs send -R, which replicates the source's ENTIRE current snapshot history for this filesystem, not just the newest snapshot - correct and wanted (TESTING_automation.md 16, Part 7 reframing): disaster-recovery/ransomware-rollback requires retaining as much target history as a full-overwrite can carry over, not just the latest point. The assertion therefore checks that the target's resulting snapshot set for ds1 equals source's CURRENT set, not a hardcoded count.",
      "actions": [
        { "do": "zfs_snapshot", "dataset": "{SRC}/ds1@part7_1a" },
        { "do": "empty_dir", "path": "{TRANSPORT}" },
        { "do": "run_sneakernet", "servername": "test-source", "verbosity": 1, "expect_exit": 0 },
        { "do": "run_sneakernet", "servername": "test-target", "verbosity": 1, "expect_exit": 0 },
        { "do": "zfs_list_snapshots", "dataset": "{SRC}/ds1", "capture": { "name": "SRC_DS1_SNAPS_71", "select": "@(.*)$", "reduce": "sorted_join" } },
        { "do": "zfs_list_snapshots", "dataset": "{DST}/ds1", "capture": { "name": "DST_DS1_SNAPS_71", "select": "@(.*)$", "reduce": "sorted_join" } },
        { "do": "rm_matching", "dir": "{SN}", "pattern": "^sneakernet_target\\.status(\\.[0-9_.-]+)?$" },
        { "do": "zfs_snapshot", "dataset": "{SRC}/ds1@part7_1b" }
      ],
      "assertions": [
        { "type": "values_equal", "a": "SRC_DS1_SNAPS_71", "b": "DST_DS1_SNAPS_71", "message": "target's snapshot set for ds1 matches source's current snapshot set - the full send preserved all available history" }
      ]
    },
    { "id": "7.2a", "title": "allowFullOverwrite: 0 (the default) - must refuse cleanly", "depends_on": ["7.1"],
      "actions": [
        { "do": "config_get", "path": "target.allowFullOverwrite", "store_as": "ALLOWOVERWRITE_DEFAULT" },
        { "do": "zfs_list_snapshots", "dataset": "{DST}/ds1", "capture": { "name": "DS1_BEFORE_72A", "reduce": "count" } },
        { "do": "empty_dir", "path": "{TRANSPORT}" },
        { "do": "run_sneakernet", "servername": "test-source", "verbosity": 3, "expect_exit": 0 },
        { "do": "run_sneakernet", "servername": "test-target", "verbosity": 3, "log": "part7_2a", "expect_exit": "any",
          "capture": { "name": "LOG72A" } },
        { "do": "zfs_list_snapshots", "dataset": "{DST}/ds1", "capture": { "name": "DS1_AFTER_72A", "reduce": "count" } }
      ],
      "assertions": [
        { "type": "regex_present", "var": "ALLOWOVERWRITE_DEFAULT", "pattern": "^0$", "message": "confirm allowFullOverwrite is 0" },
        { "type": "regex_present", "var": "LOG72A", "pattern": "(?i)cannot overwrite.*allowFullOverwrite|allowFullOverwrite.*to let sneakernet destroy", "message": "an actionable error is logged mentioning allowFullOverwrite" },
        { "type": "values_equal", "a": "DS1_BEFORE_72A", "b": "DS1_AFTER_72A", "message": "before and after snapshot counts are equal - nothing was destroyed" }
      ]
    },
    { "id": "7.2b", "title": "allowFullOverwrite: 1 - destroy-and-retry succeeds", "depends_on": ["7.2a"],
      "config": { "mutations": [ { "op": "set", "path": "target.allowFullOverwrite", "value": "1" } ], "restore": "none" },
      "_comment": "Step 7.2a's source run already recorded its attempted full send as sent, so the status must be wiped again to force a full send to retry against.",
      "actions": [
        { "do": "rm_matching", "dir": "{SN}", "pattern": "^sneakernet_target\\.status(\\.[0-9_.-]+)?$" },
        { "do": "empty_dir", "path": "{TRANSPORT}" },
        { "do": "run_sneakernet", "servername": "test-source", "verbosity": 3, "expect_exit": 0 },
        { "do": "run_sneakernet", "servername": "test-target", "verbosity": 3, "log": "part7_2b", "expect_exit": 0,
          "capture": { "name": "LOG72B" } },
        { "do": "zfs_list_snapshots", "dataset": "{SRC}/ds1", "capture": { "name": "SRC_DS1_SNAPS_72B", "select": "@(.*)$", "reduce": "sorted_join" } },
        { "do": "zfs_list_snapshots", "dataset": "{DST}/ds1", "capture": { "name": "DST_DS1_SNAPS_72B", "select": "@(.*)$", "reduce": "sorted_join" } }
      ],
      "assertions": [
        { "type": "regex_present", "var": "LOG72B", "pattern": "(?i)allowFullOverwrite is enabled - destroying existing.*to receive a full stream", "message": "log shows the destroy-and-retry being triggered" },
        { "type": "regex_present", "var": "LOG72B", "pattern": "(?i)Retrying receive", "message": "log shows the retry" },
        { "type": "values_equal", "a": "SRC_DS1_SNAPS_72B", "b": "DST_DS1_SNAPS_72B", "message": "the receive succeeded and the target's snapshot set for ds1 matches source's current set - the destroy-and-retry preserved all available history, same reasoning as Step 7.1" }
      ]
    },
    { "id": "7.2c", "title": "Safety check: an unrelated failure must NOT destroy anything", "depends_on": ["7.2b"],
      "_comment": "TESTING.md's literal text hand-crafts the garbage file as 'storage.testing.dst.ds1', but sneakernet's own filename convention (dirnameToFileName/fullDatasetName, confirmed by reading sneakernet source) uses the flat configured 'dataset' key with no pool-path prefix - Step 8.5 confirms this, hand-crafting its plain file as bare 'ds2'. The dotted long form would fail dataset-name translation before ever reaching decrypt/receive, silently testing nothing. Using the flat name 'ds1' here so the garbage stream actually reaches zfs receive and produces the genuine import error this step is meant to verify. ds2 is disabled for the duration of this step (auto-restored after) so its own, unrelated destroy-and-retry cycle (ds2 already has target snapshots from earlier Parts) can't bleed into LOG72C's assertions below - this step is scoped to ds1 only.",
      "config": { "mutations": [ { "op": "comment_out", "path": "datasets.ds2" } ] },
      "actions": [
        { "do": "zfs_destroy", "dataset": "{DST}/ds1", "recursive": 1, "missing_ok": 1 },
        { "do": "rm_matching", "dir": "{SN}", "pattern": "^sneakernet_target\\.status(\\.[0-9_.-]+)?$" },
        { "do": "zfs_snapshot", "dataset": "{SRC}/ds1@part7_2c" },
        { "do": "empty_dir", "path": "{TRANSPORT}" },
        { "do": "run_sneakernet", "servername": "test-source", "verbosity": 1, "expect_exit": 0 },
        { "do": "run_sneakernet", "servername": "test-target", "verbosity": 1, "expect_exit": 0 },
        { "do": "dataset_exists", "dataset": "{DST}/ds1", "store_as": "DS1_RECREATED" },
        { "do": "rm_matching", "dir": "{TRANSPORT}/datasets", "pattern": "^ds1(\\.IV)?$" },
        { "do": "random_bytes", "bytes": 200000, "store_as": "GARBAGE72C" },
        { "do": "random_hex", "bytes": 16, "store_as": "IVHEX72C" },
        { "do": "openssl_encrypt_to_file", "key": "$TESTKEY", "iv": "$IVHEX72C", "plaintext": "$GARBAGE72C",
          "out_path": "{TRANSPORT}/datasets/ds1" },
        { "do": "write_file", "path": "{TRANSPORT}/datasets/ds1.IV", "content": "$IVHEX72C" },
        { "do": "write_file", "path": "{TRANSPORT}/serial.txt", "content": "1" },
        { "do": "zfs_list_snapshots", "dataset": "{DST}/ds1", "capture": { "name": "DS1_BEFORE_72C", "reduce": "count" } },
        { "do": "run_sneakernet", "servername": "test-target", "verbosity": 3, "log": "part7_2c", "expect_exit": "any",
          "capture": { "name": "LOG72C" } },
        { "do": "zfs_list_snapshots", "dataset": "{DST}/ds1", "capture": { "name": "DS1_AFTER_72C", "reduce": "count" } }
      ],
      "assertions": [
        { "type": "true", "var": "DS1_RECREATED", "message": "target has ds1 with a snapshot again" },
        { "type": "regex_absent", "var": "LOG72C", "pattern": "(?i)destroying existing", "message": "PASS: no destroy - an unrelated (garbage-stream) failure must not trigger destroy-and-retry" },
        { "type": "regex_absent", "var": "LOG72C", "pattern": "(?i)destination .*exists|has snapshots|must destroy", "message": "the failure is a genuine import/receive error, not classified as the snapshot-exists family" },
        { "type": "values_equal", "a": "DS1_BEFORE_72C", "b": "DS1_AFTER_72C", "message": "before equals after - nothing was destroyed" }
      ]
    },
    { "id": "7.2-cleanup", "title": "Restore allowFullOverwrite: 0", "depends_on": ["7.2c"],
      "config": { "restore": "none", "mutations": [
        { "op": "set", "path": "target.allowFullOverwrite", "value": "0" }
      ] }
    }
  ]
}
