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.
@@ -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
+ }