@dreki-gg/pi-lsp 0.3.0 → 0.4.0

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.
@@ -0,0 +1,107 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { Effect, Layer } from 'effect';
3
+
4
+ import { loadConfigEffect, scaffoldGlobalConfigEffect } from '../extensions/lsp/config';
5
+ import { CommandResolver, type CommandAvailability } from '../extensions/lsp/effects/command';
6
+ import { FileSystem, type FileSystemService } from '../extensions/lsp/effects/filesystem';
7
+ import { ConfigReadError } from '../extensions/lsp/errors';
8
+
9
+ /** Build an in-memory FileSystem service over a path→content map. */
10
+ function fakeFs(files: Record<string, string>, writes?: Record<string, string>): FileSystemService {
11
+ return {
12
+ readTextFile: (path) =>
13
+ path in files
14
+ ? Effect.succeed(files[path])
15
+ : Effect.fail(new ConfigReadError({ path, cause: new Error('ENOENT') })),
16
+ fileExists: (path) => Effect.succeed(path in files),
17
+ writeTextFile: (path, content) =>
18
+ Effect.sync(() => {
19
+ if (writes) writes[path] = content;
20
+ files[path] = content;
21
+ }),
22
+ };
23
+ }
24
+
25
+ function fakeResolver(availability: CommandAvailability) {
26
+ return Layer.succeed(CommandResolver, { resolve: () => Effect.succeed(availability) });
27
+ }
28
+
29
+ describe('effect services', () => {
30
+ test('loadConfigEffect resolves servers via injected services (no disk, no shell)', async () => {
31
+ const home = process.env.HOME ?? '';
32
+ const globalPath = `${home}/.pi/agent/extensions/lsp/config.json`;
33
+ const fs = fakeFs({
34
+ [globalPath]: JSON.stringify({
35
+ lsp: { ts: { command: ['ts-ls', '--stdio'], extensions: ['.ts'] } },
36
+ }),
37
+ });
38
+
39
+ const layer = Layer.merge(Layer.succeed(FileSystem, fs), fakeResolver('global'));
40
+ const config = await Effect.runPromise(
41
+ loadConfigEffect('/workspace').pipe(Effect.provide(layer)),
42
+ );
43
+
44
+ expect(config.globalDisabled).toBe(false);
45
+ expect(config.servers).toEqual([
46
+ {
47
+ name: 'ts',
48
+ command: 'ts-ls',
49
+ args: ['--stdio'],
50
+ extensions: ['.ts'],
51
+ env: {},
52
+ initializationOptions: {},
53
+ },
54
+ ]);
55
+ });
56
+
57
+ test('loadConfigEffect rewrites command to npx when only available via npx', async () => {
58
+ const home = process.env.HOME ?? '';
59
+ const globalPath = `${home}/.pi/agent/extensions/lsp/config.json`;
60
+ const fs = fakeFs({
61
+ [globalPath]: JSON.stringify({
62
+ lsp: { ts: { command: ['ts-ls', '--stdio'], extensions: ['.ts'] } },
63
+ }),
64
+ });
65
+
66
+ const layer = Layer.merge(Layer.succeed(FileSystem, fs), fakeResolver('npx'));
67
+ const config = await Effect.runPromise(
68
+ loadConfigEffect('/workspace').pipe(Effect.provide(layer)),
69
+ );
70
+
71
+ expect(config.servers[0]?.command).toBe('npx');
72
+ expect(config.servers[0]?.args).toEqual(['--yes', 'ts-ls', '--stdio']);
73
+ });
74
+
75
+ test('loadConfigEffect drops servers whose command cannot be resolved', async () => {
76
+ const home = process.env.HOME ?? '';
77
+ const globalPath = `${home}/.pi/agent/extensions/lsp/config.json`;
78
+ const fs = fakeFs({
79
+ [globalPath]: JSON.stringify({
80
+ lsp: { ts: { command: ['missing-ls'], extensions: ['.ts'] } },
81
+ }),
82
+ });
83
+
84
+ const layer = Layer.merge(Layer.succeed(FileSystem, fs), fakeResolver(null));
85
+ const config = await Effect.runPromise(
86
+ loadConfigEffect('/workspace').pipe(Effect.provide(layer)),
87
+ );
88
+
89
+ expect(config.servers).toEqual([]);
90
+ });
91
+
92
+ test('scaffoldGlobalConfigEffect writes the starter template when nothing exists', async () => {
93
+ const writes: Record<string, string> = {};
94
+ const fs = fakeFs({}, writes);
95
+
96
+ const created = await Effect.runPromise(
97
+ scaffoldGlobalConfigEffect('/workspace').pipe(Effect.provideService(FileSystem, fs)),
98
+ );
99
+
100
+ expect(created).toBe(true);
101
+ const written = Object.values(writes)[0] ?? '';
102
+ const parsed = JSON.parse(written);
103
+ for (const server of Object.values(parsed.lsp as Record<string, { disabled?: boolean }>)) {
104
+ expect(server.disabled).toBe(true);
105
+ }
106
+ });
107
+ });
@@ -107,8 +107,9 @@ describe('extension/session layer', () => {
107
107
 
108
108
  const globalConfigPath = join(home, '.pi', 'agent', 'extensions', 'lsp', 'config.json');
109
109
  expect(await fileExists(globalConfigPath)).toBe(true);
110
- const text = await readFile(globalConfigPath, 'utf8');
111
- expect(text).toContain('typescript-language-server');
110
+ const parsed = JSON.parse(await readFile(globalConfigPath, 'utf8'));
111
+ // typescript is present only as a disabled example — never auto-enabled.
112
+ expect(parsed.lsp.typescript.disabled).toBe(true);
112
113
  expect(ui.notifications.some((n) => n.includes('created starter config'))).toBe(true);
113
114
  });
