Skip to content

Commit 01ac29f

Browse files
committed
Fix unit tests to handle JSON parsing errors (#8)
1 parent 783e697 commit 01ac29f

8 files changed

Lines changed: 464 additions & 72 deletions

File tree

.github/workflows/test.yml

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,94 @@ jobs:
2121
- name: Run security tests
2222
run: node tests/run-tests.js security
2323

24+
json_parsing_test:
25+
runs-on: ubuntu-latest
26+
steps:
27+
- uses: actions/checkout@v3
28+
- name: Set up Node.js
29+
uses: actions/setup-node@v3
30+
with:
31+
node-version: '16'
32+
cache: 'npm'
33+
- name: Install dependencies
34+
run: npm ci
35+
- name: Test JSON parsing error handling
36+
run: |
37+
# Create test script
38+
echo "
39+
/**
40+
* Simple JSON Parsing Test
41+
*/
42+
43+
const { safeJsonParse } = require('./lib/utils/security-middleware');
44+
const { formatJsonResult } = require('./lib/utils/structured-output');
45+
46+
let allTestsPassed = true;
47+
let testsRun = 0;
48+
49+
function runTest(name, testFn) {
50+
testsRun++;
51+
console.log(\`\n=== Testing \${name} ===\`);
52+
try {
53+
const passed = testFn();
54+
console.log(\`Test passed: \${passed}\`);
55+
if (!passed) allTestsPassed = false;
56+
return passed;
57+
} catch (error) {
58+
console.error(\`Test failed: \${error.message}\`);
59+
allTestsPassed = false;
60+
return false;
61+
}
62+
}
63+
64+
// Test valid JSON
65+
runTest('Valid JSON', () => {
66+
const validJson = '{\"name\":\"test\",\"value\":42}';
67+
const validResult = safeJsonParse(validJson);
68+
return validResult.name === 'test' && validResult.value === 42;
69+
});
70+
71+
// Test empty string
72+
runTest('Empty String', () => {
73+
const emptyResult = safeJsonParse('', { default: true });
74+
return emptyResult.default === true;
75+
});
76+
77+
// Test malformed JSON
78+
runTest('Malformed JSON', () => {
79+
const malformedJson = '{name: \"test\"}';
80+
const malformedResult = safeJsonParse(malformedJson, { default: true });
81+
return malformedResult.default === true;
82+
});
83+
84+
// Test non-string input
85+
runTest('Non-String Input', () => {
86+
const nullResult = safeJsonParse(null, { default: true });
87+
return nullResult.default === true;
88+
});
89+
90+
// Test circular references
91+
runTest('Circular References', () => {
92+
const obj = { name: 'circular' };
93+
obj.self = obj; // Create circular reference
94+
const circularResult = formatJsonResult(obj);
95+
return circularResult.success === true &&
96+
circularResult.data.name === 'circular' &&
97+
typeof circularResult.data.self === 'string';
98+
});
99+
100+
console.log(\`\n=== Testing Complete ===\`);
101+
console.log(\`\${testsRun} tests run, all tests passed: \${allTestsPassed}\`);
102+
103+
process.exit(allTestsPassed ? 0 : 1);
104+
" > json-parsing-test.js
105+
106+
# Run the test
107+
node json-parsing-test.js
108+
24109
unit_test:
25110
runs-on: ubuntu-latest
26-
needs: security_test
111+
needs: [security_test, json_parsing_test]
27112
steps:
28113
- uses: actions/checkout@v3
29114
- name: Set up Node.js

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
.tasktracker/stats/
1111
.tasktracker/cron.log
1212
.tasktracker/code_health.json
13+
# Exclude backup files
14+
.tasktracker/*.backup.*
15+
.tasktracker/*.corrupted.*
1316
# But keep the .tasktracker directory itself with a .gitkeep
1417
!.tasktracker/.gitkeep
1518
!.tasktracker/reports/.gitkeep
@@ -35,6 +38,12 @@ coverage/
3538
test-results/
3639
tests/temp/*
3740
!tests/temp/.gitkeep
41+
tests/test-json-parsing/
42+
test-json-parsing/
43+
simple-json-test.js
44+
*-json-test.js
45+
tests/*-json-test.js
46+
json-parsing-test.js
3847

3948
# IDE and editor files
4049
.idea/

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,4 +196,12 @@
196196
- Initial release of TaskTracker
197197
- Basic task tracking functionality
198198
- File change detection
199-
- Simple reporting features
199+
- Simple reporting features
200+
201+
## [Unreleased]
202+
### Added
203+
- Improved JSON parsing error handling to prevent test failures
204+
- Enhanced error messages for JSON parsing failures with more context
205+
- Added support for handling circular references in JSON serialization
206+
- Created dedicated test suite for JSON parsing functionality
207+
- Fixed unit tests to handle JSON parsing errors gracefully

lib/core/formatting.js

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -98,37 +98,49 @@ function output(message, type = 'info', options = {}) {
9898
if (globalOptions.json) {
9999
let jsonResult;
100100

101-
switch (type) {
102-
case 'error':
103-
jsonResult = structuredOutput.formatError(message, {
104-
errorCode: options.errorCode || 'ERROR',
105-
metadata: options.metadata || {}
106-
});
107-
break;
108-
109-
case 'data':
110-
// If it's already a structured result, use it directly
111-
if (message && typeof message === 'object' && 'success' in message) {
112-
jsonResult = message;
113-
} else {
114-
jsonResult = structuredOutput.formatJsonResult(message, {
101+
try {
102+
switch (type) {
103+
case 'error':
104+
jsonResult = structuredOutput.formatError(message, {
105+
errorCode: options.errorCode || 'ERROR',
115106
metadata: options.metadata || {}
116107
});
117-
}
118-
break;
108+
break;
119109

120-
default:
121-
// For info, success, warning - create appropriate structured format
122-
jsonResult = structuredOutput.formatJsonResult(message, {
123-
metadata: {
124-
messageType: type,
125-
...options.metadata
110+
case 'data':
111+
// If it's already a structured result, use it directly
112+
if (message && typeof message === 'object' && 'success' in message) {
113+
jsonResult = message;
114+
} else {
115+
jsonResult = structuredOutput.formatJsonResult(message, {
116+
metadata: options.metadata || {}
117+
});
126118
}
127-
});
119+
break;
120+
121+
default:
122+
// For info, success, warning - create appropriate structured format
123+
jsonResult = structuredOutput.formatJsonResult(message, {
124+
metadata: {
125+
messageType: type,
126+
...options.metadata
127+
}
128+
});
129+
}
130+
131+
// Ensure the result is valid JSON by checking if it can be stringified
132+
const jsonString = JSON.stringify(jsonResult, null, 2);
133+
134+
// Output valid JSON
135+
console.log(jsonString);
136+
} catch (err) {
137+
// If JSON conversion fails for any reason, output an error response
138+
console.error(JSON.stringify({
139+
success: false,
140+
error: `Failed to generate JSON output: ${err.message}`,
141+
data: null
142+
}));
128143
}
129-
130-
// Output JSON
131-
console.log(JSON.stringify(jsonResult, null, 2));
132144
return;
133145
}
134146

lib/utils/security-middleware.js

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,9 +203,36 @@ function safeFileOperation(operation, filePath, data = null) {
203203
*/
204204
function safeJsonParse(jsonString, defaultValue = {}) {
205205
try {
206-
return JSON.parse(jsonString);
206+
if (typeof jsonString !== 'string') {
207+
throw new Error('Input is not a string');
208+
}
209+
210+
// Trim the input to remove any leading/trailing whitespace
211+
const trimmedJson = jsonString.trim();
212+
213+
if (trimmedJson === '') {
214+
throw new Error('Empty JSON string');
215+
}
216+
217+
return JSON.parse(trimmedJson);
207218
} catch (error) {
208-
output(`❌ Error parsing JSON: ${error.message}`, 'error');
219+
// Provide more specific error details for debugging
220+
let errorMsg = `Error parsing JSON: ${error.message}`;
221+
222+
// Add position information if available (for SyntaxError)
223+
if (error instanceof SyntaxError && 'position' in error) {
224+
errorMsg += ` at position ${error.position}`;
225+
}
226+
227+
// Adding a snippet of the problematic JSON for context
228+
if (typeof jsonString === 'string' && jsonString.length > 0) {
229+
const snippet = jsonString.length > 50
230+
? jsonString.substring(0, 47) + '...'
231+
: jsonString;
232+
errorMsg += ` | Input snippet: "${snippet}"`;
233+
}
234+
235+
output(`❌ ${errorMsg}`, 'error');
209236
return defaultValue;
210237
}
211238
}

lib/utils/structured-output.js

Lines changed: 69 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,87 @@
55
* especially useful for AI agent integration and non-interactive usage.
66
*/
77

8+
/**
9+
* Safely serialize data, handling non-JSON serializable values
10+
* @param {any} data Data to serialize
11+
* @returns {any} Safely serializable value
12+
*/
13+
function safeSerialize(data) {
14+
const seen = new WeakSet();
15+
16+
return JSON.parse(JSON.stringify(data, (key, value) => {
17+
// Handle undefined
18+
if (value === undefined) return null;
19+
20+
// Handle functions
21+
if (typeof value === 'function') return '[Function]';
22+
23+
// Handle error objects
24+
if (value instanceof Error) {
25+
return {
26+
name: value.name,
27+
message: value.message,
28+
stack: value.stack
29+
};
30+
}
31+
32+
// Handle circular references
33+
if (typeof value === 'object' && value !== null) {
34+
if (seen.has(value)) {
35+
return '[Circular]';
36+
}
37+
seen.add(value);
38+
}
39+
40+
return value;
41+
}));
42+
}
43+
844
/**
945
* Format a result into a standardized JSON structure
1046
* @param {any} data Result data to format
1147
* @param {object} options Formatting options
1248
* @returns {object} Standardized JSON object
1349
*/
1450
function formatJsonResult(data, options = {}) {
15-
// Check if data already has a standardized format
16-
if (data && typeof data === 'object' && 'success' in data) {
17-
// Just ensure consistent structure
51+
try {
52+
// Check if data already has a standardized format
53+
if (data && typeof data === 'object' && 'success' in data) {
54+
// Just ensure consistent structure
55+
return {
56+
success: !!data.success,
57+
data: safeSerialize(data.data || data.result || null),
58+
error: data.error || null,
59+
metadata: {
60+
...safeSerialize(data.metadata || {}),
61+
timestamp: new Date().toISOString()
62+
}
63+
};
64+
}
65+
66+
// Format data into standardized structure
67+
return {
68+
success: !options.error,
69+
data: safeSerialize(data),
70+
error: options.error || null,
71+
metadata: {
72+
timestamp: new Date().toISOString(),
73+
...safeSerialize(options.metadata || {})
74+
}
75+
};
76+
} catch (error) {
77+
// Provide a fallback if serialization fails
1878
return {
19-
success: !!data.success,
20-
data: data.data || data.result || null,
21-
error: data.error || null,
79+
success: false,
80+
data: null,
81+
error: `Failed to serialize data: ${error.message}`,
2282
metadata: {
23-
...data.metadata,
24-
timestamp: new Date().toISOString()
83+
timestamp: new Date().toISOString(),
84+
originalDataType: data === null ? 'null' : typeof data,
85+
isArray: Array.isArray(data)
2586
}
2687
};
2788
}
28-
29-
// Format data into standardized structure
30-
return {
31-
success: !options.error,
32-
data: data,
33-
error: options.error || null,
34-
metadata: {
35-
timestamp: new Date().toISOString(),
36-
...options.metadata
37-
}
38-
};
3989
}
4090

4191
/**

0 commit comments

Comments
 (0)