test(codex-lsp): cover lazy metadata fallback

This commit is contained in:
YeonGyu-Kim
2026-05-29 13:18:46 +09:00
parent 8896db9806
commit 26ec219bcd
@@ -1,5 +1,6 @@
import { PassThrough } from "node:stream";
import { describe, expect, it } from "vitest";
import { resolveLazyLspIdleTimeoutMs } from "../src/lazy-lsp-mcp.js";
import { resolveLazyLspIdleTimeoutMs, runLazyLspMcpServer } from "../src/lazy-lsp-mcp.js";
import { DEFAULT_LAZY_MCP_IDLE_TIMEOUT_MS } from "../src/lazy-mcp-proxy.js";
describe("lazy LSP MCP config", () => {
@@ -18,4 +19,84 @@ describe("lazy LSP MCP config", () => {
expect(malformed.value).toBe(fallback);
expect(malformed.warning).toContain("Ignoring malformed lazy MCP idle timeout");
});
it("#given missing lazy backend config #when metadata requests are served #then the server stays usable without starting a backend", async () => {
// given
const input = new PassThrough();
const output = new PassThrough();
const chunks: string[] = [];
const previousBackendConfig = process.env["CODEX_LSP_LAZY_BACKEND"];
delete process.env["CODEX_LSP_LAZY_BACKEND"];
output.on("data", (chunk: Buffer | string) => {
chunks.push(chunk.toString());
});
try {
const server = runLazyLspMcpServer(input, output);
// when
input.write(
`${JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "test" } },
})}\n`,
);
input.write(`${JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} })}\n`);
input.end();
await server;
} finally {
restoreEnv("CODEX_LSP_LAZY_BACKEND", previousBackendConfig);
}
// then
const responses = parseJsonRpcResponses(chunks.join(""));
expect(responses).toHaveLength(2);
expect(responses[0]?.result?.serverInfo).toEqual({ name: "lsp", version: "0.2.0" });
expect(toolNames(responses[1])).toContain("status");
});
});
interface JsonRpcTestResponse {
readonly result?: {
readonly serverInfo?: unknown;
readonly tools?: unknown;
};
}
function parseJsonRpcResponses(output: string): readonly JsonRpcTestResponse[] {
return output
.trim()
.split(/\n+/)
.filter((line) => line.length > 0)
.map((line) => {
const parsed: unknown = JSON.parse(line);
return isJsonRpcTestResponse(parsed) ? parsed : {};
});
}
function toolNames(response: JsonRpcTestResponse | undefined): readonly string[] {
const tools = response?.result?.tools;
if (!Array.isArray(tools)) return [];
return tools.flatMap((tool) => {
if (!isRecord(tool) || typeof tool["name"] !== "string") return [];
return [tool["name"]];
});
}
function restoreEnv(name: string, value: string | undefined): void {
if (value === undefined) {
delete process.env[name];
return;
}
process.env[name] = value;
}
function isJsonRpcTestResponse(value: unknown): value is JsonRpcTestResponse {
return isRecord(value);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}