@robota-sdk/agent-transport 3.0.0-beta.82 → 3.0.0-beta.84
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/CHANGELOG.md +795 -0
- package/README.md +80 -118
- package/dist/browser/client.d.ts +2 -2
- package/dist/browser/client.js +1 -1
- package/dist/browser/index.d.ts +87 -13
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/observer-messages-CEeisRaF.js +2 -0
- package/dist/browser/observer-messages-CEeisRaF.js.map +1 -0
- package/dist/browser/observer-messages-Dnff2Tdb.d.ts +471 -0
- package/dist/browser/observer-messages-Dnff2Tdb.d.ts.map +1 -0
- package/dist/node/index.cjs +1 -1
- package/dist/node/index.d.cts +308 -16
- package/dist/node/index.d.cts.map +1 -1
- package/dist/node/index.d.ts +308 -16
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +1 -1
- package/dist/node/index.js.map +1 -1
- package/dist/node/node.cjs +1 -1
- package/dist/node/node.d.cts +69 -1
- package/dist/node/node.d.cts.map +1 -1
- package/dist/node/node.d.ts +70 -2
- package/dist/node/node.d.ts.map +1 -1
- package/dist/node/node.js +1 -1
- package/dist/node/node.js.map +1 -1
- package/package.json +17 -14
- package/src/__tests__/bearer-resource-gate.test.ts +120 -0
- package/src/__tests__/history-attached-budget.test.ts +216 -0
- package/src/__tests__/message-decoders.test.ts +496 -6
- package/src/__tests__/project-read-messages.test.ts +118 -0
- package/src/__tests__/session-detail-loop-status.test.ts +169 -0
- package/src/__tests__/session-directory-messages.test.ts +322 -0
- package/src/__tests__/session-error-classification.test.ts +113 -0
- package/src/__tests__/session-history-wire.test.ts +342 -0
- package/src/__tests__/session-message-handler-observe.test.ts +217 -0
- package/src/__tests__/session-message-handler.test.ts +139 -4
- package/src/__tests__/settings-messages.test.ts +193 -0
- package/src/__tests__/ws-broadcast-events.test.ts +10 -0
- package/src/client.ts +9 -1
- package/src/index.ts +18 -2
- package/src/loop-control-messages.ts +34 -0
- package/src/message-decoders.ts +238 -6
- package/src/node/bearer-resource-gate.ts +291 -0
- package/src/node/index.ts +13 -0
- package/src/observer-messages.ts +47 -0
- package/src/open-prompts.ts +76 -0
- package/src/outbound-delivery.ts +11 -2
- package/src/project-read-messages.ts +74 -0
- package/src/protocol-session.ts +23 -1
- package/src/session-directory-messages.ts +162 -0
- package/src/session-error-classification.ts +91 -0
- package/src/session-events.ts +100 -19
- package/src/session-message-handler.ts +110 -15
- package/src/session-query-messages.ts +123 -5
- package/src/session-resume-bridge.ts +8 -0
- package/src/settings-messages.ts +103 -0
- package/src/wire-messages.ts +238 -11
- package/dist/browser/message-decoders-BB8n-H2X.d.ts +0 -252
- package/dist/browser/message-decoders-BB8n-H2X.d.ts.map +0 -1
- package/dist/browser/message-decoders-CDmDoZPR.js +0 -2
- package/dist/browser/message-decoders-CDmDoZPR.js.map +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SESSION_CHANGE_REFUSAL_CODES } from '@robota-sdk/agent-interface-session';
|
|
1
2
|
import { describe, expect, it } from 'vitest';
|
|
2
3
|
|
|
3
4
|
import {
|
|
@@ -18,11 +19,27 @@ import type { TClientMessage, TServerMessage } from '../wire-messages.js';
|
|
|
18
19
|
*/
|
|
19
20
|
const CLIENT_SAMPLES: Readonly<Record<TClientMessage['type'], TClientMessage>> = {
|
|
20
21
|
submit: { type: 'submit', prompt: 'hi' },
|
|
21
|
-
command: { type: 'command', name: 'help', args: 'x' },
|
|
22
|
+
command: { type: 'command', name: 'help', args: 'x', requestId: 'command-1' },
|
|
22
23
|
abort: { type: 'abort' },
|
|
23
24
|
'cancel-queue': { type: 'cancel-queue' },
|
|
24
25
|
'get-messages': { type: 'get-messages' },
|
|
26
|
+
'get-history': { type: 'get-history', fromIndex: 40 },
|
|
27
|
+
'get-prompts': { type: 'get-prompts' },
|
|
25
28
|
'get-context': { type: 'get-context' },
|
|
29
|
+
'get-commands': { type: 'get-commands' },
|
|
30
|
+
'get-status': { type: 'get-status' },
|
|
31
|
+
'list-models': { type: 'list-models', requestId: 'request-6' },
|
|
32
|
+
'get-agent-definitions': { type: 'get-agent-definitions', requestId: 'request-7' },
|
|
33
|
+
'list-sessions': { type: 'list-sessions', requestId: 'request-3' },
|
|
34
|
+
'new-session': { type: 'new-session', requestId: 'request-4' },
|
|
35
|
+
'switch-session': { type: 'switch-session', sessionId: 'session-2', requestId: 'request-5' },
|
|
36
|
+
'rename-session': {
|
|
37
|
+
type: 'rename-session',
|
|
38
|
+
sessionId: 'session-2',
|
|
39
|
+
name: 'Renamed',
|
|
40
|
+
requestId: 'request-6',
|
|
41
|
+
},
|
|
42
|
+
'delete-session': { type: 'delete-session', sessionId: 'session-2', requestId: 'request-7' },
|
|
26
43
|
'get-usage-report': { type: 'get-usage-report' },
|
|
27
44
|
'get-personal-usage-report': {
|
|
28
45
|
type: 'get-personal-usage-report',
|
|
@@ -38,6 +55,16 @@ const CLIENT_SAMPLES: Readonly<Record<TClientMessage['type'], TClientMessage>> =
|
|
|
38
55
|
'get-executing': { type: 'get-executing' },
|
|
39
56
|
'get-pending': { type: 'get-pending' },
|
|
40
57
|
'get-execution-workspace': { type: 'get-execution-workspace' },
|
|
58
|
+
'read-execution-detail': {
|
|
59
|
+
type: 'read-execution-detail',
|
|
60
|
+
requestId: 'detail-1',
|
|
61
|
+
entryId: 'main',
|
|
62
|
+
cursor: { offset: 20 },
|
|
63
|
+
},
|
|
64
|
+
'stop-waiting-loop': { type: 'stop-waiting-loop', requestId: 'loop-1' },
|
|
65
|
+
'project-status': { type: 'project-status', requestId: 'project-status-1' },
|
|
66
|
+
'project-diff': { type: 'project-diff', requestId: 'project-diff-1', path: 'a.txt' },
|
|
67
|
+
'project-memory': { type: 'project-memory', requestId: 'project-memory-1' },
|
|
41
68
|
'get-background-tasks': {
|
|
42
69
|
type: 'get-background-tasks',
|
|
43
70
|
filter: { kind: 'agent', includeClosed: true },
|
|
@@ -58,6 +85,12 @@ const CLIENT_SAMPLES: Readonly<Record<TClientMessage['type'], TClientMessage>> =
|
|
|
58
85
|
'ask-response': { type: 'ask-response', id: 'a1', response: { type: 'answer', values: ['y'] } },
|
|
59
86
|
resume: { type: 'resume', lastSeq: 4 },
|
|
60
87
|
ack: { type: 'ack', seq: 9 },
|
|
88
|
+
'get-settings': { type: 'get-settings', requestId: 'settings-1' },
|
|
89
|
+
'update-settings': {
|
|
90
|
+
type: 'update-settings',
|
|
91
|
+
requestId: 'settings-2',
|
|
92
|
+
patch: { field: 'outputStyle', styleId: 'concise' },
|
|
93
|
+
},
|
|
61
94
|
};
|
|
62
95
|
|
|
63
96
|
const SERVER_SAMPLES: Readonly<Record<TServerMessage['type'], TServerMessage>> = {
|
|
@@ -69,9 +102,93 @@ const SERVER_SAMPLES: Readonly<Record<TServerMessage['type'], TServerMessage>> =
|
|
|
69
102
|
complete: { type: 'complete', result: {} as never },
|
|
70
103
|
interrupted: { type: 'interrupted', result: {} as never },
|
|
71
104
|
error: { type: 'error', message: 'm' },
|
|
72
|
-
command_result: {
|
|
73
|
-
|
|
105
|
+
command_result: {
|
|
106
|
+
type: 'command_result',
|
|
107
|
+
name: 'n',
|
|
108
|
+
message: 'm',
|
|
109
|
+
success: true,
|
|
110
|
+
requestId: 'command-1',
|
|
111
|
+
},
|
|
112
|
+
// #3288 §2: `display` is optional (an older host may not send it) and foreign-owned
|
|
113
|
+
// (`IHistoryDisplaySegment`, `agent-interface-session`) — checked shallowly here, same as
|
|
114
|
+
// `tool_start`'s `state`, per this file's own header.
|
|
115
|
+
messages: {
|
|
116
|
+
type: 'messages',
|
|
117
|
+
messages: [],
|
|
118
|
+
display: [
|
|
119
|
+
{ type: 'text', role: 'user', content: 'hi' },
|
|
120
|
+
{ type: 'tool', tool: { toolName: 'Bash', firstArg: 'ls', isRunning: false } as never },
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
history: {
|
|
124
|
+
type: 'history',
|
|
125
|
+
startIndex: 3,
|
|
126
|
+
total: 4,
|
|
127
|
+
entries: [
|
|
128
|
+
{
|
|
129
|
+
id: 'e1',
|
|
130
|
+
timestamp: '2026-09-26T01:02:03.004Z',
|
|
131
|
+
category: 'event',
|
|
132
|
+
type: 'skill-activation',
|
|
133
|
+
data: { name: 'review' },
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
},
|
|
74
137
|
context: { type: 'context', state: {} as never },
|
|
138
|
+
history_changed: { type: 'history_changed' },
|
|
139
|
+
turn_source: { type: 'turn_source', source: 'peer' },
|
|
140
|
+
commands: { type: 'commands', commands: [], skills: [] },
|
|
141
|
+
session_status: { type: 'session_status', status: {} as never },
|
|
142
|
+
model_list: {
|
|
143
|
+
type: 'model_list',
|
|
144
|
+
requestId: 'request-6',
|
|
145
|
+
groups: [
|
|
146
|
+
{ profileName: 'anthropic', providerLabel: 'Anthropic', models: [{ id: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6' }] },
|
|
147
|
+
],
|
|
148
|
+
currentProfile: 'anthropic',
|
|
149
|
+
currentModel: 'claude-sonnet-4-6',
|
|
150
|
+
},
|
|
151
|
+
agent_definitions: {
|
|
152
|
+
type: 'agent_definitions',
|
|
153
|
+
requestId: 'request-7',
|
|
154
|
+
agents: [
|
|
155
|
+
{ name: 'general-purpose', description: 'General-purpose task execution agent.', definedIn: 'Built-in' },
|
|
156
|
+
{ name: 'Explore', description: 'Read-only codebase exploration agent.', definedIn: 'Built-in' },
|
|
157
|
+
],
|
|
158
|
+
current: 'general-purpose',
|
|
159
|
+
},
|
|
160
|
+
sessions: { type: 'sessions', requestId: 'request-3', listing: {} as never },
|
|
161
|
+
sessions_error: {
|
|
162
|
+
type: 'sessions_error',
|
|
163
|
+
requestId: 'request-3',
|
|
164
|
+
code: 'list_failed',
|
|
165
|
+
message: 'unreadable store',
|
|
166
|
+
},
|
|
167
|
+
session_switched: { type: 'session_switched', event: { sessionId: 'session-2' } },
|
|
168
|
+
session_change_failed: {
|
|
169
|
+
type: 'session_change_failed',
|
|
170
|
+
code: 'prompt_pending',
|
|
171
|
+
message: 'Answer the pending prompt first.',
|
|
172
|
+
requestId: 'request-5',
|
|
173
|
+
},
|
|
174
|
+
session_renamed_in_list: {
|
|
175
|
+
type: 'session_renamed_in_list',
|
|
176
|
+
requestId: 'request-6',
|
|
177
|
+
sessionId: 'session-2',
|
|
178
|
+
name: 'Renamed',
|
|
179
|
+
},
|
|
180
|
+
session_rename_failed: {
|
|
181
|
+
type: 'session_rename_failed',
|
|
182
|
+
requestId: 'request-6',
|
|
183
|
+
message: 'No session session-2 in this workspace.',
|
|
184
|
+
},
|
|
185
|
+
session_deleted: { type: 'session_deleted', requestId: 'request-7', sessionId: 'session-2' },
|
|
186
|
+
session_delete_failed: {
|
|
187
|
+
type: 'session_delete_failed',
|
|
188
|
+
requestId: 'request-7',
|
|
189
|
+
code: 'live_elsewhere',
|
|
190
|
+
message: 'Another client is on this session.',
|
|
191
|
+
},
|
|
75
192
|
usage_report: { type: 'usage_report', report: {} as never },
|
|
76
193
|
personal_usage_report: {
|
|
77
194
|
type: 'personal_usage_report',
|
|
@@ -98,8 +215,48 @@ const SERVER_SAMPLES: Readonly<Record<TServerMessage['type'], TServerMessage>> =
|
|
|
98
215
|
message: 'not found',
|
|
99
216
|
},
|
|
100
217
|
executing: { type: 'executing', executing: false },
|
|
101
|
-
pending: { type: 'pending', pending: null },
|
|
218
|
+
pending: { type: 'pending', pending: null, pendingCount: 0 },
|
|
102
219
|
execution_workspace_event: { type: 'execution_workspace_event', snapshot: {} as never },
|
|
220
|
+
execution_detail: {
|
|
221
|
+
type: 'execution_detail',
|
|
222
|
+
requestId: 'detail-1',
|
|
223
|
+
page: { entryId: 'main', records: [], nextCursor: { offset: 20 } },
|
|
224
|
+
},
|
|
225
|
+
execution_detail_error: {
|
|
226
|
+
type: 'execution_detail_error',
|
|
227
|
+
requestId: 'detail-1',
|
|
228
|
+
message: 'Unknown entry: main',
|
|
229
|
+
},
|
|
230
|
+
waiting_loop_stop: {
|
|
231
|
+
type: 'waiting_loop_stop',
|
|
232
|
+
requestId: 'loop-1',
|
|
233
|
+
outcome: { kind: 'stopped', loopId: 'loop-a', message: 'Stopped loop-a.' },
|
|
234
|
+
},
|
|
235
|
+
project_status: {
|
|
236
|
+
type: 'project_status',
|
|
237
|
+
requestId: 'project-status-1',
|
|
238
|
+
result: {
|
|
239
|
+
kind: 'status',
|
|
240
|
+
branch: 'main',
|
|
241
|
+
unborn: false,
|
|
242
|
+
files: [{ path: 'a.txt', status: 'Modified', added: 1, removed: 2 }],
|
|
243
|
+
truncated: false,
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
project_diff: {
|
|
247
|
+
type: 'project_diff',
|
|
248
|
+
requestId: 'project-diff-1',
|
|
249
|
+
result: {
|
|
250
|
+
kind: 'diff',
|
|
251
|
+
diffLines: [{ type: 'add', text: 'new line', lineNumber: 3 }],
|
|
252
|
+
truncated: false,
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
project_memory: {
|
|
256
|
+
type: 'project_memory',
|
|
257
|
+
requestId: 'project-memory-1',
|
|
258
|
+
result: { kind: 'memory', content: '# Memory', path: '.robota/memory/MEMORY.md', truncated: false },
|
|
259
|
+
},
|
|
103
260
|
background_task_event: { type: 'background_task_event', event: {} as never },
|
|
104
261
|
background_job_group_event: { type: 'background_job_group_event', event: {} as never },
|
|
105
262
|
plan_event: { type: 'plan_event', event: {} as never },
|
|
@@ -122,8 +279,15 @@ const SERVER_SAMPLES: Readonly<Record<TServerMessage['type'], TServerMessage>> =
|
|
|
122
279
|
taskId: 't',
|
|
123
280
|
success: true,
|
|
124
281
|
},
|
|
125
|
-
protocol_error: { type: 'protocol_error', message: 'm' },
|
|
282
|
+
protocol_error: { type: 'protocol_error', message: 'm', requestId: 'command-1' },
|
|
126
283
|
resume_gap: { type: 'resume_gap' },
|
|
284
|
+
settings: { type: 'settings', requestId: 'settings-1', settings: {} as never },
|
|
285
|
+
settings_error: {
|
|
286
|
+
type: 'settings_error',
|
|
287
|
+
requestId: 'settings-1',
|
|
288
|
+
code: 'not_available',
|
|
289
|
+
message: 'Settings are not available on this host.',
|
|
290
|
+
},
|
|
127
291
|
};
|
|
128
292
|
|
|
129
293
|
const MALFORMED_CLIENT: ReadonlyArray<[string, unknown]> = [
|
|
@@ -170,6 +334,133 @@ const MALFORMED_CLIENT: ReadonlyArray<[string, unknown]> = [
|
|
|
170
334
|
['resume with a string lastSeq', { type: 'resume', lastSeq: '4' }],
|
|
171
335
|
['ack with NaN', { type: 'ack', seq: Number.NaN }],
|
|
172
336
|
['cancel-background-task with an empty taskId', { type: 'cancel-background-task', taskId: '' }],
|
|
337
|
+
['list-sessions without requestId', { type: 'list-sessions' }],
|
|
338
|
+
['list-models without requestId', { type: 'list-models' }],
|
|
339
|
+
['list-models with an empty requestId', { type: 'list-models', requestId: '' }],
|
|
340
|
+
['get-agent-definitions without requestId', { type: 'get-agent-definitions' }],
|
|
341
|
+
[
|
|
342
|
+
'get-agent-definitions with an empty requestId',
|
|
343
|
+
{ type: 'get-agent-definitions', requestId: '' },
|
|
344
|
+
],
|
|
345
|
+
['get-history from a negative index', { type: 'get-history', fromIndex: -1 }],
|
|
346
|
+
['get-history from a fractional index', { type: 'get-history', fromIndex: 1.5 }],
|
|
347
|
+
['command with an empty requestId', { type: 'command', name: 'n', requestId: '' }],
|
|
348
|
+
['switch-session with an empty sessionId', { type: 'switch-session', sessionId: '' }],
|
|
349
|
+
[
|
|
350
|
+
'switch-session with a numeric requestId',
|
|
351
|
+
{ type: 'switch-session', sessionId: 's', requestId: 1 },
|
|
352
|
+
],
|
|
353
|
+
['new-session with an empty requestId', { type: 'new-session', requestId: '' }],
|
|
354
|
+
['read-execution-detail without requestId', { type: 'read-execution-detail', entryId: 'main' }],
|
|
355
|
+
[
|
|
356
|
+
'read-execution-detail with an empty entryId',
|
|
357
|
+
{ type: 'read-execution-detail', requestId: 'r', entryId: '' },
|
|
358
|
+
],
|
|
359
|
+
[
|
|
360
|
+
'read-execution-detail with a negative offset',
|
|
361
|
+
{ type: 'read-execution-detail', requestId: 'r', entryId: 'main', cursor: { offset: -1 } },
|
|
362
|
+
],
|
|
363
|
+
[
|
|
364
|
+
'read-execution-detail with a string offset',
|
|
365
|
+
{ type: 'read-execution-detail', requestId: 'r', entryId: 'main', cursor: { offset: '2' } },
|
|
366
|
+
],
|
|
367
|
+
['stop-waiting-loop without requestId', { type: 'stop-waiting-loop' }],
|
|
368
|
+
['stop-waiting-loop with an empty requestId', { type: 'stop-waiting-loop', requestId: '' }],
|
|
369
|
+
['get-settings without requestId', { type: 'get-settings' }],
|
|
370
|
+
['get-settings with an empty requestId', { type: 'get-settings', requestId: '' }],
|
|
371
|
+
['update-settings without a patch', { type: 'update-settings', requestId: 'r' }],
|
|
372
|
+
[
|
|
373
|
+
'update-settings with an unknown patch field',
|
|
374
|
+
{ type: 'update-settings', requestId: 'r', patch: { field: 'nope' } },
|
|
375
|
+
],
|
|
376
|
+
[
|
|
377
|
+
'update-settings with a prototype-only patch field',
|
|
378
|
+
{ type: 'update-settings', requestId: 'r', patch: { field: 'toString' } },
|
|
379
|
+
],
|
|
380
|
+
[
|
|
381
|
+
'update-settings language patch with a numeric language',
|
|
382
|
+
{ type: 'update-settings', requestId: 'r', patch: { field: 'language', language: 1 } },
|
|
383
|
+
],
|
|
384
|
+
[
|
|
385
|
+
'update-settings outputStyle patch missing styleId',
|
|
386
|
+
{ type: 'update-settings', requestId: 'r', patch: { field: 'outputStyle' } },
|
|
387
|
+
],
|
|
388
|
+
[
|
|
389
|
+
'update-settings preset patch with an empty presetId',
|
|
390
|
+
{ type: 'update-settings', requestId: 'r', patch: { field: 'preset', presetId: '' } },
|
|
391
|
+
],
|
|
392
|
+
[
|
|
393
|
+
'update-settings permissionMode patch missing mode',
|
|
394
|
+
{ type: 'update-settings', requestId: 'r', patch: { field: 'permissionMode' } },
|
|
395
|
+
],
|
|
396
|
+
[
|
|
397
|
+
'update-settings sandbox patch with a string enabled',
|
|
398
|
+
{ type: 'update-settings', requestId: 'r', patch: { field: 'sandbox', enabled: 'true' } },
|
|
399
|
+
],
|
|
400
|
+
[
|
|
401
|
+
'update-settings removePermissionRule patch with an unknown kind',
|
|
402
|
+
{
|
|
403
|
+
type: 'update-settings',
|
|
404
|
+
requestId: 'r',
|
|
405
|
+
patch: { field: 'removePermissionRule', scope: 'user', kind: 'nope', pattern: 'Bash(*)' },
|
|
406
|
+
},
|
|
407
|
+
],
|
|
408
|
+
[
|
|
409
|
+
'update-settings removePermissionRule patch missing pattern',
|
|
410
|
+
{
|
|
411
|
+
type: 'update-settings',
|
|
412
|
+
requestId: 'r',
|
|
413
|
+
patch: { field: 'removePermissionRule', scope: 'user', kind: 'allow' },
|
|
414
|
+
},
|
|
415
|
+
],
|
|
416
|
+
[
|
|
417
|
+
'update-settings mcpServerEnabled patch missing serverId',
|
|
418
|
+
{ type: 'update-settings', requestId: 'r', patch: { field: 'mcpServerEnabled', enabled: true } },
|
|
419
|
+
],
|
|
420
|
+
[
|
|
421
|
+
'update-settings mcpServerEnabled patch with a string enabled',
|
|
422
|
+
{
|
|
423
|
+
type: 'update-settings',
|
|
424
|
+
requestId: 'r',
|
|
425
|
+
patch: { field: 'mcpServerEnabled', serverId: 'docs', enabled: 'true' },
|
|
426
|
+
},
|
|
427
|
+
],
|
|
428
|
+
[
|
|
429
|
+
'update-settings pluginEnabled patch with an empty pluginId',
|
|
430
|
+
{
|
|
431
|
+
type: 'update-settings',
|
|
432
|
+
requestId: 'r',
|
|
433
|
+
patch: { field: 'pluginEnabled', pluginId: '', enabled: true },
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
[
|
|
437
|
+
'update-settings installPlugin patch missing pluginId',
|
|
438
|
+
{ type: 'update-settings', requestId: 'r', patch: { field: 'installPlugin' } },
|
|
439
|
+
],
|
|
440
|
+
[
|
|
441
|
+
'update-settings uninstallPlugin patch with a numeric pluginId',
|
|
442
|
+
{ type: 'update-settings', requestId: 'r', patch: { field: 'uninstallPlugin', pluginId: 1 } },
|
|
443
|
+
],
|
|
444
|
+
[
|
|
445
|
+
'update-settings providerProfile patch missing profileName',
|
|
446
|
+
{ type: 'update-settings', requestId: 'r', patch: { field: 'providerProfile' } },
|
|
447
|
+
],
|
|
448
|
+
[
|
|
449
|
+
'update-settings providerProfile patch with an empty profileName',
|
|
450
|
+
{ type: 'update-settings', requestId: 'r', patch: { field: 'providerProfile', profileName: '' } },
|
|
451
|
+
],
|
|
452
|
+
[
|
|
453
|
+
'update-settings providerModel patch missing modelId',
|
|
454
|
+
{ type: 'update-settings', requestId: 'r', patch: { field: 'providerModel', profileName: 'anthropic' } },
|
|
455
|
+
],
|
|
456
|
+
[
|
|
457
|
+
'update-settings providerModel patch missing profileName',
|
|
458
|
+
{ type: 'update-settings', requestId: 'r', patch: { field: 'providerModel', modelId: 'claude-haiku-4-5' } },
|
|
459
|
+
],
|
|
460
|
+
[
|
|
461
|
+
'update-settings deleteProviderProfile patch missing profileName',
|
|
462
|
+
{ type: 'update-settings', requestId: 'r', patch: { field: 'deleteProviderProfile' } },
|
|
463
|
+
],
|
|
173
464
|
];
|
|
174
465
|
|
|
175
466
|
const MALFORMED_SERVER: ReadonlyArray<[string, unknown]> = [
|
|
@@ -182,13 +473,120 @@ const MALFORMED_SERVER: ReadonlyArray<[string, unknown]> = [
|
|
|
182
473
|
['thinking with a string flag', { type: 'thinking', isThinking: 'yes' }],
|
|
183
474
|
['messages with a non-array', { type: 'messages', messages: {} }],
|
|
184
475
|
['messages with a primitive entry', { type: 'messages', messages: ['x'] }],
|
|
185
|
-
['
|
|
476
|
+
['messages with a numeric driverId', { type: 'messages', messages: [], driverId: 12345 }],
|
|
477
|
+
['messages with a non-array display', { type: 'messages', messages: [], display: {} }],
|
|
478
|
+
['messages with a display holding a primitive entry', { type: 'messages', messages: [], display: ['x'] }],
|
|
479
|
+
['pending with a number', { type: 'pending', pending: 1, pendingCount: 1 }],
|
|
480
|
+
['pending with a negative count', { type: 'pending', pending: null, pendingCount: -1 }],
|
|
481
|
+
['pending with a fractional count', { type: 'pending', pending: 'p', pendingCount: 1.5 }],
|
|
186
482
|
['background_task with an array task', { type: 'background_task', taskId: 't', task: [] }],
|
|
187
483
|
[
|
|
188
484
|
'control result with an unknown action',
|
|
189
485
|
{ type: 'background_task_control_result', action: 'nuke', taskId: 't', success: true },
|
|
190
486
|
],
|
|
191
487
|
['command_result without success', { type: 'command_result', name: 'n', message: 'm' }],
|
|
488
|
+
[
|
|
489
|
+
'sessions_error with an unknown code',
|
|
490
|
+
{ type: 'sessions_error', requestId: 'r', code: 'nope', message: 'm' },
|
|
491
|
+
],
|
|
492
|
+
['session_switched without event', { type: 'session_switched' }],
|
|
493
|
+
['model_list without requestId', { type: 'model_list', groups: [], currentModel: 'm' }],
|
|
494
|
+
[
|
|
495
|
+
'model_list with a non-array groups',
|
|
496
|
+
{ type: 'model_list', requestId: 'r', groups: {}, currentModel: 'm' },
|
|
497
|
+
],
|
|
498
|
+
['model_list without currentModel', { type: 'model_list', requestId: 'r', groups: [] }],
|
|
499
|
+
[
|
|
500
|
+
'agent_definitions without requestId',
|
|
501
|
+
{ type: 'agent_definitions', agents: [], current: 'general-purpose' },
|
|
502
|
+
],
|
|
503
|
+
[
|
|
504
|
+
'agent_definitions with a non-array agents',
|
|
505
|
+
{ type: 'agent_definitions', requestId: 'r', agents: {}, current: 'general-purpose' },
|
|
506
|
+
],
|
|
507
|
+
[
|
|
508
|
+
'agent_definitions with an agent missing definedIn',
|
|
509
|
+
{
|
|
510
|
+
type: 'agent_definitions',
|
|
511
|
+
requestId: 'r',
|
|
512
|
+
agents: [{ name: 'a', description: 'd' }],
|
|
513
|
+
current: 'a',
|
|
514
|
+
},
|
|
515
|
+
],
|
|
516
|
+
['agent_definitions without current', { type: 'agent_definitions', requestId: 'r', agents: [] }],
|
|
517
|
+
[
|
|
518
|
+
'session_change_failed with an unknown code',
|
|
519
|
+
{ type: 'session_change_failed', code: 'nope', message: 'm' },
|
|
520
|
+
],
|
|
521
|
+
['session_change_failed without message', { type: 'session_change_failed', code: 'limit' }],
|
|
522
|
+
[
|
|
523
|
+
'session_change_failed with a numeric requestId',
|
|
524
|
+
{ type: 'session_change_failed', code: 'limit', message: 'm', requestId: 1 },
|
|
525
|
+
],
|
|
526
|
+
['history without entries', { type: 'history', startIndex: 0, total: 0 }],
|
|
527
|
+
['history without its place', { type: 'history', entries: [] }],
|
|
528
|
+
['history with a negative total', { type: 'history', startIndex: 0, total: -1, entries: [] }],
|
|
529
|
+
['history with a primitive entry', { type: 'history', startIndex: 0, total: 1, entries: ['x'] }],
|
|
530
|
+
[
|
|
531
|
+
'history with a numeric timestamp',
|
|
532
|
+
{
|
|
533
|
+
type: 'history',
|
|
534
|
+
startIndex: 0,
|
|
535
|
+
total: 1,
|
|
536
|
+
entries: [{ id: 'e', timestamp: 1, category: 'chat', type: 'user' }],
|
|
537
|
+
},
|
|
538
|
+
],
|
|
539
|
+
[
|
|
540
|
+
'history with an entry missing its category',
|
|
541
|
+
{
|
|
542
|
+
type: 'history',
|
|
543
|
+
startIndex: 0,
|
|
544
|
+
total: 1,
|
|
545
|
+
entries: [{ id: 'e', timestamp: 't', type: 'user' }],
|
|
546
|
+
},
|
|
547
|
+
],
|
|
548
|
+
[
|
|
549
|
+
'protocol_error with a numeric requestId',
|
|
550
|
+
{ type: 'protocol_error', message: 'm', requestId: 1 },
|
|
551
|
+
],
|
|
552
|
+
['turn_source with an unknown source', { type: 'turn_source', source: 'robot' }],
|
|
553
|
+
['turn_source without source', { type: 'turn_source' }],
|
|
554
|
+
['execution_detail without page', { type: 'execution_detail', requestId: 'r' }],
|
|
555
|
+
['execution_detail with an array page', { type: 'execution_detail', requestId: 'r', page: [] }],
|
|
556
|
+
['execution_detail without requestId', { type: 'execution_detail', page: {} }],
|
|
557
|
+
['execution_detail_error without message', { type: 'execution_detail_error', requestId: 'r' }],
|
|
558
|
+
['waiting_loop_stop without outcome', { type: 'waiting_loop_stop', requestId: 'r' }],
|
|
559
|
+
[
|
|
560
|
+
'waiting_loop_stop with an unknown kind',
|
|
561
|
+
{ type: 'waiting_loop_stop', requestId: 'r', outcome: { kind: 'paused' } },
|
|
562
|
+
],
|
|
563
|
+
[
|
|
564
|
+
'waiting_loop_stop stopped without loopId',
|
|
565
|
+
{ type: 'waiting_loop_stop', requestId: 'r', outcome: { kind: 'stopped' } },
|
|
566
|
+
],
|
|
567
|
+
[
|
|
568
|
+
'waiting_loop_stop several without message',
|
|
569
|
+
{ type: 'waiting_loop_stop', requestId: 'r', outcome: { kind: 'several' } },
|
|
570
|
+
],
|
|
571
|
+
[
|
|
572
|
+
'waiting_loop_stop failed with a numeric loopId',
|
|
573
|
+
{
|
|
574
|
+
type: 'waiting_loop_stop',
|
|
575
|
+
requestId: 'r',
|
|
576
|
+
outcome: { kind: 'failed', loopId: 1, message: 'm' },
|
|
577
|
+
},
|
|
578
|
+
],
|
|
579
|
+
[
|
|
580
|
+
'waiting_loop_stop with a prototype-only kind',
|
|
581
|
+
{ type: 'waiting_loop_stop', requestId: 'r', outcome: { kind: 'toString' } },
|
|
582
|
+
],
|
|
583
|
+
['settings without requestId', { type: 'settings', settings: {} }],
|
|
584
|
+
['settings with an array settings', { type: 'settings', requestId: 'r', settings: [] }],
|
|
585
|
+
[
|
|
586
|
+
'settings_error with an unknown code',
|
|
587
|
+
{ type: 'settings_error', requestId: 'r', code: 'nope', message: 'm' },
|
|
588
|
+
],
|
|
589
|
+
['settings_error without message', { type: 'settings_error', requestId: 'r', code: 'invalid' }],
|
|
192
590
|
];
|
|
193
591
|
|
|
194
592
|
describe('decodeClientMessage (issue #2045)', () => {
|
|
@@ -206,12 +604,42 @@ describe('decodeClientMessage (issue #2045)', () => {
|
|
|
206
604
|
expect(decodeClientMessage(value).ok).toBe(false);
|
|
207
605
|
});
|
|
208
606
|
|
|
607
|
+
// #3282 §4b: "Providers & Models" — Use, Model and Delete each add one `update-settings` patch
|
|
608
|
+
// field; `CLIENT_SAMPLES` above only round-trips one field (`outputStyle`) per variant, so these
|
|
609
|
+
// three get their own explicit round trip.
|
|
610
|
+
it.each([
|
|
611
|
+
{ field: 'providerProfile', profileName: 'backup' },
|
|
612
|
+
{ field: 'providerModel', profileName: 'backup', modelId: 'claude-haiku-4-5' },
|
|
613
|
+
{ field: 'deleteProviderProfile', profileName: 'backup' },
|
|
614
|
+
])('decodes an update-settings patch with field %j', (patch) => {
|
|
615
|
+
const message = { type: 'update-settings', requestId: 'r', patch };
|
|
616
|
+
expect(decodeClientMessage(JSON.parse(JSON.stringify(message)))).toEqual({
|
|
617
|
+
ok: true,
|
|
618
|
+
message,
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
|
|
209
622
|
it('keeps the protocol wording for a bad submit prompt', () => {
|
|
210
623
|
expect(decodeClientMessage({ type: 'submit', prompt: 1 })).toEqual({
|
|
211
624
|
ok: false,
|
|
212
625
|
reason: 'prompt must be a non-empty string',
|
|
213
626
|
});
|
|
214
627
|
});
|
|
628
|
+
|
|
629
|
+
it.each([
|
|
630
|
+
{ field: 'mcpServerEnabled', serverId: 'docs', enabled: true },
|
|
631
|
+
{ field: 'reloadMcpServers' },
|
|
632
|
+
{ field: 'pluginEnabled', pluginId: 'formatter@robota', enabled: false },
|
|
633
|
+
{ field: 'reloadPlugins' },
|
|
634
|
+
{ field: 'installPlugin', pluginId: 'linter@robota' },
|
|
635
|
+
{ field: 'uninstallPlugin', pluginId: 'formatter@robota' },
|
|
636
|
+
] as const)('accepts the $field settings patch (#3282 §4 part b-2)', (patch) => {
|
|
637
|
+
const message = { type: 'update-settings' as const, requestId: 'r', patch };
|
|
638
|
+
expect(decodeClientMessage(JSON.parse(JSON.stringify(message)))).toEqual({
|
|
639
|
+
ok: true,
|
|
640
|
+
message,
|
|
641
|
+
});
|
|
642
|
+
});
|
|
215
643
|
});
|
|
216
644
|
|
|
217
645
|
describe('decodeServerMessage (issue #2045)', () => {
|
|
@@ -228,6 +656,68 @@ describe('decodeServerMessage (issue #2045)', () => {
|
|
|
228
656
|
it.each(MALFORMED_SERVER)('refuses %s', (_label, value) => {
|
|
229
657
|
expect(decodeServerMessage(value).ok).toBe(false);
|
|
230
658
|
});
|
|
659
|
+
|
|
660
|
+
it('accepts every refusal code the session contracts declare, and a refusal without requestId', () => {
|
|
661
|
+
for (const code of SESSION_CHANGE_REFUSAL_CODES) {
|
|
662
|
+
expect(decodeServerMessage({ type: 'session_change_failed', code, message: 'm' }).ok).toBe(
|
|
663
|
+
true,
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it('accepts every waiting-loop stop outcome', () => {
|
|
669
|
+
for (const outcome of [
|
|
670
|
+
{ kind: 'none' },
|
|
671
|
+
{ kind: 'several', message: 'Use /loop stop <id>.' },
|
|
672
|
+
{ kind: 'stopped', loopId: 'loop-a' },
|
|
673
|
+
{ kind: 'failed', message: 'store unavailable' },
|
|
674
|
+
{ kind: 'failed', loopId: 'loop-a', message: 'store unavailable' },
|
|
675
|
+
]) {
|
|
676
|
+
expect(decodeServerMessage({ type: 'waiting_loop_stop', requestId: 'r', outcome }).ok).toBe(
|
|
677
|
+
true,
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
it('accepts a pending frame without its count, as a host from before the count sends it', () => {
|
|
683
|
+
// Refusing it would cut a newer client off from an older daemon on every prompt it queues.
|
|
684
|
+
expect(decodeFrame('{"type":"pending","pending":null}', decodeServerMessage)).toEqual({
|
|
685
|
+
ok: true,
|
|
686
|
+
message: { type: 'pending', pending: null },
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
it('accepts an error frame carrying its classification (#3289 §3), and one without it', () => {
|
|
691
|
+
const classified = {
|
|
692
|
+
type: 'error',
|
|
693
|
+
message: 'Authentication Error: invalid x-api-key',
|
|
694
|
+
code: 'rate_limit',
|
|
695
|
+
provider: 'anthropic',
|
|
696
|
+
retryAfterSeconds: 30,
|
|
697
|
+
};
|
|
698
|
+
expect(decodeServerMessage(classified)).toEqual({ ok: true, message: classified });
|
|
699
|
+
expect(decodeServerMessage({ type: 'error', message: 'boom' })).toEqual({
|
|
700
|
+
ok: true,
|
|
701
|
+
message: { type: 'error', message: 'boom' },
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it('accepts an error frame naming the model a model_unavailable failure tried', () => {
|
|
706
|
+
const classified = {
|
|
707
|
+
type: 'error',
|
|
708
|
+
message: 'No model available for provider "anthropic"',
|
|
709
|
+
code: 'model_unavailable',
|
|
710
|
+
provider: 'anthropic',
|
|
711
|
+
model: 'claude-x',
|
|
712
|
+
};
|
|
713
|
+
expect(decodeServerMessage(classified)).toEqual({ ok: true, message: classified });
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it('refuses an error frame with an unrecognized code', () => {
|
|
717
|
+
expect(
|
|
718
|
+
decodeServerMessage({ type: 'error', message: 'm', code: 'not-a-real-code' }).ok,
|
|
719
|
+
).toBe(false);
|
|
720
|
+
});
|
|
231
721
|
});
|
|
232
722
|
|
|
233
723
|
describe('decodeFrame — the one carrier path', () => {
|