@mingxy/ocosay 1.0.3 → 1.0.4
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/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/plugin.js +2 -2
- package/dist/plugin.js.map +1 -1
- package/package.json +1 -1
- package/TECH_PLAN.md +0 -352
- package/__mocks__/@opencode-ai/plugin.ts +0 -32
- package/jest.config.js +0 -15
- package/src/config.ts +0 -183
- package/src/core/backends/afplay-backend.ts +0 -162
- package/src/core/backends/aplay-backend.ts +0 -160
- package/src/core/backends/base.ts +0 -117
- package/src/core/backends/index.ts +0 -128
- package/src/core/backends/naudiodon-backend.ts +0 -164
- package/src/core/backends/powershell-backend.ts +0 -173
- package/src/core/player.ts +0 -322
- package/src/core/speaker.ts +0 -283
- package/src/core/stream-player.ts +0 -326
- package/src/core/stream-reader.ts +0 -190
- package/src/core/streaming-synthesizer.ts +0 -123
- package/src/core/types.ts +0 -185
- package/src/index.ts +0 -236
- package/src/plugin.ts +0 -178
- package/src/providers/base.ts +0 -150
- package/src/providers/minimax.ts +0 -515
- package/src/tools/tts.ts +0 -277
- package/src/types/config.ts +0 -38
- package/src/types/naudiodon.d.ts +0 -19
- package/tests/__mocks__/@opencode-ai/plugin.ts +0 -32
- package/tests/backends.test.ts +0 -831
- package/tests/config.test.ts +0 -327
- package/tests/index.test.ts +0 -201
- package/tests/integration-test.d.ts +0 -6
- package/tests/integration-test.d.ts.map +0 -1
- package/tests/integration-test.js +0 -84
- package/tests/integration-test.js.map +0 -1
- package/tests/integration-test.ts +0 -93
- package/tests/p1-fixes.test.ts +0 -160
- package/tests/plugin.test.ts +0 -312
- package/tests/provider.test.d.ts +0 -2
- package/tests/provider.test.d.ts.map +0 -1
- package/tests/provider.test.js +0 -69
- package/tests/provider.test.js.map +0 -1
- package/tests/provider.test.ts +0 -87
- package/tests/speaker.test.d.ts +0 -2
- package/tests/speaker.test.d.ts.map +0 -1
- package/tests/speaker.test.js +0 -63
- package/tests/speaker.test.js.map +0 -1
- package/tests/speaker.test.ts +0 -232
- package/tests/stream-player.test.ts +0 -303
- package/tests/stream-reader.test.ts +0 -269
- package/tests/streaming-synthesizer.test.ts +0 -225
- package/tests/tts-tools.test.ts +0 -270
- package/tests/types.test.d.ts +0 -2
- package/tests/types.test.d.ts.map +0 -1
- package/tests/types.test.js +0 -61
- package/tests/types.test.js.map +0 -1
- package/tests/types.test.ts +0 -63
- package/tsconfig.json +0 -22
package/tests/config.test.ts
DELETED
|
@@ -1,327 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs'
|
|
2
|
-
import * as path from 'path'
|
|
3
|
-
import * as os from 'os'
|
|
4
|
-
import { stripComments, generateDefaultConfig, mergeWithDefaults, loadOrCreateConfig } from '../src/config'
|
|
5
|
-
import { DEFAULT_CONFIG } from '../src/types/config'
|
|
6
|
-
import type { OcosayConfig } from '../src/types/config'
|
|
7
|
-
|
|
8
|
-
jest.mock('fs')
|
|
9
|
-
jest.mock('path')
|
|
10
|
-
jest.mock('os')
|
|
11
|
-
|
|
12
|
-
describe('config.ts', () => {
|
|
13
|
-
const mockHomedir = '/home/testuser'
|
|
14
|
-
const mockConfigPath = path.join(mockHomedir, '.config', 'opencode', 'ocosay.jsonc')
|
|
15
|
-
|
|
16
|
-
beforeEach(() => {
|
|
17
|
-
jest.clearAllMocks()
|
|
18
|
-
;(path.join as jest.Mock).mockReturnValue(mockConfigPath)
|
|
19
|
-
;(path.dirname as jest.Mock).mockReturnValue(path.dirname(mockConfigPath))
|
|
20
|
-
;(os.homedir as jest.Mock).mockReturnValue(mockHomedir)
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
describe('stripComments', () => {
|
|
24
|
-
it('should strip single-line comments //', () => {
|
|
25
|
-
const input = '{"enabled": true // comment'
|
|
26
|
-
const result = stripComments(input)
|
|
27
|
-
expect(result).toBe('{"enabled": true ')
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('should strip multi-line comments /* */', () => {
|
|
31
|
-
const input = '{"enabled": true /* comment */}'
|
|
32
|
-
const result = stripComments(input)
|
|
33
|
-
expect(result).toBe('{"enabled": true }')
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('should strip multiple comments', () => {
|
|
37
|
-
const input = `
|
|
38
|
-
{
|
|
39
|
-
// single line comment
|
|
40
|
-
"enabled": true,
|
|
41
|
-
/* multi
|
|
42
|
-
line
|
|
43
|
-
comment */
|
|
44
|
-
"speed": 1.0
|
|
45
|
-
}
|
|
46
|
-
`
|
|
47
|
-
const result = stripComments(input)
|
|
48
|
-
expect(result).not.toContain('//')
|
|
49
|
-
expect(result).not.toContain('/*')
|
|
50
|
-
expect(result).not.toContain('*/')
|
|
51
|
-
expect(result).toContain('"enabled": true')
|
|
52
|
-
expect(result).toContain('"speed": 1.0')
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('should preserve JSON content', () => {
|
|
56
|
-
const input = '{"enabled": true, "speed": 1.0}'
|
|
57
|
-
const result = stripComments(input)
|
|
58
|
-
expect(JSON.parse(result)).toEqual({ enabled: true, speed: 1.0 })
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('should preserve quoted strings containing // and /* */', () => {
|
|
62
|
-
const input = '{"url": "https://example.com/api?foo=1&bar=2"}'
|
|
63
|
-
const result = stripComments(input)
|
|
64
|
-
expect(JSON.parse(result)).toEqual({ url: 'https://example.com/api?foo=1&bar=2' })
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('should preserve single-quoted strings', () => {
|
|
68
|
-
const input = "{'key': 'value with // comment'}"
|
|
69
|
-
const result = stripComments(input)
|
|
70
|
-
expect(result).toContain('value with // comment')
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('should preserve template literals', () => {
|
|
74
|
-
const input = '`string with // comment`'
|
|
75
|
-
const result = stripComments(input)
|
|
76
|
-
expect(result).toBe('`string with // comment`')
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('should handle comment at end of content', () => {
|
|
80
|
-
const input = '{"key": "value"} // trailing'
|
|
81
|
-
const result = stripComments(input)
|
|
82
|
-
expect(result).toBe('{"key": "value"} ')
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it('should handle empty string', () => {
|
|
86
|
-
const result = stripComments('')
|
|
87
|
-
expect(result).toBe('')
|
|
88
|
-
})
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
describe('generateDefaultConfig', () => {
|
|
92
|
-
it('should return complete OcosayConfig object', () => {
|
|
93
|
-
const config = generateDefaultConfig()
|
|
94
|
-
expect(config).toHaveProperty('enabled')
|
|
95
|
-
expect(config).toHaveProperty('autoPlay')
|
|
96
|
-
expect(config).toHaveProperty('autoRead')
|
|
97
|
-
expect(config).toHaveProperty('streamMode')
|
|
98
|
-
expect(config).toHaveProperty('streamBufferSize')
|
|
99
|
-
expect(config).toHaveProperty('streamBufferTimeout')
|
|
100
|
-
expect(config).toHaveProperty('speed')
|
|
101
|
-
expect(config).toHaveProperty('volume')
|
|
102
|
-
expect(config).toHaveProperty('pitch')
|
|
103
|
-
expect(config).toHaveProperty('providers')
|
|
104
|
-
expect(config).toHaveProperty('providers.minimax')
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('should have apiKey and baseURL as empty strings', () => {
|
|
108
|
-
const config = generateDefaultConfig()
|
|
109
|
-
expect(config.providers.minimax.apiKey).toBe('')
|
|
110
|
-
expect(config.providers.minimax.baseURL).toBe('')
|
|
111
|
-
expect(config.providers.minimax.voiceId).toBe('')
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
it('should have correct default values from DEFAULT_CONFIG', () => {
|
|
115
|
-
const config = generateDefaultConfig()
|
|
116
|
-
expect(config.enabled).toBe(DEFAULT_CONFIG.enabled)
|
|
117
|
-
expect(config.autoPlay).toBe(DEFAULT_CONFIG.autoPlay)
|
|
118
|
-
expect(config.autoRead).toBe(DEFAULT_CONFIG.autoRead)
|
|
119
|
-
expect(config.streamMode).toBe(DEFAULT_CONFIG.streamMode)
|
|
120
|
-
expect(config.streamBufferSize).toBe(DEFAULT_CONFIG.streamBufferSize)
|
|
121
|
-
expect(config.streamBufferTimeout).toBe(DEFAULT_CONFIG.streamBufferTimeout)
|
|
122
|
-
expect(config.speed).toBe(DEFAULT_CONFIG.speed)
|
|
123
|
-
expect(config.volume).toBe(DEFAULT_CONFIG.volume)
|
|
124
|
-
expect(config.pitch).toBe(DEFAULT_CONFIG.pitch)
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('should have correct minimax provider defaults', () => {
|
|
128
|
-
const config = generateDefaultConfig()
|
|
129
|
-
expect(config.providers.minimax.model).toBe('stream')
|
|
130
|
-
expect(config.providers.minimax.ttsModel).toBe('speech-2.8-hd')
|
|
131
|
-
expect(config.providers.minimax.audioFormat).toBe('mp3')
|
|
132
|
-
})
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
describe('mergeWithDefaults', () => {
|
|
136
|
-
it('should override defaults with user config', () => {
|
|
137
|
-
const userConfig = { enabled: false, speed: 2.0 }
|
|
138
|
-
const result = mergeWithDefaults(userConfig, DEFAULT_CONFIG)
|
|
139
|
-
expect(result.enabled).toBe(false)
|
|
140
|
-
expect(result.speed).toBe(2.0)
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
it('should use defaults for unspecified fields', () => {
|
|
144
|
-
const userConfig: Partial<OcosayConfig> = { enabled: false }
|
|
145
|
-
const result = mergeWithDefaults(userConfig, DEFAULT_CONFIG)
|
|
146
|
-
expect(result.enabled).toBe(false)
|
|
147
|
-
expect(result.autoPlay).toBe(DEFAULT_CONFIG.autoPlay)
|
|
148
|
-
expect(result.autoRead).toBe(DEFAULT_CONFIG.autoRead)
|
|
149
|
-
expect(result.streamMode).toBe(DEFAULT_CONFIG.streamMode)
|
|
150
|
-
expect(result.speed).toBe(DEFAULT_CONFIG.speed)
|
|
151
|
-
expect(result.volume).toBe(DEFAULT_CONFIG.volume)
|
|
152
|
-
expect(result.pitch).toBe(DEFAULT_CONFIG.pitch)
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
it('should return defaults when no user config provided', () => {
|
|
156
|
-
const result = mergeWithDefaults({}, DEFAULT_CONFIG)
|
|
157
|
-
expect(result.enabled).toBe(DEFAULT_CONFIG.enabled)
|
|
158
|
-
expect(result.autoPlay).toBe(DEFAULT_CONFIG.autoPlay)
|
|
159
|
-
expect(result.autoRead).toBe(DEFAULT_CONFIG.autoRead)
|
|
160
|
-
expect(result.streamBufferSize).toBe(DEFAULT_CONFIG.streamBufferSize)
|
|
161
|
-
expect(result.streamBufferTimeout).toBe(DEFAULT_CONFIG.streamBufferTimeout)
|
|
162
|
-
expect(result.speed).toBe(DEFAULT_CONFIG.speed)
|
|
163
|
-
expect(result.volume).toBe(DEFAULT_CONFIG.volume)
|
|
164
|
-
expect(result.pitch).toBe(DEFAULT_CONFIG.pitch)
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
it('should preserve all default fields', () => {
|
|
168
|
-
const result = mergeWithDefaults({}, DEFAULT_CONFIG)
|
|
169
|
-
expect(Object.keys(result).sort()).toEqual(Object.keys(DEFAULT_CONFIG).sort())
|
|
170
|
-
})
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
describe('loadOrCreateConfig', () => {
|
|
174
|
-
beforeEach(() => {
|
|
175
|
-
// Reset module to clear any cached state
|
|
176
|
-
jest.resetModules()
|
|
177
|
-
jest.clearAllMocks()
|
|
178
|
-
;(path.join as jest.Mock).mockReturnValue(mockConfigPath)
|
|
179
|
-
;(path.dirname as jest.Mock).mockReturnValue(path.dirname(mockConfigPath))
|
|
180
|
-
;(os.homedir as jest.Mock).mockReturnValue(mockHomedir)
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
it('should create default config when config file does not exist', () => {
|
|
184
|
-
;(fs.existsSync as jest.Mock).mockReturnValue(false)
|
|
185
|
-
;(fs.mkdirSync as jest.Mock).mockReturnValue(undefined)
|
|
186
|
-
;(fs.writeFileSync as jest.Mock).mockReturnValue(undefined)
|
|
187
|
-
;(fs.chmodSync as jest.Mock).mockReturnValue(undefined)
|
|
188
|
-
|
|
189
|
-
const config = loadOrCreateConfig()
|
|
190
|
-
|
|
191
|
-
expect(fs.mkdirSync).toHaveBeenCalledWith(path.dirname(mockConfigPath), { recursive: true })
|
|
192
|
-
expect(fs.writeFileSync).toHaveBeenCalled()
|
|
193
|
-
expect(fs.chmodSync).toHaveBeenCalledWith(mockConfigPath, 0o600)
|
|
194
|
-
expect(config.providers.minimax.apiKey).toBe('')
|
|
195
|
-
expect(config.providers.minimax.baseURL).toBe('')
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
it('should create directory if it does not exist', () => {
|
|
199
|
-
;(fs.existsSync as jest.Mock).mockImplementation((p: string) => {
|
|
200
|
-
if (p === path.dirname(mockConfigPath)) return false
|
|
201
|
-
if (p === mockConfigPath) return false
|
|
202
|
-
return false
|
|
203
|
-
})
|
|
204
|
-
;(fs.mkdirSync as jest.Mock).mockReturnValue(undefined)
|
|
205
|
-
;(fs.writeFileSync as jest.Mock).mockReturnValue(undefined)
|
|
206
|
-
;(fs.chmodSync as jest.Mock).mockReturnValue(undefined)
|
|
207
|
-
|
|
208
|
-
loadOrCreateConfig()
|
|
209
|
-
|
|
210
|
-
expect(fs.mkdirSync).toHaveBeenCalledWith(path.dirname(mockConfigPath), { recursive: true })
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
it('should read and merge existing config file', () => {
|
|
214
|
-
const existingConfig = {
|
|
215
|
-
enabled: false,
|
|
216
|
-
speed: 1.5,
|
|
217
|
-
providers: {
|
|
218
|
-
minimax: {
|
|
219
|
-
apiKey: 'test-key',
|
|
220
|
-
baseURL: 'https://api.minimax.io'
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
;(fs.existsSync as jest.Mock).mockImplementation((p: string) => {
|
|
226
|
-
return p === mockConfigPath || p === path.dirname(mockConfigPath)
|
|
227
|
-
})
|
|
228
|
-
;(fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(existingConfig))
|
|
229
|
-
|
|
230
|
-
const config = loadOrCreateConfig()
|
|
231
|
-
|
|
232
|
-
expect(fs.readFileSync).toHaveBeenCalledWith(mockConfigPath, 'utf-8')
|
|
233
|
-
expect(config.enabled).toBe(false)
|
|
234
|
-
expect(config.speed).toBe(1.5)
|
|
235
|
-
expect(config.providers.minimax.apiKey).toBe('test-key')
|
|
236
|
-
expect(config.providers.minimax.baseURL).toBe('https://api.minimax.io')
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
it('should merge user config with defaults', () => {
|
|
240
|
-
const userConfig = {
|
|
241
|
-
enabled: false
|
|
242
|
-
// other fields missing
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
;(fs.existsSync as jest.Mock).mockImplementation((p: string) => {
|
|
246
|
-
return p === mockConfigPath || p === path.dirname(mockConfigPath)
|
|
247
|
-
})
|
|
248
|
-
;(fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(userConfig))
|
|
249
|
-
|
|
250
|
-
const config = loadOrCreateConfig()
|
|
251
|
-
|
|
252
|
-
expect(config.enabled).toBe(false)
|
|
253
|
-
expect(config.autoPlay).toBe(DEFAULT_CONFIG.autoPlay)
|
|
254
|
-
expect(config.speed).toBe(DEFAULT_CONFIG.speed)
|
|
255
|
-
})
|
|
256
|
-
|
|
257
|
-
it('should handle config file with comments', () => {
|
|
258
|
-
const jsoncContent = `{
|
|
259
|
-
// This is a comment
|
|
260
|
-
"enabled": false,
|
|
261
|
-
/* Another comment */
|
|
262
|
-
"speed": 1.8
|
|
263
|
-
}`
|
|
264
|
-
|
|
265
|
-
;(fs.existsSync as jest.Mock).mockImplementation((p: string) => {
|
|
266
|
-
return p === mockConfigPath || p === path.dirname(mockConfigPath)
|
|
267
|
-
})
|
|
268
|
-
;(fs.readFileSync as jest.Mock).mockReturnValue(jsoncContent)
|
|
269
|
-
|
|
270
|
-
const config = loadOrCreateConfig()
|
|
271
|
-
|
|
272
|
-
expect(config.enabled).toBe(false)
|
|
273
|
-
expect(config.speed).toBe(1.8)
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
it('should use default config on parse error', () => {
|
|
277
|
-
;(fs.existsSync as jest.Mock).mockImplementation((p: string) => {
|
|
278
|
-
return p === mockConfigPath || p === path.dirname(mockConfigPath)
|
|
279
|
-
})
|
|
280
|
-
;(fs.readFileSync as jest.Mock).mockReturnValue('invalid json {')
|
|
281
|
-
|
|
282
|
-
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
283
|
-
const config = loadOrCreateConfig()
|
|
284
|
-
|
|
285
|
-
expect(consoleErrorSpy).toHaveBeenCalled()
|
|
286
|
-
expect(config.providers.minimax.apiKey).toBe('')
|
|
287
|
-
expect(config.providers.minimax.baseURL).toBe('')
|
|
288
|
-
|
|
289
|
-
consoleErrorSpy.mockRestore()
|
|
290
|
-
})
|
|
291
|
-
|
|
292
|
-
it('should handle minimax provider partial config', () => {
|
|
293
|
-
const userConfig = {
|
|
294
|
-
providers: {
|
|
295
|
-
minimax: {
|
|
296
|
-
apiKey: 'my-key'
|
|
297
|
-
// baseURL, voiceId, model, ttsModel, audioFormat missing
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
;(fs.existsSync as jest.Mock).mockImplementation((p: string) => {
|
|
303
|
-
return p === mockConfigPath || p === path.dirname(mockConfigPath)
|
|
304
|
-
})
|
|
305
|
-
;(fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(userConfig))
|
|
306
|
-
|
|
307
|
-
const config = loadOrCreateConfig()
|
|
308
|
-
|
|
309
|
-
expect(config.providers.minimax.apiKey).toBe('my-key')
|
|
310
|
-
expect(config.providers.minimax.baseURL).toBe('')
|
|
311
|
-
expect(config.providers.minimax.model).toBe('stream')
|
|
312
|
-
expect(config.providers.minimax.ttsModel).toBe('speech-2.8-hd')
|
|
313
|
-
expect(config.providers.minimax.audioFormat).toBe('mp3')
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
it('should set correct file permissions on new config', () => {
|
|
317
|
-
;(fs.existsSync as jest.Mock).mockReturnValue(false)
|
|
318
|
-
;(fs.mkdirSync as jest.Mock).mockReturnValue(undefined)
|
|
319
|
-
;(fs.writeFileSync as jest.Mock).mockReturnValue(undefined)
|
|
320
|
-
;(fs.chmodSync as jest.Mock).mockReturnValue(undefined)
|
|
321
|
-
|
|
322
|
-
loadOrCreateConfig()
|
|
323
|
-
|
|
324
|
-
expect(fs.chmodSync).toHaveBeenCalledWith(mockConfigPath, 0o600)
|
|
325
|
-
})
|
|
326
|
-
})
|
|
327
|
-
})
|
package/tests/index.test.ts
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
import { pluginInfo, toolNames, initialize, destroy, isStreamEnabled, getStreamReader } from '../src/index'
|
|
2
|
-
import { registerProvider, unregisterProvider, BaseTTSProvider } from '../src/providers/base'
|
|
3
|
-
import { TTSCapabilities } from '../src/core/types'
|
|
4
|
-
|
|
5
|
-
class MockProvider extends BaseTTSProvider {
|
|
6
|
-
name = 'mock'
|
|
7
|
-
capabilities: TTSCapabilities = { speak: true, stream: true }
|
|
8
|
-
|
|
9
|
-
protected async doSpeak(text: string, voice: string | undefined, model: any) {
|
|
10
|
-
return {
|
|
11
|
-
audioData: Buffer.from([1, 2, 3]),
|
|
12
|
-
format: 'mp3',
|
|
13
|
-
isStream: model === 'stream',
|
|
14
|
-
duration: 1.0
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
describe('index module exports', () => {
|
|
20
|
-
describe('pluginInfo', () => {
|
|
21
|
-
it('should export correct plugin info', () => {
|
|
22
|
-
expect(pluginInfo.name).toBe('ocosay')
|
|
23
|
-
expect(pluginInfo.version).toBe('1.0.0')
|
|
24
|
-
expect(pluginInfo.description).toContain('TTS')
|
|
25
|
-
})
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
describe('toolNames', () => {
|
|
29
|
-
it('should export array of tool names', () => {
|
|
30
|
-
expect(Array.isArray(toolNames)).toBe(true)
|
|
31
|
-
expect(toolNames.length).toBeGreaterThan(0)
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('should include tts_speak', () => {
|
|
35
|
-
expect(toolNames).toContain('tts_speak')
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
it('should include tts_stop', () => {
|
|
39
|
-
expect(toolNames).toContain('tts_stop')
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('should include tts_stream_speak', () => {
|
|
43
|
-
expect(toolNames).toContain('tts_stream_speak')
|
|
44
|
-
})
|
|
45
|
-
})
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
describe('TuiEventBus integration', () => {
|
|
49
|
-
const originalGlobal = global as any
|
|
50
|
-
let mockProvider: MockProvider
|
|
51
|
-
|
|
52
|
-
beforeEach(() => {
|
|
53
|
-
mockProvider = new MockProvider()
|
|
54
|
-
registerProvider('mock', mockProvider)
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
afterEach(async () => {
|
|
58
|
-
delete originalGlobal.__opencode_tuieventbus__
|
|
59
|
-
await destroy()
|
|
60
|
-
unregisterProvider('mock')
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
describe('P1-1: eventBus available', () => {
|
|
64
|
-
it('should subscribe to message.part.delta and message.part.end events', async () => {
|
|
65
|
-
const mockOn = jest.fn()
|
|
66
|
-
const mockOff = jest.fn()
|
|
67
|
-
const MockTuiEventBus = jest.fn().mockImplementation(() => ({
|
|
68
|
-
on: mockOn,
|
|
69
|
-
off: mockOff
|
|
70
|
-
}))
|
|
71
|
-
originalGlobal.__opencode_tuieventbus__ = MockTuiEventBus
|
|
72
|
-
|
|
73
|
-
await initialize({
|
|
74
|
-
autoRead: true,
|
|
75
|
-
defaultProvider: 'mock'
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
expect(mockOn).toHaveBeenCalledWith('message.part.delta', expect.any(Function))
|
|
79
|
-
expect(mockOn).toHaveBeenCalledWith('message.part.end', expect.any(Function))
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('should handle delta events via streamReader', async () => {
|
|
83
|
-
const mockOn = jest.fn()
|
|
84
|
-
const mockOff = jest.fn()
|
|
85
|
-
let deltaHandler: Function | undefined
|
|
86
|
-
|
|
87
|
-
mockOn.mockImplementation((event: string, handler: Function) => {
|
|
88
|
-
if (event === 'message.part.delta') {
|
|
89
|
-
deltaHandler = handler
|
|
90
|
-
}
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
const MockTuiEventBus = jest.fn().mockImplementation(() => ({
|
|
94
|
-
on: mockOn,
|
|
95
|
-
off: mockOff
|
|
96
|
-
}))
|
|
97
|
-
originalGlobal.__opencode_tuieventbus__ = MockTuiEventBus
|
|
98
|
-
|
|
99
|
-
await initialize({
|
|
100
|
-
autoRead: true,
|
|
101
|
-
defaultProvider: 'mock'
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
const mockEvent = {
|
|
105
|
-
sessionId: 'session-1',
|
|
106
|
-
messageId: 'msg-1',
|
|
107
|
-
partId: 'part-1',
|
|
108
|
-
properties: { delta: 'Hello' }
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
expect(deltaHandler).toBeDefined()
|
|
112
|
-
deltaHandler!(mockEvent)
|
|
113
|
-
})
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
describe('P1-1: eventBus unavailable (graceful fallback)', () => {
|
|
117
|
-
it('should not throw when eventBus is not available', async () => {
|
|
118
|
-
delete originalGlobal.__opencode_tuieventbus__
|
|
119
|
-
|
|
120
|
-
await expect(initialize({
|
|
121
|
-
autoRead: true,
|
|
122
|
-
defaultProvider: 'mock'
|
|
123
|
-
})).resolves.not.toThrow()
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
it('should have stream components initialized even without eventBus', async () => {
|
|
127
|
-
delete originalGlobal.__opencode_tuieventbus__
|
|
128
|
-
|
|
129
|
-
await initialize({
|
|
130
|
-
autoRead: true,
|
|
131
|
-
defaultProvider: 'mock'
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
expect(isStreamEnabled()).toBe(true)
|
|
135
|
-
expect(getStreamReader()).toBeDefined()
|
|
136
|
-
})
|
|
137
|
-
})
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
describe('initialize() idempotency', () => {
|
|
141
|
-
let mockProvider: MockProvider
|
|
142
|
-
|
|
143
|
-
beforeEach(() => {
|
|
144
|
-
mockProvider = new MockProvider()
|
|
145
|
-
registerProvider('mock', mockProvider)
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
afterEach(async () => {
|
|
149
|
-
await destroy()
|
|
150
|
-
unregisterProvider('mock')
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
describe('P1-2: repeated initialize() calls', () => {
|
|
154
|
-
it('should be safe to call initialize() multiple times', async () => {
|
|
155
|
-
await initialize({
|
|
156
|
-
defaultProvider: 'mock'
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
await expect(initialize({
|
|
160
|
-
defaultProvider: 'mock'
|
|
161
|
-
})).resolves.not.toThrow()
|
|
162
|
-
|
|
163
|
-
await expect(initialize({
|
|
164
|
-
defaultProvider: 'mock'
|
|
165
|
-
})).resolves.not.toThrow()
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
it('should not re-initialize components on repeated calls', async () => {
|
|
169
|
-
await initialize({
|
|
170
|
-
autoRead: true,
|
|
171
|
-
defaultProvider: 'mock'
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
const firstStreamReader = getStreamReader()
|
|
175
|
-
expect(firstStreamReader).toBeDefined()
|
|
176
|
-
|
|
177
|
-
await initialize({
|
|
178
|
-
defaultProvider: 'mock'
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
const secondStreamReader = getStreamReader()
|
|
182
|
-
expect(secondStreamReader).toBe(firstStreamReader)
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
it('should return early if already initialized', async () => {
|
|
186
|
-
const spyInitialize = jest.spyOn(mockProvider, 'initialize')
|
|
187
|
-
|
|
188
|
-
await initialize({
|
|
189
|
-
defaultProvider: 'mock'
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
spyInitialize.mockClear()
|
|
193
|
-
|
|
194
|
-
await initialize({
|
|
195
|
-
defaultProvider: 'mock'
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
expect(spyInitialize).not.toHaveBeenCalled()
|
|
199
|
-
})
|
|
200
|
-
})
|
|
201
|
-
})
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"integration-test.d.ts","sourceRoot":"","sources":["integration-test.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
const { initialize } = require('../dist/index')
|
|
2
|
-
const { getProvider } = require('../dist/providers/base')
|
|
3
|
-
|
|
4
|
-
const API_KEY = process.env.MINIMAX_API_KEY
|
|
5
|
-
|
|
6
|
-
if (!API_KEY) {
|
|
7
|
-
console.error('❌ 请设置环境变量 MINIMAX_API_KEY')
|
|
8
|
-
console.log('示例: MINIMAX_API_KEY=your-api-key node tests/integration-test.js')
|
|
9
|
-
process.exit(1)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
async function test() {
|
|
13
|
-
console.log('🔧 初始化 ocosay...')
|
|
14
|
-
|
|
15
|
-
await initialize({
|
|
16
|
-
providers: {
|
|
17
|
-
minimax: {
|
|
18
|
-
apiKey: API_KEY,
|
|
19
|
-
voiceId: 'male-qn-qingse',
|
|
20
|
-
model: 'stream'
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
console.log('✅ 初始化成功!\n')
|
|
26
|
-
|
|
27
|
-
console.log('📝 测试 1: 同步合成 (model: sync)')
|
|
28
|
-
try {
|
|
29
|
-
const provider = getProvider('minimax')
|
|
30
|
-
const result = await provider.speak('你好,这是同步合成测试!', { model: 'sync' })
|
|
31
|
-
const data = Buffer.isBuffer(result.audioData) ? result.audioData : Buffer.from([])
|
|
32
|
-
console.log('✅ 同步合成成功! 音频大小: ' + data.length + ' bytes, 格式: ' + result.format)
|
|
33
|
-
} catch (error) {
|
|
34
|
-
console.error('❌ 同步合成失败:', error.message || error)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
console.log('')
|
|
38
|
-
|
|
39
|
-
console.log('📝 测试 2: 流式合成 (model: stream)')
|
|
40
|
-
try {
|
|
41
|
-
const provider = getProvider('minimax')
|
|
42
|
-
const result = await provider.speak('你好,这是流式合成测试!', { model: 'stream' })
|
|
43
|
-
const data = Buffer.isBuffer(result.audioData) ? result.audioData : Buffer.from([])
|
|
44
|
-
console.log('✅ 流式合成成功! 音频大小: ' + data.length + ' bytes, 格式: ' + result.format)
|
|
45
|
-
} catch (error) {
|
|
46
|
-
console.error('❌ 流式合成失败:', error.message || error)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
console.log('')
|
|
50
|
-
|
|
51
|
-
console.log('📝 测试 3: 异步合成 (model: async)')
|
|
52
|
-
try {
|
|
53
|
-
const provider = getProvider('minimax')
|
|
54
|
-
const result = await provider.speak('你好,这是异步合成测试!', { model: 'async' })
|
|
55
|
-
const data = Buffer.isBuffer(result.audioData) ? result.audioData : Buffer.from([])
|
|
56
|
-
console.log('✅ 异步合成成功! 音频大小: ' + data.length + ' bytes, 格式: ' + result.format)
|
|
57
|
-
} catch (error) {
|
|
58
|
-
console.error('❌ 异步合成失败:', error.message || error)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
console.log('')
|
|
62
|
-
|
|
63
|
-
console.log('📝 测试 4: 列出音色')
|
|
64
|
-
try {
|
|
65
|
-
const provider = getProvider('minimax')
|
|
66
|
-
const voices = await provider.listVoices()
|
|
67
|
-
console.log('✅ 获取到 ' + voices.length + ' 个音色:')
|
|
68
|
-
voices.slice(0, 5).forEach(v => {
|
|
69
|
-
console.log(' - ' + v.id + ': ' + v.name + ' (' + (v.language || 'unknown') + ')')
|
|
70
|
-
})
|
|
71
|
-
} catch (error) {
|
|
72
|
-
console.error('❌ 列出音色失败:', error.message || error)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
console.log('')
|
|
76
|
-
console.log('🎉 集成测试完成!')
|
|
77
|
-
|
|
78
|
-
process.exit(0)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
test().catch(err => {
|
|
82
|
-
console.error('❌ 测试失败:', err)
|
|
83
|
-
process.exit(1)
|
|
84
|
-
})
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"integration-test.js","sourceRoot":"","sources":["integration-test.ts"],"names":[],"mappings":"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"}
|