()
| 538 | |
| 539 | #[tokio::test] |
| 540 | async fn phase_emits_budget_exhausted_when_capped() { |
| 541 | use crate::budget::BudgetGuard; |
| 542 | use crate::llm::TokenUsage; |
| 543 | |
| 544 | let budget = Arc::new(WorkflowBudget::new(Some(10))); |
| 545 | // Spend past the cap before the phase runs (simulates child loops having |
| 546 | // already recorded usage into the shared ledger). |
| 547 | budget |
| 548 | .record_after_llm( |
| 549 | "s", |
| 550 | &TokenUsage { |
| 551 | total_tokens: 12, |
| 552 | ..Default::default() |
| 553 | }, |
| 554 | ) |
| 555 | .await; |
| 556 | |
| 557 | let wf = Workflow::builder(Arc::new(EchoExecutor::new())) |
| 558 | .with_budget(Arc::clone(&budget)) |
| 559 | .build(); |
| 560 | let mut rx = wf.subscribe(); |
| 561 | wf.phase("p", vec![spec("a", "explore", "p")]).await; |
| 562 | |
| 563 | let mut saw_exhausted = false; |
| 564 | while let Ok(ev) = rx.try_recv() { |
| 565 | if let WorkflowEvent::BudgetExhausted { resource, .. } = ev { |
| 566 | assert_eq!(resource, "workflow_tokens"); |
| 567 | saw_exhausted = true; |
| 568 | } |
| 569 | } |
| 570 | assert!( |
| 571 | saw_exhausted, |
| 572 | "phase boundary emits BudgetExhausted once capped" |
| 573 | ); |
| 574 | assert_eq!(wf.budget_snapshot().unwrap().consumed_tokens, 12); |
| 575 | } |
| 576 | |
| 577 | #[tokio::test] |
| 578 | async fn no_budget_means_no_snapshot_and_no_exhausted_event() { |
nothing calls this directly
no test coverage detected