1use super::{
36 DegradationEntry, RunId, RunRecord, RunStatus, RunStore, RunStoreError, StepEntry, TaskId,
37};
38use async_trait::async_trait;
39use rusqlite::{params, OptionalExtension};
40use rusqlite_isle::{AsyncIsle, AsyncIsleDriver, IsleError};
41use std::path::Path;
42
43const SCHEMA_SQL: &str = "\
44CREATE TABLE IF NOT EXISTS runs (\
45 id TEXT PRIMARY KEY, \
46 task_id TEXT NOT NULL, \
47 status TEXT NOT NULL, \
48 step_entries_json TEXT NOT NULL, \
49 degradations_json TEXT NOT NULL DEFAULT '[]', \
50 operator_sid TEXT, \
51 result_ref_json TEXT, \
52 created_at INTEGER NOT NULL, \
53 updated_at INTEGER NOT NULL\
54);\
55CREATE INDEX IF NOT EXISTS ix_runs_task_id ON runs(task_id, created_at);\
56";
57
58fn migrate_degradations_column(conn: &rusqlite::Connection) -> rusqlite::Result<()> {
63 let mut stmt = conn.prepare("PRAGMA table_info(runs)")?;
64 let has_column = stmt
65 .query_map([], |row| row.get::<_, String>(1))?
66 .collect::<Result<Vec<String>, _>>()?
67 .iter()
68 .any(|name| name == "degradations_json");
69 if !has_column {
70 conn.execute_batch(
71 "ALTER TABLE runs ADD COLUMN degradations_json TEXT NOT NULL DEFAULT '[]';",
72 )?;
73 }
74 Ok(())
75}
76
77pub struct SqliteRunStore {
85 isle: AsyncIsle,
86}
87
88impl SqliteRunStore {
89 pub async fn open(path: impl AsRef<Path>) -> Result<(Self, AsyncIsleDriver), RunStoreError> {
92 let (isle, driver) = AsyncIsle::spawn(path.as_ref().to_path_buf(), |conn| {
93 conn.execute_batch(SCHEMA_SQL)?;
94 migrate_degradations_column(conn)
95 })
96 .await
97 .map_err(map_isle_err)?;
98 Ok((Self { isle }, driver))
99 }
100
101 pub async fn open_in_memory() -> Result<(Self, AsyncIsleDriver), RunStoreError> {
103 let (isle, driver) = AsyncIsle::open_in_memory(|conn| {
104 conn.execute_batch(SCHEMA_SQL)?;
105 migrate_degradations_column(conn)
106 })
107 .await
108 .map_err(map_isle_err)?;
109 Ok((Self { isle }, driver))
110 }
111}
112
113fn map_isle_err(e: IsleError) -> RunStoreError {
114 RunStoreError::Other(format!("sqlite: {e}"))
115}
116
117type RunRow = (
121 String,
122 String,
123 String,
124 String,
125 String,
126 Option<String>,
127 Option<String>,
128 i64,
129 i64,
130);
131
132const RUN_SELECT_COLUMNS: &str = "id, task_id, status, step_entries_json, degradations_json, \
133 operator_sid, result_ref_json, created_at, updated_at";
134
135fn row_to_record(row: RunRow) -> Result<RunRecord, RunStoreError> {
136 let (
137 id,
138 task_id,
139 status_json,
140 step_entries_json,
141 degradations_json,
142 operator_sid,
143 result_ref_json,
144 created_at,
145 updated_at,
146 ) = row;
147 let status: RunStatus = serde_json::from_str(&status_json)
148 .map_err(|e| RunStoreError::Other(format!("decode status: {e}")))?;
149 let step_entries: Vec<StepEntry> = serde_json::from_str(&step_entries_json)
150 .map_err(|e| RunStoreError::Other(format!("decode step_entries: {e}")))?;
151 let degradations: Vec<DegradationEntry> = serde_json::from_str(°radations_json)
152 .map_err(|e| RunStoreError::Other(format!("decode degradations: {e}")))?;
153 let result_ref: Option<serde_json::Value> = match result_ref_json {
154 Some(text) => Some(
155 serde_json::from_str(&text)
156 .map_err(|e| RunStoreError::Other(format!("decode result_ref: {e}")))?,
157 ),
158 None => None,
159 };
160 let id = RunId::parse(id).map_err(|e| RunStoreError::Other(format!("decode id: {e}")))?;
164 let task_id =
165 TaskId::parse(task_id).map_err(|e| RunStoreError::Other(format!("decode task_id: {e}")))?;
166 Ok(RunRecord {
167 id,
168 task_id,
169 status,
170 step_entries,
171 degradations,
172 operator_sid,
173 result_ref,
174 created_at: created_at as u64,
175 updated_at: updated_at as u64,
176 })
177}
178
179#[async_trait]
180impl RunStore for SqliteRunStore {
181 fn name(&self) -> &str {
182 "sqlite"
183 }
184
185 async fn create(&self, record: RunRecord) -> Result<(), RunStoreError> {
186 let id = record.id.to_string();
187 let id_for_conflict = record.id.clone();
188 let task_id = record.task_id.to_string();
189 let status_json = serde_json::to_string(&record.status)
190 .map_err(|e| RunStoreError::Other(format!("encode status: {e}")))?;
191 let step_entries_json = serde_json::to_string(&record.step_entries)
192 .map_err(|e| RunStoreError::Other(format!("encode step_entries: {e}")))?;
193 let degradations_json = serde_json::to_string(&record.degradations)
194 .map_err(|e| RunStoreError::Other(format!("encode degradations: {e}")))?;
195 let operator_sid = record.operator_sid.clone();
196 let result_ref_json = record
197 .result_ref
198 .as_ref()
199 .map(serde_json::to_string)
200 .transpose()
201 .map_err(|e| RunStoreError::Other(format!("encode result_ref: {e}")))?;
202 let created_at = record.created_at as i64;
203 let updated_at = record.updated_at as i64;
204
205 self.isle
206 .call(move |conn| {
207 let tx = conn.transaction()?;
208 let exists: i64 = tx.query_row(
209 "SELECT COUNT(*) FROM runs WHERE id = ?1",
210 params![id],
211 |row| row.get(0),
212 )?;
213 if exists > 0 {
214 return Err(rusqlite::Error::SqliteFailure(
215 rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_CONSTRAINT),
216 Some(format!("__mlua_swarm_duplicate:{id}")),
217 ));
218 }
219 tx.execute(
220 "INSERT INTO runs (id, task_id, status, step_entries_json, \
221 degradations_json, operator_sid, result_ref_json, created_at, updated_at) \
222 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
223 params![
224 id,
225 task_id,
226 status_json,
227 step_entries_json,
228 degradations_json,
229 operator_sid,
230 result_ref_json,
231 created_at,
232 updated_at,
233 ],
234 )?;
235 tx.commit()?;
236 Ok(())
237 })
238 .await
239 .map_err(|e| match &e {
240 IsleError::Sqlite(rusqlite::Error::SqliteFailure(_, Some(msg)))
241 if msg.starts_with("__mlua_swarm_duplicate:") =>
242 {
243 RunStoreError::Duplicate(id_for_conflict.clone())
244 }
245 _ => map_isle_err(e),
246 })
247 }
248
249 async fn get(&self, id: &RunId) -> Result<RunRecord, RunStoreError> {
250 let id_str = id.to_string();
251 let id_for_notfound = id.clone();
252 let row = self
253 .isle
254 .call(move |conn| {
255 conn.query_row(
256 &format!("SELECT {RUN_SELECT_COLUMNS} FROM runs WHERE id = ?1"),
257 params![id_str],
258 |row| {
259 Ok((
260 row.get::<_, String>(0)?,
261 row.get::<_, String>(1)?,
262 row.get::<_, String>(2)?,
263 row.get::<_, String>(3)?,
264 row.get::<_, String>(4)?,
265 row.get::<_, Option<String>>(5)?,
266 row.get::<_, Option<String>>(6)?,
267 row.get::<_, i64>(7)?,
268 row.get::<_, i64>(8)?,
269 ))
270 },
271 )
272 .optional()
273 })
274 .await
275 .map_err(map_isle_err)?;
276 match row {
277 Some(row) => row_to_record(row),
278 None => Err(RunStoreError::NotFound(id_for_notfound)),
279 }
280 }
281
282 async fn list_by_task(&self, task_id: &TaskId) -> Result<Vec<RunRecord>, RunStoreError> {
283 let task_id_str = task_id.to_string();
284 let rows = self
285 .isle
286 .call(move |conn| {
287 let mut stmt = conn.prepare(&format!(
288 "SELECT {RUN_SELECT_COLUMNS} FROM runs \
289 WHERE task_id = ?1 ORDER BY created_at ASC"
290 ))?;
291 let iter = stmt.query_map(params![task_id_str], |row| {
292 Ok((
293 row.get::<_, String>(0)?,
294 row.get::<_, String>(1)?,
295 row.get::<_, String>(2)?,
296 row.get::<_, String>(3)?,
297 row.get::<_, String>(4)?,
298 row.get::<_, Option<String>>(5)?,
299 row.get::<_, Option<String>>(6)?,
300 row.get::<_, i64>(7)?,
301 row.get::<_, i64>(8)?,
302 ))
303 })?;
304 let mut out = Vec::new();
305 for r in iter {
306 out.push(r?);
307 }
308 Ok(out)
309 })
310 .await
311 .map_err(map_isle_err)?;
312 rows.into_iter().map(row_to_record).collect()
313 }
314
315 async fn append_step_entry(&self, id: &RunId, entry: StepEntry) -> Result<(), RunStoreError> {
316 let id_str = id.to_string();
317 let id_for_notfound = id.clone();
318 let updated_at = crate::types::now_unix() as i64;
319
320 let updated = self
321 .isle
322 .call(move |conn| {
323 let tx = conn.transaction()?;
324 let existing: Option<String> = tx
325 .query_row(
326 "SELECT step_entries_json FROM runs WHERE id = ?1",
327 params![id_str],
328 |row| row.get(0),
329 )
330 .optional()?;
331 let Some(existing_json) = existing else {
332 return Ok(false);
333 };
334 let mut entries: Vec<StepEntry> = serde_json::from_str(&existing_json)
335 .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
336 entries.push(entry);
337 let new_json = serde_json::to_string(&entries)
338 .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
339 tx.execute(
340 "UPDATE runs SET step_entries_json = ?1, updated_at = ?2 WHERE id = ?3",
341 params![new_json, updated_at, id_str],
342 )?;
343 tx.commit()?;
344 Ok(true)
345 })
346 .await
347 .map_err(map_isle_err)?;
348
349 if updated {
350 Ok(())
351 } else {
352 Err(RunStoreError::NotFound(id_for_notfound))
353 }
354 }
355
356 async fn append_degradation(
357 &self,
358 id: &RunId,
359 entry: DegradationEntry,
360 ) -> Result<(), RunStoreError> {
361 let id_str = id.to_string();
362 let id_for_notfound = id.clone();
363 let updated_at = crate::types::now_unix() as i64;
364
365 let updated = self
366 .isle
367 .call(move |conn| {
368 let tx = conn.transaction()?;
369 let existing: Option<String> = tx
370 .query_row(
371 "SELECT degradations_json FROM runs WHERE id = ?1",
372 params![id_str],
373 |row| row.get(0),
374 )
375 .optional()?;
376 let Some(existing_json) = existing else {
377 return Ok(false);
378 };
379 let mut entries: Vec<DegradationEntry> = serde_json::from_str(&existing_json)
380 .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
381 entries.push(entry);
382 let new_json = serde_json::to_string(&entries)
383 .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
384 tx.execute(
385 "UPDATE runs SET degradations_json = ?1, updated_at = ?2 WHERE id = ?3",
386 params![new_json, updated_at, id_str],
387 )?;
388 tx.commit()?;
389 Ok(true)
390 })
391 .await
392 .map_err(map_isle_err)?;
393
394 if updated {
395 Ok(())
396 } else {
397 Err(RunStoreError::NotFound(id_for_notfound))
398 }
399 }
400
401 async fn update_status(&self, id: &RunId, status: RunStatus) -> Result<(), RunStoreError> {
402 let id_str = id.to_string();
403 let id_for_notfound = id.clone();
404 let status_json = serde_json::to_string(&status)
405 .map_err(|e| RunStoreError::Other(format!("encode status: {e}")))?;
406 let updated_at = crate::types::now_unix() as i64;
407 let n = self
408 .isle
409 .call(move |conn| {
410 conn.execute(
411 "UPDATE runs SET status = ?1, updated_at = ?2 WHERE id = ?3",
412 params![status_json, updated_at, id_str],
413 )
414 })
415 .await
416 .map_err(map_isle_err)?;
417 if n == 0 {
418 Err(RunStoreError::NotFound(id_for_notfound))
419 } else {
420 Ok(())
421 }
422 }
423
424 async fn set_result(
425 &self,
426 id: &RunId,
427 result_ref: serde_json::Value,
428 ) -> Result<(), RunStoreError> {
429 let id_str = id.to_string();
430 let id_for_notfound = id.clone();
431 let result_ref_json = serde_json::to_string(&result_ref)
432 .map_err(|e| RunStoreError::Other(format!("encode result_ref: {e}")))?;
433 let updated_at = crate::types::now_unix() as i64;
434 let n = self
435 .isle
436 .call(move |conn| {
437 conn.execute(
438 "UPDATE runs SET result_ref_json = ?1, updated_at = ?2 WHERE id = ?3",
439 params![result_ref_json, updated_at, id_str],
440 )
441 })
442 .await
443 .map_err(map_isle_err)?;
444 if n == 0 {
445 Err(RunStoreError::NotFound(id_for_notfound))
446 } else {
447 Ok(())
448 }
449 }
450
451 async fn list_running(&self) -> Result<Vec<RunRecord>, RunStoreError> {
452 let status_json = serde_json::to_string(&RunStatus::Running)
453 .map_err(|e| RunStoreError::Other(format!("encode status: {e}")))?;
454 let rows = self
455 .isle
456 .call(move |conn| {
457 let mut stmt = conn.prepare(&format!(
458 "SELECT {RUN_SELECT_COLUMNS} FROM runs WHERE status = ?1"
459 ))?;
460 let iter = stmt.query_map(params![status_json], |row| {
461 Ok((
462 row.get::<_, String>(0)?,
463 row.get::<_, String>(1)?,
464 row.get::<_, String>(2)?,
465 row.get::<_, String>(3)?,
466 row.get::<_, String>(4)?,
467 row.get::<_, Option<String>>(5)?,
468 row.get::<_, Option<String>>(6)?,
469 row.get::<_, i64>(7)?,
470 row.get::<_, i64>(8)?,
471 ))
472 })?;
473 let mut out = Vec::new();
474 for r in iter {
475 out.push(r?);
476 }
477 Ok(out)
478 })
479 .await
480 .map_err(map_isle_err)?;
481 rows.into_iter().map(row_to_record).collect()
482 }
483}
484
485#[cfg(test)]
490mod tests {
491 use super::*;
492 use serde_json::json;
493
494 fn mk(id: &str, task_id: &str, created_at: u64) -> RunRecord {
495 RunRecord {
496 id: RunId::parse(id).unwrap(),
497 task_id: TaskId::parse(task_id).unwrap(),
498 status: RunStatus::Pending,
499 step_entries: vec![],
500 degradations: vec![],
501 operator_sid: None,
502 result_ref: None,
503 created_at,
504 updated_at: created_at,
505 }
506 }
507
508 fn mk_degradation(tool: &str, at: u64) -> DegradationEntry {
509 DegradationEntry {
510 tool: tool.to_string(),
511 error: "boom".to_string(),
512 fallback: "cached-default".to_string(),
513 note: None,
514 step_ref: Some("worker".to_string()),
515 attempt: Some(1),
516 at,
517 }
518 }
519
520 #[tokio::test]
521 async fn create_then_get() {
522 let (s, driver) = SqliteRunStore::open_in_memory().await.unwrap();
523 s.create(mk("R-1", "T-1", 100)).await.unwrap();
524 let got = s.get(&RunId::parse("R-1").unwrap()).await.unwrap();
525 assert_eq!(got.task_id, TaskId::parse("T-1").unwrap());
526 assert_eq!(got.status, RunStatus::Pending);
527 assert!(got.step_entries.is_empty());
528 assert_eq!(got.result_ref, None);
529 drop(s);
530 driver.shutdown().await.unwrap();
531 }
532
533 #[tokio::test]
534 async fn duplicate_create_rejected() {
535 let (s, driver) = SqliteRunStore::open_in_memory().await.unwrap();
536 s.create(mk("R-1", "T-1", 100)).await.unwrap();
537 let err = s.create(mk("R-1", "T-1", 200)).await.unwrap_err();
538 assert!(matches!(err, RunStoreError::Duplicate(_)), "got: {err:?}");
539 drop(s);
540 driver.shutdown().await.unwrap();
541 }
542
543 #[tokio::test]
544 async fn get_missing_returns_not_found() {
545 let (s, driver) = SqliteRunStore::open_in_memory().await.unwrap();
546 let err = s.get(&RunId::parse("R-nope").unwrap()).await.unwrap_err();
547 assert!(matches!(err, RunStoreError::NotFound(_)));
548 drop(s);
549 driver.shutdown().await.unwrap();
550 }
551
552 #[tokio::test]
553 async fn list_by_task_filters_and_orders_ascending() {
554 let (s, driver) = SqliteRunStore::open_in_memory().await.unwrap();
555 s.create(mk("R-1", "T-1", 300)).await.unwrap();
556 s.create(mk("R-2", "T-2", 50)).await.unwrap();
557 s.create(mk("R-3", "T-1", 100)).await.unwrap();
558 let list = s
559 .list_by_task(&TaskId::parse("T-1").unwrap())
560 .await
561 .unwrap();
562 let ids: Vec<_> = list.iter().map(|r| r.id.to_string()).collect();
563 assert_eq!(ids, vec!["R-3", "R-1"]);
564 drop(s);
565 driver.shutdown().await.unwrap();
566 }
567
568 #[tokio::test]
569 async fn append_step_entry_accumulates_in_order() {
570 let (s, driver) = SqliteRunStore::open_in_memory().await.unwrap();
571 s.create(mk("R-1", "T-1", 100)).await.unwrap();
572 s.append_step_entry(
573 &RunId::parse("R-1").unwrap(),
574 StepEntry {
575 step_id: crate::types::StepId::parse("ST-1").unwrap(),
576 step_ref: Some("step-a".into()),
577 status: Some("dispatched".into()),
578 at: 101,
579 },
580 )
581 .await
582 .unwrap();
583 s.append_step_entry(
584 &RunId::parse("R-1").unwrap(),
585 StepEntry {
586 step_id: crate::types::StepId::parse("ST-2").unwrap(),
587 step_ref: Some("step-b".into()),
588 status: Some("passed".into()),
589 at: 102,
590 },
591 )
592 .await
593 .unwrap();
594 let got = s.get(&RunId::parse("R-1").unwrap()).await.unwrap();
595 assert_eq!(got.step_entries.len(), 2);
596 assert_eq!(got.step_entries[0].step_ref, Some("step-a".into()));
597 assert_eq!(got.step_entries[1].step_ref, Some("step-b".into()));
598 drop(s);
599 driver.shutdown().await.unwrap();
600 }
601
602 #[tokio::test]
603 async fn append_step_entry_unknown_run_fails() {
604 let (s, driver) = SqliteRunStore::open_in_memory().await.unwrap();
605 let err = s
606 .append_step_entry(
607 &RunId::parse("R-nope").unwrap(),
608 StepEntry {
609 step_id: crate::types::StepId::parse("ST-1").unwrap(),
610 step_ref: None,
611 status: None,
612 at: 1,
613 },
614 )
615 .await
616 .unwrap_err();
617 assert!(matches!(err, RunStoreError::NotFound(_)));
618 drop(s);
619 driver.shutdown().await.unwrap();
620 }
621
622 #[tokio::test]
623 async fn append_degradation_accumulates_in_order() {
624 let (s, driver) = SqliteRunStore::open_in_memory().await.unwrap();
625 s.create(mk("R-1", "T-1", 100)).await.unwrap();
626 s.append_degradation(
627 &RunId::parse("R-1").unwrap(),
628 mk_degradation("web_search", 101),
629 )
630 .await
631 .unwrap();
632 s.append_degradation(
633 &RunId::parse("R-1").unwrap(),
634 mk_degradation("code_exec", 102),
635 )
636 .await
637 .unwrap();
638 let got = s.get(&RunId::parse("R-1").unwrap()).await.unwrap();
639 assert_eq!(got.degradations.len(), 2);
640 assert_eq!(got.degradations[0].tool, "web_search");
641 assert_eq!(got.degradations[1].tool, "code_exec");
642 drop(s);
643 driver.shutdown().await.unwrap();
644 }
645
646 #[tokio::test]
647 async fn append_degradation_unknown_run_fails() {
648 let (s, driver) = SqliteRunStore::open_in_memory().await.unwrap();
649 let err = s
650 .append_degradation(
651 &RunId::parse("R-nope").unwrap(),
652 mk_degradation("web_search", 1),
653 )
654 .await
655 .unwrap_err();
656 assert!(matches!(err, RunStoreError::NotFound(_)));
657 drop(s);
658 driver.shutdown().await.unwrap();
659 }
660
661 #[tokio::test]
662 async fn append_degradation_bumps_updated_at() {
663 let (s, driver) = SqliteRunStore::open_in_memory().await.unwrap();
664 s.create(mk("R-1", "T-1", 100)).await.unwrap();
665 s.append_degradation(
666 &RunId::parse("R-1").unwrap(),
667 mk_degradation("web_search", 200),
668 )
669 .await
670 .unwrap();
671 let got = s.get(&RunId::parse("R-1").unwrap()).await.unwrap();
672 assert!(got.updated_at > 100);
673 drop(s);
674 driver.shutdown().await.unwrap();
675 }
676
677 #[tokio::test]
678 async fn update_status_persists() {
679 let (s, driver) = SqliteRunStore::open_in_memory().await.unwrap();
680 s.create(mk("R-1", "T-1", 100)).await.unwrap();
681 s.update_status(&RunId::parse("R-1").unwrap(), RunStatus::Done)
682 .await
683 .unwrap();
684 let got = s.get(&RunId::parse("R-1").unwrap()).await.unwrap();
685 assert_eq!(got.status, RunStatus::Done);
686 drop(s);
687 driver.shutdown().await.unwrap();
688 }
689
690 #[tokio::test]
691 async fn set_result_persists() {
692 let (s, driver) = SqliteRunStore::open_in_memory().await.unwrap();
693 s.create(mk("R-1", "T-1", 100)).await.unwrap();
694 s.set_result(&RunId::parse("R-1").unwrap(), json!({"ok": true}))
695 .await
696 .unwrap();
697 let got = s.get(&RunId::parse("R-1").unwrap()).await.unwrap();
698 assert_eq!(got.result_ref, Some(json!({"ok": true})));
699 drop(s);
700 driver.shutdown().await.unwrap();
701 }
702
703 #[tokio::test]
704 async fn persists_across_reopen() {
705 let dir = tempfile::tempdir().unwrap();
706 let path = dir.path().join("runs.db");
707
708 {
709 let (s, driver) = SqliteRunStore::open(&path).await.unwrap();
710 s.create(mk("R-keep", "T-keep", 42)).await.unwrap();
711 s.append_step_entry(
712 &RunId::parse("R-keep").unwrap(),
713 StepEntry {
714 step_id: crate::types::StepId::parse("ST-1").unwrap(),
715 step_ref: Some("step-a".into()),
716 status: Some("dispatched".into()),
717 at: 43,
718 },
719 )
720 .await
721 .unwrap();
722 drop(s);
723 driver.shutdown().await.unwrap();
724 }
725
726 let (s, driver) = SqliteRunStore::open(&path).await.unwrap();
727 let got = s.get(&RunId::parse("R-keep").unwrap()).await.unwrap();
728 assert_eq!(got.task_id, TaskId::parse("T-keep").unwrap());
729 assert_eq!(got.step_entries.len(), 1);
730 assert_eq!(got.step_entries[0].step_ref, Some("step-a".into()));
731 drop(s);
732 driver.shutdown().await.unwrap();
733 }
734
735 #[tokio::test]
736 async fn list_running_filters_by_status() {
737 let (s, driver) = SqliteRunStore::open_in_memory().await.unwrap();
738 s.create(mk("R-1", "T-1", 100)).await.unwrap();
739 s.create(mk("R-2", "T-2", 200)).await.unwrap();
740 s.create(mk("R-3", "T-3", 300)).await.unwrap();
741 s.update_status(&RunId::parse("R-2").unwrap(), RunStatus::Running)
742 .await
743 .unwrap();
744 s.update_status(&RunId::parse("R-3").unwrap(), RunStatus::Done)
745 .await
746 .unwrap();
747 let running = s.list_running().await.unwrap();
748 assert_eq!(running.len(), 1);
749 assert_eq!(running[0].id, RunId::parse("R-2").unwrap());
750 assert_eq!(running[0].status, RunStatus::Running);
751 drop(s);
752 driver.shutdown().await.unwrap();
753 }
754}