@robota-sdk/agent-transport 3.0.0-beta.78 → 3.0.0-beta.81
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/README.md +84 -26
- package/dist/browser/client.d.ts +2 -0
- package/dist/browser/client.js +1 -0
- package/dist/browser/index.d.ts +520 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +2 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/message-decoders-BB8n-H2X.d.ts +252 -0
- package/dist/browser/message-decoders-BB8n-H2X.d.ts.map +1 -0
- package/dist/browser/message-decoders-CDmDoZPR.js +2 -0
- package/dist/browser/message-decoders-CDmDoZPR.js.map +1 -0
- package/dist/node/index.cjs +1 -1
- package/dist/node/index.d.cts +766 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +765 -27
- 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 -0
- package/dist/node/node.d.cts +109 -0
- package/dist/node/node.d.cts.map +1 -0
- package/dist/node/node.d.ts +109 -0
- package/dist/node/node.d.ts.map +1 -0
- package/dist/node/node.js +2 -0
- package/dist/node/node.js.map +1 -0
- package/package.json +68 -47
- package/src/__tests__/access-token-verifier.test.ts +672 -0
- package/src/__tests__/admission.test.ts +130 -0
- package/src/__tests__/channel-frames.test.ts +214 -0
- package/src/__tests__/driver-attribution-capability.test.ts +83 -0
- package/src/__tests__/handoff-chunking.test.ts +215 -0
- package/src/__tests__/handoff-manifest.test.ts +52 -0
- package/src/__tests__/message-decoders.test.ts +249 -0
- package/src/__tests__/outbound-delivery.test.ts +558 -0
- package/src/__tests__/peer-message-ledger.test.ts +200 -0
- package/src/__tests__/personal-usage-report.test.ts +193 -0
- package/src/__tests__/resume-buffer.test.ts +93 -0
- package/src/__tests__/session-event-delivery.test.ts +101 -0
- package/src/__tests__/session-message-handler-e5.test.ts +99 -0
- package/src/__tests__/session-message-handler.test.ts +590 -0
- package/src/__tests__/session-resume-bridge.test.ts +406 -0
- package/src/__tests__/submit-prompt-shape.test.ts +78 -0
- package/src/__tests__/usage-report-carrier.test.ts +98 -0
- package/src/__tests__/ws-broadcast-events.test.ts +84 -0
- package/src/background-messages.ts +169 -0
- package/src/channel-frames.ts +182 -0
- package/src/client.ts +9 -0
- package/src/handoff-chunking.ts +235 -0
- package/src/index.ts +112 -3
- package/src/message-decoders.ts +229 -0
- package/src/message-parser.ts +33 -0
- package/src/node/access-token-verifier.ts +363 -0
- package/src/node/admission.ts +153 -0
- package/src/node/handoff-manifest.ts +77 -0
- package/src/node/index.ts +10 -0
- package/src/outbound-delivery.ts +231 -0
- package/src/peer-message-ledger.ts +193 -0
- package/src/protocol-session.ts +28 -0
- package/src/resume-buffer.ts +123 -0
- package/src/session-events.ts +198 -0
- package/src/session-message-handler.ts +282 -0
- package/src/session-query-messages.ts +42 -0
- package/src/session-resume-bridge.ts +252 -0
- package/src/usage-messages.ts +195 -0
- package/src/wire-messages.ts +165 -0
- package/dist/node/headless/index.cjs +0 -1
- package/dist/node/headless/index.d.ts +0 -2
- package/dist/node/headless/index.js +0 -1
- package/dist/node/headless-DG_MEbf0.js +0 -19
- package/dist/node/headless-DG_MEbf0.js.map +0 -1
- package/dist/node/headless-xq77JBq0.cjs +0 -18
- package/dist/node/index-CrJ2Lhn3.d.ts +0 -96
- package/dist/node/index-CrJ2Lhn3.d.ts.map +0 -1
- package/dist/node/index-JIvzrtaA.d.ts +0 -52
- package/dist/node/index-JIvzrtaA.d.ts.map +0 -1
- package/dist/node/index-JTD_xtEQ.d.ts +0 -96
- package/dist/node/index-JTD_xtEQ.d.ts.map +0 -1
- package/dist/node/index-y_HBOfzO.d.ts +0 -52
- package/dist/node/index-y_HBOfzO.d.ts.map +0 -1
- package/dist/node/programmatic/index.cjs +0 -1
- package/dist/node/programmatic/index.d.ts +0 -2
- package/dist/node/programmatic/index.js +0 -1
- package/dist/node/programmatic-6wLf68qJ.cjs +0 -1
- package/dist/node/programmatic-70dxIXtS.js +0 -2
- package/dist/node/programmatic-70dxIXtS.js.map +0 -1
- package/dist/node/testing/index.cjs +0 -1
- package/dist/node/testing/index.d.ts +0 -2
- package/dist/node/testing/index.js +0 -1
- package/src/__tests__/programmatic/programmatic-ask-user.test.ts +0 -32
- package/src/__tests__/programmatic/programmatic-driver.test.ts +0 -107
- package/src/__tests__/programmatic/programmatic-flows.test.ts +0 -140
- package/src/__tests__/transport-registry.stop-all.test.ts +0 -53
- package/src/headless/HeadlessInteractionChannel.ts +0 -129
- package/src/headless/__tests__/headless-channel-options.test.ts +0 -106
- package/src/headless/__tests__/headless-goal-runner.test.ts +0 -108
- package/src/headless/__tests__/headless-provider-failure.integration.test.ts +0 -143
- package/src/headless/__tests__/headless-runner-initialization.test.ts +0 -45
- package/src/headless/__tests__/headless-runner.test.ts +0 -508
- package/src/headless/__tests__/headless-skill-activation.integration.test.ts +0 -430
- package/src/headless/__tests__/headless-transport.test.ts +0 -267
- package/src/headless/cli-input.ts +0 -50
- package/src/headless/headless-runner.ts +0 -230
- package/src/headless/headless-stream-json.ts +0 -148
- package/src/headless/headless-transport.ts +0 -43
- package/src/headless/index.ts +0 -8
- package/src/headless/print-terminal.ts +0 -57
- package/src/programmatic/ProgrammaticInteractionChannel.ts +0 -82
- package/src/programmatic/createProgrammaticAgent.ts +0 -71
- package/src/programmatic/index.ts +0 -5
- package/src/testing/index.ts +0 -10
- package/src/transport-registry.ts +0 -103
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { sealHandoffRecord, verifyHandoffPayload } from '../node/handoff-manifest.js';
|
|
4
|
+
|
|
5
|
+
import type { IInteractiveSessionRecord } from '@robota-sdk/agent-interface-session';
|
|
6
|
+
|
|
7
|
+
function aRecord(over: Partial<IInteractiveSessionRecord> = {}): IInteractiveSessionRecord {
|
|
8
|
+
return {
|
|
9
|
+
id: 'session_1',
|
|
10
|
+
cwd: '/home/alice/project',
|
|
11
|
+
createdAt: '2026-08-17T00:00:00.000Z',
|
|
12
|
+
updatedAt: '2026-08-17T01:00:00.000Z',
|
|
13
|
+
messages: [],
|
|
14
|
+
...over,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('HANDOFF-001 TC-06 — a payload that did not arrive whole is refused', () => {
|
|
19
|
+
it('accepts the exact bytes that were sealed', () => {
|
|
20
|
+
const { serialized, integrity } = sealHandoffRecord(aRecord());
|
|
21
|
+
|
|
22
|
+
expect(verifyHandoffPayload(serialized, integrity).intact).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('reports a truncated payload as truncated, not as corruption', () => {
|
|
26
|
+
// Checked BEFORE the digest. Hashing a short buffer to discover it was short wastes the work
|
|
27
|
+
// and reports a dropped connection as tampering.
|
|
28
|
+
const { serialized, integrity } = sealHandoffRecord(aRecord());
|
|
29
|
+
|
|
30
|
+
const verdict = verifyHandoffPayload(serialized.slice(0, -20), integrity);
|
|
31
|
+
|
|
32
|
+
expect(verdict.failure).toBe('truncated');
|
|
33
|
+
expect(verdict.expectedBytes).toBe(integrity.byteLength);
|
|
34
|
+
expect(verdict.actualBytes).toBeLessThan(integrity.byteLength);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('reports a same-length substitution as a digest mismatch', () => {
|
|
38
|
+
// The case length alone cannot catch, which is why the digest is there at all.
|
|
39
|
+
const { serialized, integrity } = sealHandoffRecord(aRecord());
|
|
40
|
+
const tampered = serialized.replace('/home/alice/project', '/home/mallor/projec');
|
|
41
|
+
|
|
42
|
+
expect(tampered).toHaveLength(serialized.length);
|
|
43
|
+
expect(verifyHandoffPayload(tampered, integrity).failure).toBe('digest-mismatch');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('two different records do not seal to the same digest', () => {
|
|
47
|
+
const a = sealHandoffRecord(aRecord());
|
|
48
|
+
const b = sealHandoffRecord(aRecord({ cwd: '/elsewhere' }));
|
|
49
|
+
|
|
50
|
+
expect(a.integrity.digest).not.toBe(b.integrity.digest);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
CLIENT_MESSAGE_SHAPES,
|
|
5
|
+
MAX_INBOUND_FRAME_BYTES,
|
|
6
|
+
SERVER_MESSAGE_SHAPES,
|
|
7
|
+
decodeClientMessage,
|
|
8
|
+
decodeFrame,
|
|
9
|
+
decodeServerMessage,
|
|
10
|
+
} from '../message-decoders.js';
|
|
11
|
+
|
|
12
|
+
import type { TClientMessage, TServerMessage } from '../wire-messages.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Issue #2045 — the owner-side runtime decoders. One well-formed sample per union variant round-trips
|
|
16
|
+
* through JSON; a shared malformed corpus (null, arrays, missing fields, wrong primitives, malformed
|
|
17
|
+
* nested values, oversized frames) is refused consistently. Both carriers call these same functions.
|
|
18
|
+
*/
|
|
19
|
+
const CLIENT_SAMPLES: Readonly<Record<TClientMessage['type'], TClientMessage>> = {
|
|
20
|
+
submit: { type: 'submit', prompt: 'hi' },
|
|
21
|
+
command: { type: 'command', name: 'help', args: 'x' },
|
|
22
|
+
abort: { type: 'abort' },
|
|
23
|
+
'cancel-queue': { type: 'cancel-queue' },
|
|
24
|
+
'get-messages': { type: 'get-messages' },
|
|
25
|
+
'get-context': { type: 'get-context' },
|
|
26
|
+
'get-usage-report': { type: 'get-usage-report' },
|
|
27
|
+
'get-personal-usage-report': {
|
|
28
|
+
type: 'get-personal-usage-report',
|
|
29
|
+
requestId: 'request-1',
|
|
30
|
+
period: '7d',
|
|
31
|
+
timezone: 'UTC',
|
|
32
|
+
},
|
|
33
|
+
'get-stored-session-usage-report': {
|
|
34
|
+
type: 'get-stored-session-usage-report',
|
|
35
|
+
requestId: 'request-2',
|
|
36
|
+
sessionId: 'session-1',
|
|
37
|
+
},
|
|
38
|
+
'get-executing': { type: 'get-executing' },
|
|
39
|
+
'get-pending': { type: 'get-pending' },
|
|
40
|
+
'get-execution-workspace': { type: 'get-execution-workspace' },
|
|
41
|
+
'get-background-tasks': {
|
|
42
|
+
type: 'get-background-tasks',
|
|
43
|
+
filter: { kind: 'agent', includeClosed: true },
|
|
44
|
+
},
|
|
45
|
+
'get-background-task': { type: 'get-background-task', taskId: 't' },
|
|
46
|
+
'get-background-job-groups': { type: 'get-background-job-groups' },
|
|
47
|
+
'get-background-job-group': { type: 'get-background-job-group', groupId: 'g' },
|
|
48
|
+
'wait-background-job-group': { type: 'wait-background-job-group', groupId: 'g' },
|
|
49
|
+
'cancel-background-task': { type: 'cancel-background-task', taskId: 't', reason: 'r' },
|
|
50
|
+
'close-background-task': { type: 'close-background-task', taskId: 't' },
|
|
51
|
+
'send-background-task': { type: 'send-background-task', taskId: 't', input: { prompt: 'p' } },
|
|
52
|
+
'read-background-task-log': {
|
|
53
|
+
type: 'read-background-task-log',
|
|
54
|
+
taskId: 't',
|
|
55
|
+
cursor: { offset: 3 },
|
|
56
|
+
},
|
|
57
|
+
'permission-response': { type: 'permission-response', id: 'p1', result: 'allow-session' },
|
|
58
|
+
'ask-response': { type: 'ask-response', id: 'a1', response: { type: 'answer', values: ['y'] } },
|
|
59
|
+
resume: { type: 'resume', lastSeq: 4 },
|
|
60
|
+
ack: { type: 'ack', seq: 9 },
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const SERVER_SAMPLES: Readonly<Record<TServerMessage['type'], TServerMessage>> = {
|
|
64
|
+
text_delta: { type: 'text_delta', delta: 'd', driverId: 'drv' },
|
|
65
|
+
user_message: { type: 'user_message', content: 'c' },
|
|
66
|
+
tool_start: { type: 'tool_start', state: {} as never },
|
|
67
|
+
tool_end: { type: 'tool_end', state: {} as never },
|
|
68
|
+
thinking: { type: 'thinking', isThinking: true },
|
|
69
|
+
complete: { type: 'complete', result: {} as never },
|
|
70
|
+
interrupted: { type: 'interrupted', result: {} as never },
|
|
71
|
+
error: { type: 'error', message: 'm' },
|
|
72
|
+
command_result: { type: 'command_result', name: 'n', message: 'm', success: true },
|
|
73
|
+
messages: { type: 'messages', messages: [] },
|
|
74
|
+
context: { type: 'context', state: {} as never },
|
|
75
|
+
usage_report: { type: 'usage_report', report: {} as never },
|
|
76
|
+
personal_usage_report: {
|
|
77
|
+
type: 'personal_usage_report',
|
|
78
|
+
requestId: 'request-1',
|
|
79
|
+
report: {} as never,
|
|
80
|
+
},
|
|
81
|
+
personal_usage_report_error: {
|
|
82
|
+
type: 'personal_usage_report_error',
|
|
83
|
+
requestId: 'request-1',
|
|
84
|
+
code: 'not_available',
|
|
85
|
+
message: 'not available',
|
|
86
|
+
},
|
|
87
|
+
stored_session_usage_report: {
|
|
88
|
+
type: 'stored_session_usage_report',
|
|
89
|
+
requestId: 'request-2',
|
|
90
|
+
sessionId: 'session-1',
|
|
91
|
+
report: {} as never,
|
|
92
|
+
},
|
|
93
|
+
stored_session_usage_report_error: {
|
|
94
|
+
type: 'stored_session_usage_report_error',
|
|
95
|
+
requestId: 'request-2',
|
|
96
|
+
sessionId: 'session-1',
|
|
97
|
+
code: 'report_failed',
|
|
98
|
+
message: 'not found',
|
|
99
|
+
},
|
|
100
|
+
executing: { type: 'executing', executing: false },
|
|
101
|
+
pending: { type: 'pending', pending: null },
|
|
102
|
+
execution_workspace_event: { type: 'execution_workspace_event', snapshot: {} as never },
|
|
103
|
+
background_task_event: { type: 'background_task_event', event: {} as never },
|
|
104
|
+
background_job_group_event: { type: 'background_job_group_event', event: {} as never },
|
|
105
|
+
plan_event: { type: 'plan_event', event: {} as never },
|
|
106
|
+
context_file_refreshed: { type: 'context_file_refreshed', event: {} as never },
|
|
107
|
+
branch_event: { type: 'branch_event', event: {} as never },
|
|
108
|
+
background_tasks: { type: 'background_tasks', tasks: [] },
|
|
109
|
+
background_task: { type: 'background_task', taskId: 't', task: null },
|
|
110
|
+
background_job_groups: { type: 'background_job_groups', groups: [] },
|
|
111
|
+
background_job_group: { type: 'background_job_group', groupId: 'g', group: null },
|
|
112
|
+
background_task_log: { type: 'background_task_log', taskId: 't', page: {} as never },
|
|
113
|
+
permission_request: { type: 'permission_request', event: {} as never },
|
|
114
|
+
ask_request: { type: 'ask_request', event: {} as never },
|
|
115
|
+
prompt_resolved: { type: 'prompt_resolved', event: {} as never },
|
|
116
|
+
ui_intent: { type: 'ui_intent', event: {} as never },
|
|
117
|
+
session_renamed: { type: 'session_renamed', event: {} as never },
|
|
118
|
+
history_cleared: { type: 'history_cleared' },
|
|
119
|
+
background_task_control_result: {
|
|
120
|
+
type: 'background_task_control_result',
|
|
121
|
+
action: 'cancel',
|
|
122
|
+
taskId: 't',
|
|
123
|
+
success: true,
|
|
124
|
+
},
|
|
125
|
+
protocol_error: { type: 'protocol_error', message: 'm' },
|
|
126
|
+
resume_gap: { type: 'resume_gap' },
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const MALFORMED_CLIENT: ReadonlyArray<[string, unknown]> = [
|
|
130
|
+
['null', null],
|
|
131
|
+
['an array', [{ type: 'abort' }]],
|
|
132
|
+
['a string', 'abort'],
|
|
133
|
+
['no type', { prompt: 'x' }],
|
|
134
|
+
['a numeric type', { type: 1 }],
|
|
135
|
+
['an unknown type', { type: 'nope' }],
|
|
136
|
+
['a prototype-only type', { type: 'toString' }],
|
|
137
|
+
['submit without prompt', { type: 'submit' }],
|
|
138
|
+
['submit with an empty prompt', { type: 'submit', prompt: '' }],
|
|
139
|
+
['submit with an array prompt', { type: 'submit', prompt: ['x'] }],
|
|
140
|
+
['command with numeric args', { type: 'command', name: 'n', args: 1 }],
|
|
141
|
+
['get-background-tasks with an array filter', { type: 'get-background-tasks', filter: [] }],
|
|
142
|
+
[
|
|
143
|
+
'get-background-tasks with an unknown kind',
|
|
144
|
+
{ type: 'get-background-tasks', filter: { kind: 'x' } },
|
|
145
|
+
],
|
|
146
|
+
[
|
|
147
|
+
'send-background-task with a string input',
|
|
148
|
+
{ type: 'send-background-task', taskId: 't', input: 'p' },
|
|
149
|
+
],
|
|
150
|
+
[
|
|
151
|
+
'send-background-task with a numeric prompt',
|
|
152
|
+
{ type: 'send-background-task', taskId: 't', input: { prompt: 1 } },
|
|
153
|
+
],
|
|
154
|
+
[
|
|
155
|
+
'read-background-task-log with a string offset',
|
|
156
|
+
{ type: 'read-background-task-log', taskId: 't', cursor: { offset: '3' } },
|
|
157
|
+
],
|
|
158
|
+
[
|
|
159
|
+
'permission-response with a string result',
|
|
160
|
+
{ type: 'permission-response', id: 'p', result: 'yes' },
|
|
161
|
+
],
|
|
162
|
+
[
|
|
163
|
+
'ask-response with values not strings',
|
|
164
|
+
{ type: 'ask-response', id: 'a', response: { type: 'answer', values: [1] } },
|
|
165
|
+
],
|
|
166
|
+
[
|
|
167
|
+
'ask-response cancelled carrying values',
|
|
168
|
+
{ type: 'ask-response', id: 'a', response: { type: 'cancelled', values: [] } },
|
|
169
|
+
],
|
|
170
|
+
['resume with a string lastSeq', { type: 'resume', lastSeq: '4' }],
|
|
171
|
+
['ack with NaN', { type: 'ack', seq: Number.NaN }],
|
|
172
|
+
['cancel-background-task with an empty taskId', { type: 'cancel-background-task', taskId: '' }],
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
const MALFORMED_SERVER: ReadonlyArray<[string, unknown]> = [
|
|
176
|
+
['null', null],
|
|
177
|
+
['an array', [{ type: 'resume_gap' }]],
|
|
178
|
+
['an unknown type', { type: 'nope' }],
|
|
179
|
+
['text_delta without delta', { type: 'text_delta' }],
|
|
180
|
+
['text_delta with a numeric driverId', { type: 'text_delta', delta: 'd', driverId: 1 }],
|
|
181
|
+
['tool_start with an array state', { type: 'tool_start', state: [] }],
|
|
182
|
+
['thinking with a string flag', { type: 'thinking', isThinking: 'yes' }],
|
|
183
|
+
['messages with a non-array', { type: 'messages', messages: {} }],
|
|
184
|
+
['messages with a primitive entry', { type: 'messages', messages: ['x'] }],
|
|
185
|
+
['pending with a number', { type: 'pending', pending: 1 }],
|
|
186
|
+
['background_task with an array task', { type: 'background_task', taskId: 't', task: [] }],
|
|
187
|
+
[
|
|
188
|
+
'control result with an unknown action',
|
|
189
|
+
{ type: 'background_task_control_result', action: 'nuke', taskId: 't', success: true },
|
|
190
|
+
],
|
|
191
|
+
['command_result without success', { type: 'command_result', name: 'n', message: 'm' }],
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
describe('decodeClientMessage (issue #2045)', () => {
|
|
195
|
+
it('has a shape for every union variant and every sample decodes through JSON', () => {
|
|
196
|
+
for (const [type, sample] of Object.entries(CLIENT_SAMPLES)) {
|
|
197
|
+
expect(CLIENT_MESSAGE_SHAPES).toHaveProperty(type);
|
|
198
|
+
expect(decodeClientMessage(JSON.parse(JSON.stringify(sample)))).toEqual({
|
|
199
|
+
ok: true,
|
|
200
|
+
message: sample,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it.each(MALFORMED_CLIENT)('refuses %s', (_label, value) => {
|
|
206
|
+
expect(decodeClientMessage(value).ok).toBe(false);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('keeps the protocol wording for a bad submit prompt', () => {
|
|
210
|
+
expect(decodeClientMessage({ type: 'submit', prompt: 1 })).toEqual({
|
|
211
|
+
ok: false,
|
|
212
|
+
reason: 'prompt must be a non-empty string',
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('decodeServerMessage (issue #2045)', () => {
|
|
218
|
+
it('has a shape for every union variant and every sample decodes through JSON', () => {
|
|
219
|
+
for (const [type, sample] of Object.entries(SERVER_SAMPLES)) {
|
|
220
|
+
expect(SERVER_MESSAGE_SHAPES).toHaveProperty(type);
|
|
221
|
+
expect(decodeServerMessage(JSON.parse(JSON.stringify(sample)))).toEqual({
|
|
222
|
+
ok: true,
|
|
223
|
+
message: sample,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it.each(MALFORMED_SERVER)('refuses %s', (_label, value) => {
|
|
229
|
+
expect(decodeServerMessage(value).ok).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe('decodeFrame — the one carrier path', () => {
|
|
234
|
+
it('refuses invalid JSON with the protocol wording and an oversized frame before parsing', () => {
|
|
235
|
+
expect(decodeFrame('not json', decodeClientMessage)).toEqual({
|
|
236
|
+
ok: false,
|
|
237
|
+
reason: 'Invalid JSON',
|
|
238
|
+
});
|
|
239
|
+
const huge = `{"type":"submit","prompt":"${'x'.repeat(MAX_INBOUND_FRAME_BYTES)}"}`;
|
|
240
|
+
expect(decodeFrame(huge, decodeClientMessage)).toEqual({
|
|
241
|
+
ok: false,
|
|
242
|
+
reason: 'Frame too large',
|
|
243
|
+
});
|
|
244
|
+
expect(decodeFrame('{"type":"abort"}', decodeClientMessage)).toEqual({
|
|
245
|
+
ok: true,
|
|
246
|
+
message: { type: 'abort' },
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
});
|