@williamzujkowski/live-coding-music-mcp 2.0.0 → 3.0.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/README.md +222 -231
- package/dist/AudioAnalyzer.js +0 -1
- package/dist/PatternStore.js +0 -1
- package/dist/index.js +0 -0
- package/dist/server/resources.js +175 -0
- package/dist/server/server.js +226 -348
- package/dist/server/tools/ai.js +78 -22
- package/dist/server/tools/analysis.js +266 -73
- package/dist/server/tools/capture.js +67 -20
- package/dist/server/tools/compose.js +159 -0
- package/dist/server/tools/diagnostics.js +112 -60
- package/dist/server/tools/editor.js +130 -66
- package/dist/server/tools/generate.js +190 -74
- package/dist/server/tools/history.js +149 -88
- package/dist/server/tools/playback.js +49 -18
- package/dist/server/tools/session.js +108 -70
- package/dist/server/tools/storage.js +86 -30
- package/dist/server/tools/transform.js +250 -116
- package/dist/server/tools/types.js +66 -1
- package/dist/services/AudioCaptureService.js +4 -3
- package/dist/services/GeminiService.js +1 -1
- package/dist/services/MIDIExportService.js +2 -4
- package/dist/services/SessionManager.js +4 -2
- package/dist/services/StrudelEngine.js +24 -126
- package/dist/services/StrudelEngineHelpers.js +151 -0
- package/package.json +1 -1
- package/dist/__tests__/MusicTheory.test.js +0 -245
- package/dist/__tests__/PatternGenerator.test.js +0 -337
- package/dist/__tests__/PatternStore.test.js +0 -153
- package/dist/__tests__/utils/MockPlaywright.js +0 -130
- package/dist/server/EnhancedMCPServer.js +0 -654
- package/dist/server/EnhancedMCPServerFixed.js +0 -2506
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
import { PatternGenerator } from '../services/PatternGenerator';
|
|
2
|
-
describe('PatternGenerator', () => {
|
|
3
|
-
let generator;
|
|
4
|
-
beforeEach(() => {
|
|
5
|
-
generator = new PatternGenerator();
|
|
6
|
-
});
|
|
7
|
-
describe('generateDrumPattern', () => {
|
|
8
|
-
test('should generate techno drum pattern', () => {
|
|
9
|
-
const pattern = generator.generateDrumPattern('techno', 0.5);
|
|
10
|
-
expect(pattern).toContain('s(');
|
|
11
|
-
expect(pattern).toContain('bd');
|
|
12
|
-
});
|
|
13
|
-
test('should generate house drum pattern', () => {
|
|
14
|
-
const pattern = generator.generateDrumPattern('house', 0.5);
|
|
15
|
-
expect(pattern).toContain('s(');
|
|
16
|
-
expect(pattern).toContain('hh');
|
|
17
|
-
});
|
|
18
|
-
test('should handle complexity levels', () => {
|
|
19
|
-
const simple = generator.generateDrumPattern('techno', 0.0);
|
|
20
|
-
const complex = generator.generateDrumPattern('techno', 1.0);
|
|
21
|
-
expect(simple).toBeTruthy();
|
|
22
|
-
expect(complex).toBeTruthy();
|
|
23
|
-
expect(simple.length).toBeLessThanOrEqual(complex.length);
|
|
24
|
-
});
|
|
25
|
-
test('should generate all supported drum styles', () => {
|
|
26
|
-
const styles = ['techno', 'house', 'dnb', 'breakbeat', 'trap', 'jungle', 'ambient', 'experimental'];
|
|
27
|
-
styles.forEach(style => {
|
|
28
|
-
const pattern = generator.generateDrumPattern(style, 0.5);
|
|
29
|
-
expect(pattern).toBeTruthy();
|
|
30
|
-
expect(pattern).toContain('s(');
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
test('should default to techno for unknown styles', () => {
|
|
34
|
-
const pattern = generator.generateDrumPattern('unknown', 0.5);
|
|
35
|
-
expect(pattern).toBeTruthy();
|
|
36
|
-
expect(pattern).toContain('bd');
|
|
37
|
-
});
|
|
38
|
-
test('should handle complexity edge cases', () => {
|
|
39
|
-
expect(generator.generateDrumPattern('techno', 0)).toBeTruthy();
|
|
40
|
-
expect(generator.generateDrumPattern('techno', 1)).toBeTruthy();
|
|
41
|
-
expect(generator.generateDrumPattern('techno', 0.5)).toBeTruthy();
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
describe('generateBassline', () => {
|
|
45
|
-
test('should generate techno bassline', () => {
|
|
46
|
-
const bass = generator.generateBassline('C', 'techno');
|
|
47
|
-
expect(bass).toContain('note(');
|
|
48
|
-
expect(bass).toContain('C2');
|
|
49
|
-
expect(bass).toContain('sawtooth');
|
|
50
|
-
});
|
|
51
|
-
test('should generate house bassline', () => {
|
|
52
|
-
const bass = generator.generateBassline('D', 'house');
|
|
53
|
-
expect(bass).toContain('D2');
|
|
54
|
-
expect(bass).toContain('sine');
|
|
55
|
-
});
|
|
56
|
-
test('should generate acid bassline with modulation', () => {
|
|
57
|
-
const bass = generator.generateBassline('E', 'acid');
|
|
58
|
-
expect(bass).toContain('E2');
|
|
59
|
-
expect(bass).toContain('cutoff');
|
|
60
|
-
expect(bass).toContain('sine.range');
|
|
61
|
-
});
|
|
62
|
-
test('should handle all supported bass styles', () => {
|
|
63
|
-
const styles = ['techno', 'house', 'dnb', 'acid', 'dub', 'funk', 'jazz', 'ambient'];
|
|
64
|
-
styles.forEach(style => {
|
|
65
|
-
const bass = generator.generateBassline('C', style);
|
|
66
|
-
expect(bass).toContain('note(');
|
|
67
|
-
expect(bass).toContain('C');
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
test('should work with different keys', () => {
|
|
71
|
-
const keys = ['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C#', 'F#'];
|
|
72
|
-
keys.forEach(key => {
|
|
73
|
-
const bass = generator.generateBassline(key, 'techno');
|
|
74
|
-
expect(bass).toContain(key);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
test('should default to techno for unknown styles', () => {
|
|
78
|
-
const bass = generator.generateBassline('C', 'unknown');
|
|
79
|
-
expect(bass).toContain('sawtooth');
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
describe('generateMelody', () => {
|
|
83
|
-
test('should generate melody from scale', () => {
|
|
84
|
-
const scale = ['c', 'd', 'e', 'f', 'g', 'a', 'b'];
|
|
85
|
-
const melody = generator.generateMelody(scale, 8);
|
|
86
|
-
expect(melody).toContain('note(');
|
|
87
|
-
expect(melody).toContain('triangle');
|
|
88
|
-
});
|
|
89
|
-
test('should generate melody with specified length', () => {
|
|
90
|
-
const scale = ['c', 'd', 'e'];
|
|
91
|
-
const melody4 = generator.generateMelody(scale, 4);
|
|
92
|
-
const melody8 = generator.generateMelody(scale, 8);
|
|
93
|
-
const melody16 = generator.generateMelody(scale, 16);
|
|
94
|
-
const notes4 = melody4.match(/[a-g]\d+/g);
|
|
95
|
-
const notes8 = melody8.match(/[a-g]\d+/g);
|
|
96
|
-
const notes16 = melody16.match(/[a-g]\d+/g);
|
|
97
|
-
expect(notes4?.length).toBe(4);
|
|
98
|
-
expect(notes8?.length).toBe(8);
|
|
99
|
-
expect(notes16?.length).toBe(16);
|
|
100
|
-
});
|
|
101
|
-
test('should respect octave range', () => {
|
|
102
|
-
const scale = ['c', 'd', 'e', 'f', 'g'];
|
|
103
|
-
const melody = generator.generateMelody(scale, 8, [4, 5]);
|
|
104
|
-
const notes = melody.match(/[a-g](\d+)/g) || [];
|
|
105
|
-
notes.forEach(note => {
|
|
106
|
-
const octave = parseInt(note.slice(-1));
|
|
107
|
-
expect(octave).toBeGreaterThanOrEqual(4);
|
|
108
|
-
expect(octave).toBeLessThanOrEqual(5);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
test('should use default parameters', () => {
|
|
112
|
-
const scale = ['c', 'd', 'e'];
|
|
113
|
-
const melody = generator.generateMelody(scale);
|
|
114
|
-
expect(melody).toBeTruthy();
|
|
115
|
-
expect(melody).toContain('note(');
|
|
116
|
-
});
|
|
117
|
-
test('should only use notes from provided scale', () => {
|
|
118
|
-
const scale = ['c', 'e', 'g'];
|
|
119
|
-
const melody = generator.generateMelody(scale, 8);
|
|
120
|
-
const notePattern = /([a-g]#?)\d+/g;
|
|
121
|
-
const matches = melody.match(notePattern) || [];
|
|
122
|
-
matches.forEach(match => {
|
|
123
|
-
const note = match.replace(/\d+/g, '');
|
|
124
|
-
expect(scale).toContain(note);
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
describe('generateChords', () => {
|
|
129
|
-
test('should generate triad voicing', () => {
|
|
130
|
-
const chords = generator.generateChords('"C" "F" "G"', 'triad');
|
|
131
|
-
expect(chords).toContain('note(');
|
|
132
|
-
expect(chords).toContain('sawtooth');
|
|
133
|
-
expect(chords).toContain('.struct("1 ~ ~ ~")');
|
|
134
|
-
});
|
|
135
|
-
test('should generate sustained voicing', () => {
|
|
136
|
-
const chords = generator.generateChords('"C" "F"', 'sustained');
|
|
137
|
-
expect(chords).toContain('.attack(0.5)');
|
|
138
|
-
expect(chords).toContain('.release(2)');
|
|
139
|
-
});
|
|
140
|
-
test('should generate pad voicing', () => {
|
|
141
|
-
const chords = generator.generateChords('"C"', 'pad');
|
|
142
|
-
expect(chords).toContain('.attack(2)');
|
|
143
|
-
expect(chords).toContain('.release(4)');
|
|
144
|
-
expect(chords).toContain('.room(0.8)');
|
|
145
|
-
});
|
|
146
|
-
test('should default to triad for unknown voicing', () => {
|
|
147
|
-
const chords = generator.generateChords('"C"', 'unknown');
|
|
148
|
-
expect(chords).toContain('.struct("1 ~ ~ ~")');
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
describe('generateCompletePattern', () => {
|
|
152
|
-
test('should generate complete techno pattern', () => {
|
|
153
|
-
const pattern = generator.generateCompletePattern('techno', 'C', 130);
|
|
154
|
-
expect(pattern).toContain('// techno');
|
|
155
|
-
expect(pattern).toContain('setcpm(130)');
|
|
156
|
-
expect(pattern).toContain('stack(');
|
|
157
|
-
expect(pattern).toContain('// Drums');
|
|
158
|
-
expect(pattern).toContain('// Bass');
|
|
159
|
-
expect(pattern).toContain('// Chords');
|
|
160
|
-
expect(pattern).toContain('// Melody');
|
|
161
|
-
});
|
|
162
|
-
test('should generate pattern for different styles', () => {
|
|
163
|
-
const styles = ['techno', 'house', 'jazz', 'ambient'];
|
|
164
|
-
styles.forEach(style => {
|
|
165
|
-
const pattern = generator.generateCompletePattern(style, 'C', 120);
|
|
166
|
-
expect(pattern).toContain(`// ${style}`);
|
|
167
|
-
expect(pattern).toContain('stack(');
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
test('should use correct key', () => {
|
|
171
|
-
const pattern = generator.generateCompletePattern('house', 'D', 120);
|
|
172
|
-
expect(pattern).toContain('D');
|
|
173
|
-
});
|
|
174
|
-
test('should set correct BPM', () => {
|
|
175
|
-
const pattern140 = generator.generateCompletePattern('dnb', 'C', 140);
|
|
176
|
-
const pattern90 = generator.generateCompletePattern('ambient', 'C', 90);
|
|
177
|
-
expect(pattern140).toContain('setcpm(140)');
|
|
178
|
-
expect(pattern90).toContain('setcpm(90)');
|
|
179
|
-
});
|
|
180
|
-
test('should use appropriate scale for style', () => {
|
|
181
|
-
const jazzPattern = generator.generateCompletePattern('jazz', 'C', 120);
|
|
182
|
-
const technoPattern = generator.generateCompletePattern('techno', 'C', 120);
|
|
183
|
-
// Jazz should use dorian, others use minor (indirectly verified through note choices)
|
|
184
|
-
expect(jazzPattern).toBeTruthy();
|
|
185
|
-
expect(technoPattern).toBeTruthy();
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
describe('generateVariation', () => {
|
|
189
|
-
test('should add subtle variation', () => {
|
|
190
|
-
const base = 's("bd*4")';
|
|
191
|
-
const varied = generator.generateVariation(base, 'subtle');
|
|
192
|
-
expect(varied).toContain(base);
|
|
193
|
-
expect(varied).toContain('.sometimes');
|
|
194
|
-
});
|
|
195
|
-
test('should add moderate variation', () => {
|
|
196
|
-
const base = 's("bd*4")';
|
|
197
|
-
const varied = generator.generateVariation(base, 'moderate');
|
|
198
|
-
expect(varied).toContain('.every(4');
|
|
199
|
-
expect(varied).toContain('.sometimes');
|
|
200
|
-
});
|
|
201
|
-
test('should add extreme variation', () => {
|
|
202
|
-
const base = 's("bd*4")';
|
|
203
|
-
const varied = generator.generateVariation(base, 'extreme');
|
|
204
|
-
expect(varied).toContain('.every(2');
|
|
205
|
-
expect(varied).toContain('.jux');
|
|
206
|
-
});
|
|
207
|
-
test('should add glitch variation', () => {
|
|
208
|
-
const base = 's("bd*4")';
|
|
209
|
-
const varied = generator.generateVariation(base, 'glitch');
|
|
210
|
-
expect(varied).toContain('.chop');
|
|
211
|
-
});
|
|
212
|
-
test('should add evolving variation', () => {
|
|
213
|
-
const base = 's("bd*4")';
|
|
214
|
-
const varied = generator.generateVariation(base, 'evolving');
|
|
215
|
-
expect(varied).toContain('.slow(4)');
|
|
216
|
-
expect(varied).toContain('.palindrome');
|
|
217
|
-
});
|
|
218
|
-
test('should default to subtle for unknown type', () => {
|
|
219
|
-
const base = 's("bd*4")';
|
|
220
|
-
const varied = generator.generateVariation(base, 'unknown');
|
|
221
|
-
expect(varied).toContain('.sometimes');
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
describe('generateFill', () => {
|
|
225
|
-
test('should generate techno fill', () => {
|
|
226
|
-
const fill = generator.generateFill('techno', 1);
|
|
227
|
-
expect(fill).toContain('s(');
|
|
228
|
-
expect(fill).toContain('bd');
|
|
229
|
-
});
|
|
230
|
-
test('should handle different bar lengths', () => {
|
|
231
|
-
const fill1 = generator.generateFill('house', 1);
|
|
232
|
-
const fill2 = generator.generateFill('house', 2);
|
|
233
|
-
expect(fill1).toContain('.fast(1)');
|
|
234
|
-
expect(fill2).toContain('.fast(2)');
|
|
235
|
-
});
|
|
236
|
-
test('should generate fills for all supported styles', () => {
|
|
237
|
-
const styles = ['techno', 'house', 'dnb', 'trap', 'breakbeat'];
|
|
238
|
-
styles.forEach(style => {
|
|
239
|
-
const fill = generator.generateFill(style, 1);
|
|
240
|
-
expect(fill).toBeTruthy();
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
test('should default to techno for unknown style', () => {
|
|
244
|
-
const fill = generator.generateFill('unknown', 1);
|
|
245
|
-
expect(fill).toContain('bd');
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
describe('generateTransition', () => {
|
|
249
|
-
test('should generate transition between styles', () => {
|
|
250
|
-
const transition = generator.generateTransition('techno', 'house', 4);
|
|
251
|
-
expect(transition).toContain('// Transition from techno to house');
|
|
252
|
-
expect(transition).toContain('stack(');
|
|
253
|
-
expect(transition).toContain('// Fade out techno');
|
|
254
|
-
expect(transition).toContain('// Fade in house');
|
|
255
|
-
});
|
|
256
|
-
test('should use specified bar length', () => {
|
|
257
|
-
const transition8 = generator.generateTransition('house', 'dnb', 8);
|
|
258
|
-
expect(transition8).toContain('.slow(8)');
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
describe('generateEuclideanPattern', () => {
|
|
262
|
-
test('should generate Euclidean pattern with default sound', () => {
|
|
263
|
-
const pattern = generator.generateEuclideanPattern(3, 8);
|
|
264
|
-
expect(pattern).toContain('s("bd")');
|
|
265
|
-
expect(pattern).toContain('.struct(');
|
|
266
|
-
});
|
|
267
|
-
test('should use specified sound', () => {
|
|
268
|
-
const pattern = generator.generateEuclideanPattern(5, 16, 'cp');
|
|
269
|
-
expect(pattern).toContain('s("cp")');
|
|
270
|
-
});
|
|
271
|
-
test('should generate valid rhythm structure', () => {
|
|
272
|
-
const pattern = generator.generateEuclideanPattern(4, 12, 'hh');
|
|
273
|
-
expect(pattern).toContain('struct');
|
|
274
|
-
// Pattern should contain the rhythm from MusicTheory
|
|
275
|
-
expect(pattern).toMatch(/1.*~/);
|
|
276
|
-
});
|
|
277
|
-
});
|
|
278
|
-
describe('generatePolyrhythm', () => {
|
|
279
|
-
test('should generate polyrhythm with multiple sounds', () => {
|
|
280
|
-
const sounds = ['bd', 'cp', 'hh'];
|
|
281
|
-
const patterns = [3, 5, 7];
|
|
282
|
-
const poly = generator.generatePolyrhythm(sounds, patterns);
|
|
283
|
-
expect(poly).toContain('stack(');
|
|
284
|
-
expect(poly).toContain('s("bd")');
|
|
285
|
-
expect(poly).toContain('s("cp")');
|
|
286
|
-
expect(poly).toContain('s("hh")');
|
|
287
|
-
expect(poly).toContain('.euclid(3, 16)');
|
|
288
|
-
expect(poly).toContain('.euclid(5, 16)');
|
|
289
|
-
expect(poly).toContain('.euclid(7, 16)');
|
|
290
|
-
});
|
|
291
|
-
test('should throw error for mismatched arrays', () => {
|
|
292
|
-
const sounds = ['bd', 'cp'];
|
|
293
|
-
const patterns = [3, 5, 7];
|
|
294
|
-
expect(() => generator.generatePolyrhythm(sounds, patterns))
|
|
295
|
-
.toThrow('Number of sounds must match number of patterns');
|
|
296
|
-
});
|
|
297
|
-
test('should handle single sound/pattern pair', () => {
|
|
298
|
-
const poly = generator.generatePolyrhythm(['bd'], [4]);
|
|
299
|
-
expect(poly).toContain('s("bd")');
|
|
300
|
-
expect(poly).toContain('.euclid(4, 16)');
|
|
301
|
-
});
|
|
302
|
-
test('should handle empty arrays', () => {
|
|
303
|
-
expect(() => generator.generatePolyrhythm([], []))
|
|
304
|
-
.not.toThrow();
|
|
305
|
-
});
|
|
306
|
-
});
|
|
307
|
-
describe('integration tests', () => {
|
|
308
|
-
test('should create complete musical composition', () => {
|
|
309
|
-
const pattern = generator.generateCompletePattern('house', 'Am', 125);
|
|
310
|
-
const variation = generator.generateVariation(pattern, 'moderate');
|
|
311
|
-
expect(pattern).toBeTruthy();
|
|
312
|
-
expect(variation).toContain(pattern);
|
|
313
|
-
});
|
|
314
|
-
test('should combine multiple generators', () => {
|
|
315
|
-
const drums = generator.generateDrumPattern('techno', 0.7);
|
|
316
|
-
const bass = generator.generateBassline('C', 'techno');
|
|
317
|
-
const fill = generator.generateFill('techno', 1);
|
|
318
|
-
expect(drums).toBeTruthy();
|
|
319
|
-
expect(bass).toBeTruthy();
|
|
320
|
-
expect(fill).toBeTruthy();
|
|
321
|
-
});
|
|
322
|
-
test('should handle complex workflow', () => {
|
|
323
|
-
// Create initial pattern
|
|
324
|
-
const initial = generator.generateCompletePattern('dnb', 'D', 174);
|
|
325
|
-
expect(initial).toBeTruthy();
|
|
326
|
-
// Add variation
|
|
327
|
-
const varied = generator.generateVariation(initial, 'glitch');
|
|
328
|
-
expect(varied).toContain(initial);
|
|
329
|
-
// Generate transition
|
|
330
|
-
const transition = generator.generateTransition('dnb', 'ambient', 8);
|
|
331
|
-
expect(transition).toBeTruthy();
|
|
332
|
-
// Generate final pattern
|
|
333
|
-
const final = generator.generateCompletePattern('ambient', 'D', 80);
|
|
334
|
-
expect(final).toBeTruthy();
|
|
335
|
-
});
|
|
336
|
-
});
|
|
337
|
-
});
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import { PatternStore } from '../PatternStore';
|
|
2
|
-
import { promises as fs } from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { tmpdir } from 'os';
|
|
5
|
-
describe('PatternStore', () => {
|
|
6
|
-
let store;
|
|
7
|
-
let testDir;
|
|
8
|
-
beforeEach(async () => {
|
|
9
|
-
// Create a temporary directory for testing
|
|
10
|
-
testDir = path.join(tmpdir(), 'strudel-test-' + Date.now());
|
|
11
|
-
await fs.mkdir(testDir, { recursive: true });
|
|
12
|
-
store = new PatternStore(testDir);
|
|
13
|
-
});
|
|
14
|
-
afterEach(async () => {
|
|
15
|
-
// Clean up test directory
|
|
16
|
-
try {
|
|
17
|
-
await fs.rm(testDir, { recursive: true, force: true });
|
|
18
|
-
}
|
|
19
|
-
catch (error) {
|
|
20
|
-
// Ignore cleanup errors
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
describe('save', () => {
|
|
24
|
-
test('should save a pattern successfully', async () => {
|
|
25
|
-
const name = 'test-pattern';
|
|
26
|
-
const content = 's("bd*4")';
|
|
27
|
-
const tags = ['techno', 'test'];
|
|
28
|
-
await store.save(name, content, tags);
|
|
29
|
-
const files = await fs.readdir(testDir);
|
|
30
|
-
expect(files).toContain('test-pattern.json');
|
|
31
|
-
});
|
|
32
|
-
test('should sanitize filename', async () => {
|
|
33
|
-
const name = 'Test Pattern With Spaces!@#';
|
|
34
|
-
const content = 's("bd*4")';
|
|
35
|
-
await store.save(name, content);
|
|
36
|
-
const files = await fs.readdir(testDir);
|
|
37
|
-
expect(files).toContain('test_pattern_with_spaces___.json');
|
|
38
|
-
});
|
|
39
|
-
test('should save pattern with timestamp', async () => {
|
|
40
|
-
const name = 'timestamped';
|
|
41
|
-
const content = 's("bd*4")';
|
|
42
|
-
await store.save(name, content);
|
|
43
|
-
const pattern = await store.load(name);
|
|
44
|
-
expect(pattern).not.toBeNull();
|
|
45
|
-
expect(pattern.timestamp).toBeDefined();
|
|
46
|
-
expect(new Date(pattern.timestamp).getTime()).toBeGreaterThan(0);
|
|
47
|
-
});
|
|
48
|
-
test('should overwrite existing pattern', async () => {
|
|
49
|
-
const name = 'overwrite-test';
|
|
50
|
-
await store.save(name, 's("bd*4")');
|
|
51
|
-
await store.save(name, 's("cp*4")');
|
|
52
|
-
const pattern = await store.load(name);
|
|
53
|
-
expect(pattern.content).toBe('s("cp*4")');
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
describe('load', () => {
|
|
57
|
-
test('should load an existing pattern', async () => {
|
|
58
|
-
const name = 'loadable';
|
|
59
|
-
const content = 's("bd*4, cp*2")';
|
|
60
|
-
const tags = ['house'];
|
|
61
|
-
await store.save(name, content, tags);
|
|
62
|
-
const loaded = await store.load(name);
|
|
63
|
-
expect(loaded).not.toBeNull();
|
|
64
|
-
expect(loaded.name).toBe(name);
|
|
65
|
-
expect(loaded.content).toBe(content);
|
|
66
|
-
expect(loaded.tags).toEqual(tags);
|
|
67
|
-
});
|
|
68
|
-
test('should return null for non-existent pattern', async () => {
|
|
69
|
-
const loaded = await store.load('does-not-exist');
|
|
70
|
-
expect(loaded).toBeNull();
|
|
71
|
-
});
|
|
72
|
-
test('should handle sanitized filenames when loading', async () => {
|
|
73
|
-
const name = 'Pattern With Spaces';
|
|
74
|
-
const content = 's("bd*4")';
|
|
75
|
-
await store.save(name, content);
|
|
76
|
-
const loaded = await store.load(name);
|
|
77
|
-
expect(loaded).not.toBeNull();
|
|
78
|
-
expect(loaded.name).toBe(name);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
describe('list', () => {
|
|
82
|
-
test('should list all patterns', async () => {
|
|
83
|
-
await store.save('pattern1', 's("bd*4")', ['techno']);
|
|
84
|
-
await store.save('pattern2', 's("cp*4")', ['house']);
|
|
85
|
-
await store.save('pattern3', 's("hh*8")', ['dnb']);
|
|
86
|
-
const patterns = await store.list();
|
|
87
|
-
expect(patterns).toHaveLength(3);
|
|
88
|
-
expect(patterns.map(p => p.name).sort()).toEqual(['pattern1', 'pattern2', 'pattern3']);
|
|
89
|
-
});
|
|
90
|
-
test('should filter patterns by tag', async () => {
|
|
91
|
-
await store.save('pattern1', 's("bd*4")', ['techno', 'dark']);
|
|
92
|
-
await store.save('pattern2', 's("cp*4")', ['house']);
|
|
93
|
-
await store.save('pattern3', 's("hh*8")', ['techno']);
|
|
94
|
-
const technoPatterns = await store.list('techno');
|
|
95
|
-
expect(technoPatterns).toHaveLength(2);
|
|
96
|
-
expect(technoPatterns.map(p => p.name).sort()).toEqual(['pattern1', 'pattern3']);
|
|
97
|
-
});
|
|
98
|
-
test('should return patterns sorted by timestamp (newest first)', async () => {
|
|
99
|
-
await store.save('old', 's("bd*4")');
|
|
100
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
101
|
-
await store.save('middle', 's("cp*4")');
|
|
102
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
103
|
-
await store.save('new', 's("hh*8")');
|
|
104
|
-
const patterns = await store.list();
|
|
105
|
-
expect(patterns[0].name).toBe('new');
|
|
106
|
-
expect(patterns[1].name).toBe('middle');
|
|
107
|
-
expect(patterns[2].name).toBe('old');
|
|
108
|
-
});
|
|
109
|
-
test('should return empty array for empty directory', async () => {
|
|
110
|
-
const patterns = await store.list();
|
|
111
|
-
expect(patterns).toEqual([]);
|
|
112
|
-
});
|
|
113
|
-
test('should ignore non-JSON files', async () => {
|
|
114
|
-
await store.save('valid', 's("bd*4")');
|
|
115
|
-
await fs.writeFile(path.join(testDir, 'invalid.txt'), 'not json');
|
|
116
|
-
const patterns = await store.list();
|
|
117
|
-
expect(patterns).toHaveLength(1);
|
|
118
|
-
expect(patterns[0].name).toBe('valid');
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
describe('edge cases', () => {
|
|
122
|
-
test('should handle patterns with special characters in content', async () => {
|
|
123
|
-
const name = 'special';
|
|
124
|
-
const content = 's("bd*4").room(0.9)\n// Comment with "quotes"';
|
|
125
|
-
await store.save(name, content);
|
|
126
|
-
const loaded = await store.load(name);
|
|
127
|
-
expect(loaded.content).toBe(content);
|
|
128
|
-
});
|
|
129
|
-
test('should handle very long pattern names', async () => {
|
|
130
|
-
const name = 'a'.repeat(200);
|
|
131
|
-
const content = 's("bd*4")';
|
|
132
|
-
await store.save(name, content);
|
|
133
|
-
const loaded = await store.load(name);
|
|
134
|
-
expect(loaded).not.toBeNull();
|
|
135
|
-
expect(loaded.name).toBe(name);
|
|
136
|
-
});
|
|
137
|
-
test('should handle empty tags array', async () => {
|
|
138
|
-
const name = 'no-tags';
|
|
139
|
-
const content = 's("bd*4")';
|
|
140
|
-
await store.save(name, content, []);
|
|
141
|
-
const loaded = await store.load(name);
|
|
142
|
-
expect(loaded.tags).toEqual([]);
|
|
143
|
-
});
|
|
144
|
-
test('should handle unicode characters in names', async () => {
|
|
145
|
-
const name = 'パターン-テスト';
|
|
146
|
-
const content = 's("bd*4")';
|
|
147
|
-
await store.save(name, content);
|
|
148
|
-
const loaded = await store.load(name);
|
|
149
|
-
expect(loaded).not.toBeNull();
|
|
150
|
-
expect(loaded.name).toBe(name);
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
});
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
export class MockPage {
|
|
2
|
-
content = '';
|
|
3
|
-
isPlayingState = false;
|
|
4
|
-
evaluateHandlers = new Map();
|
|
5
|
-
url = '';
|
|
6
|
-
async goto(url, options) {
|
|
7
|
-
this.url = url;
|
|
8
|
-
return {};
|
|
9
|
-
}
|
|
10
|
-
async waitForSelector(selector, options) {
|
|
11
|
-
return {};
|
|
12
|
-
}
|
|
13
|
-
async waitForTimeout(timeout) {
|
|
14
|
-
return new Promise(resolve => setTimeout(resolve, Math.min(timeout, 10)));
|
|
15
|
-
}
|
|
16
|
-
async click(selector, options) {
|
|
17
|
-
if (selector.includes('play')) {
|
|
18
|
-
this.isPlayingState = true;
|
|
19
|
-
}
|
|
20
|
-
else if (selector.includes('stop')) {
|
|
21
|
-
this.isPlayingState = false;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
async type(text, options) {
|
|
25
|
-
this.content += text;
|
|
26
|
-
}
|
|
27
|
-
async route(url, handler) {
|
|
28
|
-
// Mock route interception
|
|
29
|
-
}
|
|
30
|
-
keyboard = {
|
|
31
|
-
press: async (key) => {
|
|
32
|
-
if (key.includes('Enter')) {
|
|
33
|
-
this.isPlayingState = true;
|
|
34
|
-
}
|
|
35
|
-
else if (key.includes('Period')) {
|
|
36
|
-
this.isPlayingState = false;
|
|
37
|
-
}
|
|
38
|
-
else if (key.includes('A')) {
|
|
39
|
-
this.content = '';
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
type: async (text) => {
|
|
43
|
-
this.content += text;
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
async evaluate(fn, ...args) {
|
|
47
|
-
// Simulate common evaluate calls
|
|
48
|
-
const fnString = fn.toString();
|
|
49
|
-
if (fnString.includes('strudelAudioAnalyzer')) {
|
|
50
|
-
if (fnString.includes('connect()')) {
|
|
51
|
-
return undefined;
|
|
52
|
-
}
|
|
53
|
-
if (fnString.includes('analyze()')) {
|
|
54
|
-
return {
|
|
55
|
-
connected: true,
|
|
56
|
-
timestamp: Date.now(),
|
|
57
|
-
features: {
|
|
58
|
-
average: 50,
|
|
59
|
-
peak: 100,
|
|
60
|
-
peakFrequency: 440,
|
|
61
|
-
centroid: 250,
|
|
62
|
-
bass: 60,
|
|
63
|
-
lowMid: 50,
|
|
64
|
-
mid: 40,
|
|
65
|
-
highMid: 30,
|
|
66
|
-
treble: 20,
|
|
67
|
-
isPlaying: this.isPlayingState,
|
|
68
|
-
isSilent: !this.isPlayingState,
|
|
69
|
-
bassToTrebleRatio: '3.00',
|
|
70
|
-
brightness: 'balanced'
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
if (fnString.includes('.cm-content')) {
|
|
76
|
-
if (fnString.includes('textContent')) {
|
|
77
|
-
return this.content;
|
|
78
|
-
}
|
|
79
|
-
if (fnString.includes('dispatch')) {
|
|
80
|
-
const newContent = args[0];
|
|
81
|
-
if (typeof newContent === 'string') {
|
|
82
|
-
this.content = newContent;
|
|
83
|
-
}
|
|
84
|
-
return undefined;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
// Allow custom handlers
|
|
88
|
-
for (const [key, handler] of this.evaluateHandlers.entries()) {
|
|
89
|
-
if (fnString.includes(key)) {
|
|
90
|
-
return handler(...args);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return undefined;
|
|
94
|
-
}
|
|
95
|
-
setEvaluateHandler(key, handler) {
|
|
96
|
-
this.evaluateHandlers.set(key, handler);
|
|
97
|
-
}
|
|
98
|
-
getContent() {
|
|
99
|
-
return this.content;
|
|
100
|
-
}
|
|
101
|
-
setContent(content) {
|
|
102
|
-
this.content = content;
|
|
103
|
-
}
|
|
104
|
-
isPlaying() {
|
|
105
|
-
return this.isPlayingState;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
export class MockBrowserContext {
|
|
109
|
-
async newPage() {
|
|
110
|
-
return new MockPage();
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
export class MockBrowser {
|
|
114
|
-
closed = false;
|
|
115
|
-
async newContext(options) {
|
|
116
|
-
return new MockBrowserContext();
|
|
117
|
-
}
|
|
118
|
-
async close() {
|
|
119
|
-
this.closed = true;
|
|
120
|
-
}
|
|
121
|
-
isClosed() {
|
|
122
|
-
return this.closed;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
export const createMockBrowser = () => {
|
|
126
|
-
return new MockBrowser();
|
|
127
|
-
};
|
|
128
|
-
export const createMockPage = () => {
|
|
129
|
-
return new MockPage();
|
|
130
|
-
};
|