@tng-sh/js 0.1.0 ā 0.1.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/bin/tng.js +94 -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 +265 -25
- 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.2');
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* @command init
|
|
@@ -142,6 +143,65 @@ program
|
|
|
142
143
|
launchInteractive();
|
|
143
144
|
});
|
|
144
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
|
+
|
|
145
205
|
/**
|
|
146
206
|
* Main command handler (handles -f and -m)
|
|
147
207
|
*/
|
|
@@ -166,6 +226,39 @@ program
|
|
|
166
226
|
}
|
|
167
227
|
});
|
|
168
228
|
|
|
229
|
+
/**
|
|
230
|
+
* @command fix
|
|
231
|
+
* Apply a specific fix to a file
|
|
232
|
+
*/
|
|
233
|
+
program
|
|
234
|
+
.command('fix')
|
|
235
|
+
.description('Apply a fix to a file')
|
|
236
|
+
.option('-f, --file <path>', 'File path to fix')
|
|
237
|
+
.option('-d, --data <json>', 'JSON data for the audit item containing fix info')
|
|
238
|
+
.action(async (options) => {
|
|
239
|
+
if (!options.file || !options.data) {
|
|
240
|
+
console.log(chalk.red('Error: Both --file and --data are required.'));
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const item = JSON.parse(options.data);
|
|
246
|
+
const result = await applyFix(options.file, item);
|
|
247
|
+
if (result.success) {
|
|
248
|
+
console.log(chalk.green(`ā Fix applied to ${options.file}`));
|
|
249
|
+
if (result.backup_path) {
|
|
250
|
+
console.log(chalk.dim(` Backup created at ${result.backup_path}`));
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
console.log(chalk.red(`ā Failed to apply fix: ${result.error}`));
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
} catch (e) {
|
|
257
|
+
console.log(chalk.red(`Error: ${e.message}`));
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
169
262
|
|
|
170
263
|
|
|
171
264
|
/**
|
|
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) {
|
|
@@ -32,6 +34,9 @@ class GenerateTestsUI {
|
|
|
32
34
|
} else if (choice === 'audit') {
|
|
33
35
|
const result = await this._showFileSelection(true);
|
|
34
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';
|
|
35
40
|
} else if (choice === 'stats') {
|
|
36
41
|
await this._showStats();
|
|
37
42
|
} else if (choice === 'about') {
|
|
@@ -93,7 +98,7 @@ class GenerateTestsUI {
|
|
|
93
98
|
});
|
|
94
99
|
}
|
|
95
100
|
|
|
96
|
-
async _showFileSelection(isAudit = false) {
|
|
101
|
+
async _showFileSelection(isAudit = false, isXray = false) {
|
|
97
102
|
const files = await this._getUserFiles();
|
|
98
103
|
|
|
99
104
|
if (files.length === 0) {
|
|
@@ -107,19 +112,19 @@ class GenerateTestsUI {
|
|
|
107
112
|
path: path.dirname(file)
|
|
108
113
|
}));
|
|
109
114
|
|
|
110
|
-
const title = isAudit ? 'Select JavaScript File to Audit' : 'Select JavaScript File';
|
|
115
|
+
const title = isXray ? 'Select File for X-Ray' : (isAudit ? 'Select JavaScript File to Audit' : 'Select JavaScript File');
|
|
111
116
|
const selectedName = this.goUiSession.showListView(title, items);
|
|
112
117
|
|
|
113
118
|
if (selectedName === 'back') return 'back';
|
|
114
119
|
if (!selectedName || selectedName === 'exit') return 'exit';
|
|
115
120
|
|
|
116
121
|
const selectedFile = path.resolve(cwd, selectedName);
|
|
117
|
-
const result = await this._showMethodsForFile(selectedFile, isAudit);
|
|
122
|
+
const result = await this._showMethodsForFile(selectedFile, isAudit, isXray);
|
|
118
123
|
if (result === 'main_menu') return 'main_menu';
|
|
119
124
|
return result;
|
|
120
125
|
}
|
|
121
126
|
|
|
122
|
-
async _showMethodsForFile(filePath, isAudit = false) {
|
|
127
|
+
async _showMethodsForFile(filePath, isAudit = false, isXray = false) {
|
|
123
128
|
let outline;
|
|
124
129
|
try {
|
|
125
130
|
const result = getFileOutline(filePath);
|
|
@@ -142,33 +147,39 @@ class GenerateTestsUI {
|
|
|
142
147
|
methodData: m
|
|
143
148
|
}));
|
|
144
149
|
|
|
145
|
-
const title = isAudit ? `Select Method to Audit for ${fileName}` : `Select Method for ${fileName}
|
|
150
|
+
const title = isXray ? `Select Method to X-Ray for ${fileName}` : (isAudit ? `Select Method to Audit for ${fileName}` : `Select Method for ${fileName}`);
|
|
146
151
|
const selectedDisplay = this.goUiSession.showListView(title, items);
|
|
147
152
|
|
|
148
|
-
if (selectedDisplay === 'back' || !selectedDisplay) return this._showFileSelection(isAudit);
|
|
153
|
+
if (selectedDisplay === 'back' || !selectedDisplay) return this._showFileSelection(isAudit, isXray);
|
|
149
154
|
|
|
150
155
|
const selectedMethod = items.find(i => i.name === selectedDisplay)?.methodData;
|
|
151
156
|
|
|
152
157
|
if (selectedMethod) {
|
|
158
|
+
if (isXray) {
|
|
159
|
+
const choice = await this._generateTestsForMethod(filePath, selectedMethod, 'visualize', false, true);
|
|
160
|
+
if (choice === 'main_menu') return 'main_menu';
|
|
161
|
+
return this._showFileSelection(isAudit, isXray);
|
|
162
|
+
}
|
|
163
|
+
|
|
153
164
|
const testType = this.goUiSession.showJsTestMenu();
|
|
154
|
-
if (testType === 'back') return this._showFileSelection(isAudit);
|
|
165
|
+
if (testType === 'back') return this._showFileSelection(isAudit, isXray);
|
|
155
166
|
|
|
156
167
|
const finalType = testType === 'auto' ? null : testType;
|
|
157
168
|
const choice = await this._generateTestsForMethod(filePath, selectedMethod, finalType, isAudit);
|
|
158
169
|
|
|
159
170
|
if (isAudit) {
|
|
160
171
|
if (choice === 'main_menu') return 'main_menu';
|
|
161
|
-
return this._showFileSelection(isAudit);
|
|
172
|
+
return this._showFileSelection(isAudit, isXray);
|
|
162
173
|
}
|
|
163
174
|
|
|
164
175
|
if (choice && choice.file_path && !choice.error) {
|
|
165
176
|
this._showPostGenerationMenu(choice);
|
|
166
177
|
}
|
|
167
178
|
}
|
|
168
|
-
return this._showFileSelection(isAudit);
|
|
179
|
+
return this._showFileSelection(isAudit, isXray);
|
|
169
180
|
}
|
|
170
181
|
|
|
171
|
-
async _generateTestsForMethod(filePath, method, testType, isAudit = false) {
|
|
182
|
+
async _generateTestsForMethod(filePath, method, testType, isAudit = false, isXray = false) {
|
|
172
183
|
if (!this._hasApiKey()) {
|
|
173
184
|
return { error: 'No API key' };
|
|
174
185
|
}
|
|
@@ -177,6 +188,10 @@ class GenerateTestsUI {
|
|
|
177
188
|
return this._handleAuditFlow(filePath, method, testType);
|
|
178
189
|
}
|
|
179
190
|
|
|
191
|
+
if (isXray) {
|
|
192
|
+
return this._handleXrayFlow(filePath, method);
|
|
193
|
+
}
|
|
194
|
+
|
|
180
195
|
const fileName = path.basename(filePath);
|
|
181
196
|
const displayName = method.class_name ? `${method.class_name}#${method.name}` : `${fileName}#${method.name}`;
|
|
182
197
|
return this._handleTestGenerationFlow(filePath, method, testType, displayName);
|
|
@@ -193,7 +208,7 @@ class GenerateTestsUI {
|
|
|
193
208
|
}
|
|
194
209
|
|
|
195
210
|
async _handleAuditFlow(filePath, method, testType) {
|
|
196
|
-
const
|
|
211
|
+
const { Worker } = require('worker_threads');
|
|
197
212
|
const streamingUi = await this.goUiSession.showStreamingAuditResults(
|
|
198
213
|
method.name,
|
|
199
214
|
method.class_name,
|
|
@@ -202,25 +217,158 @@ class GenerateTestsUI {
|
|
|
202
217
|
|
|
203
218
|
if (!streamingUi) return null;
|
|
204
219
|
|
|
220
|
+
let results = {
|
|
221
|
+
issues: [],
|
|
222
|
+
behaviours: [],
|
|
223
|
+
method_name: method.name,
|
|
224
|
+
class_name: method.class_name,
|
|
225
|
+
method_source_with_lines: method.source_code
|
|
226
|
+
};
|
|
227
|
+
let auditFinished = false;
|
|
228
|
+
|
|
229
|
+
// Start audit in background worker
|
|
230
|
+
const worker = new Worker(path.join(__dirname, 'auditWorker.js'), {
|
|
231
|
+
workerData: { filePath, methodName: method.name, className: method.class_name || null, testType: testType || null }
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const itemIds = new Set();
|
|
235
|
+
worker.on('message', (msg) => {
|
|
236
|
+
if (msg.type === 'item') {
|
|
237
|
+
if (msg.data.startsWith('{')) {
|
|
238
|
+
streamingUi.write(msg.data);
|
|
239
|
+
try {
|
|
240
|
+
const data = JSON.parse(msg.data);
|
|
241
|
+
|
|
242
|
+
// Handle metadata updates from stream
|
|
243
|
+
if (data.method_name) results.method_name = data.method_name;
|
|
244
|
+
if (data.class_name) results.class_name = data.class_name;
|
|
245
|
+
if (data.method_source_with_lines) results.method_source_with_lines = data.method_source_with_lines;
|
|
246
|
+
if (data.source_code) results.method_source_with_lines = data.source_code;
|
|
247
|
+
|
|
248
|
+
// Only push if it looks like an actual audit item
|
|
249
|
+
if (data.test_name || data.summary || data.category) {
|
|
250
|
+
const id = `${data.category}-${data.test_name}-${data.line_number}`;
|
|
251
|
+
if (!itemIds.has(id)) {
|
|
252
|
+
if (data.category === 'behavior' || data.category === 'behaviour') results.behaviours.push(data);
|
|
253
|
+
else results.issues.push(data);
|
|
254
|
+
itemIds.add(id);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} catch (e) { }
|
|
258
|
+
}
|
|
259
|
+
} else if (msg.type === 'result') {
|
|
260
|
+
try {
|
|
261
|
+
const finalResults = typeof msg.data === 'string' ? JSON.parse(msg.data) : msg.data;
|
|
262
|
+
|
|
263
|
+
// Merge final results with our local ones, preserving 'fixed' state
|
|
264
|
+
const mergeList = (localList, incomingList) => {
|
|
265
|
+
if (!incomingList || incomingList.length === 0) return localList;
|
|
266
|
+
|
|
267
|
+
return incomingList.map(incomingItem => {
|
|
268
|
+
const localItem = localList.find(it =>
|
|
269
|
+
it.test_name === incomingItem.test_name &&
|
|
270
|
+
it.line_number === incomingItem.line_number
|
|
271
|
+
);
|
|
272
|
+
if (localItem && localItem.fixed) {
|
|
273
|
+
return { ...incomingItem, fixed: true };
|
|
274
|
+
}
|
|
275
|
+
return incomingItem;
|
|
276
|
+
});
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
results.issues = mergeList(results.issues, finalResults.issues || finalResults.findings);
|
|
280
|
+
results.behaviours = mergeList(results.behaviours, finalResults.behaviours);
|
|
281
|
+
|
|
282
|
+
if (finalResults.method_source_with_lines) results.method_source_with_lines = finalResults.method_source_with_lines;
|
|
283
|
+
if (finalResults.method_name) results.method_name = finalResults.method_name;
|
|
284
|
+
} catch (e) { }
|
|
285
|
+
auditFinished = true;
|
|
286
|
+
} else if (msg.type === 'error') {
|
|
287
|
+
console.error(chalk.red(`Audit error: ${msg.data}`));
|
|
288
|
+
auditFinished = true;
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
worker.on('error', (err) => {
|
|
293
|
+
console.error(chalk.red(`Worker error: ${err.message}`));
|
|
294
|
+
auditFinished = true;
|
|
295
|
+
});
|
|
296
|
+
|
|
205
297
|
try {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
(
|
|
213
|
-
|
|
214
|
-
|
|
298
|
+
let currentChoice = null;
|
|
299
|
+
|
|
300
|
+
// Wait for INITIAL streaming session to complete (user picks an action or quits)
|
|
301
|
+
await streamingUi.wait;
|
|
302
|
+
|
|
303
|
+
const getResponse = (outputFile) => {
|
|
304
|
+
if (fs.existsSync(outputFile)) {
|
|
305
|
+
const output = fs.readFileSync(outputFile, 'utf8').trim();
|
|
306
|
+
if (output) {
|
|
307
|
+
try {
|
|
308
|
+
const parsed = JSON.parse(output);
|
|
309
|
+
return typeof parsed === 'object' ? parsed : { action: output };
|
|
310
|
+
} catch (e) {
|
|
311
|
+
return { action: output };
|
|
312
|
+
}
|
|
215
313
|
}
|
|
216
314
|
}
|
|
217
|
-
|
|
315
|
+
return null;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
currentChoice = getResponse(streamingUi.outputFile);
|
|
319
|
+
|
|
320
|
+
// Action loop: handles fix/open and returns to UI
|
|
321
|
+
while (currentChoice && (currentChoice.action === 'fix' || currentChoice.action === 'open')) {
|
|
322
|
+
const itemIndex = typeof currentChoice.index === 'number' ? currentChoice.index : 0;
|
|
323
|
+
|
|
324
|
+
if (currentChoice.action === 'fix') {
|
|
325
|
+
console.log(chalk.cyan(`Applying fix...`));
|
|
326
|
+
const fixResult = await applyFix(filePath, currentChoice.item);
|
|
327
|
+
if (fixResult.success) {
|
|
328
|
+
console.log(chalk.green(`ā Fix applied successfully.`));
|
|
329
|
+
// Mark as fixed locally
|
|
330
|
+
[results.issues, results.behaviours].forEach(list => {
|
|
331
|
+
list.forEach(it => {
|
|
332
|
+
if (it.test_name === currentChoice.item.test_name && it.line_number === currentChoice.item.line_number) {
|
|
333
|
+
it.fixed = true;
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
} else {
|
|
338
|
+
console.log(chalk.red(`ā Failed to apply fix: ${fixResult.error || 'Unknown error'}`));
|
|
339
|
+
}
|
|
340
|
+
} else if (currentChoice.action === 'open') {
|
|
341
|
+
this._openInEditor(filePath, currentChoice.item.line_number);
|
|
342
|
+
}
|
|
218
343
|
|
|
219
|
-
|
|
220
|
-
|
|
344
|
+
// Ensure source has line numbers for static UI if it doesn't already
|
|
345
|
+
if (results.method_source_with_lines && !/^\s*\d+:/.test(results.method_source_with_lines)) {
|
|
346
|
+
results.method_source_with_lines = results.method_source_with_lines
|
|
347
|
+
.split('\n')
|
|
348
|
+
.map((line, i) => `${i + 1}: ${line}`)
|
|
349
|
+
.join('\n');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Re-launch UI statically with updated results
|
|
353
|
+
const choiceStr = await this.goUiSession.showAuditResults(results, itemIndex);
|
|
354
|
+
if (choiceStr === 'back' || choiceStr === 'main_menu' || choiceStr === 'exit') {
|
|
355
|
+
currentChoice = null;
|
|
356
|
+
} else {
|
|
357
|
+
try {
|
|
358
|
+
const parsed = JSON.parse(choiceStr);
|
|
359
|
+
currentChoice = typeof parsed === 'object' ? parsed : { action: choiceStr };
|
|
360
|
+
} catch (e) {
|
|
361
|
+
currentChoice = { action: choiceStr };
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return { message: 'Audit complete', results };
|
|
221
367
|
} catch (e) {
|
|
222
|
-
console.error(chalk.red(`\nAudit failed: ${e.message}\n`));
|
|
368
|
+
console.error(chalk.red(`\nAudit flow failed: ${e.message}\n`));
|
|
223
369
|
return null;
|
|
370
|
+
} finally {
|
|
371
|
+
await worker.terminate();
|
|
224
372
|
}
|
|
225
373
|
}
|
|
226
374
|
|
|
@@ -260,6 +408,78 @@ class GenerateTestsUI {
|
|
|
260
408
|
return uiResult || null;
|
|
261
409
|
}
|
|
262
410
|
|
|
411
|
+
async _handleXrayFlow(filePath, method) {
|
|
412
|
+
const actionName = 'Generating X-Ray for';
|
|
413
|
+
const displayName = method.name;
|
|
414
|
+
|
|
415
|
+
const progressHandler = async (progress) => {
|
|
416
|
+
progress.update('Analyzing method structure...');
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
const config = loadConfig();
|
|
420
|
+
// Override test_type to 'visualize' which behaves like test generation but returns Mermaid code
|
|
421
|
+
const resultJson = generateTest(
|
|
422
|
+
filePath,
|
|
423
|
+
method.name,
|
|
424
|
+
method.class_name || null,
|
|
425
|
+
'visualize',
|
|
426
|
+
JSON.stringify(config),
|
|
427
|
+
(msg, percent) => {
|
|
428
|
+
// Simple progress updates
|
|
429
|
+
progress.update(msg, { percent });
|
|
430
|
+
}
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
// The result is expected to be a JSON string containing the mermaid definition
|
|
434
|
+
// But generateTest returns the full file content usually.
|
|
435
|
+
// We need to clarify if Backend returns just Mermaid or a file.
|
|
436
|
+
// Assuming Backend returns JSON with { "visualize_result": "mermaid code..." } or similar
|
|
437
|
+
// or if it returns raw text, we handle it.
|
|
438
|
+
|
|
439
|
+
// For 'visualize' type, existing backend generates a file or JSON.
|
|
440
|
+
// We should parse it.
|
|
441
|
+
let mermaidCode = "";
|
|
442
|
+
try {
|
|
443
|
+
const parsed = JSON.parse(resultJson);
|
|
444
|
+
if (parsed.mermaid_code) mermaidCode = parsed.mermaid_code;
|
|
445
|
+
else mermaidCode = resultJson; // Fallback
|
|
446
|
+
} catch (e) {
|
|
447
|
+
mermaidCode = resultJson;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
success: true,
|
|
452
|
+
mermaidCode: mermaidCode
|
|
453
|
+
};
|
|
454
|
+
} catch (e) {
|
|
455
|
+
progress.error(`Failed to generate X-Ray: ${e.message}`);
|
|
456
|
+
return { error: e.message };
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const result = await this.goUiSession.showProgress(`${actionName} ${displayName}`, progressHandler);
|
|
461
|
+
|
|
462
|
+
if (result && result.mermaidCode) {
|
|
463
|
+
console.log(chalk.bold('\n--- X-Ray Result (Mermaid.js) ---\n'));
|
|
464
|
+
// Simple highlighting for terminal
|
|
465
|
+
console.log(chalk.blue(result.mermaidCode));
|
|
466
|
+
console.log(chalk.bold('\n---------------------------------\n'));
|
|
467
|
+
|
|
468
|
+
// Prompt user action
|
|
469
|
+
this._copyToClipboard(result.mermaidCode);
|
|
470
|
+
console.log(chalk.green('ā Copied to clipboard! Paste into https://mermaid.live'));
|
|
471
|
+
|
|
472
|
+
// Wait for user confirmation to return
|
|
473
|
+
const { execSync } = require('child_process');
|
|
474
|
+
try {
|
|
475
|
+
// Just a pause hack if we want, but showListView might be better.
|
|
476
|
+
// For now, let's just return.
|
|
477
|
+
} catch (e) { }
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return result;
|
|
481
|
+
}
|
|
482
|
+
|
|
263
483
|
_updateProgress(progress, msg, percent) {
|
|
264
484
|
const agentMap = {
|
|
265
485
|
'context_agent_status': { label: 'Context Builder', step: 1 },
|
|
@@ -394,6 +614,26 @@ class GenerateTestsUI {
|
|
|
394
614
|
}
|
|
395
615
|
}
|
|
396
616
|
|
|
617
|
+
_openInEditor(filePath, lineNumber) {
|
|
618
|
+
const { execSync } = require('child_process');
|
|
619
|
+
try {
|
|
620
|
+
// Try to open in VS Code if available, fallback to default editor
|
|
621
|
+
const location = lineNumber ? `${filePath}:${lineNumber}` : filePath;
|
|
622
|
+
try {
|
|
623
|
+
execSync(`code --goto ${location}`, { stdio: 'ignore' });
|
|
624
|
+
console.log(chalk.blue(`\nOpening ${location} in VS Code...`));
|
|
625
|
+
} catch (e) {
|
|
626
|
+
if (process.platform === 'darwin') {
|
|
627
|
+
execSync(`open ${filePath}`);
|
|
628
|
+
} else if (process.platform === 'linux') {
|
|
629
|
+
execSync(`xdg-open ${filePath}`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
} catch (e) {
|
|
633
|
+
console.log(chalk.yellow(`\nā ļø Failed to open editor: ${e.message}`));
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
397
637
|
async _getUserFiles() {
|
|
398
638
|
const patterns = ['**/*.{js,ts,jsx,tsx}'];
|
|
399
639
|
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
|