{
  "part": 15,
  "title": "Config-rotation one-shot script (updateConfigKeys)",
  "reset": "standard",
  "steps": [
    { "id": "15.1", "title": "Prepare a customized copy",
      "actions": [
        { "do": "copy_file", "from": "{CODE}/sneakernet/cleanupScripts/updateConfigKeys", "to": "{ONESHOT}/rotateTest" },
        { "do": "patch_oneshot_updates", "path": "{ONESHOT}/rotateTest", "entries": [ "debug=7" ] },
        { "do": "read_file", "path": "{ONESHOT}/rotateTest", "capture": { "name": "ROTATE_TEXT_151" } }
      ],
      "assertions": [
        { "type": "regex_present", "var": "ROTATE_TEXT_151", "pattern": "(?m)^    'debug=7',$", "message": "the customized copy's @updates block was rewritten to just debug=7" }
      ]
    },
    { "id": "15.2", "title": "Run it and confirm the target's config actually changed", "depends_on": ["15.1"],
      "actions": [
        { "do": "empty_dir", "path": "{TRANSPORT}" },
        { "do": "run_sneakernet", "servername": "test-source", "verbosity": 2, "expect_exit": 0 },
        { "do": "list_matching", "dir": "{ONESHOT}", "pattern": "^rotateTest$",
          "capture": { "name": "ONESHOT_AFTER_152", "reduce": "count" } },
        { "do": "run_sneakernet", "servername": "test-target", "verbosity": 2, "log": "part15_2", "expect_exit": 0,
          "capture": { "name": "LOG152" } },
        { "do": "read_file", "path": "{CONFIG}", "capture": { "name": "CONFIG_TEXT_152" } },
        { "do": "list_matching", "dir": "{SN}", "pattern": "^sneakernet\\.conf\\.yaml\\.bak\\.\\S+$",
          "capture": { "name": "BAK_NAME_152" } }
      ],
      "assertions": [
        { "type": "count_equals", "var": "ONESHOT_AFTER_152", "n": 0, "message": "rotateTest was shipped and removed - a one-shot only ships once" },
        { "type": "regex_present", "var": "LOG152", "pattern": "(?m)^Updated: debug$", "message": "the script's own report shows it updated debug" },
        { "type": "regex_present", "var": "LOG152", "pattern": "(?m)^  Old value: 0$", "message": "old value 0 is reported" },
        { "type": "regex_present", "var": "LOG152", "pattern": "(?m)^  New value: 7$", "message": "new value 7 is reported" },
        { "type": "regex_present", "var": "CONFIG_TEXT_152", "pattern": "(?m)^debug: ['\"]?7['\"]?$", "message": "sneakernet.conf.yaml on disk now actually shows debug: '7'" },
        { "type": "true", "var": "BAK_NAME_152", "message": "a timestamped backup file sneakernet.conf.yaml.bak.<timestamp> exists" }
      ]
    },
    { "id": "15.2-verify-backup", "title": "Confirm the backup holds the OLD value", "depends_on": ["15.2"],
      "actions": [
        { "do": "read_file_matching", "dir": "{SN}", "pattern": "^sneakernet\\.conf\\.yaml\\.bak\\.\\S+$",
          "capture": { "name": "BAK_TEXT_152" } }
      ],
      "assertions": [
        { "type": "regex_present", "var": "BAK_TEXT_152", "pattern": "(?m)^debug: ['\"]?0['\"]?$", "message": "the backup file contains the old value (debug: '0')" }
      ]
    },
    { "id": "15.2-cleanup", "title": "Set debug back to 0", "depends_on": ["15.2-verify-backup"],
      "config": { "restore": "none", "mutations": [
        { "op": "set", "path": "debug", "value": "0" }
      ] }
    },
    { "id": "15.3", "title": "Confirm it refuses to run against a broken config", "depends_on": ["15.2-cleanup"],
      "_comment": "restore defaults to 'auto' (not overridden here) so the harness reverts the config to its pre-step (good) text after this step regardless of outcome - the exact recovery TESTING.md describes doing by hand from a backup copy. The step's own assertion instead checks that nothing WITHIN the run further corrupted the already-broken file beyond our own deliberate append.",
      "config": {
        "mutations": [ { "op": "append_raw", "text": "not valid yaml: [[[\n" } ],
        "expects_invalid_config": true
      },
      "actions": [
        { "do": "copy_file", "from": "{CODE}/sneakernet/cleanupScripts/updateConfigKeys", "to": "{ONESHOT}/rotateTest2" },
        { "do": "read_file", "path": "{CONFIG}", "capture": { "name": "CONFIG_AFTER_CORRUPTION_153" } },
        { "do": "empty_dir", "path": "{TRANSPORT}" },
        { "do": "run_sneakernet", "servername": "test-source", "verbosity": 2, "log": "part15_3", "expect_exit": "any",
          "capture": { "name": "LOG153" } },
        { "do": "read_file", "path": "{CONFIG}", "capture": { "name": "CONFIG_AFTER_RUN_153" } },
        { "do": "rm_matching", "dir": "{ONESHOT}", "pattern": "^rotateTest2$" }
      ],
      "assertions": [
        { "type": "values_equal", "a": "CONFIG_AFTER_CORRUPTION_153", "b": "CONFIG_AFTER_RUN_153", "message": "sneakernet.conf.yaml is confirmed unchanged by the run attempt - still broken, not truncated to something worse; nothing got silently corrupted" }
      ]
    }
  ]
}
