1use std::path::Path;
2use std::process::Command as ShellCommand;
3
4use anyhow::{anyhow, Context, Result};
5use chrono::Utc;
6
7use crate::bean::{AttemptOutcome, AttemptRecord, Bean, Status};
8use crate::config::Config;
9use crate::discovery::find_bean_file;
10use crate::index::Index;
11
12fn git_head_sha(working_dir: &Path) -> Option<String> {
14 ShellCommand::new("git")
15 .args(["rev-parse", "HEAD"])
16 .current_dir(working_dir)
17 .output()
18 .ok()
19 .filter(|o| o.status.success())
20 .and_then(|o| String::from_utf8(o.stdout).ok())
21 .map(|s| s.trim().to_string())
22 .filter(|s| !s.is_empty())
23}
24
25fn run_verify_check(verify_cmd: &str, project_root: &Path) -> Result<bool> {
27 let output = ShellCommand::new("sh")
28 .args(["-c", verify_cmd])
29 .current_dir(project_root)
30 .stdout(std::process::Stdio::null())
31 .stderr(std::process::Stdio::null())
32 .status()
33 .with_context(|| format!("Failed to execute verify command: {}", verify_cmd))?;
34
35 Ok(output.success())
36}
37
38pub fn cmd_claim(beans_dir: &Path, id: &str, by: Option<String>, force: bool) -> Result<()> {
48 let bean_path = find_bean_file(beans_dir, id).map_err(|_| anyhow!("Bean not found: {}", id))?;
49
50 let mut bean =
51 Bean::from_file(&bean_path).with_context(|| format!("Failed to load bean: {}", id))?;
52
53 if bean.status != Status::Open {
54 return Err(anyhow!(
55 "Bean {} is {} -- only open beans can be claimed",
56 id,
57 bean.status
58 ));
59 }
60
61 let has_verify = bean.verify.as_ref().is_some_and(|v| !v.trim().is_empty());
63 if !has_verify {
64 eprintln!(
65 "Warning: Claiming GOAL (no verify). Consider decomposing with: bn create \"spec\" --parent {} --verify \"test\"",
66 id
67 );
68 }
69
70 if has_verify && !force {
72 let project_root = beans_dir
73 .parent()
74 .ok_or_else(|| anyhow!("Cannot determine project root from beans dir"))?;
75 let verify_cmd = bean.verify.as_ref().unwrap();
76
77 eprintln!("Running verify before claim: {}", verify_cmd);
78 let passed = run_verify_check(verify_cmd, project_root)?;
79
80 if passed {
81 return Err(anyhow!(
82 "Cannot claim bean {}: verify already passes\n\n\
83 The verify command succeeded before any work was done.\n\
84 This means either the test is bogus or the work is already complete.\n\n\
85 Use --force to override.",
86 id
87 ));
88 }
89
90 bean.fail_first = true;
92 bean.checkpoint = git_head_sha(project_root);
93 }
94
95 if let Some(tokens) = bean.tokens {
97 let config = Config::load(beans_dir).unwrap_or_else(|_| Config {
98 project: String::new(),
99 next_id: 1,
100 auto_close_parent: true,
101 max_tokens: 30000,
102 run: None,
103 plan: None,
104 max_loops: 10,
105 max_concurrent: 4,
106 poll_interval: 30,
107 extends: vec![],
108 rules_file: None,
109 file_locking: false,
110 on_close: None,
111 on_fail: None,
112 post_plan: None,
113 verify_timeout: None,
114 review: None,
115 });
116 if tokens > config.max_tokens as u64 {
117 return Err(anyhow!(
118 "Cannot claim bean {}: too large for implementation
119
120 {} tokens > {} limit
121
122This bean appears to be a GOAL (high-level outcome) rather than a SPEC
123(concrete, implementable contract).
124
125To make it implementable:
1261. Break it into smaller specs using: bn create \"spec title\" --parent {}
1272. Each spec should have clear inputs/outputs and a verify command
1283. Or use the decompose skill to interactively clarify requirements
129
130See: Goal → Spec → Test framework
131 - GOAL = WHY (this bean)
132 - SPEC = WHAT (child beans with contracts)
133 - TEST = verify command (proves spec is met)",
134 id,
135 tokens,
136 config.max_tokens,
137 id
138 ));
139 }
140 }
141
142 let now = Utc::now();
143 bean.status = Status::InProgress;
144 bean.claimed_by = by.clone();
145 bean.claimed_at = Some(now);
146 bean.updated_at = now;
147
148 let attempt_num = bean.attempt_log.len() as u32 + 1;
150 bean.attempt_log.push(AttemptRecord {
151 num: attempt_num,
152 outcome: AttemptOutcome::Abandoned, notes: None,
154 agent: by.clone(),
155 started_at: Some(now),
156 finished_at: None,
157 });
158
159 bean.to_file(&bean_path)
160 .with_context(|| format!("Failed to save bean: {}", id))?;
161
162 let claimer = by.as_deref().unwrap_or("anonymous");
163 println!("Claimed bean {}: {} (by {})", id, bean.title, claimer);
164
165 let index = Index::build(beans_dir).with_context(|| "Failed to rebuild index")?;
167 index
168 .save(beans_dir)
169 .with_context(|| "Failed to save index")?;
170
171 Ok(())
172}
173
174pub fn cmd_release(beans_dir: &Path, id: &str) -> Result<()> {
178 let bean_path = find_bean_file(beans_dir, id).map_err(|_| anyhow!("Bean not found: {}", id))?;
179
180 let mut bean =
181 Bean::from_file(&bean_path).with_context(|| format!("Failed to load bean: {}", id))?;
182
183 let now = Utc::now();
184
185 if let Some(attempt) = bean.attempt_log.last_mut() {
187 if attempt.finished_at.is_none() {
188 attempt.outcome = AttemptOutcome::Abandoned;
189 attempt.finished_at = Some(now);
190 }
191 }
192
193 bean.claimed_by = None;
194 bean.claimed_at = None;
195 bean.status = Status::Open;
196 bean.updated_at = now;
197
198 bean.to_file(&bean_path)
199 .with_context(|| format!("Failed to save bean: {}", id))?;
200
201 println!("Released claim on bean {}: {}", id, bean.title);
202
203 let index = Index::build(beans_dir).with_context(|| "Failed to rebuild index")?;
205 index
206 .save(beans_dir)
207 .with_context(|| "Failed to save index")?;
208
209 Ok(())
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use std::fs;
216 use tempfile::TempDir;
217
218 fn setup_test_beans_dir() -> (TempDir, std::path::PathBuf) {
219 let dir = TempDir::new().unwrap();
220 let beans_dir = dir.path().join(".beans");
221 fs::create_dir(&beans_dir).unwrap();
222 (dir, beans_dir)
223 }
224
225 #[test]
226 fn test_claim_open_bean() {
227 let (_dir, beans_dir) = setup_test_beans_dir();
228 let bean = Bean::new("1", "Task");
229 bean.to_file(beans_dir.join("1.yaml")).unwrap();
230
231 cmd_claim(&beans_dir, "1", Some("alice".to_string()), true).unwrap();
232
233 let updated = Bean::from_file(beans_dir.join("1.yaml")).unwrap();
234 assert_eq!(updated.status, Status::InProgress);
235 assert_eq!(updated.claimed_by, Some("alice".to_string()));
236 assert!(updated.claimed_at.is_some());
237 }
238
239 #[test]
240 fn test_claim_without_by() {
241 let (_dir, beans_dir) = setup_test_beans_dir();
242 let bean = Bean::new("1", "Task");
243 bean.to_file(beans_dir.join("1.yaml")).unwrap();
244
245 cmd_claim(&beans_dir, "1", None, true).unwrap();
246
247 let updated = Bean::from_file(beans_dir.join("1.yaml")).unwrap();
248 assert_eq!(updated.status, Status::InProgress);
249 assert_eq!(updated.claimed_by, None);
250 assert!(updated.claimed_at.is_some());
251 }
252
253 #[test]
254 fn test_claim_non_open_bean_fails() {
255 let (_dir, beans_dir) = setup_test_beans_dir();
256 let mut bean = Bean::new("1", "Task");
257 bean.status = Status::InProgress;
258 bean.to_file(beans_dir.join("1.yaml")).unwrap();
259
260 let result = cmd_claim(&beans_dir, "1", Some("bob".to_string()), true);
261 assert!(result.is_err());
262 }
263
264 #[test]
265 fn test_claim_closed_bean_fails() {
266 let (_dir, beans_dir) = setup_test_beans_dir();
267 let mut bean = Bean::new("1", "Task");
268 bean.status = Status::Closed;
269 bean.to_file(beans_dir.join("1.yaml")).unwrap();
270
271 let result = cmd_claim(&beans_dir, "1", Some("bob".to_string()), true);
272 assert!(result.is_err());
273 }
274
275 #[test]
276 fn test_claim_nonexistent_bean_fails() {
277 let (_dir, beans_dir) = setup_test_beans_dir();
278 let result = cmd_claim(&beans_dir, "99", Some("alice".to_string()), true);
279 assert!(result.is_err());
280 }
281
282 #[test]
283 fn test_release_claimed_bean() {
284 let (_dir, beans_dir) = setup_test_beans_dir();
285 let mut bean = Bean::new("1", "Task");
286 bean.status = Status::InProgress;
287 bean.claimed_by = Some("alice".to_string());
288 bean.claimed_at = Some(Utc::now());
289 bean.to_file(beans_dir.join("1.yaml")).unwrap();
290
291 cmd_release(&beans_dir, "1").unwrap();
292
293 let updated = Bean::from_file(beans_dir.join("1.yaml")).unwrap();
294 assert_eq!(updated.status, Status::Open);
295 assert_eq!(updated.claimed_by, None);
296 assert_eq!(updated.claimed_at, None);
297 }
298
299 #[test]
300 fn test_release_nonexistent_bean_fails() {
301 let (_dir, beans_dir) = setup_test_beans_dir();
302 let result = cmd_release(&beans_dir, "99");
303 assert!(result.is_err());
304 }
305
306 #[test]
307 fn test_claim_rebuilds_index() {
308 let (_dir, beans_dir) = setup_test_beans_dir();
309 let bean = Bean::new("1", "Task");
310 bean.to_file(beans_dir.join("1.yaml")).unwrap();
311
312 cmd_claim(&beans_dir, "1", Some("alice".to_string()), true).unwrap();
313
314 let index = Index::load(&beans_dir).unwrap();
315 assert_eq!(index.beans.len(), 1);
316 let entry = &index.beans[0];
317 assert_eq!(entry.status, Status::InProgress);
318 }
319
320 #[test]
321 fn test_release_rebuilds_index() {
322 let (_dir, beans_dir) = setup_test_beans_dir();
323 let mut bean = Bean::new("1", "Task");
324 bean.status = Status::InProgress;
325 bean.to_file(beans_dir.join("1.yaml")).unwrap();
326
327 cmd_release(&beans_dir, "1").unwrap();
328
329 let index = Index::load(&beans_dir).unwrap();
330 assert_eq!(index.beans.len(), 1);
331 let entry = &index.beans[0];
332 assert_eq!(entry.status, Status::Open);
333 }
334
335 #[test]
336 fn test_claim_bean_exceeding_max_tokens_fails() {
337 let (_dir, beans_dir) = setup_test_beans_dir();
338
339 let config = crate::config::Config {
341 project: "test".to_string(),
342 next_id: 2,
343 auto_close_parent: true,
344 max_tokens: 30000,
345 run: None,
346 plan: None,
347 max_loops: 10,
348 max_concurrent: 4,
349 poll_interval: 30,
350 extends: vec![],
351 rules_file: None,
352 file_locking: false,
353 on_close: None,
354 on_fail: None,
355 post_plan: None,
356 verify_timeout: None,
357 review: None,
358 };
359 config.save(&beans_dir).unwrap();
360
361 let mut bean = Bean::new("1", "Large Bean");
363 bean.tokens = Some(45000);
364 bean.to_file(beans_dir.join("1.yaml")).unwrap();
365
366 let result = cmd_claim(&beans_dir, "1", Some("alice".to_string()), true);
367 assert!(result.is_err());
368 let err_msg = result.unwrap_err().to_string();
369 assert!(err_msg.contains("too large"));
370 assert!(err_msg.contains("45000"));
371 assert!(err_msg.contains("30000"));
372 assert!(err_msg.contains("GOAL"));
373 assert!(err_msg.contains("SPEC"));
374 assert!(err_msg.contains("--parent"));
375 }
376
377 #[test]
378 fn test_claim_bean_under_max_tokens_succeeds() {
379 let (_dir, beans_dir) = setup_test_beans_dir();
380
381 let config = crate::config::Config {
383 project: "test".to_string(),
384 next_id: 2,
385 auto_close_parent: true,
386 max_tokens: 30000,
387 run: None,
388 plan: None,
389 max_loops: 10,
390 max_concurrent: 4,
391 poll_interval: 30,
392 extends: vec![],
393 rules_file: None,
394 file_locking: false,
395 on_close: None,
396 on_fail: None,
397 post_plan: None,
398 verify_timeout: None,
399 review: None,
400 };
401 config.save(&beans_dir).unwrap();
402
403 let mut bean = Bean::new("1", "Small Bean");
405 bean.tokens = Some(15000);
406 bean.to_file(beans_dir.join("1.yaml")).unwrap();
407
408 let result = cmd_claim(&beans_dir, "1", Some("alice".to_string()), true);
409 assert!(result.is_ok());
410
411 let updated = Bean::from_file(beans_dir.join("1.yaml")).unwrap();
412 assert_eq!(updated.status, Status::InProgress);
413 }
414
415 #[test]
416 fn test_claim_bean_without_tokens_succeeds() {
417 let (_dir, beans_dir) = setup_test_beans_dir();
418
419 let config = crate::config::Config {
421 project: "test".to_string(),
422 next_id: 2,
423 auto_close_parent: true,
424 max_tokens: 30000,
425 run: None,
426 plan: None,
427 max_loops: 10,
428 max_concurrent: 4,
429 poll_interval: 30,
430 extends: vec![],
431 rules_file: None,
432 file_locking: false,
433 on_close: None,
434 on_fail: None,
435 post_plan: None,
436 verify_timeout: None,
437 review: None,
438 };
439 config.save(&beans_dir).unwrap();
440
441 let bean = Bean::new("1", "No Token Count");
443 bean.to_file(beans_dir.join("1.yaml")).unwrap();
444
445 let result = cmd_claim(&beans_dir, "1", Some("alice".to_string()), true);
446 assert!(result.is_ok());
447
448 let updated = Bean::from_file(beans_dir.join("1.yaml")).unwrap();
449 assert_eq!(updated.status, Status::InProgress);
450 }
451
452 #[test]
453 fn test_claim_bean_at_exact_limit_succeeds() {
454 let (_dir, beans_dir) = setup_test_beans_dir();
455
456 let config = crate::config::Config {
458 project: "test".to_string(),
459 next_id: 2,
460 auto_close_parent: true,
461 max_tokens: 30000,
462 run: None,
463 plan: None,
464 max_loops: 10,
465 max_concurrent: 4,
466 poll_interval: 30,
467 extends: vec![],
468 rules_file: None,
469 file_locking: false,
470 on_close: None,
471 on_fail: None,
472 post_plan: None,
473 verify_timeout: None,
474 review: None,
475 };
476 config.save(&beans_dir).unwrap();
477
478 let mut bean = Bean::new("1", "Exact Limit Bean");
480 bean.tokens = Some(30000);
481 bean.to_file(beans_dir.join("1.yaml")).unwrap();
482
483 let result = cmd_claim(&beans_dir, "1", Some("alice".to_string()), true);
484 assert!(result.is_ok());
485 }
486
487 #[test]
488 fn test_claim_bean_without_verify_succeeds_with_warning() {
489 let (_dir, beans_dir) = setup_test_beans_dir();
490
491 let bean = Bean::new("1", "Add authentication");
493 bean.to_file(beans_dir.join("1.yaml")).unwrap();
495
496 let result = cmd_claim(&beans_dir, "1", Some("alice".to_string()), true);
498 assert!(result.is_ok());
499
500 let updated = Bean::from_file(beans_dir.join("1.yaml")).unwrap();
501 assert_eq!(updated.status, Status::InProgress);
502 assert_eq!(updated.claimed_by, Some("alice".to_string()));
503 }
504
505 #[test]
506 fn test_claim_bean_with_verify_succeeds() {
507 let (_dir, beans_dir) = setup_test_beans_dir();
508
509 let mut bean = Bean::new("1", "Add login endpoint");
511 bean.verify = Some("cargo test login".to_string());
512 bean.to_file(beans_dir.join("1.yaml")).unwrap();
513
514 let result = cmd_claim(&beans_dir, "1", Some("alice".to_string()), true);
516 assert!(result.is_ok());
517
518 let updated = Bean::from_file(beans_dir.join("1.yaml")).unwrap();
519 assert_eq!(updated.status, Status::InProgress);
520 }
521
522 #[test]
523 fn test_claim_bean_with_empty_verify_warns() {
524 let (_dir, beans_dir) = setup_test_beans_dir();
525
526 let mut bean = Bean::new("1", "Vague task");
528 bean.verify = Some(" ".to_string()); bean.to_file(beans_dir.join("1.yaml")).unwrap();
530
531 let result = cmd_claim(&beans_dir, "1", Some("alice".to_string()), true);
533 assert!(result.is_ok());
534
535 let updated = Bean::from_file(beans_dir.join("1.yaml")).unwrap();
536 assert_eq!(updated.status, Status::InProgress);
537 }
538
539 #[test]
544 fn verify_on_claim_passing_verify_rejected() {
545 let (_dir, beans_dir) = setup_test_beans_dir();
546
547 let mut bean = Bean::new("1", "Already done");
549 bean.verify = Some("true".to_string());
550 bean.to_file(beans_dir.join("1.yaml")).unwrap();
551
552 let result = cmd_claim(&beans_dir, "1", Some("alice".to_string()), false);
554 assert!(result.is_err());
555 let err_msg = result.unwrap_err().to_string();
556 assert!(err_msg.contains("verify already passes"));
557 assert!(err_msg.contains("--force"));
558
559 let unchanged = Bean::from_file(beans_dir.join("1.yaml")).unwrap();
561 assert_eq!(unchanged.status, Status::Open);
562 }
563
564 #[test]
565 fn verify_on_claim_failing_verify_succeeds() {
566 let (_dir, beans_dir) = setup_test_beans_dir();
567
568 let mut bean = Bean::new("1", "Real work needed");
570 bean.verify = Some("false".to_string());
571 bean.to_file(beans_dir.join("1.yaml")).unwrap();
572
573 let result = cmd_claim(&beans_dir, "1", Some("alice".to_string()), false);
575 assert!(result.is_ok());
576
577 let updated = Bean::from_file(beans_dir.join("1.yaml")).unwrap();
578 assert_eq!(updated.status, Status::InProgress);
579 assert_eq!(updated.claimed_by, Some("alice".to_string()));
580 assert!(
581 updated.fail_first,
582 "fail_first should be set when verify fails at claim time"
583 );
584 }
585
586 #[test]
587 fn verify_on_claim_force_overrides() {
588 let (_dir, beans_dir) = setup_test_beans_dir();
589
590 let mut bean = Bean::new("1", "Force claim");
592 bean.verify = Some("true".to_string());
593 bean.to_file(beans_dir.join("1.yaml")).unwrap();
594
595 let result = cmd_claim(&beans_dir, "1", Some("alice".to_string()), true);
597 assert!(result.is_ok());
598
599 let updated = Bean::from_file(beans_dir.join("1.yaml")).unwrap();
600 assert_eq!(updated.status, Status::InProgress);
601 assert_eq!(updated.claimed_by, Some("alice".to_string()));
602 }
603
604 #[test]
605 fn verify_on_claim_checkpoint_sha_stored() {
606 let (_dir, beans_dir) = setup_test_beans_dir();
607
608 let mut bean = Bean::new("1", "Checkpoint test");
610 bean.verify = Some("false".to_string());
611 bean.to_file(beans_dir.join("1.yaml")).unwrap();
612
613 let project_root = beans_dir.parent().unwrap();
615 std::process::Command::new("git")
616 .args(["init"])
617 .current_dir(project_root)
618 .output()
619 .unwrap();
620 std::process::Command::new("git")
621 .args(["add", "."])
622 .current_dir(project_root)
623 .output()
624 .unwrap();
625 std::process::Command::new("git")
626 .args(["commit", "-m", "init", "--allow-empty"])
627 .current_dir(project_root)
628 .env("GIT_AUTHOR_NAME", "test")
629 .env("GIT_AUTHOR_EMAIL", "test@test.com")
630 .env("GIT_COMMITTER_NAME", "test")
631 .env("GIT_COMMITTER_EMAIL", "test@test.com")
632 .output()
633 .unwrap();
634
635 let result = cmd_claim(&beans_dir, "1", Some("alice".to_string()), false);
636 assert!(result.is_ok());
637
638 let updated = Bean::from_file(beans_dir.join("1.yaml")).unwrap();
639 assert!(
640 updated.checkpoint.is_some(),
641 "checkpoint SHA should be stored"
642 );
643 let sha = updated.checkpoint.unwrap();
644 assert_eq!(sha.len(), 40, "SHA should be 40 hex chars, got: {}", sha);
645 assert!(
646 sha.chars().all(|c| c.is_ascii_hexdigit()),
647 "SHA should be hex"
648 );
649 }
650
651 #[test]
652 fn verify_on_claim_no_verify_skips_check() {
653 let (_dir, beans_dir) = setup_test_beans_dir();
654
655 let bean = Bean::new("1", "No verify");
657 bean.to_file(beans_dir.join("1.yaml")).unwrap();
658
659 let result = cmd_claim(&beans_dir, "1", Some("alice".to_string()), false);
660 assert!(result.is_ok());
661
662 let updated = Bean::from_file(beans_dir.join("1.yaml")).unwrap();
663 assert_eq!(updated.status, Status::InProgress);
664 assert!(
665 !updated.fail_first,
666 "fail_first should not be set without verify"
667 );
668 assert!(updated.checkpoint.is_none(), "no checkpoint without verify");
669 }
670
671 #[test]
676 fn claim_starts_attempt() {
677 let (_dir, beans_dir) = setup_test_beans_dir();
678 let bean = Bean::new("1", "Task");
679 bean.to_file(beans_dir.join("1.yaml")).unwrap();
680
681 cmd_claim(&beans_dir, "1", Some("agent-1".to_string()), true).unwrap();
682
683 let updated = Bean::from_file(beans_dir.join("1.yaml")).unwrap();
684 assert_eq!(updated.attempt_log.len(), 1);
685 assert_eq!(updated.attempt_log[0].num, 1);
686 assert_eq!(updated.attempt_log[0].agent, Some("agent-1".to_string()));
687 assert!(updated.attempt_log[0].started_at.is_some());
688 assert!(updated.attempt_log[0].finished_at.is_none());
689 }
690
691 #[test]
692 fn release_marks_attempt_abandoned() {
693 let (_dir, beans_dir) = setup_test_beans_dir();
694 let mut bean = Bean::new("1", "Task");
695 bean.status = Status::InProgress;
696 bean.claimed_by = Some("agent-1".to_string());
697 bean.attempt_log.push(AttemptRecord {
698 num: 1,
699 outcome: AttemptOutcome::Abandoned,
700 notes: None,
701 agent: Some("agent-1".to_string()),
702 started_at: Some(Utc::now()),
703 finished_at: None,
704 });
705 bean.to_file(beans_dir.join("1.yaml")).unwrap();
706
707 cmd_release(&beans_dir, "1").unwrap();
708
709 let updated = Bean::from_file(beans_dir.join("1.yaml")).unwrap();
710 assert_eq!(updated.attempt_log.len(), 1);
711 assert_eq!(updated.attempt_log[0].outcome, AttemptOutcome::Abandoned);
712 assert!(updated.attempt_log[0].finished_at.is_some());
713 }
714
715 #[test]
716 fn multiple_claims_accumulate_attempts() {
717 let (_dir, beans_dir) = setup_test_beans_dir();
718 let bean = Bean::new("1", "Task");
719 bean.to_file(beans_dir.join("1.yaml")).unwrap();
720
721 cmd_claim(&beans_dir, "1", Some("agent-1".to_string()), true).unwrap();
723 cmd_release(&beans_dir, "1").unwrap();
725 cmd_claim(&beans_dir, "1", Some("agent-2".to_string()), true).unwrap();
727
728 let updated = Bean::from_file(beans_dir.join("1.yaml")).unwrap();
729 assert_eq!(updated.attempt_log.len(), 2);
730 assert_eq!(updated.attempt_log[0].num, 1);
731 assert_eq!(updated.attempt_log[0].outcome, AttemptOutcome::Abandoned);
732 assert!(updated.attempt_log[0].finished_at.is_some());
733 assert_eq!(updated.attempt_log[1].num, 2);
734 assert_eq!(updated.attempt_log[1].agent, Some("agent-2".to_string()));
735 assert!(updated.attempt_log[1].finished_at.is_none());
736 }
737}