From af2d061880a8532314f57d4f1ec6eb5265520091 Mon Sep 17 00:00:00 2001 From: sasa Date: Tue, 27 Oct 2026 20:42:12 +0200 Subject: [PATCH] Fix timestamp formats conflict --- PLANS.md | 7 +- docs/evidence/tui-creation-timestamp.md | 47 +++++++++++ src/tui/forms.rs | 32 +++++-- tests/tui_workflow.rs | 106 ++++++++++++++++++++++++ 4 files changed, 181 insertions(+), 11 deletions(-) create mode 100644 docs/evidence/tui-creation-timestamp.md diff --git a/PLANS.md b/PLANS.md index 0571a2f..b6f3591 100644 --- a/PLANS.md +++ b/PLANS.md @@ -119,7 +119,9 @@ Each row is a delivery milestone, not a completion claim. Expand its record befo | 7 | Complete terminal management workflow | 6 | accepted | | 8 | Alpine Testcontainers integration | 7; suitable disposable Docker environment | accepted | | 9 | Real systemd acceptance and release readiness | 8; disposable systemd VM | retired | -| 10 | Visible styled keyboard quick reference in bottom panel | 8; existing M7 TUI | in_progress | +| 10 | Visible styled keyboard quick reference in bottom panel | 8; existing M7 TUI | accepted | +| 11 | Valid subnet routes shown as a capability limitation | 10 | accepted | +| 12 | Aligned, responsive client table | 11 | accepted | ### Milestone 1 — Rust foundation, settings, and domain states @@ -3544,6 +3546,7 @@ Stop after this milestone; do not resume VM work, commit or deploy. ## Current progress and resume point +- Bounded TUI maintenance TUI-TS-001 (2026-10-27): new-client form creation timestamps now have whole-second UTC precision before strict metadata rendering. A fixed fractional-second instant and a synthetic blank-IP form-to-draft workflow passed; saved bytes stayed unchanged. Independent review found no actionable issue, and pinned-toolchain Rust completion gates passed. [Timestamp repair evidence](docs/evidence/tui-creation-timestamp.md) identifies the source and release artifact. This repairs M7 behavior without claiming new Alpine or VM validation; earlier artifact identities remain historical after this Rust change. - Bounded TUI maintenance (2026-10-26): repaired list arrows opening client details. Up/Down now only move the highlight; Enter opens details and Esc returns. Initial navigation follows the renderer's first-row highlight. Independent review closed NAV-001 with no remaining scoped findings; validation is recorded in [navigation repair evidence](docs/evidence/tui-navigation.md). This does not advance M9 or supply VM/Alpine acceptance evidence; prior artifact identities remain historical after this Rust change. - Milestones 1–8 are accepted; milestone 9 is retired by user direction and unaccepted. Its real systemd/D-Bus checks remain unverified and are not a prerequisite to M10. - Milestone 10 is accepted: the styled keyboard quick reference passed independent review, focused synthetic/PTY checks and Rust completion gates. @@ -3561,7 +3564,7 @@ Stop after this milestone; do not resume VM work, commit or deploy. - 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. -- Next action: stop after M12 and await user direction. Do not restart the VM, access production, commit, or deploy. +- Next action: scoped timestamp repair is complete; await user direction. Do not restart the VM, access production, commit, or deploy. ## Plan maintenance diff --git a/docs/evidence/tui-creation-timestamp.md b/docs/evidence/tui-creation-timestamp.md new file mode 100644 index 0000000..b52c8ed --- /dev/null +++ b/docs/evidence/tui-creation-timestamp.md @@ -0,0 +1,47 @@ +# TUI new-client creation timestamp repair + +Validated on 2026-10-27 at 20:39 +02:00. Bounded maintenance task +TUI-TS-001 repairs the new-client form path from the accepted M7 workflow. +No production, Alpine, or VM execution was performed. + +## Behavior and review + +The form now clears nanoseconds on its UTC instant before RFC3339 formatting. +The metadata renderer and its exact `YYYY-MM-DDTHH:MM:SSZ` validation are +unchanged. A deterministic test supplies a fixed UTC instant with nonzero +nanoseconds, checks its exact whole-second output, and sends that output to +the metadata renderer. A synthetic workflow enters an alias and blank IPv4 and +IPv6 form fields, submits the resulting command through the application worker, +and observes a dual-stack draft client. It checks that saved settings, server +configuration, internal state, and vault bytes are unchanged. The test submits +no Commit or restart command. Independent GPT-6-Luna/high reviewer found no +actionable scoped findings after examining the actual diff and running both +focused regressions. + +## Integrated checks and identity + +Native `x86_64-unknown-linux-gnu` host; rustc/Cargo 1.97.1, rustfmt 1.9.0, +Clippy 0.1.97. Commands below ran from the repository root and exited 0: + +- `cargo test --locked creation_timestamp_discards_fractional_seconds_and_renders_as_valid_metadata`: 1 passed. +- `cargo test --locked --test tui_workflow create_form_submits_automatic_dual_stack_client_without_changing_saved_state`: 1 passed. +- `cargo fmt --all -- --check`. +- `cargo clippy --locked --all-targets -- -D warnings`. +- `cargo test --locked`: 345 passed, 3 explicitly ignored, 0 failed, including 2 documentation tests. +- `cargo build --locked --release`. +- `git diff --check`. + +Base commit: `6d8d6d572df73a534b9a8dc7c5af769f2447bcfa`. Dirty Rust +inputs are the two files below; documentation changes record this repair. +SHA256 of changed Rust files: + +- `src/tui/forms.rs`: `322153ae33e85b8dc59d887ff7810751f15e2326ce77e7f1105c9aab947fe9be` +- `tests/tui_workflow.rs`: `9f14c4bf9b80e65c3f6f5ccae93192015d738590b672f11d26aa824feb103438` + +Native artifact `target/release/wireguard-admin` SHA256: +`d59801b0de32038ae11bbfed3d4bd01f4c36fe4119e79f3b332fec5462793687`. + +These fixture results do not verify a physical terminal, Alpine networking, +or real systemd/D-Bus behavior. The previously retired M9 acceptance remains +unverified. Previous Alpine and VM source/artifact identities do not identify +this revision. diff --git a/src/tui/forms.rs b/src/tui/forms.rs index a560c2d..51e9782 100644 --- a/src/tui/forms.rs +++ b/src/tui/forms.rs @@ -689,15 +689,12 @@ impl FormController { }, _ => return FormAction::Feedback("Manual addressing requires both IPv4 and IPv6 values.".to_owned()), }; - let created_utc = match time::OffsetDateTime::now_utc() - .format(&time::format_description::well_known::Rfc3339) - { - Ok(value) => value, - Err(_) => { - return FormAction::Feedback( - "Could not prepare client creation time.".to_owned(), - ); - } + let Some(created_utc) = + format_creation_timestamp(time::OffsetDateTime::now_utc()) + else { + return FormAction::Feedback( + "Could not prepare client creation time.".to_owned(), + ); }; let command = UiCommand::CreateClient { alias: alias.trim().to_owned(), @@ -924,6 +921,13 @@ fn field_value_mut<'a>( } } +fn format_creation_timestamp(now: time::OffsetDateTime) -> Option { + now.replace_nanosecond(0) + .ok()? + .format(&time::format_description::well_known::Rfc3339) + .ok() +} + trait BoundedString { fn push_bounded(&mut self, character: char); } @@ -1054,6 +1058,16 @@ mod tests { assert!(matches!(form.handle_key(KeyCode::Esc), FormAction::Cancel)); } + #[test] + fn creation_timestamp_discards_fractional_seconds_and_renders_as_valid_metadata() { + let instant = time::OffsetDateTime::from_unix_timestamp_nanos(1_700_000_000_123_456_789) + .expect("fixed UTC instant is in range"); + + let timestamp = format_creation_timestamp(instant).expect("timestamp formats"); + assert_eq!(timestamp, "2023-11-14T22:13:20Z"); + assert!(crate::wireguard::metadata::render_block("device", Some(×tamp)).is_ok()); + } + #[test] fn rename_requires_full_key_confirmation_and_restart_exposes_exact_draft_choice() { let identity = PublicKey::from_bytes([0x5a; 32]); diff --git a/tests/tui_workflow.rs b/tests/tui_workflow.rs index 11813e4..7b297ef 100644 --- a/tests/tui_workflow.rs +++ b/tests/tui_workflow.rs @@ -9,6 +9,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::thread; use std::time::{Duration, Instant}; +use crossterm::event::KeyCode; use wireguard_admin::app::{FirstIssueAttempt, RestartChoice, RestartStage}; use wireguard_admin::backup::BackupReason; use wireguard_admin::config::{ResolvedSettings, SettingsFile}; @@ -19,6 +20,7 @@ use wireguard_admin::tui::events::{ ApplicationWorker, CommandResult, FirstIssueStatus, QrStatus, SecretText, SensitiveCommand, ShutdownProgress, StatisticsReader, UiCommand, WorkerConfig, WorkerEvent, }; +use wireguard_admin::tui::forms::{FormAction, FormController}; use wireguard_admin::vault::WireGuardPrivateKey; const SETTINGS: &str = r#" @@ -528,6 +530,110 @@ fn worker_setup_create_commit_and_delivery_keep_saved_and_runtime_boundaries() { finish(worker); } +#[test] +fn create_form_submits_automatic_dual_stack_client_without_changing_saved_state() { + let fixture = fixture(); + let reader = CountingStatistics::default(); + let reads = Arc::clone(&reader.0); + let mut worker = worker(&fixture, reader); + + let setup = worker + .submit_sensitive(SensitiveCommand::PrepareInitialSetup(SecretText::new( + "synthetic vault password".to_owned(), + ))) + .expect("setup submitted"); + assert!(matches!( + wait_for(&mut worker, setup.request_id), + WorkerEvent::OperationFinished { + result: Ok(CommandResult::InitialSetupPrepared(_)), + .. + } + )); + let saved_before_create = fixture.store.read_saved().expect("saved state reads"); + + let mut form = FormController::default(); + form.open_create(); + for character in "form-created device".chars() { + assert!(matches!( + form.handle_key(KeyCode::Char(character)), + FormAction::None + )); + } + assert!(matches!(form.handle_key(KeyCode::Enter), FormAction::None)); + assert!(matches!(form.handle_key(KeyCode::Enter), FormAction::None)); + let FormAction::Submit(command @ UiCommand::CreateClient { .. }) = + form.handle_key(KeyCode::Enter) + else { + panic!("blank address fields submit an automatically addressed client"); + }; + let UiCommand::CreateClient { + alias, + manual_addresses, + created_utc, + } = &command + else { + unreachable!(); + }; + assert_eq!(alias, "form-created device"); + assert!(manual_addresses.is_none()); + let timestamp = created_utc.as_deref().expect("form includes creation time"); + assert!(wireguard_admin::wireguard::metadata::render_block(alias, Some(timestamp)).is_ok()); + + let operation = worker.submit(command).expect("form command accepted"); + let (identity, view) = match wait_for(&mut worker, operation.request_id) { + WorkerEvent::OperationFinished { result, view, .. } => { + let CommandResult::ClientCreated(identity) = result.expect("create succeeds") else { + panic!("create returns client identity"); + }; + (identity, view) + } + _ => unreachable!("wait_for returns the requested completion"), + }; + let row = view + .client_states + .get(&identity) + .expect("created client appears in draft projection"); + assert!( + row.saved.is_none(), + "client remains absent from saved state" + ); + let draft = row.draft.as_ref().expect("created client is in draft"); + assert_eq!(draft.alias, "form-created device"); + assert_eq!( + draft.addresses.ipv4, + "10.20.0.2".parse::().unwrap() + ); + let ipv6_range = "fd20::/64".parse::().unwrap(); + assert!( + draft + .addresses + .ipv6 + .is_some_and(|address| ipv6_range.contains(&address)), + "automatic allocation includes an address from the configured IPv6 range" + ); + let saved_after_create = fixture + .store + .read_saved() + .expect("saved state remains readable"); + assert_eq!(saved_after_create.settings, saved_before_create.settings); + assert_eq!( + saved_after_create.server_config, + saved_before_create.server_config + ); + assert_eq!( + saved_after_create.internal_state, + saved_before_create.internal_state + ); + assert_eq!(saved_after_create.vault, saved_before_create.vault); + assert_eq!( + reads.load(Ordering::SeqCst), + 0, + "no runtime restart/read occurs" + ); + + finish(worker); +} + #[test] fn worker_backup_restore_preview_cancel_and_confirm_are_separate_saved_operations() { let fixture = fixture();