@lythos/agent-adapter-deepseek-serve 0.16.0 → 0.16.1
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/package.json +2 -2
- package/src/deepseek-serve.test.ts +28 -28
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lythos/agent-adapter-deepseek-serve",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "bun test src/ --pass-with-no-tests",
|
|
@@ -22,6 +22,6 @@
|
|
|
22
22
|
"serve-mode"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@lythos/agent-adapter": "^0.16.
|
|
25
|
+
"@lythos/agent-adapter": "^0.16.1"
|
|
26
26
|
}
|
|
27
27
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* No real serve process needed — pure state machine + injectable fs.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { describe,
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from 'bun:test'
|
|
7
7
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
8
8
|
import { tmpdir } from 'node:os'
|
|
9
9
|
import { join } from 'node:path'
|
|
@@ -30,14 +30,14 @@ describe('ServeLock FSM — lock file lifecycle', () => {
|
|
|
30
30
|
beforeEach(() => { lockDir = tempLockDir() })
|
|
31
31
|
afterEach(() => { try { rmSync(lockDir, { recursive: true, force: true }) } catch {} })
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
it('FSM: no lock → null', () => {
|
|
34
34
|
const lockPath = join(lockDir, 'deepseek-serve.json')
|
|
35
35
|
// inline logic
|
|
36
36
|
// Simulate readLock logic
|
|
37
37
|
expect(existsSync(lockPath)).toBe(false)
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
it('FSM: write lock → read back', () => {
|
|
41
41
|
const lock = { pid: 12345, port: 17878, version: '0.8.14', startedAt: new Date().toISOString(), threads: {} }
|
|
42
42
|
writeTestLock(lockDir, lock)
|
|
43
43
|
const content = readFileSync(join(lockDir, 'deepseek-serve.json'), 'utf-8')
|
|
@@ -48,7 +48,7 @@ describe('ServeLock FSM — lock file lifecycle', () => {
|
|
|
48
48
|
expect(parsed.threads).toEqual({})
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
it('FSM: corrupt lock → null (not crash)', () => {
|
|
52
52
|
writeFileSync(join(lockDir, 'deepseek-serve.json'), 'not valid json {{{')
|
|
53
53
|
// readLock should return null, not throw
|
|
54
54
|
try {
|
|
@@ -62,7 +62,7 @@ describe('ServeLock FSM — lock file lifecycle', () => {
|
|
|
62
62
|
}
|
|
63
63
|
})
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
it('FSM: updateThreadMapping adds entry', () => {
|
|
66
66
|
const lock = { pid: 12345, port: 17878, version: '0.8.14', startedAt: new Date().toISOString(), threads: {} as Record<string, string> }
|
|
67
67
|
writeTestLock(lockDir, lock)
|
|
68
68
|
// Simulate update
|
|
@@ -74,7 +74,7 @@ describe('ServeLock FSM — lock file lifecycle', () => {
|
|
|
74
74
|
expect(Object.keys(parsed.threads).length).toBe(1)
|
|
75
75
|
})
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
it('FSM: multiple sessions map to different threads', () => {
|
|
78
78
|
const lock = { pid: 12345, port: 17878, version: '0.8.14', startedAt: new Date().toISOString(), threads: {} as Record<string, string> }
|
|
79
79
|
lock.threads['arena-20260508-001'] = 'thr_aaa'
|
|
80
80
|
lock.threads['arena-20260508-002'] = 'thr_bbb'
|
|
@@ -90,14 +90,14 @@ describe('ServeLock FSM — lock file lifecycle', () => {
|
|
|
90
90
|
// ── PID detection ───────────────────────────────────────────────────────────
|
|
91
91
|
|
|
92
92
|
describe('isProcessAlive — PID signal', () => {
|
|
93
|
-
|
|
93
|
+
it('current process PID is alive', () => {
|
|
94
94
|
const alive = (() => {
|
|
95
95
|
try { process.kill(process.pid, 0); return true } catch { return false }
|
|
96
96
|
})()
|
|
97
97
|
expect(alive).toBe(true)
|
|
98
98
|
})
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
it('impossible PID is dead', () => {
|
|
101
101
|
// PID 99999 is extremely unlikely to exist
|
|
102
102
|
const alive = (() => {
|
|
103
103
|
try { process.kill(99999, 0); return true } catch { return false }
|
|
@@ -105,7 +105,7 @@ describe('isProcessAlive — PID signal', () => {
|
|
|
105
105
|
expect(alive).toBe(false)
|
|
106
106
|
})
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
it('PID 0 is alive (self)', () => {
|
|
109
109
|
// process.kill(0, 0) signals the whole process group — always succeeds
|
|
110
110
|
const alive = (() => {
|
|
111
111
|
try { process.kill(0, 0); return true } catch { return false }
|
|
@@ -117,30 +117,30 @@ describe('isProcessAlive — PID signal', () => {
|
|
|
117
117
|
// ── Version parsing ─────────────────────────────────────────────────────────
|
|
118
118
|
|
|
119
119
|
describe('getVersion — parse deepseek --version', () => {
|
|
120
|
-
|
|
120
|
+
it('parses standard version format', () => {
|
|
121
121
|
const out = 'deepseek 0.8.14\n'
|
|
122
122
|
const match = out.match(/(\d+\.\d+\.\d+)/)
|
|
123
123
|
expect(match?.[1]).toBe('0.8.14')
|
|
124
124
|
})
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
it('parses version from mixed output', () => {
|
|
127
127
|
const out = 'DeepSeek TUI v0.8.14 (abc1234)\nRuntime: Rust 1.80\n'
|
|
128
128
|
const match = out.match(/(\d+\.\d+\.\d+)/)
|
|
129
129
|
expect(match?.[1]).toBe('0.8.14')
|
|
130
130
|
})
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
it('returns null for no version', () => {
|
|
133
133
|
const out = 'command not found\n'
|
|
134
134
|
const match = out.match(/(\d+\.\d+\.\d+)/)
|
|
135
135
|
expect(match).toBe(null)
|
|
136
136
|
})
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
it('0.8.x passes version check', () => {
|
|
139
139
|
const version = '0.8.14'
|
|
140
140
|
expect(version.startsWith('0.8.')).toBe(true)
|
|
141
141
|
})
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
it('0.9.x still works (with warning)', () => {
|
|
144
144
|
const version = '0.9.0'
|
|
145
145
|
expect(version.startsWith('0.8.')).toBe(false)
|
|
146
146
|
// Should warn but continue
|
|
@@ -150,7 +150,7 @@ describe('getVersion — parse deepseek --version', () => {
|
|
|
150
150
|
// ── Session ID format ───────────────────────────────────────────────────────
|
|
151
151
|
|
|
152
152
|
describe('nextSessionId — format', () => {
|
|
153
|
-
|
|
153
|
+
it('starts with arena- prefix', () => {
|
|
154
154
|
let counter = 0
|
|
155
155
|
const ts = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 15)
|
|
156
156
|
counter++
|
|
@@ -158,7 +158,7 @@ describe('nextSessionId — format', () => {
|
|
|
158
158
|
expect(id.startsWith('arena-')).toBe(true)
|
|
159
159
|
})
|
|
160
160
|
|
|
161
|
-
|
|
161
|
+
it('counter increments', () => {
|
|
162
162
|
let counter = 0
|
|
163
163
|
const ts = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 15)
|
|
164
164
|
const ids: string[] = []
|
|
@@ -176,12 +176,12 @@ describe('nextSessionId — format', () => {
|
|
|
176
176
|
// ── Adapter registration ────────────────────────────────────────────────────
|
|
177
177
|
|
|
178
178
|
describe('adapter registration', () => {
|
|
179
|
-
|
|
179
|
+
it('adapter is registered under name "deepseek"', async () => {
|
|
180
180
|
const { deepseekServeAdapter } = await import('./deepseek-serve')
|
|
181
181
|
expect(deepseekServeAdapter.name).toBe('deepseek')
|
|
182
182
|
})
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
it('adapter has spawn method', async () => {
|
|
185
185
|
const { deepseekServeAdapter } = await import('./deepseek-serve')
|
|
186
186
|
expect(typeof deepseekServeAdapter.spawn).toBe('function')
|
|
187
187
|
})
|
|
@@ -190,7 +190,7 @@ describe('adapter registration', () => {
|
|
|
190
190
|
// ── Actor FSM: state transitions (conceptual) ───────────────────────────────
|
|
191
191
|
|
|
192
192
|
describe('Actor FSM — state transitions', () => {
|
|
193
|
-
|
|
193
|
+
it('state: cold start → serve running', () => {
|
|
194
194
|
// Given: no lock file, serve not running
|
|
195
195
|
// When: ensureServeRunning()
|
|
196
196
|
// Then: findFreePort → spawn serve → health check → writeLock → return port
|
|
@@ -200,7 +200,7 @@ describe('Actor FSM — state transitions', () => {
|
|
|
200
200
|
expect(states).toContain('error')
|
|
201
201
|
})
|
|
202
202
|
|
|
203
|
-
|
|
203
|
+
it('state: warm reuse → skip start', () => {
|
|
204
204
|
// Given: lock exists, PID alive, health check passes
|
|
205
205
|
// When: ensureServeRunning()
|
|
206
206
|
// Then: cachedPort → health check → return (no new process)
|
|
@@ -208,7 +208,7 @@ describe('Actor FSM — state transitions', () => {
|
|
|
208
208
|
expect(expectedPath.length).toBe(3)
|
|
209
209
|
})
|
|
210
210
|
|
|
211
|
-
|
|
211
|
+
it('state: dead PID → restart', () => {
|
|
212
212
|
// Given: lock exists, PID dead
|
|
213
213
|
// When: ensureServeRunning()
|
|
214
214
|
// Then: isProcessAlive → false → findFreePort → spawn → health → writeLock
|
|
@@ -216,7 +216,7 @@ describe('Actor FSM — state transitions', () => {
|
|
|
216
216
|
expect(expectedPath.length).toBe(7)
|
|
217
217
|
})
|
|
218
218
|
|
|
219
|
-
|
|
219
|
+
it('state: port occupied → increment', () => {
|
|
220
220
|
// Given: base port 17878 occupied
|
|
221
221
|
// When: findFreePort(17878)
|
|
222
222
|
// Then: try 17879, 17880... until free
|
|
@@ -232,26 +232,26 @@ describe('Thread API — request paths', () => {
|
|
|
232
232
|
const PORT = 17878
|
|
233
233
|
const THREAD_ID = 'thr_test123'
|
|
234
234
|
|
|
235
|
-
|
|
235
|
+
it('POST /v1/threads — create thread', () => {
|
|
236
236
|
const path = `/v1/threads`
|
|
237
237
|
const url = `http://127.0.0.1:${PORT}${path}`
|
|
238
238
|
expect(path).toBe('/v1/threads')
|
|
239
239
|
expect(url).toContain(':17878')
|
|
240
240
|
})
|
|
241
241
|
|
|
242
|
-
|
|
242
|
+
it('POST /v1/threads/{id}/turns — send turn', () => {
|
|
243
243
|
const path = `/v1/threads/${THREAD_ID}/turns`
|
|
244
244
|
expect(path).toContain(THREAD_ID)
|
|
245
245
|
expect(path).toContain('/turns')
|
|
246
246
|
})
|
|
247
247
|
|
|
248
|
-
|
|
248
|
+
it('GET /v1/threads/{id}/events — SSE stream', () => {
|
|
249
249
|
const path = `/v1/threads/${THREAD_ID}/events`
|
|
250
250
|
const url = `http://127.0.0.1:${PORT}${path}?since_seq=0`
|
|
251
251
|
expect(url).toContain('since_seq=0')
|
|
252
252
|
})
|
|
253
253
|
|
|
254
|
-
|
|
254
|
+
it('GET /health — health check', () => {
|
|
255
255
|
const url = `http://127.0.0.1:${PORT}/health`
|
|
256
256
|
expect(url.endsWith('/health')).toBe(true)
|
|
257
257
|
})
|
|
@@ -260,7 +260,7 @@ describe('Thread API — request paths', () => {
|
|
|
260
260
|
// ── Lock file schema ────────────────────────────────────────────────────────
|
|
261
261
|
|
|
262
262
|
describe('ServeLock schema', () => {
|
|
263
|
-
|
|
263
|
+
it('valid lock matches expected shape', () => {
|
|
264
264
|
const lock = {
|
|
265
265
|
pid: 12345,
|
|
266
266
|
port: 17878,
|
|
@@ -275,7 +275,7 @@ describe('ServeLock schema', () => {
|
|
|
275
275
|
expect(typeof lock.threads).toBe('object')
|
|
276
276
|
})
|
|
277
277
|
|
|
278
|
-
|
|
278
|
+
it('threads must be string→string map', () => {
|
|
279
279
|
const threads: Record<string, string> = {}
|
|
280
280
|
threads['session-a'] = 'thr_111'
|
|
281
281
|
threads['session-b'] = 'thr_222'
|