@tng-sh/js 0.1.0 → 0.1.1
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/bin/tng.js +35 -1
- package/binaries/go-ui-darwin-amd64 +0 -0
- package/binaries/go-ui-darwin-arm64 +0 -0
- package/binaries/go-ui-linux-amd64 +0 -0
- package/binaries/go-ui-linux-arm64 +0 -0
- package/index.d.ts +10 -0
- package/index.js +3 -1
- package/lib/auditWorker.js +28 -0
- package/lib/config.js +1 -0
- package/lib/fixApplier.js +59 -0
- package/lib/generateTestsUi.js +170 -15
- package/lib/goUiSession.js +7 -3
- package/package.json +1 -1
- package/tng_sh_js.darwin-arm64.node +0 -0
- package/tng_sh_js.darwin-x64.node +0 -0
- package/tng_sh_js.linux-arm64-gnu.node +0 -0
- package/tng_sh_js.linux-x64-gnu.node +0 -0
package/bin/tng.js
CHANGED
|
@@ -7,6 +7,7 @@ const path = require('path');
|
|
|
7
7
|
const { loadConfig } = require('../lib/config');
|
|
8
8
|
const { saveTestFile } = require('../lib/saveFile');
|
|
9
9
|
const { ping, getUserStats } = require('../index');
|
|
10
|
+
const { applyFix } = require('../lib/fixApplier');
|
|
10
11
|
|
|
11
12
|
// Handle EPIPE errors gracefully when child process (Go UI) exits
|
|
12
13
|
process.stdout.on('error', (err) => {
|
|
@@ -27,7 +28,7 @@ process.on('uncaughtException', (err) => {
|
|
|
27
28
|
program
|
|
28
29
|
.name('tng')
|
|
29
30
|
.description('TNG - Automated Test Generation, and audit generation for JavaScript')
|
|
30
|
-
.version('0.1.
|
|
31
|
+
.version('0.1.1');
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* @command init
|
|
@@ -166,6 +167,39 @@ program
|
|
|
166
167
|
}
|
|
167
168
|
});
|
|
168
169
|
|
|
170
|
+
/**
|
|
171
|
+
* @command fix
|
|
172
|
+
* Apply a specific fix to a file
|
|
173
|
+
*/
|
|
174
|
+
program
|
|
175
|
+
.command('fix')
|
|
176
|
+
.description('Apply a fix to a file')
|
|
177
|
+
.option('-f, --file <path>', 'File path to fix')
|
|
178
|
+
.option('-d, --data <json>', 'JSON data for the audit item containing fix info')
|
|
179
|
+
.action(async (options) => {
|
|
180
|
+
if (!options.file || !options.data) {
|
|
181
|
+
console.log(chalk.red('Error: Both --file and --data are required.'));
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const item = JSON.parse(options.data);
|
|
187
|
+
const result = await applyFix(options.file, item);
|
|
188
|
+
if (result.success) {
|
|
189
|
+
console.log(chalk.green(`✓ Fix applied to ${options.file}`));
|
|
190
|
+
if (result.backup_path) {
|
|
191
|
+
console.log(chalk.dim(` Backup created at ${result.backup_path}`));
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
console.log(chalk.red(`❌ Failed to apply fix: ${result.error}`));
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
} catch (e) {
|
|
198
|
+
console.log(chalk.red(`Error: ${e.message}`));
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
169
203
|
|
|
170
204
|
|
|
171
205
|
/**
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/index.d.ts
CHANGED
|
@@ -29,6 +29,16 @@ export declare function getUserStats(baseUrl: string, apiKey: string): string
|
|
|
29
29
|
* Analyzes the source, builds context, and streams results via the provided callback.
|
|
30
30
|
*/
|
|
31
31
|
export declare function runAudit(filePath: string, methodName: string, className: string | undefined | null, testType: string | undefined | null, configJson: string, callback: (...args: any[]) => any): string
|
|
32
|
+
/**
|
|
33
|
+
* Applies a single edit operation to a file.
|
|
34
|
+
* Uses backup and atomic write for safety.
|
|
35
|
+
*/
|
|
36
|
+
export declare function applyEdit(filePath: string, search: string, replace: string, lineHint?: number | undefined | null): string
|
|
37
|
+
/**
|
|
38
|
+
* Applies multiple edit operations atomically.
|
|
39
|
+
* All succeed or all fail (with rollback).
|
|
40
|
+
*/
|
|
41
|
+
export declare function applyEditsAtomic(operationsJson: string): string
|
|
32
42
|
/**
|
|
33
43
|
* Orchestrates the test generation process.
|
|
34
44
|
* Analyzes code, submits a job, and polls for the final generated test.
|
package/index.js
CHANGED
|
@@ -310,7 +310,7 @@ if (!nativeBinding) {
|
|
|
310
310
|
throw new Error(`Failed to load native binding`)
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
const { getFileOutline, getProjectMetadata, findCallSites, ping, submitJob, getUserStats, runAudit, generateTest } = nativeBinding
|
|
313
|
+
const { getFileOutline, getProjectMetadata, findCallSites, ping, submitJob, getUserStats, runAudit, applyEdit, applyEditsAtomic, generateTest } = nativeBinding
|
|
314
314
|
|
|
315
315
|
module.exports.getFileOutline = getFileOutline
|
|
316
316
|
module.exports.getProjectMetadata = getProjectMetadata
|
|
@@ -319,4 +319,6 @@ module.exports.ping = ping
|
|
|
319
319
|
module.exports.submitJob = submitJob
|
|
320
320
|
module.exports.getUserStats = getUserStats
|
|
321
321
|
module.exports.runAudit = runAudit
|
|
322
|
+
module.exports.applyEdit = applyEdit
|
|
323
|
+
module.exports.applyEditsAtomic = applyEditsAtomic
|
|
322
324
|
module.exports.generateTest = generateTest
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const { parentPort, workerData } = require('worker_threads');
|
|
2
|
+
const { runAudit } = require('../index');
|
|
3
|
+
const { loadConfig } = require('./config');
|
|
4
|
+
|
|
5
|
+
const { filePath, methodName, className, testType } = workerData;
|
|
6
|
+
const config = loadConfig();
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const resultJson = runAudit(
|
|
10
|
+
filePath,
|
|
11
|
+
methodName,
|
|
12
|
+
className,
|
|
13
|
+
testType,
|
|
14
|
+
JSON.stringify(config),
|
|
15
|
+
(msg) => {
|
|
16
|
+
if (parentPort) {
|
|
17
|
+
parentPort.postMessage({ type: 'item', data: msg });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
if (parentPort) {
|
|
22
|
+
parentPort.postMessage({ type: 'result', data: resultJson });
|
|
23
|
+
}
|
|
24
|
+
} catch (e) {
|
|
25
|
+
if (parentPort) {
|
|
26
|
+
parentPort.postMessage({ type: 'error', data: e.message });
|
|
27
|
+
}
|
|
28
|
+
}
|
package/lib/config.js
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const { applyEdit } = require('../index');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parses a SEARCH_AND_REPLACE block from a string
|
|
5
|
+
* @param {string} codeChange
|
|
6
|
+
* @returns {{search: string, replace: string} | null}
|
|
7
|
+
*/
|
|
8
|
+
function parseSearchReplace(codeChange) {
|
|
9
|
+
if (!codeChange) return null;
|
|
10
|
+
|
|
11
|
+
// Try to find SEARCH: and REPLACE: blocks
|
|
12
|
+
const searchIdx = codeChange.indexOf('SEARCH:');
|
|
13
|
+
const replaceIdx = codeChange.indexOf('REPLACE:');
|
|
14
|
+
|
|
15
|
+
if (searchIdx !== -1 && replaceIdx !== -1 && searchIdx < replaceIdx) {
|
|
16
|
+
const search = codeChange.substring(searchIdx + 7, replaceIdx).trim();
|
|
17
|
+
const replace = codeChange.substring(replaceIdx + 8).trim();
|
|
18
|
+
return { search, replace };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Appies a fix to a file
|
|
26
|
+
* @param {string} filePath
|
|
27
|
+
* @param {object} item
|
|
28
|
+
* @returns {object} result
|
|
29
|
+
*/
|
|
30
|
+
async function applyFix(filePath, item) {
|
|
31
|
+
try {
|
|
32
|
+
if (!item) {
|
|
33
|
+
return { success: false, error: 'Item is null or undefined' };
|
|
34
|
+
}
|
|
35
|
+
const codeChange = item.fix_code_change || (item.fix && item.fix.how && item.fix.how.code_change);
|
|
36
|
+
if (!codeChange) {
|
|
37
|
+
return { success: false, error: 'No code change found in fix data' };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const parsed = parseSearchReplace(codeChange);
|
|
41
|
+
if (!parsed) {
|
|
42
|
+
return { success: false, error: 'Failed to parse SEARCH_AND_REPLACE block' };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const resultJson = applyEdit(
|
|
46
|
+
filePath,
|
|
47
|
+
parsed.search,
|
|
48
|
+
parsed.replace,
|
|
49
|
+
typeof item.line_number === 'number' && item.line_number > 0 ? item.line_number : null
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const result = JSON.parse(resultJson);
|
|
53
|
+
return result;
|
|
54
|
+
} catch (e) {
|
|
55
|
+
return { success: false, error: e.message };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { applyFix, parseSearchReplace };
|
package/lib/generateTestsUi.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
1
2
|
const path = require('path');
|
|
2
3
|
const glob = require('fast-glob');
|
|
3
4
|
const chalk = require('chalk');
|
|
4
5
|
const GoUISession = require('./goUiSession');
|
|
5
|
-
const { getFileOutline,
|
|
6
|
+
const { getFileOutline, generateTest, ping, getUserStats } = require('../index');
|
|
6
7
|
const { loadConfig } = require('./config');
|
|
7
8
|
const { saveTestFile } = require('./saveFile');
|
|
9
|
+
const { applyFix } = require('./fixApplier');
|
|
8
10
|
|
|
9
11
|
class GenerateTestsUI {
|
|
10
12
|
constructor(cliMode = false) {
|
|
@@ -193,7 +195,7 @@ class GenerateTestsUI {
|
|
|
193
195
|
}
|
|
194
196
|
|
|
195
197
|
async _handleAuditFlow(filePath, method, testType) {
|
|
196
|
-
const
|
|
198
|
+
const { Worker } = require('worker_threads');
|
|
197
199
|
const streamingUi = await this.goUiSession.showStreamingAuditResults(
|
|
198
200
|
method.name,
|
|
199
201
|
method.class_name,
|
|
@@ -202,25 +204,158 @@ class GenerateTestsUI {
|
|
|
202
204
|
|
|
203
205
|
if (!streamingUi) return null;
|
|
204
206
|
|
|
207
|
+
let results = {
|
|
208
|
+
issues: [],
|
|
209
|
+
behaviours: [],
|
|
210
|
+
method_name: method.name,
|
|
211
|
+
class_name: method.class_name,
|
|
212
|
+
method_source_with_lines: method.source_code
|
|
213
|
+
};
|
|
214
|
+
let auditFinished = false;
|
|
215
|
+
|
|
216
|
+
// Start audit in background worker
|
|
217
|
+
const worker = new Worker(path.join(__dirname, 'auditWorker.js'), {
|
|
218
|
+
workerData: { filePath, methodName: method.name, className: method.class_name || null, testType: testType || null }
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const itemIds = new Set();
|
|
222
|
+
worker.on('message', (msg) => {
|
|
223
|
+
if (msg.type === 'item') {
|
|
224
|
+
if (msg.data.startsWith('{')) {
|
|
225
|
+
streamingUi.write(msg.data);
|
|
226
|
+
try {
|
|
227
|
+
const data = JSON.parse(msg.data);
|
|
228
|
+
|
|
229
|
+
// Handle metadata updates from stream
|
|
230
|
+
if (data.method_name) results.method_name = data.method_name;
|
|
231
|
+
if (data.class_name) results.class_name = data.class_name;
|
|
232
|
+
if (data.method_source_with_lines) results.method_source_with_lines = data.method_source_with_lines;
|
|
233
|
+
if (data.source_code) results.method_source_with_lines = data.source_code;
|
|
234
|
+
|
|
235
|
+
// Only push if it looks like an actual audit item
|
|
236
|
+
if (data.test_name || data.summary || data.category) {
|
|
237
|
+
const id = `${data.category}-${data.test_name}-${data.line_number}`;
|
|
238
|
+
if (!itemIds.has(id)) {
|
|
239
|
+
if (data.category === 'behavior' || data.category === 'behaviour') results.behaviours.push(data);
|
|
240
|
+
else results.issues.push(data);
|
|
241
|
+
itemIds.add(id);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} catch (e) { }
|
|
245
|
+
}
|
|
246
|
+
} else if (msg.type === 'result') {
|
|
247
|
+
try {
|
|
248
|
+
const finalResults = typeof msg.data === 'string' ? JSON.parse(msg.data) : msg.data;
|
|
249
|
+
|
|
250
|
+
// Merge final results with our local ones, preserving 'fixed' state
|
|
251
|
+
const mergeList = (localList, incomingList) => {
|
|
252
|
+
if (!incomingList || incomingList.length === 0) return localList;
|
|
253
|
+
|
|
254
|
+
return incomingList.map(incomingItem => {
|
|
255
|
+
const localItem = localList.find(it =>
|
|
256
|
+
it.test_name === incomingItem.test_name &&
|
|
257
|
+
it.line_number === incomingItem.line_number
|
|
258
|
+
);
|
|
259
|
+
if (localItem && localItem.fixed) {
|
|
260
|
+
return { ...incomingItem, fixed: true };
|
|
261
|
+
}
|
|
262
|
+
return incomingItem;
|
|
263
|
+
});
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
results.issues = mergeList(results.issues, finalResults.issues || finalResults.findings);
|
|
267
|
+
results.behaviours = mergeList(results.behaviours, finalResults.behaviours);
|
|
268
|
+
|
|
269
|
+
if (finalResults.method_source_with_lines) results.method_source_with_lines = finalResults.method_source_with_lines;
|
|
270
|
+
if (finalResults.method_name) results.method_name = finalResults.method_name;
|
|
271
|
+
} catch (e) { }
|
|
272
|
+
auditFinished = true;
|
|
273
|
+
} else if (msg.type === 'error') {
|
|
274
|
+
console.error(chalk.red(`Audit error: ${msg.data}`));
|
|
275
|
+
auditFinished = true;
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
worker.on('error', (err) => {
|
|
280
|
+
console.error(chalk.red(`Worker error: ${err.message}`));
|
|
281
|
+
auditFinished = true;
|
|
282
|
+
});
|
|
283
|
+
|
|
205
284
|
try {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
(
|
|
213
|
-
|
|
214
|
-
|
|
285
|
+
let currentChoice = null;
|
|
286
|
+
|
|
287
|
+
// Wait for INITIAL streaming session to complete (user picks an action or quits)
|
|
288
|
+
await streamingUi.wait;
|
|
289
|
+
|
|
290
|
+
const getResponse = (outputFile) => {
|
|
291
|
+
if (fs.existsSync(outputFile)) {
|
|
292
|
+
const output = fs.readFileSync(outputFile, 'utf8').trim();
|
|
293
|
+
if (output) {
|
|
294
|
+
try {
|
|
295
|
+
const parsed = JSON.parse(output);
|
|
296
|
+
return typeof parsed === 'object' ? parsed : { action: output };
|
|
297
|
+
} catch (e) {
|
|
298
|
+
return { action: output };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
currentChoice = getResponse(streamingUi.outputFile);
|
|
306
|
+
|
|
307
|
+
// Action loop: handles fix/open and returns to UI
|
|
308
|
+
while (currentChoice && (currentChoice.action === 'fix' || currentChoice.action === 'open')) {
|
|
309
|
+
const itemIndex = typeof currentChoice.index === 'number' ? currentChoice.index : 0;
|
|
310
|
+
|
|
311
|
+
if (currentChoice.action === 'fix') {
|
|
312
|
+
console.log(chalk.cyan(`Applying fix...`));
|
|
313
|
+
const fixResult = await applyFix(filePath, currentChoice.item);
|
|
314
|
+
if (fixResult.success) {
|
|
315
|
+
console.log(chalk.green(`✓ Fix applied successfully.`));
|
|
316
|
+
// Mark as fixed locally
|
|
317
|
+
[results.issues, results.behaviours].forEach(list => {
|
|
318
|
+
list.forEach(it => {
|
|
319
|
+
if (it.test_name === currentChoice.item.test_name && it.line_number === currentChoice.item.line_number) {
|
|
320
|
+
it.fixed = true;
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
} else {
|
|
325
|
+
console.log(chalk.red(`❌ Failed to apply fix: ${fixResult.error || 'Unknown error'}`));
|
|
215
326
|
}
|
|
327
|
+
} else if (currentChoice.action === 'open') {
|
|
328
|
+
this._openInEditor(filePath, currentChoice.item.line_number);
|
|
216
329
|
}
|
|
217
|
-
);
|
|
218
330
|
|
|
219
|
-
|
|
220
|
-
|
|
331
|
+
// Ensure source has line numbers for static UI if it doesn't already
|
|
332
|
+
if (results.method_source_with_lines && !/^\s*\d+:/.test(results.method_source_with_lines)) {
|
|
333
|
+
results.method_source_with_lines = results.method_source_with_lines
|
|
334
|
+
.split('\n')
|
|
335
|
+
.map((line, i) => `${i + 1}: ${line}`)
|
|
336
|
+
.join('\n');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Re-launch UI statically with updated results
|
|
340
|
+
const choiceStr = await this.goUiSession.showAuditResults(results, itemIndex);
|
|
341
|
+
if (choiceStr === 'back' || choiceStr === 'main_menu' || choiceStr === 'exit') {
|
|
342
|
+
currentChoice = null;
|
|
343
|
+
} else {
|
|
344
|
+
try {
|
|
345
|
+
const parsed = JSON.parse(choiceStr);
|
|
346
|
+
currentChoice = typeof parsed === 'object' ? parsed : { action: choiceStr };
|
|
347
|
+
} catch (e) {
|
|
348
|
+
currentChoice = { action: choiceStr };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return { message: 'Audit complete', results };
|
|
221
354
|
} catch (e) {
|
|
222
|
-
console.error(chalk.red(`\nAudit failed: ${e.message}\n`));
|
|
355
|
+
console.error(chalk.red(`\nAudit flow failed: ${e.message}\n`));
|
|
223
356
|
return null;
|
|
357
|
+
} finally {
|
|
358
|
+
await worker.terminate();
|
|
224
359
|
}
|
|
225
360
|
}
|
|
226
361
|
|
|
@@ -394,6 +529,26 @@ class GenerateTestsUI {
|
|
|
394
529
|
}
|
|
395
530
|
}
|
|
396
531
|
|
|
532
|
+
_openInEditor(filePath, lineNumber) {
|
|
533
|
+
const { execSync } = require('child_process');
|
|
534
|
+
try {
|
|
535
|
+
// Try to open in VS Code if available, fallback to default editor
|
|
536
|
+
const location = lineNumber ? `${filePath}:${lineNumber}` : filePath;
|
|
537
|
+
try {
|
|
538
|
+
execSync(`code --goto ${location}`, { stdio: 'ignore' });
|
|
539
|
+
console.log(chalk.blue(`\nOpening ${location} in VS Code...`));
|
|
540
|
+
} catch (e) {
|
|
541
|
+
if (process.platform === 'darwin') {
|
|
542
|
+
execSync(`open ${filePath}`);
|
|
543
|
+
} else if (process.platform === 'linux') {
|
|
544
|
+
execSync(`xdg-open ${filePath}`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
} catch (e) {
|
|
548
|
+
console.log(chalk.yellow(`\n⚠️ Failed to open editor: ${e.message}`));
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
397
552
|
async _getUserFiles() {
|
|
398
553
|
const patterns = ['**/*.{js,ts,jsx,tsx}'];
|
|
399
554
|
const ignore = [
|
package/lib/goUiSession.js
CHANGED
|
@@ -268,8 +268,7 @@ class GoUISession {
|
|
|
268
268
|
console.error('Test results display error:', error.message);
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
|
-
|
|
272
|
-
async showAuditResults(auditResult) {
|
|
271
|
+
async showAuditResults(auditResult, index = 0) {
|
|
273
272
|
const dataJson = JSON.stringify(auditResult);
|
|
274
273
|
const inputFile = this._trackTempFile(this._createTempFile('audit-data', '.json'));
|
|
275
274
|
const outputFile = this._trackTempFile(this._createTempFile('audit-choice', '.txt'));
|
|
@@ -283,7 +282,12 @@ class GoUISession {
|
|
|
283
282
|
process.stdin.pause();
|
|
284
283
|
|
|
285
284
|
await new Promise((resolve, reject) => {
|
|
286
|
-
const
|
|
285
|
+
const args = ['audit-results', '--file', inputFile, '--output', outputFile];
|
|
286
|
+
if (index > 0) {
|
|
287
|
+
args.push('--index', index.toString());
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const child = spawn(this._binaryPath, args, {
|
|
287
291
|
stdio: ['inherit', 'inherit', 'inherit'],
|
|
288
292
|
env: process.env
|
|
289
293
|
});
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|