1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
//! GH-254: a task must not report converged without reaching its declared state.
//!
//! `completion_check` was consulted only as a guard on whether to RUN the
//! command. It was never re-evaluated afterwards, so `converged` meant "the
//! command exited 0", not "the resource is in the state it declares". The lock
//! then recorded success and the next `plan` reported `no changes` over a host
//! that had never converged.
//!
//! This is not hypothetical. On paiml/infra's `lean-toolchain`, `sudo: true`
//! made `$HOME=/root`, so the Lean toolchain installed where the runner user
//! could not read it. Every command in the script succeeded. `forjar apply`
//! reported `1 converged, 0 failed`. `command -v lean` failed immediately
//! afterwards.
//!
//! The distinction these tests pin is between two different failures that
//! previously looked identical — except that the second looked like a success:
//!
//! * the command errored -> already reported as failure
//! * the command ran and achieved nothing -> reported as CONVERGED
//!
//! The emitted script is EXECUTED here rather than pattern-matched. A
//! `script.contains("completion_check")` assertion would pass on a script that
//! never runs the check, which is the class of test that let the original
//! defect through.
use forjar::core::types::{MachineTarget, Resource, ResourceType};
use std::process::Command;
fn task(command: &str, completion_check: Option<&str>) -> Resource {
Resource {
resource_type: ResourceType::Task,
machine: MachineTarget::Single("local".to_string()),
command: Some(command.to_string()),
completion_check: completion_check.map(str::to_string),
..Default::default()
}
}
/// Run an emitted apply script under bash and return its exit status.
fn run(script: &str) -> std::process::Output {
Command::new("bash")
.arg("-c")
.arg(script)
.output()
.expect("bash must run")
}
#[test]
fn a_command_that_succeeds_without_converging_fails() {
// THE REGRESSION. `true` exits 0 and achieves nothing; `false` is a
// completion_check that can never hold. Before GH-254 this emitted a script
// that exited 0, and forjar reported the resource converged.
let script = forjar::resources::task::apply_script(&task("true", Some("false")));
let out = run(&script);
assert!(
!out.status.success(),
"a task whose completion_check still fails must NOT converge.\n\
script:\n{script}"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("not-converged"),
"the failure must say the declared state was not reached, so it is \
distinguishable from a command error: {stderr}"
);
}
#[test]
fn a_command_that_genuinely_converges_still_passes() {
// The gate must be passable, or it trains people to delete it. Here the
// command actually produces the condition the check tests.
let dir = tempfile::tempdir().unwrap();
let marker = dir.path().join("done");
let script = forjar::resources::task::apply_script(&task(
&format!("touch {}", marker.display()),
Some(&format!("test -f {}", marker.display())),
));
let out = run(&script);
assert!(
out.status.success(),
"a task that reaches its declared state must converge.\nstderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(marker.exists());
}
#[test]
fn a_failing_command_is_still_reported_as_a_command_failure() {
// The two failures must stay distinguishable. A command that errors should
// NOT be relabelled as a convergence problem — that would trade one
// misleading report for another.
let script = forjar::resources::task::apply_script(&task("exit 3", Some("true")));
let out = run(&script);
assert!(!out.status.success(), "a failing command must fail");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("not-converged"),
"a command error must not be reported as a convergence failure: {stderr}"
);
}
#[test]
fn a_task_without_a_completion_check_is_unaffected() {
// No check declared means nothing to verify. This must not become a way to
// fail tasks that never made the claim in the first place.
let script = forjar::resources::task::apply_script(&task("true", None));
let out = run(&script);
assert!(
out.status.success(),
"a task with no completion_check must behave as before.\nstderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn a_multiline_completion_check_still_emits_valid_bash() {
// A YAML `|` block scalar keeps its trailing newline. completion_check
// was interpolated directly into `if ! { <check> ; }; then`, so a
// multi-line check like "test A &&\ntest B\n" put the closing `; }; then`
// on its own line — right after a newline that already terminated the
// previous command, making the leading `;` an empty statement: "syntax
// error near unexpected token `;'". Every multi-line completion_check in
// paiml/infra's machines/intel/forjar.yaml hit this on --force apply
// (2026-08-18): 14 resources failed with the same parse error before any
// command in the script ran.
let script =
forjar::resources::task::apply_script(&task("true", Some("test 1 = 1 &&\ntest 2 = 2\n")));
assert!(
std::process::Command::new("bash")
.arg("-n")
.arg("-c")
.arg(&script)
.status()
.expect("bash must run")
.success(),
"a multi-line completion_check must still produce syntactically valid bash:\n{script}"
);
let out = run(&script);
assert!(
out.status.success(),
"a task whose multi-line completion_check genuinely holds must converge.\nstderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn the_check_runs_after_the_command_not_before() {
// Ordering is the whole point. If the verification ran first it would test
// the pre-command state and pass vacuously for any task whose check
// happened to hold already — which is exactly the guard semantics being
// fixed. Here the command CREATES the condition, so a check evaluated
// before it would fail.
let dir = tempfile::tempdir().unwrap();
let marker = dir.path().join("created-by-the-command");
assert!(!marker.exists());
let script = forjar::resources::task::apply_script(&task(
&format!("touch {}", marker.display()),
Some(&format!("test -f {}", marker.display())),
));
let out = run(&script);
assert!(
out.status.success(),
"the verification must run AFTER the command, or a task that creates \
its own precondition can never pass.\nstderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}