@qwickapps/qwickbrain-proxy 1.0.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.
Files changed (90) hide show
  1. package/.github/workflows/publish.yml +92 -0
  2. package/CHANGELOG.md +47 -0
  3. package/LICENSE +45 -0
  4. package/README.md +165 -0
  5. package/dist/bin/cli.d.ts +3 -0
  6. package/dist/bin/cli.d.ts.map +1 -0
  7. package/dist/bin/cli.js +142 -0
  8. package/dist/bin/cli.js.map +1 -0
  9. package/dist/db/client.d.ts +10 -0
  10. package/dist/db/client.d.ts.map +1 -0
  11. package/dist/db/client.js +23 -0
  12. package/dist/db/client.js.map +1 -0
  13. package/dist/db/schema.d.ts +551 -0
  14. package/dist/db/schema.d.ts.map +1 -0
  15. package/dist/db/schema.js +65 -0
  16. package/dist/db/schema.js.map +1 -0
  17. package/dist/index.d.ts +7 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +5 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/lib/__tests__/cache-manager.test.d.ts +2 -0
  22. package/dist/lib/__tests__/cache-manager.test.d.ts.map +1 -0
  23. package/dist/lib/__tests__/cache-manager.test.js +202 -0
  24. package/dist/lib/__tests__/cache-manager.test.js.map +1 -0
  25. package/dist/lib/__tests__/connection-manager.test.d.ts +2 -0
  26. package/dist/lib/__tests__/connection-manager.test.d.ts.map +1 -0
  27. package/dist/lib/__tests__/connection-manager.test.js +188 -0
  28. package/dist/lib/__tests__/connection-manager.test.js.map +1 -0
  29. package/dist/lib/__tests__/proxy-server.test.d.ts +2 -0
  30. package/dist/lib/__tests__/proxy-server.test.d.ts.map +1 -0
  31. package/dist/lib/__tests__/proxy-server.test.js +205 -0
  32. package/dist/lib/__tests__/proxy-server.test.js.map +1 -0
  33. package/dist/lib/__tests__/qwickbrain-client.test.d.ts +2 -0
  34. package/dist/lib/__tests__/qwickbrain-client.test.d.ts.map +1 -0
  35. package/dist/lib/__tests__/qwickbrain-client.test.js +233 -0
  36. package/dist/lib/__tests__/qwickbrain-client.test.js.map +1 -0
  37. package/dist/lib/cache-manager.d.ts +25 -0
  38. package/dist/lib/cache-manager.d.ts.map +1 -0
  39. package/dist/lib/cache-manager.js +149 -0
  40. package/dist/lib/cache-manager.js.map +1 -0
  41. package/dist/lib/connection-manager.d.ts +26 -0
  42. package/dist/lib/connection-manager.d.ts.map +1 -0
  43. package/dist/lib/connection-manager.js +130 -0
  44. package/dist/lib/connection-manager.js.map +1 -0
  45. package/dist/lib/proxy-server.d.ts +19 -0
  46. package/dist/lib/proxy-server.d.ts.map +1 -0
  47. package/dist/lib/proxy-server.js +258 -0
  48. package/dist/lib/proxy-server.js.map +1 -0
  49. package/dist/lib/qwickbrain-client.d.ts +24 -0
  50. package/dist/lib/qwickbrain-client.d.ts.map +1 -0
  51. package/dist/lib/qwickbrain-client.js +197 -0
  52. package/dist/lib/qwickbrain-client.js.map +1 -0
  53. package/dist/types/config.d.ts +186 -0
  54. package/dist/types/config.d.ts.map +1 -0
  55. package/dist/types/config.js +42 -0
  56. package/dist/types/config.js.map +1 -0
  57. package/dist/types/mcp.d.ts +223 -0
  58. package/dist/types/mcp.d.ts.map +1 -0
  59. package/dist/types/mcp.js +78 -0
  60. package/dist/types/mcp.js.map +1 -0
  61. package/dist/version.d.ts +2 -0
  62. package/dist/version.d.ts.map +1 -0
  63. package/dist/version.js +9 -0
  64. package/dist/version.js.map +1 -0
  65. package/drizzle/0000_fat_rafael_vega.sql +41 -0
  66. package/drizzle/0001_goofy_invisible_woman.sql +2 -0
  67. package/drizzle/meta/0000_snapshot.json +276 -0
  68. package/drizzle/meta/0001_snapshot.json +295 -0
  69. package/drizzle/meta/_journal.json +20 -0
  70. package/drizzle.config.ts +12 -0
  71. package/package.json +65 -0
  72. package/src/bin/cli.ts +158 -0
  73. package/src/db/client.ts +34 -0
  74. package/src/db/schema.ts +68 -0
  75. package/src/index.ts +6 -0
  76. package/src/lib/__tests__/cache-manager.test.ts +264 -0
  77. package/src/lib/__tests__/connection-manager.test.ts +255 -0
  78. package/src/lib/__tests__/proxy-server.test.ts +261 -0
  79. package/src/lib/__tests__/qwickbrain-client.test.ts +310 -0
  80. package/src/lib/cache-manager.ts +201 -0
  81. package/src/lib/connection-manager.ts +156 -0
  82. package/src/lib/proxy-server.ts +320 -0
  83. package/src/lib/qwickbrain-client.ts +260 -0
  84. package/src/types/config.ts +47 -0
  85. package/src/types/mcp.ts +97 -0
  86. package/src/version.ts +11 -0
  87. package/test/fixtures/test-mcp.json +5 -0
  88. package/test-mcp-client.js +67 -0
  89. package/test-proxy.sh +25 -0
  90. package/tsconfig.json +22 -0
