| 155 |
155 |
|
);
|
| 156 |
156 |
|
}
|
| 157 |
157 |
|
|
|
158 |
+ |
/// Nothing outside a conditional may read a path the server prune deletes.
|
|
159 |
+ |
///
|
|
160 |
+ |
/// The rule above catches a conditional with a silent half. This catches the
|
|
161 |
+ |
/// other shape of the same mistake, which has no conditional to inspect: an
|
|
162 |
+ |
/// unconditional assertion about a path that only one profile still has.
|
|
163 |
+ |
///
|
|
164 |
+ |
/// It is not hypothetical. `/usr/share/alloy/skel-night` is removed by the
|
|
165 |
+ |
/// prune and was then asserted present, unconditionally, three instructions
|
|
166 |
+ |
/// from the end — so `PROFILE=server` failed every time, and nobody knew until
|
|
167 |
+ |
/// the first server image was built on 2026-08-03, two days after the split
|
|
168 |
+ |
/// landed. Every static check in the tree passed the whole time, this one
|
|
169 |
+ |
/// included, because it did not exist.
|
|
170 |
+ |
///
|
|
171 |
+ |
/// Deliberately narrow: only the paths the prune names, and only whole-word
|
|
172 |
+ |
/// matches. A general "does this instruction hold on both profiles" is not
|
|
173 |
+ |
/// something a text check can answer, and pretending otherwise would produce a
|
|
174 |
+ |
/// test that fails on things that are fine.
|
|
175 |
+ |
#[test]
|
|
176 |
+ |
fn nothing_unconditional_reads_what_the_server_prune_removes() {
|
|
177 |
+ |
let text = containerfile();
|
|
178 |
+ |
let instructions = instructions(&text);
|
|
179 |
+ |
|
|
180 |
+ |
let pruning = instructions
|
|
181 |
+ |
.iter()
|
|
182 |
+ |
.find(|i| i.contains("/etc/skel/.config/sway") && i.contains("rm -rf"))
|
|
183 |
+ |
.expect("no server prune of the desktop skeleton");
|
|
184 |
+ |
|
|
185 |
+ |
// The paths between `rm -rf` and the first `;` that follows it.
|
|
186 |
+ |
let removed: Vec<&str> = pruning
|
|
187 |
+ |
.split_once("rm -rf ")
|
|
188 |
+ |
.expect("the prune has an rm -rf")
|
|
189 |
+ |
.1
|
|
190 |
+ |
.split(';')
|
|
191 |
+ |
.next()
|
|
192 |
+ |
.unwrap_or("")
|
|
193 |
+ |
.split_whitespace()
|
|
194 |
+ |
.filter(|word| word.starts_with('/'))
|
|
195 |
+ |
.collect();
|
|
196 |
+ |
assert!(!removed.is_empty(), "read no paths out of the server prune");
|
|
197 |
+ |
|
|
198 |
+ |
for instruction in &instructions {
|
|
199 |
+ |
// The prune itself, and anything that already branches on the profile,
|
|
200 |
+ |
// are both accounted for: the branch is what the other test polices.
|
|
201 |
+ |
if std::ptr::eq(instruction, pruning) || instruction.contains("$PROFILE\" =") {
|
|
202 |
+ |
continue;
|
|
203 |
+ |
}
|
|
204 |
+ |
// The rust-build stage runs before the runtime image exists and stages
|
|
205 |
+ |
// these trees under its own prefixes, which the prune never touches.
|
|
206 |
+ |
if instruction.contains("/staged-skel") {
|
|
207 |
+ |
continue;
|
|
208 |
+ |
}
|
|
209 |
+ |
for path in &removed {
|
|
210 |
+ |
assert!(
|
|
211 |
+ |
!instruction.contains(path),
|
|
212 |
+ |
"`{path}` is removed by the server prune, but this instruction reads it \
|
|
213 |
+ |
with no $PROFILE conditional, so it can only hold on client:\n {}",
|
|
214 |
+ |
&instruction[..instruction.len().min(200)],
|
|
215 |
+ |
);
|
|
216 |
+ |
}
|
|
217 |
+ |
}
|
|
218 |
+ |
}
|
|
219 |
+ |
|
| 158 |
220 |
|
/// The one thing the brief insists survives on a headless machine: the bare
|
| 159 |
221 |
|
/// console palette is the only themed surface there, so a prune that took it
|
| 160 |
222 |
|
/// would take the whole design system with it.
|