
ArgumentsTest.pudu
Pudu81 lines5.4 KB
1/** @Test.Domain.Catalog.ArgumentsTest.Suite — tool argument validation */2module PuduLangMcp.Domain.Catalog.ArgumentsTest34import Std.Io as Io5import Std.Json as Json6import Std.Test as Test7import PuduLangMcp.Domain.Catalog.Arguments as Arguments8import PuduLangMcp.Domain.Catalog.ToolSpec as ToolSpec9import PuduLangMcp.Errors.ToolError as ToolError101112fn sample() -> ToolSpec.ToolSpec {13 ToolSpec.ToolSpec {14 name: "sample",15 title: "Sample",16 description: "A tool for tests.",17 properties: [18 ToolSpec.text("query", "What to find.", true),19 ToolSpec.integer("limit", "How many.", false, 1, 10),20 ToolSpec.text("source", "Code.", false),21 ToolSpec.text("path", "A file.", false)22 ],23 exactlyOne: ["source", "path"],24 annotations: ToolSpec.Annotations{readOnly: true, destructive: false, idempotent: true, openWorld: false}25 }26}272829fn triple() -> ToolSpec.ToolSpec {30 ToolSpec.ToolSpec{..sample(), properties: [ToolSpec.text("a", "A.", false), ToolSpec.text("b", "B.", false), ToolSpec.text("c", "C.", false)], exactlyOne: ["a", "b", "c"]}31}323334fn tripleAccepts(members: &Array[(Str, Json.Json)]) -> Bool {35 Arguments.validate(&triple(), &Json.object(members)) != Err(ToolError.ExactlyOne(["a", "b", "c"]))36}373839fn failureOf(members: &Array[(Str, Json.Json)]) -> Option[ToolError.ToolFailure] {40 match Arguments.validate(&sample(), &Json.object(members)) {41 case Ok(_) => None42 case Err(failure) => Some(failure)43 }44}454647fn main() -> Int {48 let valid = [("query", Json.Text("x")), ("source", Json.Text("module Main"))]49 let accepted = Arguments.validate(&sample(), &Json.object(&valid.push(("limit", Json.Number(3)))))50 let checks = Test.suite("Domain.Catalog.Arguments", &[51 Test.succeeded("valid arguments are accepted", &accepted),52 Test.equals("a text argument is read", &match accepted { case Ok(args) => Arguments.text(&args, "query") case Err(_) => None }, &Some("x")),53 Test.equals("an integer argument is read", &match accepted { case Ok(args) => Arguments.integerOr(&args, "limit", 0) case Err(_) => 0 }, &3),54 Test.equals("an absent integer takes its fallback", &match Arguments.validate(&sample(), &Json.object(&valid)) { case Ok(args) => Arguments.integerOr(&args, "limit", 7) case Err(_) => 0 }, &7),55 Test.equals("null arguments are an empty object", &match Arguments.validate(&sample(), &Json.Null) { case Err(failure) => Some(failure) case Ok(_) => None }, &Some(ToolError.MissingArgument("query"))),56 Test.equals("a non-object is refused", &match Arguments.validate(&sample(), &Json.list(&[])) { case Err(failure) => Some(failure) case Ok(_) => None }, &Some(ToolError.NotAnObject)),57 Test.equals("an undeclared argument is refused", &failureOf(&valid.push(("limt", Json.Number(3)))), &Some(ToolError.UnknownArgument("limt"))),58 Test.equals("a missing required argument is refused", &failureOf(&[("source", Json.Text("m"))]), &Some(ToolError.MissingArgument("query"))),59 Test.equals("a blank required text is refused", &failureOf(&[("query", Json.Text(" ")), ("source", Json.Text("m"))]), &Some(ToolError.WrongType("query", "a non-empty string"))),60 Test.equals("a number for a text is refused", &failureOf(&[("query", Json.Number(1)), ("source", Json.Text("m"))]), &Some(ToolError.WrongType("query", "a string"))),61 Test.equals("text for an integer is refused", &failureOf(&valid.push(("limit", Json.Text("3")))), &Some(ToolError.WrongType("limit", "an integer"))),62 Test.equals("a fraction for an integer is refused", &failureOf(&valid.push(("limit", Json.Fractional("2.5")))), &Some(ToolError.WrongType("limit", "an integer"))),63 Test.equals("null for an argument is the wrong kind", &failureOf(&valid.push(("limit", Json.Null))), &Some(ToolError.WrongType("limit", "an integer"))),64 Test.equals("an integer below its minimum is refused", &failureOf(&valid.push(("limit", Json.Number(0)))), &Some(ToolError.OutOfRange("limit", "between 1 and 10"))),65 Test.equals("an integer above its maximum is refused", &failureOf(&valid.push(("limit", Json.Number(11)))), &Some(ToolError.OutOfRange("limit", "between 1 and 10"))),66 Test.equals("the minimum itself is accepted", &failureOf(&valid.push(("limit", Json.Number(1)))), &None),67 Test.equals("the maximum itself is accepted", &failureOf(&valid.push(("limit", Json.Number(10)))), &None),68 Test.equals("neither of an exclusive pair is refused", &failureOf(&[("query", Json.Text("x"))]), &Some(ToolError.ExactlyOne(["source", "path"]))),69 Test.equals("both of an exclusive pair are refused", &failureOf(&[("query", Json.Text("x")), ("source", Json.Text("m")), ("path", Json.Text("a.pudu"))]), &Some(ToolError.ExactlyOne(["source", "path"]))),70 Test.that("one of three exclusive names is accepted", tripleAccepts(&[("b", Json.Text("x"))])),71 Test.not("two of three exclusive names are refused", tripleAccepts(&[("a", Json.Text("x")), ("c", Json.Text("y"))])),72 Test.not("none of three exclusive names is refused", tripleAccepts(&[])),73 Test.equals("a blank optional text is accepted", &failureOf(&[("query", Json.Text("x")), ("path", Json.Text(""))]), &None)74 ])75 let ran = Test.run(&checks)76 for failure in Test.failuresOf(&ran) {77 let _reported = Io.writeErrorLine(failure)78 }79 Test.report(&ran)80}81