Add explicit vocabulary and named operation contexts

This commit is contained in:
2026-10-29 22:41:38 +02:00
parent ede45d224a
commit 8413b382b1
14 changed files with 344 additions and 241 deletions
+14 -12
View File
@@ -129,7 +129,7 @@ Each row is a delivery milestone, not a completion claim. Expand its record befo
| 17 | Adaptive client list, persistent details and clear states | 16 accepted; existing M7/M12 TUI | accepted | | 17 | Adaptive client list, persistent details and clear states | 16 accepted; existing M7/M12 TUI | accepted |
| 18 | Readable, complete in-app help for implemented workflows | 17 | accepted | | 18 | Readable, complete in-app help for implemented workflows | 17 | accepted |
| 19 | Refactoring contract, terminology and behavior map | 18 | accepted | | 19 | Refactoring contract, terminology and behavior map | 18 | accepted |
| 20 | Explicit vocabulary and named operation contexts | 19 | pending | | 20 | Explicit vocabulary and named operation contexts | 19 | accepted |
| 21 | Domain ownership of reservations and apply knowledge | 20 | pending | | 21 | Domain ownership of reservations and apply knowledge | 20 | pending |
| 22 | Transactional draft editing and client creation vertical slice | 21 | pending | | 22 | Transactional draft editing and client creation vertical slice | 21 | pending |
| 23 | Readable saved-state Commit and restore scenarios | 22 | pending | | 23 | Readable saved-state Commit and restore scenarios | 22 | pending |
@@ -3877,7 +3877,7 @@ Accepted for presentation and synthetic fixture scope after independent review,
## Approved domain refactoring programme — milestones 19–27 ## Approved domain refactoring programme — milestones 19–27
Status: M19 accepted; M20–M27 are pending. The user approved the proposed direction and requested detailed milestones on 2026-10-29. This paragraph records that plan's origin; current task progress is in the milestone records and resume point below. M18 remains accepted. These milestones preserve the approved product contract and all existing acceptance limitations, including retired/unaccepted M9 and unverified real-system paths. They do not resume VM work. Status: M19–M20 accepted; M21–M27 pending. The user approved the proposed direction and requested detailed milestones on 2026-10-29. This paragraph records that plan's origin; current task progress is in the milestone records and resume point below. M18 remains accepted. These milestones preserve the approved product contract and all existing acceptance limitations, including retired/unaccepted M9 and unverified real-system paths. They do not resume VM work.
### Shared execution contract for M19–M27 ### Shared execution contract for M19–M27
@@ -3928,7 +3928,7 @@ Evidence/handoff: [M19 evidence](docs/evidence/milestone-19.md) identifies the d
### Milestone 20 — Explicit vocabulary and named operation contexts ### Milestone 20 — Explicit vocabulary and named operation contexts
Status: pending. Prerequisite: M19 accepted. Status: accepted (2026-10-29T22:40:28+02:00). Prerequisite: M19 accepted (2026-10-29T22:24:30+02:00). Coordinator model GPT-6-Sol/medium confirmed earlier in this session by the user; worker dispatches selected GPT-6-Luna/high explicitly. Initial tree clean at `ede45d224a9ad255a919900e556a03d7e575430f`; pinned Rust/Cargo 1.97.1, rustfmt 1.9.0 and Clippy 0.1.97 used.
#### Outcome and scope #### Outcome and scope
@@ -3936,9 +3936,9 @@ Remove name ambiguity and opaque tuples without moving business rules. Planned o
| Task | Owner | State | Bounded deliverable | | Task | Owner | State | Bounded deliverable |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| M20.1 | implementer | pending | Rename conflicting knowledge/observation types and migrate callers | | M20.1 | implementer `/root/m20_vocab` | complete | Renamed domain knowledge to ApplyStatus, removed duplicate TUI enum and unnecessary observation aliases, migrated callers; focused tests passed |
| M20.2 | implementer | pending | Replace prepared-operation tuples with private named structures | | M20.2 | implementer `/root/m20_vocab` | complete | Replaced prepared-operation tuples with private named contexts/mode; focused behavior and workflow tests passed |
| M20.3 | reviewer; repairer; coordinator | pending | Review semantic equivalence, repair, RUST acceptance | | M20.3 | fresh reviewer `/root/m20_review`; repairer `/root/m20_repair`; coordinator | complete | M20-R1 repaired and closed on independent re-review; final integrated RUST gates passed |
1. Rename the domain `RuntimeObservation` (currently Unknown/Unavailable/Verified) to the M19 canonical knowledge name, such as `ApplyStatus`; retain runtime::RuntimeObservation for measured interface data. Decide whether tui::AppliedState is a necessary presentation mapping or an exact redundant copy. Remove only redundant representation; do not merge raw statistics and verification status. Inventory every `as Statistics`/`as TechnicalObservation` alias and change only aliases made unnecessary by clarified names. 1. Rename the domain `RuntimeObservation` (currently Unknown/Unavailable/Verified) to the M19 canonical knowledge name, such as `ApplyStatus`; retain runtime::RuntimeObservation for measured interface data. Decide whether tui::AppliedState is a necessary presentation mapping or an exact redundant copy. Remove only redundant representation; do not merge raw statistics and verification status. Inventory every `as Statistics`/`as TechnicalObservation` alias and change only aliases made unnecessary by clarified names.
2. Replace ApplicationExecutor.prepared_destination's six-element tuple with named fields for identity, QR choice, delivery mode, retry attempt, generation and PreparedDestination. Replace restore_preview's tuple similarly. Preserve secret ownership, clear-on-unrelated-command behavior, stale-generation refusal, overwrite confirmation and cancellation. Avoid adding Clone/Debug to secret-bearing types. 2. Replace ApplicationExecutor.prepared_destination's six-element tuple with named fields for identity, QR choice, delivery mode, retry attempt, generation and PreparedDestination. Replace restore_preview's tuple similarly. Preserve secret ownership, clear-on-unrelated-command behavior, stale-generation refusal, overwrite confirmation and cancellation. Avoid adding Clone/Debug to secret-bearing types.
@@ -3947,11 +3947,13 @@ Remove name ambiguity and opaque tuples without moving business rules. Planned o
#### Acceptance and validation #### Acceptance and validation
- [ ] M20-A1: present apply knowledge and measured observation have distinct canonical names; no accidental meaning change in UI labels. - [x] M20-A1: present apply knowledge and measured observation have distinct canonical names; UI labels retain their meaning.
- [ ] M20-A2: prepared contexts use named fields; generation/cancel/confirmation behavior and secret redaction remain equivalent. - [x] M20-A2: prepared contexts use named fields; generation/cancel/confirmation behavior and secret redaction remain equivalent.
- [ ] M20-A3: actual consumers compile, dependencies/formats untouched; focused worker/state tests and RUST pass after independent review. - [x] M20-A3: actual consumers compile, dependencies/formats untouched; focused worker/state tests and final RUST gates passed after independent review.
Focused starting commands: `cargo test --locked domain::`, `cargo test --locked runtime::`, `cargo test --locked tui::events::`, `cargo test --locked tui::state::`; adjust filters only to actual moved modules and report executed tests. Evidence: docs/evidence/milestone-20.md. Outcome: not accepted. Handoff: M21 only after acceptance. Review and repair: Fresh independent source review found no behavior findings. M20-R1 (P2, `src/app.rs` test and `src/container_driver.rs` test-only path) is closed: repairer replaced two `clone_on_copy` calls with dereferences; independent re-review and affected tests/Clippy passed.
Evidence/handoff: [M20 evidence](docs/evidence/milestone-20.md) records the clean base, dirty source patch/artifact fingerprints, focused tests, R1 closure, exact final checks and limits. Final coordinator commands `cargo fmt --all -- --check`, `cargo clippy --locked --all-targets -- -D warnings`, `cargo test --locked` (391 passed, 3 ignored plus 2 doctests) and `cargo build --locked --release` all exited 0; `git diff --check` passed. Outcome: accepted for host/synthetic scope; stop after M20. Next action only on a separate request is M21.1; no fresh Alpine/VM result is claimed.
### Milestone 21 — Domain ownership of reservations and apply knowledge ### Milestone 21 — Domain ownership of reservations and apply knowledge
@@ -4157,7 +4159,7 @@ Evidence: docs/evidence/milestone-27.md. Outcome: not accepted. Handoff: stop af
## Current progress and resume point ## Current progress and resume point
- M19 is accepted (2026-10-29T22:24:30+02:00): glossary and source-referenced migration contract passed independent R1–R6 closure and integrated DOC checks; [M19 evidence](docs/evidence/milestone-19.md) records inputs and limits. Stop after M19; M20–M27 remain pending. M18 remains accepted; M9 remains retired and real-system limitations remain unchanged. - M20 is accepted (2026-10-29T22:40:28+02:00): renamed apply knowledge, named prepared contexts, independent M20-R1 closure and final RUST gates passed; [M20 evidence](docs/evidence/milestone-20.md) records source/artifact identity and limits. Stop after M20; M21–M27 remain pending. M19 remains accepted; M9 remains retired/unaccepted and its real-system limitations remain unchanged.
- M18 was accepted on 2026-10-29: structured, accurate in-app Help, responsive wrapping and bounded scrolling passed independent R1/R2 closure, synthetic/PTY checks and pinned Rust completion gates. [M18 evidence](docs/evidence/milestone-18.md) records final inputs and limits. At that handoff, M19 had not started. - M18 was accepted on 2026-10-29: structured, accurate in-app Help, responsive wrapping and bounded scrolling passed independent R1/R2 closure, synthetic/PTY checks and pinned Rust completion gates. [M18 evidence](docs/evidence/milestone-18.md) records final inputs and limits. At that handoff, M19 had not started.
@@ -4186,7 +4188,7 @@ Evidence: docs/evidence/milestone-27.md. Outcome: not accepted. Handoff: stop af
- Milestone 8 delivered reviewed opt-in Alpine Testcontainers coverage for real IPv4 WireGuard peers, application lifecycle, saved/runtime separation, restoration, import/reissue, backup failures and the real Rustix reader. Host gates and three fresh real-container runs passed on one stable source fingerprint; M8-A1–A10 accepted. Real IPv6 behavior is unverified by user-authorized scope. - Milestone 8 delivered reviewed opt-in Alpine Testcontainers coverage for real IPv4 WireGuard peers, application lifecycle, saved/runtime separation, restoration, import/reissue, backup failures and the real Rustix reader. Host gates and three fresh real-container runs passed on one stable source fingerprint; M8-A1–A10 accepted. Real IPv6 behavior is unverified by user-authorized scope.
- Milestone 9 is retired and unaccepted: the reviewed disposable VM preparation and guarded first start did not establish Debian/systemd acceptance. The exact VM was powered off after the user stopped work. M9.2–M9.3 were not started; M9-A1–A9 are unchecked. - Milestone 9 is retired and unaccepted: the reviewed disposable VM preparation and guarded first start did not establish Debian/systemd acceptance. The exact VM was powered off after the user stopped work. M9.2–M9.3 were not started; M9-A1–A9 are unchecked.
- M18 handoff remains complete. M19 is accepted; the next implementation action, when separately requested, is M20.1. Keep M16 validation limitations separate from TUI evidence. Do not access production, commit, deploy or resume VM work. - M18/M19 handoffs remain complete. M20 is accepted; the next implementation action, when separately requested, is M21.1. Keep M16 validation limitations separate from TUI evidence. Do not access production, commit, deploy or resume VM work.
## Plan maintenance ## Plan maintenance
+80
View File
@@ -0,0 +1,80 @@
# Milestone 20 evidence: vocabulary and prepared contexts
Recorded 2026-10-29T22:38:53+02:00. Base commit
`ede45d224a9ad255a919900e556a03d7e575430f` had a clean working tree.
The final tree is dirty only for this milestone's `PLANS.md`, Rust source and
test changes, and this evidence report. No `Cargo.toml` or `Cargo.lock` diff,
new dependency, persisted-format change, production configuration, or secret
fixture was introduced. The final Rust source/test patch fingerprint is
SHA256 `cba97207f926f1927c70643754db2743603e0f72892115ef8d959696d769992a`,
produced by:
```sh
git diff -- src/app.rs src/domain.rs src/persistence.rs src/container_driver.rs src/tui/events.rs src/tui/render.rs src/tui/state.rs src/tui/terminal.rs tests/client_delivery.rs tests/commit_restore.rs tests/runtime_restart.rs tests/tui_workflow.rs | sha256sum
```
The release artifact `target/release/wireguard-admin` built from those inputs
has SHA256 `7a6ee46b1d9c39bac9a7b3627b0b8ba30e70321126ebd16b1948b42b8658ac98`.
`file target/release/wireguard-admin` identified an x86-64 dynamically linked
GNU/Linux PIE with interpreter `/lib64/ld-linux-x86-64.so.2`.
## Implementation and acceptance mapping
- M20.1 renamed the domain's current knowledge enum to `ApplyStatus`, retained
`runtime::RuntimeObservation` for measured interface data, and migrated actual
application, persistence, TUI and integration callers. The exact duplicate
TUI `AppliedState` enum/mapping and now unnecessary `Statistics`,
`TechnicalObservation` and `ObservedRuntime` aliases were removed. UI labels
and current-versus-historical state transitions were preserved (M20-A1).
- M20.2 replaced the six-value prepared delivery tuple and three-value restore
tuple with private named contexts. A private `DeliveryMode` distinguishes
first issue and reissue without changing public `reissue` flags. Neither
context derives `Clone` or `Debug`; identity, attempt, generation, overwrite,
cancellation, unrelated-command clearing, password ownership and QR/file
order remain unchanged. A behavioral test covers cancel/unrelated-command
clearing (M20-A2).
- All affected production and test callers compile; no manifests, formats or
transport behavior changed. Focused tests, final full RUST gates and
independent review passed (M20-A3).
## Independent review and repair
A GPT-6-Luna/high implementer completed M20.1 and M20.2 sequentially. A
separate fresh GPT-6-Luna/high read-only reviewer inspected the integrated
diff, production paths, callers, tests and secret-bearing contexts and found
no behavior issue. The coordinator's first Clippy gate found M20-R1 (P2): two
test/test-only `.clone()` calls on the now-`Copy` status. A separate
GPT-6-Luna/high repairer changed only those two calls to dereferences in
`src/app.rs` and `src/container_driver.rs`. The reviewer independently
rechecked the repair and affected tests, closed R1 and found no new issue.
No actionable finding remains.
## Checks and limits
Environment at execution: `rustc`/Cargo 1.97.1 on
`x86_64-unknown-linux-gnu`, rustfmt 1.9.0 and Clippy 0.1.97, matching the
pinned compiler. Focused implementer/reviewer runs covered `domain::` (13),
`runtime::` (25), `tui::state::` (7), `tui::events::` (7),
`--test commit_restore` (9), `--test runtime_restart` (3),
`--test tui_workflow` (8), and `--test client_delivery` (5). After R1, the
repairer ran `cargo test --locked app::` (58 passed) and
`cargo test --locked --lib container_driver::` (23 passed, 2 ignored). The
reviewer reran affected app/event tests. These are host synthetic/PTY checks;
ignored Alpine selectors were not run.
| Final coordinator command | Actual result |
| --- | --- |
| `cargo fmt --all -- --check` | exit 0 |
| `cargo clippy --locked --all-targets -- -D warnings` | exit 0 after M20-R1 repair |
| `cargo test --locked` | exit 0: 391 unit/integration tests passed, 3 ignored; 2 doctests passed; 0 failed |
| `cargo build --locked --release` | exit 0; artifact SHA256 above |
| `git diff --check` | exit 0 |
The initial Clippy run exited 101 on M20-R1 before repair and is not the final
gate result. Default tests did not execute the three explicitly ignored
Alpine/selected-daemon tests. No Alpine Testcontainers or disposable systemd VM
run was required for this naming/context refactor or performed. Prior Alpine
results are historical for earlier source and do not validate this artifact;
real systemd/D-Bus and IPv6 limitations remain as previously recorded. No
production, live VPN, host service or host network operation was performed.
+43 -53
View File
@@ -19,15 +19,12 @@ use crate::delivery::files::{self, FileOutputError, PreparedDestination};
use crate::delivery::qr; use crate::delivery::qr;
use crate::delivery::{self, ClientConfigPayload, DeliverySettings, QrCodeOutput, QrError}; use crate::delivery::{self, ClientConfigPayload, DeliverySettings, QrCodeOutput, QrError};
use crate::domain::{ use crate::domain::{
AddressAvailability, Client, ClientAddresses, ClientStates, DomainError, PublicKey, AddressAvailability, ApplyStatus, Client, ClientAddresses, ClientStates, DomainError,
RuntimeObservation, Snapshot, allocate_ipv4, allocate_ipv6, validate_manual_ipv4, PublicKey, Snapshot, allocate_ipv4, allocate_ipv6, validate_manual_ipv4, validate_manual_ipv6,
validate_manual_ipv6,
}; };
use crate::persistence::{BackupComplete, PersistedState, PersistenceError, SavedStore}; use crate::persistence::{BackupComplete, PersistedState, PersistenceError, SavedStore};
use crate::runtime::transport::TransportError; use crate::runtime::transport::TransportError;
use crate::runtime::{ use crate::runtime::{self, RuntimeObservation, SavedPeerProjection, TechnicalMatch};
self, RuntimeObservation as TechnicalObservation, SavedPeerProjection, TechnicalMatch,
};
use crate::service::{self, RestartDeadline, RestartExecution, RestartOutcome}; use crate::service::{self, RestartDeadline, RestartExecution, RestartOutcome};
use crate::vault::source::{ use crate::vault::source::{
PreparedImportSetup, SelectedFileImportReport, SourceImportError, SourceImportReport, PreparedImportSetup, SelectedFileImportReport, SourceImportError, SourceImportReport,
@@ -504,7 +501,7 @@ impl Application {
let states = ClientStates::new( let states = ClientStates::new(
snapshot_from_document(&document), snapshot_from_document(&document),
None, None,
RuntimeObservation::Unknown, ApplyStatus::Unknown,
)?; )?;
Self::open_saved(store, saved, states) Self::open_saved(store, saved, states)
} }
@@ -984,7 +981,7 @@ impl Application {
let Ok((projection, _)) = restart_projection_from_document(document) else { let Ok((projection, _)) = restart_projection_from_document(document) else {
return false; return false;
}; };
self.states.applied() == &RuntimeObservation::Verified self.states.applied() == &ApplyStatus::Verified
&& self.verified_saved_projection.as_ref() == Some(&projection) && self.verified_saved_projection.as_ref() == Some(&projection)
} }
@@ -1487,7 +1484,7 @@ impl Application {
preview: RestartPreview, preview: RestartPreview,
choice: RestartChoice, choice: RestartChoice,
service: impl FnOnce(&str, RestartDeadline) -> RestartExecution, service: impl FnOnce(&str, RestartDeadline) -> RestartExecution,
runtime_read: impl FnOnce(&str, Duration) -> Result<TechnicalObservation, TransportError>, runtime_read: impl FnOnce(&str, Duration) -> Result<RuntimeObservation, TransportError>,
install: impl FnOnce(&SavedStore, &SavedBytes, BackupComplete) -> Result<(), PersistenceError>, install: impl FnOnce(&SavedStore, &SavedBytes, BackupComplete) -> Result<(), PersistenceError>,
) -> RestartReport { ) -> RestartReport {
self.restart_with_adapters_and_clock( self.restart_with_adapters_and_clock(
@@ -1532,7 +1529,7 @@ impl Application {
preview: RestartPreview, preview: RestartPreview,
choice: RestartChoice, choice: RestartChoice,
service: impl FnOnce(&str, RestartDeadline) -> RestartExecution, service: impl FnOnce(&str, RestartDeadline) -> RestartExecution,
runtime_read: impl FnOnce(&str, Duration) -> Result<TechnicalObservation, TransportError>, runtime_read: impl FnOnce(&str, Duration) -> Result<RuntimeObservation, TransportError>,
install: impl FnOnce(&SavedStore, &SavedBytes, BackupComplete) -> Result<(), PersistenceError>, install: impl FnOnce(&SavedStore, &SavedBytes, BackupComplete) -> Result<(), PersistenceError>,
now: impl Fn() -> Instant, now: impl Fn() -> Instant,
) -> RestartReport { ) -> RestartReport {
@@ -1860,7 +1857,7 @@ impl Application {
let mut proposed = preview.saved_bytes; let mut proposed = preview.saved_bytes;
let document = ConfigDocument::parse(&proposed.server_config)?; let document = ConfigDocument::parse(&proposed.server_config)?;
let snapshot = snapshot_from_document(&document); let snapshot = snapshot_from_document(&document);
let mut restored_states = ClientStates::new(snapshot, None, RuntimeObservation::Unknown)?; let mut restored_states = ClientStates::new(snapshot, None, ApplyStatus::Unknown)?;
let mut archived_reservations = BTreeSet::new(); let mut archived_reservations = BTreeSet::new();
let mut reservations = self.states.reserved_addresses().clone(); let mut reservations = self.states.reserved_addresses().clone();
// A restore can remove clients that are currently saved even when their // A restore can remove clients that are currently saved even when their
@@ -1947,7 +1944,7 @@ impl Application {
.map(VaultBundle::from_toml) .map(VaultBundle::from_toml)
.transpose()?; .transpose()?;
let snapshot = snapshot_from_document(&document); let snapshot = snapshot_from_document(&document);
let mut states = ClientStates::new(snapshot, None, RuntimeObservation::Unknown)?; let mut states = ClientStates::new(snapshot, None, ApplyStatus::Unknown)?;
if let Some(bytes) = &preview.saved_bytes.internal_state { if let Some(bytes) = &preview.saved_bytes.internal_state {
PersistedState::from_toml(bytes)?.apply_to(&mut states)?; PersistedState::from_toml(bytes)?.apply_to(&mut states)?;
} }
@@ -2508,16 +2505,14 @@ mod tests {
use crate::backup::{BackupReason, HistoricalApplyStatus, create_archive, preview_archive}; use crate::backup::{BackupReason, HistoricalApplyStatus, create_archive, preview_archive};
use crate::config::SettingsFile; use crate::config::SettingsFile;
use crate::delivery::qr::QrError; use crate::delivery::qr::QrError;
use crate::domain::{ use crate::domain::{ApplyStatus, Client, ClientAddresses, ClientStates, PublicKey, Snapshot};
Client, ClientAddresses, ClientStates, PublicKey, RuntimeObservation, Snapshot,
};
use crate::persistence::{ use crate::persistence::{
BackupComplete, InstallCheckpoint, PersistedState, PersistenceError, SavedStore, BackupComplete, InstallCheckpoint, PersistedState, PersistenceError, SavedStore,
}; };
use crate::runtime::transport::TransportError; use crate::runtime::transport::TransportError;
use crate::runtime::{ use crate::runtime::{
InterfaceObservation, Observed, PeerSetCompleteness, RuntimeObservation as ObservedRuntime, InterfaceObservation, Observed, PeerSetCompleteness, RuntimeObservation, RuntimePeer,
RuntimePeer, RuntimePeerSet, RuntimePeerSet,
}; };
use crate::service::{RestartExecution, RestartFailure, RestartOutcome}; use crate::service::{RestartExecution, RestartFailure, RestartOutcome};
use crate::vault::{VaultBundle, WireGuardPrivateKey}; use crate::vault::{VaultBundle, WireGuardPrivateKey};
@@ -2586,12 +2581,8 @@ restart_timeout_seconds = 30
) )
.expect("state client valid"); .expect("state client valid");
let snapshot = Snapshot::new([saved_client]).expect("snapshot valid"); let snapshot = Snapshot::new([saved_client]).expect("snapshot valid");
let states = ClientStates::new( let states = ClientStates::new(snapshot.clone(), Some(snapshot), ApplyStatus::Verified)
snapshot.clone(), .expect("initial applied state valid");
Some(snapshot),
RuntimeObservation::Verified,
)
.expect("initial applied state valid");
let store = SavedStore::new(root, resolved).expect("saved store mapping valid"); let store = SavedStore::new(root, resolved).expect("saved store mapping valid");
(store, states, key, secret) (store, states, key, secret)
} }
@@ -2637,7 +2628,7 @@ restart_timeout_seconds = 30
app app
} }
fn observed_runtime(app: &Application) -> ObservedRuntime { fn observed_runtime(app: &Application) -> RuntimeObservation {
let records = app.document().peers().iter().map(|peer| { let records = app.document().peers().iter().map(|peer| {
let identity = peer.identity.expect("synthetic peer identity"); let identity = peer.identity.expect("synthetic peer identity");
( (
@@ -2650,7 +2641,7 @@ restart_timeout_seconds = 30
}, },
) )
}); });
ObservedRuntime { RuntimeObservation {
observed_at: None, observed_at: None,
interface: InterfaceObservation::Present { interface: InterfaceObservation::Present {
name: "wg0".to_owned(), name: "wg0".to_owned(),
@@ -3590,7 +3581,7 @@ restart_timeout_seconds = 30
String::from_utf8_lossy(app.document().render()) String::from_utf8_lossy(app.document().render())
.contains("# wg-admin:alias=bmV3IGFsaWFz") .contains("# wg-admin:alias=bmV3IGFsaWFz")
); );
assert_eq!(app.states().applied(), &RuntimeObservation::Unknown); assert_eq!(app.states().applied(), &ApplyStatus::Unknown);
assert_eq!( assert_eq!(
app.states().saved().get(&key).expect("saved peer").alias, app.states().saved().get(&key).expect("saved peer").alias,
"legacy-view" "legacy-view"
@@ -3681,7 +3672,7 @@ restart_timeout_seconds = 30
app.states().saved().get(&key).expect("saved peer").alias, app.states().saved().get(&key).expect("saved peer").alias,
"new alias" "new alias"
); );
assert_eq!(app.states().applied(), &RuntimeObservation::Unknown); assert_eq!(app.states().applied(), &ApplyStatus::Unknown);
assert!(app.states().applied_snapshot().is_some()); assert!(app.states().applied_snapshot().is_some());
let backup = preview_archive(&archive).expect("pre-commit backup parses"); let backup = preview_archive(&archive).expect("pre-commit backup parses");
assert_eq!(backup.reason, BackupReason::PreCommit); assert_eq!(backup.reason, BackupReason::PreCommit);
@@ -3769,11 +3760,10 @@ restart_timeout_seconds = 30
assert!(state.reserved_addresses.contains(&"10.20.0.2".to_owned())); assert!(state.reserved_addresses.contains(&"10.20.0.2".to_owned()));
assert!(state.reserved_addresses.contains(&"fd20::2".to_owned())); assert!(state.reserved_addresses.contains(&"fd20::2".to_owned()));
let reopened_states = let reopened_states = ClientStates::new(Snapshot::default(), None, ApplyStatus::Unknown)
ClientStates::new(Snapshot::default(), None, RuntimeObservation::Unknown) .expect("empty supported snapshot is valid");
.expect("empty supported snapshot is valid");
let reopened = Application::open(store, reopened_states).expect("session reopens"); let reopened = Application::open(store, reopened_states).expect("session reopens");
assert_eq!(reopened.states().applied(), &RuntimeObservation::Unknown); assert_eq!(reopened.states().applied(), &ApplyStatus::Unknown);
assert!( assert!(
reopened reopened
.states() .states()
@@ -3912,7 +3902,7 @@ restart_timeout_seconds = 30
app.confirm_restore(preview) app.confirm_restore(preview)
.expect("restore uses previewed bytes"); .expect("restore uses previewed bytes");
assert!(!String::from_utf8_lossy(app.document().render()).contains("draft alias")); assert!(!String::from_utf8_lossy(app.document().render()).contains("draft alias"));
assert_eq!(app.states().applied(), &RuntimeObservation::Unknown); assert_eq!(app.states().applied(), &ApplyStatus::Unknown);
assert!(app.vault().is_some()); assert!(app.vault().is_some());
assert!(app.states().saved().get(&key).is_some()); assert!(app.states().saved().get(&key).is_some());
} }
@@ -3943,7 +3933,7 @@ restart_timeout_seconds = 30
let before_restore = store.read_saved().expect("current saved bytes"); let before_restore = store.read_saved().expect("current saved bytes");
app.rename_client(key, "draft survives failed restore") app.rename_client(key, "draft survives failed restore")
.expect("stage draft"); .expect("stage draft");
let applied_before = app.states().applied().clone(); let applied_before = *app.states().applied();
let preview = app let preview = app
.preview_restore(selected.file_name().unwrap().to_str().unwrap()) .preview_restore(selected.file_name().unwrap().to_str().unwrap())
.expect("selected archive preview"); .expect("selected archive preview");
@@ -4106,7 +4096,7 @@ restart_timeout_seconds = 30
.expect("restored reservation state parses"); .expect("restored reservation state parses");
assert!(state.reserved_addresses.contains(&"fd20::2".to_owned())); assert!(state.reserved_addresses.contains(&"fd20::2".to_owned()));
assert!(app.vault().is_none()); assert!(app.vault().is_none());
assert_eq!(app.states().applied(), &RuntimeObservation::Unknown); assert_eq!(app.states().applied(), &ApplyStatus::Unknown);
} }
#[test] #[test]
@@ -4167,7 +4157,7 @@ restart_timeout_seconds = 30
.iter() .iter()
.all(|item| item.historical_apply == HistoricalApplyStatus::Unknown) .all(|item| item.historical_apply == HistoricalApplyStatus::Unknown)
); );
assert_eq!(app.states().applied(), &RuntimeObservation::Unknown); assert_eq!(app.states().applied(), &ApplyStatus::Unknown);
} }
#[test] #[test]
@@ -4284,7 +4274,7 @@ restart_timeout_seconds = 30
assert!(report.backup_path.is_some()); assert!(report.backup_path.is_some());
assert!(report.restart_submitted); assert!(report.restart_submitted);
assert_eq!(*events.borrow(), vec!["service", "runtime", "persist"]); assert_eq!(*events.borrow(), vec!["service", "runtime", "persist"]);
assert_eq!(app.states().applied(), &RuntimeObservation::Verified); assert_eq!(app.states().applied(), &ApplyStatus::Verified);
assert_eq!( assert_eq!(
app.states().draft().get(&key).unwrap().alias, app.states().draft().get(&key).unwrap().alias,
"restart fixture" "restart fixture"
@@ -4352,7 +4342,7 @@ restart_timeout_seconds = 30
); );
assert_eq!(report.stage, RestartStage::Complete); assert_eq!(report.stage, RestartStage::Complete);
assert_eq!(app.states().applied(), &RuntimeObservation::Verified); assert_eq!(app.states().applied(), &ApplyStatus::Verified);
assert!(app.states().applied_snapshot().is_none()); assert!(app.states().applied_snapshot().is_none());
assert!(app.states().reserved_addresses().is_empty()); assert!(app.states().reserved_addresses().is_empty());
assert_eq!(app.saved_bytes().server_config, original_config); assert_eq!(app.saved_bytes().server_config, original_config);
@@ -4364,7 +4354,7 @@ restart_timeout_seconds = 30
let reopened = Application::open_from_saved_store(app.store.clone()) let reopened = Application::open_from_saved_store(app.store.clone())
.expect("reopen historically verified saved state"); .expect("reopen historically verified saved state");
assert_eq!(reopened.historical_apply, historical_apply); assert_eq!(reopened.historical_apply, historical_apply);
assert_eq!(reopened.states().applied(), &RuntimeObservation::Unknown); assert_eq!(reopened.states().applied(), &ApplyStatus::Unknown);
assert!(reopened.states().applied_snapshot().is_none()); assert!(reopened.states().applied_snapshot().is_none());
assert!(reopened.states().reserved_addresses().is_empty()); assert!(reopened.states().reserved_addresses().is_empty());
assert!( assert!(
@@ -4471,12 +4461,12 @@ restart_timeout_seconds = 30
assert_eq!(report.stage, RestartStage::Verification); assert_eq!(report.stage, RestartStage::Verification);
assert_eq!(report.failure, Some(RestartFailureKind::RuntimeMismatch)); assert_eq!(report.failure, Some(RestartFailureKind::RuntimeMismatch));
assert_eq!(app.states().applied(), &RuntimeObservation::Unknown); assert_eq!(app.states().applied(), &ApplyStatus::Unknown);
assert_eq!(app.states().reserved_addresses(), &reserved_before); assert_eq!(app.states().reserved_addresses(), &reserved_before);
let reopened = Application::open_from_saved_store(app.store.clone()) let reopened = Application::open_from_saved_store(app.store.clone())
.expect("failed verification leaves saved reservations durable"); .expect("failed verification leaves saved reservations durable");
assert_eq!(reopened.states().reserved_addresses(), &reserved_before); assert_eq!(reopened.states().reserved_addresses(), &reserved_before);
assert_eq!(reopened.states().applied(), &RuntimeObservation::Unknown); assert_eq!(reopened.states().applied(), &ApplyStatus::Unknown);
} }
#[test] #[test]
@@ -4590,7 +4580,7 @@ restart_timeout_seconds = 30
assert!(!report.restart_may_still_complete); assert!(!report.restart_may_still_complete);
assert!(report.manual_recovery_required); assert!(report.manual_recovery_required);
assert!(!runtime_called.get()); assert!(!runtime_called.get());
assert_eq!(app.states().applied(), &RuntimeObservation::Unknown); assert_eq!(app.states().applied(), &ApplyStatus::Unknown);
assert!(!app.states().reserved_addresses().is_empty()); assert!(!app.states().reserved_addresses().is_empty());
} }
@@ -4641,7 +4631,7 @@ restart_timeout_seconds = 30
assert!(!report.restart_may_still_complete); assert!(!report.restart_may_still_complete);
assert!(report.manual_recovery_required); assert!(report.manual_recovery_required);
assert!(!install_called.get()); assert!(!install_called.get());
assert_eq!(app.states().applied(), &RuntimeObservation::Unknown); assert_eq!(app.states().applied(), &ApplyStatus::Unknown);
assert!(!app.states().reserved_addresses().is_empty()); assert!(!app.states().reserved_addresses().is_empty());
} }
@@ -4697,7 +4687,7 @@ restart_timeout_seconds = 30
assert!(!report.restart_may_still_complete); assert!(!report.restart_may_still_complete);
assert!(report.manual_recovery_required); assert!(report.manual_recovery_required);
assert!(!install_called.get()); assert!(!install_called.get());
assert_eq!(app.states().applied(), &RuntimeObservation::Unknown); assert_eq!(app.states().applied(), &ApplyStatus::Unknown);
} }
} }
@@ -4844,7 +4834,7 @@ restart_timeout_seconds = 30
| RestartFailure::JobResultUnknown | RestartFailure::JobResultUnknown
) )
); );
assert_eq!(app.states().applied(), &RuntimeObservation::Unknown); assert_eq!(app.states().applied(), &ApplyStatus::Unknown);
} }
} }
@@ -4865,7 +4855,7 @@ restart_timeout_seconds = 30
phase: crate::service::RestartPhase::WaitForJob, phase: crate::service::RestartPhase::WaitForJob,
}, },
|_, _| { |_, _| {
Ok(ObservedRuntime { Ok(RuntimeObservation {
observed_at: None, observed_at: None,
interface: InterfaceObservation::Missing, interface: InterfaceObservation::Missing,
}) })
@@ -4890,7 +4880,7 @@ restart_timeout_seconds = 30
); );
assert_eq!(failed_write.stage, RestartStage::Persistence); assert_eq!(failed_write.stage, RestartStage::Persistence);
assert!(failed_write.manual_recovery_required); assert!(failed_write.manual_recovery_required);
assert_eq!(app.states().applied(), &RuntimeObservation::Unknown); assert_eq!(app.states().applied(), &ApplyStatus::Unknown);
assert!(!app.states().reserved_addresses().is_empty()); assert!(!app.states().reserved_addresses().is_empty());
} }
@@ -4983,7 +4973,7 @@ restart_timeout_seconds = 30
verified_at_utc: verified_at.to_owned() verified_at_utc: verified_at.to_owned()
} }
); );
assert_eq!(reopened.states().applied(), &RuntimeObservation::Unknown); assert_eq!(reopened.states().applied(), &ApplyStatus::Unknown);
} }
#[test] #[test]
@@ -5021,7 +5011,7 @@ restart_timeout_seconds = 30
Err(AppError::Persistence(PersistenceError::RecoveryRequired)) Err(AppError::Persistence(PersistenceError::RecoveryRequired))
)); ));
assert_eq!(app.historical_apply, HistoricalApplyStatus::Unknown); assert_eq!(app.historical_apply, HistoricalApplyStatus::Unknown);
assert_eq!(app.states().applied(), &RuntimeObservation::Unknown); assert_eq!(app.states().applied(), &ApplyStatus::Unknown);
assert_eq!(app.saved_bytes().internal_state, before.internal_state); assert_eq!(app.saved_bytes().internal_state, before.internal_state);
let resolved = SettingsFile::parse_toml(SETTINGS) let resolved = SettingsFile::parse_toml(SETTINGS)
@@ -5036,7 +5026,7 @@ restart_timeout_seconds = 30
assert_eq!(recovered.internal_state, before.internal_state); assert_eq!(recovered.internal_state, before.internal_state);
let reopened = Application::open(reopened_store, states).expect("application reopens"); let reopened = Application::open(reopened_store, states).expect("application reopens");
assert_eq!(reopened.historical_apply, HistoricalApplyStatus::Unknown); assert_eq!(reopened.historical_apply, HistoricalApplyStatus::Unknown);
assert_eq!(reopened.states().applied(), &RuntimeObservation::Unknown); assert_eq!(reopened.states().applied(), &ApplyStatus::Unknown);
} }
#[test] #[test]
@@ -5074,7 +5064,7 @@ restart_timeout_seconds = 30
); );
let reopened = Application::open(store, states).expect("committed state reopens"); let reopened = Application::open(store, states).expect("committed state reopens");
assert_eq!(reopened.historical_apply, HistoricalApplyStatus::Unknown); assert_eq!(reopened.historical_apply, HistoricalApplyStatus::Unknown);
assert_eq!(reopened.states().applied(), &RuntimeObservation::Unknown); assert_eq!(reopened.states().applied(), &ApplyStatus::Unknown);
} }
#[test] #[test]
@@ -5127,7 +5117,7 @@ restart_timeout_seconds = 30
let pre_restore = app.confirm_restore(preview).expect("restore succeeds"); let pre_restore = app.confirm_restore(preview).expect("restore succeeds");
assert_eq!(app.historical_apply, HistoricalApplyStatus::Unknown); assert_eq!(app.historical_apply, HistoricalApplyStatus::Unknown);
assert_eq!(app.states().applied(), &RuntimeObservation::Unknown); assert_eq!(app.states().applied(), &ApplyStatus::Unknown);
let archived = preview_archive(&pre_restore).expect("pre-restore backup validates"); let archived = preview_archive(&pre_restore).expect("pre-restore backup validates");
assert_eq!( assert_eq!(
archived.historical_apply, archived.historical_apply,
@@ -5146,11 +5136,11 @@ restart_timeout_seconds = 30
let reopened_states = ClientStates::new( let reopened_states = ClientStates::new(
snapshot_from_document(&restored_document), snapshot_from_document(&restored_document),
None, None,
RuntimeObservation::Unknown, ApplyStatus::Unknown,
) )
.unwrap(); .unwrap();
let reopened = Application::open(store, reopened_states).expect("restored state reopens"); let reopened = Application::open(store, reopened_states).expect("restored state reopens");
assert_eq!(reopened.historical_apply, HistoricalApplyStatus::Unknown); assert_eq!(reopened.historical_apply, HistoricalApplyStatus::Unknown);
assert_eq!(reopened.states().applied(), &RuntimeObservation::Unknown); assert_eq!(reopened.states().applied(), &ApplyStatus::Unknown);
} }
} }
+1 -1
View File
@@ -1047,7 +1047,7 @@ fn run_failure_prevents_application_scenario(
let files_before_restart = protected_saved_file_snapshot(fixture_root)?; let files_before_restart = protected_saved_file_snapshot(fixture_root)?;
let reservations_before_restart = app.states().reserved_addresses().clone(); let reservations_before_restart = app.states().reserved_addresses().clone();
let history_before_restart = saved_last_verified_timestamp(app.saved_bytes())?; let history_before_restart = saved_last_verified_timestamp(app.saved_bytes())?;
let applied_before_restart = app.states().applied().clone(); let applied_before_restart = *app.states().applied();
let applied_snapshot_before_restart = app.states().applied_snapshot().cloned(); let applied_snapshot_before_restart = app.states().applied_snapshot().cloned();
let runtime_before_restart = observe_fixture_peer_set()?; let runtime_before_restart = observe_fixture_peer_set()?;
let apply_called = std::cell::Cell::new(false); let apply_called = std::cell::Cell::new(false);
+32 -36
View File
@@ -146,9 +146,9 @@ impl Snapshot {
} }
} }
/// Runtime knowledge is not inferred from saved state or missing data. /// Current apply knowledge is not inferred from saved state or missing data.
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RuntimeObservation { pub enum ApplyStatus {
Unknown, Unknown,
Unavailable, Unavailable,
Verified, Verified,
@@ -160,7 +160,7 @@ pub struct ClientStates {
draft: Snapshot, draft: Snapshot,
saved: Snapshot, saved: Snapshot,
applied_snapshot: Option<Snapshot>, applied_snapshot: Option<Snapshot>,
applied: RuntimeObservation, applied: ApplyStatus,
reserved_addresses: BTreeSet<IpAddr>, reserved_addresses: BTreeSet<IpAddr>,
} }
@@ -169,9 +169,9 @@ impl ClientStates {
pub fn new( pub fn new(
saved: Snapshot, saved: Snapshot,
applied_snapshot: Option<Snapshot>, applied_snapshot: Option<Snapshot>,
applied: RuntimeObservation, applied: ApplyStatus,
) -> Result<Self, DomainError> { ) -> Result<Self, DomainError> {
if applied == RuntimeObservation::Verified && applied_snapshot.as_ref() != Some(&saved) { if applied == ApplyStatus::Verified && applied_snapshot.as_ref() != Some(&saved) {
return Err(DomainError::AppliedSnapshotMismatch); return Err(DomainError::AppliedSnapshotMismatch);
} }
let saved_addresses = saved.addresses().collect::<BTreeSet<_>>(); let saved_addresses = saved.addresses().collect::<BTreeSet<_>>();
@@ -199,7 +199,7 @@ impl ClientStates {
&self.saved &self.saved
} }
pub fn applied(&self) -> &RuntimeObservation { pub fn applied(&self) -> &ApplyStatus {
&self.applied &self.applied
} }
@@ -226,7 +226,7 @@ impl ClientStates {
} }
self.reserved_addresses = reservations; self.reserved_addresses = reservations;
self.applied_snapshot = None; self.applied_snapshot = None;
self.applied = RuntimeObservation::Unknown; self.applied = ApplyStatus::Unknown;
Ok(()) Ok(())
} }
@@ -243,10 +243,10 @@ impl ClientStates {
.filter(|address| !next_addresses.contains(address)), .filter(|address| !next_addresses.contains(address)),
); );
self.saved = self.draft.clone(); self.saved = self.draft.clone();
if self.applied == RuntimeObservation::Verified if self.applied == ApplyStatus::Verified
&& self.applied_snapshot.as_ref() != Some(&self.saved) && self.applied_snapshot.as_ref() != Some(&self.saved)
{ {
self.applied = RuntimeObservation::Unknown; self.applied = ApplyStatus::Unknown;
} }
} }
@@ -261,11 +261,11 @@ impl ClientStates {
observed: Snapshot, observed: Snapshot,
) -> Result<(), DomainError> { ) -> Result<(), DomainError> {
if observed != self.saved { if observed != self.saved {
self.applied = RuntimeObservation::Unknown; self.applied = ApplyStatus::Unknown;
return Err(DomainError::AppliedSnapshotMismatch); return Err(DomainError::AppliedSnapshotMismatch);
} }
self.applied_snapshot = Some(observed); self.applied_snapshot = Some(observed);
self.applied = RuntimeObservation::Verified; self.applied = ApplyStatus::Verified;
self.reserved_addresses.clear(); self.reserved_addresses.clear();
Ok(()) Ok(())
} }
@@ -282,17 +282,17 @@ impl ClientStates {
return self.mark_applied_after_verification(observed); return self.mark_applied_after_verification(observed);
} }
self.applied_snapshot = None; self.applied_snapshot = None;
self.applied = RuntimeObservation::Verified; self.applied = ApplyStatus::Verified;
self.reserved_addresses.clear(); self.reserved_addresses.clear();
Ok(()) Ok(())
} }
pub fn mark_runtime_unavailable(&mut self) { pub fn mark_runtime_unavailable(&mut self) {
self.applied = RuntimeObservation::Unavailable; self.applied = ApplyStatus::Unavailable;
} }
pub fn mark_runtime_unknown(&mut self) { pub fn mark_runtime_unknown(&mut self) {
self.applied = RuntimeObservation::Unknown; self.applied = ApplyStatus::Unknown;
} }
/// Addresses in draft, saved, or last verified applied data cannot be reused. /// Addresses in draft, saved, or last verified applied data cannot be reused.
@@ -558,12 +558,12 @@ mod tests {
} }
#[test] #[test]
fn unknown_and_unavailable_runtime_observations_are_explicit() { fn unknown_and_unavailable_apply_statuses_are_explicit() {
let saved = Snapshot::new([client(1, "one", "10.0.0.2", None)]).unwrap(); let saved = Snapshot::new([client(1, "one", "10.0.0.2", None)]).unwrap();
let unknown = ClientStates::new(saved.clone(), None, RuntimeObservation::Unknown).unwrap(); let unknown = ClientStates::new(saved.clone(), None, ApplyStatus::Unknown).unwrap();
assert_eq!(unknown.applied(), &RuntimeObservation::Unknown); assert_eq!(unknown.applied(), &ApplyStatus::Unknown);
let unavailable = ClientStates::new(saved, None, RuntimeObservation::Unavailable).unwrap(); let unavailable = ClientStates::new(saved, None, ApplyStatus::Unavailable).unwrap();
assert_eq!(unavailable.applied(), &RuntimeObservation::Unavailable); assert_eq!(unavailable.applied(), &ApplyStatus::Unavailable);
} }
#[test] #[test]
@@ -572,16 +572,16 @@ mod tests {
let other = Snapshot::new([client(2, "two", "10.0.0.3", None)]).unwrap(); let other = Snapshot::new([client(2, "two", "10.0.0.3", None)]).unwrap();
assert_eq!( assert_eq!(
ClientStates::new(saved.clone(), None, RuntimeObservation::Verified).err(), ClientStates::new(saved.clone(), None, ApplyStatus::Verified).err(),
Some(DomainError::AppliedSnapshotMismatch) Some(DomainError::AppliedSnapshotMismatch)
); );
assert_eq!( assert_eq!(
ClientStates::new(saved.clone(), Some(other), RuntimeObservation::Verified).err(), ClientStates::new(saved.clone(), Some(other), ApplyStatus::Verified).err(),
Some(DomainError::AppliedSnapshotMismatch) Some(DomainError::AppliedSnapshotMismatch)
); );
let verified = let verified =
ClientStates::new(saved.clone(), Some(saved), RuntimeObservation::Verified).unwrap(); ClientStates::new(saved.clone(), Some(saved), ApplyStatus::Verified).unwrap();
assert_eq!(verified.applied(), &RuntimeObservation::Verified); assert_eq!(verified.applied(), &ApplyStatus::Verified);
} }
#[test] #[test]
@@ -591,12 +591,8 @@ mod tests {
client(2, "two", "10.0.0.3", None), client(2, "two", "10.0.0.3", None),
]) ])
.unwrap(); .unwrap();
let mut states = ClientStates::new( let mut states =
saved.clone(), ClientStates::new(saved.clone(), Some(saved.clone()), ApplyStatus::Verified).unwrap();
Some(saved.clone()),
RuntimeObservation::Verified,
)
.unwrap();
states.draft_mut().remove(&key(2)); states.draft_mut().remove(&key(2));
states.discard_draft(); states.discard_draft();
assert_eq!(states.draft(), &saved); assert_eq!(states.draft(), &saved);
@@ -604,10 +600,10 @@ mod tests {
states.draft_mut().remove(&key(2)); states.draft_mut().remove(&key(2));
states.commit_draft(); states.commit_draft();
assert_eq!(states.saved().clients().count(), 1); assert_eq!(states.saved().clients().count(), 1);
assert_eq!(states.applied(), &RuntimeObservation::Unknown); assert_eq!(states.applied(), &ApplyStatus::Unknown);
assert_eq!(states.applied_snapshot(), Some(&saved)); assert_eq!(states.applied_snapshot(), Some(&saved));
states.mark_runtime_unavailable(); states.mark_runtime_unavailable();
assert_eq!(states.applied(), &RuntimeObservation::Unavailable); assert_eq!(states.applied(), &ApplyStatus::Unavailable);
assert!(states.applied_snapshot().is_some()); assert!(states.applied_snapshot().is_some());
assert!( assert!(
states states
@@ -628,7 +624,7 @@ mod tests {
#[test] #[test]
fn a_draft_only_deletion_does_not_create_a_committed_reservation() { fn a_draft_only_deletion_does_not_create_a_committed_reservation() {
let saved = Snapshot::new([client(1, "one", "10.0.0.2", None)]).unwrap(); let saved = Snapshot::new([client(1, "one", "10.0.0.2", None)]).unwrap();
let mut states = ClientStates::new(saved, None, RuntimeObservation::Unknown).unwrap(); let mut states = ClientStates::new(saved, None, ApplyStatus::Unknown).unwrap();
states.draft_mut().remove(&key(1)); states.draft_mut().remove(&key(1));
assert!(states.reserved_addresses().is_empty()); assert!(states.reserved_addresses().is_empty());
assert!( assert!(
@@ -644,7 +640,7 @@ mod tests {
let mut states = ClientStates::new( let mut states = ClientStates::new(
initial.clone(), initial.clone(),
Some(initial.clone()), Some(initial.clone()),
RuntimeObservation::Verified, ApplyStatus::Verified,
) )
.unwrap(); .unwrap();
let wrong = Snapshot::new([client(2, "other", "10.0.0.3", None)]).unwrap(); let wrong = Snapshot::new([client(2, "other", "10.0.0.3", None)]).unwrap();
@@ -652,7 +648,7 @@ mod tests {
states.mark_applied_after_verification(wrong), states.mark_applied_after_verification(wrong),
Err(DomainError::AppliedSnapshotMismatch) Err(DomainError::AppliedSnapshotMismatch)
); );
assert_eq!(states.applied(), &RuntimeObservation::Unknown); assert_eq!(states.applied(), &ApplyStatus::Unknown);
assert_eq!(states.applied_snapshot(), Some(&initial)); assert_eq!(states.applied_snapshot(), Some(&initial));
states.draft_mut().remove(&key(1)); states.draft_mut().remove(&key(1));
@@ -662,7 +658,7 @@ mod tests {
states states
.mark_applied_after_verification(saved.clone()) .mark_applied_after_verification(saved.clone())
.unwrap(); .unwrap();
assert_eq!(states.applied(), &RuntimeObservation::Verified); assert_eq!(states.applied(), &ApplyStatus::Verified);
assert!(states.reserved_addresses().is_empty()); assert!(states.reserved_addresses().is_empty());
assert!( assert!(
!states !states
+4 -10
View File
@@ -953,22 +953,16 @@ restart_timeout_seconds = 30
) )
.unwrap(); .unwrap();
let saved = crate::domain::Snapshot::new([client]).unwrap(); let saved = crate::domain::Snapshot::new([client]).unwrap();
let mut states = crate::domain::ClientStates::new( let mut states =
saved, crate::domain::ClientStates::new(saved, None, crate::domain::ApplyStatus::Unknown)
None, .unwrap();
crate::domain::RuntimeObservation::Unknown,
)
.unwrap();
let state = PersistedState::new( let state = PersistedState::new(
["10.0.0.8".to_owned()], ["10.0.0.8".to_owned()],
Some("2026-10-19T12:30:00Z".to_owned()), Some("2026-10-19T12:30:00Z".to_owned()),
) )
.unwrap(); .unwrap();
state.apply_to(&mut states).unwrap(); state.apply_to(&mut states).unwrap();
assert_eq!( assert_eq!(states.applied(), &crate::domain::ApplyStatus::Unknown);
states.applied(),
&crate::domain::RuntimeObservation::Unknown
);
assert!( assert!(
states states
.reserved_addresses() .reserved_addresses()
+107 -44
View File
@@ -287,17 +287,30 @@ pub trait OperationExecutor: Send + 'static {
} }
} }
enum DeliveryMode {
FirstIssue,
Reissue,
}
struct PreparedDestinationContext {
identity: PublicKey,
include_qr: bool,
mode: DeliveryMode,
attempt: FirstIssueAttempt,
generation: u64,
destination: PreparedDestination,
}
struct RestorePreviewContext {
_archive_name: String,
generation: u64,
preview: RestorePreview,
}
#[derive(Default)] #[derive(Default)]
pub struct ApplicationExecutor { pub struct ApplicationExecutor {
prepared_destination: Option<( prepared_destination: Option<PreparedDestinationContext>,
PublicKey, restore_preview: Option<RestorePreviewContext>,
bool,
bool,
FirstIssueAttempt,
u64,
PreparedDestination,
)>,
restore_preview: Option<(String, u64, RestorePreview)>,
} }
impl OperationExecutor for ApplicationExecutor { impl OperationExecutor for ApplicationExecutor {
@@ -379,14 +392,18 @@ impl OperationExecutor for ApplicationExecutor {
attempt, attempt,
prepared_generation: generation, prepared_generation: generation,
}; };
self.prepared_destination = Some(( self.prepared_destination = Some(PreparedDestinationContext {
identity, identity,
include_qr, include_qr,
reissue, mode: if reissue {
DeliveryMode::Reissue
} else {
DeliveryMode::FirstIssue
},
attempt, attempt,
generation, generation,
destination, destination,
)); });
CommandResult::DeliveryDestination(summary) CommandResult::DeliveryDestination(summary)
}), }),
UiCommand::DeliverFirstIssue { UiCommand::DeliverFirstIssue {
@@ -394,23 +411,16 @@ impl OperationExecutor for ApplicationExecutor {
overwrite_confirmed, overwrite_confirmed,
prepared_generation, prepared_generation,
} => { } => {
let ( let prepared = self
identity,
include_qr,
reissue,
prepared_attempt,
destination_generation,
mut destination,
) = self
.prepared_destination .prepared_destination
.take() .take()
.ok_or_else(|| OperationError { .ok_or_else(|| OperationError {
message: "No prepared output destination is available.".to_owned(), message: "No prepared output destination is available.".to_owned(),
})?; })?;
if reissue if !matches!(prepared.mode, DeliveryMode::FirstIssue)
|| prepared_attempt != attempt || prepared.attempt != attempt
|| !destination_generation_matches( || !destination_generation_matches(
destination_generation, prepared.generation,
prepared_generation, prepared_generation,
generation, generation,
) )
@@ -420,6 +430,9 @@ impl OperationExecutor for ApplicationExecutor {
.to_owned(), .to_owned(),
}); });
} }
let identity = prepared.identity;
let include_qr = prepared.include_qr;
let mut destination = prepared.destination;
if destination.overwrite_confirmation_required() { if destination.overwrite_confirmation_required() {
if !overwrite_confirmed { if !overwrite_confirmed {
return Err(OperationError { return Err(OperationError {
@@ -519,7 +532,11 @@ impl OperationExecutor for ApplicationExecutor {
changes, changes,
preview_generation: generation, preview_generation: generation,
}; };
self.restore_preview = Some((file_name, generation, preview)); self.restore_preview = Some(RestorePreviewContext {
_archive_name: file_name,
generation,
preview,
});
CommandResult::RestorePreview(summary) CommandResult::RestorePreview(summary)
}) })
} }
@@ -532,14 +549,12 @@ impl OperationExecutor for ApplicationExecutor {
Ok(CommandResult::Cancelled) Ok(CommandResult::Cancelled)
} }
UiCommand::ConfirmRestore { preview_generation } => { UiCommand::ConfirmRestore { preview_generation } => {
let (_, stored_generation, preview) = let context = self.restore_preview.take().ok_or_else(|| OperationError {
self.restore_preview.take().ok_or_else(|| OperationError { message: "Restore preview is no longer available; select the archive again."
message: .to_owned(),
"Restore preview is no longer available; select the archive again." })?;
.to_owned(),
})?;
if !restore_preview_generation_matches( if !restore_preview_generation_matches(
stored_generation, context.generation,
preview_generation, preview_generation,
generation, generation,
) { ) {
@@ -547,7 +562,8 @@ impl OperationExecutor for ApplicationExecutor {
message: "Restore preview is stale; select the archive again.".to_owned(), message: "Restore preview is stale; select the archive again.".to_owned(),
}); });
} }
app.confirm_restore(preview).map(CommandResult::Restored) app.confirm_restore(context.preview)
.map(CommandResult::Restored)
} }
}; };
result.map_err(|error| OperationError { result.map_err(|error| OperationError {
@@ -573,28 +589,23 @@ impl OperationExecutor for ApplicationExecutor {
overwrite_confirmed, overwrite_confirmed,
prepared_generation, prepared_generation,
} => { } => {
let ( let prepared = self
prepared_identity,
_include_qr,
reissue,
_attempt,
destination_generation,
mut destination,
) = self
.prepared_destination .prepared_destination
.take() .take()
.ok_or_else(|| OperationError { .ok_or_else(|| OperationError {
message: "No prepared output destination is available.".to_owned(), message: "No prepared output destination is available.".to_owned(),
})?; })?;
if prepared_identity != identity if prepared.identity != identity
|| !reissue || !matches!(prepared.mode, DeliveryMode::Reissue)
|| destination_generation != prepared_generation || prepared.generation != prepared_generation
{ {
return Err(OperationError { return Err(OperationError {
message: "Prepared output destination is stale; select it again." message: "Prepared output destination is stale; select it again."
.to_owned(), .to_owned(),
}); });
} }
let include_qr = prepared.include_qr;
let mut destination = prepared.destination;
if destination.overwrite_confirmation_required() { if destination.overwrite_confirmation_required() {
if !overwrite_confirmed { if !overwrite_confirmed {
return Err(OperationError { return Err(OperationError {
@@ -604,7 +615,7 @@ impl OperationExecutor for ApplicationExecutor {
destination = destination.confirm_overwrite().map_err(operation_error)?; destination = destination.confirm_overwrite().map_err(operation_error)?;
} }
let delivery = app let delivery = app
.reissue_client_file(identity, password.expose(), destination, _include_qr) .reissue_client_file(identity, password.expose(), destination, include_qr)
.map_err(|error| OperationError { .map_err(|error| OperationError {
message: error.to_string(), message: error.to_string(),
})?; })?;
@@ -1088,6 +1099,58 @@ mod tests {
assert!(!restore_preview_generation_matches(8, 8, 8)); assert!(!restore_preview_generation_matches(8, 8, 8));
} }
#[test]
fn prepared_delivery_is_cleared_by_cancel_and_unrelated_commands() {
let (_root, mut app) = application();
let identity = PublicKey::from_bytes([44; 32]);
let mut executor = ApplicationExecutor::default();
let path = _root.path().join("prepared.conf");
let prepare = || UiCommand::PrepareDelivery {
identity,
path: path.clone(),
include_qr: false,
reissue: false,
attempt: FirstIssueAttempt::Initial,
};
let deliver = || UiCommand::DeliverFirstIssue {
attempt: FirstIssueAttempt::Initial,
overwrite_confirmed: false,
prepared_generation: 7,
};
assert!(matches!(
executor.execute_at(&mut app, prepare(), 7),
Ok(CommandResult::DeliveryDestination(_))
));
assert!(matches!(
executor.execute_at(&mut app, UiCommand::CancelPreparedDelivery, 7),
Ok(CommandResult::Cancelled)
));
assert_eq!(
executor
.execute_at(&mut app, deliver(), 7)
.unwrap_err()
.message,
"No prepared output destination is available."
);
assert!(matches!(
executor.execute_at(&mut app, prepare(), 7),
Ok(CommandResult::DeliveryDestination(_))
));
assert!(matches!(
executor.execute_at(&mut app, UiCommand::ListBackups, 7),
Ok(CommandResult::Backups(_))
));
assert_eq!(
executor
.execute_at(&mut app, deliver(), 7)
.unwrap_err()
.message,
"No prepared output destination is available."
);
}
struct FakeStatistics { struct FakeStatistics {
started: Option<mpsc::SyncSender<()>>, started: Option<mpsc::SyncSender<()>>,
release: Option<Receiver<()>>, release: Option<Receiver<()>>,
+20 -20
View File
@@ -654,9 +654,9 @@ fn qr_state_summary(state: &UiState, width: usize) -> Option<String> {
.filter(|row| row.saved.is_some()) .filter(|row| row.saved.is_some())
.count(), .count(),
match view.applied { match view.applied {
super::state::AppliedState::Verified => "verified", crate::domain::ApplyStatus::Verified => "verified",
super::state::AppliedState::Unknown => "not verified", crate::domain::ApplyStatus::Unknown => "not verified",
super::state::AppliedState::Unavailable => "observation unavailable", crate::domain::ApplyStatus::Unavailable => "observation unavailable",
} }
)) ))
} }
@@ -1210,9 +1210,9 @@ fn persistent_state_line(view: &SessionView) -> String {
.filter(|row| row.saved.is_some()) .filter(|row| row.saved.is_some())
.count(); .count();
let applied = match view.applied { let applied = match view.applied {
super::state::AppliedState::Verified => "verified", crate::domain::ApplyStatus::Verified => "verified",
super::state::AppliedState::Unknown => "not verified", crate::domain::ApplyStatus::Unknown => "not verified",
super::state::AppliedState::Unavailable => "unavailable", crate::domain::ApplyStatus::Unavailable => "unavailable",
}; };
format!( format!(
"Draft: {draft}; Saved peers: {saved_peers} / managed {saved_managed}; Applied(saved): {applied}" "Draft: {draft}; Saved peers: {saved_peers} / managed {saved_managed}; Applied(saved): {applied}"
@@ -1797,16 +1797,16 @@ fn saved_status(view: &SessionView) -> String {
fn applied_status(view: &SessionView) -> String { fn applied_status(view: &SessionView) -> String {
match view.applied { match view.applied {
super::state::AppliedState::Verified if view.has_draft => { crate::domain::ApplyStatus::Verified if view.has_draft => {
"Applied: verified for saved configuration (draft excluded)".to_owned() "Applied: verified for saved configuration (draft excluded)".to_owned()
} }
super::state::AppliedState::Verified => { crate::domain::ApplyStatus::Verified => {
"Applied: verified for saved configuration".to_owned() "Applied: verified for saved configuration".to_owned()
} }
super::state::AppliedState::Unknown => { crate::domain::ApplyStatus::Unknown => {
"Applied: not verified for saved configuration".to_owned() "Applied: not verified for saved configuration".to_owned()
} }
super::state::AppliedState::Unavailable => "Applied: observation unavailable".to_owned(), crate::domain::ApplyStatus::Unavailable => "Applied: observation unavailable".to_owned(),
} }
} }
@@ -2329,7 +2329,7 @@ mod tests {
client_states: BTreeMap::new(), client_states: BTreeMap::new(),
saved_peer_count: Some(0), saved_peer_count: Some(0),
document_issues: Vec::new(), document_issues: Vec::new(),
applied: super::super::state::AppliedState::Unknown, applied: crate::domain::ApplyStatus::Unknown,
reserved_addresses: Default::default(), reserved_addresses: Default::default(),
statistics: RuntimeObservation::unknown(), statistics: RuntimeObservation::unknown(),
has_draft: true, has_draft: true,
@@ -2545,7 +2545,7 @@ mod tests {
)]), )]),
saved_peer_count: Some(1), saved_peer_count: Some(1),
document_issues: Vec::new(), document_issues: Vec::new(),
applied: super::super::state::AppliedState::Unknown, applied: crate::domain::ApplyStatus::Unknown,
reserved_addresses: Default::default(), reserved_addresses: Default::default(),
statistics: RuntimeObservation::unknown(), statistics: RuntimeObservation::unknown(),
has_draft: false, has_draft: false,
@@ -2558,13 +2558,13 @@ mod tests {
let identity = PublicKey::from_bytes([61; 32]); let identity = PublicKey::from_bytes([61; 32]);
let mut view = saved_client_view(identity); let mut view = saved_client_view(identity);
view.has_draft = false; view.has_draft = false;
view.applied = super::super::state::AppliedState::Verified; view.applied = crate::domain::ApplyStatus::Verified;
let summary = state_summary(&view); let summary = state_summary(&view);
assert!(summary.contains("Draft: no changes")); assert!(summary.contains("Draft: no changes"));
assert!(summary.contains("Saved: 1 peer sections; 1 managed clients")); assert!(summary.contains("Saved: 1 peer sections; 1 managed clients"));
assert!(summary.contains("Applied: verified for saved configuration")); assert!(summary.contains("Applied: verified for saved configuration"));
view.applied = super::super::state::AppliedState::Unknown; view.applied = crate::domain::ApplyStatus::Unknown;
assert_eq!( assert_eq!(
applied_status(&view), applied_status(&view),
"Applied: not verified for saved configuration" "Applied: not verified for saved configuration"
@@ -2572,7 +2572,7 @@ mod tests {
assert!(!state_summary(&view).contains("restart pending")); assert!(!state_summary(&view).contains("restart pending"));
view.has_draft = true; view.has_draft = true;
view.applied = super::super::state::AppliedState::Verified; view.applied = crate::domain::ApplyStatus::Verified;
assert!( assert!(
draft_status(&view) draft_status(&view)
.contains("Draft: pending; draft peers: 1; managed-client changes: 0") .contains("Draft: pending; draft peers: 1; managed-client changes: 0")
@@ -2619,13 +2619,13 @@ mod tests {
); );
assert!(draft_status(&view).contains("managed-client changes: 2")); assert!(draft_status(&view).contains("managed-client changes: 2"));
view.applied = super::super::state::AppliedState::Unknown; view.applied = crate::domain::ApplyStatus::Unknown;
assert_eq!( assert_eq!(
applied_status(&view), applied_status(&view),
"Applied: not verified for saved configuration" "Applied: not verified for saved configuration"
); );
assert!(!state_summary(&view).contains("restart pending")); assert!(!state_summary(&view).contains("restart pending"));
view.applied = super::super::state::AppliedState::Unavailable; view.applied = crate::domain::ApplyStatus::Unavailable;
assert_eq!(applied_status(&view), "Applied: observation unavailable"); assert_eq!(applied_status(&view), "Applied: observation unavailable");
let mut state = UiState { let mut state = UiState {
@@ -2637,7 +2637,7 @@ mod tests {
assert_eq!(state_summary(state.view.as_ref().unwrap()), before); assert_eq!(state_summary(state.view.as_ref().unwrap()), before);
assert_eq!( assert_eq!(
state.view.as_ref().unwrap().applied, state.view.as_ref().unwrap().applied,
super::super::state::AppliedState::Unavailable crate::domain::ApplyStatus::Unavailable
); );
} }
@@ -3932,7 +3932,7 @@ mod tests {
)]), )]),
saved_peer_count: Some(1), saved_peer_count: Some(1),
document_issues: Vec::new(), document_issues: Vec::new(),
applied: super::super::state::AppliedState::Unknown, applied: crate::domain::ApplyStatus::Unknown,
reserved_addresses: Default::default(), reserved_addresses: Default::default(),
statistics: RuntimeObservation::unknown(), statistics: RuntimeObservation::unknown(),
has_draft: false, has_draft: false,
@@ -4055,7 +4055,7 @@ mod tests {
]), ]),
saved_peer_count: Some(2), saved_peer_count: Some(2),
document_issues: vec![], document_issues: vec![],
applied: super::super::state::AppliedState::Unknown, applied: crate::domain::ApplyStatus::Unknown,
reserved_addresses: Default::default(), reserved_addresses: Default::default(),
statistics: RuntimeObservation::unknown(), statistics: RuntimeObservation::unknown(),
has_draft: true, has_draft: true,
+12 -24
View File
@@ -4,8 +4,8 @@ use std::collections::BTreeSet;
use std::net::IpAddr; use std::net::IpAddr;
use crate::app::{Application, ClientDisplay}; use crate::app::{Application, ClientDisplay};
use crate::domain::{Client, PublicKey, RuntimeObservation}; use crate::domain::{ApplyStatus, Client, PublicKey};
use crate::runtime::RuntimeObservation as Statistics; use crate::runtime::RuntimeObservation;
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Screen { pub enum Screen {
@@ -34,13 +34,6 @@ pub enum DialogKind {
Exit, Exit,
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AppliedState {
Unknown,
Unavailable,
Verified,
}
/// Public client values for both states. No configuration bytes or secrets are /// Public client values for both states. No configuration bytes or secrets are
/// retained in a view snapshot. /// retained in a view snapshot.
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@@ -58,9 +51,9 @@ pub struct SessionView {
/// saved bytes can be projected. This includes nonmanaged peers. /// saved bytes can be projected. This includes nonmanaged peers.
pub saved_peer_count: Option<usize>, pub saved_peer_count: Option<usize>,
pub document_issues: Vec<crate::wireguard::DocumentIssue>, pub document_issues: Vec<crate::wireguard::DocumentIssue>,
pub applied: AppliedState, pub applied: ApplyStatus,
pub reserved_addresses: BTreeSet<IpAddr>, pub reserved_addresses: BTreeSet<IpAddr>,
pub statistics: Statistics, pub statistics: RuntimeObservation,
pub has_draft: bool, pub has_draft: bool,
pub vault_initialized: bool, pub vault_initialized: bool,
} }
@@ -83,11 +76,6 @@ impl SessionView {
) )
}) })
.collect(); .collect();
let applied = match states.applied() {
RuntimeObservation::Unknown => AppliedState::Unknown,
RuntimeObservation::Unavailable => AppliedState::Unavailable,
RuntimeObservation::Verified => AppliedState::Verified,
};
let projection = app.display_projection(); let projection = app.display_projection();
let saved_peer_count = let saved_peer_count =
crate::wireguard::ConfigDocument::parse(&app.saved_bytes().server_config) crate::wireguard::ConfigDocument::parse(&app.saved_bytes().server_config)
@@ -99,9 +87,9 @@ impl SessionView {
client_states, client_states,
saved_peer_count, saved_peer_count,
document_issues: projection.document_issues, document_issues: projection.document_issues,
applied, applied: *states.applied(),
reserved_addresses: states.reserved_addresses().clone(), reserved_addresses: states.reserved_addresses().clone(),
statistics: Statistics::unknown(), statistics: RuntimeObservation::unknown(),
has_draft: app.has_draft_changes(), has_draft: app.has_draft_changes(),
vault_initialized: app.vault_initialized(), vault_initialized: app.vault_initialized(),
} }
@@ -318,7 +306,7 @@ impl UiState {
/// Apply only statistics sampled from the still-current saved/draft view. /// Apply only statistics sampled from the still-current saved/draft view.
/// This never changes the applied-state marker. /// This never changes the applied-state marker.
pub fn apply_statistics(&mut self, generation: u64, statistics: Statistics) -> bool { pub fn apply_statistics(&mut self, generation: u64, statistics: RuntimeObservation) -> bool {
if generation != self.generation { if generation != self.generation {
return false; return false;
} }
@@ -521,9 +509,9 @@ mod tests {
client_states: Default::default(), client_states: Default::default(),
saved_peer_count: None, saved_peer_count: None,
document_issues: Vec::new(), document_issues: Vec::new(),
applied: AppliedState::Unknown, applied: ApplyStatus::Unknown,
reserved_addresses: BTreeSet::new(), reserved_addresses: BTreeSet::new(),
statistics: Statistics::unknown(), statistics: RuntimeObservation::unknown(),
has_draft: false, has_draft: false,
vault_initialized: true, vault_initialized: true,
} }
@@ -669,9 +657,9 @@ mod tests {
let key = PublicKey::from_bytes([4; 32]); let key = PublicKey::from_bytes([4; 32]);
let mut state = UiState::default(); let mut state = UiState::default();
assert!(state.apply_view(view(0, vec![row(key, "client", true)]))); assert!(state.apply_view(view(0, vec![row(key, "client", true)])));
assert!(state.apply_statistics(0, Statistics::unknown())); assert!(state.apply_statistics(0, RuntimeObservation::unknown()));
assert_eq!(state.view.as_ref().unwrap().applied, AppliedState::Unknown); assert_eq!(state.view.as_ref().unwrap().applied, ApplyStatus::Unknown);
state.generation = 1; state.generation = 1;
assert!(!state.apply_statistics(0, Statistics::unknown())); assert!(!state.apply_statistics(0, RuntimeObservation::unknown()));
} }
} }
+8 -7
View File
@@ -960,9 +960,10 @@ mod tests {
use ratatui::backend::TestBackend; use ratatui::backend::TestBackend;
use crate::app::ClientDisplay; use crate::app::ClientDisplay;
use crate::domain::ApplyStatus;
use crate::runtime::RuntimeObservation; use crate::runtime::RuntimeObservation;
use crate::tui::events::OperationError; use crate::tui::events::OperationError;
use crate::tui::state::{AppliedState, OperationKind, SessionView}; use crate::tui::state::{OperationKind, SessionView};
use crate::wireguard::PeerIssue; use crate::wireguard::PeerIssue;
struct FakeWorker { struct FakeWorker {
@@ -1032,7 +1033,7 @@ mod tests {
client_states: Default::default(), client_states: Default::default(),
saved_peer_count: Some(0), saved_peer_count: Some(0),
document_issues: Vec::new(), document_issues: Vec::new(),
applied: AppliedState::Unknown, applied: ApplyStatus::Unknown,
reserved_addresses: Default::default(), reserved_addresses: Default::default(),
statistics: RuntimeObservation::unknown(), statistics: RuntimeObservation::unknown(),
has_draft: false, has_draft: false,
@@ -1163,7 +1164,7 @@ mod tests {
let identity = crate::domain::PublicKey::from_bytes([1; 32]); let identity = crate::domain::PublicKey::from_bytes([1; 32]);
let mut view = navigation_view(); let mut view = navigation_view();
view.has_draft = true; view.has_draft = true;
view.applied = AppliedState::Verified; view.applied = ApplyStatus::Verified;
let mut state = UiState { let mut state = UiState {
screen: Screen::Help, screen: Screen::Help,
selected_identity: Some(identity), selected_identity: Some(identity),
@@ -1192,7 +1193,7 @@ mod tests {
assert_eq!(state.selected_peer_index, selected_peer_index); assert_eq!(state.selected_peer_index, selected_peer_index);
assert_eq!(state.view, view_before); assert_eq!(state.view, view_before);
assert!(state.view.as_ref().unwrap().has_draft); assert!(state.view.as_ref().unwrap().has_draft);
assert_eq!(state.view.as_ref().unwrap().applied, AppliedState::Verified); assert_eq!(state.view.as_ref().unwrap().applied, ApplyStatus::Verified);
// The scrolling helper accepts no worker or command sink, so Help input // The scrolling helper accepts no worker or command sink, so Help input
// cannot submit work or mutate saved/applied application state. // cannot submit work or mutate saved/applied application state.
assert!(!handle_help_navigation_key( assert!(!handle_help_navigation_key(
@@ -1313,7 +1314,7 @@ mod tests {
document_issues: vec![crate::wireguard::DocumentIssue::MalformedSectionHeader { document_issues: vec![crate::wireguard::DocumentIssue::MalformedSectionHeader {
line: 1, line: 1,
}], }],
applied: AppliedState::Unknown, applied: ApplyStatus::Unknown,
reserved_addresses: Default::default(), reserved_addresses: Default::default(),
statistics: RuntimeObservation::unknown(), statistics: RuntimeObservation::unknown(),
has_draft: false, has_draft: false,
@@ -1344,7 +1345,7 @@ mod tests {
client_states: Default::default(), client_states: Default::default(),
saved_peer_count: Some(2), saved_peer_count: Some(2),
document_issues: Vec::new(), document_issues: Vec::new(),
applied: AppliedState::Unknown, applied: ApplyStatus::Unknown,
reserved_addresses: Default::default(), reserved_addresses: Default::default(),
statistics: RuntimeObservation::unknown(), statistics: RuntimeObservation::unknown(),
has_draft: false, has_draft: false,
@@ -1578,7 +1579,7 @@ mod tests {
client_states: Default::default(), client_states: Default::default(),
saved_peer_count: Some(0), saved_peer_count: Some(0),
document_issues: Vec::new(), document_issues: Vec::new(),
applied: AppliedState::Unknown, applied: ApplyStatus::Unknown,
reserved_addresses: Default::default(), reserved_addresses: Default::default(),
statistics: RuntimeObservation::unknown(), statistics: RuntimeObservation::unknown(),
has_draft: false, has_draft: false,
+3 -5
View File
@@ -7,9 +7,7 @@ use wireguard_admin::app::{
}; };
use wireguard_admin::config::{ResolvedSettings, SettingsFile}; use wireguard_admin::config::{ResolvedSettings, SettingsFile};
use wireguard_admin::delivery::files::FileOutputError; use wireguard_admin::delivery::files::FileOutputError;
use wireguard_admin::domain::{ use wireguard_admin::domain::{ApplyStatus, Client, ClientAddresses, ClientStates, Snapshot};
Client, ClientAddresses, ClientStates, RuntimeObservation, Snapshot,
};
use wireguard_admin::persistence::SavedStore; use wireguard_admin::persistence::SavedStore;
use wireguard_admin::vault::import::{parse_client_config, reconcile}; use wireguard_admin::vault::import::{parse_client_config, reconcile};
use wireguard_admin::vault::{VaultBundle, WireGuardPrivateKey}; use wireguard_admin::vault::{VaultBundle, WireGuardPrivateKey};
@@ -59,7 +57,7 @@ fn fixture(server: String, states: ClientStates) -> Fixture {
} }
fn empty_states() -> ClientStates { fn empty_states() -> ClientStates {
ClientStates::new(Snapshot::default(), None, RuntimeObservation::Unknown).expect("empty state") ClientStates::new(Snapshot::default(), None, ApplyStatus::Unknown).expect("empty state")
} }
fn open(fixture: &Fixture) -> Application { fn open(fixture: &Fixture) -> Application {
@@ -265,7 +263,7 @@ fn imported_ipv4_only_parameters_are_preserved_and_authenticated_conflict_blocks
let snapshot = Snapshot::new([client]).expect("snapshot"); let snapshot = Snapshot::new([client]).expect("snapshot");
let import_fixture = fixture( let import_fixture = fixture(
server.clone(), server.clone(),
ClientStates::new(snapshot, None, RuntimeObservation::Unknown).expect("client state"), ClientStates::new(snapshot, None, ApplyStatus::Unknown).expect("client state"),
); );
let document = ConfigDocument::parse(server.as_bytes()).expect("server document"); let document = ConfigDocument::parse(server.as_bytes()).expect("server document");
let psk = WireGuardPrivateKey::generate().expect("synthetic preshared key"); let psk = WireGuardPrivateKey::generate().expect("synthetic preshared key");
+6 -10
View File
@@ -6,7 +6,7 @@ use wireguard_admin::app::{AppError, Application};
use wireguard_admin::backup::{BackupReason, preview_archive, preview_archive_bytes}; use wireguard_admin::backup::{BackupReason, preview_archive, preview_archive_bytes};
use wireguard_admin::config::{ResolvedSettings, SettingsFile}; use wireguard_admin::config::{ResolvedSettings, SettingsFile};
use wireguard_admin::domain::{ use wireguard_admin::domain::{
Client, ClientAddresses, ClientStates, PublicKey, RuntimeObservation, Snapshot, ApplyStatus, Client, ClientAddresses, ClientStates, PublicKey, Snapshot,
}; };
use wireguard_admin::persistence::{PersistedState, SavedStore}; use wireguard_admin::persistence::{PersistedState, SavedStore};
use wireguard_admin::vault::{VaultBundle, WireGuardPrivateKey}; use wireguard_admin::vault::{VaultBundle, WireGuardPrivateKey};
@@ -75,12 +75,8 @@ fn fixture(with_client: bool) -> Fixture {
) )
.expect("server fixture is private"); .expect("server fixture is private");
let snapshot = Snapshot::new(clients).expect("initial snapshot"); let snapshot = Snapshot::new(clients).expect("initial snapshot");
let states = ClientStates::new( let states = ClientStates::new(snapshot.clone(), Some(snapshot), ApplyStatus::Verified)
snapshot.clone(), .expect("initial states");
Some(snapshot),
RuntimeObservation::Verified,
)
.expect("initial states");
let store = SavedStore::new(root.path(), settings.clone()).expect("saved store"); let store = SavedStore::new(root.path(), settings.clone()).expect("saved store");
Fixture { Fixture {
root, root,
@@ -240,7 +236,7 @@ fn restore_reserves_addresses_of_current_saved_clients_that_the_archive_removes(
.expect("new client"); .expect("new client");
app.add_client(client, None).expect("stage client"); app.add_client(client, None).expect("stage client");
app.commit().expect("save client"); app.commit().expect("save client");
assert_eq!(app.states().applied(), &RuntimeObservation::Unknown); assert_eq!(app.states().applied(), &ApplyStatus::Unknown);
let preview = app let preview = app
.preview_restore(archive_name(&empty_archive)) .preview_restore(archive_name(&empty_archive))
@@ -264,7 +260,7 @@ fn restore_reserves_addresses_of_current_saved_clients_that_the_archive_removes(
.contains(&"fd20::9".to_owned()) .contains(&"fd20::9".to_owned())
); );
assert_eq!(app.states().applied(), &RuntimeObservation::Unknown); assert_eq!(app.states().applied(), &ApplyStatus::Unknown);
assert!( assert!(
app.states() app.states()
.reserved_addresses() .reserved_addresses()
@@ -520,5 +516,5 @@ fn deletion_uses_public_key_and_removes_only_that_vault_record() {
.expect("vault parses"); .expect("vault parses");
assert!(!bundle.contains_record(fixture.key)); assert!(!bundle.contains_record(fixture.key));
assert!(bundle.contains_record(retained_key)); assert!(bundle.contains_record(retained_key));
assert_eq!(app.states().applied(), &RuntimeObservation::Unknown); assert_eq!(app.states().applied(), &ApplyStatus::Unknown);
} }
+4 -6
View File
@@ -4,9 +4,7 @@ use std::path::Path;
use wireguard_admin::app::{Application, RestartChoice, RestartStage}; use wireguard_admin::app::{Application, RestartChoice, RestartStage};
use wireguard_admin::config::{ResolvedSettings, SettingsFile}; use wireguard_admin::config::{ResolvedSettings, SettingsFile};
use wireguard_admin::domain::{ use wireguard_admin::domain::{ApplyStatus, Client, ClientAddresses, ClientStates, Snapshot};
Client, ClientAddresses, ClientStates, RuntimeObservation, Snapshot,
};
use wireguard_admin::persistence::SavedStore; use wireguard_admin::persistence::SavedStore;
use wireguard_admin::vault::WireGuardPrivateKey; use wireguard_admin::vault::WireGuardPrivateKey;
@@ -67,7 +65,7 @@ fn fixture(service: &str, timeout: u64) -> Fixture {
} }
fn open(fixture: &Fixture) -> Application { fn open(fixture: &Fixture) -> Application {
let states = ClientStates::new(Snapshot::default(), None, RuntimeObservation::Unknown) let states = ClientStates::new(Snapshot::default(), None, ApplyStatus::Unknown)
.expect("empty saved states"); .expect("empty saved states");
Application::open(fixture.store.clone(), states).expect("open synthetic application") Application::open(fixture.store.clone(), states).expect("open synthetic application")
} }
@@ -121,7 +119,7 @@ fn public_restart_preview_and_cancel_preserve_draft_and_saved_files() {
.expect("no cancellation backup") .expect("no cancellation backup")
.is_empty() .is_empty()
); );
assert_eq!(app.states().applied(), &RuntimeObservation::Unknown); assert_eq!(app.states().applied(), &ApplyStatus::Unknown);
} }
#[test] #[test]
@@ -174,7 +172,7 @@ fn restore_changes_the_settings_used_by_the_next_restart_preview() {
app.saved_bytes().settings, app.saved_bytes().settings,
fixture.initial_settings.as_bytes() fixture.initial_settings.as_bytes()
); );
assert_eq!(app.states().applied(), &RuntimeObservation::Unknown); assert_eq!(app.states().applied(), &ApplyStatus::Unknown);
let restart = app let restart = app
.preview_restart() .preview_restart()
.expect("preview restored generation without invoking adapters"); .expect("preview restored generation without invoking adapters");
+10 -13
View File
@@ -13,9 +13,9 @@ use crossterm::event::KeyCode;
use wireguard_admin::app::{FirstIssueAttempt, RestartChoice, RestartStage}; use wireguard_admin::app::{FirstIssueAttempt, RestartChoice, RestartStage};
use wireguard_admin::backup::BackupReason; use wireguard_admin::backup::BackupReason;
use wireguard_admin::config::{ResolvedSettings, SettingsFile}; use wireguard_admin::config::{ResolvedSettings, SettingsFile};
use wireguard_admin::domain::{ClientStates, PublicKey, RuntimeObservation, Snapshot}; use wireguard_admin::domain::{ApplyStatus, ClientStates, PublicKey, Snapshot};
use wireguard_admin::persistence::SavedStore; use wireguard_admin::persistence::SavedStore;
use wireguard_admin::runtime::{InterfaceObservation, RuntimeObservation as Statistics}; use wireguard_admin::runtime::{InterfaceObservation, RuntimeObservation};
use wireguard_admin::tui::events::{ use wireguard_admin::tui::events::{
ApplicationWorker, CommandResult, FirstIssueStatus, QrStatus, SecretText, SensitiveCommand, ApplicationWorker, CommandResult, FirstIssueStatus, QrStatus, SecretText, SensitiveCommand,
ShutdownProgress, StatisticsReader, UiCommand, WorkerConfig, WorkerEvent, ShutdownProgress, StatisticsReader, UiCommand, WorkerConfig, WorkerEvent,
@@ -106,9 +106,9 @@ fn saved_peer_projection_counts_legacy_peers_without_inventing_managed_state() {
struct CountingStatistics(Arc<AtomicUsize>); struct CountingStatistics(Arc<AtomicUsize>);
impl StatisticsReader for CountingStatistics { impl StatisticsReader for CountingStatistics {
fn read(&mut self, _interface: &str, _timeout: Duration) -> Statistics { fn read(&mut self, _interface: &str, _timeout: Duration) -> RuntimeObservation {
self.0.fetch_add(1, Ordering::SeqCst); self.0.fetch_add(1, Ordering::SeqCst);
Statistics { RuntimeObservation {
observed_at: None, observed_at: None,
interface: InterfaceObservation::Unavailable, interface: InterfaceObservation::Unavailable,
} }
@@ -116,8 +116,8 @@ impl StatisticsReader for CountingStatistics {
} }
fn worker(fixture: &Fixture, reader: CountingStatistics) -> ApplicationWorker { fn worker(fixture: &Fixture, reader: CountingStatistics) -> ApplicationWorker {
let states = ClientStates::new(Snapshot::default(), None, RuntimeObservation::Unknown) let states =
.expect("empty states"); ClientStates::new(Snapshot::default(), None, ApplyStatus::Unknown).expect("empty states");
let app = wireguard_admin::app::Application::open(fixture.store.clone(), states) let app = wireguard_admin::app::Application::open(fixture.store.clone(), states)
.expect("application opens on synthetic files"); .expect("application opens on synthetic files");
ApplicationWorker::spawn( ApplicationWorker::spawn(
@@ -166,7 +166,7 @@ fn terminal_pty_smoke() {
const CHILD: &str = "WG_ADMIN_SYNTHETIC_PTY_CHILD"; const CHILD: &str = "WG_ADMIN_SYNTHETIC_PTY_CHILD";
if std::env::var_os(CHILD).is_some() { if std::env::var_os(CHILD).is_some() {
let fixture = fixture(); let fixture = fixture();
let states = ClientStates::new(Snapshot::default(), None, RuntimeObservation::Unknown) let states = ClientStates::new(Snapshot::default(), None, ApplyStatus::Unknown)
.expect("empty states"); .expect("empty states");
let app = wireguard_admin::app::Application::open(fixture.store, states) let app = wireguard_admin::app::Application::open(fixture.store, states)
.expect("synthetic application opens"); .expect("synthetic application opens");
@@ -813,10 +813,7 @@ fn worker_backup_restore_preview_cancel_and_confirm_are_separate_saved_operation
match wait_for(&mut worker, restore.request_id) { match wait_for(&mut worker, restore.request_id) {
WorkerEvent::OperationFinished { result, view, .. } => { WorkerEvent::OperationFinished { result, view, .. } => {
assert!(matches!(result.unwrap(), CommandResult::Restored(_))); assert!(matches!(result.unwrap(), CommandResult::Restored(_)));
assert_eq!( assert_eq!(view.applied, wireguard_admin::domain::ApplyStatus::Unknown);
view.applied,
wireguard_admin::tui::state::AppliedState::Unknown
);
assert!(!view.has_draft); assert!(!view.has_draft);
} }
_ => unreachable!(), _ => unreachable!(),
@@ -916,8 +913,8 @@ fn worker_commit_stops_at_mandatory_backup_failure_and_keeps_draft_and_saved_byt
#[test] #[test]
fn worker_restore_refreshes_operational_allocation_settings_before_next_create() { fn worker_restore_refreshes_operational_allocation_settings_before_next_create() {
let fixture = fixture(); let fixture = fixture();
let states = ClientStates::new(Snapshot::default(), None, RuntimeObservation::Unknown) let states =
.expect("empty states"); ClientStates::new(Snapshot::default(), None, ApplyStatus::Unknown).expect("empty states");
let mut original_app = wireguard_admin::app::Application::open(fixture.store.clone(), states) let mut original_app = wireguard_admin::app::Application::open(fixture.store.clone(), states)
.expect("original application opens"); .expect("original application opens");
original_app original_app