taist 1.1.0
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/.taistrc.json +15 -0
- package/LICENSE +20 -0
- package/README.md +782 -0
- package/index.js +147 -0
- package/lib/ast-transformer.js +282 -0
- package/lib/execution-tracer.js +370 -0
- package/lib/loader-hooks.js +71 -0
- package/lib/loader.js +154 -0
- package/lib/output-formatter.js +151 -0
- package/lib/spawn-traced.js +204 -0
- package/lib/toon-formatter.js +351 -0
- package/lib/traced.js +223 -0
- package/lib/tracer-setup.js +115 -0
- package/lib/vitest-plugin.js +138 -0
- package/lib/vitest-runner.js +326 -0
- package/lib/watch-handler.js +300 -0
- package/package.json +71 -0
- package/taist.js +294 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vitest Runner - Execute tests and collect results
|
|
3
|
+
* Custom runner and reporter for Vitest integration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Writable } from 'node:stream';
|
|
7
|
+
import { writeFileSync, readFileSync, existsSync, unlinkSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { join, dirname } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { startVitest } from 'vitest/node';
|
|
12
|
+
import { ExecutionTracer } from './execution-tracer.js';
|
|
13
|
+
import { taistPlugin } from './vitest-plugin.js';
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Null writable stream that discards all output
|
|
19
|
+
* Used to suppress Vitest's stdout/stderr when not in verbose mode
|
|
20
|
+
*/
|
|
21
|
+
class NullWritable extends Writable {
|
|
22
|
+
_write(chunk, encoding, callback) {
|
|
23
|
+
callback();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class VitestRunner {
|
|
28
|
+
constructor(options = {}) {
|
|
29
|
+
this.options = options;
|
|
30
|
+
this.tracer = options.tracer || new ExecutionTracer(options.trace || {});
|
|
31
|
+
this.results = null;
|
|
32
|
+
this.verbose = options.verbose || false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Run tests
|
|
37
|
+
* @param {Object} config - Test configuration
|
|
38
|
+
* @returns {Object} Test results
|
|
39
|
+
*/
|
|
40
|
+
async run(config = {}) {
|
|
41
|
+
const vitestConfig = this.buildVitestConfig(config);
|
|
42
|
+
|
|
43
|
+
// Set up trace file for cross-process trace collection
|
|
44
|
+
let traceFilePath = null;
|
|
45
|
+
if (this.options.trace?.enabled) {
|
|
46
|
+
traceFilePath = join(tmpdir(), `taist-traces-${Date.now()}.json`);
|
|
47
|
+
writeFileSync(traceFilePath, '[]');
|
|
48
|
+
process.env.TAIST_TRACE_FILE = traceFilePath;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
if (this.options.trace?.enabled) {
|
|
53
|
+
this.tracer.start();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Create VitestOptions with stream redirection when not in verbose mode
|
|
57
|
+
const vitestOptions = {};
|
|
58
|
+
if (!this.verbose) {
|
|
59
|
+
vitestOptions.stdout = new NullWritable();
|
|
60
|
+
vitestOptions.stderr = new NullWritable();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Add the taist plugin and setup files when tracing is enabled
|
|
64
|
+
const viteOverrides = {};
|
|
65
|
+
if (this.options.trace?.enabled) {
|
|
66
|
+
viteOverrides.plugins = [
|
|
67
|
+
taistPlugin({
|
|
68
|
+
enabled: true,
|
|
69
|
+
depth: this.options.trace?.depth || 2
|
|
70
|
+
})
|
|
71
|
+
];
|
|
72
|
+
// Add setup file to inject tracer into test workers
|
|
73
|
+
vitestConfig.setupFiles = [
|
|
74
|
+
...(vitestConfig.setupFiles || []),
|
|
75
|
+
join(__dirname, 'tracer-setup.js')
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const vitest = await startVitest(
|
|
80
|
+
'test',
|
|
81
|
+
[],
|
|
82
|
+
vitestConfig,
|
|
83
|
+
viteOverrides, // Vite config with our instrumentation plugin
|
|
84
|
+
vitestOptions // stdout/stderr stream redirection
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (!vitest) {
|
|
88
|
+
throw new Error('Failed to start Vitest');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Wait for tests to complete
|
|
92
|
+
await vitest.close();
|
|
93
|
+
|
|
94
|
+
// Collect results
|
|
95
|
+
this.results = this.collectResults(vitest);
|
|
96
|
+
|
|
97
|
+
// Add trace data if enabled - read from trace file written by workers
|
|
98
|
+
if (this.options.trace?.enabled && traceFilePath) {
|
|
99
|
+
try {
|
|
100
|
+
if (existsSync(traceFilePath)) {
|
|
101
|
+
const traceContent = readFileSync(traceFilePath, 'utf-8');
|
|
102
|
+
this.results.trace = JSON.parse(traceContent);
|
|
103
|
+
// Clean up trace file
|
|
104
|
+
unlinkSync(traceFilePath);
|
|
105
|
+
}
|
|
106
|
+
} catch (e) {
|
|
107
|
+
// If reading traces fails, continue without them
|
|
108
|
+
this.results.trace = [];
|
|
109
|
+
}
|
|
110
|
+
this.tracer.stop();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return this.results;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return {
|
|
116
|
+
stats: {
|
|
117
|
+
total: 0,
|
|
118
|
+
passed: 0,
|
|
119
|
+
failed: 1,
|
|
120
|
+
skipped: 0
|
|
121
|
+
},
|
|
122
|
+
failures: [{
|
|
123
|
+
test: 'Test execution',
|
|
124
|
+
error: error.message,
|
|
125
|
+
stack: error.stack
|
|
126
|
+
}],
|
|
127
|
+
duration: 0
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Build Vitest configuration
|
|
134
|
+
*/
|
|
135
|
+
buildVitestConfig(config) {
|
|
136
|
+
const include = config.tests || config.test || ['**/*.test.js', '**/*.spec.js'];
|
|
137
|
+
const includeArray = Array.isArray(include) ? include : [include];
|
|
138
|
+
|
|
139
|
+
const vitestConfig = {
|
|
140
|
+
include: includeArray,
|
|
141
|
+
watch: false,
|
|
142
|
+
reporters: [], // No reporters to suppress output
|
|
143
|
+
ui: false,
|
|
144
|
+
outputFile: false,
|
|
145
|
+
logHeapUsage: false,
|
|
146
|
+
maxConcurrency: 1,
|
|
147
|
+
silent: true,
|
|
148
|
+
// When tracing is enabled, run tests in main thread to share globalThis
|
|
149
|
+
...(this.options.trace?.enabled ? {
|
|
150
|
+
pool: 'forks',
|
|
151
|
+
poolOptions: {
|
|
152
|
+
forks: {
|
|
153
|
+
singleFork: true
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
isolate: false
|
|
157
|
+
} : {}),
|
|
158
|
+
...config
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Only add coverage if explicitly enabled
|
|
162
|
+
if (this.options.coverage === true) {
|
|
163
|
+
vitestConfig.coverage = {
|
|
164
|
+
enabled: true,
|
|
165
|
+
reporter: ['json-summary'],
|
|
166
|
+
all: true,
|
|
167
|
+
...this.options.coverage
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return vitestConfig;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Collect results from Vitest
|
|
176
|
+
*/
|
|
177
|
+
collectResults(vitest) {
|
|
178
|
+
const state = vitest.state;
|
|
179
|
+
const files = state.getFiles();
|
|
180
|
+
|
|
181
|
+
const stats = {
|
|
182
|
+
total: 0,
|
|
183
|
+
passed: 0,
|
|
184
|
+
failed: 0,
|
|
185
|
+
skipped: 0
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const failures = [];
|
|
189
|
+
let totalDuration = 0;
|
|
190
|
+
|
|
191
|
+
// Process all test files
|
|
192
|
+
for (const file of files) {
|
|
193
|
+
const tasks = this.getAllTasks(file);
|
|
194
|
+
|
|
195
|
+
for (const task of tasks) {
|
|
196
|
+
if (task.type !== 'test') continue;
|
|
197
|
+
|
|
198
|
+
stats.total++;
|
|
199
|
+
|
|
200
|
+
if (task.result?.state === 'pass') {
|
|
201
|
+
stats.passed++;
|
|
202
|
+
} else if (task.result?.state === 'fail') {
|
|
203
|
+
stats.failed++;
|
|
204
|
+
failures.push(this.formatFailure(task, file));
|
|
205
|
+
} else if (task.result?.state === 'skip') {
|
|
206
|
+
stats.skipped++;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (task.result?.duration) {
|
|
210
|
+
totalDuration += task.result.duration;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const results = {
|
|
216
|
+
stats,
|
|
217
|
+
failures,
|
|
218
|
+
duration: totalDuration
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// Add coverage if available
|
|
222
|
+
if (vitest.coverageProvider) {
|
|
223
|
+
results.coverage = this.extractCoverage(vitest);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return results;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get all tasks from a file recursively
|
|
231
|
+
*/
|
|
232
|
+
getAllTasks(file) {
|
|
233
|
+
const tasks = [];
|
|
234
|
+
|
|
235
|
+
const collect = (task) => {
|
|
236
|
+
tasks.push(task);
|
|
237
|
+
if (task.tasks) {
|
|
238
|
+
task.tasks.forEach(collect);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
collect(file);
|
|
243
|
+
return tasks;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Format a test failure
|
|
248
|
+
*/
|
|
249
|
+
formatFailure(task, file) {
|
|
250
|
+
const error = task.result?.errors?.[0] || task.result?.error;
|
|
251
|
+
|
|
252
|
+
const failure = {
|
|
253
|
+
test: this.getTestName(task),
|
|
254
|
+
location: this.getLocation(task, file)
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
if (error) {
|
|
258
|
+
failure.error = error.message || String(error);
|
|
259
|
+
failure.stack = error.stack;
|
|
260
|
+
|
|
261
|
+
// Extract diff if available
|
|
262
|
+
if (error.actual !== undefined || error.expected !== undefined) {
|
|
263
|
+
failure.diff = {
|
|
264
|
+
expected: error.expected,
|
|
265
|
+
actual: error.actual
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return failure;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Get full test name
|
|
275
|
+
*/
|
|
276
|
+
getTestName(task) {
|
|
277
|
+
const names = [];
|
|
278
|
+
let current = task;
|
|
279
|
+
|
|
280
|
+
while (current) {
|
|
281
|
+
if (current.name && current.type !== 'file') {
|
|
282
|
+
names.unshift(current.name);
|
|
283
|
+
}
|
|
284
|
+
current = current.suite;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return names.join(' > ') || task.name;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Get test location
|
|
292
|
+
*/
|
|
293
|
+
getLocation(task, file) {
|
|
294
|
+
if (task.location) {
|
|
295
|
+
return {
|
|
296
|
+
file: file.filepath || file.name,
|
|
297
|
+
line: task.location.line,
|
|
298
|
+
column: task.location.column
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return file.filepath || file.name;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Extract coverage information
|
|
307
|
+
*/
|
|
308
|
+
extractCoverage(vitest) {
|
|
309
|
+
// This would extract coverage from vitest.coverageProvider
|
|
310
|
+
// For now, return placeholder
|
|
311
|
+
return {
|
|
312
|
+
percent: 0,
|
|
313
|
+
covered: 0,
|
|
314
|
+
total: 0
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Get results
|
|
320
|
+
*/
|
|
321
|
+
getResults() {
|
|
322
|
+
return this.results;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export default VitestRunner;
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watch Handler - File watching and incremental test runs
|
|
3
|
+
* Enables iterative development with AI tools
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chokidar from 'chokidar';
|
|
7
|
+
import { EventEmitter } from 'events';
|
|
8
|
+
|
|
9
|
+
export class WatchHandler extends EventEmitter {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
super();
|
|
12
|
+
|
|
13
|
+
this.options = {
|
|
14
|
+
ignore: options.ignore || ['node_modules/**', '.git/**', 'dist/**', 'build/**'],
|
|
15
|
+
delay: options.delay || 500,
|
|
16
|
+
maxHistory: options.maxHistory || 10,
|
|
17
|
+
...options
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
this.watcher = null;
|
|
21
|
+
this.history = [];
|
|
22
|
+
this.iteration = 0;
|
|
23
|
+
this.isRunning = false;
|
|
24
|
+
this.debounceTimer = null;
|
|
25
|
+
this.changedFiles = new Set();
|
|
26
|
+
this.lastResults = null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Start watching files
|
|
31
|
+
* @param {Array} paths - Paths to watch
|
|
32
|
+
* @param {Function} onRun - Callback to run tests
|
|
33
|
+
*/
|
|
34
|
+
async start(paths, onRun) {
|
|
35
|
+
if (this.watcher) {
|
|
36
|
+
throw new Error('Watch handler already started');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.onRun = onRun;
|
|
40
|
+
|
|
41
|
+
const watchPaths = Array.isArray(paths) ? paths : [paths];
|
|
42
|
+
|
|
43
|
+
this.watcher = chokidar.watch(watchPaths, {
|
|
44
|
+
ignored: this.options.ignore,
|
|
45
|
+
persistent: true,
|
|
46
|
+
ignoreInitial: true,
|
|
47
|
+
awaitWriteFinish: {
|
|
48
|
+
stabilityThreshold: 200,
|
|
49
|
+
pollInterval: 100
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
this.watcher
|
|
54
|
+
.on('change', (path) => this.handleChange(path))
|
|
55
|
+
.on('add', (path) => this.handleChange(path))
|
|
56
|
+
.on('unlink', (path) => this.handleChange(path))
|
|
57
|
+
.on('error', (error) => this.emit('error', error));
|
|
58
|
+
|
|
59
|
+
// Run initial tests
|
|
60
|
+
await this.runTests([]);
|
|
61
|
+
|
|
62
|
+
this.emit('ready');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Stop watching
|
|
67
|
+
*/
|
|
68
|
+
async stop() {
|
|
69
|
+
if (this.watcher) {
|
|
70
|
+
await this.watcher.close();
|
|
71
|
+
this.watcher = null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (this.debounceTimer) {
|
|
75
|
+
clearTimeout(this.debounceTimer);
|
|
76
|
+
this.debounceTimer = null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Handle file change
|
|
82
|
+
*/
|
|
83
|
+
handleChange(path) {
|
|
84
|
+
this.changedFiles.add(path);
|
|
85
|
+
|
|
86
|
+
// Debounce test runs
|
|
87
|
+
if (this.debounceTimer) {
|
|
88
|
+
clearTimeout(this.debounceTimer);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.debounceTimer = setTimeout(() => {
|
|
92
|
+
const changes = Array.from(this.changedFiles);
|
|
93
|
+
this.changedFiles.clear();
|
|
94
|
+
this.runTests(changes);
|
|
95
|
+
}, this.options.delay);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Run tests
|
|
100
|
+
*/
|
|
101
|
+
async runTests(changes) {
|
|
102
|
+
if (this.isRunning) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.isRunning = true;
|
|
107
|
+
this.iteration++;
|
|
108
|
+
|
|
109
|
+
const startTime = Date.now();
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
this.emit('run-start', { iteration: this.iteration, changes });
|
|
113
|
+
|
|
114
|
+
const results = await this.onRun();
|
|
115
|
+
|
|
116
|
+
const duration = Date.now() - startTime;
|
|
117
|
+
|
|
118
|
+
// Create history entry
|
|
119
|
+
const entry = this.createHistoryEntry(results, changes, duration);
|
|
120
|
+
this.addToHistory(entry);
|
|
121
|
+
|
|
122
|
+
// Store results for comparison
|
|
123
|
+
this.lastResults = results;
|
|
124
|
+
|
|
125
|
+
this.emit('run-complete', {
|
|
126
|
+
iteration: this.iteration,
|
|
127
|
+
results,
|
|
128
|
+
changes,
|
|
129
|
+
duration,
|
|
130
|
+
history: entry
|
|
131
|
+
});
|
|
132
|
+
} catch (error) {
|
|
133
|
+
this.emit('run-error', {
|
|
134
|
+
iteration: this.iteration,
|
|
135
|
+
error,
|
|
136
|
+
changes
|
|
137
|
+
});
|
|
138
|
+
} finally {
|
|
139
|
+
this.isRunning = false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Create history entry
|
|
145
|
+
*/
|
|
146
|
+
createHistoryEntry(results, changes, duration) {
|
|
147
|
+
const summary = {
|
|
148
|
+
pass: results.stats?.passed || 0,
|
|
149
|
+
fail: results.stats?.failed || 0,
|
|
150
|
+
total: results.stats?.total || 0
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Compare with previous results
|
|
154
|
+
if (this.lastResults) {
|
|
155
|
+
summary.new_failures = this.findNewFailures(results, this.lastResults);
|
|
156
|
+
summary.fixed = this.findFixedTests(results, this.lastResults);
|
|
157
|
+
} else {
|
|
158
|
+
summary.new_failures = [];
|
|
159
|
+
summary.fixed = [];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Extract key errors (top 3)
|
|
163
|
+
summary.key_errors = (results.failures || [])
|
|
164
|
+
.slice(0, 3)
|
|
165
|
+
.map(f => this.extractErrorMessage(f));
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
iteration: this.iteration,
|
|
169
|
+
timestamp: new Date().toISOString(),
|
|
170
|
+
changes,
|
|
171
|
+
summary,
|
|
172
|
+
duration
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Find new failures
|
|
178
|
+
*/
|
|
179
|
+
findNewFailures(current, previous) {
|
|
180
|
+
const currentFailures = new Set(
|
|
181
|
+
(current.failures || []).map(f => f.test)
|
|
182
|
+
);
|
|
183
|
+
const previousFailures = new Set(
|
|
184
|
+
(previous.failures || []).map(f => f.test)
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
return Array.from(currentFailures).filter(test => !previousFailures.has(test));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Find fixed tests
|
|
192
|
+
*/
|
|
193
|
+
findFixedTests(current, previous) {
|
|
194
|
+
const currentFailures = new Set(
|
|
195
|
+
(current.failures || []).map(f => f.test)
|
|
196
|
+
);
|
|
197
|
+
const previousFailures = new Set(
|
|
198
|
+
(previous.failures || []).map(f => f.test)
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
return Array.from(previousFailures).filter(test => !currentFailures.has(test));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Extract error message
|
|
206
|
+
*/
|
|
207
|
+
extractErrorMessage(failure) {
|
|
208
|
+
if (failure.error) {
|
|
209
|
+
if (typeof failure.error === 'string') return failure.error;
|
|
210
|
+
if (failure.error.message) return failure.error.message;
|
|
211
|
+
return String(failure.error);
|
|
212
|
+
}
|
|
213
|
+
return 'Unknown error';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Add entry to history
|
|
218
|
+
*/
|
|
219
|
+
addToHistory(entry) {
|
|
220
|
+
this.history.push(entry);
|
|
221
|
+
|
|
222
|
+
// Keep only recent history
|
|
223
|
+
if (this.history.length > this.options.maxHistory) {
|
|
224
|
+
this.history.shift();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get history
|
|
230
|
+
*/
|
|
231
|
+
getHistory() {
|
|
232
|
+
return this.history;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get summary of recent iterations
|
|
237
|
+
*/
|
|
238
|
+
getSummary(count = 5) {
|
|
239
|
+
const recent = this.history.slice(-count);
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
iterations: recent.length,
|
|
243
|
+
current: recent[recent.length - 1],
|
|
244
|
+
trend: this.analyzeTrend(recent)
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Analyze trend in test results
|
|
250
|
+
*/
|
|
251
|
+
analyzeTrend(entries) {
|
|
252
|
+
if (entries.length < 2) {
|
|
253
|
+
return 'stable';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const first = entries[0].summary.fail;
|
|
257
|
+
const last = entries[entries.length - 1].summary.fail;
|
|
258
|
+
|
|
259
|
+
if (last < first) return 'improving';
|
|
260
|
+
if (last > first) return 'degrading';
|
|
261
|
+
return 'stable';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get formatted history for output
|
|
266
|
+
*/
|
|
267
|
+
formatHistory(count = 3) {
|
|
268
|
+
const recent = this.history.slice(-count);
|
|
269
|
+
|
|
270
|
+
return recent.map(entry => {
|
|
271
|
+
const lines = [];
|
|
272
|
+
lines.push(`[${entry.iteration}] ${entry.summary.pass}/${entry.summary.total}`);
|
|
273
|
+
|
|
274
|
+
if (entry.summary.new_failures.length > 0) {
|
|
275
|
+
lines.push(` New: ${entry.summary.new_failures.join(', ')}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (entry.summary.fixed.length > 0) {
|
|
279
|
+
lines.push(` Fixed: ${entry.summary.fixed.join(', ')}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (entry.summary.key_errors.length > 0) {
|
|
283
|
+
lines.push(` Errors: ${entry.summary.key_errors[0]}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return lines.join('\n');
|
|
287
|
+
}).join('\n\n');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Clear history
|
|
292
|
+
*/
|
|
293
|
+
clearHistory() {
|
|
294
|
+
this.history = [];
|
|
295
|
+
this.iteration = 0;
|
|
296
|
+
this.lastResults = null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export default WatchHandler;
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "taist",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Token-Optimized Testing Framework for AI-Assisted Development",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"taist": "taist.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"lib/",
|
|
12
|
+
"index.js",
|
|
13
|
+
"taist.js",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"README.md",
|
|
16
|
+
".taistrc.json"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest",
|
|
21
|
+
"test:unit": "vitest run test/unit",
|
|
22
|
+
"test:integration": "vitest run test/integration",
|
|
23
|
+
"test:coverage": "vitest run --coverage",
|
|
24
|
+
"test:ai": "node taist.js test --format toon",
|
|
25
|
+
"test:ai:watch": "node taist.js watch",
|
|
26
|
+
"test:ai:trace": "node taist.js test --trace --depth 3",
|
|
27
|
+
"demo": "node taist.js test -t ./examples/calculator.test.js",
|
|
28
|
+
"demo:json": "node taist.js test -t ./examples/calculator.test.js --format json",
|
|
29
|
+
"demo:compact": "node taist.js test -t ./examples/calculator.test.js --format compact",
|
|
30
|
+
"demo:failing": "node taist.js test -t ./examples/failing.test.js || true",
|
|
31
|
+
"prepublishOnly": "npm run test"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"testing",
|
|
35
|
+
"ai",
|
|
36
|
+
"tdd",
|
|
37
|
+
"vitest",
|
|
38
|
+
"token-optimized",
|
|
39
|
+
"llm",
|
|
40
|
+
"claude",
|
|
41
|
+
"copilot",
|
|
42
|
+
"ai-testing",
|
|
43
|
+
"test-runner",
|
|
44
|
+
"toon"
|
|
45
|
+
],
|
|
46
|
+
"author": "David Purkiss",
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/davidpurkiss/taist.git"
|
|
51
|
+
},
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/davidpurkiss/taist/issues"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://github.com/davidpurkiss/taist#readme",
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"commander": "^11.1.0",
|
|
58
|
+
"vitest": "^1.0.4",
|
|
59
|
+
"chokidar": "^3.5.3",
|
|
60
|
+
"picocolors": "^1.0.0",
|
|
61
|
+
"acorn": "^8.11.0",
|
|
62
|
+
"estree-walker": "^3.0.3",
|
|
63
|
+
"magic-string": "^0.30.5"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@vitest/ui": "^1.0.4"
|
|
67
|
+
},
|
|
68
|
+
"engines": {
|
|
69
|
+
"node": ">=18.0.0"
|
|
70
|
+
}
|
|
71
|
+
}
|