114
115
 
@@ -251,10 +251,7 @@ function handleDocSync(uri: string, text: string) {
251
251
  function handleNotification(msg: JsonRpcNotification) {
252
252
  switch (msg.method) {
253
253
  case 'textDocument/didOpen':
254
- handleDocSync(
255
- msg.params?.textDocument?.uri ?? lastUri,
256
- msg.params?.textDocument?.text ?? '',
257
- );
254
+ handleDocSync(msg.params?.textDocument?.uri ?? lastUri, msg.params?.textDocument?.text ?? '');
258
255
  break;
259
256
  case 'textDocument/didChange':
260
257
  handleDocSync(
@@ -263,10 +260,7 @@ function handleNotification(msg: JsonRpcNotification) {
263
260
  );
264
261
  break;
265
262
  case 'textDocument/didSave':
266
- handleDocSync(
267
- msg.params?.textDocument?.uri ?? lastUri,
268
- msg.params?.text ?? '',
269
- );
263
+ handleDocSync(msg.params?.textDocument?.uri ?? lastUri, msg.params?.text ?? '');
270
264
  break;
271
265
  case 'exit':
272
266
  process.exit(0);
@@ -139,17 +139,17 @@ describe('unified lsp tool dispatch', () => {
139
139
  emptyManager({
140
140
  anyClient: () => ({
141
141
  workspaceSymbol: async (query: string) => [
142
- {
143
- name: `${query}Service`,
144
- kind: 5,
145
- location: {
146
- uri: 'file:///repo/src/service.ts',
147
- range: { start: { line: 9, character: 0 }, end: { line: 9, character: 5 } },
142
+ {
143
+ name: `${query}Service`,
144
+ kind: 5,
145
+ location: {
146
+ uri: 'file:///repo/src/service.ts',
147
+ range: { start: { line: 9, character: 0 }, end: { line: 9, character: 5 } },
148
+ },
149
+ containerName: 'services',
148
150
  },
149
- containerName: 'services',
150
- },
151
- ],
152
- }),
151
+ ],
152
+ }),
153
153
  }),
154
154
  );
155
155