Fix timestamp formats conflict
This commit is contained in:
@@ -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 |
|
| 7 | Complete terminal management workflow | 6 | accepted |
|
||||||
| 8 | Alpine Testcontainers integration | 7; suitable disposable Docker environment | accepted |
|
| 8 | Alpine Testcontainers integration | 7; suitable disposable Docker environment | accepted |
|
||||||
| 9 | Real systemd acceptance and release readiness | 8; disposable systemd VM | retired |
|
| 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
|
### 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
|
## 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.
|
- 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.
|
- 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.
|
- 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 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.
|
||||||
- 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
|
## Plan maintenance
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
+23
-9
@@ -689,15 +689,12 @@ impl FormController {
|
|||||||
},
|
},
|
||||||
_ => return FormAction::Feedback("Manual addressing requires both IPv4 and IPv6 values.".to_owned()),
|
_ => return FormAction::Feedback("Manual addressing requires both IPv4 and IPv6 values.".to_owned()),
|
||||||
};
|
};
|
||||||
let created_utc = match time::OffsetDateTime::now_utc()
|
let Some(created_utc) =
|
||||||
.format(&time::format_description::well_known::Rfc3339)
|
format_creation_timestamp(time::OffsetDateTime::now_utc())
|
||||||
{
|
else {
|
||||||
Ok(value) => value,
|
return FormAction::Feedback(
|
||||||
Err(_) => {
|
"Could not prepare client creation time.".to_owned(),
|
||||||
return FormAction::Feedback(
|
);
|
||||||
"Could not prepare client creation time.".to_owned(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
let command = UiCommand::CreateClient {
|
let command = UiCommand::CreateClient {
|
||||||
alias: alias.trim().to_owned(),
|
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 {
|
trait BoundedString {
|
||||||
fn push_bounded(&mut self, character: char);
|
fn push_bounded(&mut self, character: char);
|
||||||
}
|
}
|
||||||
@@ -1054,6 +1058,16 @@ mod tests {
|
|||||||
assert!(matches!(form.handle_key(KeyCode::Esc), FormAction::Cancel));
|
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]
|
#[test]
|
||||||
fn rename_requires_full_key_confirmation_and_restart_exposes_exact_draft_choice() {
|
fn rename_requires_full_key_confirmation_and_restart_exposes_exact_draft_choice() {
|
||||||
let identity = PublicKey::from_bytes([0x5a; 32]);
|
let identity = PublicKey::from_bytes([0x5a; 32]);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
|
|||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
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};
|
||||||
@@ -19,6 +20,7 @@ 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,
|
||||||
};
|
};
|
||||||
|
use wireguard_admin::tui::forms::{FormAction, FormController};
|
||||||
use wireguard_admin::vault::WireGuardPrivateKey;
|
use wireguard_admin::vault::WireGuardPrivateKey;
|
||||||
|
|
||||||
const SETTINGS: &str = r#"
|
const SETTINGS: &str = r#"
|
||||||
@@ -528,6 +530,110 @@ fn worker_setup_create_commit_and_delivery_keep_saved_and_runtime_boundaries() {
|
|||||||
finish(worker);
|
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]
|
#[test]
|
||||||
fn worker_backup_restore_preview_cancel_and_confirm_are_separate_saved_operations() {
|
fn worker_backup_restore_preview_cancel_and_confirm_are_separate_saved_operations() {
|
||||||
let fixture = fixture();
|
let fixture = fixture();
|
||||||
|
|||||||
Reference in New Issue
Block a user