c-next 0.1.60 → 0.1.61

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.
Files changed (50) hide show
  1. package/README.md +8 -0
  2. package/package.json +1 -1
  3. package/src/cli/serve/ServeCommand.ts +144 -25
  4. package/src/cli/serve/__tests__/ServeCommand.test.ts +227 -3
  5. package/src/lib/__tests__/parseCHeader.test.ts +194 -0
  6. package/src/lib/parseCHeader.ts +114 -0
  7. package/src/lib/types/TSymbolKind.ts +2 -1
  8. package/src/transpiler/Transpiler.ts +145 -76
  9. package/src/transpiler/logic/analysis/FunctionCallAnalyzer.ts +74 -46
  10. package/src/transpiler/logic/analysis/InitializationAnalyzer.ts +103 -68
  11. package/src/transpiler/logic/analysis/NullCheckAnalyzer.ts +44 -35
  12. package/src/transpiler/logic/symbols/CSymbolCollector.ts +23 -16
  13. package/src/transpiler/logic/symbols/cnext/index.ts +47 -27
  14. package/src/transpiler/output/codegen/CodeGenerator.ts +348 -503
  15. package/src/transpiler/output/codegen/TypeResolver.ts +56 -32
  16. package/src/transpiler/output/codegen/TypeValidator.ts +24 -75
  17. package/src/transpiler/output/codegen/__tests__/ExpressionWalker.test.ts +10 -7
  18. package/src/transpiler/output/codegen/__tests__/TrackVariableTypeHelpers.test.ts +38 -37
  19. package/src/transpiler/output/codegen/assignment/AssignmentContextBuilder.ts +96 -46
  20. package/src/transpiler/output/codegen/generators/__tests__/GeneratorRegistry.test.ts +51 -0
  21. package/src/transpiler/output/codegen/generators/statements/SwitchGenerator.ts +77 -40
  22. package/src/transpiler/output/codegen/generators/support/IncludeGenerator.ts +93 -47
  23. package/src/transpiler/output/codegen/helpers/AssignmentTargetExtractor.ts +86 -0
  24. package/src/transpiler/output/codegen/helpers/BaseIdentifierBuilder.ts +53 -0
  25. package/src/transpiler/output/codegen/helpers/CastValidator.ts +146 -0
  26. package/src/transpiler/output/codegen/helpers/ChildStatementCollector.ts +116 -0
  27. package/src/transpiler/output/codegen/helpers/LiteralEvaluator.ts +61 -0
  28. package/src/transpiler/output/codegen/helpers/PostfixChainBuilder.ts +101 -0
  29. package/src/transpiler/output/codegen/helpers/SimpleIdentifierResolver.ts +54 -0
  30. package/src/transpiler/output/codegen/helpers/StatementExpressionCollector.ts +117 -0
  31. package/src/transpiler/output/codegen/helpers/TransitiveModificationPropagator.ts +127 -0
  32. package/src/transpiler/output/codegen/helpers/TypeGenerationHelper.ts +249 -0
  33. package/src/transpiler/output/codegen/helpers/__tests__/AssignmentTargetExtractor.test.ts +105 -0
  34. package/src/transpiler/output/codegen/helpers/__tests__/BaseIdentifierBuilder.test.ts +68 -0
  35. package/src/transpiler/output/codegen/helpers/__tests__/CastValidator.test.ts +233 -0
  36. package/src/transpiler/output/codegen/helpers/__tests__/ChildStatementCollector.test.ts +191 -0
  37. package/src/transpiler/output/codegen/helpers/__tests__/LiteralEvaluator.test.ts +62 -0
  38. package/src/transpiler/output/codegen/helpers/__tests__/PostfixChainBuilder.test.ts +147 -0
  39. package/src/transpiler/output/codegen/helpers/__tests__/SimpleIdentifierResolver.test.ts +105 -0
  40. package/src/transpiler/output/codegen/helpers/__tests__/StatementExpressionCollector.test.ts +221 -0
  41. package/src/transpiler/output/codegen/helpers/__tests__/TransitiveModificationPropagator.test.ts +289 -0
  42. package/src/transpiler/output/codegen/helpers/__tests__/TypeGenerationHelper.test.ts +369 -0
  43. package/src/transpiler/output/codegen/types/IBaseIdentifierResult.ts +11 -0
  44. package/src/transpiler/output/codegen/types/IPostfixChainDeps.ts +16 -0
  45. package/src/transpiler/output/codegen/types/IPostfixOperation.ts +11 -0
  46. package/src/transpiler/output/codegen/types/ISimpleIdentifierDeps.ts +21 -0
  47. package/src/lib/__tests__/transpiler.test.ts +0 -303
  48. package/src/lib/transpiler.ts +0 -149
  49. package/src/lib/types/ITranspileOptions.ts +0 -15
  50. package/src/lib/types/ITranspileResult.ts +0 -20
