- 2475
fn plan_step_options_are_typed_and_reach_the_text_form() { - 2476
let plan = structured_outputs_from_text( - 2477
r#"{"semantic_type":"plan.timeline","payload":{"title":"Saturday","items":[{"label":"Morning","time":"9:00–12:00","detail":"Pancakes, then the park"},{"label":"Afternoon","detail":"Pick one","options":[{"label":"Science museum","detail":"Hands-on exhibits","facts":["20 min away","2 hours"]},{"label":"Botanic garden"}]}]}}"#, - 2478
); - 2479
assert_eq!(plan.len(), 1, "a step with options is a valid plan"); - 2480
let text = structured_markdown(&plan[0]); - 2481
assert!(text.contains("- Morning (9:00–12:00): Pancakes, then the park")); - 2482
assert!( - 2483
text.contains(" - Option: Science museum: Hands-on exhibits · 20 min away, 2 hours") - 2484
); - 2485
assert!(text.contains(" - Option: Botanic garden")); - 2486
- 2487
for malformed in [ - 2488
r#"{"label":"Afternoon","options":"museum or garden"}"#, - 2489
r#"{"label":"Afternoon","options":[{"detail":"no label"}]}"#, - 2490
r#"{"label":"Afternoon","options":[{"label":"Museum","facts":"far"}]}"#, - 2491
r#"{"label":"Afternoon","time":3}"#, - 2492
] { - 2493
let source = format!( - 2494
r#"{{"semantic_type":"plan.timeline","payload":{{"title":"Saturday","items":[{malformed}]}}}}"# - 2495
); - 2496
assert!( - 2497
structured_outputs_from_text(&source).is_empty(), - 2498
"rejected: {malformed}" - 2499
); - 2500
} - 2501
} - 2502
- 2503
#[test] - 2504
fn universal_recipes_selection_from_signals_and_context() { - 2505
let catalog = built_in_recipes(); - 2506
- 2507
// 1. Research synthesis - 2508
let research_text = "Key takeaways from our findings:\n1. Rust is fast.\nSources:\nhttps://example.com/rust"; - 2509
let sigs = signals_from_text(research_text); - 2510
let decision = catalog - 2511
.choose(&sigs, "desktop") - 2512
.expect("research synthesis decision"); - 2513
assert_eq!(decision.recipe_id, "research.synthesis"); - 2514
- 2515
// 2. Diff inspector - 2516
let diff_text = "Files changed:\n```diff\n@@ -1,2 +1,3 @@\n+added\n```"; - 2517
let sigs = signals_from_text(diff_text); - 2518
let decision = catalog - 2519
.choose(&sigs, "desktop") - 2520
.expect("diff inspector decision"); - 2521
assert_eq!(decision.recipe_id, "coding.diff_inspector"); - 2522
- 2523
// 3. Test report - 2524
let test_text = "Test suite executed: 42 passed, 0 failures."; - 2525
let sigs = signals_from_text(test_text); - 2526
assert!(catalog.choose(&sigs, "desktop").is_none()); - 2527
- 2528
// 4. Terminal session from bash tool context - 2529
let cmd = serde_json::json!({"command": "docker ps -a"}); - 2530
let ctx = SignalContext { - 2531
text: "Container status: exit 0", - 2532
tool_name: Some("bash"), - 2533
tool_input: Some(&cmd), - 2534
tool_output: Some("CONTAINER ID IMAGE STATUS"), - 2535
is_error: false, - 2536
domains: &[], - 2537
}; - 2538
let sigs = signals_from_context(&ctx); - 2539
let decision = catalog.choose(&sigs, "desktop").expect("terminal decision"); - 2540
assert_eq!(decision.recipe_id, "terminal.session"); - 2541
- 2542
// 5. Culinary recipe - 2543
let recipe_text = - 2544
"Recipe for Salmon:\nPrep time: 10m\nIngredients:\n- 2 fillets\n- 2 tbsp olive oil"; - 2545
let sigs = signals_from_text(recipe_text); - 2546
let decision = catalog.choose(&sigs, "desktop").expect("recipe decision"); - 2547
assert_eq!(decision.recipe_id, "lifestyle.culinary_recipe"); - 2548
- 2549
// 6. Data spreadsheet grid - 2550
let table_text = "Dataset breakdown:\n| MRR | Growth |\n| --- | --- |\n| $10k | +20% |"; - 2551
let sigs = signals_from_text(table_text); - 2552
let decision = catalog.choose(&sigs, "desktop").expect("grid decision"); - 2553
assert_eq!(decision.recipe_id, "data.spreadsheet_grid"); - 2554
- 2555
// Incidental substrings in news prose must not select a coding report. - 2556
let news = "Protests continued while the market surpassed expectations."; - 2557
let sigs = signals_from_text(news); - 2558
assert!(!sigs.iter().any(|signal| signal == "tests")); - 2559
assert!(!sigs.iter().any(|signal| signal == "pass_fail")); - 2560
assert_ne!( - 2561
catalog.choose(&sigs, "desktop").map(|d| d.recipe_id), - 2562
Some("coding.test_report".into()) - 2563
); - 2564
- 2565
// 7. Multi chart - 2566
let chart_text = - 2567
"Telemetry metrics chart showing p99 latency trend and req/sec throughput."; - 2568
let sigs = signals_from_text(chart_text); - 2569
let decision = catalog.choose(&sigs, "desktop").expect("chart decision"); - 2570
assert_eq!(decision.recipe_id, "data.multi_chart"); - 2571
} - 2572
- 2573
#[test] - 2574
fn accepted_plugin_output_surfaces_declared_outcome_requirements() { - 2575
let mut skills = SkillRegistry::default(); - 2576
let mut renderers = BTreeMap::new(); - 2577
renderers.insert( - 2578
"desktop".into(), - 2579
RendererBinding { - 2580
renderer: "native:custom".into(), - 2581
interactive: false, - 2582
requires: Vec::new(), - 2583
}, - 2584
); - 2585
assert!( - 2586
skills - 2587
.register(PresentationSkillManifest { - 2588
id: "planning".into(), - 2589
version: "1.0.0".into(), - 2590
api: PRESENTATION_SKILL_API.into(), - 2591
provides: vec!["plan".into()], - 2592
renderers, - 2593
schema: None, - 2594
outcome_requirements: vec![OutcomeRequirementDeclaration { - 2595
id: "plan-next-steps".into(), - 2596
kind: "constraint".into(), - 2597
description: "include next steps".into(), - 2598
importance: "must".into(), - 2599
}], - 2600
}) - 2601
.is_ok() - 2602
); - 2603
let planner = PresentationPlanner { - 2604
skills, - 2605
recipes: RecipeCatalog::default(), - 2606
}; - 2607
let plan = planner.plan( - 2608
&[], - 2609
"desktop", - 2610
&[], - 2611
&[StructuredOutput { - 2612
semantic_type: "plan".into(), - 2613
schema_version: crate::PRESENTATION_SCHEMA_VERSION, - 2614
skill_id: "planning".into(), - 2615
skill_version: "1.0.0".into(), - 2616
payload: serde_json::json!({"steps": []}), - 2617
}], - 2618
); - 2619
assert_eq!(plan.outcome_requirements.len(), 1); - 2620
assert_eq!(plan.outcome_requirements[0].id, "plan-next-steps"); - 2621
let mut outcome = vak_intent::OutcomeSpec::from_reading( - 2622
"make a plan", - 2623
&vak_intent::Reading::general(), - 2624
1, - 2625
); - 2626
assert!(plan.merge_outcome_requirements(&mut outcome).is_empty()); - 2627
assert!( - 2628
outcome - 2629
.requirements - 2630
.iter() - 2631
.any(|requirement| requirement.id == "plan-next-steps") - 2632
); - 2633
} - 2634
} - 2635
- 2636
#[cfg(test)] - 2637
mod structural_signal_tests { - 2638
use super::signals_from_text; - 2639
- 2640
fn has(text: &str, signal: &str) -> bool { - 2641
signals_from_text(text).iter().any(|s| s == signal) - 2642
} - 2643
- 2644
#[test] - 2645
fn a_real_markdown_table_is_tabular_even_when_indented_or_unspaced() { - 2646
assert!(has( - 2647
"| Index | Change |\n|---|---|\n| Nifty | -0.22% |\n", - 2648
"tabular" - 2649
)); - 2650
assert!(has( - 2651
"Results:\n\n | a | b |\n | - | - |\n | 1 | 2 |\n", - 2652
"tabular" - 2653
)); - 2654
} - 2655
- 2656
#[test] - 2657
fn table_syntax_inside_a_code_sample_is_not_a_table() { - 2658
let text = "Markdown tables look like this:\n\n```\n| a | b |\n|---|---|\n```\n"; - 2659
assert!( - 2660
!has(text, "tabular"), - 2661
"a table shown as source is not a table" - 2662
); - 2663
assert!(!has("use `|---|` as the separator row", "tabular")); - 2664
} - 2665
- 2666
#[test] - 2667
fn tab_separated_rows_are_tabular_but_one_stray_tab_is_not() { - 2668
assert!(has("name\tqty\napple\t3\npear\t5\n", "tabular")); - 2669
assert!(!has("He paused,\tthen left.", "tabular")); - 2670
} - 2671
- 2672
#[test] - 2673
fn diffs_are_recognised_by_structure() { - 2674
assert!(has("```diff\n- old\n+ new\n```\n", "diff")); - 2675
assert!(has("diff --git a/x b/x\n--- a/x\n+++ b/x\n", "diff")); - 2676
assert!(has("@@ -1,2 +1,3 @@ fn main\n line\n", "diff")); - 2677
assert!( - 2678
!has("the @@ sign and diff of opinions", "diff"), - 2679
"prose mentioning them is not a diff" - 2680
); - 2681
} - 2682
} - 2683
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.