taist 0.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 +18 -0
- package/LICENSE +20 -0
- package/README.md +471 -0
- package/index.js +147 -0
- package/instrument.js +233 -0
- package/lib/config-loader.js +102 -0
- package/lib/execution-tracer.js +350 -0
- package/lib/instrument-all.js +332 -0
- package/lib/logger.js +61 -0
- package/lib/module-hooks.js +109 -0
- package/lib/module-patcher.js +42 -0
- package/lib/output-formatter.js +151 -0
- package/lib/service-tracer.js +548 -0
- package/lib/toon-formatter.js +498 -0
- package/lib/trace-collector.js +221 -0
- package/lib/trace-context.js +80 -0
- package/lib/trace-reporter.js +329 -0
- package/lib/trace-session.js +119 -0
- package/lib/transform.js +362 -0
- package/lib/vitest-runner.js +436 -0
- package/lib/watch-handler.js +300 -0
- package/package.json +90 -0
- package/taist.js +527 -0
- package/testing.js +29 -0
|
@@ -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,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "taist",
|
|
3
|
+
"version": "0.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
|
+
"instrument.js",
|
|
15
|
+
"testing.js",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"README.md",
|
|
18
|
+
".taistrc.json"
|
|
19
|
+
],
|
|
20
|
+
"exports": {
|
|
21
|
+
".": "./index.js",
|
|
22
|
+
"./instrument": "./instrument.js",
|
|
23
|
+
"./testing": "./testing.js",
|
|
24
|
+
"./module-patcher": "./lib/module-patcher.js",
|
|
25
|
+
"./trace-collector": "./lib/trace-collector.js",
|
|
26
|
+
"./trace-reporter": "./lib/trace-reporter.js",
|
|
27
|
+
"./trace-context": "./lib/trace-context.js",
|
|
28
|
+
"./instrument-all": "./lib/instrument-all.js",
|
|
29
|
+
"./config-loader": "./lib/config-loader.js",
|
|
30
|
+
"./lib/*": "./lib/*"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"test:watch": "vitest",
|
|
35
|
+
"test:unit": "vitest run test/unit",
|
|
36
|
+
"test:integration": "vitest run test/integration",
|
|
37
|
+
"test:coverage": "vitest run --coverage",
|
|
38
|
+
"test:ai": "node taist.js test --format toon",
|
|
39
|
+
"test:ai:watch": "node taist.js watch",
|
|
40
|
+
"test:ai:trace": "node taist.js test --trace --depth 3",
|
|
41
|
+
"demo": "node taist.js test -t ./examples/calculator.test.js",
|
|
42
|
+
"demo:json": "node taist.js test -t ./examples/calculator.test.js --format json",
|
|
43
|
+
"demo:compact": "node taist.js test -t ./examples/calculator.test.js --format compact",
|
|
44
|
+
"demo:failing": "node taist.js test -t ./examples/failing.test.js || true",
|
|
45
|
+
"example:integration": "node examples/integration-service/run-monitored-tests.js",
|
|
46
|
+
"example:integration:watch": "node examples/integration-service/run-monitored-tests.js --watch",
|
|
47
|
+
"example:integration:trace": "node examples/integration-service/run-monitored-tests.js --trace-deep",
|
|
48
|
+
"example:integration:json": "node examples/integration-service/run-monitored-tests.js --json",
|
|
49
|
+
"example:integration:compact": "node examples/integration-service/run-monitored-tests.js --compact",
|
|
50
|
+
"example:integration:enhanced": "node examples/integration-service/run-monitored-tests-enhanced.js",
|
|
51
|
+
"verify": "./verify-tracing.sh",
|
|
52
|
+
"verify:trace": "node examples/integration-service/test-traced.js",
|
|
53
|
+
"prepublishOnly": "npm run test"
|
|
54
|
+
},
|
|
55
|
+
"keywords": [
|
|
56
|
+
"testing",
|
|
57
|
+
"ai",
|
|
58
|
+
"tdd",
|
|
59
|
+
"vitest",
|
|
60
|
+
"token-optimized",
|
|
61
|
+
"llm",
|
|
62
|
+
"claude",
|
|
63
|
+
"copilot",
|
|
64
|
+
"ai-testing",
|
|
65
|
+
"test-runner",
|
|
66
|
+
"toon"
|
|
67
|
+
],
|
|
68
|
+
"author": "David Purkiss",
|
|
69
|
+
"license": "MIT",
|
|
70
|
+
"repository": {
|
|
71
|
+
"type": "git",
|
|
72
|
+
"url": "https://github.com/davidpurkiss/taist.git"
|
|
73
|
+
},
|
|
74
|
+
"bugs": {
|
|
75
|
+
"url": "https://github.com/davidpurkiss/taist/issues"
|
|
76
|
+
},
|
|
77
|
+
"homepage": "https://github.com/davidpurkiss/taist#readme",
|
|
78
|
+
"dependencies": {
|
|
79
|
+
"commander": "^11.1.0",
|
|
80
|
+
"vitest": "^2.1.8",
|
|
81
|
+
"chokidar": "^3.5.3",
|
|
82
|
+
"picocolors": "^1.0.0"
|
|
83
|
+
},
|
|
84
|
+
"devDependencies": {
|
|
85
|
+
"@vitest/ui": "^2.1.8"
|
|
86
|
+
},
|
|
87
|
+
"engines": {
|
|
88
|
+
"node": ">=18.0.0"
|
|
89
|
+
}
|
|
90
|
+
}
|