package/README.md CHANGED
@@ -94,6 +94,14 @@ cnext src/ -o build/src --header-out build/include --clean
94
94
  cnext --help
95
95
  ```
96
96
 
97
+ ## VS Code Extension
98
+
99
+ The C-Next VS Code extension provides syntax highlighting, live C preview, IntelliSense, and error diagnostics.
100
+
101
+ **Install from:** [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=jlaustill.vscode-c-next) (coming soon)
102
+
103
+ **Source:** [github.com/jlaustill/vscode-c-next](https://github.com/jlaustill/vscode-c-next)
104
+
97
105
  ## Getting Started with PlatformIO
98
106
 
99
107
  C-Next integrates seamlessly with PlatformIO embedded projects. The transpiler automatically converts `.cnx` files to `.c` before each build.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c-next",
3
- "version": "0.1.60",
3
+ "version": "0.1.61",
4
4
  "description": "A safer C for embedded systems development. Transpiles to clean, readable C.",
5
5
  "packageManager": "npm@11.9.0",
6
6
  "type": "module",
@@ -1,20 +1,29 @@
1
1
  /**
2
2
  * ServeCommand
3
3
  * JSON-RPC server for VS Code extension communication
4
+ *
5
+ * Phase 2b (ADR-060): Uses the full Transpiler for transpilation and symbol
6
+ * extraction, enabling include resolution, C++ auto-detection, and cross-file
7
+ * symbol support.
4
8
  */
5
9
 
6
10
  import { createInterface, Interface } from "node:readline";
11
+ import { dirname } from "node:path";
7
12
  import JsonRpcHandler from "./JsonRpcHandler";
8
13
  import IJsonRpcRequest from "./types/IJsonRpcRequest";
9
14
  import IJsonRpcResponse from "./types/IJsonRpcResponse";
10
15
  import ConfigPrinter from "../ConfigPrinter";
11
- import transpile from "../../lib/transpiler";
16
+ import ConfigLoader from "../ConfigLoader";
17
+ import Transpiler from "../../transpiler/Transpiler";
12
18
  import parseWithSymbols from "../../lib/parseWithSymbols";
19
+ import parseCHeader from "../../lib/parseCHeader";
13
20
 
14
21
  /**
15
- * Method handler type
22
+ * Method handler type (async to support Transpiler.transpileSource)
16
23
  */
17
- type MethodHandler = (params?: Record<string, unknown>) => IMethodResult;
24
+ type MethodHandler = (
25
+ params?: Record<string, unknown>,
26
+ ) => Promise<IMethodResult>;
18
27
 
19
28
  /**
20
29
  * Result from a method handler
@@ -41,14 +50,17 @@ class ServeCommand {
41
50
  private static shouldShutdown = false;
42
51
  private static readline: Interface | null = null;
43
52
  private static debugMode = false;
53
+ private static transpiler: Transpiler | null = null;
44
54
 
45
55
  /**
46
56
  * Method handlers registry
47
57
  */
48
58
  private static readonly methods: Record<string, MethodHandler> = {
49
59
  getVersion: ServeCommand.handleGetVersion,
60
+ initialize: ServeCommand.handleInitialize,
50
61
  transpile: ServeCommand.handleTranspile,
51
62
  parseSymbols: ServeCommand.handleParseSymbols,
63
+ parseCHeader: ServeCommand.handleParseCHeader,
52
64
  shutdown: ServeCommand.handleShutdown,
53
65
  };
54
66
 
@@ -115,20 +127,23 @@ class ServeCommand {
115
127
  const request = parseResult.request!;
116
128
  this.log(`method: ${request.method}`);
117
129
 
118
- // Dispatch to method handler
119
- const response = this.dispatch(request);
120
- this.writeResponse(response);
130
+ // Dispatch to method handler (async)
131
+ this.dispatch(request).then((response) => {
132
+ this.writeResponse(response);
121
133
 
122
- // Handle shutdown after response is written
123
- if (this.shouldShutdown) {
124
- this.readline?.close();
125
- }
134
+ // Handle shutdown after response is written
135
+ if (this.shouldShutdown) {
136
+ this.readline?.close();
137
+ }
138
+ });
126
139
  }
127
140
 
128
141
  /**
129
142
  * Dispatch a request to the appropriate handler
130
143
  */
131
- private static dispatch(request: IJsonRpcRequest): IJsonRpcResponse {
144
+ private static async dispatch(
145
+ request: IJsonRpcRequest,
146
+ ): Promise<IJsonRpcResponse> {
132
147
  const handler = this.methods[request.method];
133
148
 
134
149
  if (!handler) {
@@ -139,7 +154,7 @@ class ServeCommand {
139
154
  );
140
155
  }
141
156
 
142
- const result = handler(request.params);
157
+ const result = await handler(request.params);
143
158
 
144
159
  if (result.success) {
145
160
  return JsonRpcHandler.formatResponse(request.id, result.result);
@@ -162,19 +177,59 @@ class ServeCommand {
162
177
  /**
163
178
  * Handle getVersion method
164
179
  */
165
- private static handleGetVersion(): IMethodResult {
180
+ private static async handleGetVersion(): Promise<IMethodResult> {
166
181
  return {
167
182
  success: true,
168
183
  result: { version: ConfigPrinter.getVersion() },
169
184
  };
170
185
  }
171
186
 
187
+ /**
188
+ * Handle initialize method
189
+ * Loads project config and creates a Transpiler instance
190
+ */
191
+ private static async handleInitialize(
192
+ params?: Record<string, unknown>,
193
+ ): Promise<IMethodResult> {
194
+ if (!params || typeof params.workspacePath !== "string") {
195
+ return {
196
+ success: false,
197
+ errorCode: JsonRpcHandler.ERROR_INVALID_PARAMS,
198
+ errorMessage: "Missing required param: workspacePath",
199
+ };
200
+ }
201
+
202
+ const workspacePath = params.workspacePath;
203
+ ServeCommand.log(`initializing with workspace: ${workspacePath}`);
204
+
205
+ const config = ConfigLoader.load(workspacePath);
206
+
207
+ ServeCommand.transpiler = new Transpiler({
208
+ inputs: [],
209
+ includeDirs: config.include ?? [],
210
+ cppRequired: config.cppRequired ?? false,
211
+ target: config.target ?? "",
212
+ debugMode: config.debugMode ?? false,
213
+ noCache: config.noCache ?? false,
214
+ });
215
+
216
+ ServeCommand.log(
217
+ `initialized (cppRequired=${config.cppRequired ?? false}, includeDirs=${(config.include ?? []).length})`,
218
+ );
219
+
220
+ return {
221
+ success: true,
222
+ result: { success: true },
223
+ };
224
+ }
225
+
172
226
  /**
173
227
  * Handle transpile method
228
+ * Uses full Transpiler for include resolution and C++ auto-detection
174
229
  */
175
- private static handleTranspile(
230
+ private static async handleTranspile(
176
231
  params?: Record<string, unknown>,
177
- ): IMethodResult {
232
+ ): Promise<IMethodResult> {
178
233
  if (!params || typeof params.source !== "string") {
179
234
  return {
180
235
  success: false,
@@ -183,7 +238,26 @@ class ServeCommand {
183
238
  };
184
239
  }
185
240
 
186
- const result = transpile(params.source);
241
+ if (!ServeCommand.transpiler) {
242
+ return {
243
+ success: false,
244
+ errorCode: JsonRpcHandler.ERROR_INVALID_PARAMS,
245
+ errorMessage: "Server not initialized. Call initialize first.",
246
+ };
247
+ }
248
+
249
+ const source = String(params.source);
250
+ const filePath =
251
+ typeof params.filePath === "string" ? params.filePath : undefined;
252
+
253
+ const options = filePath
254
+ ? { workingDir: dirname(filePath), sourcePath: filePath }
255
+ : undefined;
256
+
257
+ const result = await ServeCommand.transpiler.transpileSource(
258
+ source,
259
+ options,
260
+ );
187
261
 
188
262
  return {
189
263
  success: true,
@@ -191,16 +265,19 @@ class ServeCommand {
191
265
  success: result.success,
192
266
  code: result.code,
193
267
  errors: result.errors,
268
+ cppDetected: ServeCommand.transpiler.isCppDetected(),
194
269
  },
195
270
  };
196
271
  }
197
272
 
198
273
  /**
199
274
  * Handle parseSymbols method
275
+ * Runs full transpilation for include/C++ detection, then extracts symbols
276
+ * from the parse tree (preserving "extract symbols even with parse errors" behavior)
200
277
  */
201
- private static handleParseSymbols(
278
+ private static async handleParseSymbols(
202
279
  params?: Record<string, unknown>,
203
- ): IMethodResult {
280
+ ): Promise<IMethodResult> {
204
281
  if (!params || typeof params.source !== "string") {
205
282
  return {
206
283
  success: false,
@@ -209,22 +286,64 @@ class ServeCommand {
209
286
  };
210
287
  }
211
288
 
212
- const result = parseWithSymbols(params.source);
289
+ const source = String(params.source);
290
+ const filePath =
291
+ typeof params.filePath === "string" ? params.filePath : undefined;
292
+
293
+ // If transpiler is initialized, run transpileSource to trigger header
294
+ // resolution and C++ detection (results are discarded, we just want
295
+ // the side effects on the symbol table)
296
+ if (ServeCommand.transpiler && filePath) {
297
+ try {
298
+ await ServeCommand.transpiler.transpileSource(source, {
299
+ workingDir: dirname(filePath),
300
+ sourcePath: filePath,
301
+ });
302
+ } catch {
303
+ // Ignore transpilation errors - we still extract symbols below
304
+ }
305
+ }
306
+
307
+ // Delegate symbol extraction to parseWithSymbols (shared with WorkspaceIndex)
308
+ const result = parseWithSymbols(source);
213
309
 
214
310
  return {
215
311
  success: true,
216
- result: {
217
- success: result.success,
218
- errors: result.errors,
219
- symbols: result.symbols,
220
- },
312
+ result,
313
+ };
314
+ }
315
+
316
+ /**
317
+ * Handle parseCHeader method
318
+ * Parses C/C++ header files and extracts symbols
319
+ */
320
+ private static async handleParseCHeader(
321
+ params?: Record<string, unknown>,
322
+ ): Promise<IMethodResult> {
323
+ if (!params || typeof params.source !== "string") {
324
+ return {
325
+ success: false,
326
+ errorCode: JsonRpcHandler.ERROR_INVALID_PARAMS,
327
+ errorMessage: "Missing required param: source",
328
+ };
329
+ }
330
+
331
+ const source = String(params.source);
332
+ const filePath =
333
+ typeof params.filePath === "string" ? params.filePath : undefined;
334
+
335
+ const result = parseCHeader(source, filePath);
336
+
337
+ return {
338
+ success: true,
339
+ result,
221
340
  };
222
341
  }
223
342
 
224
343
  /**
225
344
  * Handle shutdown method
226
345
  */
227
- private static handleShutdown(): IMethodResult {
346
+ private static async handleShutdown(): Promise<IMethodResult> {
228
347
  ServeCommand.shouldShutdown = true;
229
348
  return {
230
349
  success: true,
@@ -25,7 +25,7 @@ describe("ServeCommand", () => {
25
25
  stdoutWriteSpy.mockRestore();
26
26
  });
27
27
 
28
- // Helper to send a request and capture the response
28
+ // Helper to send a request and capture the async response
29
29
  async function sendRequest(request: object): Promise<object> {
30
30
  const line = JSON.stringify(request);
31
31
  // Access private handleLine method for testing
@@ -34,8 +34,14 @@ describe("ServeCommand", () => {
34
34
  ).handleLine.bind(ServeCommand);
35
35
  handleLine(line);
36
36
 
37
- // Parse the response from stdout.write call
38
- const writeCall = stdoutWriteSpy.mock.calls[0];
37
+ // Wait for async dispatch to complete and write response
38
+ await vi.waitFor(() => {
39
+ expect(stdoutWriteSpy.mock.calls.length).toBeGreaterThan(0);
40
+ });
41
+
42
+ // Parse the response from the latest stdout.write call
43
+ const writeCall =
44
+ stdoutWriteSpy.mock.calls[stdoutWriteSpy.mock.calls.length - 1];
39
45
  if (writeCall) {
40
46
  const responseStr = writeCall[0] as string;
41
47
  return JSON.parse(responseStr.trim());
@@ -54,8 +60,67 @@ describe("ServeCommand", () => {
54
60
  });
55
61
  });
56
62
 
63
+ describe("initialize", () => {
64
+ it("initializes with workspace path", async () => {
65
+ const response = await sendRequest({
66
+ id: 10,
67
+ method: "initialize",
68
+ params: { workspacePath: "/tmp" },
69
+ });
70
+
71
+ expect(response).toMatchObject({
72
+ id: 10,
73
+ result: { success: true },
74
+ });
75
+ });
76
+
77
+ it("returns error for missing workspacePath param", async () => {
78
+ const response = await sendRequest({
79
+ id: 11,
80
+ method: "initialize",
81
+ params: {},
82
+ });
83
+
84
+ expect(response).toMatchObject({
85
+ id: 11,
86
+ error: {
87
+ code: JsonRpcHandler.ERROR_INVALID_PARAMS,
88
+ message: "Missing required param: workspacePath",
89
+ },
90
+ });
91
+ });
92
+ });
93
+
57
94
  describe("transpile", () => {
95
+ it("returns error when server not initialized", async () => {
96
+ // Reset transpiler to null by accessing private field
97
+ (ServeCommand as unknown as { transpiler: null }).transpiler = null;
98
+ stdoutWriteSpy.mockClear();
99
+
100
+ const response = await sendRequest({
101
+ id: 50,
102
+ method: "transpile",
103
+ params: { source: "u8 x <- 5;" },
104
+ });
105
+
106
+ expect(response).toMatchObject({
107
+ id: 50,
108
+ error: {
109
+ code: JsonRpcHandler.ERROR_INVALID_PARAMS,
110
+ message: "Server not initialized. Call initialize first.",
111
+ },
112
+ });
113
+ });
114
+
58
115
  it("transpiles valid C-Next source", async () => {
116
+ // Initialize first (required for transpile)
117
+ await sendRequest({
118
+ id: 100,
119
+ method: "initialize",
120
+ params: { workspacePath: "/tmp" },
121
+ });
122
+ stdoutWriteSpy.mockClear();
123
+
59
124
  const response = await sendRequest({
60
125
  id: 2,
61
126
  method: "transpile",
@@ -73,6 +138,14 @@ describe("ServeCommand", () => {
73
138
  });
74
139
 
75
140
  it("returns errors for invalid source", async () => {
141
+ // Initialize first
142
+ await sendRequest({
143
+ id: 101,
144
+ method: "initialize",
145
+ params: { workspacePath: "/tmp" },
146
+ });
147
+ stdoutWriteSpy.mockClear();
148
+
76
149
  const response = await sendRequest({
77
150
  id: 3,
78
151
  method: "transpile",
@@ -139,6 +212,35 @@ describe("ServeCommand", () => {
139
212
  );
140
213
  });
141
214
 
215
+ it("parses symbols with filePath after initialization", async () => {
216
+ // Initialize first
217
+ await sendRequest({
218
+ id: 60,
219
+ method: "initialize",
220
+ params: { workspacePath: "/tmp" },
221
+ });
222
+ stdoutWriteSpy.mockClear();
223
+
224
+ const response = await sendRequest({
225
+ id: 61,
226
+ method: "parseSymbols",
227
+ params: {
228
+ source: "void myFunc() { }",
229
+ filePath: "/tmp/test.cnx",
230
+ },
231
+ });
232
+
233
+ const result = response as {
234
+ id: number;
235
+ result: { success: boolean; symbols: Array<{ name: string }> };
236
+ };
237
+ expect(result.id).toBe(61);
238
+ expect(result.result.success).toBe(true);
239
+ expect(result.result.symbols).toEqual(
240
+ expect.arrayContaining([expect.objectContaining({ name: "myFunc" })]),
241
+ );
242
+ });
243
+
142
244
  it("returns error for missing source param", async () => {
143
245
  const response = await sendRequest({
144
246
  id: 7,
@@ -154,6 +256,95 @@ describe("ServeCommand", () => {
154
256
  },
155
257
  });
156
258
  });
259
+
260
+ it("returns error for missing params", async () => {
261
+ const response = await sendRequest({
262
+ id: 70,
263
+ method: "parseSymbols",
264
+ });
265
+
266
+ expect(response).toMatchObject({
267
+ id: 70,
268
+ error: {
269
+ code: JsonRpcHandler.ERROR_INVALID_PARAMS,
270
+ message: "Missing required param: source",
271
+ },
272
+ });
273
+ });
274
+ });
275
+
276
+ describe("parseCHeader", () => {
277
+ it("parses symbols from C header source", async () => {
278
+ const response = await sendRequest({
279
+ id: 71,
280
+ method: "parseCHeader",
281
+ params: { source: "int myFunction(void);" },
282
+ });
283
+
284
+ const result = response as {
285
+ id: number;
286
+ result: { success: boolean; symbols: Array<{ name: string }> };
287
+ };
288
+ expect(result.id).toBe(71);
289
+ expect(result.result.success).toBe(true);
290
+ expect(result.result.symbols).toEqual(
291
+ expect.arrayContaining([
292
+ expect.objectContaining({ name: "myFunction" }),
293
+ ]),
294
+ );
295
+ });
296
+
297
+ it("parses struct definitions", async () => {
298
+ const response = await sendRequest({
299
+ id: 72,
300
+ method: "parseCHeader",
301
+ params: {
302
+ source: "typedef struct { int x; int y; } Point;",
303
+ filePath: "/tmp/types.h",
304
+ },
305
+ });
306
+
307
+ const result = response as {
308
+ id: number;
309
+ result: { success: boolean; symbols: Array<{ name: string }> };
310
+ };
311
+ expect(result.id).toBe(72);
312
+ expect(result.result.success).toBe(true);
313
+ expect(result.result.symbols).toEqual(
314
+ expect.arrayContaining([expect.objectContaining({ name: "Point" })]),
315
+ );
316
+ });
317
+
318
+ it("returns error for missing source param", async () => {
319
+ const response = await sendRequest({
320
+ id: 73,
321
+ method: "parseCHeader",
322
+ params: {},
323
+ });
324
+
325
+ expect(response).toMatchObject({
326
+ id: 73,
327
+ error: {
328
+ code: JsonRpcHandler.ERROR_INVALID_PARAMS,
329
+ message: "Missing required param: source",
330
+ },
331
+ });
332
+ });
333
+
334
+ it("returns error for missing params", async () => {
335
+ const response = await sendRequest({
336
+ id: 74,
337
+ method: "parseCHeader",
338
+ });
339
+
340
+ expect(response).toMatchObject({
341
+ id: 74,
342
+ error: {
343
+ code: JsonRpcHandler.ERROR_INVALID_PARAMS,
344
+ message: "Missing required param: source",
345
+ },
346
+ });
347
+ });
157
348
  });
158
349
 
159
350
  describe("shutdown", () => {
@@ -197,11 +388,44 @@ describe("ServeCommand", () => {
197
388
  handleLine("");
198
389
  handleLine(" ");
199
390
 
391
+ // Wait a tick to ensure any async work would have started
392
+ await new Promise((r) => setTimeout(r, 10));
393
+
200
394
  // No response should be written for empty lines
201
395
  expect(stdoutWriteSpy).not.toHaveBeenCalled();
202
396
  });
203
397
  });
204
398
 
399
+ describe("debug logging", () => {
400
+ it("writes debug messages to stderr when debug mode is enabled", async () => {
401
+ const stderrWriteSpy = vi
402
+ .spyOn(process.stderr, "write")
403
+ .mockImplementation(() => true);
404
+
405
+ // Enable debug mode via private field
406
+ (ServeCommand as unknown as { debugMode: boolean }).debugMode = true;
407
+
408
+ const response = await sendRequest({
409
+ id: 80,
410
+ method: "getVersion",
411
+ });
412
+
413
+ expect(response).toMatchObject({
414
+ id: 80,
415
+ result: { version: expect.any(String) },
416
+ });
417
+
418
+ // Debug logs should have been written to stderr
419
+ expect(stderrWriteSpy).toHaveBeenCalled();
420
+ const debugOutput = stderrWriteSpy.mock.calls.map((c) => c[0]).join("");
421
+ expect(debugOutput).toContain("[serve]");
422
+
423
+ // Clean up
424
+ (ServeCommand as unknown as { debugMode: boolean }).debugMode = false;
425
+ stderrWriteSpy.mockRestore();
426
+ });
427
+ });
428
+
205
429
  describe("invalid JSON", () => {
206
430
  it("returns parse error", async () => {
207
431
  const handleLine = (