use super::*;
fn request(
target: &Path,
id: &str,
files: &[(&str, &str, u32)],
) -> (serde_json::Value, Vec<String>) {
request_bytes(target, id, bundle_bytes(files))
}
fn request_bytes(
target: &Path,
id: &str,
(bytes, logical, artifact): (Vec<u8>, String, String),
) -> (serde_json::Value, Vec<String>) {
let bundle = target.parent().unwrap().join(format!("{id}.zip"));
fs::write(&bundle, &bytes).unwrap();
let flags = bundle_flags(&bundle, &logical, &artifact, bytes.len());
let mut plan = args(
"plan-operation",
target,
&[
"--operation",
"replace",
"--recovery-mode",
"forward",
"--provider-release-digest",
RELEASE,
"--operation-id",
id,
"--expires-at",
far_future(),
],
);
plan.extend(flags.clone());
let planned = run(plan);
let plan_path = target.parent().unwrap().join(format!("{id}.json"));
fs::write(
&plan_path,
crate::setup_core::canonical::to_canonical_bytes(&planned["plan"]).unwrap(),
)
.unwrap();
let mut apply = args(
"apply-operation",
target,
&[
"--plan",
plan_path.to_str().unwrap(),
"--plan-digest",
planned["plan_digest"].as_str().unwrap(),
"--provider-release-digest",
RELEASE,
],
);
apply.extend(flags);
(planned, apply)
}
fn contribution_bytes(path: &str, host: &[u8], assembled: &str) -> (Vec<u8>, String, String) {
use crate::provider_v3::zip::{
self,
build::{Entry, write},
};
let (bytes, _, _) = bundle_bytes(&[(path, assembled, 0o644)]);
let mut members = zip::read(&bytes).unwrap();
let mut manifest: serde_json::Value = serde_json::from_slice(&members[0].data).unwrap();
manifest["files"][0]["contribution"] = serde_json::json!({
"parser_id": if Path::new(path).extension().is_some_and(|ext| ext == "toml") { "toml/1" } else { "json/1" },
"ownership_key":"managed", "host_digest":digest::of_bytes(host)
});
manifest.as_object_mut().unwrap().remove("bundle_digest");
let logical =
digest::of_domain_canonical_json(crate::provider_v3::bundle::BUNDLE_DOMAIN, &manifest).unwrap();
manifest["bundle_digest"] = logical.clone().into();
members[0].data = crate::setup_core::canonical::to_canonical_bytes(&manifest).unwrap();
let entries: Vec<_> = members
.into_iter()
.map(|m| Entry {
name: m.name,
data: m.data,
mode: m.mode.unwrap(),
})
.collect();
let bytes = write(&entries);
let artifact = digest::of_bytes(&bytes);
(bytes, logical, artifact)
}
#[test]
fn assembled_contributions_do_not_claim_preserved_host_fields() {
let examples = [
(
"settings.json",
r#"{"user":9007199254740993}"#,
r#"{"user":9007199254740993,"managed":{"enabled":true}}"#,
),
(
"skills/host.jsonc",
"{\n // user comment\n \"user\":9007199254740993,\n}",
"{\"user\":9007199254740993,\"managed\":{\"enabled\":true}}",
),
(
"skills/host.toml",
"# user comment\nuser = 9007199254740993\n",
"user = 9007199254740993\n[managed]\nenabled = true\n",
),
];
for (index, (path, host, assembled)) in examples.into_iter().enumerate() {
let target = seeded(&format!("forward-contribution-{index}"));
fs::write(target.join(path), host).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(target.join(path), fs::Permissions::from_mode(0o600)).unwrap();
}
let (_, apply) = request_bytes(
&target,
"operation_FIELDS_1",
contribution_bytes(path, host.as_bytes(), assembled),
);
fs::write(target.join(path), host.replace("9007199254740993", "123")).unwrap();
assert_eq!(refuse(apply.clone()).reason(), Some(WireReason::Stale));
fs::write(target.join(path), host).unwrap();
assert_eq!(run(apply)["state"], "verified");
assert_recorded_contribution(&target, path, host);
let (_, withdraw) = request(
&target,
"operation_FIELDS_2",
&[("skills/next.md", "next", 0o644)],
);
assert_eq!(run(withdraw)["state"], "verified");
assert_eq!(
run(args("status", &target, &[]))["configuration_ownership"],
serde_json::json!([{
"path":"skills/next.md","ownership":"whole","parser_id":null,"ownership_keys":[]
}])
);
let retained = fs::read_to_string(target.join(path)).unwrap();
assert!(retained.contains("9007199254740993"));
assert!(!retained.contains("managed"));
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
assert_eq!(
fs::metadata(target.join(path))
.unwrap()
.permissions()
.mode()
& 0o777,
0o600
);
}
if Path::new(path)
.extension()
.is_some_and(|ext| ext == "jsonc")
|| Path::new(path).extension().is_some_and(|ext| ext == "toml")
{
assert!(retained.contains("user comment"));
}
assert!(!target.join(TEST.control_directory).join("backups").exists());
fs::remove_dir_all(target.parent().unwrap()).unwrap();
}
}
fn assert_recorded_contribution(target: &Path, path: &str, host: &str) {
let ledger: serde_json::Value = serde_json::from_slice(
&fs::read(
target
.join(TEST.control_directory)
.join("written-fields.json"),
)
.unwrap(),
)
.unwrap();
assert_eq!(ledger[path], serde_json::json!(["managed"]));
let expected = serde_json::json!([{
"path":path,"ownership":"contribution",
"parser_id":if Path::new(path).extension().is_some_and(|ext| ext == "toml") { "toml/1" } else { "json/1" },
"ownership_keys":["managed"]
}]);
assert_eq!(
run(args("status", target, &[]))["configuration_ownership"],
expected
);
let installed = fs::read(target.join(path)).unwrap();
fs::write(target.join(path), host).unwrap();
let drifted = run(args("status", target, &[]));
assert_eq!(drifted["provider_state"]["drift_state"], "local_drift");
assert_eq!(drifted["configuration_ownership"], expected);
fs::write(target.join(path), installed).unwrap();
let ledger_path = target
.join(TEST.control_directory)
.join("written-fields.json");
for broken in [
format!(r#"{{"{path}":["managed"],"{path}":["other"]}}"#),
format!(r#"{{"{path}":["managed","managed"]}}"#),
r#"{"unrecorded.json":["managed"]}"#.into(),
"[".into(),
] {
fs::write(&ledger_path, broken).unwrap();
assert!(run(args("status", target, &[]))["configuration_ownership"].is_null());
}
fs::write(&ledger_path, serde_json::to_vec(&ledger).unwrap()).unwrap();
}
fn settings(target: &Path) -> serde_json::Value {
serde_json::from_slice(&fs::read(target.join("skills/settings.json")).unwrap()).unwrap()
}
#[test]
fn forward_whole_files_replace_exact_bytes_and_replay_only_the_original_receipt() {
let target = seeded("forward-config");
assert_eq!(
run(args("status", &target, &[]))["configuration_ownership"],
serde_json::json!([])
);
let (planned, apply) = request(
&target,
"operation_FORWARD_1",
&[
(
"skills/settings.json",
r#"{ "managed": 1, "retired": true }"#,
0o644,
),
("skills/new.md", "owned skill", 0o644),
],
);
assert_eq!(planned["plan"]["recovery_mode"], "forward");
assert!(
!target.join(TEST.control_directory).exists(),
"planning created control state"
);
let first = run(apply.clone());
assert_eq!(first["state"], "verified");
assert!(first["backup_ref"].is_null());
assert_eq!(
run(args("status", &target, &[]))["configuration_ownership"],
serde_json::json!([
{"path":"skills/new.md","ownership":"whole","parser_id":null,"ownership_keys":[]},
{"path":"skills/settings.json","ownership":"whole","parser_id":null,"ownership_keys":[]}
])
);
assert_eq!(
fs::read(target.join("skills/settings.json")).unwrap(),
br#"{ "managed": 1, "retired": true }"#
);
assert_eq!(
settings(&target),
serde_json::json!({"managed":1,"retired":true})
);
let (_, replace) = request(
&target,
"operation_FORWARD_2",
&[
("skills/settings.json", r#"{"managed":2}"#, 0o644),
("skills/next.md", "next owned skill", 0o644),
],
);
assert_eq!(run(replace)["state"], "verified");
assert_eq!(settings(&target), serde_json::json!({"managed":2}));
assert!(!target.join("skills/new.md").exists());
assert_eq!(fs::read(target.join("skills/a.md")).unwrap(), b"skill one");
let bundle_path = apply.windows(2).find(|pair| pair[0] == "--bundle").unwrap()[1].clone();
fs::remove_file(bundle_path).unwrap();
assert_eq!(run(apply), first, "historical replay changed its outcome");
assert_eq!(
settings(&target)["managed"],
2,
"historical replay overwrote the current setup"
);
assert_eq!(
fs::read(target.join(".credentials.json")).unwrap(),
b"SECRET"
);
assert!(!target.join(TEST.control_directory).join("backups").exists());
assert!(
!target
.join(TEST.control_directory)
.join("transaction")
.exists()
);
assert!(
Journal::read(&target.join(TEST.control_directory))
.unwrap()
.is_none()
);
let StateReading::Current(state) = ProviderState::read(&target, TEST.state_file).unwrap()
else {
panic!("provider state is missing")
};
assert!(state.backup_ref.is_none());
assert_eq!(state.operation_id, "operation_FORWARD_2");
let (_, withdraw) = request(
&target,
"operation_FORWARD_3",
&[("skills/last.md", "last", 0o644)],
);
assert_eq!(run(withdraw)["state"], "verified");
assert!(!target.join("skills/settings.json").exists());
assert_unknown_content_refused(&target, "skills/a.md", "would overwrite");
assert_unknown_content_refused(&target, "settings.json", r#"{"model":"changed"}"#);
fs::remove_dir_all(target.parent().unwrap()).unwrap();
}
fn assert_unknown_content_refused(target: &Path, path: &str, body: &str) {
let before = fs::read(target.join(path)).unwrap();
let (bytes, logical, artifact) = bundle_bytes(&[(path, body, 0o644)]);
let bundle = target.parent().unwrap().join("conflict.zip");
fs::write(&bundle, &bytes).unwrap();
let mut conflict = args(
"plan-operation",
target,
&[
"--operation",
"install",
"--recovery-mode",
"forward",
"--provider-release-digest",
RELEASE,
"--operation-id",
"operation_FORWARD_3",
"--expires-at",
far_future(),
],
);
conflict.extend(bundle_flags(&bundle, &logical, &artifact, bytes.len()));
assert_eq!(refuse(conflict).reason(), Some(WireReason::Stale));
assert_eq!(fs::read(target.join(path)).unwrap(), before);
}