
ServerTest.pudu
Pudu93 lines6.7 KB
1/** @Test.Integration.ServerTest.Suite — the real server over stdio with the real compiler */2module Integration.ServerTest34import Std.Io as Io5import Std.Json as Json6import Std.List as List7import Std.Option as Option8import Std.Result as Result9import Std.Test as Test10import PuduLangMcp.Constants.Protocol as Protocol11import PuduLangMcp.Services.Process.Bounded as Bounded12import PuduLangMcp.Services.Toolchain as Toolchain13import PuduLangMcp.Utils.JsonAccess as Access141516const SESSION_MILLIS: Int = 180000171819const SESSION_CAP: Int = 16777216202122fn request(id: Int, method: Str, params: Json.Json) -> Str {23 Json.encode(&Json.object(&[("jsonrpc", Json.Text("2.0")), ("id", Json.Number(id)), ("method", Json.Text(method)), ("params", params)]))24}252627fn tool(name: Str, arguments: &Array[(Str, Json.Json)]) -> Json.Json {28 Json.object(&[("name", Json.Text(name)), ("arguments", Json.object(arguments))])29}303132fn textOf(replies: &Array[Json.Json], id: Int) -> Str {33 match List.find(replies, |reply: Json.Json| Access.integer(&reply, "id") == Some(id)) {34 case Some(reply) => Option.unwrapOr(Option.andThen(Option.andThen(Json.path(&reply, &["result", "content"]), |content: Json.Json| Json.at(&content, 0)), |block: Json.Json| Access.text(&block, "text")), Json.encode(&reply))35 case None => ""36 }37}383940fn replyAt(replies: &Array[Json.Json], index: Int) -> Json.Json {41 Option.unwrapOr(List.get(replies, index), Json.Null)42}434445fn main() -> Int {46 let toolchain = Toolchain.locate()47 let compiler = Option.unwrapOr(toolchain.compiler, "")48 let modernMeta = Json.object(&[(Protocol.META_PROTOCOL_VERSION, Json.Text(Protocol.MODERN_VERSION)), (Protocol.META_CLIENT_CAPABILITIES, Json.object(&[]))])49 let program = "module Main\n\nimport Std.Io as Io\n\n/// Doubles a number.\nfn double(value: Int) -> Int \{ value * 2 \}\n\nfn main() -> Int \{\n let _written = Io.writeLine(\"hello from the run\")\n double(21) - 42\n\}\n"50 let lines = [51 request(1, "initialize", Json.object(&[("protocolVersion", Json.Text("2025-11-25")), ("capabilities", Json.object(&[])), ("clientInfo", Json.object(&[("name", Json.Text("test")), ("version", Json.Text("1"))]))])),52 Json.encode(&Json.object(&[("jsonrpc", Json.Text("2.0")), ("method", Json.Text("notifications/initialized"))])),53 request(2, "tools/list", Json.object(&[])),54 request(3, "tools/call", tool("pudu_check", &[("source", Json.Text("module Main\n\nfn main() -> Int \{ \"x\" \}\n"))])),55 request(4, "tools/call", tool("pudu_hover", &[("source", Json.Text(program)), ("line", Json.Number(10)), ("character", Json.Number(4))])),56 request(5, "tools/call", tool("pudu_run", &[("source", Json.Text(program))])),57 request(6, "tools/call", tool("pudu_module_reference", &[("module", Json.Text("Std.Json"))])),58 request(7, "tools/call", tool("pudu_docs_search", &[("query", Json.Text("ownership"))])),59 request(8, "tools/call", tool("pudu_format", &[("source", Json.Text("module Main\nfn main()->Int\{0\}\n"))])),60 request(9, "server/discover", Json.object(&[(Protocol.META_KEY, modernMeta)])),61 request(10, "tools/call", Json.object(&[("name", Json.Text("pudu_document_symbols")), ("arguments", Json.object(&[("source", Json.Text(program))])), (Protocol.META_KEY, modernMeta)])),62 "not json"63 ]64 let session = Bounded.run(compiler, &["run", "src/Main.pudu"], lines.join("\n") + "\n", "", SESSION_MILLIS, SESSION_CAP)65 let finished = Result.unwrapOr(session, Bounded.Finished{status: -99, output: "", errors: "", timedOut: false, truncated: false, millis: 0})66 let written = finished.output.split("\n").filter(|line: Str| !line.trim().isEmpty())67 let replies = written.map(|line: Str| Result.unwrapOr(Json.decode(line), Json.Null))68 let ids = replies.map(|reply: Json.Json| Access.member(&reply, "id"))69 let skip = compiler.isEmpty()70 let checks = Test.suite("Integration.Server", &[71 Test.skipIf(skip, "no pudu compiler on this machine", &Test.equals("the server exits cleanly at end of input", &finished.status, &0)),72 Test.skipIf(skip, "no pudu compiler on this machine", &Test.equals("every request gets exactly one line, and nothing else is written", &written.length(), &11)),73 Test.skipIf(skip, "no pudu compiler on this machine", &Test.that("every line is JSON", replies.filter(|reply: Json.Json| reply == Json.Null).isEmpty())),74 Test.skipIf(skip, "no pudu compiler on this machine", &Test.equals("replies keep request order", &List.take(&ids, 3), &[Some(Json.Number(1)), Some(Json.Number(2)), Some(Json.Number(3))])),75 Test.skipIf(skip, "no pudu compiler on this machine", &Test.that("initialize echoes the version", Json.encode(&replyAt(&replies, 0)).contains("\"protocolVersion\":\"2025-11-25\""))),76 Test.skipIf(skip, "no pudu compiler on this machine", &Test.that("every tool is listed", Json.encode(&replyAt(&replies, 1)).contains("pudu_workspace_symbols"))),77 Test.skipIf(skip, "no pudu compiler on this machine", &Test.that("check answers the real diagnostic", textOf(&replies, 3).contains("E3001"))),78 Test.skipIf(skip, "no pudu compiler on this machine", &Test.that("hover answers the real type", textOf(&replies, 4).contains("fn(Int) -> Int"))),79 Test.skipIf(skip, "no pudu compiler on this machine", &Test.that("a confined run prints and exits", textOf(&replies, 5).startsWith("exit status 0") && textOf(&replies, 5).contains("hello from the run"))),80 Test.skipIf(skip, "no pudu compiler on this machine", &Test.that("the module reference comes from the installed library", textOf(&replies, 6).contains("fn Std.Json.decode"))),81 Test.skipIf(skip, "no pudu compiler on this machine", &Test.that("the documentation answers from the corpus", textOf(&replies, 7).contains("docs/ownership"))),82 Test.skipIf(skip, "no pudu compiler on this machine", &Test.that("format lays the code out", textOf(&replies, 8).contains("fn main() -> Int \{ 0 \}"))),83 Test.skipIf(skip, "no pudu compiler on this machine", &Test.that("a modern request is served inside a legacy process", Json.encode(&replyAt(&replies, 8)).contains("supportedVersions"))),84 Test.skipIf(skip, "no pudu compiler on this machine", &Test.that("document symbols come from the language server", textOf(&replies, 10).contains("function double"))),85 Test.skipIf(skip, "no pudu compiler on this machine", &Test.equals("garbage is answered with a parse error", &Access.pathInt(&replyAt(&replies, 10), &["error", "code"]), &Some(Protocol.PARSE_ERROR)))86 ])87 let report = Test.run(&checks)88 for failure in Test.failuresOf(&report) {89 let _reported = Io.writeErrorLine(failure)90 }91 Test.report(&report)92}93