use super::types::*;
use crate::mcp::DriftHandler;
use pforge_runtime::Handler;
fn write_lock(state_dir: &std::path::Path, machine: &str, resources_yaml: &str) {
let md = state_dir.join(machine);
std::fs::create_dir_all(&md).unwrap();
std::fs::write(
md.join("state.lock.yaml"),
format!(
"schema: \"1.0\"\nmachine: {machine}\nhostname: {machine}\ngenerated_at: now\n\
generator: test\nblake3_version: \"1\"\nresources:\n{resources_yaml}"
),
)
.unwrap();
}
#[tokio::test]
async fn a_locked_package_with_an_observed_hash_is_actually_queried() {
let d = tempfile::tempdir().unwrap();
let cfg = d.path().join("forjar.yaml");
std::fs::write(
&cfg,
"version: \"1.0\"\nname: e05query\nmachines:\n local:\n hostname: localhost\n\
\x20 addr: 127.0.0.1\nresources:\n pkg:\n type: package\n machine: local\n\
\x20 provider: apt\n packages: [forjar-not-a-real-package]\n",
)
.unwrap();
let state_dir = d.path().join("state");
write_lock(
&state_dir,
"local",
" pkg:\n type: package\n status: converged\n hash: \"h\"\n\
\x20 observed: \"a-hash-the-machine-cannot-produce\"\n",
);
let out = DriftHandler
.handle(DriftInput {
path: cfg.display().to_string(),
state_dir: Some(state_dir.display().to_string()),
machine: None,
})
.await
.expect("drift runs");
let json = serde_json::to_value(&out).expect("DriftOutput serialises");
let m = &json["census"][0];
assert_eq!(
m["inspected_by_type"]["package"], 1,
"forjar#407: the package was never queried — a census that only \
renames the skip reason is not the fix: {m}"
);
let f = out
.findings
.iter()
.find(|f| f.resource == "pkg")
.unwrap_or_else(|| panic!("no verdict for `pkg`: {:?}", out.findings));
assert_ne!(
f.actual_hash, "a-hash-the-machine-cannot-produce",
"the actual hash must come from the target's stdout: {f:?}"
);
assert!(out.drifted, "{json}");
}
#[tokio::test]
async fn the_readonly_verb_must_not_run_a_config_declared_completion_check() {
let d = tempfile::tempdir().unwrap();
let trap = d.path().join("COMPLETION_CHECK_FIRED");
let cfg = d.path().join("forjar.yaml");
std::fs::write(
&cfg,
format!(
"version: \"1.0\"\nname: e05trap\nmachines:\n local:\n hostname: localhost\n\
\x20 addr: 127.0.0.1\nresources:\n guard:\n type: task\n machine: local\n\
\x20 command: \"true\"\n completion_check: \"touch {}\"\n",
trap.display()
),
)
.unwrap();
let state_dir = d.path().join("state");
write_lock(
&state_dir,
"local",
" guard:\n type: task\n status: converged\n hash: \"h\"\n",
);
let _ = DriftHandler
.handle(DriftInput {
path: cfg.display().to_string(),
state_dir: Some(state_dir.display().to_string()),
machine: None,
})
.await
.expect("drift runs");
assert!(
!trap.exists(),
"forjar#372: `forjar_drift` publishes readOnlyHint: true and \
`unattended::sanitize_config` promises this surface never executes \
what a config declares — but the completion_check of a locked task \
ran on the controller. An agent pointed at an untrusted checkout \
carrying its own `state/` executes whatever that checkout declares."
);
}
#[tokio::test]
async fn declining_to_run_the_check_is_reported_not_silent() {
let d = tempfile::tempdir().unwrap();
let trap = d.path().join("COMPLETION_CHECK_FIRED");
let cfg = d.path().join("forjar.yaml");
std::fs::write(
&cfg,
format!(
"version: \"1.0\"\nname: e05disclose\nmachines:\n local:\n hostname: localhost\n\
\x20 addr: 127.0.0.1\nresources:\n guard:\n type: task\n machine: local\n\
\x20 command: \"true\"\n completion_check: \"touch {}\"\n",
trap.display()
),
)
.unwrap();
let state_dir = d.path().join("state");
write_lock(
&state_dir,
"local",
" guard:\n type: task\n status: converged\n hash: \"h\"\n",
);
let out = DriftHandler
.handle(DriftInput {
path: cfg.display().to_string(),
state_dir: Some(state_dir.display().to_string()),
machine: None,
})
.await
.expect("drift runs");
let json = serde_json::to_value(&out).expect("DriftOutput serialises");
let m = &json["census"][0];
assert_eq!(
m["skipped_by_reason"]["--no-task-checks"], 1,
"the assertion was not executed and the census does not say so: {m}"
);
assert!(
out.unattended_skipped.iter().any(|s| s.contains("guard")),
"forjar#372's disclosure names what was skipped and why: {:?}",
out.unattended_skipped
);
}