1use std::collections::HashSet;
2use std::path::Path;
3
4use anyhow::{Context, Result};
5
6use crate::bean::{AttemptOutcome, Bean};
7use crate::config::Config;
8use crate::ctx_assembler::{assemble_context, extract_paths, read_file};
9use crate::discovery::find_bean_file;
10use crate::index::Index;
11
12fn load_rules(beans_dir: &Path) -> Option<String> {
17 let config = Config::load(beans_dir).ok()?;
18 let rules_path = config.rules_path(beans_dir);
19
20 let content = std::fs::read_to_string(&rules_path).ok()?;
21 let trimmed = content.trim();
22
23 if trimmed.is_empty() {
24 return None;
25 }
26
27 let line_count = content.lines().count();
28 if line_count > 1000 {
29 eprintln!(
30 "Warning: RULES.md is very large ({} lines). Consider trimming it.",
31 line_count
32 );
33 }
34
35 Some(content)
36}
37
38fn format_rules_section(rules: &str) -> String {
40 format!(
41 "═══ PROJECT RULES ═══════════════════════════════════════════\n\
42 {}\n\
43 ═════════════════════════════════════════════════════════════\n\n",
44 rules.trim_end()
45 )
46}
47
48fn format_attempt_notes_section(bean: &Bean) -> Option<String> {
53 let mut parts: Vec<String> = Vec::new();
54
55 if let Some(ref notes) = bean.notes {
57 let trimmed = notes.trim();
58 if !trimmed.is_empty() {
59 parts.push(format!("Bean notes:\n{}", trimmed));
60 }
61 }
62
63 let attempt_entries: Vec<String> = bean
65 .attempt_log
66 .iter()
67 .filter_map(|a| {
68 let notes = a.notes.as_deref()?.trim();
69 if notes.is_empty() {
70 return None;
71 }
72 let outcome = match a.outcome {
73 AttemptOutcome::Success => "success",
74 AttemptOutcome::Failed => "failed",
75 AttemptOutcome::Abandoned => "abandoned",
76 };
77 let agent_str = a
78 .agent
79 .as_deref()
80 .map(|ag| format!(" ({})", ag))
81 .unwrap_or_default();
82 Some(format!(
83 "Attempt #{}{} [{}]: {}",
84 a.num, agent_str, outcome, notes
85 ))
86 })
87 .collect();
88
89 if !attempt_entries.is_empty() {
90 parts.push(attempt_entries.join("\n"));
91 }
92
93 if parts.is_empty() {
94 return None;
95 }
96
97 Some(format!(
98 "═══ Previous Attempts ════════════════════════════════════════\n\
99 {}\n\
100 ══════════════════════════════════════════════════════════════\n\n",
101 parts.join("\n\n").trim_end()
102 ))
103}
104
105fn extract_rust_structure(content: &str) -> Vec<String> {
112 let mut result = Vec::new();
113
114 for line in content.lines() {
115 let trimmed = line.trim();
116
117 if trimmed.is_empty()
118 || trimmed.starts_with("//")
119 || trimmed.starts_with("/*")
120 || trimmed.starts_with('*')
121 {
122 continue;
123 }
124
125 if trimmed.starts_with("use ") {
127 result.push(trimmed.to_string());
128 continue;
129 }
130
131 let is_decl = trimmed.starts_with("pub fn ")
133 || trimmed.starts_with("pub async fn ")
134 || trimmed.starts_with("pub(crate) fn ")
135 || trimmed.starts_with("pub(crate) async fn ")
136 || trimmed.starts_with("fn ")
137 || trimmed.starts_with("async fn ")
138 || trimmed.starts_with("pub struct ")
139 || trimmed.starts_with("pub(crate) struct ")
140 || trimmed.starts_with("struct ")
141 || trimmed.starts_with("pub enum ")
142 || trimmed.starts_with("pub(crate) enum ")
143 || trimmed.starts_with("enum ")
144 || trimmed.starts_with("pub trait ")
145 || trimmed.starts_with("pub(crate) trait ")
146 || trimmed.starts_with("trait ")
147 || trimmed.starts_with("pub type ")
148 || trimmed.starts_with("type ")
149 || trimmed.starts_with("impl ")
150 || trimmed.starts_with("pub const ")
151 || trimmed.starts_with("pub(crate) const ")
152 || trimmed.starts_with("const ")
153 || trimmed.starts_with("pub static ")
154 || trimmed.starts_with("static ");
155
156 if is_decl {
157 let sig = trimmed.trim_end_matches('{').trim_end();
159 result.push(sig.to_string());
160 }
161 }
162
163 result
164}
165
166fn extract_ts_structure(content: &str) -> Vec<String> {
171 let mut result = Vec::new();
172
173 for line in content.lines() {
174 let trimmed = line.trim();
175
176 if trimmed.is_empty()
177 || trimmed.starts_with("//")
178 || trimmed.starts_with("/*")
179 || trimmed.starts_with('*')
180 {
181 continue;
182 }
183
184 if trimmed.starts_with("import ") {
186 result.push(trimmed.to_string());
187 continue;
188 }
189
190 let is_decl = trimmed.starts_with("export function ")
191 || trimmed.starts_with("export async function ")
192 || trimmed.starts_with("export default function ")
193 || trimmed.starts_with("function ")
194 || trimmed.starts_with("async function ")
195 || trimmed.starts_with("export class ")
196 || trimmed.starts_with("export abstract class ")
197 || trimmed.starts_with("class ")
198 || trimmed.starts_with("export interface ")
199 || trimmed.starts_with("interface ")
200 || trimmed.starts_with("export type ")
201 || trimmed.starts_with("export enum ")
202 || trimmed.starts_with("export const ")
203 || trimmed.starts_with("export default class ")
204 || trimmed.starts_with("export default async function ");
205
206 if is_decl {
207 let sig = trimmed.trim_end_matches('{').trim_end();
208 result.push(sig.to_string());
209 }
210 }
211
212 result
213}
214
215fn extract_python_structure(content: &str) -> Vec<String> {
220 let mut result = Vec::new();
221
222 for line in content.lines() {
223 let trimmed = line.trim();
224
225 if trimmed.is_empty() || trimmed.starts_with('#') {
226 continue;
227 }
228
229 if line.starts_with("import ") || line.starts_with("from ") {
231 result.push(trimmed.to_string());
232 continue;
233 }
234
235 if trimmed.starts_with("def ")
237 || trimmed.starts_with("async def ")
238 || trimmed.starts_with("class ")
239 {
240 let sig = trimmed.trim_end_matches(':').trim_end();
241 result.push(sig.to_string());
242 }
243 }
244
245 result
246}
247
248pub fn extract_file_structure(path: &str, content: &str) -> Option<String> {
254 let ext = Path::new(path).extension()?.to_str()?;
255
256 let lines: Vec<String> = match ext {
257 "rs" => extract_rust_structure(content),
258 "ts" | "tsx" => extract_ts_structure(content),
259 "py" => extract_python_structure(content),
260 _ => return None,
261 };
262
263 if lines.is_empty() {
264 return None;
265 }
266
267 Some(lines.join("\n"))
268}
269
270fn format_structure_block(structures: &[(&str, String)]) -> Option<String> {
274 if structures.is_empty() {
275 return None;
276 }
277
278 let mut body = String::new();
279 for (path, structure) in structures {
280 body.push_str(&format!("### {}\n```\n{}\n```\n\n", path, structure));
281 }
282
283 Some(format!(
284 "═══ File Structure ═══════════════════════════════════════════\n\
285 {}\
286 ══════════════════════════════════════════════════════════════\n\n",
287 body
288 ))
289}
290
291fn format_bean_spec_section(bean: &Bean) -> String {
295 let mut s = String::new();
296 s.push_str("═══ BEAN ════════════════════════════════════════════════════\n");
297 s.push_str(&format!("ID: {}\n", bean.id));
298 s.push_str(&format!("Title: {}\n", bean.title));
299 s.push_str(&format!("Priority: P{}\n", bean.priority));
300 s.push_str(&format!("Status: {}\n", bean.status));
301
302 if let Some(ref verify) = bean.verify {
303 s.push_str(&format!("Verify: {}\n", verify));
304 }
305
306 if !bean.produces.is_empty() {
307 s.push_str(&format!("Produces: {}\n", bean.produces.join(", ")));
308 }
309 if !bean.requires.is_empty() {
310 s.push_str(&format!("Requires: {}\n", bean.requires.join(", ")));
311 }
312 if !bean.dependencies.is_empty() {
313 s.push_str(&format!("Dependencies: {}\n", bean.dependencies.join(", ")));
314 }
315 if let Some(ref parent) = bean.parent {
316 s.push_str(&format!("Parent: {}\n", parent));
317 }
318
319 if let Some(ref desc) = bean.description {
320 s.push_str(&format!("\n## Description\n{}\n", desc));
321 }
322 if let Some(ref acceptance) = bean.acceptance {
323 s.push_str(&format!("\n## Acceptance Criteria\n{}\n", acceptance));
324 }
325
326 s.push_str("═════════════════════════════════════════════════════════════\n\n");
327 s
328}
329
330struct DepProvider {
334 artifact: String,
335 bean_id: String,
336 bean_title: String,
337 status: String,
338 description: Option<String>,
339}
340
341fn resolve_dependency_context(beans_dir: &Path, bean: &Bean) -> Vec<DepProvider> {
344 if bean.requires.is_empty() {
345 return Vec::new();
346 }
347
348 let index = match Index::load_or_rebuild(beans_dir) {
349 Ok(idx) => idx,
350 Err(_) => return Vec::new(),
351 };
352
353 let mut providers = Vec::new();
354
355 for required in &bean.requires {
356 let producer = index.beans.iter().find(|e| {
357 e.id != bean.id && e.parent == bean.parent && e.produces.contains(required)
358 });
359
360 if let Some(entry) = producer {
361 let desc = find_bean_file(beans_dir, &entry.id)
362 .ok()
363 .and_then(|p| Bean::from_file(&p).ok())
364 .and_then(|b| b.description.clone());
365
366 providers.push(DepProvider {
367 artifact: required.clone(),
368 bean_id: entry.id.clone(),
369 bean_title: entry.title.clone(),
370 status: format!("{}", entry.status),
371 description: desc,
372 });
373 }
374 }
375
376 providers
377}
378
379fn format_dependency_section(providers: &[DepProvider]) -> Option<String> {
381 if providers.is_empty() {
382 return None;
383 }
384
385 let mut s = String::new();
386 s.push_str("═══ DEPENDENCY CONTEXT ══════════════════════════════════════\n");
387
388 for p in providers {
389 s.push_str(&format!(
390 "Bean {} ({}) produces `{}` [{}]\n",
391 p.bean_id, p.bean_title, p.artifact, p.status
392 ));
393 if let Some(ref desc) = p.description {
394 let preview: String = desc.chars().take(500).collect();
395 s.push_str(&format!("{}\n", preview));
396 if desc.len() > 500 {
397 s.push_str("...\n");
398 }
399 }
400 s.push('\n');
401 }
402
403 s.push_str("═════════════════════════════════════════════════════════════\n\n");
404 Some(s)
405}
406
407fn merge_paths(bean: &Bean) -> Vec<String> {
413 let mut seen = HashSet::new();
414 let mut result = Vec::new();
415
416 for p in &bean.paths {
417 if seen.insert(p.clone()) {
418 result.push(p.clone());
419 }
420 }
421
422 let description = bean.description.as_deref().unwrap_or("");
423 for p in extract_paths(description) {
424 if seen.insert(p.clone()) {
425 result.push(p);
426 }
427 }
428
429 result
430}
431
432pub fn cmd_context(beans_dir: &Path, id: &str, json: bool, structure_only: bool) -> Result<()> {
449 let bean_path =
450 find_bean_file(beans_dir, id).context(format!("Could not find bean with ID: {}", id))?;
451
452 let bean = Bean::from_file(&bean_path).context(format!(
453 "Failed to parse bean from: {}",
454 bean_path.display()
455 ))?;
456
457 let project_dir = beans_dir
458 .parent()
459 .ok_or_else(|| anyhow::anyhow!("Invalid .beans/ path: {}", beans_dir.display()))?;
460
461 let paths = merge_paths(&bean);
463
464 let rules = load_rules(beans_dir);
466 let attempt_notes = format_attempt_notes_section(&bean);
467 let dep_providers = resolve_dependency_context(beans_dir, &bean);
468
469 struct FileEntry {
471 path: String,
472 content: Option<String>,
473 structure: Option<String>,
474 }
475
476 let canonical_base = project_dir
477 .canonicalize()
478 .context("Cannot canonicalize project dir")?;
479
480 let mut entries: Vec<FileEntry> = Vec::new();
481 for path_str in &paths {
482 let full_path = project_dir.join(path_str);
483 let canonical = full_path.canonicalize().ok();
484
485 let in_bounds = canonical
486 .as_ref()
487 .map(|c| c.starts_with(&canonical_base))
488 .unwrap_or(false);
489
490 let content = if let Some(ref c) = canonical {
491 if in_bounds {
492 read_file(c).ok()
493 } else {
494 None
495 }
496 } else {
497 None
498 };
499
500 let structure = content
501 .as_deref()
502 .and_then(|c| extract_file_structure(path_str, c));
503
504 entries.push(FileEntry {
505 path: path_str.clone(),
506 content,
507 structure,
508 });
509 }
510
511 if json {
512 let files: Vec<serde_json::Value> = entries
513 .iter()
514 .map(|entry| {
515 let exists = entry.content.is_some();
516 let mut file_obj = serde_json::json!({
517 "path": entry.path,
518 "exists": exists,
519 });
520 if !structure_only {
521 file_obj["content"] = serde_json::Value::String(
522 entry
523 .content
524 .as_deref()
525 .unwrap_or("(not found)")
526 .to_string(),
527 );
528 }
529 if let Some(ref s) = entry.structure {
530 file_obj["structure"] = serde_json::Value::String(s.clone());
531 }
532 file_obj
533 })
534 .collect();
535
536 let dep_json: Vec<serde_json::Value> = dep_providers
537 .iter()
538 .map(|p| {
539 serde_json::json!({
540 "artifact": p.artifact,
541 "bean_id": p.bean_id,
542 "title": p.bean_title,
543 "status": p.status,
544 "description": p.description,
545 })
546 })
547 .collect();
548
549 let mut obj = serde_json::json!({
550 "id": bean.id,
551 "title": bean.title,
552 "priority": bean.priority,
553 "status": format!("{}", bean.status),
554 "verify": bean.verify,
555 "description": bean.description,
556 "acceptance": bean.acceptance,
557 "produces": bean.produces,
558 "requires": bean.requires,
559 "dependencies": bean.dependencies,
560 "parent": bean.parent,
561 "files": files,
562 "dependency_context": dep_json,
563 });
564 if let Some(ref rules_content) = rules {
565 obj["rules"] = serde_json::Value::String(rules_content.clone());
566 }
567 if let Some(ref notes) = attempt_notes {
568 obj["attempt_notes"] = serde_json::Value::String(notes.clone());
569 }
570 println!("{}", serde_json::to_string_pretty(&obj)?);
571 } else {
572 let mut output = String::new();
573
574 output.push_str(&format_bean_spec_section(&bean));
576
577 if let Some(ref notes) = attempt_notes {
579 output.push_str(notes);
580 }
581
582 if let Some(ref rules_content) = rules {
584 output.push_str(&format_rules_section(rules_content));
585 }
586
587 if let Some(dep_section) = format_dependency_section(&dep_providers) {
589 output.push_str(&dep_section);
590 }
591
592 let structure_pairs: Vec<(&str, String)> = entries
594 .iter()
595 .filter_map(|e| {
596 e.structure
597 .as_ref()
598 .map(|s| (e.path.as_str(), s.clone()))
599 })
600 .collect();
601
602 if let Some(structure_block) = format_structure_block(&structure_pairs) {
603 output.push_str(&structure_block);
604 }
605
606 if !structure_only {
608 let file_paths: Vec<String> = paths.clone();
609 if !file_paths.is_empty() {
610 let context = assemble_context(file_paths, project_dir)
611 .context("Failed to assemble context")?;
612 output.push_str(&context);
613 }
614 }
615
616 print!("{}", output);
617 }
618
619 Ok(())
620}
621#[cfg(test)]
622mod tests {
623 use super::*;
624 use std::fs;
625 use tempfile::TempDir;
626
627 fn setup_test_env() -> (TempDir, std::path::PathBuf) {
628 let dir = TempDir::new().unwrap();
629 let beans_dir = dir.path().join(".beans");
630 fs::create_dir(&beans_dir).unwrap();
631 (dir, beans_dir)
632 }
633
634 #[test]
635 fn context_with_no_paths_in_description() {
636 let (_dir, beans_dir) = setup_test_env();
637
638 let mut bean = crate::bean::Bean::new("1", "Test bean");
640 bean.description = Some("A description with no file paths".to_string());
641 let slug = crate::util::title_to_slug(&bean.title);
642 let bean_path = beans_dir.join(format!("1-{}.md", slug));
643 bean.to_file(&bean_path).unwrap();
644
645 let result = cmd_context(&beans_dir, "1", false, false);
647 assert!(result.is_ok());
648 }
649
650 #[test]
651 fn context_with_paths_in_description() {
652 let (dir, beans_dir) = setup_test_env();
653 let project_dir = dir.path();
654
655 let src_dir = project_dir.join("src");
657 fs::create_dir(&src_dir).unwrap();
658 fs::write(src_dir.join("foo.rs"), "fn main() {}").unwrap();
659
660 let mut bean = crate::bean::Bean::new("1", "Test bean");
662 bean.description = Some("Check src/foo.rs for implementation".to_string());
663 let slug = crate::util::title_to_slug(&bean.title);
664 let bean_path = beans_dir.join(format!("1-{}.md", slug));
665 bean.to_file(&bean_path).unwrap();
666
667 let result = cmd_context(&beans_dir, "1", false, false);
668 assert!(result.is_ok());
669 }
670
671 #[test]
672 fn context_bean_not_found() {
673 let (_dir, beans_dir) = setup_test_env();
674
675 let result = cmd_context(&beans_dir, "999", false, false);
676 assert!(result.is_err());
677 }
678
679 #[test]
680 fn load_rules_returns_none_when_file_missing() {
681 let (_dir, beans_dir) = setup_test_env();
682 fs::write(beans_dir.join("config.yaml"), "project: test\nnext_id: 1\n").unwrap();
684
685 let result = load_rules(&beans_dir);
686 assert!(result.is_none());
687 }
688
689 #[test]
690 fn load_rules_returns_none_when_file_empty() {
691 let (_dir, beans_dir) = setup_test_env();
692 fs::write(beans_dir.join("config.yaml"), "project: test\nnext_id: 1\n").unwrap();
693 fs::write(beans_dir.join("RULES.md"), " \n\n ").unwrap();
694
695 let result = load_rules(&beans_dir);
696 assert!(result.is_none());
697 }
698
699 #[test]
700 fn load_rules_returns_content_when_present() {
701 let (_dir, beans_dir) = setup_test_env();
702 fs::write(beans_dir.join("config.yaml"), "project: test\nnext_id: 1\n").unwrap();
703 fs::write(beans_dir.join("RULES.md"), "# My Rules\nNo unwrap.\n").unwrap();
704
705 let result = load_rules(&beans_dir);
706 assert!(result.is_some());
707 assert!(result.unwrap().contains("No unwrap."));
708 }
709
710 #[test]
711 fn load_rules_uses_custom_rules_file_path() {
712 let (_dir, beans_dir) = setup_test_env();
713 fs::write(
714 beans_dir.join("config.yaml"),
715 "project: test\nnext_id: 1\nrules_file: custom-rules.md\n",
716 )
717 .unwrap();
718 fs::write(beans_dir.join("custom-rules.md"), "Custom rules here").unwrap();
719
720 let result = load_rules(&beans_dir);
721 assert!(result.is_some());
722 assert!(result.unwrap().contains("Custom rules here"));
723 }
724
725 #[test]
726 fn format_rules_section_wraps_with_delimiters() {
727 let output = format_rules_section("# Rules\nBe nice.\n");
728 assert!(output.starts_with("═══ PROJECT RULES"));
729 assert!(output.contains("# Rules\nBe nice."));
730 assert!(
731 output.ends_with("═════════════════════════════════════════════════════════════\n\n")
732 );
733 }
734
735 fn make_bean_with_attempts() -> crate::bean::Bean {
738 use crate::bean::{AttemptOutcome, AttemptRecord};
739 let mut bean = crate::bean::Bean::new("1", "Test bean");
740 bean.attempt_log = vec![
741 AttemptRecord {
742 num: 1,
743 outcome: AttemptOutcome::Abandoned,
744 notes: Some("Tried X, hit bug Y".to_string()),
745 agent: Some("pi-agent".to_string()),
746 started_at: None,
747 finished_at: None,
748 },
749 AttemptRecord {
750 num: 2,
751 outcome: AttemptOutcome::Failed,
752 notes: Some("Fixed Y, now Z fails".to_string()),
753 agent: None,
754 started_at: None,
755 finished_at: None,
756 },
757 ];
758 bean
759 }
760
761 #[test]
762 fn format_attempt_notes_returns_none_when_no_notes() {
763 let bean = crate::bean::Bean::new("1", "Empty bean");
764 let result = format_attempt_notes_section(&bean);
766 assert!(result.is_none());
767 }
768
769 #[test]
770 fn format_attempt_notes_returns_none_when_attempts_have_no_notes() {
771 use crate::bean::{AttemptOutcome, AttemptRecord};
772 let mut bean = crate::bean::Bean::new("1", "Empty bean");
773 bean.attempt_log = vec![AttemptRecord {
774 num: 1,
775 outcome: AttemptOutcome::Abandoned,
776 notes: None,
777 agent: None,
778 started_at: None,
779 finished_at: None,
780 }];
781 let result = format_attempt_notes_section(&bean);
782 assert!(result.is_none());
783 }
784
785 #[test]
786 fn format_attempt_notes_includes_attempt_log_notes() {
787 let bean = make_bean_with_attempts();
788 let result = format_attempt_notes_section(&bean).expect("should produce output");
789 assert!(
790 result.contains("Previous Attempts"),
791 "should have section header"
792 );
793 assert!(result.contains("Attempt #1"), "should include attempt 1");
794 assert!(result.contains("pi-agent"), "should include agent name");
795 assert!(result.contains("abandoned"), "should include outcome");
796 assert!(
797 result.contains("Tried X, hit bug Y"),
798 "should include notes text"
799 );
800 assert!(result.contains("Attempt #2"), "should include attempt 2");
801 assert!(
802 result.contains("Fixed Y, now Z fails"),
803 "should include attempt 2 notes"
804 );
805 }
806
807 #[test]
808 fn format_attempt_notes_includes_bean_notes() {
809 let mut bean = crate::bean::Bean::new("1", "Test bean");
810 bean.notes = Some("Watch out for edge cases".to_string());
811 let result = format_attempt_notes_section(&bean).expect("should produce output");
812 assert!(result.contains("Watch out for edge cases"));
813 assert!(result.contains("Bean notes:"));
814 }
815
816 #[test]
817 fn format_attempt_notes_skips_empty_notes_strings() {
818 use crate::bean::{AttemptOutcome, AttemptRecord};
819 let mut bean = crate::bean::Bean::new("1", "Test bean");
820 bean.notes = Some(" ".to_string()); bean.attempt_log = vec![AttemptRecord {
822 num: 1,
823 outcome: AttemptOutcome::Abandoned,
824 notes: Some(" ".to_string()), agent: None,
826 started_at: None,
827 finished_at: None,
828 }];
829 let result = format_attempt_notes_section(&bean);
830 assert!(
831 result.is_none(),
832 "whitespace-only notes should produce no output"
833 );
834 }
835
836 #[test]
837 fn context_includes_attempt_notes_in_text_output() {
838 let (dir, beans_dir) = setup_test_env();
839 let project_dir = dir.path();
840
841 let src_dir = project_dir.join("src");
843 fs::create_dir(&src_dir).unwrap();
844 fs::write(src_dir.join("foo.rs"), "fn main() {}").unwrap();
845
846 let mut bean = make_bean_with_attempts();
848 bean.id = "1".to_string();
849 bean.description = Some("Check src/foo.rs for implementation".to_string());
850 let slug = crate::util::title_to_slug(&bean.title);
851 let bean_path = beans_dir.join(format!("1-{}.md", slug));
852 bean.to_file(&bean_path).unwrap();
853
854 let result = cmd_context(&beans_dir, "1", false, false);
856 assert!(result.is_ok());
857 }
858
859 #[test]
860 fn context_includes_attempt_notes_in_json_output() {
861 let (dir, beans_dir) = setup_test_env();
862 let project_dir = dir.path();
863
864 let src_dir = project_dir.join("src");
865 fs::create_dir(&src_dir).unwrap();
866 fs::write(src_dir.join("foo.rs"), "fn main() {}").unwrap();
867
868 let mut bean = make_bean_with_attempts();
869 bean.id = "1".to_string();
870 bean.description = Some("Check src/foo.rs for implementation".to_string());
871 let slug = crate::util::title_to_slug(&bean.title);
872 let bean_path = beans_dir.join(format!("1-{}.md", slug));
873 bean.to_file(&bean_path).unwrap();
874
875 let result = cmd_context(&beans_dir, "1", true, false);
876 assert!(result.is_ok());
877 }
878}