@@ -0,0 +1,255 @@
1
+ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
2
+ import { ConnectionManager } from '../connection-manager.js';
3
+ import type { QwickBrainClient } from '../qwickbrain-client.js';
4
+ import type { Config } from '../../types/config.js';
5
+
6
+ describe('ConnectionManager', () => {
7
+ let mockClient: QwickBrainClient;
8
+ let connectionManager: ConnectionManager;
9
+ let config: Config['connection'];
10
+
11
+ beforeEach(() => {
12
+ config = {
13
+ healthCheckInterval: 100, // Short interval for testing
14
+ timeout: 5000,
15
+ maxReconnectAttempts: 3,
16
+ reconnectBackoff: {
17
+ initial: 50,
18
+ multiplier: 2,
19
+ max: 500,
20
+ },
21
+ };
22
+
23
+ mockClient = {
24
+ healthCheck: vi.fn().mockResolvedValue(true),
25
+ } as unknown as QwickBrainClient;
26
+
27
+ connectionManager = new ConnectionManager(mockClient, config);
28
+ });
29
+
30
+ afterEach(() => {
31
+ connectionManager.stop();
32
+ });
33
+
34
+ describe('getState', () => {
35
+ it('should return initial state as disconnected', () => {
36
+ expect(connectionManager.getState()).toBe('disconnected');
37
+ });
38
+ });
39
+
40
+ describe('start and healthCheck', () => {
41
+ it('should transition to connected on successful health check', async () => {
42
+ await connectionManager.start();
43
+
44
+ expect(connectionManager.getState()).toBe('connected');
45
+ expect(mockClient.healthCheck).toHaveBeenCalled();
46
+ });
47
+
48
+ it('should transition to reconnecting after failed health check', async () => {
49
+ mockClient.healthCheck = vi.fn().mockRejectedValue(new Error('Connection failed'));
50
+
51
+ await connectionManager.start();
52
+
53
+ // After health check fails, it immediately schedules reconnect
54
+ expect(connectionManager.getState()).toBe('reconnecting');
55
+ });
56
+
57
+ it('should emit stateChange event on state transition', async () => {
58
+ const stateChangeListener = vi.fn();
59
+ connectionManager.on('stateChange', stateChangeListener);
60
+
61
+ await connectionManager.start();
62
+
63
+ expect(stateChangeListener).toHaveBeenCalledWith({
64
+ from: 'disconnected',
65
+ to: 'connected',
66
+ });
67
+ });
68
+
69
+ it('should emit connected event with latency', async () => {
70
+ const connectedListener = vi.fn();
71
+ connectionManager.on('connected', connectedListener);
72
+
73
+ await connectionManager.start();
74
+
75
+ expect(connectedListener).toHaveBeenCalled();
76
+ const call = connectedListener.mock.calls[0][0];
77
+ expect(call).toHaveProperty('latencyMs');
78
+ expect(typeof call.latencyMs).toBe('number');
79
+ });
80
+
81
+ it('should emit disconnected event on failure', async () => {
82
+ mockClient.healthCheck = vi.fn().mockRejectedValue(new Error('Failed'));
83
+ const disconnectedListener = vi.fn();
84
+ connectionManager.on('disconnected', disconnectedListener);
85
+
86
+ await connectionManager.start();
87
+
88
+ expect(disconnectedListener).toHaveBeenCalled();
89
+ expect(disconnectedListener.mock.calls[0][0]).toHaveProperty('error');
90
+ });
91
+ });
92
+
93
+ describe('stop', () => {
94
+ it('should transition to offline state', () => {
95
+ connectionManager.stop();
96
+
97
+ expect(connectionManager.getState()).toBe('offline');
98
+ });
99
+
100
+ it('should stop health check timer', async () => {
101
+ await connectionManager.start();
102
+ const healthCheckCount = (mockClient.healthCheck as any).mock.calls.length;
103
+
104
+ connectionManager.stop();
105
+
106
+ // Wait longer than health check interval
107
+ await new Promise(resolve => setTimeout(resolve, 300));
108
+
109
+ // Health check should not have been called again
110
+ expect((mockClient.healthCheck as any).mock.calls.length).toBe(healthCheckCount);
111
+ });
112
+ });
113
+
114
+ describe('reconnection logic', () => {
115
+ it('should attempt to reconnect on health check failure', async () => {
116
+ let callCount = 0;
117
+ mockClient.healthCheck = vi.fn().mockImplementation(() => {
118
+ callCount++;
119
+ if (callCount === 1) {
120
+ return Promise.reject(new Error('Failed'));
121
+ }
122
+ return Promise.resolve(true);
123
+ });
124
+
125
+ const reconnectingListener = vi.fn();
126
+ connectionManager.on('reconnecting', reconnectingListener);
127
+
128
+ await connectionManager.start();
129
+
130
+ // Wait for reconnect attempt
131
+ await new Promise(resolve => setTimeout(resolve, 100));
132
+
133
+ expect(reconnectingListener).toHaveBeenCalled();
134
+ expect(reconnectingListener.mock.calls[0][0]).toHaveProperty('attempt');
135
+ expect(reconnectingListener.mock.calls[0][0]).toHaveProperty('delay');
136
+ });
137
+
138
+ it('should use exponential backoff for reconnection', async () => {
139
+ mockClient.healthCheck = vi.fn().mockRejectedValue(new Error('Failed'));
140
+
141
+ const reconnectingListener = vi.fn();
142
+ connectionManager.on('reconnecting', reconnectingListener);
143
+
144
+ await connectionManager.start();
145
+
146
+ // Wait for multiple reconnect attempts
147
+ await new Promise(resolve => setTimeout(resolve, 500));
148
+
149
+ const delays = reconnectingListener.mock.calls.map(call => call[0].delay);
150
+
151
+ // Delays should increase exponentially
152
+ for (let i = 1; i < delays.length; i++) {
153
+ expect(delays[i]).toBeGreaterThan(delays[i - 1]);
154
+ }
155
+ });
156
+
157
+ it('should emit maxReconnectAttemptsReached after max attempts', async () => {
158
+ mockClient.healthCheck = vi.fn().mockRejectedValue(new Error('Failed'));
159
+
160
+ const maxAttemptsListener = vi.fn();
161
+ connectionManager.on('maxReconnectAttemptsReached', maxAttemptsListener);
162
+
163
+ await connectionManager.start();
164
+
165
+ // Wait for all reconnect attempts
166
+ await new Promise(resolve => setTimeout(resolve, 2000));
167
+
168
+ expect(maxAttemptsListener).toHaveBeenCalled();
169
+ expect(connectionManager.getState()).toBe('offline');
170
+ });
171
+
172
+ it('should reset reconnect attempts on successful connection', async () => {
173
+ let callCount = 0;
174
+ mockClient.healthCheck = vi.fn().mockImplementation(() => {
175
+ callCount++;
176
+ // Fail first two, then succeed
177
+ if (callCount <= 2) {
178
+ return Promise.reject(new Error('Failed'));
179
+ }
180
+ return Promise.resolve(true);
181
+ });
182
+
183
+ await connectionManager.start();
184
+
185
+ // Wait for reconnection
186
+ await new Promise(resolve => setTimeout(resolve, 300));
187
+
188
+ expect(connectionManager.getState()).toBe('connected');
189
+ });
190
+ });
191
+
192
+ describe('execute', () => {
193
+ it('should execute function when connected', async () => {
194
+ await connectionManager.start();
195
+
196
+ const testFn = vi.fn().mockResolvedValue('result');
197
+ const result = await connectionManager.execute(testFn);
198
+
199
+ expect(result).toBe('result');
200
+ expect(testFn).toHaveBeenCalled();
201
+ });
202
+
203
+ it('should throw error when not connected', async () => {
204
+ const testFn = vi.fn();
205
+
206
+ await expect(connectionManager.execute(testFn)).rejects.toThrow('QwickBrain unavailable');
207
+ expect(testFn).not.toHaveBeenCalled();
208
+ });
209
+
210
+ it('should record failure if execution throws', async () => {
211
+ await connectionManager.start();
212
+
213
+ const testFn = vi.fn().mockRejectedValue(new Error('Execution failed'));
214
+
215
+ await expect(connectionManager.execute(testFn)).rejects.toThrow('Execution failed');
216
+
217
+ // Should transition to reconnecting after failure
218
+ expect(connectionManager.getState()).toBe('reconnecting');
219
+ });
220
+
221
+ it('should wait for state transitions to complete (race condition test)', async () => {
222
+ await connectionManager.start();
223
+
224
+ // Simulate rapid state changes
225
+ connectionManager.setState('disconnected');
226
+ connectionManager.setState('connected');
227
+
228
+ const testFn = vi.fn().mockResolvedValue('result');
229
+
230
+ // This should wait for state transitions to settle
231
+ const result = await connectionManager.execute(testFn);
232
+
233
+ expect(result).toBe('result');
234
+ });
235
+ });
236
+
237
+ describe('recordFailure', () => {
238
+ it('should transition to reconnecting when connected', () => {
239
+ connectionManager['state'] = 'connected';
240
+
241
+ connectionManager.recordFailure();
242
+
243
+ // After failure, it immediately schedules reconnect
244
+ expect(connectionManager.getState()).toBe('reconnecting');
245
+ });
246
+
247
+ it('should not affect offline state', () => {
248
+ connectionManager.stop();
249
+
250
+ connectionManager.recordFailure();
251
+
252
+ expect(connectionManager.getState()).toBe('offline');
253
+ });
254
+ });
255
+ });
@@ -0,0 +1,261 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { mkdtempSync, rmSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { tmpdir } from 'os';
5
+ import { createDatabase, runMigrations } from '../../db/client.js';
6
+ import { ProxyServer } from '../proxy-server.js';
7
+ import type { Config } from '../../types/config.js';
8
+
9
+ // Mock MCP SDK
10
+ vi.mock('@modelcontextprotocol/sdk/client/index.js', () => ({
11
+ Client: vi.fn().mockImplementation(() => ({
12
+ connect: vi.fn().mockResolvedValue(undefined),
13
+ close: vi.fn().mockResolvedValue(undefined),
14
+ callTool: vi.fn(),
15
+ listTools: vi.fn().mockResolvedValue({ tools: [] }),
16
+ })),
17
+ }));
18
+
19
+ vi.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
20
+ Server: vi.fn().mockImplementation(() => ({
21
+ setRequestHandler: vi.fn(),
22
+ connect: vi.fn().mockResolvedValue(undefined),
23
+ close: vi.fn().mockResolvedValue(undefined),
24
+ })),
25
+ }));
26
+
27
+ vi.mock('@modelcontextprotocol/sdk/server/stdio.js', () => ({
28
+ StdioServerTransport: vi.fn(),
29
+ }));
30
+
31
+ vi.mock('@modelcontextprotocol/sdk/client/sse.js', () => ({
32
+ SSEClientTransport: vi.fn(),
33
+ }));
34
+
35
+ describe('ProxyServer', () => {
36
+ let tmpDir: string;
37
+ let config: Config;
38
+ let db: ReturnType<typeof createDatabase>['db'];
39
+ let server: ProxyServer;
40
+
41
+ beforeEach(() => {
42
+ tmpDir = mkdtempSync(join(tmpdir(), 'proxy-test-'));
43
+ const dbResult = createDatabase(tmpDir);
44
+ db = dbResult.db;
45
+
46
+ // Run migrations to create tables
47
+ runMigrations(db);
48
+
49
+ config = {
50
+ qwickbrain: {
51
+ mode: 'sse',
52
+ url: 'http://test.local:3000',
53
+ },
54
+ cache: {
55
+ dir: tmpDir,
56
+ ttl: {
57
+ workflows: 3600,
58
+ rules: 3600,
59
+ documents: 1800,
60
+ memories: 900,
61
+ },
62
+ preload: [],
63
+ },
64
+ connection: {
65
+ healthCheckInterval: 30000,
66
+ timeout: 5000,
67
+ maxReconnectAttempts: 5,
68
+ reconnectBackoff: {
69
+ initial: 1000,
70
+ multiplier: 2,
71
+ max: 30000,
72
+ },
73
+ },
74
+ sync: {
75
+ interval: 60000,
76
+ batchSize: 10,
77
+ },
78
+ };
79
+
80
+ server = new ProxyServer(db, config);
81
+ });
82
+
83
+ afterEach(async () => {
84
+ await server.stop();
85
+ rmSync(tmpDir, { recursive: true, force: true });
86
+ });
87
+
88
+ describe('initialization', () => {
89
+ it('should initialize with all components', () => {
90
+ expect(server['server']).toBeDefined();
91
+ expect(server['connectionManager']).toBeDefined();
92
+ expect(server['cacheManager']).toBeDefined();
93
+ expect(server['qwickbrainClient']).toBeDefined();
94
+ });
95
+
96
+ it('should set up MCP request handlers', () => {
97
+ const setRequestHandlerMock = server['server'].setRequestHandler;
98
+
99
+ // Should have set up handlers for ListTools and CallTool
100
+ expect(setRequestHandlerMock).toHaveBeenCalled();
101
+ expect((setRequestHandlerMock as any).mock.calls.length).toBeGreaterThanOrEqual(2);
102
+ });
103
+ });
104
+
105
+ describe('cache cleanup on startup', () => {
106
+ it('should clean up expired cache entries on start', async () => {
107
+ // Add some expired entries
108
+ const shortTTLConfig = { ...config };
109
+ shortTTLConfig.cache.ttl = {
110
+ workflows: 0,
111
+ rules: 0,
112
+ documents: 0,
113
+ memories: 0,
114
+ };
115
+
116
+ const tempServer = new ProxyServer(db, shortTTLConfig);
117
+
118
+ await tempServer['cacheManager'].setDocument('workflow', 'test', 'content');
119
+ await new Promise(resolve => setTimeout(resolve, 100));
120
+
121
+ // Verify entry exists before cleanup
122
+ const beforeCleanup = await tempServer['cacheManager'].getDocument('workflow', 'test');
123
+ expect(beforeCleanup).not.toBeNull();
124
+
125
+ await tempServer.start();
126
+
127
+ // Entry should be removed after cleanup
128
+ const afterCleanup = await tempServer['cacheManager'].getDocument('workflow', 'test');
129
+ expect(afterCleanup).toBeNull();
130
+
131
+ await tempServer.stop();
132
+ });
133
+ });
134
+
135
+ describe('graceful degradation', () => {
136
+ it('should serve stale cache when disconnected', async () => {
137
+ // Create server with short TTL to make cache expire
138
+ const shortTTLConfig = { ...config };
139
+ shortTTLConfig.cache.ttl = {
140
+ workflows: 0,
141
+ rules: 0,
142
+ documents: 0,
143
+ memories: 0,
144
+ };
145
+ const tempServer = new ProxyServer(db, shortTTLConfig);
146
+
147
+ // Add item to cache
148
+ await tempServer['cacheManager'].setDocument('workflow', 'test', 'cached content');
149
+
150
+ // Wait for cache to expire
151
+ await new Promise(resolve => setTimeout(resolve, 100));
152
+
153
+ // Simulate disconnected state
154
+ tempServer['connectionManager']['state'] = 'disconnected';
155
+
156
+ const result = await tempServer['handleGetDocument']('workflow', 'test');
157
+
158
+ expect((result.data as any).content).toBe('cached content');
159
+ expect(result._metadata.source).toBe('stale_cache');
160
+
161
+ await tempServer.stop();
162
+ });
163
+
164
+ it('should return error when no cache available and disconnected', async () => {
165
+ // Simulate disconnected state with no cache
166
+ server['connectionManager']['state'] = 'disconnected';
167
+
168
+ const result = await server['handleGetDocument']('workflow', 'non-existent');
169
+
170
+ expect(result.error).toBeDefined();
171
+ expect(result.error?.code).toBe('UNAVAILABLE');
172
+ expect(result.error?.message).toContain('QwickBrain unavailable');
173
+ });
174
+ });
175
+
176
+ describe('metadata', () => {
177
+ it('should include correct metadata for cache hit', async () => {
178
+ await server['cacheManager'].setDocument('workflow', 'test', 'content');
179
+
180
+ // Simulate connected state
181
+ server['connectionManager']['state'] = 'connected';
182
+
183
+ const result = await server['handleGetDocument']('workflow', 'test');
184
+
185
+ expect(result._metadata.source).toBe('cache');
186
+ expect(result._metadata.age_seconds).toBeDefined();
187
+ expect(typeof result._metadata.age_seconds).toBe('number');
188
+ });
189
+
190
+ it('should create metadata with correct source', () => {
191
+ const cacheMeta = server['createMetadata']('cache', 100);
192
+ expect(cacheMeta).toEqual({
193
+ source: 'cache',
194
+ age_seconds: 100,
195
+ status: server['connectionManager'].getState(),
196
+ });
197
+
198
+ const liveMeta = server['createMetadata']('live');
199
+ expect(liveMeta).toEqual({
200
+ source: 'live',
201
+ status: server['connectionManager'].getState(),
202
+ });
203
+
204
+ const staleMeta = server['createMetadata']('stale_cache', 500);
205
+ expect(staleMeta).toEqual({
206
+ source: 'stale_cache',
207
+ age_seconds: 500,
208
+ status: server['connectionManager'].getState(),
209
+ });
210
+ });
211
+ });
212
+
213
+ describe('error handling', () => {
214
+ it('should handle QwickBrain errors gracefully', async () => {
215
+ // Mock client to throw error
216
+ server['qwickbrainClient'].getDocument = vi.fn().mockRejectedValue(
217
+ new Error('Network error')
218
+ );
219
+
220
+ // Simulate connected state
221
+ server['connectionManager']['state'] = 'connected';
222
+
223
+ const result = await server['handleGetDocument']('workflow', 'test');
224
+
225
+ // Should fall back to error response when remote fails and no cache
226
+ expect(result.error).toBeDefined();
227
+ expect(result.error?.code).toBe('UNAVAILABLE');
228
+ });
229
+ });
230
+
231
+ describe('project scoping', () => {
232
+ it('should handle project-scoped documents', async () => {
233
+ await server['cacheManager'].setDocument(
234
+ 'design',
235
+ 'test-design',
236
+ 'design content',
237
+ 'my-project'
238
+ );
239
+
240
+ server['connectionManager']['state'] = 'connected';
241
+
242
+ const result = await server['handleGetDocument']('design', 'test-design', 'my-project');
243
+
244
+ expect((result.data as any).content).toBe('design content');
245
+ expect((result.data as any).project).toBe('my-project');
246
+ });
247
+
248
+ it('should distinguish global vs project-scoped items', async () => {
249
+ await server['cacheManager'].setDocument('rule', 'test', 'global rule');
250
+ await server['cacheManager'].setDocument('rule', 'test', 'project rule', 'proj1');
251
+
252
+ server['connectionManager']['state'] = 'connected';
253
+
254
+ const globalResult = await server['handleGetDocument']('rule', 'test');
255
+ const projectResult = await server['handleGetDocument']('rule', 'test', 'proj1');
256
+
257
+ expect((globalResult.data as any).content).toBe('global rule');
258
+ expect((projectResult.data as any).content).toBe('project rule');
259
+ });
260
+ });
261
+ });