Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-mcp

Model Context Protocol server for Pudu language documentation and compiler tools

0.1.1Apache-2.01

InstallClose

ConversationTest.pudu

Pudu68 lines5.1 KB

GitHub ↗
1/** @Test.Domain.Lsp.ConversationTest.Suite — framing and transcript reading */2module PuduLangMcp.Domain.Lsp.ConversationTest34import Std.Io as Io5import Std.Json as Json6import Std.Test as Test7import PuduLangMcp.Domain.Lsp.Conversation as Conversation8import PuduLangMcp.Utils.JsonAccess as Access910/// A question at a 1-based position.11fn question(method: Str, line: Int, character: Int) -> Conversation.Question {12  Conversation.Question{method: method, line: line, character: character, newName: "renamed", query: "dou"}13}1415/// A framed message as a server would write it.16fn framed(body: Str) -> Str {17  "Content-Length: " + show(body.toBytes().length()) + "\r\n\r\n" + body18}1920/// Runs the suite.21fn main() -> Int {22  let hover = Conversation.params("file:///a.pudu", &question("textDocument/hover", 3, 5))23  let references = Conversation.params("file:///a.pudu", &question("textDocument/references", 1, 1))24  let rename = Conversation.params("file:///a.pudu", &question("textDocument/rename", 1, 1))25  let action = Conversation.params("file:///a.pudu", &question("textDocument/codeAction", 2, 2))26  let symbols = Conversation.params("file:///a.pudu", &question("workspace/symbol", 1, 1))27  let outline = Conversation.params("file:///a.pudu", &question("textDocument/documentSymbol", 1, 1))28  let clamped = Conversation.params("file:///a.pudu", &question("textDocument/hover", 0, -4))29  let said = Conversation.conversation("file:///", "file:///a.pudu", "module A", &question("textDocument/hover", 1, 1))30  let symbolsWithSource = Conversation.conversation("file:///", "file:///a.pudu", "module A", &question("workspace/symbol", 1, 1))31  let completing = Conversation.params("file:///a.pudu", &question("textDocument/completion", 1, 1))32  let origin = Conversation.params("file:///a.pudu", &question("textDocument/hover", 0, 0))33  let unopened = Conversation.conversation("file:///", "", "", &question("workspace/symbol", 1, 1))34  let transcript = framed("\{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\{\}\}") + framed("\{\"jsonrpc\":\"2.0\",\"method\":\"textDocument/publishDiagnostics\",\"params\":\{\"diagnostics\":[\{\"message\":\"m\"\}]\}\}") + framed("\{\"jsonrpc\":\"2.0\",\"id\":2,\"result\":\{\"contents\":\"é\"\}\}") + framed("garbage")35  let answer = Conversation.answerIn(transcript)36  let refused = Conversation.answerIn(framed("\{\"jsonrpc\":\"2.0\",\"id\":2,\"error\":\{\"code\":-32601,\"message\":\"no such method\"\}\}"))37  let silent = Conversation.answerIn(framed("\{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\{\}\}"))38  let nulled = Conversation.answerIn(framed("\{\"jsonrpc\":\"2.0\",\"id\":2,\"result\":null\}"))39  let checks = Test.suite("Domain.Lsp.Conversation", &[40      Test.equals("positions become 0-based", &Access.pathInt(&hover, &["position", "line"]), &Some(2)),41      Test.equals("characters become 0-based", &Access.pathInt(&hover, &["position", "character"]), &Some(4)),42      Test.equals("positions below 1 clamp to 0", &(Access.pathInt(&clamped, &["position", "line"]), Access.pathInt(&clamped, &["position", "character"])), &(Some(0), Some(0))),43      Test.equals("references include the declaration", &Json.path(&references, &["context", "includeDeclaration"]), &Some(Json.Boolean(true))),44      Test.equals("a rename carries the new name", &Access.text(&rename, "newName"), &Some("renamed")),45      Test.equals("a code action asks over a zero-width range", &Json.path(&action, &["range", "start"]), &Json.path(&action, &["range", "end"])),46      Test.equals("a workspace symbol query carries only the query", &Access.members(&symbols).length(), &1),47      Test.equals("an outline carries only the document", &Access.members(&outline).length(), &1),48      Test.that("the conversation opens the document", said.contains("textDocument/didOpen")),49      Test.that("the conversation ends with exit", said.contains("\"method\":\"exit\"")),50      Test.not("a workspace question without source opens nothing", unopened.contains("didOpen")),51      Test.that("a workspace question with source opens it", symbolsWithSource.contains("didOpen")),52      Test.equals("completion says it was asked for", &Json.path(&completing, &["context", "triggerKind"]), &Some(Json.Number(1))),53      Test.equals("hover carries no completion context", &Json.path(&hover, &["context"]), &None),54      Test.equals("position zero clamps to zero", &(Access.pathInt(&origin, &["position", "line"]), Access.pathInt(&origin, &["position", "character"])), &(Some(0), Some(0))),55      Test.that("frames count UTF-8 bytes", Conversation.frame(&Json.Text("é")).startsWith("Content-Length: 4\r\n\r\n")),56      Test.equals("the question's reply is the answer", &answer.result, &Some(Json.object(&[("contents", Json.Text("é"))]))),57      Test.equals("published diagnostics are collected", &answer.diagnostics.length(), &1),58      Test.equals("a server error is the answer's error", &refused.error, &Some("no such method")),59      Test.equals("no reply to the question is no result", &silent.result, &None),60      Test.equals("a null reply is a result", &nulled.result, &Some(Json.Null))61    ])62  let ran = Test.run(&checks)63  for failure in Test.failuresOf(&ran) {64    let _reported = Io.writeErrorLine(failure)65  }66  Test.report(&ran)67}68