
ConversationTest.pudu
Pudu68 lines5.1 KB
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 Access91011fn question(method: Str, line: Int, character: Int) -> Conversation.Question {12 Conversation.Question{method: method, line: line, character: character, newName: "renamed", query: "dou"}13}141516fn framed(body: Str) -> Str {17 "Content-Length: " + show(body.toBytes().length()) + "\r\n\r\n" + body18}192021fn 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