Fix timestamp formats conflict

This commit is contained in:
2026-10-27 20:42:12 +02:00
parent 6d8d6d572d
commit af2d061880
4 changed files with 181 additions and 11 deletions
+5 -2
View File
@@ -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
+47
View File
@@ -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.
+20 -6
View File
@@ -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(_) => {
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<String> {
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(&timestamp)).is_ok());
}
#[test]
fn rename_requires_full_key_confirmation_and_restart_exposes_exact_draft_choice() {
let identity = PublicKey::from_bytes([0x5a; 32]);
+106
View File
@@ -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::<std::net::Ipv4Addr>().unwrap()
);
let ipv6_range = "fd20::/64".parse::<ipnet::Ipv6Net>().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();