@tng-sh/js 0.1.1 → 0.1.3
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 +101 -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 +2 -0
- package/index.js +2 -1
- package/lib/generateTestsUi.js +146 -10
- 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
|
@@ -28,7 +28,7 @@ process.on('uncaughtException', (err) => {
|
|
|
28
28
|
program
|
|
29
29
|
.name('tng')
|
|
30
30
|
.description('TNG - Automated Test Generation, and audit generation for JavaScript')
|
|
31
|
-
.version('0.1.
|
|
31
|
+
.version('0.1.3');
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* @command init
|
|
@@ -143,6 +143,65 @@ program
|
|
|
143
143
|
launchInteractive();
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* @command xray
|
|
148
|
+
* Generate X-Ray (Mermaid Visualization)
|
|
149
|
+
*/
|
|
150
|
+
program
|
|
151
|
+
.command('xray')
|
|
152
|
+
.alias('x')
|
|
153
|
+
.description('Generate X-Ray visualization')
|
|
154
|
+
.option('-f, --file <path>', 'Input file path')
|
|
155
|
+
.option('-m, --method <name>', 'Method name to visualize')
|
|
156
|
+
.action(async (options) => {
|
|
157
|
+
if (!options.file || !options.method) {
|
|
158
|
+
console.log(chalk.red('Error: Both --file and --method are required for X-Ray.'));
|
|
159
|
+
console.log(chalk.yellow('Usage: tng xray -f <file> -m <method>'));
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log(chalk.blue(`🔍 Generating X-Ray for ${options.method} in ${options.file}...`));
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const config = loadConfig();
|
|
167
|
+
|
|
168
|
+
// Re-use logic similar to _handleXrayFlow but tailored for CLI non-interactive output
|
|
169
|
+
const { generateTest } = require('../index');
|
|
170
|
+
|
|
171
|
+
// We use a custom callback to show progress
|
|
172
|
+
const callback = (msg, percent) => {
|
|
173
|
+
if (percent % 20 === 0) console.log(chalk.dim(`[${percent}%] ${msg}`));
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// We pass 'visualize' as the test type
|
|
177
|
+
const resultJson = generateTest(
|
|
178
|
+
path.resolve(options.file),
|
|
179
|
+
options.method,
|
|
180
|
+
null,
|
|
181
|
+
'visualize',
|
|
182
|
+
JSON.stringify(config),
|
|
183
|
+
callback
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
let mermaidCode = "";
|
|
187
|
+
try {
|
|
188
|
+
const parsed = JSON.parse(resultJson);
|
|
189
|
+
mermaidCode = parsed.mermaid_code || resultJson;
|
|
190
|
+
} catch (e) {
|
|
191
|
+
mermaidCode = resultJson;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
console.log(chalk.bold('\n--- X-Ray Result (Mermaid.js) ---\n'));
|
|
195
|
+
console.log(chalk.blue(mermaidCode));
|
|
196
|
+
console.log(chalk.bold('\n---------------------------------\n'));
|
|
197
|
+
console.log(chalk.green('✓ Copy the code above and paste into https://mermaid.live'));
|
|
198
|
+
|
|
199
|
+
} catch (e) {
|
|
200
|
+
console.log(chalk.red(`Error: ${e.message}`));
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
146
205
|
/**
|
|
147
206
|
* Main command handler (handles -f and -m)
|
|
148
207
|
*/
|
|
@@ -151,10 +210,51 @@ program
|
|
|
151
210
|
.option('-f, --file <path>', 'JavaScript file path')
|
|
152
211
|
.option('-t, --type <type>', 'Component type (react_component, express_handler, etc) [required]')
|
|
153
212
|
.option('-a, --audit', 'Run audit mode instead of test generation')
|
|
213
|
+
.option('--trace', 'Run symbolic trace visualization')
|
|
154
214
|
.option('--json', 'Output results as JSON events (machine-readable)')
|
|
155
215
|
|
|
156
216
|
.action(async (options) => {
|
|
157
217
|
if (options.method && options.file) {
|
|
218
|
+
if (options.trace) {
|
|
219
|
+
const { getSymbolicTrace } = require('../index');
|
|
220
|
+
const GoUISession = require('../lib/goUiSession');
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const traceJson = getSymbolicTrace(
|
|
224
|
+
path.resolve(options.file),
|
|
225
|
+
options.method,
|
|
226
|
+
null
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
if (options.json) {
|
|
230
|
+
console.log(traceJson);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Create temp file for the trace
|
|
235
|
+
const tmpDir = require('os').tmpdir();
|
|
236
|
+
const tmpFile = path.join(tmpDir, `trace-${Date.now()}.json`);
|
|
237
|
+
fs.writeFileSync(tmpFile, traceJson);
|
|
238
|
+
|
|
239
|
+
// Launch Go UI
|
|
240
|
+
const session = new GoUISession();
|
|
241
|
+
const binaryPath = session._binaryPath;
|
|
242
|
+
|
|
243
|
+
const { spawnSync } = require('child_process');
|
|
244
|
+
spawnSync(binaryPath, ['trace-results', '--file', tmpFile], {
|
|
245
|
+
stdio: 'inherit'
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Cleanup
|
|
249
|
+
try { fs.unlinkSync(tmpFile); } catch (e) { }
|
|
250
|
+
|
|
251
|
+
} catch (e) {
|
|
252
|
+
console.log(chalk.red(`Trace failed: ${e.message}`));
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
158
258
|
if (!options.type && !options.audit) {
|
|
159
259
|
console.log(chalk.red('Error: --type <type> is required.'));
|
|
160
260
|
process.exit(1);
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/index.d.ts
CHANGED
|
@@ -44,3 +44,5 @@ export declare function applyEditsAtomic(operationsJson: string): string
|
|
|
44
44
|
* Analyzes code, submits a job, and polls for the final generated test.
|
|
45
45
|
*/
|
|
46
46
|
export declare function generateTest(filePath: string, methodName: string, className: string | undefined | null, testType: string | undefined | null, configJson: string, callback: (...args: any[]) => any): string
|
|
47
|
+
/** Analyzes a method and generates a symbolic execution trace. */
|
|
48
|
+
export declare function getSymbolicTrace(filePath: string, methodName: string, className?: string | undefined | null): string
|
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, applyEdit, applyEditsAtomic, generateTest } = nativeBinding
|
|
313
|
+
const { getFileOutline, getProjectMetadata, findCallSites, ping, submitJob, getUserStats, runAudit, applyEdit, applyEditsAtomic, generateTest, getSymbolicTrace } = nativeBinding
|
|
314
314
|
|
|
315
315
|
module.exports.getFileOutline = getFileOutline
|
|
316
316
|
module.exports.getProjectMetadata = getProjectMetadata
|
|
@@ -322,3 +322,4 @@ module.exports.runAudit = runAudit
|
|
|
322
322
|
module.exports.applyEdit = applyEdit
|
|
323
323
|
module.exports.applyEditsAtomic = applyEditsAtomic
|
|
324
324
|
module.exports.generateTest = generateTest
|
|
325
|
+
module.exports.getSymbolicTrace = getSymbolicTrace
|
package/lib/generateTestsUi.js
CHANGED
|
@@ -34,6 +34,12 @@ class GenerateTestsUI {
|
|
|
34
34
|
} else if (choice === 'audit') {
|
|
35
35
|
const result = await this._showFileSelection(true);
|
|
36
36
|
if (result === 'exit') return 'exit';
|
|
37
|
+
} else if (choice === 'xray') {
|
|
38
|
+
const result = await this._showFileSelection(false, true);
|
|
39
|
+
if (result === 'exit') return 'exit';
|
|
40
|
+
} else if (choice === 'trace') {
|
|
41
|
+
const result = await this._showFileSelection(false, false, true);
|
|
42
|
+
if (result === 'exit') return 'exit';
|
|
37
43
|
} else if (choice === 'stats') {
|
|
38
44
|
await this._showStats();
|
|
39
45
|
} else if (choice === 'about') {
|
|
@@ -95,7 +101,7 @@ class GenerateTestsUI {
|
|
|
95
101
|
});
|
|
96
102
|
}
|
|
97
103
|
|
|
98
|
-
async _showFileSelection(isAudit = false) {
|
|
104
|
+
async _showFileSelection(isAudit = false, isXray = false, isTrace = false) {
|
|
99
105
|
const files = await this._getUserFiles();
|
|
100
106
|
|
|
101
107
|
if (files.length === 0) {
|
|
@@ -109,19 +115,23 @@ class GenerateTestsUI {
|
|
|
109
115
|
path: path.dirname(file)
|
|
110
116
|
}));
|
|
111
117
|
|
|
112
|
-
|
|
118
|
+
let title = 'Select JavaScript File';
|
|
119
|
+
if (isAudit) title = 'Select JavaScript File to Audit';
|
|
120
|
+
else if (isXray) title = 'Select File for X-Ray';
|
|
121
|
+
else if (isTrace) title = 'Select File for Symbolic Trace';
|
|
122
|
+
|
|
113
123
|
const selectedName = this.goUiSession.showListView(title, items);
|
|
114
124
|
|
|
115
125
|
if (selectedName === 'back') return 'back';
|
|
116
126
|
if (!selectedName || selectedName === 'exit') return 'exit';
|
|
117
127
|
|
|
118
128
|
const selectedFile = path.resolve(cwd, selectedName);
|
|
119
|
-
const result = await this._showMethodsForFile(selectedFile, isAudit);
|
|
129
|
+
const result = await this._showMethodsForFile(selectedFile, isAudit, isXray, isTrace);
|
|
120
130
|
if (result === 'main_menu') return 'main_menu';
|
|
121
131
|
return result;
|
|
122
132
|
}
|
|
123
133
|
|
|
124
|
-
async _showMethodsForFile(filePath, isAudit = false) {
|
|
134
|
+
async _showMethodsForFile(filePath, isAudit = false, isXray = false, isTrace = false) {
|
|
125
135
|
let outline;
|
|
126
136
|
try {
|
|
127
137
|
const result = getFileOutline(filePath);
|
|
@@ -144,33 +154,48 @@ class GenerateTestsUI {
|
|
|
144
154
|
methodData: m
|
|
145
155
|
}));
|
|
146
156
|
|
|
147
|
-
|
|
157
|
+
let title = 'Select Method';
|
|
158
|
+
if (isAudit) title = `Select Method to Audit for ${fileName}`;
|
|
159
|
+
else if (isXray) title = `Select Method to X-Ray for ${fileName}`;
|
|
160
|
+
else if (isTrace) title = `Select Method to Trace for ${fileName}`;
|
|
161
|
+
|
|
148
162
|
const selectedDisplay = this.goUiSession.showListView(title, items);
|
|
149
163
|
|
|
150
|
-
if (selectedDisplay === 'back' || !selectedDisplay) return this._showFileSelection(isAudit);
|
|
164
|
+
if (selectedDisplay === 'back' || !selectedDisplay) return this._showFileSelection(isAudit, isXray, isTrace);
|
|
151
165
|
|
|
152
166
|
const selectedMethod = items.find(i => i.name === selectedDisplay)?.methodData;
|
|
153
167
|
|
|
154
168
|
if (selectedMethod) {
|
|
169
|
+
if (isTrace) {
|
|
170
|
+
await this._launchTrace(filePath, selectedMethod.name);
|
|
171
|
+
return this._showFileSelection(isAudit, isXray, isTrace);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (isXray) {
|
|
175
|
+
const choice = await this._generateTestsForMethod(filePath, selectedMethod, 'visualize', false, true);
|
|
176
|
+
if (choice === 'main_menu') return 'main_menu';
|
|
177
|
+
return this._showFileSelection(isAudit, isXray);
|
|
178
|
+
}
|
|
179
|
+
|
|
155
180
|
const testType = this.goUiSession.showJsTestMenu();
|
|
156
|
-
if (testType === 'back') return this._showFileSelection(isAudit);
|
|
181
|
+
if (testType === 'back') return this._showFileSelection(isAudit, isXray);
|
|
157
182
|
|
|
158
183
|
const finalType = testType === 'auto' ? null : testType;
|
|
159
184
|
const choice = await this._generateTestsForMethod(filePath, selectedMethod, finalType, isAudit);
|
|
160
185
|
|
|
161
186
|
if (isAudit) {
|
|
162
187
|
if (choice === 'main_menu') return 'main_menu';
|
|
163
|
-
return this._showFileSelection(isAudit);
|
|
188
|
+
return this._showFileSelection(isAudit, isXray);
|
|
164
189
|
}
|
|
165
190
|
|
|
166
191
|
if (choice && choice.file_path && !choice.error) {
|
|
167
192
|
this._showPostGenerationMenu(choice);
|
|
168
193
|
}
|
|
169
194
|
}
|
|
170
|
-
return this._showFileSelection(isAudit);
|
|
195
|
+
return this._showFileSelection(isAudit, isXray);
|
|
171
196
|
}
|
|
172
197
|
|
|
173
|
-
async _generateTestsForMethod(filePath, method, testType, isAudit = false) {
|
|
198
|
+
async _generateTestsForMethod(filePath, method, testType, isAudit = false, isXray = false) {
|
|
174
199
|
if (!this._hasApiKey()) {
|
|
175
200
|
return { error: 'No API key' };
|
|
176
201
|
}
|
|
@@ -179,6 +204,10 @@ class GenerateTestsUI {
|
|
|
179
204
|
return this._handleAuditFlow(filePath, method, testType);
|
|
180
205
|
}
|
|
181
206
|
|
|
207
|
+
if (isXray) {
|
|
208
|
+
return this._handleXrayFlow(filePath, method);
|
|
209
|
+
}
|
|
210
|
+
|
|
182
211
|
const fileName = path.basename(filePath);
|
|
183
212
|
const displayName = method.class_name ? `${method.class_name}#${method.name}` : `${fileName}#${method.name}`;
|
|
184
213
|
return this._handleTestGenerationFlow(filePath, method, testType, displayName);
|
|
@@ -395,6 +424,78 @@ class GenerateTestsUI {
|
|
|
395
424
|
return uiResult || null;
|
|
396
425
|
}
|
|
397
426
|
|
|
427
|
+
async _handleXrayFlow(filePath, method) {
|
|
428
|
+
const actionName = 'Generating X-Ray for';
|
|
429
|
+
const displayName = method.name;
|
|
430
|
+
|
|
431
|
+
const progressHandler = async (progress) => {
|
|
432
|
+
progress.update('Analyzing method structure...');
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
const config = loadConfig();
|
|
436
|
+
// Override test_type to 'visualize' which behaves like test generation but returns Mermaid code
|
|
437
|
+
const resultJson = generateTest(
|
|
438
|
+
filePath,
|
|
439
|
+
method.name,
|
|
440
|
+
method.class_name || null,
|
|
441
|
+
'visualize',
|
|
442
|
+
JSON.stringify(config),
|
|
443
|
+
(msg, percent) => {
|
|
444
|
+
// Simple progress updates
|
|
445
|
+
progress.update(msg, { percent });
|
|
446
|
+
}
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
// The result is expected to be a JSON string containing the mermaid definition
|
|
450
|
+
// But generateTest returns the full file content usually.
|
|
451
|
+
// We need to clarify if Backend returns just Mermaid or a file.
|
|
452
|
+
// Assuming Backend returns JSON with { "visualize_result": "mermaid code..." } or similar
|
|
453
|
+
// or if it returns raw text, we handle it.
|
|
454
|
+
|
|
455
|
+
// For 'visualize' type, existing backend generates a file or JSON.
|
|
456
|
+
// We should parse it.
|
|
457
|
+
let mermaidCode = "";
|
|
458
|
+
try {
|
|
459
|
+
const parsed = JSON.parse(resultJson);
|
|
460
|
+
if (parsed.mermaid_code) mermaidCode = parsed.mermaid_code;
|
|
461
|
+
else mermaidCode = resultJson; // Fallback
|
|
462
|
+
} catch (e) {
|
|
463
|
+
mermaidCode = resultJson;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
success: true,
|
|
468
|
+
mermaidCode: mermaidCode
|
|
469
|
+
};
|
|
470
|
+
} catch (e) {
|
|
471
|
+
progress.error(`Failed to generate X-Ray: ${e.message}`);
|
|
472
|
+
return { error: e.message };
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const result = await this.goUiSession.showProgress(`${actionName} ${displayName}`, progressHandler);
|
|
477
|
+
|
|
478
|
+
if (result && result.mermaidCode) {
|
|
479
|
+
console.log(chalk.bold('\n--- X-Ray Result (Mermaid.js) ---\n'));
|
|
480
|
+
// Simple highlighting for terminal
|
|
481
|
+
console.log(chalk.blue(result.mermaidCode));
|
|
482
|
+
console.log(chalk.bold('\n---------------------------------\n'));
|
|
483
|
+
|
|
484
|
+
// Prompt user action
|
|
485
|
+
this._copyToClipboard(result.mermaidCode);
|
|
486
|
+
console.log(chalk.green('✓ Copied to clipboard! Paste into https://mermaid.live'));
|
|
487
|
+
|
|
488
|
+
// Wait for user confirmation to return
|
|
489
|
+
const { execSync } = require('child_process');
|
|
490
|
+
try {
|
|
491
|
+
// Just a pause hack if we want, but showListView might be better.
|
|
492
|
+
// For now, let's just return.
|
|
493
|
+
} catch (e) { }
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return result;
|
|
497
|
+
}
|
|
498
|
+
|
|
398
499
|
_updateProgress(progress, msg, percent) {
|
|
399
500
|
const agentMap = {
|
|
400
501
|
'context_agent_status': { label: 'Context Builder', step: 1 },
|
|
@@ -572,6 +673,41 @@ class GenerateTestsUI {
|
|
|
572
673
|
return [];
|
|
573
674
|
}
|
|
574
675
|
}
|
|
676
|
+
|
|
677
|
+
async _launchTrace(filePath, methodName) {
|
|
678
|
+
const { getSymbolicTrace } = require('../index');
|
|
679
|
+
const fs = require('fs');
|
|
680
|
+
const path = require('path');
|
|
681
|
+
const chalk = require('chalk');
|
|
682
|
+
|
|
683
|
+
try {
|
|
684
|
+
// 1. Generate Trace (with Spinner)
|
|
685
|
+
const result = this.goUiSession.showSpinner(`Tracing ${methodName}...`, () => {
|
|
686
|
+
try {
|
|
687
|
+
const traceJson = getSymbolicTrace(filePath, methodName, null);
|
|
688
|
+
const tmpDir = require('os').tmpdir();
|
|
689
|
+
const f = path.join(tmpDir, `trace-${Date.now()}.json`);
|
|
690
|
+
fs.writeFileSync(f, traceJson);
|
|
691
|
+
return { success: true, file: f };
|
|
692
|
+
} catch (e) {
|
|
693
|
+
return { success: false, message: e.message };
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
if (result && result.success && result.file) {
|
|
698
|
+
// 2. Show Trace UI
|
|
699
|
+
const { spawnSync } = require('child_process');
|
|
700
|
+
spawnSync(this.goUiSession._binaryPath, ['trace-results', '--file', result.file], {
|
|
701
|
+
stdio: 'inherit'
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
try { fs.unlinkSync(result.file); } catch (e) { }
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
} catch (e) {
|
|
708
|
+
console.error(chalk.red(`Trace error: ${e.message}`));
|
|
709
|
+
}
|
|
710
|
+
}
|
|
575
711
|
}
|
|
576
712
|
|
|
577
713
|
module.exports = GenerateTestsUI;
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|