返回源码地图

packages/sdk/client/tests/fake-runtime.ts

main snapshot · da00f7f5358f · 正文引用章节 14;完整原文可核对,不声称全文件人工逐行审计

完整原文供逐行核对;页面收录不代表每行都经过人工语义审核。MIT 许可见 许可证。

1#!/usr/bin/env node
2/**
3 * Scripted stand-in for the DeepSeek Harness SDK runtime, driven entirely by
4 * env vars — no model, no network, no harness imports. Speaks the runtime's
5 * newline-delimited JSON-RPC protocol on stdio: answers `initialize`,
6 * `session/prompt` (streaming scripted `session.event` notifications, then
7 * `session.finished`, then the response), and `shutdown`.
8 *
9 * Script vocabulary (all optional):
10 * - `FAKE_TEXT`: assistant text for each turn (default `hello from fake runtime`).
11 * - `FAKE_STATUS`: the `session.finished` status (default `ok`).
12 * - `FAKE_REASON_KIND`: the `session.finished` reason kind (default `completed`; `none` omits the reason).
13 * - `FAKE_ABORT_REASON_KIND`: nested cause for an `aborted` turn (default `user`).
14 * - `FAKE_SUBAGENT`: also emit a child session (subagent.started + child event + subagent.finished).
15 * - `FAKE_ECHO_CWD`: prefix the assistant text with the process cwd.
16 * - `FAKE_ECHO_ENV`: comma-separated env names to echo as `name=value` lines in the assistant text.
17 * - `FAKE_MALFORMED`: `initialize` returns `{}` (no serverInfo); `prompt` returns `{}` (no accepted).
18 * - `FAKE_MALFORMED_PROMPT`: `initialize` is normal; only `prompt` returns `{}` (no accepted).
19 * - `FAKE_INIT_ERROR`: `initialize` answers a JSON-RPC error response with code 7.
20 * - `FAKE_INIT_ERROR_ONCE_FILE`: fail `initialize` (code 7) only when this
21 * marker file does NOT exist yet, creating it — so the first runtime
22 * process fails the handshake and a respawned one succeeds (retry probe).
23 * - `FAKE_ECHO_CWD_IN_INIT`: reply `serverInfo.version` = this process's cwd
24 * (wire-visible spawn-cwd probe).
25 * - `FAKE_MALFORMED_EVENT`: the turn's `session.event` carries a number as
26 * the event; `FAKE_MALFORMED_MESSAGE`: assistant/message content is not an
27 * array; `FAKE_MESSAGE_WITHOUT_DATA`: assistant/message with no data
28 * member; `FAKE_MALFORMED_REASON`: the `turn/end` carries a bare reason
29 * (`1`), an aborted reason without its cause (`aborted`), an unknown abort
30 * cause (`abort-unknown`), a hook cause without its reason (`hook`), or no
31 * data member (`no-data`) for wire-validation probes.
32 * - `FAKE_EMPTY_MESSAGE`: record an empty assistant/message whose embedded
33 * stream contains only usage and max-tokens settlement.
34 * - `FAKE_HANG_INIT`: never answer `initialize` (mid-handshake cancel probe).
35 * - `FAKE_INIT_READY` + `FAKE_INIT_GO`: touch the READY file when `initialize`
36 * arrives, then poll for the GO file before answering (deterministic
37 * cancel-during-handshake window).
38 * - `FAKE_HANG_PROMPT`: never answer `session/prompt` (for timeout/dispose tests).
39 * - `FAKE_EXIT_DURING_PROMPT`: commit one interrupted assistant message, then
40 * exit 17 while the owned session run is waiting for its terminal state.
41 * - `FAKE_STREAM_THEN_MALFORMED`: commit a partial assistant attempt for the
42 * prompt, then answer `{}` (no accepted) — same-pipe ordering makes the
43 * attempt arrive before the protocol failure (partial-output retention probe).
44 * - `FAKE_IGNORE_EOF` + `FAKE_SIGTERM_FILE`: keep running after stdin EOF; touch the file on SIGTERM (ladder probe).
45 * - `FAKE_TRAP_SIGTERM`: with `FAKE_IGNORE_EOF`, survive SIGTERM too (SIGKILL-rung probe).
46 * - `FAKE_EXIT_BEFORE_INIT`: exit 3 immediately (spawn-then-die probe).
47 * - `FAKE_STDERR`: write this line to stderr at boot (diagnostics-tail probe).
48 * - `FAKE_STDERR_NO_NEWLINE`: write this to stderr WITHOUT a newline (buffer-flush probe).
49 * - `FAKE_RECORD_INIT`: append each `initialize` params JSON to this file (handshake probe).
50 */
51
52import { appendFileSync, existsSync, writeFileSync } from 'node:fs'
53import process from 'node:process'
54import { createInterface } from 'node:readline'
55
56const env = process.env
57
58if (env.FAKE_STDERR !== undefined) process.stderr.write(`${env.FAKE_STDERR}\n`)
59if (env.FAKE_STDERR_NO_NEWLINE !== undefined) process.stderr.write(env.FAKE_STDERR_NO_NEWLINE)
60if (env.FAKE_EXIT_BEFORE_INIT !== undefined) process.exit(3)
61
62if (env.FAKE_IGNORE_EOF !== undefined) {
63 // Simulate a runtime that never quiesces from EOF so the dispose ladder
64 // must escalate; record which rung fired.
65 process.stdin.resume()
66 process.stdin.on('end', () => { setInterval(() => {}, 1_000) })
67 process.on('SIGTERM', () => {
68 if (env.FAKE_SIGTERM_FILE !== undefined) writeFileSync(env.FAKE_SIGTERM_FILE, 'sigterm\n')
69 if (env.FAKE_TRAP_SIGTERM === undefined) process.exit(0)
70 })
71}
72
73function write(message: object): void {
74 process.stdout.write(`${JSON.stringify(message)}\n`)
75}
76
77function notify(method: string, params: object): void {
78 write({ jsonrpc: '2.0', method, params })
79}
80
81let seq = 0
82function event(sessionId: string, type: string, data: object): void {
83 notify('session.event', { sessionId, event: { type, seq: seq++, time: 0, data } })
84}
85
86function assistantText(): string {
87 const parts: string[] = []
88 if (env.FAKE_ECHO_CWD !== undefined) parts.push(`cwd=${process.cwd()}`)
89 for (const name of (env.FAKE_ECHO_ENV ?? '').split(',').filter(entry => entry.length > 0)) {
90 parts.push(`${name}=${env[name] ?? ''}`)
91 }
92 parts.push(env.FAKE_TEXT ?? 'hello from fake runtime')
93 return parts.join('\n')
94}
95
96function textStream(text: string): object[] {
97 return [
98 { type: 'chunk', time: 0, chunk: { type: 'block-start', index: 0, blockType: 'text' } },
99 { type: 'text-chunks', time0: 0, index: 0, dt: [], texts: [text] },
100 { type: 'chunk', time: 0, chunk: { type: 'block-end', index: 0, block: { type: 'text', text } } },
101 { type: 'chunk', time: 0, chunk: { type: 'finish', reason: { kind: 'stop' } } },
102 ]
103}
104
105function usageOnlyStream(): object[] {
106 return [
107 {
108 type: 'chunk',
109 time: 0,
110 chunk: { type: 'usage', usage: { inputTokens: 1, outputTokens: 0, totalTokens: 1 } },
111 },
112 { type: 'chunk', time: 0, chunk: { type: 'finish', reason: { kind: 'max-tokens' } } },
113 ]
114}
115
116function runTurn(sessionId: string): void {
117 const text = assistantText()
118 if (env.FAKE_MALFORMED_EVENT !== undefined) {
119 notify('session.event', { sessionId, event: 42 })
120 return
121 }
122 event(sessionId, 'turn/start', { turn: 0 })
123 if (env.FAKE_MALFORMED_MESSAGE !== undefined) {
124 event(sessionId, 'assistant/attempt', {
125 turn: 0,
126 step: 0,
127 stream: textStream(text),
128 })
129 event(sessionId, 'assistant/message', {
130 turn: 0,
131 step: 0,
132 message: {
133 id: 'fake-malformed-message',
134 role: 'assistant',
135 content: 'not-an-array',
136 source: { kind: 'model', provider: 'fake', model: 'fake' },
137 },
138 stream: textStream(text),
139 })
140 return
141 }
142 if (env.FAKE_MESSAGE_WITHOUT_DATA !== undefined) {
143 notify('session.event', { sessionId, event: { type: 'assistant/message', seq: seq++, time: 0 } })
144 return
145 }
146 if (env.FAKE_EMPTY_MESSAGE !== undefined) {
147 event(sessionId, 'assistant/attempt', {
148 turn: 0,
149 step: 0,
150 stream: textStream(text),
151 })
152 }
153 event(sessionId, 'assistant/message', {
154 turn: 0,
155 step: 0,
156 message: {
157 id: `fake-assistant-${seq}`,
158 role: 'assistant',
159 // Model the usage-only message recorded after a max-tokens step that
160 // assembled no output blocks.
161 content: env.FAKE_EMPTY_MESSAGE !== undefined ? [] : [{ type: 'text', text }],
162 source: { kind: 'model', provider: 'fake', model: 'fake' },
163 },
164 stream: env.FAKE_EMPTY_MESSAGE !== undefined ? usageOnlyStream() : textStream(text),
165 })
166 const reasonKind = env.FAKE_REASON_KIND ?? 'completed'
167 if (reasonKind !== 'none') {
168 if (env.FAKE_MALFORMED_REASON === 'no-data') {
169 notify('session.event', { sessionId, event: { type: 'turn/end', seq: seq++, time: 0 } })
170 return
171 }
172 const reason = env.FAKE_MALFORMED_REASON === 'aborted'
173 ? { kind: 'aborted' }
174 : env.FAKE_MALFORMED_REASON === 'abort-unknown'
175 ? { kind: 'aborted', reason: { kind: 'future' } }
176 : env.FAKE_MALFORMED_REASON === 'hook'
177 ? { kind: 'aborted', reason: { kind: 'hook' } }
178 : env.FAKE_MALFORMED_REASON !== undefined
179 ? 'not-a-reason-envelope'
180 : reasonKind === 'aborted'
181 ? {
182 kind: 'aborted',
183 reason: env.FAKE_ABORT_REASON_KIND === 'hook'
184 ? { kind: 'hook', reason: 'scripted hook abort' }
185 : { kind: env.FAKE_ABORT_REASON_KIND ?? 'user' },
186 }
187 : reasonKind === 'error'
188 ? { kind: 'error', error: { message: 'scripted child error', code: 'UNKNOWN' } }
189 : { kind: reasonKind }
190 event(sessionId, 'turn/end', { turn: 0, reason })
191 }
192 if (env.FAKE_SUBAGENT !== undefined) {
193 const childId = `${sessionId}-child`
194 notify('subagent.started', { parentSessionId: sessionId, childSessionId: childId })
195 event(childId, 'assistant/message', {
196 turn: 0,
197 step: 0,
198 message: {
199 id: `fake-child-${seq}`,
200 role: 'assistant',
201 content: [{ type: 'text', text: 'child says hi' }],
202 source: { kind: 'model', provider: 'fake', model: 'fake' },
203 },
204 stream: textStream('child says hi'),
205 })
206 notify('subagent.finished', {
207 provider: 'spawn',
208 agentId: childId,
209 parentSessionId: sessionId,
210 childSessionId: childId,
211 status: 'ok',
212 stopReason: 'completed',
213 lastAssistantMessage: [{ type: 'text', text: 'child says hi' }],
214 })
215 }
216}
217
218function sessionIdOf(params: Record<string, unknown> | undefined): string {
219 const value = params?.sessionId
220 return typeof value === 'string' ? value : ''
221}
222
223const reader = createInterface({ input: process.stdin })
224reader.on('line', (line) => {
225 if (line.trim().length === 0) return
226 const frame = JSON.parse(line) as { id?: string | number; method?: string; params?: Record<string, unknown> }
227 if (frame.method === undefined || frame.id === undefined) return
228 const respond = (result: object): void => { write({ jsonrpc: '2.0', id: frame.id, result }) }
229 switch (frame.method) {
230 case 'initialize':
231 if (env.FAKE_RECORD_INIT !== undefined) appendFileSync(env.FAKE_RECORD_INIT, `${JSON.stringify(frame.params)}\n`)
232 if (env.FAKE_HANG_INIT !== undefined) return
233 if (env.FAKE_INIT_READY !== undefined && env.FAKE_INIT_GO !== undefined) {
234 writeFileSync(env.FAKE_INIT_READY, 'ready\n')
235 const go = env.FAKE_INIT_GO
236 const id = frame.id
237 const poll = setInterval(() => {
238 if (!existsSync(go)) return
239 clearInterval(poll)
240 write({ jsonrpc: '2.0', id, result: { serverInfo: { name: 'deepseek-harness-sdk-runtime', version: '0.0.1' } } })
241 }, 5)
242 return
243 }
244 if (env.FAKE_INIT_ERROR !== undefined) {
245 write({ jsonrpc: '2.0', id: frame.id, error: { code: 7, message: 'scripted init failure', data: { hint: 'fake' } } })
246 return
247 }
248 if (env.FAKE_INIT_ERROR_ONCE_FILE !== undefined && !existsSync(env.FAKE_INIT_ERROR_ONCE_FILE)) {
249 writeFileSync(env.FAKE_INIT_ERROR_ONCE_FILE, 'failed-once\n')
250 write({ jsonrpc: '2.0', id: frame.id, error: { code: 7, message: 'scripted first-boot failure' } })
251 return
252 }
253 if (env.FAKE_MALFORMED !== undefined) {
254 respond({})
255 return
256 }
257 if (env.FAKE_ECHO_CWD_IN_INIT !== undefined) {
258 respond({ serverInfo: { name: 'deepseek-harness-sdk-runtime', version: process.cwd() } })
259 return
260 }
261 respond({ serverInfo: { name: 'deepseek-harness-sdk-runtime', version: '0.0.1' } })
262 return
263 case 'session/prompt': {
264 const sessionId = sessionIdOf(frame.params)
265 const messageId = `fake-user-${seq}`
266 event(sessionId, 'agent/inbox/spliced', {
267 target: 'next-turn',
268 start: 0,
269 inserted: [{
270 id: messageId,
271 role: 'user',
272 content: [],
273 source: { kind: 'user' },
274 }],
275 })
276 notify('session.status', { sessionId, status: 'running' })
277 if (env.FAKE_STREAM_THEN_MALFORMED !== undefined) {
278 event(sessionId, 'assistant/attempt', {
279 turn: 0,
280 step: 0,
281 stream: textStream('streamed then cut short'),
282 })
283 respond({})
284 return
285 }
286 if (env.FAKE_EXIT_DURING_PROMPT !== undefined) {
287 const partial = env.FAKE_TEXT ?? 'partial before exit'
288 respond({ messageId })
289 event(sessionId, 'assistant/message', {
290 turn: 0,
291 step: 0,
292 message: {
293 id: `fake-partial-${seq}`,
294 role: 'assistant',
295 content: [{ type: 'text', text: partial }],
296 source: { kind: 'model', provider: 'fake', model: 'fake' },
297 },
298 stream: textStream(partial),
299 interrupted: true,
300 })
301 setImmediate(() => { process.exit(17) })
302 return
303 }
304 if (env.FAKE_HANG_PROMPT !== undefined) return
305 if (env.FAKE_MALFORMED !== undefined || env.FAKE_MALFORMED_PROMPT !== undefined) {
306 respond({})
307 return
308 }
309 runTurn(sessionId)
310 notify('session.status', { sessionId, status: 'idle' })
311 respond({ messageId })
312 return
313 }
314 case 'shutdown':
315 respond({})
316 // An EOF-ignoring fake also refuses the protocol exit, so the client's
317 // dispose ladder (not this cooperative path) must reap it.
318 if (env.FAKE_IGNORE_EOF === undefined) setImmediate(() => process.exit(0))
319 return
320 default:
321 write({ jsonrpc: '2.0', id: frame.id, error: { code: -32603, message: `unknown method: ${frame.method}` } })
322 }
323})