|
9 | 9 | //! |
10 | 10 | //! ## Scope (this chunk) |
11 | 11 | //! |
12 | | -//! `meta`, `kernel`, `packages`, and `services` are emitted. Unprobed |
13 | | -//! `meta` fields (`timezone`, `arch_level`, `locale`, `keymap`) emit |
14 | | -//! conservative defaults that the operator hand-edits after reconcile |
15 | | -//! lands the file at `<config_dir>/hosts/<host>.imported.ncl`. |
16 | | -//! Subsequent chunks add the `users` and config-file blocks. |
| 12 | +//! `meta`, `kernel`, `packages`, `services`, and `users` are emitted. |
| 13 | +//! Unprobed `meta` fields (`timezone`, `arch_level`, `locale`, |
| 14 | +//! `keymap`) emit conservative defaults that the operator hand-edits |
| 15 | +//! after reconcile lands the file at |
| 16 | +//! `<config_dir>/hosts/<host>.imported.ncl`. Subsequent chunks add the |
| 17 | +//! config-file block. |
| 18 | +//! |
| 19 | +//! ## Users-block caveat |
| 20 | +//! |
| 21 | +//! [`ProbedState`] carries no user inventory — Pearlite manages |
| 22 | +//! declared users (Plan §6.7) but does not enumerate `/etc/passwd` at |
| 23 | +//! probe time, since reconcile-without-declarations would surface |
| 24 | +//! every system account (root, daemon, http, etc.) as drift. The |
| 25 | +//! emitter therefore writes `users = []` as a placeholder; the |
| 26 | +//! operator is expected to add `UserDecl` entries by hand after the |
| 27 | +//! initial import. A populated users inventory is a v1.1 candidate |
| 28 | +//! (PRD §17.1) and would slot in here without changing the block |
| 29 | +//! shape. |
17 | 30 | //! |
18 | 31 | //! ## Services-block caveat |
19 | 32 | //! |
@@ -75,10 +88,15 @@ pub fn emit_host(probed: &ProbedState) -> String { |
75 | 88 | push_kernel(&mut out, &probed.kernel.package); |
76 | 89 | push_packages(&mut out, probed.pacman.as_ref(), probed.cargo.as_ref()); |
77 | 90 | push_services(&mut out, probed.services.as_ref()); |
| 91 | + push_users(&mut out); |
78 | 92 | out.push_str("}\n"); |
79 | 93 | out |
80 | 94 | } |
81 | 95 |
|
| 96 | +fn push_users(out: &mut String) { |
| 97 | + out.push_str(" users = [],\n"); |
| 98 | +} |
| 99 | + |
82 | 100 | fn push_meta(out: &mut String, hostname: &str) { |
83 | 101 | out.push_str(" meta = {\n"); |
84 | 102 | push_field(out, "hostname", hostname); |
@@ -445,6 +463,28 @@ mod tests { |
445 | 463 | ); |
446 | 464 | } |
447 | 465 |
|
| 466 | + #[test] |
| 467 | + fn emit_users_emits_empty_array_placeholder() { |
| 468 | + // ProbedState carries no user inventory, so the emitter writes |
| 469 | + // a literal empty array — the operator hand-edits after the |
| 470 | + // imported.ncl lands. |
| 471 | + let out = emit_host(&probed_with("forge", "linux-cachyos")); |
| 472 | + assert!(out.contains("users = [],")); |
| 473 | + } |
| 474 | + |
| 475 | + #[test] |
| 476 | + fn emit_users_appears_after_services() { |
| 477 | + let mut probed = probed_with("forge", "linux-cachyos"); |
| 478 | + probed.services = Some(make_services(&["sshd.service"], &[], &[])); |
| 479 | + let out = emit_host(&probed); |
| 480 | + let services_at = out.find("services = ").expect("services block"); |
| 481 | + let users_at = out.find("users = ").expect("users block"); |
| 482 | + assert!( |
| 483 | + services_at < users_at, |
| 484 | + "users must follow services for stable golden-fixture diffs" |
| 485 | + ); |
| 486 | + } |
| 487 | + |
448 | 488 | #[test] |
449 | 489 | fn emit_meta_block_appears_before_kernel_block() { |
450 | 490 | // Stable ordering matters for operator review (and for |
|
0 commit comments