summaryrefslogtreecommitdiffhomepage
path: root/packages/core/tests/config
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-02 17:52:14 +0900
committerAdam Malczewski <[email protected]>2026-06-02 17:52:14 +0900
commit062d01bd2f5c3ab6de7747dc5028e66b81dac6f5 (patch)
tree6097df0d53265f1a5e734aadab75c0334cb8e0e7 /packages/core/tests/config
parentb3aca3efe9e8cda79db6e2c7fa20482880ed16c3 (diff)
downloaddispatch-062d01bd2f5c3ab6de7747dc5028e66b81dac6f5.tar.gz
dispatch-062d01bd2f5c3ab6de7747dc5028e66b81dac6f5.zip
feat(lsp): add config-driven LSP support (Roblox Luau via luau-lsp)
Add Language Server Protocol integration modeled on opencode's, wired for this codebase's plain-TypeScript tool/agent architecture. Core (@dispatch/core): - lsp/client.ts: LSP/JSON-RPC client over stdio (vscode-jsonrpc) with the initialize handshake, didOpen/didChange sync, push + pull diagnostics (textDocument/diagnostic, workspace/diagnostic), and a generic request() passthrough for hover/definition/references/documentSymbol. - lsp/server.ts: resolves dispatch.toml [lsp] entries into spawn specs. Config-driven only — no builtin registry, no auto-download. - lsp/manager.ts: process-wide LspManager owning client lifecycles, keyed by root+serverID, lazy spawn + reuse + graceful shutdown. - lsp/language.ts: extension->languageId map incl. .luau -> "luau". - lsp/diagnostic.ts: error-only <diagnostics> block formatting (1-based). - tools/lsp.ts: on-demand 'lsp' tool (1-based coords -> 0-based wire). - write-file.ts: optional onAfterWrite hook for diagnostics-on-write. - config schema: validate [lsp] block; DispatchConfig.lsp + LspServerConfig. API (@dispatch/api): - AgentManager owns one LspManager; per-working-directory server cache cleared on config reload; diagnostics appended to write_file results; 'lsp' tool gated by new perm_lsp setting; shutdownAll on destroy(). Config: - dispatch.toml: documented, commented [lsp.luau-lsp] Roblox example. Tests: fake-lsp-server fixture + client/manager/server/diagnostic/schema/ tool/write-hook suites, plus an opt-in real-binary luau-lsp smoke test (auto-skipped when luau-lsp is absent). 652 pass; biome + 3 typechecks green.
Diffstat (limited to 'packages/core/tests/config')
-rw-r--r--packages/core/tests/config/lsp-schema.test.ts110
1 files changed, 110 insertions, 0 deletions
diff --git a/packages/core/tests/config/lsp-schema.test.ts b/packages/core/tests/config/lsp-schema.test.ts
new file mode 100644
index 0000000..2b71cc2
--- /dev/null
+++ b/packages/core/tests/config/lsp-schema.test.ts
@@ -0,0 +1,110 @@
+import { describe, expect, it } from "vitest";
+import { validateConfig } from "../../src/config/schema.js";
+
+describe("config schema — [lsp] block", () => {
+ it("parses a valid custom server entry", () => {
+ const { config, errors } = validateConfig({
+ permissions: {},
+ lsp: {
+ "luau-lsp": {
+ command: ["luau-lsp", "lsp"],
+ extensions: [".luau"],
+ initialization: { "luau-lsp": { platform: { type: "roblox" } } },
+ },
+ },
+ });
+ expect(errors).toHaveLength(0);
+ expect(config.lsp).toBeDefined();
+ const entry = config.lsp?.["luau-lsp"];
+ expect(entry?.command).toEqual(["luau-lsp", "lsp"]);
+ expect(entry?.extensions).toEqual([".luau"]);
+ expect(entry?.initialization).toEqual({
+ "luau-lsp": { platform: { type: "roblox" } },
+ });
+ });
+
+ it("preserves env and nested initialization verbatim", () => {
+ const { config } = validateConfig({
+ permissions: {},
+ lsp: {
+ "luau-lsp": {
+ command: ["luau-lsp", "lsp"],
+ extensions: [".luau"],
+ env: { PATH: "/custom/bin" },
+ initialization: {
+ "luau-lsp": {
+ sourcemap: { enabled: true, autogenerate: true },
+ diagnostics: { strictDatamodelTypes: false },
+ },
+ },
+ },
+ },
+ });
+ const entry = config.lsp?.["luau-lsp"];
+ expect(entry?.env).toEqual({ PATH: "/custom/bin" });
+ expect(entry?.initialization).toEqual({
+ "luau-lsp": {
+ sourcemap: { enabled: true, autogenerate: true },
+ diagnostics: { strictDatamodelTypes: false },
+ },
+ });
+ });
+
+ it("rejects a custom server missing command", () => {
+ const { config, errors } = validateConfig({
+ permissions: {},
+ lsp: { broken: { extensions: [".luau"] } },
+ });
+ expect(errors.some((e) => e.path === "lsp.broken.command")).toBe(true);
+ expect(config.lsp).toBeUndefined();
+ });
+
+ it("rejects a custom server missing extensions", () => {
+ const { errors } = validateConfig({
+ permissions: {},
+ lsp: { broken: { command: ["x"] } },
+ });
+ expect(errors.some((e) => e.path === "lsp.broken.extensions")).toBe(true);
+ });
+
+ it("rejects an empty command array", () => {
+ const { errors } = validateConfig({
+ permissions: {},
+ lsp: { broken: { command: [], extensions: [".luau"] } },
+ });
+ expect(errors.some((e) => e.path === "lsp.broken.command")).toBe(true);
+ });
+
+ it("keeps a disabled entry without requiring command/extensions", () => {
+ const { config, errors } = validateConfig({
+ permissions: {},
+ lsp: { "luau-lsp": { disabled: true } },
+ });
+ expect(errors).toHaveLength(0);
+ expect(config.lsp?.["luau-lsp"]?.disabled).toBe(true);
+ });
+
+ it("skips a malformed entry but keeps valid siblings", () => {
+ const { config, errors } = validateConfig({
+ permissions: {},
+ lsp: {
+ good: { command: ["a"], extensions: [".luau"] },
+ bad: { extensions: [".luau"] },
+ },
+ });
+ expect(config.lsp?.good).toBeDefined();
+ expect(config.lsp?.bad).toBeUndefined();
+ expect(errors.length).toBeGreaterThan(0);
+ });
+
+ it("omits lsp entirely when not present", () => {
+ const { config, errors } = validateConfig({ permissions: {} });
+ expect(errors).toHaveLength(0);
+ expect(config.lsp).toBeUndefined();
+ });
+
+ it("flags a non-object lsp value", () => {
+ const { errors } = validateConfig({ permissions: {}, lsp: "nope" });
+ expect(errors.some((e) => e.path === "lsp")).toBe(true);
+ });
+});