Skip to main content

capsule_core/config/
log.rs

1use std::fmt;
2use std::sync::mpsc;
3use std::thread::{Builder, JoinHandle};
4
5use nanoid::nanoid;
6use serde::{Deserialize, Serialize};
7use tokio::sync::oneshot;
8
9use crate::config::database::{Database, DatabaseError};
10
11#[derive(Debug)]
12pub enum LogError {
13    DatabaseError(String),
14    WalLoggerDied(String),
15    LogResponseLost(String),
16}
17
18impl fmt::Display for LogError {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        match self {
21            LogError::DatabaseError(msg) => write!(f, "Log error > {}", msg),
22            LogError::WalLoggerDied(msg) => write!(f, "Log error > WAL logger died > {}", msg),
23            LogError::LogResponseLost(msg) => write!(f, "Log error > Log response lost > {}", msg),
24        }
25    }
26}
27
28impl From<DatabaseError> for LogError {
29    fn from(err: DatabaseError) -> Self {
30        LogError::DatabaseError(err.to_string())
31    }
32}
33impl From<mpsc::SendError<LogCommand>> for LogError {
34    fn from(err: mpsc::SendError<LogCommand>) -> Self {
35        LogError::WalLoggerDied(err.to_string())
36    }
37}
38
39impl From<oneshot::error::RecvError> for LogError {
40    fn from(err: oneshot::error::RecvError) -> Self {
41        LogError::LogResponseLost(err.to_string())
42    }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(rename_all = "snake_case")]
47pub enum InstanceState {
48    Created,
49    Running,
50    Completed,
51    Failed,
52    Interrupted,
53    TimedOut,
54}
55
56impl fmt::Display for InstanceState {
57    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
58        let state_str = match self {
59            InstanceState::Created => "created",
60            InstanceState::Running => "running",
61            InstanceState::Completed => "completed",
62            InstanceState::Failed => "failed",
63            InstanceState::Interrupted => "interrupted",
64            InstanceState::TimedOut => "timed_out",
65        };
66        write!(f, "{}", state_str)
67    }
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct InstanceLog {
72    pub id: String,
73    pub agent_name: String,
74    pub agent_version: String,
75    pub task_id: String,
76    pub task_name: String,
77    pub state: InstanceState,
78    pub fuel_limit: u64,
79    pub fuel_consumed: u64,
80    pub created_at: i64,
81    pub updated_at: i64,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct CreateInstanceLog {
86    pub agent_name: String,
87    pub agent_version: String,
88    pub task_id: String,
89    pub task_name: String,
90    pub state: InstanceState,
91    pub fuel_limit: u64,
92    pub fuel_consumed: u64,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct UpdateInstanceLog {
97    pub task_id: String,
98    pub state: InstanceState,
99    pub fuel_consumed: u64,
100}
101
102enum LogCommand {
103    Create {
104        log: CreateInstanceLog,
105        response: tokio::sync::oneshot::Sender<Result<(), LogError>>,
106    },
107
108    Update {
109        log: UpdateInstanceLog,
110        response: tokio::sync::oneshot::Sender<Result<(), LogError>>,
111    },
112}
113
114#[derive()]
115pub struct Log {
116    pub db: Database,
117    log_tx: mpsc::Sender<LogCommand>,
118    _log_handle: JoinHandle<()>,
119}
120
121impl Log {
122    pub fn new(path: Option<&str>, database_name: &str) -> Result<Self, LogError> {
123        let db = Database::new(path, database_name)?;
124
125        Self::ensure_schema(&db)?;
126
127        let (log_tx, log_handle) = Self::spawn_wal_worker(db.clone());
128
129        Ok(Self {
130            db,
131            log_tx,
132            _log_handle: log_handle,
133        })
134    }
135
136    fn ensure_schema(db: &Database) -> Result<(), LogError> {
137        let table_exists = db.table_exists("instance_log")?;
138
139        if !table_exists {
140            db.create_table(
141                "instance_log",
142                &[
143                    "agent_name TEXT NOT NULL",
144                    "agent_version TEXT NOT NULL",
145                    "task_id TEXT NOT NULL",
146                    "task_name TEXT NOT NULL",
147                    "state TEXT NOT NULL",
148                    "fuel_limit INTEGER NOT NULL",
149                    "fuel_consumed INTEGER NOT NULL",
150                ],
151                &[],
152            )?;
153
154            db.execute(
155                "CREATE INDEX IF NOT EXISTS idx_instance_log_task_id ON instance_log(task_id)",
156                [],
157            )?;
158
159            db.execute(
160                "CREATE INDEX IF NOT EXISTS idx_instance_log_created_at ON instance_log(created_at)",
161                [],
162            )?;
163        }
164
165        Ok(())
166    }
167
168    fn spawn_wal_worker(db: Database) -> (mpsc::Sender<LogCommand>, JoinHandle<()>) {
169        let (tx, rx) = mpsc::channel();
170
171        let handle = Builder::new()
172            .name("wal-logger".to_string())
173            .spawn(move || {
174                Self::wal_worker_loop(db, rx);
175            })
176            .expect("Failed to spawn WAL logger thread");
177
178        (tx, handle)
179    }
180
181    fn wal_worker_loop(db: Database, rx: mpsc::Receiver<LogCommand>) {
182        while let Ok(cmd) = rx.recv() {
183            match cmd {
184                LogCommand::Create { log, response } => {
185                    let result = Self::execute_create(&db, log);
186                    let _ = response.send(result);
187                }
188                LogCommand::Update { log, response } => {
189                    let result = Self::execute_update(&db, log);
190                    let _ = response.send(result);
191                }
192            }
193        }
194    }
195
196    fn execute_create(db: &Database, log: CreateInstanceLog) -> Result<(), LogError> {
197        db.execute(
198            "INSERT INTO instance_log (id, agent_name, agent_version, task_id, task_name, state, fuel_limit, fuel_consumed) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
199            [
200                &nanoid!(10),
201                &log.agent_name,
202                &log.agent_version,
203                &log.task_id,
204                &log.task_name,
205                &log.state.to_string(),
206                &log.fuel_limit.to_string(),
207                &log.fuel_consumed.to_string(),
208            ],
209        )?;
210
211        Ok(())
212    }
213
214    fn execute_update(db: &Database, log: UpdateInstanceLog) -> Result<(), LogError> {
215        db.execute(
216            "UPDATE instance_log SET state = ?, fuel_consumed = ? WHERE task_id = ?",
217            [
218                &log.state.to_string(),
219                &log.fuel_consumed.to_string(),
220                &log.task_id,
221            ],
222        )?;
223
224        Ok(())
225    }
226
227    pub async fn commit_log(&self, log: CreateInstanceLog) -> Result<(), LogError> {
228        let (tx, rx) = oneshot::channel();
229
230        self.log_tx.send(LogCommand::Create { log, response: tx })?;
231
232        rx.await?
233    }
234
235    pub async fn update_log(&self, log: UpdateInstanceLog) -> Result<(), LogError> {
236        let (tx, rx) = oneshot::channel();
237
238        self.log_tx.send(LogCommand::Update { log, response: tx })?;
239
240        rx.await?
241    }
242
243    pub fn get_logs(&self) -> Result<Vec<InstanceLog>, LogError> {
244        let logs = self.db.query(
245            "SELECT id, agent_name, agent_version, task_id, task_name, state, fuel_limit, fuel_consumed, created_at, updated_at FROM instance_log ORDER BY created_at DESC",
246            [],
247            |row| {
248                let id_str: String = row.get(0)?;
249                let state_str: String = row.get(5)?;
250                Ok(InstanceLog {
251                    id: id_str,
252                    agent_name: row.get(1)?,
253                    agent_version: row.get(2)?,
254                    task_id: row.get(3)?,
255                    task_name: row.get(4)?,
256                    state: match state_str.as_str() {
257                        "created" => InstanceState::Created,
258                        "running" => InstanceState::Running,
259                        "completed" => InstanceState::Completed,
260                        "failed" => InstanceState::Failed,
261                        "interrupted" => InstanceState::Interrupted,
262                        _ => return Err(DatabaseError::InvalidQuery(format!("Invalid state: {}", state_str))),
263                    },
264                    fuel_limit: row.get::<_, i64>(6)? as u64,
265                    fuel_consumed: row.get::<_, i64>(7)? as u64,
266                    created_at: row.get(8)?,
267                    updated_at: row.get(9)?,
268                })
269            }
270        )?;
271
272        Ok(logs)
273    }
274
275    pub fn clear_logs(&self) -> Result<(), LogError> {
276        let logs = self.get_logs()?;
277
278        for log in logs {
279            if log.state != InstanceState::Completed && log.state != InstanceState::Running {
280                self.delete_log(&log.task_id)?;
281            }
282        }
283
284        Ok(())
285    }
286
287    pub fn delete_log(&self, task_id: &str) -> Result<(), LogError> {
288        self.db
289            .execute("DELETE FROM instance_log WHERE task_id = ?", [task_id])?;
290
291        Ok(())
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298    fn run_async<F>(future: F) -> F::Output
299    where
300        F: std::future::Future,
301    {
302        let rt = tokio::runtime::Builder::new_current_thread()
303            .enable_all()
304            .build()
305            .expect("Failed to create runtime");
306        rt.block_on(future)
307    }
308
309    mod creation {
310        use super::*;
311
312        #[test]
313        fn test_new_log() {
314            let log = Log::new(None, "trace.db-wal").unwrap();
315
316            let conn = log.db.conn.lock().unwrap();
317
318            let mut stmt = conn
319                .prepare(
320                    "SELECT name FROM sqlite_master WHERE type='table' AND name='instance_log'",
321                )
322                .expect("Failed to prepare query");
323
324            let mut index_stmt = conn
325                .prepare("SELECT name FROM sqlite_master WHERE type='index' AND name='idx_instance_log_task_id'")
326                .expect("Failed to prepare query");
327
328            let exists = stmt.exists([]).expect("Failed to check if table exists");
329
330            let index_exists: bool = index_stmt
331                .exists([])
332                .expect("Failed to check if index exists");
333
334            assert!(exists, "Table instance_log does not exist");
335            assert!(
336                index_exists,
337                "Index idx_instance_log_task_id does not exist"
338            );
339        }
340
341        #[test]
342        fn test_commit_log() {
343            let log = Log::new(None, "trace.db-wal").unwrap();
344
345            run_async(async {
346                log.commit_log(CreateInstanceLog {
347                    agent_name: "agent_name".to_string(),
348                    agent_version: "agent_version".to_string(),
349                    task_id: "task_id".to_string(),
350                    task_name: "task_name".to_string(),
351                    state: InstanceState::Created,
352                    fuel_limit: 100,
353                    fuel_consumed: 0,
354                })
355                .await
356                .expect("Failed to commit log");
357            });
358
359            let conn = log.db.conn.lock().unwrap();
360
361            let mut stmt = conn
362                .prepare("SELECT task_name FROM instance_log WHERE task_id = 'task_id'")
363                .expect("Failed to prepare query");
364
365            let exists = stmt.exists([]).expect("Failed to check if instance exists");
366
367            assert!(exists, "instance does not exist");
368        }
369    }
370
371    mod update {
372        use super::*;
373
374        #[test]
375        fn test_update_log() {
376            let log = Log::new(None, "trace.db-wal").unwrap();
377
378            {
379                let conn = log.db.conn.lock().unwrap();
380
381                conn.execute("INSERT INTO instance_log (id, agent_name, agent_version, task_id, task_name, state, fuel_limit, fuel_consumed) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
382                    &nanoid!(10),
383                    "test_agent",
384                    "1.0.0",
385                    "test_task_123",
386                    "Test Task",
387                    "created",
388                    "15000000",
389                    "0",
390                ]).expect("Failed to insert test data");
391            }
392
393            run_async(async {
394                log.update_log(UpdateInstanceLog {
395                    task_id: "test_task_123".to_string(),
396                    state: InstanceState::Running,
397                    fuel_consumed: 10,
398                })
399                .await
400                .expect("Failed to update log");
401            });
402
403            let conn = log.db.conn.lock().unwrap();
404
405            let state: String = conn
406                .query_row(
407                    "SELECT state FROM instance_log WHERE task_id = 'test_task_123'",
408                    [],
409                    |row| row.get(0),
410                )
411                .expect("Failed to query state");
412
413            let fuel_consumed: i64 = conn
414                .query_row(
415                    "SELECT fuel_consumed FROM instance_log WHERE task_id = 'test_task_123'",
416                    [],
417                    |row| row.get(0),
418                )
419                .expect("Failed to query fuel_consumed");
420
421            assert_eq!(state, "running", "State should be updated to running");
422            assert_eq!(fuel_consumed, 10, "Fuel consumed should be updated to 10");
423        }
424    }
425
426    mod deletion {
427        use super::*;
428
429        #[test]
430        fn test_delete_log() {
431            let log = Log::new(None, "trace.db-wal").unwrap();
432
433            {
434                let conn = log.db.conn.lock().unwrap();
435
436                conn.execute("INSERT INTO instance_log (id, agent_name, agent_version, task_id, task_name, state, fuel_limit, fuel_consumed, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
437                    &nanoid!(10),
438                    "test_agent",
439                    "1.0.0",
440                    "task_to_delete",
441                    "Task To Delete",
442                    "created",
443                    "10000",
444                    "0",
445                    "1000",
446                    "1000",
447                ]).expect("Failed to insert first test log");
448
449                conn.execute("INSERT INTO instance_log (id, agent_name, agent_version, task_id, task_name, state, fuel_limit, fuel_consumed, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
450                    &nanoid!(10),
451                    "test_agent",
452                    "1.0.0",
453                    "task_to_delete",
454                    "Task To Delete",
455                    "running",
456                    "10000",
457                    "5000",
458                    "2000",
459                    "2000",
460                ]).expect("Failed to insert second test log");
461
462                conn.execute("INSERT INTO instance_log (id, agent_name, agent_version, task_id, task_name, state, fuel_limit, fuel_consumed, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
463                    &nanoid!(10),
464                    "other_agent",
465                    "2.0.0",
466                    "task_to_keep",
467                    "Task To Keep",
468                    "completed",
469                    "5000",
470                    "2500",
471                    "1500",
472                    "1500",
473                ]).expect("Failed to insert task to keep");
474            }
475
476            let conn = log.db.conn.lock().unwrap();
477
478            let count_before: i64 = conn
479                .query_row(
480                    "SELECT COUNT(*) FROM instance_log WHERE task_id = 'task_to_delete'",
481                    [],
482                    |row| row.get(0),
483                )
484                .expect("Failed to count logs before deletion");
485            assert_eq!(
486                count_before, 2,
487                "Expected 2 logs for task_to_delete before deletion"
488            );
489
490            drop(conn);
491
492            log.delete_log("task_to_delete")
493                .expect("Failed to delete logs");
494
495            let conn = log.db.conn.lock().unwrap();
496
497            let count_after: i64 = conn
498                .query_row(
499                    "SELECT COUNT(*) FROM instance_log WHERE task_id = 'task_to_delete'",
500                    [],
501                    |row| row.get(0),
502                )
503                .expect("Failed to count logs after deletion");
504            assert_eq!(
505                count_after, 0,
506                "Expected 0 logs for task_to_delete after deletion"
507            );
508
509            let count_kept: i64 = conn
510                .query_row(
511                    "SELECT COUNT(*) FROM instance_log WHERE task_id = 'task_to_keep'",
512                    [],
513                    |row| row.get(0),
514                )
515                .expect("Failed to count kept logs");
516            assert_eq!(count_kept, 1, "Expected 1 log for task_to_keep to remain");
517
518            let kept_state: String = conn
519                .query_row(
520                    "SELECT state FROM instance_log WHERE task_id = 'task_to_keep'",
521                    [],
522                    |row| row.get(0),
523                )
524                .expect("Failed to query kept log state");
525            assert_eq!(
526                kept_state, "completed",
527                "Kept log should have correct state"
528            );
529        }
530
531        #[test]
532        fn test_clear_logs() {
533            let log = Log::new(None, "trace.db-wal").unwrap();
534
535            {
536                let conn = log.db.conn.lock().unwrap();
537
538                conn.execute("INSERT INTO instance_log (id, agent_name, agent_version, task_id, task_name, state, fuel_limit, fuel_consumed, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
539                    &nanoid!(10),
540                    "test_agent",
541                    "1.0.0",
542                    "task_created",
543                    "Task Created",
544                    "created",
545                    "10000",
546                    "0",
547                    "1000",
548                    "1000",
549                ]).expect("Failed to insert created log");
550
551                conn.execute("INSERT INTO instance_log (id, agent_name, agent_version, task_id, task_name, state, fuel_limit, fuel_consumed, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
552                    &nanoid!(10),
553                    "test_agent",
554                    "1.0.0",
555                    "task_running",
556                    "Task Running",
557                    "running",
558                    "10000",
559                    "5000",
560                    "2000",
561                    "2000",
562                ]).expect("Failed to insert running log");
563
564                conn.execute("INSERT INTO instance_log (id, agent_name, agent_version, task_id, task_name, state, fuel_limit, fuel_consumed, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
565                    &nanoid!(10),
566                    "test_agent",
567                    "1.0.0",
568                    "task_completed",
569                    "Task Completed",
570                    "completed",
571                    "10000",
572                    "8500",
573                    "3000",
574                    "3000",
575                ]).expect("Failed to insert completed log");
576
577                conn.execute("INSERT INTO instance_log (id, agent_name, agent_version, task_id, task_name, state, fuel_limit, fuel_consumed, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
578                    &nanoid!(10),
579                    "test_agent",
580                    "1.0.0",
581                    "task_failed",
582                    "Task Failed",
583                    "failed",
584                    "5000",
585                    "2500",
586                    "1500",
587                    "1500",
588                ]).expect("Failed to insert failed log");
589
590                conn.execute("INSERT INTO instance_log (id, agent_name, agent_version, task_id, task_name, state, fuel_limit, fuel_consumed, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
591                    &nanoid!(10),
592                    "test_agent",
593                    "1.0.0",
594                    "task_interrupted",
595                    "Task Interrupted",
596                    "interrupted",
597                    "7000",
598                    "3000",
599                    "2500",
600                    "2500",
601                ]).expect("Failed to insert interrupted log");
602            }
603
604            let conn = log.db.conn.lock().unwrap();
605
606            let count_before: i64 = conn
607                .query_row("SELECT COUNT(*) FROM instance_log", [], |row| row.get(0))
608                .expect("Failed to count logs before clear");
609            assert_eq!(count_before, 5, "Expected 5 logs before clear");
610
611            drop(conn);
612
613            log.clear_logs().expect("Failed to clear logs");
614
615            let conn = log.db.conn.lock().unwrap();
616
617            let count_after: i64 = conn
618                .query_row("SELECT COUNT(*) FROM instance_log", [], |row| row.get(0))
619                .expect("Failed to count logs after clear");
620            assert_eq!(
621                count_after, 2,
622                "Expected 2 logs after clear (running and completed)"
623            );
624
625            let running_exists: bool = conn
626                .query_row(
627                    "SELECT COUNT(*) FROM instance_log WHERE task_id = 'task_running'",
628                    [],
629                    |row| {
630                        let count: i64 = row.get(0)?;
631                        Ok(count > 0)
632                    },
633                )
634                .expect("Failed to check running log");
635            assert!(running_exists, "Running log should still exist");
636
637            let completed_exists: bool = conn
638                .query_row(
639                    "SELECT COUNT(*) FROM instance_log WHERE task_id = 'task_completed'",
640                    [],
641                    |row| {
642                        let count: i64 = row.get(0)?;
643                        Ok(count > 0)
644                    },
645                )
646                .expect("Failed to check completed log");
647            assert!(completed_exists, "Completed log should still exist");
648
649            let created_exists: bool = conn
650                .query_row(
651                    "SELECT COUNT(*) FROM instance_log WHERE task_id = 'task_created'",
652                    [],
653                    |row| {
654                        let count: i64 = row.get(0)?;
655                        Ok(count > 0)
656                    },
657                )
658                .expect("Failed to check created log");
659            assert!(!created_exists, "Created log should be deleted");
660
661            let failed_exists: bool = conn
662                .query_row(
663                    "SELECT COUNT(*) FROM instance_log WHERE task_id = 'task_failed'",
664                    [],
665                    |row| {
666                        let count: i64 = row.get(0)?;
667                        Ok(count > 0)
668                    },
669                )
670                .expect("Failed to check failed log");
671            assert!(!failed_exists, "Failed log should be deleted");
672
673            let interrupted_exists: bool = conn
674                .query_row(
675                    "SELECT COUNT(*) FROM instance_log WHERE task_id = 'task_interrupted'",
676                    [],
677                    |row| {
678                        let count: i64 = row.get(0)?;
679                        Ok(count > 0)
680                    },
681                )
682                .expect("Failed to check interrupted log");
683            assert!(!interrupted_exists, "Interrupted log should be deleted");
684        }
685    }
686
687    mod queries {
688        use super::*;
689
690        #[test]
691        fn test_get_logs() {
692            let log = Log::new(None, "trace.db-wal").unwrap();
693
694            {
695                let conn = log.db.conn.lock().unwrap();
696
697                conn.execute("INSERT INTO instance_log (id, agent_name, agent_version, task_id, task_name, state, fuel_limit, fuel_consumed, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
698                &nanoid!(10),
699                "test_agent",
700                "1.0.0",
701                "test_task_123",
702                "Test Task",
703                "created",
704                "10000",
705                "0",
706                "1000",
707                "1000",
708            ]).expect("Failed to insert first test log");
709
710                conn.execute("INSERT INTO instance_log (id, agent_name, agent_version, task_id, task_name, state, fuel_limit, fuel_consumed, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
711                &nanoid!(10),
712                "test_agent",
713                "1.0.0",
714                "test_task_123",
715                "Test Task",
716                "running",
717                "10000",
718                "5000",
719                "2000",
720                "2000",
721            ]).expect("Failed to insert second test log");
722
723                conn.execute("INSERT INTO instance_log (id, agent_name, agent_version, task_id, task_name, state, fuel_limit, fuel_consumed, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
724                &nanoid!(10),
725                "test_agent",
726                "1.0.0",
727                "test_task_123",
728                "Test Task",
729                "completed",
730                "10000",
731                "8500",
732                "3000",
733                "3000",
734            ]).expect("Failed to insert third test log");
735
736                conn.execute("INSERT INTO instance_log (id, agent_name, agent_version, task_id, task_name, state, fuel_limit, fuel_consumed, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
737                &nanoid!(10),
738                "other_agent",
739                "2.0.0",
740                "other_task_456",
741                "Other Task",
742                "failed",
743                "5000",
744                "2500",
745                "1500",
746                "1500",
747            ]).expect("Failed to insert other task log");
748            }
749
750            let logs = log.get_logs().expect("Failed to get logs");
751
752            assert_eq!(logs.len(), 4, "Expected 4 total logs");
753
754            assert_eq!(
755                logs[0].state.to_string(),
756                "completed",
757                "First log should be completed"
758            );
759            assert_eq!(
760                logs[0].fuel_consumed, 8500,
761                "First log fuel_consumed should be 8500"
762            );
763            assert_eq!(
764                logs[0].created_at, 3000,
765                "First log created_at should be 3000"
766            );
767
768            assert_eq!(
769                logs[1].state.to_string(),
770                "running",
771                "Second log should be running"
772            );
773            assert_eq!(
774                logs[1].fuel_consumed, 5000,
775                "Second log fuel_consumed should be 5000"
776            );
777            assert_eq!(
778                logs[1].created_at, 2000,
779                "Second log created_at should be 2000"
780            );
781
782            assert_eq!(
783                logs[2].task_id, "other_task_456",
784                "Third log should be other_task_456"
785            );
786            assert_eq!(
787                logs[2].state.to_string(),
788                "failed",
789                "Third log should be failed"
790            );
791            assert_eq!(
792                logs[2].created_at, 1500,
793                "Third log created_at should be 1500"
794            );
795
796            assert_eq!(
797                logs[3].task_id, "test_task_123",
798                "Fourth log should be test_task_123"
799            );
800            assert_eq!(
801                logs[3].state.to_string(),
802                "created",
803                "Fourth log should be created"
804            );
805            assert_eq!(
806                logs[3].fuel_consumed, 0,
807                "Fourth log fuel_consumed should be 0"
808            );
809            assert_eq!(
810                logs[3].created_at, 1000,
811                "Fourth log created_at should be 1000"
812            );
813
814            let test_task_logs: Vec<_> = logs
815                .iter()
816                .filter(|l| l.task_id == "test_task_123")
817                .collect();
818            assert_eq!(test_task_logs.len(), 3, "Expected 3 logs for test_task_123");
819
820            for log_entry in test_task_logs {
821                assert_eq!(
822                    log_entry.agent_name, "test_agent",
823                    "test_task_123 logs should have agent_name test_agent"
824                );
825                assert_eq!(
826                    log_entry.task_name, "Test Task",
827                    "test_task_123 logs should have task_name Test Task"
828                );
829            }
830
831            let other_task_logs: Vec<_> = logs
832                .iter()
833                .filter(|l| l.task_id == "other_task_456")
834                .collect();
835            assert_eq!(
836                other_task_logs.len(),
837                1,
838                "Expected 1 log for other_task_456"
839            );
840            assert_eq!(
841                other_task_logs[0].agent_name, "other_agent",
842                "other_task_456 log should have agent_name other_agent"
843            );
844        }
845    }
846}