From 0a2e7daec9ef495a5d4d8a828fdb22143629862b Mon Sep 17 00:00:00 2001 From: thibaud-lclr Date: Thu, 16 Apr 2026 17:23:36 +0200 Subject: [PATCH] fix(db): uniformiser les timestamps sqlite closes #5 --- src-tauri/migrations/001_init.sql | 10 +- src-tauri/migrations/003_add_agents.sql | 4 +- src-tauri/migrations/004_default_agents.sql | 8 +- .../005_orchestration_modules_chat_tasks.sql | 24 ++--- .../007_normalize_timestamps_rfc3339.sql | 93 +++++++++++++++++++ src-tauri/src/db.rs | 7 +- src-tauri/src/models/ticket.rs | 31 +++++-- src-tauri/src/models/tracker.rs | 6 +- src-tauri/src/models/worktree.rs | 11 ++- src-tauri/src/services/poller.rs | 48 +++++++++- 10 files changed, 202 insertions(+), 40 deletions(-) create mode 100644 src-tauri/migrations/007_normalize_timestamps_rfc3339.sql diff --git a/src-tauri/migrations/001_init.sql b/src-tauri/migrations/001_init.sql index d4a2c0a..98519ae 100644 --- a/src-tauri/migrations/001_init.sql +++ b/src-tauri/migrations/001_init.sql @@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS projects ( path TEXT NOT NULL, cloned_from TEXT, base_branch TEXT NOT NULL DEFAULT 'main', - created_at TEXT NOT NULL DEFAULT (datetime('now')) + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) ); CREATE TABLE IF NOT EXISTS tuleap_credentials ( @@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS watched_trackers ( polling_interval INTEGER NOT NULL DEFAULT 10, agent_config_json TEXT NOT NULL, filters_json TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')) + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) ); CREATE TABLE IF NOT EXISTS processed_tickets ( @@ -36,7 +36,7 @@ CREATE TABLE IF NOT EXISTS processed_tickets ( developer_report TEXT, worktree_path TEXT, branch_name TEXT, - detected_at TEXT NOT NULL DEFAULT (datetime('now')), + detected_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), processed_at TEXT ); @@ -49,7 +49,7 @@ CREATE TABLE IF NOT EXISTS worktrees ( path TEXT NOT NULL, branch_name TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'Active', - created_at TEXT NOT NULL DEFAULT (datetime('now')), + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), merged_at TEXT, merged_into TEXT ); @@ -62,5 +62,5 @@ CREATE TABLE IF NOT EXISTS notifications ( title TEXT NOT NULL, message TEXT NOT NULL, read INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL DEFAULT (datetime('now')) + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) ); diff --git a/src-tauri/migrations/003_add_agents.sql b/src-tauri/migrations/003_add_agents.sql index 9a98822..e1b5882 100644 --- a/src-tauri/migrations/003_add_agents.sql +++ b/src-tauri/migrations/003_add_agents.sql @@ -4,8 +4,8 @@ CREATE TABLE IF NOT EXISTS agents ( role TEXT NOT NULL, tool TEXT NOT NULL, custom_prompt TEXT NOT NULL DEFAULT '', - created_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')) + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) ); ALTER TABLE watched_trackers ADD COLUMN analyst_agent_id TEXT; diff --git a/src-tauri/migrations/004_default_agents.sql b/src-tauri/migrations/004_default_agents.sql index 0a2807e..16b513d 100644 --- a/src-tauri/migrations/004_default_agents.sql +++ b/src-tauri/migrations/004_default_agents.sql @@ -12,8 +12,8 @@ SELECT 'codex', '', 1, - datetime('now'), - datetime('now') + strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), + strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE NOT EXISTS ( SELECT 1 FROM agents WHERE role = 'analyst' AND is_default = 1 ); @@ -26,8 +26,8 @@ SELECT 'claude_code', '', 1, - datetime('now'), - datetime('now') + strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), + strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE NOT EXISTS ( SELECT 1 FROM agents WHERE role = 'developer' AND is_default = 1 ); diff --git a/src-tauri/migrations/005_orchestration_modules_chat_tasks.sql b/src-tauri/migrations/005_orchestration_modules_chat_tasks.sql index 3822fad..dd66ee9 100644 --- a/src-tauri/migrations/005_orchestration_modules_chat_tasks.sql +++ b/src-tauri/migrations/005_orchestration_modules_chat_tasks.sql @@ -6,8 +6,8 @@ CREATE TABLE IF NOT EXISTS project_modules ( description TEXT NOT NULL DEFAULT '', enabled INTEGER NOT NULL DEFAULT 1, config_json TEXT NOT NULL DEFAULT '{}', - created_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')), + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), UNIQUE(project_id, module_key) ); @@ -19,8 +19,8 @@ CREATE TABLE IF NOT EXISTS project_live_sessions ( agent_id TEXT NOT NULL REFERENCES agents(id), title TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'active', - created_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')) + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) ); CREATE INDEX IF NOT EXISTS idx_live_sessions_project_id ON project_live_sessions(project_id); @@ -30,7 +30,7 @@ CREATE TABLE IF NOT EXISTS project_live_messages ( session_id TEXT NOT NULL REFERENCES project_live_sessions(id) ON DELETE CASCADE, sender TEXT NOT NULL, content TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')) + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) ); CREATE INDEX IF NOT EXISTS idx_live_messages_session_id ON project_live_messages(session_id); @@ -44,7 +44,7 @@ CREATE TABLE IF NOT EXISTS project_agent_tasks ( status TEXT NOT NULL DEFAULT 'pending', result TEXT, error TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')), + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), started_at TEXT, finished_at TEXT ); @@ -71,8 +71,8 @@ SELECT 'Surveille Tuleap et lance le pipeline analyste/developpeur.', 1, '{}', - datetime('now'), - datetime('now') + strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), + strftime('%Y-%m-%dT%H:%M:%fZ', 'now') FROM projects p; INSERT OR IGNORE INTO project_modules ( @@ -94,8 +94,8 @@ SELECT 'Discussion live avec un agent sur le contexte du projet.', 1, '{}', - datetime('now'), - datetime('now') + strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), + strftime('%Y-%m-%dT%H:%M:%fZ', 'now') FROM projects p; INSERT OR IGNORE INTO project_modules ( @@ -117,6 +117,6 @@ SELECT 'File de tâches asynchrones traitées par des agents pré-définis.', 1, '{}', - datetime('now'), - datetime('now') + strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), + strftime('%Y-%m-%dT%H:%M:%fZ', 'now') FROM projects p; diff --git a/src-tauri/migrations/007_normalize_timestamps_rfc3339.sql b/src-tauri/migrations/007_normalize_timestamps_rfc3339.sql new file mode 100644 index 0000000..8dc8c62 --- /dev/null +++ b/src-tauri/migrations/007_normalize_timestamps_rfc3339.sql @@ -0,0 +1,93 @@ +BEGIN; + +UPDATE projects +SET created_at = strftime('%Y-%m-%dT%H:%M:%fZ', created_at) +WHERE created_at IS NOT NULL + AND strftime('%Y-%m-%dT%H:%M:%fZ', created_at) IS NOT NULL; + +UPDATE watched_trackers +SET created_at = strftime('%Y-%m-%dT%H:%M:%fZ', created_at) +WHERE created_at IS NOT NULL + AND strftime('%Y-%m-%dT%H:%M:%fZ', created_at) IS NOT NULL; + +UPDATE watched_trackers +SET last_polled_at = strftime('%Y-%m-%dT%H:%M:%fZ', last_polled_at) +WHERE last_polled_at IS NOT NULL + AND strftime('%Y-%m-%dT%H:%M:%fZ', last_polled_at) IS NOT NULL; + +UPDATE processed_tickets +SET detected_at = strftime('%Y-%m-%dT%H:%M:%fZ', detected_at) +WHERE detected_at IS NOT NULL + AND strftime('%Y-%m-%dT%H:%M:%fZ', detected_at) IS NOT NULL; + +UPDATE processed_tickets +SET processed_at = strftime('%Y-%m-%dT%H:%M:%fZ', processed_at) +WHERE processed_at IS NOT NULL + AND strftime('%Y-%m-%dT%H:%M:%fZ', processed_at) IS NOT NULL; + +UPDATE worktrees +SET created_at = strftime('%Y-%m-%dT%H:%M:%fZ', created_at) +WHERE created_at IS NOT NULL + AND strftime('%Y-%m-%dT%H:%M:%fZ', created_at) IS NOT NULL; + +UPDATE worktrees +SET merged_at = strftime('%Y-%m-%dT%H:%M:%fZ', merged_at) +WHERE merged_at IS NOT NULL + AND strftime('%Y-%m-%dT%H:%M:%fZ', merged_at) IS NOT NULL; + +UPDATE notifications +SET created_at = strftime('%Y-%m-%dT%H:%M:%fZ', created_at) +WHERE created_at IS NOT NULL + AND strftime('%Y-%m-%dT%H:%M:%fZ', created_at) IS NOT NULL; + +UPDATE agents +SET created_at = strftime('%Y-%m-%dT%H:%M:%fZ', created_at) +WHERE created_at IS NOT NULL + AND strftime('%Y-%m-%dT%H:%M:%fZ', created_at) IS NOT NULL; + +UPDATE agents +SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', updated_at) +WHERE updated_at IS NOT NULL + AND strftime('%Y-%m-%dT%H:%M:%fZ', updated_at) IS NOT NULL; + +UPDATE project_modules +SET created_at = strftime('%Y-%m-%dT%H:%M:%fZ', created_at) +WHERE created_at IS NOT NULL + AND strftime('%Y-%m-%dT%H:%M:%fZ', created_at) IS NOT NULL; + +UPDATE project_modules +SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', updated_at) +WHERE updated_at IS NOT NULL + AND strftime('%Y-%m-%dT%H:%M:%fZ', updated_at) IS NOT NULL; + +UPDATE project_live_sessions +SET created_at = strftime('%Y-%m-%dT%H:%M:%fZ', created_at) +WHERE created_at IS NOT NULL + AND strftime('%Y-%m-%dT%H:%M:%fZ', created_at) IS NOT NULL; + +UPDATE project_live_sessions +SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', updated_at) +WHERE updated_at IS NOT NULL + AND strftime('%Y-%m-%dT%H:%M:%fZ', updated_at) IS NOT NULL; + +UPDATE project_live_messages +SET created_at = strftime('%Y-%m-%dT%H:%M:%fZ', created_at) +WHERE created_at IS NOT NULL + AND strftime('%Y-%m-%dT%H:%M:%fZ', created_at) IS NOT NULL; + +UPDATE project_agent_tasks +SET created_at = strftime('%Y-%m-%dT%H:%M:%fZ', created_at) +WHERE created_at IS NOT NULL + AND strftime('%Y-%m-%dT%H:%M:%fZ', created_at) IS NOT NULL; + +UPDATE project_agent_tasks +SET started_at = strftime('%Y-%m-%dT%H:%M:%fZ', started_at) +WHERE started_at IS NOT NULL + AND strftime('%Y-%m-%dT%H:%M:%fZ', started_at) IS NOT NULL; + +UPDATE project_agent_tasks +SET finished_at = strftime('%Y-%m-%dT%H:%M:%fZ', finished_at) +WHERE finished_at IS NOT NULL + AND strftime('%Y-%m-%dT%H:%M:%fZ', finished_at) IS NOT NULL; + +COMMIT; diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 3fc8a4b..cc37589 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -7,6 +7,7 @@ const MIGRATION_003: &str = include_str!("../migrations/003_add_agents.sql"); const MIGRATION_004: &str = include_str!("../migrations/004_default_agents.sql"); const MIGRATION_005: &str = include_str!("../migrations/005_orchestration_modules_chat_tasks.sql"); const MIGRATION_006: &str = include_str!("../migrations/006_processed_tickets_unique_index.sql"); +const MIGRATION_007: &str = include_str!("../migrations/007_normalize_timestamps_rfc3339.sql"); pub fn init(db_path: &Path) -> Result { let conn = Connection::open(db_path)?; @@ -56,6 +57,10 @@ fn migrate(conn: &Connection) -> Result<()> { conn.execute_batch(MIGRATION_006)?; conn.pragma_update(None, "user_version", 6)?; } + if version < 7 { + conn.execute_batch(MIGRATION_007)?; + conn.pragma_update(None, "user_version", 7)?; + } Ok(()) } @@ -111,7 +116,7 @@ mod tests { let version: i32 = conn .pragma_query_value(None, "user_version", |row| row.get(0)) .unwrap(); - assert_eq!(version, 6); + assert_eq!(version, 7); } #[test] diff --git a/src-tauri/src/models/ticket.rs b/src-tauri/src/models/ticket.rs index 2ff2653..8ae053f 100644 --- a/src-tauri/src/models/ticket.rs +++ b/src-tauri/src/models/ticket.rs @@ -146,9 +146,10 @@ impl ProcessedTicket { } pub fn set_developer_report(conn: &Connection, id: &str, report: &str) -> Result<()> { + let now = chrono::Utc::now().to_rfc3339(); conn.execute( - "UPDATE processed_tickets SET developer_report = ?1, processed_at = datetime('now') WHERE id = ?2", - params![report, id], + "UPDATE processed_tickets SET developer_report = ?1, processed_at = ?2 WHERE id = ?3", + params![report, now, id], )?; Ok(()) } @@ -177,9 +178,10 @@ impl ProcessedTicket { } pub fn set_error(conn: &Connection, id: &str, error_message: &str) -> Result<()> { + let now = chrono::Utc::now().to_rfc3339(); conn.execute( - "UPDATE processed_tickets SET status = 'Error', analyst_report = COALESCE(analyst_report, '') || ?1, processed_at = datetime('now') WHERE id = ?2", - params![error_message, id], + "UPDATE processed_tickets SET status = 'Error', analyst_report = COALESCE(analyst_report, '') || ?1, processed_at = ?2 WHERE id = ?3", + params![error_message, now, id], )?; Ok(()) } @@ -378,8 +380,15 @@ mod tests { ProcessedTicket::set_developer_report(&conn, &ticket.id, "Fixed in main.rs").unwrap(); let updated = ProcessedTicket::get_by_id(&conn, &ticket.id).unwrap(); - assert_eq!(updated.developer_report.unwrap(), "Fixed in main.rs"); - assert!(updated.processed_at.is_some()); + assert_eq!( + updated.developer_report.as_deref(), + Some("Fixed in main.rs") + ); + let processed_at = updated + .processed_at + .as_deref() + .expect("processed_at should be set"); + assert!(chrono::DateTime::parse_from_rfc3339(processed_at).is_ok()); } #[test] @@ -422,6 +431,14 @@ mod tests { ProcessedTicket::set_error(&conn, &ticket.id, "CLI timeout after 600s").unwrap(); let updated = ProcessedTicket::get_by_id(&conn, &ticket.id).unwrap(); assert_eq!(updated.status, "Error"); - assert_eq!(updated.analyst_report.unwrap(), "CLI timeout after 600s"); + assert_eq!( + updated.analyst_report.as_deref(), + Some("CLI timeout after 600s") + ); + let processed_at = updated + .processed_at + .as_deref() + .expect("processed_at should be set"); + assert!(chrono::DateTime::parse_from_rfc3339(processed_at).is_ok()); } } diff --git a/src-tauri/src/models/tracker.rs b/src-tauri/src/models/tracker.rs index f04f2d2..23a9858 100644 --- a/src-tauri/src/models/tracker.rs +++ b/src-tauri/src/models/tracker.rs @@ -516,7 +516,11 @@ mod tests { .expect("update_last_polled should succeed"); let updated = WatchedTracker::get_by_id(&conn, &created.id).unwrap(); - assert!(updated.last_polled_at.is_some()); + let last_polled_at = updated + .last_polled_at + .as_deref() + .expect("last_polled_at should be set"); + assert!(chrono::DateTime::parse_from_rfc3339(last_polled_at).is_ok()); } #[test] diff --git a/src-tauri/src/models/worktree.rs b/src-tauri/src/models/worktree.rs index f704005..709109c 100644 --- a/src-tauri/src/models/worktree.rs +++ b/src-tauri/src/models/worktree.rs @@ -88,9 +88,10 @@ impl Worktree { } pub fn set_merged(conn: &Connection, id: &str, target_branch: &str) -> Result<()> { + let now = chrono::Utc::now().to_rfc3339(); conn.execute( - "UPDATE worktrees SET status = 'Merged', merged_at = datetime('now'), merged_into = ?1 WHERE id = ?2", - params![target_branch, id], + "UPDATE worktrees SET status = 'Merged', merged_at = ?1, merged_into = ?2 WHERE id = ?3", + params![now, target_branch, id], )?; Ok(()) } @@ -206,7 +207,11 @@ mod tests { let updated = Worktree::get_by_id(&conn, &wt.id).unwrap(); assert_eq!(updated.status, "Merged"); assert_eq!(updated.merged_into.unwrap(), "feature/login"); - assert!(updated.merged_at.is_some()); + let merged_at = updated + .merged_at + .as_deref() + .expect("merged_at should be set"); + assert!(chrono::DateTime::parse_from_rfc3339(merged_at).is_ok()); } #[test] diff --git a/src-tauri/src/services/poller.rs b/src-tauri/src/services/poller.rs index 341273b..46e9477 100644 --- a/src-tauri/src/services/poller.rs +++ b/src-tauri/src/services/poller.rs @@ -84,12 +84,12 @@ fn should_poll(tracker: &WatchedTracker) -> bool { Some(s) => s, }; - let last = match chrono::DateTime::parse_from_rfc3339(last_polled_at) { - Ok(dt) => dt, - Err(e) => { + let last = match parse_timestamp(last_polled_at) { + Some(dt) => dt, + None => { eprintln!( - "poller: failed to parse last_polled_at '{}': {}", - last_polled_at, e + "poller: failed to parse last_polled_at '{}': unsupported format", + last_polled_at ); return true; // Treat as never polled on parse error } @@ -99,6 +99,21 @@ fn should_poll(tracker: &WatchedTracker) -> bool { elapsed >= tracker.polling_interval as i64 } +fn parse_timestamp(value: &str) -> Option> { + if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(value) { + return Some(dt.with_timezone(&chrono::Utc)); + } + + if let Ok(naive) = chrono::NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S%.f") { + return Some(chrono::DateTime::::from_naive_utc_and_offset( + naive, + chrono::Utc, + )); + } + + None +} + async fn poll_single_tracker( db: &Arc>, client: &TuleapClient, @@ -230,3 +245,26 @@ async fn poll_single_tracker( }), ); } + +#[cfg(test)] +mod tests { + use super::parse_timestamp; + + #[test] + fn parse_timestamp_supports_rfc3339() { + let parsed = parse_timestamp("2026-04-16T10:15:30.123Z"); + assert!(parsed.is_some()); + } + + #[test] + fn parse_timestamp_supports_legacy_sqlite_datetime() { + let parsed = parse_timestamp("2026-04-16 10:15:30"); + assert!(parsed.is_some()); + } + + #[test] + fn parse_timestamp_rejects_invalid_values() { + let parsed = parse_timestamp("not-a-date"); + assert!(parsed.is_none()); + } +}