@effected/lsp 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/testing.d.ts ADDED
@@ -0,0 +1,307 @@
1
+ import { Duration, Effect, PlatformError, Schema, Scope } from "effect";
2
+ import { ChildProcess, ChildProcessSpawner } from "effect/process";
3
+ //#region src/LspMessage.d.ts
4
+ /**
5
+ * A JSON-RPC 2.0 message as an LSP frame body carries it: a request (an `id`
6
+ * and a `method`), a notification (a `method`, no `id`) or a response (an
7
+ * `id` and either `result` or `error`).
8
+ *
9
+ * @remarks
10
+ * Structural on purpose. Every field but `jsonrpc` is optional, because one
11
+ * shape describes all three kinds and the reader distinguishes them by which
12
+ * fields are present: `method` and `id` is a request, `method` alone is a
13
+ * notification, `id` without `method` is a response. A decoded frame body is
14
+ * `unknown` until a caller has checked it is one of these.
15
+ *
16
+ * @public
17
+ */
18
+ interface LspMessage {
19
+ /** Always `"2.0"`. */
20
+ readonly jsonrpc: "2.0";
21
+ /** Present on a request and a response; `null` on a response to a request whose id could not be read. */
22
+ readonly id?: number | string | null;
23
+ /** Present on a request and a notification. */
24
+ readonly method?: string;
25
+ /** A request's or notification's parameters. */
26
+ readonly params?: unknown;
27
+ /** A successful response's result; `null` is a valid result (`shutdown` answers it). */
28
+ readonly result?: unknown;
29
+ /** A failed response's error. */
30
+ readonly error?: {
31
+ readonly code: number;
32
+ readonly message: string;
33
+ readonly data?: unknown;
34
+ };
35
+ }
36
+ //#endregion
37
+ //#region src/LspTestFailure.d.ts
38
+ declare const LspTestFailure_base: Schema.Class<LspTestFailure, Schema.TaggedStruct<"LspTestFailure", {
39
+ /** Which failure occurred; see the list above. */
40
+ readonly reason: Schema.Literals<readonly ["StreamEnded", "InvalidFrame", "NotJsonRpc", "TimedOut"]>;
41
+ /** A human-readable description of what went wrong. */
42
+ readonly message: Schema.String;
43
+ }>, import("effect/Cause").YieldableError>;
44
+ /**
45
+ * Why {@link LspProbe.initialize} could not prove a Language Server boots.
46
+ *
47
+ * @remarks
48
+ * - `StreamEnded`: the server's stdout ended before the response the probe
49
+ * was waiting for, usually because it exited; a stream that ended inside a
50
+ * frame lands here too. The message carries the exit code and stderr.
51
+ * - `InvalidFrame`: stdout carried bytes that are not an LSP frame — most
52
+ * often a log line written to stdout, which is the protocol wire. The
53
+ * message carries the frame error, excerpt included.
54
+ * - `NotJsonRpc`: a well-framed body that is not a JSON-RPC 2.0 message.
55
+ * - `TimedOut`: the exchange outlived `LspProbeOptions.timeout`. The message
56
+ * names the step the probe was waiting on and the stderr so far; a server
57
+ * that ignores `exit` and waits for stdin to close fails here.
58
+ *
59
+ * A JSON-RPC error answering `initialize` or `shutdown` is not a failure: it
60
+ * is returned in the result for the caller to assert on.
61
+ *
62
+ * @public
63
+ */
64
+ export declare class LspTestFailure extends LspTestFailure_base {}
65
+ //#endregion
66
+ //#region src/LspProbe.d.ts
67
+ /**
68
+ * Options for {@link LspProbe.initialize}.
69
+ *
70
+ * @public
71
+ */
72
+ interface LspProbeOptions {
73
+ /** `initialize`'s `rootUri`. Defaults to `null`: no workspace folder. */
74
+ readonly rootUri?: string | null;
75
+ /** `initialize`'s `processId`. Defaults to `null`, so the server never watches a client process for liveness. */
76
+ readonly processId?: number | null;
77
+ /** The client capabilities `initialize` announces. Defaults to `{}`. */
78
+ readonly capabilities?: {
79
+ readonly [key: string]: unknown;
80
+ };
81
+ /** `initialize`'s `initializationOptions`; omitted from the request unless given. */
82
+ readonly initializationOptions?: unknown;
83
+ /** A ceiling over the whole exchange, spawn excluded. Defaults to 30 seconds. */
84
+ readonly timeout?: Duration.Input;
85
+ }
86
+ /**
87
+ * What {@link LspProbe.initialize} observed.
88
+ *
89
+ * @public
90
+ */
91
+ interface LspProbeResult {
92
+ /** The id-1 `initialize` response. A refused handshake is still a response: assert `response.error === undefined`. */
93
+ readonly response: LspMessage;
94
+ /** The id-2 `shutdown` response; a clean server answers `result: null`. */
95
+ readonly shutdown: LspMessage;
96
+ /** Every message the server sent, in order: responses, notifications, and requests the probe left unanswered. */
97
+ readonly messages: ReadonlyArray<LspMessage>;
98
+ /** Everything the server wrote to stderr; a clean boot leaves it empty. */
99
+ readonly stderr: string;
100
+ /** The server's exit code; 0 means `exit` arrived after `shutdown`, as the specification requires. */
101
+ readonly exitCode: number;
102
+ }
103
+ /**
104
+ * The smallest proof that an installed Language Server bin boots: the whole
105
+ * LSP lifecycle over stdio, and exit code 0.
106
+ *
107
+ * @remarks
108
+ * The exchange is `initialize` (id 1), its response, the `initialized`
109
+ * notification, `shutdown` (id 2), its response, and the `exit`
110
+ * notification; then the probe waits for the server to exit. The caller
111
+ * asserts `response.error === undefined`, `exitCode === 0` and, for a clean
112
+ * boot, `stderr === ""` — this is the LSP half of a packed-install proof, the
113
+ * twin of `@effected/mcp/testing`'s `McpProbe`. Every frame is encoded with
114
+ * `LspFrame.encode` from the main entry, so `Content-Length` counts bytes.
115
+ *
116
+ * - `initialized` is sent because the specification requires it before any
117
+ * other request, `shutdown` included; it is skipped only when `initialize`
118
+ * answered with an error, and the sequence still ends `shutdown`, `exit`.
119
+ * - Stdin stays open after `exit`. The server must terminate itself on
120
+ * `exit`, as an editor never closes the pipe first; a server that waits
121
+ * for stdin to close fails with `TimedOut` instead of passing.
122
+ * - A request the server sends (`client/registerCapability`, say) is
123
+ * recorded in `messages` and never answered.
124
+ * - Never hangs: stdout that ends before a response fails `StreamEnded` with
125
+ * the exit code and stderr; bytes that are not a frame fail `InvalidFrame`;
126
+ * and the whole exchange runs under `timeout` (30 seconds by default),
127
+ * which fails `TimedOut` naming the step it was waiting on. The child is
128
+ * killed when the probe's scope closes.
129
+ * - The timeout reads `Clock`, so under `it.effect`'s virtual clock it
130
+ * never fires on its own: run the probe under `it.live`.
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * import { LspProbe } from "@effected/lsp/testing";
135
+ * import { Effect } from "effect";
136
+ * import { ChildProcess } from "effect/process";
137
+ *
138
+ * const program = Effect.gen(function* () {
139
+ * const probe = yield* LspProbe.initialize(ChildProcess.make(process.execPath, ["./bin/lsp.js", "--stdio"]));
140
+ * // A clean boot: no JSON-RPC error, exit code 0, empty stderr.
141
+ * return probe.response.error === undefined && probe.exitCode === 0 && probe.stderr === "";
142
+ * });
143
+ * // Requires a `ChildProcessSpawner` from the platform layer.
144
+ * ```
145
+ *
146
+ * @public
147
+ */
148
+ export declare class LspProbe {
149
+ private constructor();
150
+ /**
151
+ * Spawn `command`, run `initialize` → `initialized` → `shutdown` → `exit`,
152
+ * and collect every server message, stderr and the exit code.
153
+ */
154
+ static readonly initialize: (command: ChildProcess.Command, options?: LspProbeOptions) => Effect.Effect<LspProbeResult, LspTestFailure | PlatformError.PlatformError, ChildProcessSpawner.ChildProcessSpawner>;
155
+ }
156
+ //#endregion
157
+ //#region src/LspProcess.d.ts
158
+ /**
159
+ * Options for {@link LspProcess.stderrUntil}.
160
+ *
161
+ * @public
162
+ */
163
+ interface LspProcessStderrUntilOptions {
164
+ /**
165
+ * How long to wait for the predicate to hold before failing with
166
+ * `TimedOut`. Real time: under `it.effect`'s `TestClock` it never fires,
167
+ * so a test that waits on stderr runs under `it.live`. Keep it below the
168
+ * test runner's own timeout, or the runner kills the test first.
169
+ */
170
+ readonly timeout: Duration.Input;
171
+ }
172
+ /**
173
+ * A spawned Language Server bin a test writes `Content-Length` frames to
174
+ * while it runs — the LSP twin of `@effected/mcp/testing`'s `McpProcess`.
175
+ *
176
+ * @remarks
177
+ * Build the command in the test file with `execPath` and an explicit `env`:
178
+ * `ChildProcess.make(process.execPath, [bin, "--stdio"], { env })`. The child
179
+ * lives for the scope `spawn` runs in, and is killed when it closes.
180
+ *
181
+ * - `nextMessage` fails `StreamEnded` once stdout ends — a stream that ends
182
+ * inside a frame included — so a child that exits early fails the test
183
+ * instead of hanging it. Bytes that are not a frame fail `InvalidFrame`; a
184
+ * frame whose body is not a JSON-RPC 2.0 message fails `NotJsonRpc`.
185
+ * - `send` writes one frame with `LspFrame.encode` from the main entry, so `Content-Length`
186
+ * counts bytes; `sendRaw` writes a string or bytes exactly as given, for a
187
+ * malformed frame or one frame split across writes.
188
+ * - `readUntilResponse` reads past notifications and server requests to the
189
+ * response with the matching id, and returns what it saw.
190
+ * - `closeStdin` is `Queue.end`, so every frame already sent is delivered
191
+ * before stdin closes. The specification makes a server exit on the `exit`
192
+ * notification with stdin still open; close stdin only to test a client
193
+ * disconnect.
194
+ * - Every stdout byte is kept, apart from the decoder: `stdoutSoFar` and
195
+ * `stdoutFinal` return it raw, and `assertOnlyFrames` proves it held
196
+ * nothing but well-formed frames, even after the decoder has failed.
197
+ *
198
+ * @example
199
+ * ```ts
200
+ * import { LspProcess } from "@effected/lsp/testing";
201
+ * import { Effect } from "effect";
202
+ * import { ChildProcess } from "effect/process";
203
+ *
204
+ * const program = Effect.gen(function* () {
205
+ * const server = yield* LspProcess.spawn(ChildProcess.make(process.execPath, ["./bin/lsp.js", "--stdio"]));
206
+ * yield* server.send({ jsonrpc: "2.0", id: 1, method: "initialize", params: { processId: null, rootUri: null, capabilities: {} } });
207
+ * const { response } = yield* server.readUntilResponse(1);
208
+ * yield* server.send({ jsonrpc: "2.0", method: "initialized", params: {} });
209
+ * yield* server.send({ jsonrpc: "2.0", id: 2, method: "shutdown" });
210
+ * yield* server.readUntilResponse(2);
211
+ * yield* server.send({ jsonrpc: "2.0", method: "exit" });
212
+ * const exitCode = yield* server.exitCode;
213
+ * // Stdout carried nothing but frames: no stray log line before, between or after them.
214
+ * yield* server.assertOnlyFrames;
215
+ * return { response, exitCode };
216
+ * }).pipe(Effect.scoped);
217
+ * // Requires a `ChildProcessSpawner` from the platform layer.
218
+ * ```
219
+ *
220
+ * @public
221
+ */
222
+ export declare class LspProcess {
223
+ /**
224
+ * Write one message to the child's stdin as a `Content-Length` frame.
225
+ *
226
+ * @remarks
227
+ * Never fails. A frame sent after `closeStdin` is dropped, and a write the
228
+ * child can no longer receive fails the stdin pump instead; the next
229
+ * `StreamEnded` message names that failure.
230
+ */
231
+ readonly send: (message: unknown) => Effect.Effect<void>;
232
+ /**
233
+ * Write `bytes` to the child's stdin exactly as given: a string as UTF-8,
234
+ * bytes unchanged, with no framing added.
235
+ *
236
+ * @remarks
237
+ * For frames `send` cannot produce: a header without `Content-Length`, a
238
+ * body that is not JSON, or one frame split across several writes,
239
+ * including mid-character by passing bytes. Never fails, on the same terms
240
+ * as `send`.
241
+ */
242
+ readonly sendRaw: (bytes: string | Uint8Array) => Effect.Effect<void>;
243
+ /**
244
+ * The next message on stdout.
245
+ *
246
+ * @remarks
247
+ * Fails `StreamEnded` once stdout ends, including inside a frame;
248
+ * `InvalidFrame` when stdout carried bytes that are not a frame (a log
249
+ * line on the wire); `NotJsonRpc` for a well-framed body that is not a
250
+ * JSON-RPC 2.0 message. A frame failure is final: every later read fails
251
+ * the same way.
252
+ */
253
+ readonly nextMessage: Effect.Effect<LspMessage, LspTestFailure>;
254
+ /** Read messages up to and including the response with this id; `seen` holds every message read, the response last. */
255
+ readonly readUntilResponse: (id: string | number) => Effect.Effect<{
256
+ readonly response: LspMessage;
257
+ readonly seen: ReadonlyArray<LspMessage>;
258
+ }, LspTestFailure>;
259
+ /** End stdin after every frame already sent. */
260
+ readonly closeStdin: Effect.Effect<void>;
261
+ /** Wait for the child to exit. */
262
+ readonly exitCode: Effect.Effect<number, PlatformError.PlatformError>;
263
+ /** Everything written to stderr so far. */
264
+ readonly stderrSoFar: Effect.Effect<string>;
265
+ /**
266
+ * Wait until everything written to stderr so far satisfies `predicate`,
267
+ * and return that text.
268
+ *
269
+ * @remarks
270
+ * Event-driven: the predicate is checked against the current text at once,
271
+ * then again on every chunk the child writes, never on a polling timer.
272
+ * Fails `StreamEnded` when stderr ends (the child exited) without the
273
+ * predicate holding, and `TimedOut` once `options.timeout` passes; both
274
+ * messages carry the stderr seen so far. Reach for it whenever a report
275
+ * lands on a later tick than the messages a test reads, such as a crash
276
+ * guard's report once the server is serving. `options.timeout` is real
277
+ * time: run the test under `it.live`.
278
+ */
279
+ readonly stderrUntil: (predicate: (stderr: string) => boolean, options: LspProcessStderrUntilOptions) => Effect.Effect<string, LspTestFailure>;
280
+ /** Everything written to stderr, once stderr has ended. Waits for the child to exit. */
281
+ readonly stderrFinal: Effect.Effect<string>;
282
+ /** Every byte written to stdout so far, undecoded. */
283
+ readonly stdoutSoFar: Effect.Effect<Uint8Array>;
284
+ /** Every byte written to stdout, once stdout has ended. Waits for the child to exit. */
285
+ readonly stdoutFinal: Effect.Effect<Uint8Array>;
286
+ /**
287
+ * Wait for stdout to end, then prove it carried nothing but well-formed
288
+ * `Content-Length` frames of JSON-RPC 2.0 messages, and return those
289
+ * messages.
290
+ *
291
+ * @remarks
292
+ * The stdout hygiene check: a stray byte before the first frame, between
293
+ * two frames or after the last fails `InvalidFrame`, whose message names
294
+ * the stream offset and quotes the offending bytes; so does a stream that
295
+ * ends inside a frame. A well-framed body that is not JSON-RPC fails
296
+ * `NotJsonRpc`. It reads the raw capture, not `nextMessage`'s queue, so
297
+ * the messages a test already read are still checked. It waits for the
298
+ * child to exit: send `exit` (or close stdin) first.
299
+ */
300
+ readonly assertOnlyFrames: Effect.Effect<ReadonlyArray<LspMessage>, LspTestFailure>;
301
+ private constructor();
302
+ /** Spawn `command` for the life of the current scope. */
303
+ static readonly spawn: (command: ChildProcess.Command) => Effect.Effect<LspProcess, PlatformError.PlatformError, ChildProcessSpawner.ChildProcessSpawner | Scope.Scope>;
304
+ }
305
+ //#endregion
306
+ export type { LspMessage, LspProbeOptions, LspProbeResult, LspProcessStderrUntilOptions };
307
+ //# sourceMappingURL=testing.d.ts.map
package/testing.js ADDED
@@ -0,0 +1,5 @@
1
+ import { LspTestFailure } from "./LspTestFailure.js";
2
+ import { LspProcess } from "./LspProcess.js";
3
+ import { LspProbe } from "./LspProbe.js";
4
+
5
+ export { LspProbe, LspProcess, LspTestFailure };
@@ -0,0 +1,11 @@
1
+ // This file is read by tools that parse documentation comments conforming to the TSDoc standard.
2
+ // It should be published with your NPM package. It should not be tracked by Git.
3
+ {
4
+ "tsdocVersion": "0.12",
5
+ "toolPackages": [
6
+ {
7
+ "packageName": "@microsoft/api-extractor",
8
+ "packageVersion": "7.59.1"
9
+ }
10
+ ]
11
+ }