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}