Skip to content

Commit db93cae

Browse files
senamakelmedullabot
andcommitted
test: isolate pformat parser coverage
Co-authored-by: Medulla <medulla@tinyhumans.ai>
1 parent ea579cc commit db93cae

2 files changed

Lines changed: 316 additions & 316 deletions

File tree

‎crates/tinytools-agent/src/pformat.rs‎

Lines changed: 2 additions & 316 deletions
Original file line numberDiff line numberDiff line change
@@ -441,319 +441,5 @@ fn coerce_value(raw: &str, ty: PFormatParamType) -> Value {
441441
// ──────────────────────────────────────────────────────────────────────
442442

443443
#[cfg(test)]
444-
mod tests {
445-
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
446-
447-
use super::*;
448-
use serde_json::json;
449-
450-
fn make_registry() -> PFormatRegistry {
451-
let mut reg = PFormatRegistry::new();
452-
reg.insert(
453-
"get_weather".to_string(),
454-
PFormatToolParams::from_schema(&json!({
455-
"type": "object",
456-
"properties": {
457-
"location": { "type": "string" },
458-
"unit": { "type": "string" }
459-
}
460-
})),
461-
);
462-
reg.insert(
463-
"shell".to_string(),
464-
PFormatToolParams::from_schema(&json!({
465-
"type": "object",
466-
"properties": {
467-
"command": { "type": "string" }
468-
}
469-
})),
470-
);
471-
reg.insert(
472-
"ping".to_string(),
473-
PFormatToolParams::from_schema(&json!({
474-
"type": "object",
475-
"properties": {}
476-
})),
477-
);
478-
reg.insert(
479-
"math".to_string(),
480-
PFormatToolParams::from_schema(&json!({
481-
"type": "object",
482-
"properties": {
483-
"x": { "type": "integer" },
484-
"y": { "type": "number" },
485-
"verbose": { "type": "boolean" }
486-
}
487-
})),
488-
);
489-
reg
490-
}
491-
492-
#[test]
493-
fn renders_zero_arg_signature() {
494-
let reg = make_registry();
495-
assert_eq!(render_signature("ping", &reg["ping"]), "ping[]");
496-
}
497-
498-
#[test]
499-
fn renders_multi_arg_signature() {
500-
let reg = make_registry();
501-
assert_eq!(
502-
render_signature("get_weather", &reg["get_weather"]),
503-
"get_weather[0|<location>|1|<unit>]"
504-
);
505-
}
506-
507-
#[test]
508-
fn parses_simple_call() {
509-
let reg = make_registry();
510-
let (name, args) = parse_call("get_weather[0|London|1|metric]", &reg).unwrap();
511-
assert_eq!(name, "get_weather");
512-
assert_eq!(args, json!({"location": "London", "unit": "metric"}));
513-
}
514-
515-
#[test]
516-
fn parses_zero_arg_call() {
517-
let reg = make_registry();
518-
let (name, args) = parse_call("ping[]", &reg).unwrap();
519-
assert_eq!(name, "ping");
520-
assert_eq!(args, json!({}));
521-
}
522-
523-
#[test]
524-
fn parses_single_arg_with_spaces() {
525-
let reg = make_registry();
526-
let (name, args) = parse_call("shell[0|ls -la /tmp]", &reg).unwrap();
527-
assert_eq!(name, "shell");
528-
assert_eq!(args, json!({"command": "ls -la /tmp"}));
529-
}
530-
531-
#[test]
532-
fn handles_pipe_escape() {
533-
let reg = make_registry();
534-
let (_, args) = parse_call(r"shell[0|cat foo \| grep bar]", &reg).unwrap();
535-
assert_eq!(args, json!({"command": "cat foo | grep bar"}));
536-
}
537-
538-
#[test]
539-
fn handles_bracket_escape() {
540-
let reg = make_registry();
541-
let (_, args) = parse_call(r"shell[0|echo \]done\]]", &reg).unwrap();
542-
assert_eq!(args, json!({"command": "echo ]done]"}));
543-
}
544-
545-
#[test]
546-
fn handles_backslash_escape() {
547-
let reg = make_registry();
548-
let (_, args) = parse_call(r"shell[0|C:\\Users\\bob]", &reg).unwrap();
549-
assert_eq!(args, json!({"command": r"C:\Users\bob"}));
550-
}
551-
552-
#[test]
553-
fn coerces_typed_arguments() {
554-
let reg = make_registry();
555-
// No `required` list on `math`, so all three are optional and sort
556-
// alphabetically: verbose, x, y. The signature the model sees is
557-
// `math[0|<verbose>|1|<x>|2|<y>]`, so these are the slots it would emit.
558-
let (_, args) = parse_call("math[0|true|1|42|2|2.75]", &reg).unwrap();
559-
assert_eq!(args, json!({"verbose": true, "x": 42, "y": 2.75}));
560-
}
561-
562-
#[test]
563-
fn coercion_falls_back_to_string_on_failure() {
564-
let reg = make_registry();
565-
let (_, args) = parse_call("math[0|maybe|1|notanumber|2|alsonotanumber]", &reg).unwrap();
566-
assert_eq!(
567-
args,
568-
json!({
569-
"verbose": "maybe",
570-
"x": "notanumber",
571-
"y": "alsonotanumber"
572-
})
573-
);
574-
}
575-
576-
#[test]
577-
fn optional_only_signature_is_alphabetical() {
578-
let reg = make_registry();
579-
// `math` declares no `required`, so every parameter is optional and the
580-
// layout falls back to `BTreeMap` order: {verbose, x, y}.
581-
assert_eq!(
582-
render_signature("math", &reg["math"]),
583-
"math[0|<verbose>|1|<x>|2|<y>]"
584-
);
585-
}
586-
587-
#[test]
588-
fn rejects_unknown_tool() {
589-
let reg = make_registry();
590-
assert!(parse_call("nope[arg]", &reg).is_none());
591-
}
592-
593-
#[test]
594-
fn rejects_missing_brackets() {
595-
let reg = make_registry();
596-
assert!(parse_call("get_weather London metric", &reg).is_none());
597-
}
598-
599-
#[test]
600-
fn rejects_trailing_garbage() {
601-
let reg = make_registry();
602-
// Closing bracket isn't last char → invalid p-format, dispatcher
603-
// should try the JSON fallback path.
604-
assert!(parse_call("get_weather[0|London|1|metric] // comment", &reg).is_none());
605-
}
606-
607-
#[test]
608-
fn rejects_a_slot_the_schema_has_no_parameter_for() {
609-
let reg = make_registry();
610-
// `get_weather` has two slots, 0 and 1. Slot 2 is not a value to drop —
611-
// it means the model is working from a layout this schema does not have,
612-
// so every *other* slot in the call is suspect too.
613-
assert!(parse_call("get_weather[0|London|1|metric|2|extra]", &reg).is_none());
614-
}
615-
616-
#[test]
617-
fn an_empty_value_omits_the_key_rather_than_sending_a_blank() {
618-
let reg = make_registry();
619-
let (_, args) = parse_call("get_weather[0|London|1|]", &reg).unwrap();
620-
// `unit` is absent, not `""`. A blank string is what the old form sent,
621-
// and for a typed parameter it fails schema validation naming a field the
622-
// model deliberately left empty — an error it cannot act on.
623-
assert_eq!(args, json!({"location": "London"}));
624-
}
625-
626-
// ── Indexed slots: the behaviour the format exists for ──────────────────
627-
628-
fn sparse_registry() -> PFormatRegistry {
629-
let mut reg = PFormatRegistry::new();
630-
// Mirrors the shape that produced the live failures: one required
631-
// parameter, several optional ones that sort ahead of it alphabetically.
632-
reg.insert(
633-
"list_threads".to_string(),
634-
PFormatToolParams::from_schema(&json!({
635-
"type": "object",
636-
"properties": {
637-
"max_results": { "type": "integer" },
638-
"query": { "type": "string" },
639-
"user_id": { "type": "string" },
640-
"verbose": { "type": "boolean" }
641-
},
642-
"required": ["query"]
643-
})),
644-
);
645-
reg
646-
}
647-
648-
#[test]
649-
fn required_parameters_are_numbered_first() {
650-
let reg = sparse_registry();
651-
// Alphabetically `query` would be slot 1, behind `max_results`. Required
652-
// first puts it at 0, which is what makes the one-argument call
653-
// `list_threads[0|…]` rather than an index the model has to look up.
654-
assert_eq!(
655-
render_signature("list_threads", &reg["list_threads"]),
656-
"list_threads[0|<query>|1|<max_results>|2|<user_id>|3|<verbose>]"
657-
);
658-
}
659-
660-
#[test]
661-
fn a_sparse_call_sends_only_the_slots_it_names() {
662-
let reg = sparse_registry();
663-
let (_, args) = parse_call("list_threads[0|from:alice|1|50]", &reg).unwrap();
664-
// No empty slots to count, and the absent parameters are absent rather
665-
// than blank.
666-
assert_eq!(args, json!({"query": "from:alice", "max_results": 50}));
667-
}
668-
669-
#[test]
670-
fn the_live_misbinding_is_now_a_refusal_rather_than_a_wrong_call() {
671-
let reg = sparse_registry();
672-
// The failure this format replaces: a bare-positional call whose leading
673-
// delimiter count was off by one bound the search text to `user_id` and
674-
// *ran*. In the indexed form the same body has no numeric index at slot
675-
// 0, so it is rejected and the model is told, instead of a wrong call
676-
// succeeding.
677-
assert!(parse_call("list_threads[|||from:alice]", &reg).is_none());
678-
}
679-
680-
#[test]
681-
fn rejects_a_bare_positional_call() {
682-
let reg = make_registry();
683-
// Deliberate: parsing this positionally is exactly the silent misbinding
684-
// the indices exist to end, so the old form is refused rather than
685-
// accepted for compatibility.
686-
assert!(parse_call("get_weather[London|metric]", &reg).is_none());
687-
}
688-
689-
#[test]
690-
fn rejects_an_odd_token_count() {
691-
let reg = make_registry();
692-
// A dropped or added delimiter. Keeping the pairs that happen to line up
693-
// would put the off-by-one straight back.
694-
assert!(parse_call("get_weather[0|London|1]", &reg).is_none());
695-
}
696-
697-
#[test]
698-
fn rejects_a_non_numeric_index() {
699-
let reg = make_registry();
700-
assert!(parse_call("get_weather[location|London]", &reg).is_none());
701-
}
702-
703-
#[test]
704-
fn a_repeated_slot_takes_the_last_value() {
705-
let reg = make_registry();
706-
// Rare enough not to be worth failing the whole call over, and the later
707-
// value is the model's latest intent.
708-
let (_, args) = parse_call("get_weather[0|London|0|Berlin]", &reg).unwrap();
709-
assert_eq!(args, json!({"location": "Berlin"}));
710-
}
711-
712-
#[test]
713-
fn an_empty_repeated_slot_retracts_the_earlier_value() {
714-
let reg = make_registry();
715-
// Both documented rules meet here: a repeated slot takes its last value,
716-
// and an empty value means "not sent". So the second `0|` retracts the
717-
// first rather than being skipped — otherwise the last-write rule holds
718-
// everywhere except the one case where the last write is a retraction.
719-
let (_, args) = parse_call("get_weather[0|London|0|]", &reg).unwrap();
720-
assert_eq!(args, json!({}));
721-
722-
// And it retracts only its own slot.
723-
let (_, args) = parse_call("get_weather[0|London|1|metric|0|]", &reg).unwrap();
724-
assert_eq!(args, json!({"unit": "metric"}));
725-
}
726-
727-
#[test]
728-
fn an_empty_body_is_a_call_with_no_arguments() {
729-
let reg = make_registry();
730-
// Distinct from `ping[]`: `get_weather` *has* slots, and sending none of
731-
// them is a valid (if incomplete) call the schema validator should judge,
732-
// not a parse failure.
733-
let (name, args) = parse_call("get_weather[]", &reg).unwrap();
734-
assert_eq!(name, "get_weather");
735-
assert_eq!(args, json!({}));
736-
}
737-
738-
#[test]
739-
fn an_escaped_pipe_stays_inside_its_value() {
740-
let reg = make_registry();
741-
// The index/value split runs over `split_pipes`, which honours escapes —
742-
// so an escaped pipe is part of one value and does not shift the pairing.
743-
let (_, args) = parse_call(r"shell[0|cat a \| grep b]", &reg).unwrap();
744-
assert_eq!(args, json!({"command": "cat a | grep b"}));
745-
}
746-
747-
#[test]
748-
fn signature_round_trips_with_parser() {
749-
let reg = make_registry();
750-
let sig = render_signature("get_weather", &reg["get_weather"]);
751-
// Render uses the same identifier the parser expects.
752-
assert!(sig.starts_with("get_weather["));
753-
let synthesised = "get_weather[0|Berlin|1|imperial]";
754-
let (name, args) = parse_call(synthesised, &reg).unwrap();
755-
assert_eq!(name, "get_weather");
756-
assert_eq!(args["location"], json!("Berlin"));
757-
assert_eq!(args["unit"], json!("imperial"));
758-
}
759-
}
444+
#[path = "pformat_test.rs"]
445+
mod tests;

0 commit comments

Comments
 (0)