@anyshift/mcp-proxy 0.2.0 → 0.2.2
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/README.md +74 -31
- package/dist/__tests__/helpers/testUtils.d.ts +127 -0
- package/dist/__tests__/helpers/testUtils.js +122 -0
- package/dist/__tests__/unit/queryAssistSchema.test.d.ts +1 -0
- package/dist/__tests__/unit/queryAssistSchema.test.js +267 -0
- package/dist/__tests__/unit/truncation.test.d.ts +5 -0
- package/dist/__tests__/unit/truncation.test.js +204 -0
- package/dist/fileWriter/index.d.ts +1 -1
- package/dist/fileWriter/index.js +1 -1
- package/dist/fileWriter/schema.d.ts +38 -11
- package/dist/fileWriter/schema.js +248 -98
- package/dist/fileWriter/writer.js +66 -110
- package/dist/index.js +64 -28
- package/dist/jq/tool.js +15 -0
- package/dist/types/index.d.ts +6 -0
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -133,39 +133,82 @@ npx @anyshift/mcp-proxy
|
|
|
133
133
|
|
|
134
134
|
## How It Works
|
|
135
135
|
|
|
136
|
+
```mermaid
|
|
137
|
+
graph TB
|
|
138
|
+
AI[🤖 AI Agent<br/>Claude]
|
|
139
|
+
|
|
140
|
+
AI -->|MCP Protocol| Proxy
|
|
141
|
+
|
|
142
|
+
subgraph Proxy["@anyshift/mcp-proxy"]
|
|
143
|
+
Start[Receive tool call]
|
|
144
|
+
CheckJQ{JQ tool?}
|
|
145
|
+
ExecuteJQ[Execute JQ locally]
|
|
146
|
+
Forward[Forward to child MCP]
|
|
147
|
+
GetResponse[Get response from child]
|
|
148
|
+
CheckSize{Size ≥ 1000 chars?}
|
|
149
|
+
WriteFile[📄 Write FULL data to file]
|
|
150
|
+
ReturnFile[Return file reference]
|
|
151
|
+
CheckTrunc{Size > 40K chars?}
|
|
152
|
+
Truncate[Truncate with notice]
|
|
153
|
+
ReturnDirect[Return response]
|
|
154
|
+
|
|
155
|
+
Start --> CheckJQ
|
|
156
|
+
CheckJQ -->|Yes| ExecuteJQ
|
|
157
|
+
CheckJQ -->|No| Forward
|
|
158
|
+
Forward --> GetResponse
|
|
159
|
+
ExecuteJQ --> CheckTrunc
|
|
160
|
+
GetResponse --> CheckSize
|
|
161
|
+
CheckSize -->|Yes| WriteFile
|
|
162
|
+
WriteFile --> ReturnFile
|
|
163
|
+
CheckSize -->|No| CheckTrunc
|
|
164
|
+
CheckTrunc -->|Yes| Truncate
|
|
165
|
+
CheckTrunc -->|No| ReturnDirect
|
|
166
|
+
|
|
167
|
+
ReturnFile -.->|📄 Small reference| AI
|
|
168
|
+
Truncate -.->|Truncated text| AI
|
|
169
|
+
ReturnDirect -.->|Full response| AI
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
Proxy -->|stdio + env vars| Child[Child MCP<br/>anyshift/datadog/grafana]
|
|
173
|
+
Child -.->|Response| Proxy
|
|
174
|
+
|
|
175
|
+
style AI fill:#e1f5ff
|
|
176
|
+
style Proxy fill:#fff4e1
|
|
177
|
+
style Child fill:#e8f5e9
|
|
178
|
+
style WriteFile fill:#c8e6c9
|
|
179
|
+
style ReturnFile fill:#c8e6c9
|
|
136
180
|
```
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
│ if response > MIN_CHARS │
|
|
158
|
-
│ │
|
|
159
|
-
│ 6. Returns modified response │
|
|
160
|
-
│ to AI agent │
|
|
161
|
-
└────────────┬────────────────────┘
|
|
162
|
-
│ Child process (stdio)
|
|
163
|
-
▼
|
|
164
|
-
┌──────────────┐
|
|
165
|
-
│ Child MCP │ (mcp-grafana, custom-mcp, etc.)
|
|
166
|
-
│ Server │ Gets env vars WITHOUT MCP_PROXY_ prefix
|
|
167
|
-
└──────────────┘
|
|
181
|
+
|
|
182
|
+
### Response Handling Examples
|
|
183
|
+
|
|
184
|
+
**Small responses (< 1,000 chars):**
|
|
185
|
+
```
|
|
186
|
+
Child: 500 chars → Proxy: Return directly → AI: 500 chars ✓
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Medium responses (1,000 - 40,000 chars):**
|
|
190
|
+
```
|
|
191
|
+
Child: 5,000 chars → Proxy: Write to file → AI: "📄 File: path/to/file.json" ✓
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Large responses (> 40,000 chars):**
|
|
195
|
+
```
|
|
196
|
+
Child: 100,000 chars → Proxy: Write FULL 100K to file → AI: "📄 File: ..." ✓
|
|
197
|
+
Note: File contains complete data, not truncated!
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**JQ tool queries:**
|
|
168
201
|
```
|
|
202
|
+
AI: JQ query → Proxy: Execute locally → AI: Result (truncated if > 40K) ✓
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Key Design Principle
|
|
206
|
+
|
|
207
|
+
**File writing happens BEFORE truncation.** This ensures:
|
|
208
|
+
- Files always contain complete, untruncated data
|
|
209
|
+
- Large responses are accessible via file references
|
|
210
|
+
- AI receives small, manageable responses
|
|
211
|
+
- No data loss due to context limits
|
|
169
212
|
|
|
170
213
|
## Integration Examples
|
|
171
214
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test utilities and helpers for MCP Proxy tests
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Create an MCP response with specified character count
|
|
6
|
+
* @param charCount - Number of characters in the response
|
|
7
|
+
* @param jsonData - Optional JSON data to include (overrides charCount)
|
|
8
|
+
* @returns MCP-formatted response
|
|
9
|
+
*/
|
|
10
|
+
export declare function createMCPResponse(charCount: number, jsonData?: object): {
|
|
11
|
+
content: {
|
|
12
|
+
type: string;
|
|
13
|
+
text: string;
|
|
14
|
+
}[];
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Create an MCP response with specific JSON data
|
|
18
|
+
*/
|
|
19
|
+
export declare function createMCPResponseWithJSON(jsonData: object): {
|
|
20
|
+
content: {
|
|
21
|
+
type: string;
|
|
22
|
+
text: string;
|
|
23
|
+
}[];
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Create an error response
|
|
27
|
+
*/
|
|
28
|
+
export declare function createErrorResponse(errorMessage: string): {
|
|
29
|
+
content: {
|
|
30
|
+
type: string;
|
|
31
|
+
text: string;
|
|
32
|
+
}[];
|
|
33
|
+
isError: boolean;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Extract file path from file reference response
|
|
37
|
+
*/
|
|
38
|
+
export declare function extractFilePath(result: any): string;
|
|
39
|
+
/**
|
|
40
|
+
* Extract reported file size from file reference
|
|
41
|
+
*/
|
|
42
|
+
export declare function extractReportedSize(result: any): number;
|
|
43
|
+
/**
|
|
44
|
+
* Check if response is a file reference
|
|
45
|
+
*/
|
|
46
|
+
export declare function isFileReference(result: any): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Check if response is truncated
|
|
49
|
+
*/
|
|
50
|
+
export declare function isTruncated(result: any): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Create a mock file system for testing
|
|
53
|
+
*/
|
|
54
|
+
export declare class MockFileSystem {
|
|
55
|
+
private files;
|
|
56
|
+
writeFile(path: string, content: string): Promise<void>;
|
|
57
|
+
readFile(path: string): Promise<string>;
|
|
58
|
+
existsSync(path: string): boolean;
|
|
59
|
+
clear(): void;
|
|
60
|
+
getFiles(): string[];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Test fixtures for common scenarios
|
|
64
|
+
*/
|
|
65
|
+
export declare const fixtures: {
|
|
66
|
+
smallResponse: {
|
|
67
|
+
content: {
|
|
68
|
+
type: string;
|
|
69
|
+
text: string;
|
|
70
|
+
}[];
|
|
71
|
+
};
|
|
72
|
+
mediumResponse: {
|
|
73
|
+
content: {
|
|
74
|
+
type: string;
|
|
75
|
+
text: string;
|
|
76
|
+
}[];
|
|
77
|
+
};
|
|
78
|
+
largeResponse: {
|
|
79
|
+
content: {
|
|
80
|
+
type: string;
|
|
81
|
+
text: string;
|
|
82
|
+
}[];
|
|
83
|
+
};
|
|
84
|
+
atThreshold: {
|
|
85
|
+
content: {
|
|
86
|
+
type: string;
|
|
87
|
+
text: string;
|
|
88
|
+
}[];
|
|
89
|
+
};
|
|
90
|
+
justBelowThreshold: {
|
|
91
|
+
content: {
|
|
92
|
+
type: string;
|
|
93
|
+
text: string;
|
|
94
|
+
}[];
|
|
95
|
+
};
|
|
96
|
+
atTruncationLimit: {
|
|
97
|
+
content: {
|
|
98
|
+
type: string;
|
|
99
|
+
text: string;
|
|
100
|
+
}[];
|
|
101
|
+
};
|
|
102
|
+
overTruncationLimit: {
|
|
103
|
+
content: {
|
|
104
|
+
type: string;
|
|
105
|
+
text: string;
|
|
106
|
+
}[];
|
|
107
|
+
};
|
|
108
|
+
errorResponse: {
|
|
109
|
+
content: {
|
|
110
|
+
type: string;
|
|
111
|
+
text: string;
|
|
112
|
+
}[];
|
|
113
|
+
isError: boolean;
|
|
114
|
+
};
|
|
115
|
+
jsonResponse: {
|
|
116
|
+
content: {
|
|
117
|
+
type: string;
|
|
118
|
+
text: string;
|
|
119
|
+
}[];
|
|
120
|
+
};
|
|
121
|
+
withPagination: {
|
|
122
|
+
content: {
|
|
123
|
+
type: string;
|
|
124
|
+
text: string;
|
|
125
|
+
}[];
|
|
126
|
+
};
|
|
127
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test utilities and helpers for MCP Proxy tests
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Create an MCP response with specified character count
|
|
6
|
+
* @param charCount - Number of characters in the response
|
|
7
|
+
* @param jsonData - Optional JSON data to include (overrides charCount)
|
|
8
|
+
* @returns MCP-formatted response
|
|
9
|
+
*/
|
|
10
|
+
export function createMCPResponse(charCount, jsonData) {
|
|
11
|
+
const content = jsonData
|
|
12
|
+
? JSON.stringify(jsonData)
|
|
13
|
+
: 'x'.repeat(Math.max(0, charCount - 50)) + JSON.stringify({ padding: true });
|
|
14
|
+
return {
|
|
15
|
+
content: [{ type: 'text', text: content }]
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Create an MCP response with specific JSON data
|
|
20
|
+
*/
|
|
21
|
+
export function createMCPResponseWithJSON(jsonData) {
|
|
22
|
+
return {
|
|
23
|
+
content: [{ type: 'text', text: JSON.stringify(jsonData) }]
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Create an error response
|
|
28
|
+
*/
|
|
29
|
+
export function createErrorResponse(errorMessage) {
|
|
30
|
+
return {
|
|
31
|
+
content: [{
|
|
32
|
+
type: 'text',
|
|
33
|
+
text: JSON.stringify({ status: 'error', message: errorMessage })
|
|
34
|
+
}],
|
|
35
|
+
isError: true
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Extract file path from file reference response
|
|
40
|
+
*/
|
|
41
|
+
export function extractFilePath(result) {
|
|
42
|
+
const text = result.content[0].text;
|
|
43
|
+
const match = text.match(/📄 File: (.+)/);
|
|
44
|
+
if (!match) {
|
|
45
|
+
throw new Error('No file path found in response');
|
|
46
|
+
}
|
|
47
|
+
return match[1].split('\n')[0].trim();
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Extract reported file size from file reference
|
|
51
|
+
*/
|
|
52
|
+
export function extractReportedSize(result) {
|
|
53
|
+
const text = result.content[0].text;
|
|
54
|
+
const match = text.match(/Size: (\d+) characters/);
|
|
55
|
+
if (!match) {
|
|
56
|
+
throw new Error('No size information found in response');
|
|
57
|
+
}
|
|
58
|
+
return parseInt(match[1], 10);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Check if response is a file reference
|
|
62
|
+
*/
|
|
63
|
+
export function isFileReference(result) {
|
|
64
|
+
return result.content &&
|
|
65
|
+
result.content[0] &&
|
|
66
|
+
result.content[0].text &&
|
|
67
|
+
result.content[0].text.includes('📄 File:');
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Check if response is truncated
|
|
71
|
+
*/
|
|
72
|
+
export function isTruncated(result) {
|
|
73
|
+
return result.content &&
|
|
74
|
+
result.content[0] &&
|
|
75
|
+
result.content[0].text &&
|
|
76
|
+
result.content[0].text.includes('=== RESPONSE TRUNCATED ===');
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Create a mock file system for testing
|
|
80
|
+
*/
|
|
81
|
+
export class MockFileSystem {
|
|
82
|
+
files = new Map();
|
|
83
|
+
async writeFile(path, content) {
|
|
84
|
+
this.files.set(path, content);
|
|
85
|
+
}
|
|
86
|
+
async readFile(path) {
|
|
87
|
+
const content = this.files.get(path);
|
|
88
|
+
if (content === undefined) {
|
|
89
|
+
throw new Error(`File not found: ${path}`);
|
|
90
|
+
}
|
|
91
|
+
return content;
|
|
92
|
+
}
|
|
93
|
+
existsSync(path) {
|
|
94
|
+
return this.files.has(path);
|
|
95
|
+
}
|
|
96
|
+
clear() {
|
|
97
|
+
this.files.clear();
|
|
98
|
+
}
|
|
99
|
+
getFiles() {
|
|
100
|
+
return Array.from(this.files.keys());
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Test fixtures for common scenarios
|
|
105
|
+
*/
|
|
106
|
+
export const fixtures = {
|
|
107
|
+
smallResponse: createMCPResponse(500),
|
|
108
|
+
mediumResponse: createMCPResponse(5000),
|
|
109
|
+
largeResponse: createMCPResponse(100000),
|
|
110
|
+
atThreshold: createMCPResponse(1000),
|
|
111
|
+
justBelowThreshold: createMCPResponse(999),
|
|
112
|
+
atTruncationLimit: createMCPResponse(40000),
|
|
113
|
+
overTruncationLimit: createMCPResponse(50000),
|
|
114
|
+
errorResponse: createErrorResponse('Test error'),
|
|
115
|
+
jsonResponse: createMCPResponseWithJSON({ data: { nested: 'value' } }),
|
|
116
|
+
withPagination: createMCPResponseWithJSON({
|
|
117
|
+
data: [{ item: 1 }, { item: 2 }],
|
|
118
|
+
pagination: { page: 1, total: 10 },
|
|
119
|
+
has_more: true,
|
|
120
|
+
next_page: '/api/page/2'
|
|
121
|
+
})
|
|
122
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import { generateQueryAssistSchema } from '../../fileWriter/schema.js';
|
|
3
|
+
describe('Query-Assist Schema Generator', () => {
|
|
4
|
+
describe('Basic Structure Detection', () => {
|
|
5
|
+
it('should detect simple object structure', () => {
|
|
6
|
+
const data = {
|
|
7
|
+
id: '123',
|
|
8
|
+
name: 'Test',
|
|
9
|
+
count: 42
|
|
10
|
+
};
|
|
11
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 2, maxPaths: 20 });
|
|
12
|
+
expect(schema).toContain('📊 STRUCTURE GUIDE');
|
|
13
|
+
expect(schema).toContain('.id');
|
|
14
|
+
expect(schema).toContain('.name');
|
|
15
|
+
expect(schema).toContain('.count');
|
|
16
|
+
expect(schema).toContain('string');
|
|
17
|
+
expect(schema).toContain('number');
|
|
18
|
+
});
|
|
19
|
+
it('should detect array structure', () => {
|
|
20
|
+
const data = {
|
|
21
|
+
items: [
|
|
22
|
+
{ id: '1', price: 100 },
|
|
23
|
+
{ id: '2', price: 200 }
|
|
24
|
+
]
|
|
25
|
+
};
|
|
26
|
+
// With maxDepth=3, we can see array item fields
|
|
27
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 3, maxPaths: 20 });
|
|
28
|
+
expect(schema).toContain('.items');
|
|
29
|
+
expect(schema).toContain('array[2]');
|
|
30
|
+
expect(schema).toContain('.items[].id');
|
|
31
|
+
expect(schema).toContain('.items[].price');
|
|
32
|
+
});
|
|
33
|
+
it('should detect nested object structure', () => {
|
|
34
|
+
const data = {
|
|
35
|
+
user: {
|
|
36
|
+
profile: {
|
|
37
|
+
name: 'Alice',
|
|
38
|
+
age: 30
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 2, maxPaths: 20 });
|
|
43
|
+
expect(schema).toContain('.user');
|
|
44
|
+
expect(schema).toContain('.user.profile');
|
|
45
|
+
expect(schema).toContain('object');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('Depth Limiting', () => {
|
|
49
|
+
it('should stop at max depth and show warning', () => {
|
|
50
|
+
const data = {
|
|
51
|
+
level1: {
|
|
52
|
+
level2: {
|
|
53
|
+
level3: {
|
|
54
|
+
level4: 'too deep'
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 2, maxPaths: 20 });
|
|
60
|
+
// Should show paths up to depth 2
|
|
61
|
+
expect(schema).toContain('.level1.level2');
|
|
62
|
+
// Should show depth limit warning in consolidated format
|
|
63
|
+
expect(schema).toContain('⚠️ Limits: DEPTH (max: 2)');
|
|
64
|
+
// level3 can appear in exploration prompts, but not as a path entry
|
|
65
|
+
// Check that level3 is not shown as a separate path line
|
|
66
|
+
const lines = schema.split('\n');
|
|
67
|
+
const pathLines = lines.filter(l => l.includes('→') && l.includes('.level'));
|
|
68
|
+
expect(pathLines.some(l => l.includes('.level1.level2.level3') && l.includes(' → '))).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
it('should show exploration prompts when depth limit hit', () => {
|
|
71
|
+
const data = {
|
|
72
|
+
deep: {
|
|
73
|
+
nested: {
|
|
74
|
+
value: 'hidden'
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 1, maxPaths: 20 });
|
|
79
|
+
expect(schema).toContain('💡 EXPLORATION GUIDE');
|
|
80
|
+
expect(schema).toContain('⚠️ Limits: DEPTH (max: 1)');
|
|
81
|
+
expect(schema).toContain('View keys:');
|
|
82
|
+
expect(schema).toContain('Check type:');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe('Key Limiting', () => {
|
|
86
|
+
it('should limit keys per object and show warning', () => {
|
|
87
|
+
const data = {};
|
|
88
|
+
for (let i = 0; i < 100; i++) {
|
|
89
|
+
data[`field_${i}`] = i;
|
|
90
|
+
}
|
|
91
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 2, maxPaths: 100, maxKeys: 20 });
|
|
92
|
+
expect(schema).toContain('💡 EXPLORATION GUIDE');
|
|
93
|
+
expect(schema).toContain('⚠️ Limits: KEYS (20 shown, 80 more)');
|
|
94
|
+
});
|
|
95
|
+
it('should show key exploration prompts when limit hit', () => {
|
|
96
|
+
const data = {};
|
|
97
|
+
for (let i = 0; i < 60; i++) {
|
|
98
|
+
data[`key${i}`] = `value${i}`;
|
|
99
|
+
}
|
|
100
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 2, maxPaths: 100, maxKeys: 30 });
|
|
101
|
+
expect(schema).toContain('💡 EXPLORATION GUIDE');
|
|
102
|
+
expect(schema).toContain('⚠️ Limits: KEYS (30 shown, 30 more)');
|
|
103
|
+
expect(schema).toContain('View keys:');
|
|
104
|
+
expect(schema).toContain('Count items:');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
describe('Path Limiting', () => {
|
|
108
|
+
it('should limit total paths shown and prioritize important ones', () => {
|
|
109
|
+
// Create data with many fields to exceed path limit
|
|
110
|
+
const data = {
|
|
111
|
+
id: '123',
|
|
112
|
+
name: 'Test'
|
|
113
|
+
};
|
|
114
|
+
// Add many top-level fields
|
|
115
|
+
for (let i = 0; i < 20; i++) {
|
|
116
|
+
data[`field${i}`] = { nested: `value${i}` };
|
|
117
|
+
}
|
|
118
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 2, maxPaths: 10, maxKeys: 50 });
|
|
119
|
+
// Should show path limit warning in consolidated format
|
|
120
|
+
expect(schema).toContain('💡 EXPLORATION GUIDE');
|
|
121
|
+
expect(schema).toContain('PATHS (10 of');
|
|
122
|
+
// Should show some paths (prioritizes objects over primitives)
|
|
123
|
+
expect(schema).toContain('.field');
|
|
124
|
+
// Count number of path lines shown (should be exactly 10)
|
|
125
|
+
const pathLines = schema.split('\n').filter(l => l.includes(' → '));
|
|
126
|
+
expect(pathLines.length).toBeLessThanOrEqual(11); // 10 paths + root = 11
|
|
127
|
+
});
|
|
128
|
+
it('should show path exploration prompts when limit hit', () => {
|
|
129
|
+
const largeData = {};
|
|
130
|
+
for (let i = 0; i < 30; i++) {
|
|
131
|
+
largeData[`field${i}`] = { nested: 'value' };
|
|
132
|
+
}
|
|
133
|
+
const schema = generateQueryAssistSchema(largeData, { maxDepth: 2, maxPaths: 10, maxKeys: 50 });
|
|
134
|
+
expect(schema).toContain('💡 EXPLORATION GUIDE');
|
|
135
|
+
expect(schema).toContain('PATHS (10 of');
|
|
136
|
+
expect(schema).toContain('View keys:');
|
|
137
|
+
expect(schema).toContain('List all paths:');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
describe('Mixed Schema Detection', () => {
|
|
141
|
+
it('should detect heterogeneous arrays', () => {
|
|
142
|
+
const data = {
|
|
143
|
+
items: [
|
|
144
|
+
{ type: 'book', pages: 200 },
|
|
145
|
+
{ type: 'video', duration: 120 },
|
|
146
|
+
{ type: 'audio', length: 180 }
|
|
147
|
+
]
|
|
148
|
+
};
|
|
149
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 2, maxPaths: 20 });
|
|
150
|
+
expect(schema).toContain('💡 EXPLORATION GUIDE');
|
|
151
|
+
expect(schema).toContain('MIXED SCHEMAS');
|
|
152
|
+
});
|
|
153
|
+
it('should show mixed schema exploration prompts', () => {
|
|
154
|
+
const data = {
|
|
155
|
+
data: [
|
|
156
|
+
{ a: 1 },
|
|
157
|
+
{ b: 2 },
|
|
158
|
+
{ c: 3 }
|
|
159
|
+
]
|
|
160
|
+
};
|
|
161
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 2, maxPaths: 20 });
|
|
162
|
+
expect(schema).toContain('💡 EXPLORATION GUIDE');
|
|
163
|
+
expect(schema).toContain('MIXED SCHEMAS');
|
|
164
|
+
expect(schema).toContain('Check variance:');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
describe('Numeric Keys Detection', () => {
|
|
168
|
+
it('should detect numeric string keys and show representative structure', () => {
|
|
169
|
+
const data = {
|
|
170
|
+
'0': { name: 'Alice', age: 30 },
|
|
171
|
+
'1': { name: 'Bob', age: 25 },
|
|
172
|
+
'2': { name: 'Charlie', age: 35 }
|
|
173
|
+
};
|
|
174
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 3, maxPaths: 20 });
|
|
175
|
+
// Should detect numeric keys
|
|
176
|
+
expect(schema).toContain('object (numeric keys)');
|
|
177
|
+
expect(schema).toContain('(3 keys)');
|
|
178
|
+
// Should show representative structure with .[<idx>] notation
|
|
179
|
+
expect(schema).toContain('.[<idx>]');
|
|
180
|
+
// Should show nested structure of representative item
|
|
181
|
+
expect(schema).toContain('.[<idx>].name');
|
|
182
|
+
expect(schema).toContain('.[<idx>].age');
|
|
183
|
+
// Should show explanation note in exploration guide
|
|
184
|
+
expect(schema).toContain('💡 EXPLORATION GUIDE');
|
|
185
|
+
expect(schema).toContain('NUMERIC KEYS:');
|
|
186
|
+
expect(schema).toContain('.["0"], .["1"]');
|
|
187
|
+
expect(schema).toContain('.[0], .[1]');
|
|
188
|
+
// Should NOT enumerate individual keys
|
|
189
|
+
expect(schema).not.toContain('.0 ');
|
|
190
|
+
expect(schema).not.toContain('.1 ');
|
|
191
|
+
expect(schema).not.toContain('.2 ');
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
describe('Nullable Fields', () => {
|
|
195
|
+
it('should detect null values', () => {
|
|
196
|
+
const data = {
|
|
197
|
+
present: 'value',
|
|
198
|
+
missing: null,
|
|
199
|
+
empty: ''
|
|
200
|
+
};
|
|
201
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 2, maxPaths: 20 });
|
|
202
|
+
expect(schema).toContain('.missing');
|
|
203
|
+
expect(schema).toContain('null');
|
|
204
|
+
expect(schema).toContain('(nullable)');
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
describe('Schema Compactness', () => {
|
|
208
|
+
it('should not include common JQ patterns (moved to tool description)', () => {
|
|
209
|
+
const data = { simple: 'data' };
|
|
210
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 2, maxPaths: 20 });
|
|
211
|
+
// Common JQ patterns are now in the JQ tool description, not in every file reference
|
|
212
|
+
expect(schema).not.toContain('COMMON JQ PATTERNS:');
|
|
213
|
+
expect(schema).not.toContain('List all keys:');
|
|
214
|
+
// Schema should still contain the structure guide header
|
|
215
|
+
expect(schema).toContain('📊 STRUCTURE GUIDE');
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
describe('Size Constraints', () => {
|
|
219
|
+
it('should generate compact output for large structures', () => {
|
|
220
|
+
// Create a large nested structure
|
|
221
|
+
const data = {};
|
|
222
|
+
for (let i = 0; i < 100; i++) {
|
|
223
|
+
data[`key${i}`] = {
|
|
224
|
+
nested: {
|
|
225
|
+
deep: {
|
|
226
|
+
value: i
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const schema = generateQueryAssistSchema(data, {
|
|
232
|
+
maxDepth: 2,
|
|
233
|
+
maxPaths: 20,
|
|
234
|
+
maxKeys: 50
|
|
235
|
+
});
|
|
236
|
+
// Schema should be compact (under 5KB as designed)
|
|
237
|
+
expect(schema.length).toBeLessThan(5000);
|
|
238
|
+
// Should contain warnings about limits
|
|
239
|
+
expect(schema).toContain('⚠️');
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
describe('Empty Data Handling', () => {
|
|
243
|
+
it('should handle empty object', () => {
|
|
244
|
+
const data = {};
|
|
245
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 2, maxPaths: 20 });
|
|
246
|
+
expect(schema).toContain('📊 STRUCTURE GUIDE');
|
|
247
|
+
expect(schema).toContain('(root)');
|
|
248
|
+
expect(schema).toContain('object');
|
|
249
|
+
expect(schema).toContain('(0 keys)');
|
|
250
|
+
});
|
|
251
|
+
it('should handle empty array', () => {
|
|
252
|
+
const data = { items: [] };
|
|
253
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 2, maxPaths: 20 });
|
|
254
|
+
expect(schema).toContain('.items');
|
|
255
|
+
expect(schema).toContain('array[0]');
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
describe('Data Size Reporting', () => {
|
|
259
|
+
it('should report data size in characters', () => {
|
|
260
|
+
const data = { test: 'value' };
|
|
261
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 2, maxPaths: 20 });
|
|
262
|
+
expect(schema).toContain('Size:');
|
|
263
|
+
expect(schema).toContain('characters');
|
|
264
|
+
expect(schema).toMatch(/Size: \d+(,\d{3})* characters/);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
});
|