wire types: use chrono DateTime<Utc> as the timestamp type throughout
This commit is contained in:
parent
1e205289c5
commit
2c5d9ed336
15 changed files with 108 additions and 190 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1373,6 +1373,7 @@ dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"base64",
|
"base64",
|
||||||
"bcrypt",
|
"bcrypt",
|
||||||
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"clap-markdown",
|
"clap-markdown",
|
||||||
"clap_complete",
|
"clap_complete",
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,10 @@ libc = "0.2"
|
||||||
axum = { version = "0.8", features = ["ws"] }
|
axum = { version = "0.8", features = ["ws"] }
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
bcrypt = "0.19"
|
bcrypt = "0.19"
|
||||||
chrono = { version = "0.4", default-features = false, features = ["std"] }
|
chrono = { version = "0.4", default-features = false, features = [
|
||||||
|
"serde",
|
||||||
|
"std",
|
||||||
|
] }
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
clap_complete = "4"
|
clap_complete = "4"
|
||||||
hive-sh4re = { path = "hive-sh4re" }
|
hive-sh4re = { path = "hive-sh4re" }
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ workspace = true
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
bcrypt.workspace = true
|
bcrypt.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -231,9 +231,9 @@ impl Approvals {
|
||||||
agent: row.agent,
|
agent: row.agent,
|
||||||
kind: kind_from_str(&row.kind)?,
|
kind: kind_from_str(&row.kind)?,
|
||||||
commit_ref: row.commit_ref,
|
commit_ref: row.commit_ref,
|
||||||
requested_at: hive_sh4re::wire_time::WireTime(row.requested_at),
|
requested_at: hive_sh4re::wire_time::from_secs(row.requested_at),
|
||||||
status: ApprovalStatus::Approved,
|
status: ApprovalStatus::Approved,
|
||||||
resolved_at: Some(hive_sh4re::wire_time::WireTime(resolved_at)),
|
resolved_at: Some(hive_sh4re::wire_time::from_secs(resolved_at)),
|
||||||
note: None,
|
note: None,
|
||||||
fetched_sha: row.fetched_sha,
|
fetched_sha: row.fetched_sha,
|
||||||
description: row.description,
|
description: row.description,
|
||||||
|
|
@ -294,9 +294,9 @@ impl Approvals {
|
||||||
agent: row.agent,
|
agent: row.agent,
|
||||||
kind: kind_from_str(&row.kind)?,
|
kind: kind_from_str(&row.kind)?,
|
||||||
commit_ref: row.commit_ref,
|
commit_ref: row.commit_ref,
|
||||||
requested_at: hive_sh4re::wire_time::WireTime(row.requested_at),
|
requested_at: hive_sh4re::wire_time::from_secs(row.requested_at),
|
||||||
status: ApprovalStatus::Cancelled,
|
status: ApprovalStatus::Cancelled,
|
||||||
resolved_at: Some(hive_sh4re::wire_time::WireTime(resolved_at)),
|
resolved_at: Some(hive_sh4re::wire_time::from_secs(resolved_at)),
|
||||||
note: Some(note),
|
note: Some(note),
|
||||||
fetched_sha: row.fetched_sha,
|
fetched_sha: row.fetched_sha,
|
||||||
description: row.description,
|
description: row.description,
|
||||||
|
|
@ -404,11 +404,11 @@ fn row_to_approval(row: &rusqlite::Row<'_>) -> rusqlite::Result<Approval> {
|
||||||
agent: row.get(1)?,
|
agent: row.get(1)?,
|
||||||
kind,
|
kind,
|
||||||
commit_ref: row.get(3)?,
|
commit_ref: row.get(3)?,
|
||||||
requested_at: hive_sh4re::wire_time::WireTime(row.get(4)?),
|
requested_at: hive_sh4re::wire_time::from_secs(row.get(4)?),
|
||||||
status,
|
status,
|
||||||
resolved_at: row
|
resolved_at: row
|
||||||
.get::<_, Option<i64>>(6)?
|
.get::<_, Option<i64>>(6)?
|
||||||
.map(hive_sh4re::wire_time::WireTime),
|
.map(hive_sh4re::wire_time::from_secs),
|
||||||
note: row.get(7)?,
|
note: row.get(7)?,
|
||||||
fetched_sha: row.get(8)?,
|
fetched_sha: row.get(8)?,
|
||||||
description: row.get(9)?,
|
description: row.get(9)?,
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,8 @@ use std::sync::{Arc, Mutex, OnceLock};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use hive_sh4re::wire_time::WireTime;
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use rusqlite::{Connection, params};
|
use rusqlite::{Connection, params};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
|
|
@ -90,7 +91,7 @@ impl AuditOutcome {
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct AuditEntry {
|
pub struct AuditEntry {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub ts_unix: WireTime,
|
pub ts_unix: DateTime<Utc>,
|
||||||
/// Agent on whose behalf the action was taken.
|
/// Agent on whose behalf the action was taken.
|
||||||
pub agent: String,
|
pub agent: String,
|
||||||
/// What was done (e.g. `restart_infra`).
|
/// What was done (e.g. `restart_infra`).
|
||||||
|
|
@ -159,7 +160,7 @@ impl AuditLog {
|
||||||
) {
|
) {
|
||||||
Ok(_) => Some(AuditEntry {
|
Ok(_) => Some(AuditEntry {
|
||||||
id: conn.last_insert_rowid(),
|
id: conn.last_insert_rowid(),
|
||||||
ts_unix: hive_sh4re::wire_time::WireTime(now),
|
ts_unix: hive_sh4re::wire_time::from_secs(now),
|
||||||
agent: agent.to_owned(),
|
agent: agent.to_owned(),
|
||||||
action: action.to_owned(),
|
action: action.to_owned(),
|
||||||
target: target.to_owned(),
|
target: target.to_owned(),
|
||||||
|
|
@ -252,7 +253,7 @@ pub fn spawn_vacuum(coord: &Arc<crate::coordinator::Coordinator>) {
|
||||||
fn row_to_entry(r: &rusqlite::Row) -> rusqlite::Result<AuditEntry> {
|
fn row_to_entry(r: &rusqlite::Row) -> rusqlite::Result<AuditEntry> {
|
||||||
Ok(AuditEntry {
|
Ok(AuditEntry {
|
||||||
id: r.get(0)?,
|
id: r.get(0)?,
|
||||||
ts_unix: hive_sh4re::wire_time::WireTime(r.get(1)?),
|
ts_unix: hive_sh4re::wire_time::from_secs(r.get(1)?),
|
||||||
agent: r.get(2)?,
|
agent: r.get(2)?,
|
||||||
action: r.get(3)?,
|
action: r.get(3)?,
|
||||||
target: r.get(4)?,
|
target: r.get(4)?,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ use std::sync::Mutex;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use hive_sh4re::wire_time::WireTime;
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use hive_sh4re::{InboxRow, Message};
|
use hive_sh4re::{InboxRow, Message};
|
||||||
use rusqlite::{Connection, OptionalExtension, params};
|
use rusqlite::{Connection, OptionalExtension, params};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
@ -75,8 +76,8 @@ pub struct PendingReminder {
|
||||||
pub message: String,
|
pub message: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub file_path: Option<String>,
|
pub file_path: Option<String>,
|
||||||
pub due_at: WireTime,
|
pub due_at: DateTime<Utc>,
|
||||||
pub created_at: WireTime,
|
pub created_at: DateTime<Utc>,
|
||||||
/// Most recent delivery failure for this row, if any. Cleared
|
/// Most recent delivery failure for this row, if any. Cleared
|
||||||
/// to NULL on operator retry. Surfaced inline in the dashboard
|
/// to NULL on operator retry. Surfaced inline in the dashboard
|
||||||
/// so a stuck reminder doesn't just silently retry forever.
|
/// so a stuck reminder doesn't just silently retry forever.
|
||||||
|
|
@ -925,8 +926,8 @@ impl Broker {
|
||||||
agent: row.get(1)?,
|
agent: row.get(1)?,
|
||||||
message: row.get(2)?,
|
message: row.get(2)?,
|
||||||
file_path: row.get(3)?,
|
file_path: row.get(3)?,
|
||||||
due_at: hive_sh4re::wire_time::WireTime(row.get(4)?),
|
due_at: hive_sh4re::wire_time::from_secs(row.get(4)?),
|
||||||
created_at: hive_sh4re::wire_time::WireTime(row.get(5)?),
|
created_at: hive_sh4re::wire_time::from_secs(row.get(5)?),
|
||||||
last_error: row.get(6)?,
|
last_error: row.get(6)?,
|
||||||
attempt_count: u32::try_from(attempts).unwrap_or(0),
|
attempt_count: u32::try_from(attempts).unwrap_or(0),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -805,7 +805,7 @@ impl Coordinator {
|
||||||
approval_kind,
|
approval_kind,
|
||||||
sha_short,
|
sha_short,
|
||||||
status,
|
status,
|
||||||
resolved_at: hive_sh4re::wire_time::WireTime(resolved_at),
|
resolved_at: hive_sh4re::wire_time::from_secs(resolved_at),
|
||||||
note,
|
note,
|
||||||
description,
|
description,
|
||||||
});
|
});
|
||||||
|
|
@ -838,8 +838,8 @@ impl Coordinator {
|
||||||
question: question.to_owned(),
|
question: question.to_owned(),
|
||||||
options: options.to_vec(),
|
options: options.to_vec(),
|
||||||
multi,
|
multi,
|
||||||
asked_at: hive_sh4re::wire_time::WireTime(asked_at),
|
asked_at: hive_sh4re::wire_time::from_secs(asked_at),
|
||||||
deadline_at: deadline_at.map(hive_sh4re::wire_time::WireTime),
|
deadline_at: deadline_at.map(hive_sh4re::wire_time::from_secs),
|
||||||
target: target.map(str::to_owned),
|
target: target.map(str::to_owned),
|
||||||
question_refs,
|
question_refs,
|
||||||
});
|
});
|
||||||
|
|
@ -869,7 +869,7 @@ impl Coordinator {
|
||||||
id,
|
id,
|
||||||
answer: answer.to_owned(),
|
answer: answer.to_owned(),
|
||||||
answerer: answerer.to_owned(),
|
answerer: answerer.to_owned(),
|
||||||
answered_at: hive_sh4re::wire_time::WireTime(answered_at),
|
answered_at: hive_sh4re::wire_time::from_secs(answered_at),
|
||||||
cancelled,
|
cancelled,
|
||||||
target: target.map(str::to_owned),
|
target: target.map(str::to_owned),
|
||||||
answer_refs,
|
answer_refs,
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ use tokio_stream::{Stream, StreamExt};
|
||||||
use crate::container_view::{ContainerView, claude_has_session};
|
use crate::container_view::{ContainerView, claude_has_session};
|
||||||
use crate::coordinator::Coordinator;
|
use crate::coordinator::Coordinator;
|
||||||
use crate::lifecycle::{self, MANAGER_NAME};
|
use crate::lifecycle::{self, MANAGER_NAME};
|
||||||
use hive_sh4re::wire_time::WireTime;
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
mod approvals;
|
mod approvals;
|
||||||
mod build_logs;
|
mod build_logs;
|
||||||
|
|
@ -434,7 +434,7 @@ struct ApprovalHistoryView {
|
||||||
/// `approved` / `denied` / `failed`.
|
/// `approved` / `denied` / `failed`.
|
||||||
status: &'static str,
|
status: &'static str,
|
||||||
/// RFC 3339 UTC. Renders as a relative time on the dashboard.
|
/// RFC 3339 UTC. Renders as a relative time on the dashboard.
|
||||||
resolved_at: WireTime,
|
resolved_at: DateTime<Utc>,
|
||||||
/// Operator-supplied deny reason (for `denied`) or build error
|
/// Operator-supplied deny reason (for `denied`) or build error
|
||||||
/// (for `failed`). None on `approved`.
|
/// (for `failed`). None on `approved`.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
|
@ -475,7 +475,7 @@ struct ApprovalView {
|
||||||
/// RFC 3339 UTC time the approval was queued. Rendered as a
|
/// RFC 3339 UTC time the approval was queued. Rendered as a
|
||||||
/// relative time on the card so the operator can spot a stale
|
/// relative time on the card so the operator can spot a stale
|
||||||
/// request.
|
/// request.
|
||||||
requested_at: WireTime,
|
requested_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace silent `.unwrap_or_default()` on the data sources behind
|
/// Replace silent `.unwrap_or_default()` on the data sources behind
|
||||||
|
|
@ -1100,7 +1100,7 @@ async fn dashboard_history(State(state): State<AppState>) -> Response {
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
body,
|
body,
|
||||||
at: hive_sh4re::wire_time::WireTime(at),
|
at: hive_sh4re::wire_time::from_secs(at),
|
||||||
in_reply_to,
|
in_reply_to,
|
||||||
file_refs,
|
file_refs,
|
||||||
})
|
})
|
||||||
|
|
@ -1120,7 +1120,7 @@ async fn dashboard_history(State(state): State<AppState>) -> Response {
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
body,
|
body,
|
||||||
at: hive_sh4re::wire_time::WireTime(at),
|
at: hive_sh4re::wire_time::from_secs(at),
|
||||||
in_reply_to,
|
in_reply_to,
|
||||||
file_refs,
|
file_refs,
|
||||||
})
|
})
|
||||||
|
|
@ -1375,7 +1375,7 @@ async fn api_operator_inbox(State(state): State<AppState>) -> Response {
|
||||||
"id": id,
|
"id": id,
|
||||||
"from": from,
|
"from": from,
|
||||||
"body": body,
|
"body": body,
|
||||||
"at": hive_sh4re::wire_time::WireTime(at),
|
"at": hive_sh4re::wire_time::from_secs(at),
|
||||||
"in_reply_to": in_reply_to,
|
"in_reply_to": in_reply_to,
|
||||||
"file_refs": file_refs,
|
"file_refs": file_refs,
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ use serde::Serialize;
|
||||||
use crate::container_view::ContainerView;
|
use crate::container_view::ContainerView;
|
||||||
use crate::dashboard::{MetaInputView, TombstoneView};
|
use crate::dashboard::{MetaInputView, TombstoneView};
|
||||||
use crate::rebuild_queue::QueueEntry;
|
use crate::rebuild_queue::QueueEntry;
|
||||||
use hive_sh4re::wire_time::WireTime;
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
#[serde(rename_all = "snake_case", tag = "kind")]
|
#[serde(rename_all = "snake_case", tag = "kind")]
|
||||||
|
|
@ -39,7 +39,7 @@ pub enum DashboardEvent {
|
||||||
from: String,
|
from: String,
|
||||||
to: String,
|
to: String,
|
||||||
body: String,
|
body: String,
|
||||||
at: WireTime,
|
at: DateTime<Utc>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
in_reply_to: Option<i64>,
|
in_reply_to: Option<i64>,
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
|
@ -54,7 +54,7 @@ pub enum DashboardEvent {
|
||||||
from: String,
|
from: String,
|
||||||
to: String,
|
to: String,
|
||||||
body: String,
|
body: String,
|
||||||
at: WireTime,
|
at: DateTime<Utc>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
in_reply_to: Option<i64>,
|
in_reply_to: Option<i64>,
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
|
@ -96,7 +96,7 @@ pub enum DashboardEvent {
|
||||||
sha_short: Option<String>,
|
sha_short: Option<String>,
|
||||||
/// `"approved"` / `"denied"` / `"failed"`.
|
/// `"approved"` / `"denied"` / `"failed"`.
|
||||||
status: &'static str,
|
status: &'static str,
|
||||||
resolved_at: WireTime,
|
resolved_at: DateTime<Utc>,
|
||||||
note: Option<String>,
|
note: Option<String>,
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
@ -112,8 +112,8 @@ pub enum DashboardEvent {
|
||||||
question: String,
|
question: String,
|
||||||
options: Vec<String>,
|
options: Vec<String>,
|
||||||
multi: bool,
|
multi: bool,
|
||||||
asked_at: WireTime,
|
asked_at: DateTime<Utc>,
|
||||||
deadline_at: Option<WireTime>,
|
deadline_at: Option<DateTime<Utc>>,
|
||||||
target: Option<String>,
|
target: Option<String>,
|
||||||
/// Verified file-path tokens that appear in `question`.
|
/// Verified file-path tokens that appear in `question`.
|
||||||
/// Same shape as broker `Sent`/`Delivered` events; the
|
/// Same shape as broker `Sent`/`Delivered` events; the
|
||||||
|
|
@ -131,7 +131,7 @@ pub enum DashboardEvent {
|
||||||
id: i64,
|
id: i64,
|
||||||
answer: String,
|
answer: String,
|
||||||
answerer: String,
|
answerer: String,
|
||||||
answered_at: WireTime,
|
answered_at: DateTime<Utc>,
|
||||||
cancelled: bool,
|
cancelled: bool,
|
||||||
target: Option<String>,
|
target: Option<String>,
|
||||||
/// Verified file-path tokens that appear in `answer`.
|
/// Verified file-path tokens that appear in `answer`.
|
||||||
|
|
@ -332,7 +332,7 @@ mod tests {
|
||||||
from: "a".into(),
|
from: "a".into(),
|
||||||
to: "b".into(),
|
to: "b".into(),
|
||||||
body: String::new(),
|
body: String::new(),
|
||||||
at: hive_sh4re::wire_time::WireTime(0),
|
at: hive_sh4re::wire_time::from_secs(0),
|
||||||
in_reply_to: None,
|
in_reply_to: None,
|
||||||
file_refs: Vec::new(),
|
file_refs: Vec::new(),
|
||||||
},
|
},
|
||||||
|
|
@ -342,7 +342,7 @@ mod tests {
|
||||||
from: "a".into(),
|
from: "a".into(),
|
||||||
to: "b".into(),
|
to: "b".into(),
|
||||||
body: String::new(),
|
body: String::new(),
|
||||||
at: hive_sh4re::wire_time::WireTime(0),
|
at: hive_sh4re::wire_time::from_secs(0),
|
||||||
in_reply_to: None,
|
in_reply_to: None,
|
||||||
file_refs: Vec::new(),
|
file_refs: Vec::new(),
|
||||||
},
|
},
|
||||||
|
|
@ -363,7 +363,7 @@ mod tests {
|
||||||
approval_kind: "apply_commit",
|
approval_kind: "apply_commit",
|
||||||
sha_short: None,
|
sha_short: None,
|
||||||
status: "approved",
|
status: "approved",
|
||||||
resolved_at: hive_sh4re::wire_time::WireTime(0),
|
resolved_at: hive_sh4re::wire_time::from_secs(0),
|
||||||
note: None,
|
note: None,
|
||||||
description: None,
|
description: None,
|
||||||
},
|
},
|
||||||
|
|
@ -374,7 +374,7 @@ mod tests {
|
||||||
question: String::new(),
|
question: String::new(),
|
||||||
options: Vec::new(),
|
options: Vec::new(),
|
||||||
multi: false,
|
multi: false,
|
||||||
asked_at: hive_sh4re::wire_time::WireTime(0),
|
asked_at: hive_sh4re::wire_time::from_secs(0),
|
||||||
deadline_at: None,
|
deadline_at: None,
|
||||||
target: None,
|
target: None,
|
||||||
question_refs: Vec::new(),
|
question_refs: Vec::new(),
|
||||||
|
|
@ -384,7 +384,7 @@ mod tests {
|
||||||
id: 1,
|
id: 1,
|
||||||
answer: String::new(),
|
answer: String::new(),
|
||||||
answerer: "a".into(),
|
answerer: "a".into(),
|
||||||
answered_at: hive_sh4re::wire_time::WireTime(0),
|
answered_at: hive_sh4re::wire_time::from_secs(0),
|
||||||
cancelled: false,
|
cancelled: false,
|
||||||
target: None,
|
target: None,
|
||||||
answer_refs: Vec::new(),
|
answer_refs: Vec::new(),
|
||||||
|
|
@ -447,7 +447,7 @@ mod tests {
|
||||||
seq: 1,
|
seq: 1,
|
||||||
entry: crate::audit_log::AuditEntry {
|
entry: crate::audit_log::AuditEntry {
|
||||||
id: 1,
|
id: 1,
|
||||||
ts_unix: hive_sh4re::wire_time::WireTime(0),
|
ts_unix: hive_sh4re::wire_time::from_secs(0),
|
||||||
agent: "atlas".into(),
|
agent: "atlas".into(),
|
||||||
action: "restart_infra".into(),
|
action: "restart_infra".into(),
|
||||||
target: "hive-ci".into(),
|
target: "hive-ci".into(),
|
||||||
|
|
@ -476,7 +476,7 @@ mod tests {
|
||||||
seq: 7,
|
seq: 7,
|
||||||
entry: crate::audit_log::AuditEntry {
|
entry: crate::audit_log::AuditEntry {
|
||||||
id: 42,
|
id: 42,
|
||||||
ts_unix: hive_sh4re::wire_time::WireTime(1_700_000_000),
|
ts_unix: hive_sh4re::wire_time::from_secs(1_700_000_000),
|
||||||
agent: "atlas".into(),
|
agent: "atlas".into(),
|
||||||
action: "restart_infra".into(),
|
action: "restart_infra".into(),
|
||||||
target: "hive-gateway".into(),
|
target: "hive-gateway".into(),
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ pub fn for_agent(coord: &Coordinator, agent: &str) -> Result<Vec<LooseEnd>> {
|
||||||
agent: a.agent,
|
agent: a.agent,
|
||||||
commit_ref: a.commit_ref,
|
commit_ref: a.commit_ref,
|
||||||
description: a.description,
|
description: a.description,
|
||||||
age_seconds: saturating_age(now, a.requested_at.secs()),
|
age_seconds: saturating_age(now, a.requested_at.timestamp()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for q in coord.questions.pending_all()? {
|
for q in coord.questions.pending_all()? {
|
||||||
|
|
@ -83,7 +83,7 @@ pub fn for_agent(coord: &Coordinator, agent: &str) -> Result<Vec<LooseEnd>> {
|
||||||
asker: q.asker,
|
asker: q.asker,
|
||||||
target: q.target,
|
target: q.target,
|
||||||
question: q.question,
|
question: q.question,
|
||||||
age_seconds: saturating_age(now, q.asked_at.secs()),
|
age_seconds: saturating_age(now, q.asked_at.timestamp()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for r in coord.broker.list_pending_reminders()? {
|
for r in coord.broker.list_pending_reminders()? {
|
||||||
|
|
@ -95,7 +95,7 @@ pub fn for_agent(coord: &Coordinator, agent: &str) -> Result<Vec<LooseEnd>> {
|
||||||
owner: r.agent,
|
owner: r.agent,
|
||||||
message: r.message,
|
message: r.message,
|
||||||
due_at: r.due_at,
|
due_at: r.due_at,
|
||||||
age_seconds: saturating_age(now, r.created_at.secs()),
|
age_seconds: saturating_age(now, r.created_at.timestamp()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(out)
|
Ok(out)
|
||||||
|
|
@ -114,7 +114,7 @@ pub fn hive_wide(coord: &Coordinator) -> Result<Vec<LooseEnd>> {
|
||||||
agent: a.agent,
|
agent: a.agent,
|
||||||
commit_ref: a.commit_ref,
|
commit_ref: a.commit_ref,
|
||||||
description: a.description,
|
description: a.description,
|
||||||
age_seconds: saturating_age(now, a.requested_at.secs()),
|
age_seconds: saturating_age(now, a.requested_at.timestamp()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for q in coord.questions.pending_all()? {
|
for q in coord.questions.pending_all()? {
|
||||||
|
|
@ -123,7 +123,7 @@ pub fn hive_wide(coord: &Coordinator) -> Result<Vec<LooseEnd>> {
|
||||||
asker: q.asker,
|
asker: q.asker,
|
||||||
target: q.target,
|
target: q.target,
|
||||||
question: q.question,
|
question: q.question,
|
||||||
age_seconds: saturating_age(now, q.asked_at.secs()),
|
age_seconds: saturating_age(now, q.asked_at.timestamp()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for r in coord.broker.list_pending_reminders()? {
|
for r in coord.broker.list_pending_reminders()? {
|
||||||
|
|
@ -132,7 +132,7 @@ pub fn hive_wide(coord: &Coordinator) -> Result<Vec<LooseEnd>> {
|
||||||
owner: r.agent,
|
owner: r.agent,
|
||||||
message: r.message,
|
message: r.message,
|
||||||
due_at: r.due_at,
|
due_at: r.due_at,
|
||||||
age_seconds: saturating_age(now, r.created_at.secs()),
|
age_seconds: saturating_age(now, r.created_at.timestamp()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(out)
|
Ok(out)
|
||||||
|
|
|
||||||
|
|
@ -497,7 +497,7 @@ fn spawn_broker_to_dashboard_forwarder(coord: Arc<Coordinator>) {
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
body,
|
body,
|
||||||
at: hive_sh4re::wire_time::WireTime(at),
|
at: hive_sh4re::wire_time::from_secs(at),
|
||||||
in_reply_to,
|
in_reply_to,
|
||||||
file_refs,
|
file_refs,
|
||||||
});
|
});
|
||||||
|
|
@ -517,7 +517,7 @@ fn spawn_broker_to_dashboard_forwarder(coord: Arc<Coordinator>) {
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
body,
|
body,
|
||||||
at: hive_sh4re::wire_time::WireTime(at),
|
at: hive_sh4re::wire_time::from_secs(at),
|
||||||
in_reply_to,
|
in_reply_to,
|
||||||
file_refs,
|
file_refs,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ use std::sync::Mutex;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use hive_sh4re::wire_time::WireTime;
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use rusqlite::{Connection, OptionalExtension, params};
|
use rusqlite::{Connection, OptionalExtension, params};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
|
|
@ -75,12 +76,12 @@ pub struct OpQuestion {
|
||||||
pub question: String,
|
pub question: String,
|
||||||
pub options: Vec<String>,
|
pub options: Vec<String>,
|
||||||
pub multi: bool,
|
pub multi: bool,
|
||||||
pub asked_at: WireTime,
|
pub asked_at: DateTime<Utc>,
|
||||||
/// Deadline after which a watchdog auto-resolves the question with
|
/// Deadline after which a watchdog auto-resolves the question with
|
||||||
/// answer `[expired]`. `None` = no expiry. Surfaced on the
|
/// answer `[expired]`. `None` = no expiry. Surfaced on the
|
||||||
/// dashboard as a remaining-time chip.
|
/// dashboard as a remaining-time chip.
|
||||||
pub deadline_at: Option<WireTime>,
|
pub deadline_at: Option<DateTime<Utc>>,
|
||||||
pub answered_at: Option<WireTime>,
|
pub answered_at: Option<DateTime<Utc>>,
|
||||||
pub answer: Option<String>,
|
pub answer: Option<String>,
|
||||||
/// Recipient of the question. `None` = the operator (dashboard
|
/// Recipient of the question. `None` = the operator (dashboard
|
||||||
/// path); `Some(<agent>)` = a peer agent asked via
|
/// path); `Some(<agent>)` = a peer agent asked via
|
||||||
|
|
@ -288,14 +289,14 @@ fn row_to_question(row: &rusqlite::Row<'_>) -> rusqlite::Result<OpQuestion> {
|
||||||
question: row.get(2)?,
|
question: row.get(2)?,
|
||||||
options,
|
options,
|
||||||
multi: multi != 0,
|
multi: multi != 0,
|
||||||
asked_at: hive_sh4re::wire_time::WireTime(row.get(5)?),
|
asked_at: hive_sh4re::wire_time::from_secs(row.get(5)?),
|
||||||
answered_at: row
|
answered_at: row
|
||||||
.get::<_, Option<i64>>(6)?
|
.get::<_, Option<i64>>(6)?
|
||||||
.map(hive_sh4re::wire_time::WireTime),
|
.map(hive_sh4re::wire_time::from_secs),
|
||||||
answer: row.get(7)?,
|
answer: row.get(7)?,
|
||||||
deadline_at: row
|
deadline_at: row
|
||||||
.get::<_, Option<i64>>(8)?
|
.get::<_, Option<i64>>(8)?
|
||||||
.map(hive_sh4re::wire_time::WireTime),
|
.map(hive_sh4re::wire_time::from_secs),
|
||||||
target: row.get(9)?,
|
target: row.get(9)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2018,8 +2018,8 @@ fn schedule_to_wire(s: crate::scheduled_prompts::Schedule) -> hive_sh4re::WireSc
|
||||||
owner: s.owner,
|
owner: s.owner,
|
||||||
body: s.body,
|
body: s.body,
|
||||||
interval_seconds: s.interval_seconds,
|
interval_seconds: s.interval_seconds,
|
||||||
next_fire_at_unix: hive_sh4re::wire_time::WireTime(s.next_fire_at_unix),
|
next_fire_at_unix: hive_sh4re::wire_time::from_secs(s.next_fire_at_unix),
|
||||||
created_at_unix: hive_sh4re::wire_time::WireTime(s.created_at_unix),
|
created_at_unix: hive_sh4re::wire_time::from_secs(s.created_at_unix),
|
||||||
source: match s.source {
|
source: match s.source {
|
||||||
crate::scheduled_prompts::ScheduleSource::Operator => {
|
crate::scheduled_prompts::ScheduleSource::Operator => {
|
||||||
hive_sh4re::WireScheduleSource::Operator
|
hive_sh4re::WireScheduleSource::Operator
|
||||||
|
|
@ -2028,16 +2028,16 @@ fn schedule_to_wire(s: crate::scheduled_prompts::Schedule) -> hive_sh4re::WireSc
|
||||||
hive_sh4re::WireScheduleSource::Approval { id }
|
hive_sh4re::WireScheduleSource::Approval { id }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cancelled_at_unix: s.cancelled_at_unix.map(hive_sh4re::wire_time::WireTime),
|
cancelled_at_unix: s.cancelled_at_unix.map(hive_sh4re::wire_time::from_secs),
|
||||||
paused_at_unix: s.paused_at_unix.map(hive_sh4re::wire_time::WireTime),
|
paused_at_unix: s.paused_at_unix.map(hive_sh4re::wire_time::from_secs),
|
||||||
description: s.description,
|
description: s.description,
|
||||||
targets: s
|
targets: s
|
||||||
.targets
|
.targets
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|t| hive_sh4re::WireScheduleTarget {
|
.map(|t| hive_sh4re::WireScheduleTarget {
|
||||||
target: t.target,
|
target: t.target,
|
||||||
cancelled_at_unix: t.cancelled_at_unix.map(hive_sh4re::wire_time::WireTime),
|
cancelled_at_unix: t.cancelled_at_unix.map(hive_sh4re::wire_time::from_secs),
|
||||||
last_fired_at_unix: t.last_fired_at_unix.map(hive_sh4re::wire_time::WireTime),
|
last_fired_at_unix: t.last_fired_at_unix.map(hive_sh4re::wire_time::from_secs),
|
||||||
last_result: t.last_result,
|
last_result: t.last_result,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
|
@ -2131,8 +2131,8 @@ mod tests {
|
||||||
owner: "operator".to_owned(),
|
owner: "operator".to_owned(),
|
||||||
body: "ping".to_owned(),
|
body: "ping".to_owned(),
|
||||||
interval_seconds: None,
|
interval_seconds: None,
|
||||||
next_fire_at_unix: hive_sh4re::wire_time::WireTime(0),
|
next_fire_at_unix: hive_sh4re::wire_time::from_secs(0),
|
||||||
created_at_unix: hive_sh4re::wire_time::WireTime(0),
|
created_at_unix: hive_sh4re::wire_time::from_secs(0),
|
||||||
source: hive_sh4re::WireScheduleSource::Operator,
|
source: hive_sh4re::WireScheduleSource::Operator,
|
||||||
cancelled_at_unix: None,
|
cancelled_at_unix: None,
|
||||||
paused_at_unix: None,
|
paused_at_unix: None,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//! Wire types shared between `hive-c0re` and the in-container harness.
|
//! Wire types shared between `hive-c0re` and the in-container harness.
|
||||||
|
|
||||||
use crate::wire_time::WireTime;
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub mod assets;
|
pub mod assets;
|
||||||
|
|
@ -210,10 +210,10 @@ pub struct Approval {
|
||||||
/// hive-c0re refreshes this + re-renders the card for re-review.
|
/// hive-c0re refreshes this + re-renders the card for re-review.
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub fetched_sha: Option<String>,
|
pub fetched_sha: Option<String>,
|
||||||
pub requested_at: WireTime,
|
pub requested_at: DateTime<Utc>,
|
||||||
pub status: ApprovalStatus,
|
pub status: ApprovalStatus,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub resolved_at: Option<WireTime>,
|
pub resolved_at: Option<DateTime<Utc>>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub note: Option<String>,
|
pub note: Option<String>,
|
||||||
/// Free-text description the manager attached at submission time;
|
/// Free-text description the manager attached at submission time;
|
||||||
|
|
@ -449,7 +449,7 @@ pub enum LooseEnd {
|
||||||
id: i64,
|
id: i64,
|
||||||
owner: String,
|
owner: String,
|
||||||
message: String,
|
message: String,
|
||||||
due_at: WireTime,
|
due_at: DateTime<Utc>,
|
||||||
age_seconds: u64,
|
age_seconds: u64,
|
||||||
},
|
},
|
||||||
/// Undelivered inbox messages waiting to be `recv`'d by this agent.
|
/// Undelivered inbox messages waiting to be `recv`'d by this agent.
|
||||||
|
|
@ -1414,17 +1414,17 @@ pub struct WireSchedule {
|
||||||
pub body: String,
|
pub body: String,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub interval_seconds: Option<u64>,
|
pub interval_seconds: Option<u64>,
|
||||||
pub next_fire_at_unix: WireTime,
|
pub next_fire_at_unix: DateTime<Utc>,
|
||||||
pub created_at_unix: WireTime,
|
pub created_at_unix: DateTime<Utc>,
|
||||||
pub source: WireScheduleSource,
|
pub source: WireScheduleSource,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub cancelled_at_unix: Option<WireTime>,
|
pub cancelled_at_unix: Option<DateTime<Utc>>,
|
||||||
/// Set while the schedule is paused. Worker skips paused rows;
|
/// Set while the schedule is paused. Worker skips paused rows;
|
||||||
/// they keep their `next_fire_at_unix` so resuming at any time
|
/// they keep their `next_fire_at_unix` so resuming at any time
|
||||||
/// fires at the next intended instant (no catch-up clamp needed
|
/// fires at the next intended instant (no catch-up clamp needed
|
||||||
/// — a paused schedule simply slips its next fire).
|
/// — a paused schedule simply slips its next fire).
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub paused_at_unix: Option<WireTime>,
|
pub paused_at_unix: Option<DateTime<Utc>>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub targets: Vec<WireScheduleTarget>,
|
pub targets: Vec<WireScheduleTarget>,
|
||||||
|
|
@ -1441,9 +1441,9 @@ pub enum WireScheduleSource {
|
||||||
pub struct WireScheduleTarget {
|
pub struct WireScheduleTarget {
|
||||||
pub target: String,
|
pub target: String,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub cancelled_at_unix: Option<WireTime>,
|
pub cancelled_at_unix: Option<DateTime<Utc>>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub last_fired_at_unix: Option<WireTime>,
|
pub last_fired_at_unix: Option<DateTime<Utc>>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub last_result: Option<String>,
|
pub last_result: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,124 +1,38 @@
|
||||||
//! Serde adaptors for timestamp fields: `i64` unix-epoch seconds in
|
//! Timestamp conventions for the wire types: fields are
|
||||||
//! Rust, RFC 3339 UTC strings (`2026-07-02T18:30:00Z`) in JSON.
|
//! `chrono::DateTime<Utc>` (serde serializes them as RFC 3339 UTC `Z`
|
||||||
//!
|
//! strings, e.g. `2026-07-02T18:30:00Z`), while sqlite storage and
|
||||||
//! Rust code keeps doing plain integer arithmetic on these fields —
|
//! agent-facing *input* args stay unix-epoch seconds (`i64`). This
|
||||||
//! only the serialized representation changes, so the dashboard (and
|
//! module owns the two conversions at those boundaries.
|
||||||
//! any other JSON consumer) can feed the value straight into
|
|
||||||
//! `new Date(s)` without the `* 1000` epoch dance.
|
|
||||||
//!
|
|
||||||
//! Deserialization is lenient: both the RFC 3339 string form and the
|
|
||||||
//! legacy bare-integer form are accepted. That keeps a rolling deploy
|
|
||||||
//! safe (an old peer emitting epoch ints into a new reader) and lets
|
|
||||||
//! previously persisted JSON blobs re-load unchanged.
|
|
||||||
//!
|
|
||||||
//! Usage: type timestamp fields as [`WireTime`] / `Option<WireTime>`
|
|
||||||
//! (keep the usual `default` + `skip_serializing_if` attributes on the
|
|
||||||
//! optional form). The type IS the adaptor — no `#[serde(with = …)]`
|
|
||||||
//! needed.
|
|
||||||
|
|
||||||
use chrono::{DateTime, SecondsFormat, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
/// A timestamp on the wire: unix-epoch seconds in Rust, RFC 3339 UTC
|
/// Convert unix-epoch seconds (the sqlite column / input-arg form)
|
||||||
/// string in JSON. Carries "this is a timestamp" in the type system
|
/// into the wire timestamp type. Out-of-range values (never produced
|
||||||
/// instead of a bare `i64` — the module docs above describe the wire
|
/// by our clocks) clamp to the epoch rather than erroring — the db
|
||||||
/// behaviour (ISO out, lenient epoch-or-ISO in).
|
/// read path must not fail on a weird row.
|
||||||
///
|
|
||||||
/// The inner value is public: arithmetic like `now + delay` stays
|
|
||||||
/// plain integer math (`WireTime(now_secs + delay)`), no chrono types
|
|
||||||
/// leak into call sites.
|
|
||||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub struct WireTime(pub i64);
|
|
||||||
|
|
||||||
impl WireTime {
|
|
||||||
/// The wrapped unix-epoch seconds.
|
|
||||||
#[must_use]
|
|
||||||
pub fn secs(self) -> i64 {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// RFC 3339 UTC `Z` string form (same as the serialized shape).
|
|
||||||
#[must_use]
|
|
||||||
pub fn to_iso(self) -> String {
|
|
||||||
to_iso(self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<i64> for WireTime {
|
|
||||||
fn from(secs: i64) -> Self {
|
|
||||||
Self(secs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for WireTime {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_str(&to_iso(self.0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl serde::Serialize for WireTime {
|
|
||||||
fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
|
|
||||||
ser.serialize_str(&to_iso(self.0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for WireTime {
|
|
||||||
fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
|
|
||||||
EpochOrIso::deserialize(de)?.into_secs().map(Self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Format unix-epoch seconds as an RFC 3339 UTC string with a `Z`
|
|
||||||
/// suffix. Out-of-range values (never produced by our clocks) clamp to
|
|
||||||
/// the epoch rather than erroring — serialization must not fail.
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn to_iso(secs: i64) -> String {
|
pub fn from_secs(secs: i64) -> DateTime<Utc> {
|
||||||
DateTime::<Utc>::from_timestamp(secs, 0)
|
DateTime::<Utc>::from_timestamp(secs, 0).unwrap_or_default()
|
||||||
.unwrap_or_default()
|
|
||||||
.to_rfc3339_opts(SecondsFormat::Secs, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse an RFC 3339 string back to unix-epoch seconds. Any UTC offset
|
|
||||||
/// is accepted and normalized.
|
|
||||||
pub fn from_iso(s: &str) -> Result<i64, chrono::ParseError> {
|
|
||||||
Ok(DateTime::parse_from_rfc3339(s)?.timestamp())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lenient wire form: either the legacy epoch integer or the RFC 3339
|
|
||||||
/// string. `untagged` tries the integer first (cheap), then the string.
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
enum EpochOrIso {
|
|
||||||
Epoch(i64),
|
|
||||||
Iso(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EpochOrIso {
|
|
||||||
fn into_secs<E: serde::de::Error>(self) -> Result<i64, E> {
|
|
||||||
match self {
|
|
||||||
Self::Epoch(secs) => Ok(secs),
|
|
||||||
Self::Iso(s) => from_iso(&s).map_err(E::custom),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::WireTime;
|
use super::from_secs;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||||
struct Row {
|
struct Row {
|
||||||
at: WireTime,
|
at: DateTime<Utc>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
maybe_at: Option<WireTime>,
|
maybe_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn serializes_epoch_as_rfc3339_z() {
|
fn serializes_as_rfc3339_z() {
|
||||||
let json = serde_json::to_string(&Row {
|
let json = serde_json::to_string(&Row {
|
||||||
at: WireTime(1_751_480_000),
|
at: from_secs(1_751_480_000),
|
||||||
maybe_at: None,
|
maybe_at: None,
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -128,8 +42,8 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn round_trips_and_serializes_some() {
|
fn round_trips_and_serializes_some() {
|
||||||
let row = Row {
|
let row = Row {
|
||||||
at: WireTime(0),
|
at: from_secs(0),
|
||||||
maybe_at: Some(WireTime(1_751_480_000)),
|
maybe_at: Some(from_secs(1_751_480_000)),
|
||||||
};
|
};
|
||||||
let json = serde_json::to_string(&row).unwrap();
|
let json = serde_json::to_string(&row).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -140,17 +54,13 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn deserializes_legacy_epoch_ints() {
|
fn deserializes_offset_form_normalized_to_utc() {
|
||||||
// Rolling-deploy skew: an old writer still emits bare epoch
|
let row: Row = serde_json::from_str(r#"{"at":"2025-07-02T20:13:20+02:00"}"#).unwrap();
|
||||||
// integers — the lenient reader must accept them.
|
assert_eq!(row.at, from_secs(1_751_480_000));
|
||||||
let row: Row = serde_json::from_str(r#"{"at":1751480000,"maybe_at":1751480000}"#).unwrap();
|
|
||||||
assert_eq!(row.at, WireTime(1_751_480_000));
|
|
||||||
assert_eq!(row.maybe_at, Some(WireTime(1_751_480_000)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn deserializes_offset_form_normalized_to_utc() {
|
fn from_secs_clamps_out_of_range_to_epoch() {
|
||||||
let row: Row = serde_json::from_str(r#"{"at":"2025-07-02T20:13:20+02:00"}"#).unwrap();
|
assert_eq!(from_secs(i64::MAX), from_secs(0));
|
||||||
assert_eq!(row.at, WireTime(1_751_480_000));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue