-
Notifications
You must be signed in to change notification settings - Fork 9.7k
Expand file tree
/
Copy pathfake-vela.mjs
More file actions
executable file
·484 lines (465 loc) · 18.3 KB
/
Copy pathfake-vela.mjs
File metadata and controls
executable file
·484 lines (465 loc) · 18.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
#!/usr/bin/env node
/**
* Fake vela CLI used by AMR integration tests. Routes by the first argv:
*
* `vela model preset --format json` → prints the local AMR picker seed.
* `vela model list --all --format json` → prints the authoritative remote
* AMR model catalog.
*
* `vela login` → writes ~/.amr/config.json (the
* active VELA_PROFILE only) and
* exits 0. Mirrors the real
* device-authorization flow's
* on-disk side-effect without the
* interactive browser approval —
* tests for Open Design's daemon
* login route only care that the
* config file appears.
*
* `vela models` → prints production-shaped public
* model ids from the Vela catalog.
*
* `vela agent run --runtime opencode` → ACP stdio runtime. Speaks just
* enough of the protocol to drive
* Open Design's `detectAcpModels`
* and `attachAcpSession` through a
* complete turn:
*
* initialize → { protocolVersion, agentCapabilities, models }
* session/new → { sessionId, models: { currentModelId, availableModels } }
* session/set_model → {}
* session/prompt → emits session/update notifications, then
* { stopReason: 'end_turn', usage }
*
* Behaviour can be tweaked through env vars set by the test:
* FAKE_VELA_SESSION_ID – session id returned by session/new
* FAKE_VELA_TEXT – assistant text streamed back to the host
* FAKE_VELA_THOUGHT – optional thought chunk streamed before text
* FAKE_VELA_LOGIN_DELAY_MS – delay before writing config.json on `login`
* so tests can observe the in-flight state
* FAKE_VELA_LOGIN_USER_EMAIL – email written into the saved profile
* FAKE_VELA_LOGIN_USER_PLAN – plan written into the saved profile
* FAKE_VELA_SESSION_NEW_ERROR – when set, session/new returns a JSON-RPC error
* FAKE_VELA_SET_MODEL_ERROR – when set, session/set_model returns a JSON-RPC error
* FAKE_VELA_PROMPT_ERROR – when set, session/prompt returns a JSON-RPC error
* FAKE_VELA_MODELS – newline-separated `vela models` stdout
* FAKE_VELA_MODEL_PRESET_JSON – JSON stdout for `model preset --format json`
* FAKE_VELA_MODEL_LIST_JSON – JSON stdout for `model list --all --format json`
* FAKE_VELA_REQUIRE_SET_MODEL – strict gate (default on); set to '0' to
* accept session/prompt without prior
* session/set_model (legacy behaviour)
*/
import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { dirname, join } from 'node:path';
import { argv, stdin, stdout, stderr, env, exit } from 'node:process';
const SESSION_ID = env.FAKE_VELA_SESSION_ID || 'fake-vela-session-1';
// Durable upstream (OpenCode) session handle reported on session/new and
// session/load — the value the daemon captures and replays to resume.
const OPENCODE_SESSION_ID = env.FAKE_VELA_OPENCODE_SESSION_ID || 'oc-fake-1';
// When set, a resumed session/prompt fails with the structured resume_failed
// error (modelling vela's pre-prompt probe finding the session gone).
const RESUME_FAILED = env.FAKE_VELA_RESUME_FAILED || '';
const ASSISTANT_TEXT = Object.prototype.hasOwnProperty.call(env, 'FAKE_VELA_TEXT')
? env.FAKE_VELA_TEXT
: 'Hello from fake vela.';
const THOUGHT_TEXT = env.FAKE_VELA_THOUGHT || '';
const SESSION_NEW_ERROR = env.FAKE_VELA_SESSION_NEW_ERROR || '';
const SET_MODEL_ERROR = env.FAKE_VELA_SET_MODEL_ERROR || '';
const PROMPT_ERROR = env.FAKE_VELA_PROMPT_ERROR || '';
const AVAILABLE_MODELS = [
{ modelId: 'openai/gpt-5.4-mini', name: 'gpt-5.4-mini' },
{ modelId: 'anthropic/claude-3.7-sonnet', name: 'claude-3.7-sonnet' },
];
const DEFAULT_MODELS_STDOUT = [
'public_model_deepseek_v3_2 vela',
'public_model_deepseek_v4_flash vela',
'public_model_deepseek_v4_pro vela',
'public_model_gemini_2_5_flash vela',
'public_model_gemini_3_1_flash_lite_preview vela',
'public_model_gemini_3_1_pro_preview vela',
'public_model_gpt_5_4 vela',
'public_model_gpt_5_4_mini vela',
'public_model_glm_5 vela',
'public_model_glm_5_1 vela',
'public_model_gpt_image_2 vela',
'public_model_kimi_k2_6 vela',
'public_model_minimax_m2_7 vela',
'public_model_qwen3_235b_a22b vela',
'public_model_seedance_2 vela',
].join('\n');
const DEFAULT_MODEL_PRESET_JSON = JSON.stringify({
source: 'preset',
data: [
{ id: 'deepseek-v4-flash' },
{ id: 'deepseek-v3.2' },
{ id: 'glm-5.1' },
{ id: 'gemini-2.5-flash' },
],
});
const DEFAULT_MODEL_LIST_JSON = JSON.stringify({
source: 'remote',
data: [
{ id: 'deepseek-v3.2' },
{ id: 'deepseek-v4-flash' },
{ id: 'deepseek-v4-pro' },
{ id: 'gemini-2.5-flash' },
{ id: 'gemini-3.1-flash-lite-preview' },
{ id: 'gemini-3.1-pro-preview' },
{ id: 'gpt-5.4' },
{ id: 'gpt-5.4-mini' },
{ id: 'glm-5' },
{ id: 'glm-5.1' },
{ id: 'gpt-image-2' },
{ id: 'kimi-k2.6' },
{ id: 'minimax-m2.7' },
{ id: 'qwen3-235b-a22b' },
{ id: 'seedance-2' },
],
});
// Real `vela agent run --runtime opencode` rejects session/prompt until
// session/set_model has been called for the current session — see the
// AMR runtime def docblock and the integration test for the negative case.
// The stub mirrors that contract so a regression in attachAcpSession that
// silently skips set_model for AMR turns is caught here, not in production.
let currentModelId = null;
const sessionsWithModel = new Set();
const STRICT_SET_MODEL = process.env.FAKE_VELA_REQUIRE_SET_MODEL !== '0';
// Whether THIS process bound a resumed upstream session via session/load.
// vela spawns one process per turn, so a fresh session/new turn and a resumed
// session/load turn are distinct processes — `didLoad` lets the RESUME_FAILED
// branch fire ONLY on the resume turn (mirroring a session that vanished
// upstream), so a daemon that clears the dead handle and reseeds with a fresh
// session/new on the next turn recovers instead of failing forever.
let didLoad = false;
function writeMessage(obj) {
stdout.write(`${JSON.stringify(obj)}\n`);
}
function writeResult(id, result) {
writeMessage({ jsonrpc: '2.0', id, result });
}
function writeNotification(method, params) {
writeMessage({ jsonrpc: '2.0', method, params });
}
function writeError(id, message, code = -32603) {
writeMessage({
jsonrpc: '2.0',
id,
error: { code, message },
});
}
function logDiag(line) {
stderr.write(`[fake-vela] ${line}\n`);
}
// Append one line per session-bind method (`new` / `load`) to the file named by
// FAKE_VELA_INVOCATION_LOG, so a multi-turn server test can assert the resume
// sequence across the separate per-turn vela processes (e.g. ['new','load','new']).
function logInvocation(method) {
const file = env.FAKE_VELA_INVOCATION_LOG;
if (!file) return;
try {
appendFileSync(file, `${JSON.stringify({ method })}\n`);
} catch {
/* best-effort diagnostics only */
}
}
function emitSessionUpdates(sessionId) {
if (THOUGHT_TEXT) {
writeNotification('session/update', {
sessionId,
update: {
sessionUpdate: 'agent_thought_chunk',
content: { type: 'text', text: THOUGHT_TEXT },
},
});
}
const chunks = ASSISTANT_TEXT.match(/.{1,16}/gs) || [ASSISTANT_TEXT];
for (const chunk of chunks) {
writeNotification('session/update', {
sessionId,
update: {
sessionUpdate: 'agent_message_chunk',
content: { type: 'text', text: chunk },
},
});
}
}
function handleMessage(msg) {
if (!msg || typeof msg !== 'object') return;
const { id, method, params } = msg;
switch (method) {
case 'initialize':
writeResult(id, {
protocolVersion: 1,
agentCapabilities: { promptCapabilities: { embeddedContext: false } },
models: {
currentModelId,
availableModels: AVAILABLE_MODELS,
},
});
return;
case 'session/new':
logInvocation('new');
if (SESSION_NEW_ERROR) {
writeError(id, SESSION_NEW_ERROR);
return;
}
writeResult(id, {
sessionId: SESSION_ID,
// FAKE_VELA_OMIT_OPENCODE_SESSION_ID models an older vela (or a handshake
// that never surfaced the durable handle): the daemon captures a null
// handle, which must CLEAR the row so the next turn opens a fresh session
// instead of resuming a non-existent one.
...(env.FAKE_VELA_OMIT_OPENCODE_SESSION_ID ? {} : { openCodeSessionId: OPENCODE_SESSION_ID }),
models: {
currentModelId,
availableModels: AVAILABLE_MODELS,
},
});
return;
case 'session/load': {
// Resume: bind the prior upstream session, echoing back the durable
// handle. (vela validates existence before the first prompt, so a missing
// session surfaces as resume_failed on session/prompt, not here.)
const durable = typeof params?.sessionId === 'string' ? params.sessionId : OPENCODE_SESSION_ID;
logInvocation('load');
didLoad = true;
writeResult(id, { sessionId: SESSION_ID, openCodeSessionId: durable });
return;
}
case 'session/set_model': {
if (SET_MODEL_ERROR) {
writeError(id, SET_MODEL_ERROR, -32099);
return;
}
const next = typeof params?.modelId === 'string' ? params.modelId.trim() : '';
const sessionId = typeof params?.sessionId === 'string' ? params.sessionId : SESSION_ID;
if (next) currentModelId = next;
sessionsWithModel.add(sessionId);
writeResult(id, {});
return;
}
case 'session/set_config_option': {
const sessionId = typeof params?.sessionId === 'string' ? params.sessionId : SESSION_ID;
// Treat config-option model selection as set_model for the purposes of
// the strict-set_model gate so adapters that go through the
// configOptions branch are not penalized.
sessionsWithModel.add(sessionId);
writeResult(id, {});
return;
}
case 'session/prompt': {
if (RESUME_FAILED && didLoad) {
// Structured resume-miss: the resumed session is gone. Mirrors vela's
// pre-prompt probe emitting resume_failed BEFORE any model call. Gated on
// `didLoad` so it fires only on a resume turn — a fresh session/new turn
// (e.g. the daemon reseeding after it cleared the dead handle) succeeds.
writeMessage({
jsonrpc: '2.0',
id,
error: {
code: -32600,
message: 'the resumed session could not be loaded',
data: { kind: 'resume_failed', phase: 'session_load', retryable: true },
},
});
return;
}
if (PROMPT_ERROR) {
writeError(id, PROMPT_ERROR, -32602);
return;
}
const sessionId = typeof params?.sessionId === 'string' ? params.sessionId : SESSION_ID;
if (STRICT_SET_MODEL && !sessionsWithModel.has(sessionId)) {
writeError(id, 'session/set_model must be called before session/prompt', -32602);
return;
}
emitSessionUpdates(sessionId);
writeResult(id, {
stopReason: 'end_turn',
usage: { inputTokens: 12, outputTokens: 7, totalTokens: 19 },
});
return;
}
case 'session/cancel':
logDiag('session/cancel received');
return;
default:
if (typeof id !== 'undefined') {
writeMessage({
jsonrpc: '2.0',
id,
error: { code: -32601, message: `unknown method: ${method}` },
});
}
return;
}
}
let buffer = '';
stdin.setEncoding('utf8');
stdin.on('data', (chunk) => {
buffer += chunk;
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const raw of lines) {
const line = raw.trim();
if (!line) continue;
let parsed;
try {
parsed = JSON.parse(line);
} catch (err) {
logDiag(`bad json on stdin: ${err instanceof Error ? err.message : String(err)}`);
continue;
}
handleMessage(parsed);
}
});
stdin.on('end', () => {
if (argv[2] === 'login') return;
stdout.end();
// Mirror real ACP runtimes that exit on EOF so the host's child.on('close')
// fires promptly and the chat run can finalize.
process.exit(0);
});
// `vela login`: the daemon's /api/integrations/vela/login route spawns this
// without expecting any ACP traffic. Real vela goes through a device-auth
// loop and writes ~/.amr/config.json on success; the stub skips the loop
// and just writes the file so Open Design's status reader and AmrLoginPill
// poller see the same on-disk projection production produces. The stdin EOF
// handler above ignores login mode so delayed login tests can keep this
// process alive without opening the ACP stdio bridge.
function loginAndExit() {
if (env.FAKE_VELA_LOGIN_FAIL) {
stderr.write(`${env.FAKE_VELA_LOGIN_FAIL}\n`);
exit(1);
}
// Models a host whose direct amr-api device-authorization path is broken
// (#3726): fail unless the daemon routed login through its IPv4 API proxy
// (which sets VELA_API_URL). Lets tests assert the direct-first / proxy-
// fallback contract of the login route.
if (
env.FAKE_VELA_LOGIN_FAIL_WITHOUT_API_URL &&
!(env.VELA_API_URL ?? '').trim()
) {
// FAKE_VELA_LOGIN_FAIL_WITHOUT_API_URL_DELAY_MS models a direct attempt that
// survives the daemon's 250ms startup grace and only then errors out before
// printing an activation URL — the pre-activation failure the proxy fallback
// must still catch.
const failDelayMs = Number(env.FAKE_VELA_LOGIN_FAIL_WITHOUT_API_URL_DELAY_MS) || 0;
if (failDelayMs > 0) {
setTimeout(() => {
stderr.write(`${env.FAKE_VELA_LOGIN_FAIL_WITHOUT_API_URL}\n`);
exit(1);
}, failDelayMs);
return;
}
stderr.write(`${env.FAKE_VELA_LOGIN_FAIL_WITHOUT_API_URL}\n`);
exit(1);
}
if (env.FAKE_VELA_ENV_DUMP_PATH) {
writeFileSync(env.FAKE_VELA_ENV_DUMP_PATH, JSON.stringify(env, null, 2), 'utf8');
}
const profile = (env.VELA_PROFILE || 'prod').trim() || 'prod';
const allowed = new Set(['prod', 'test', 'local']);
if (!allowed.has(profile)) {
stderr.write(`[fake-vela] unknown profile ${profile}; defaulting to prod\n`);
}
const profileName = allowed.has(profile) ? profile : 'prod';
const delayMs = Number(env.FAKE_VELA_LOGIN_DELAY_MS) || 0;
const userEmail = env.FAKE_VELA_LOGIN_USER_EMAIL || 'fake-user@example.com';
const userPlan = env.FAKE_VELA_LOGIN_USER_PLAN || 'free';
const finish = () => {
const file = join(homedir(), '.amr', 'config.json');
mkdirSync(dirname(file), { recursive: true });
const payload = {
profiles: {
[profileName]: {
controlKey: 'fake-control-key-0000000000000000000000',
runtimeKey: 'fake-runtime-key-0000000000000000000000',
apiUrl:
profileName === 'local' ? 'http://localhost:18080' : '',
linkUrl:
profileName === 'local' ? 'http://localhost:18081' : '',
user: {
id: 'fake-user-id',
email: userEmail,
name: 'Fake User',
plan: userPlan,
},
},
},
};
writeFileSync(file, JSON.stringify(payload, null, 2), 'utf8');
stdout.write(`Login successful for ${userEmail}.\n`);
exit(0);
};
// Print the device-auth activation block first (what real `vela login` emits
// and what the daemon's waitForActivation keys off to detect steady state),
// then write config after the optional delay so the in-flight window is real.
stdout.write('Open this URL to continue:\n');
stdout.write('https://fake-vela.example/cli/activate?deviceId=fake-device\n\n');
stdout.write('Code: FAKE-CODE\n');
if (delayMs > 0) setTimeout(finish, delayMs);
else finish();
}
// `vela --version`: the daemon's executable-resolution probe (def.versionArgs)
// expects a version string and a clean exit, NOT the ACP stdio loop.
if (argv[2] === '--version' || (argv.includes('--version') && argv[2] !== 'agent')) {
stdout.write('vela 0.0.0-fake\n');
exit(0);
}
if (argv[2] === 'login') {
loginAndExit();
}
if (argv[2] === 'models') {
stdout.write(`${env.FAKE_VELA_MODELS || DEFAULT_MODELS_STDOUT}\n`);
exit(0);
}
// `vela billing summary --format json` → live account projection.
// FAKE_VELA_BILLING_TIER – membershipTier (plan) in the JSON
// FAKE_VELA_BILLING_BALANCE_USD – balanceUsd in the JSON
// With neither set, behave as if billing is unavailable (exit 1) so the
// route's cold-cache fallback keeps returning config-only as before.
if (argv[2] === 'billing' && argv[3] === 'summary') {
if (env.FAKE_VELA_BILLING_LOG) {
appendFileSync(
env.FAKE_VELA_BILLING_LOG,
`${Date.now()}\t${env.VELA_RUNTIME_KEY || ''}\n`,
);
}
if (env.FAKE_VELA_BILLING_UNKNOWN_COMMAND) {
stderr.write('Error: unknown command "billing" for "vela"\n');
exit(1);
}
const delayMs = Number(env.FAKE_VELA_BILLING_DELAY_MS) || 0;
const finishBilling = () => {
const tier = env.FAKE_VELA_BILLING_TIER;
const balance = env.FAKE_VELA_BILLING_BALANCE_USD;
if (!tier && !balance) {
stderr.write('billing summary unavailable\n');
exit(1);
}
stdout.write(
`${JSON.stringify({
...(tier ? { membershipTier: tier } : {}),
balanceUsd: balance ?? null,
})}\n`,
);
exit(0);
};
if (delayMs > 0) {
setTimeout(finishBilling, delayMs);
} else {
finishBilling();
}
}
if (argv[2] === 'model' && argv.includes('--format') && argv.includes('json')) {
if (argv[3] === 'preset') {
stdout.write(`${env.FAKE_VELA_MODEL_PRESET_JSON || DEFAULT_MODEL_PRESET_JSON}\n`);
exit(0);
}
if (argv[3] === 'list') {
stdout.write(`${env.FAKE_VELA_MODEL_LIST_JSON || DEFAULT_MODEL_LIST_JSON}\n`);
exit(0);
}
}