Skip to main content

rivetkit_inspector_protocol/
versioned.rs

1use anyhow::{Result, bail};
2use serde_bare::Uint;
3use vbare::OwnedVersionedData;
4
5use crate::generated::{v1, v2, v3, v4, v5};
6
7const WORKFLOW_HISTORY_DROPPED_ERROR: &str = "inspector.workflow_history_dropped";
8const QUEUE_DROPPED_ERROR: &str = "inspector.queue_dropped";
9const TRACE_DROPPED_ERROR: &str = "inspector.trace_dropped";
10const DATABASE_DROPPED_ERROR: &str = "inspector.database_dropped";
11
12pub enum ToServer {
13	V1(v1::ToServer),
14	V2(v2::ToServer),
15	V3(v3::ToServer),
16	V4(v4::ToServer),
17	V5(v5::ToServer),
18}
19
20impl OwnedVersionedData for ToServer {
21	type Latest = v5::ToServer;
22
23	fn wrap_latest(latest: Self::Latest) -> Self {
24		Self::V5(latest)
25	}
26
27	fn unwrap_latest(self) -> Result<Self::Latest> {
28		match self {
29			Self::V5(data) => Ok(data),
30			_ => bail!("version not latest"),
31		}
32	}
33
34	fn deserialize_version(payload: &[u8], version: u16) -> Result<Self> {
35		match version {
36			1 => Ok(Self::V1(serde_bare::from_slice(payload)?)),
37			2 => Ok(Self::V2(serde_bare::from_slice(payload)?)),
38			3 => Ok(Self::V3(serde_bare::from_slice(payload)?)),
39			4 => Ok(Self::V4(serde_bare::from_slice(payload)?)),
40			5 => Ok(Self::V5(serde_bare::from_slice(payload)?)),
41			_ => bail!("invalid inspector protocol version for ToServer: {version}"),
42		}
43	}
44
45	fn serialize_version(self, version: u16) -> Result<Vec<u8>> {
46		match (self, version) {
47			(Self::V1(data), 1) => serde_bare::to_vec(&data).map_err(Into::into),
48			(Self::V2(data), 2) => serde_bare::to_vec(&data).map_err(Into::into),
49			(Self::V3(data), 3) => serde_bare::to_vec(&data).map_err(Into::into),
50			(Self::V4(data), 4) => serde_bare::to_vec(&data).map_err(Into::into),
51			(Self::V5(data), 5) => serde_bare::to_vec(&data).map_err(Into::into),
52			(_, version) => bail!("unexpected inspector protocol version for ToServer: {version}"),
53		}
54	}
55
56	fn deserialize_converters() -> Vec<impl Fn(Self) -> Result<Self>> {
57		vec![
58			Self::v1_to_v2,
59			Self::v2_to_v3,
60			Self::v3_to_v4,
61			Self::v4_to_v5,
62		]
63	}
64
65	fn serialize_converters() -> Vec<impl Fn(Self) -> Result<Self>> {
66		vec![
67			Self::v5_to_v4,
68			Self::v4_to_v3,
69			Self::v3_to_v2,
70			Self::v2_to_v1,
71		]
72	}
73}
74
75impl ToServer {
76	fn v1_to_v2(self) -> Result<Self> {
77		let Self::V1(data) = self else {
78			bail!("expected inspector protocol v1 ToServer")
79		};
80
81		let body = match data.body {
82			v1::ToServerBody::PatchStateRequest(req) => {
83				v2::ToServerBody::PatchStateRequest(req.into())
84			}
85			v1::ToServerBody::StateRequest(req) => v2::ToServerBody::StateRequest(req.into()),
86			v1::ToServerBody::ConnectionsRequest(req) => {
87				v2::ToServerBody::ConnectionsRequest(req.into())
88			}
89			v1::ToServerBody::ActionRequest(req) => v2::ToServerBody::ActionRequest(req.into()),
90			v1::ToServerBody::RpcsListRequest(req) => v2::ToServerBody::RpcsListRequest(req.into()),
91			v1::ToServerBody::EventsRequest(_) | v1::ToServerBody::ClearEventsRequest(_) => {
92				bail!("cannot convert inspector v1 events requests to v2")
93			}
94		};
95
96		Ok(Self::V2(v2::ToServer { body }))
97	}
98
99	fn v2_to_v3(self) -> Result<Self> {
100		let Self::V2(data) = self else {
101			bail!("expected inspector protocol v2 ToServer")
102		};
103		Ok(Self::V3(data.into()))
104	}
105
106	fn v3_to_v4(self) -> Result<Self> {
107		let Self::V3(data) = self else {
108			bail!("expected inspector protocol v3 ToServer")
109		};
110
111		let body = match data.body {
112			v3::ToServerBody::PatchStateRequest(req) => {
113				v4::ToServerBody::PatchStateRequest(req.into())
114			}
115			v3::ToServerBody::StateRequest(req) => v4::ToServerBody::StateRequest(req.into()),
116			v3::ToServerBody::ConnectionsRequest(req) => {
117				v4::ToServerBody::ConnectionsRequest(req.into())
118			}
119			v3::ToServerBody::ActionRequest(req) => v4::ToServerBody::ActionRequest(req.into()),
120			v3::ToServerBody::RpcsListRequest(req) => v4::ToServerBody::RpcsListRequest(req.into()),
121			v3::ToServerBody::TraceQueryRequest(req) => {
122				v4::ToServerBody::TraceQueryRequest(req.into())
123			}
124			v3::ToServerBody::QueueRequest(req) => v4::ToServerBody::QueueRequest(req.into()),
125			v3::ToServerBody::WorkflowHistoryRequest(req) => {
126				v4::ToServerBody::WorkflowHistoryRequest(req.into())
127			}
128			v3::ToServerBody::DatabaseSchemaRequest(req) => {
129				v4::ToServerBody::DatabaseSchemaRequest(req.into())
130			}
131			v3::ToServerBody::DatabaseTableRowsRequest(req) => {
132				v4::ToServerBody::DatabaseTableRowsRequest(req.into())
133			}
134		};
135
136		Ok(Self::V4(v4::ToServer { body }))
137	}
138
139	fn v4_to_v5(self) -> Result<Self> {
140		let Self::V4(data) = self else {
141			bail!("expected inspector protocol v4 ToServer")
142		};
143		Ok(Self::V5(data.into()))
144	}
145
146	fn v5_to_v4(self) -> Result<Self> {
147		let Self::V5(data) = self else {
148			bail!("expected inspector protocol v5 ToServer")
149		};
150		Ok(Self::V4(data.into()))
151	}
152
153	fn v4_to_v3(self) -> Result<Self> {
154		let Self::V4(data) = self else {
155			bail!("expected inspector protocol v4 ToServer")
156		};
157
158		let body = match data.body {
159			v4::ToServerBody::PatchStateRequest(req) => {
160				v3::ToServerBody::PatchStateRequest(req.into())
161			}
162			v4::ToServerBody::StateRequest(req) => v3::ToServerBody::StateRequest(req.into()),
163			v4::ToServerBody::ConnectionsRequest(req) => {
164				v3::ToServerBody::ConnectionsRequest(req.into())
165			}
166			v4::ToServerBody::ActionRequest(req) => v3::ToServerBody::ActionRequest(req.into()),
167			v4::ToServerBody::RpcsListRequest(req) => v3::ToServerBody::RpcsListRequest(req.into()),
168			v4::ToServerBody::TraceQueryRequest(req) => {
169				v3::ToServerBody::TraceQueryRequest(req.into())
170			}
171			v4::ToServerBody::QueueRequest(req) => v3::ToServerBody::QueueRequest(req.into()),
172			v4::ToServerBody::WorkflowHistoryRequest(req) => {
173				v3::ToServerBody::WorkflowHistoryRequest(req.into())
174			}
175			v4::ToServerBody::WorkflowReplayRequest(_) => {
176				bail!("cannot convert inspector v4 workflow replay requests to v3")
177			}
178			v4::ToServerBody::DatabaseSchemaRequest(req) => {
179				v3::ToServerBody::DatabaseSchemaRequest(req.into())
180			}
181			v4::ToServerBody::DatabaseTableRowsRequest(req) => {
182				v3::ToServerBody::DatabaseTableRowsRequest(req.into())
183			}
184		};
185
186		Ok(Self::V3(v3::ToServer { body }))
187	}
188
189	fn v3_to_v2(self) -> Result<Self> {
190		let Self::V3(data) = self else {
191			bail!("expected inspector protocol v3 ToServer")
192		};
193
194		let body = match data.body {
195			v3::ToServerBody::PatchStateRequest(req) => {
196				v2::ToServerBody::PatchStateRequest(req.into())
197			}
198			v3::ToServerBody::StateRequest(req) => v2::ToServerBody::StateRequest(req.into()),
199			v3::ToServerBody::ConnectionsRequest(req) => {
200				v2::ToServerBody::ConnectionsRequest(req.into())
201			}
202			v3::ToServerBody::ActionRequest(req) => v2::ToServerBody::ActionRequest(req.into()),
203			v3::ToServerBody::RpcsListRequest(req) => v2::ToServerBody::RpcsListRequest(req.into()),
204			v3::ToServerBody::TraceQueryRequest(req) => {
205				v2::ToServerBody::TraceQueryRequest(req.into())
206			}
207			v3::ToServerBody::QueueRequest(req) => v2::ToServerBody::QueueRequest(req.into()),
208			v3::ToServerBody::WorkflowHistoryRequest(req) => {
209				v2::ToServerBody::WorkflowHistoryRequest(req.into())
210			}
211			v3::ToServerBody::DatabaseSchemaRequest(_)
212			| v3::ToServerBody::DatabaseTableRowsRequest(_) => {
213				bail!("cannot convert inspector v3 database requests to v2")
214			}
215		};
216
217		Ok(Self::V2(v2::ToServer { body }))
218	}
219
220	fn v2_to_v1(self) -> Result<Self> {
221		let Self::V2(data) = self else {
222			bail!("expected inspector protocol v2 ToServer")
223		};
224
225		let body = match data.body {
226			v2::ToServerBody::PatchStateRequest(req) => {
227				v1::ToServerBody::PatchStateRequest(req.into())
228			}
229			v2::ToServerBody::StateRequest(req) => v1::ToServerBody::StateRequest(req.into()),
230			v2::ToServerBody::ConnectionsRequest(req) => {
231				v1::ToServerBody::ConnectionsRequest(req.into())
232			}
233			v2::ToServerBody::ActionRequest(req) => v1::ToServerBody::ActionRequest(req.into()),
234			v2::ToServerBody::RpcsListRequest(req) => v1::ToServerBody::RpcsListRequest(req.into()),
235			v2::ToServerBody::TraceQueryRequest(_)
236			| v2::ToServerBody::QueueRequest(_)
237			| v2::ToServerBody::WorkflowHistoryRequest(_) => {
238				bail!("cannot convert inspector v2 queue/trace/workflow requests to v1")
239			}
240		};
241
242		Ok(Self::V1(v1::ToServer { body }))
243	}
244}
245
246pub enum ToClient {
247	V1(v1::ToClient),
248	V2(v2::ToClient),
249	V3(v3::ToClient),
250	V4(v4::ToClient),
251	V5(v5::ToClient),
252}
253
254impl OwnedVersionedData for ToClient {
255	type Latest = v5::ToClient;
256
257	fn wrap_latest(latest: Self::Latest) -> Self {
258		Self::V5(latest)
259	}
260
261	fn unwrap_latest(self) -> Result<Self::Latest> {
262		match self {
263			Self::V5(data) => Ok(data),
264			_ => bail!("version not latest"),
265		}
266	}
267
268	fn deserialize_version(payload: &[u8], version: u16) -> Result<Self> {
269		match version {
270			1 => Ok(Self::V1(serde_bare::from_slice(payload)?)),
271			2 => Ok(Self::V2(serde_bare::from_slice(payload)?)),
272			3 => Ok(Self::V3(serde_bare::from_slice(payload)?)),
273			4 => Ok(Self::V4(serde_bare::from_slice(payload)?)),
274			5 => Ok(Self::V5(serde_bare::from_slice(payload)?)),
275			_ => bail!("invalid inspector protocol version for ToClient: {version}"),
276		}
277	}
278
279	fn serialize_version(self, version: u16) -> Result<Vec<u8>> {
280		match (self, version) {
281			(Self::V1(data), 1) => serde_bare::to_vec(&data).map_err(Into::into),
282			(Self::V2(data), 2) => serde_bare::to_vec(&data).map_err(Into::into),
283			(Self::V3(data), 3) => serde_bare::to_vec(&data).map_err(Into::into),
284			(Self::V4(data), 4) => serde_bare::to_vec(&data).map_err(Into::into),
285			(Self::V5(data), 5) => serde_bare::to_vec(&data).map_err(Into::into),
286			(_, version) => bail!("unexpected inspector protocol version for ToClient: {version}"),
287		}
288	}
289
290	fn deserialize_converters() -> Vec<impl Fn(Self) -> Result<Self>> {
291		vec![
292			Self::v1_to_v2,
293			Self::v2_to_v3,
294			Self::v3_to_v4,
295			Self::v4_to_v5,
296		]
297	}
298
299	fn serialize_converters() -> Vec<impl Fn(Self) -> Result<Self>> {
300		vec![
301			Self::v5_to_v4,
302			Self::v4_to_v3,
303			Self::v3_to_v2,
304			Self::v2_to_v1,
305		]
306	}
307}
308
309impl ToClient {
310	fn v1_to_v2(self) -> Result<Self> {
311		let Self::V1(data) = self else {
312			bail!("expected inspector protocol v1 ToClient")
313		};
314
315		let body = match data.body {
316			v1::ToClientBody::StateResponse(resp) => v2::ToClientBody::StateResponse(resp.into()),
317			v1::ToClientBody::ConnectionsResponse(resp) => {
318				v2::ToClientBody::ConnectionsResponse(resp.into())
319			}
320			v1::ToClientBody::ActionResponse(resp) => v2::ToClientBody::ActionResponse(resp.into()),
321			v1::ToClientBody::RpcsListResponse(resp) => {
322				v2::ToClientBody::RpcsListResponse(resp.into())
323			}
324			v1::ToClientBody::ConnectionsUpdated(update) => {
325				v2::ToClientBody::ConnectionsUpdated(update.into())
326			}
327			v1::ToClientBody::StateUpdated(update) => v2::ToClientBody::StateUpdated(update.into()),
328			v1::ToClientBody::Error(error) => v2::ToClientBody::Error(error.into()),
329			v1::ToClientBody::Init(init) => v2::ToClientBody::Init(v2::Init {
330				connections: convert_vec(init.connections),
331				state: init.state,
332				is_state_enabled: init.is_state_enabled,
333				rpcs: init.rpcs,
334				is_database_enabled: init.is_database_enabled,
335				queue_size: Uint(0),
336				workflow_history: None,
337				is_workflow_enabled: false,
338			}),
339			v1::ToClientBody::EventsResponse(_) | v1::ToClientBody::EventsUpdated(_) => {
340				bail!("cannot convert inspector v1 events responses to v2")
341			}
342		};
343
344		Ok(Self::V2(v2::ToClient { body }))
345	}
346
347	fn v2_to_v3(self) -> Result<Self> {
348		let Self::V2(data) = self else {
349			bail!("expected inspector protocol v2 ToClient")
350		};
351		Ok(Self::V3(data.into()))
352	}
353
354	fn v3_to_v4(self) -> Result<Self> {
355		let Self::V3(data) = self else {
356			bail!("expected inspector protocol v3 ToClient")
357		};
358
359		let body = match data.body {
360			v3::ToClientBody::StateResponse(resp) => v4::ToClientBody::StateResponse(resp.into()),
361			v3::ToClientBody::ConnectionsResponse(resp) => {
362				v4::ToClientBody::ConnectionsResponse(resp.into())
363			}
364			v3::ToClientBody::ActionResponse(resp) => v4::ToClientBody::ActionResponse(resp.into()),
365			v3::ToClientBody::ConnectionsUpdated(update) => {
366				v4::ToClientBody::ConnectionsUpdated(update.into())
367			}
368			v3::ToClientBody::QueueUpdated(update) => v4::ToClientBody::QueueUpdated(update.into()),
369			v3::ToClientBody::StateUpdated(update) => v4::ToClientBody::StateUpdated(update.into()),
370			v3::ToClientBody::WorkflowHistoryUpdated(update) => {
371				v4::ToClientBody::WorkflowHistoryUpdated(update.into())
372			}
373			v3::ToClientBody::RpcsListResponse(resp) => {
374				v4::ToClientBody::RpcsListResponse(resp.into())
375			}
376			v3::ToClientBody::TraceQueryResponse(resp) => {
377				v4::ToClientBody::TraceQueryResponse(resp.into())
378			}
379			v3::ToClientBody::QueueResponse(resp) => v4::ToClientBody::QueueResponse(resp.into()),
380			v3::ToClientBody::WorkflowHistoryResponse(resp) => {
381				v4::ToClientBody::WorkflowHistoryResponse(resp.into())
382			}
383			v3::ToClientBody::Error(error) => v4::ToClientBody::Error(error.into()),
384			v3::ToClientBody::Init(init) => v4::ToClientBody::Init(init.into()),
385			v3::ToClientBody::DatabaseSchemaResponse(resp) => {
386				v4::ToClientBody::DatabaseSchemaResponse(resp.into())
387			}
388			v3::ToClientBody::DatabaseTableRowsResponse(resp) => {
389				v4::ToClientBody::DatabaseTableRowsResponse(resp.into())
390			}
391		};
392
393		Ok(Self::V4(v4::ToClient { body }))
394	}
395
396	fn v4_to_v5(self) -> Result<Self> {
397		let Self::V4(data) = self else {
398			bail!("expected inspector protocol v4 ToClient")
399		};
400
401		let body = match data.body {
402			v4::ToClientBody::StateResponse(resp) => v5::ToClientBody::StateResponse(resp.into()),
403			v4::ToClientBody::ConnectionsResponse(resp) => {
404				v5::ToClientBody::ConnectionsResponse(resp.into())
405			}
406			v4::ToClientBody::ActionResponse(resp) => v5::ToClientBody::ActionResponse(resp.into()),
407			v4::ToClientBody::ConnectionsUpdated(update) => {
408				v5::ToClientBody::ConnectionsUpdated(update.into())
409			}
410			v4::ToClientBody::QueueUpdated(update) => v5::ToClientBody::QueueUpdated(update.into()),
411			v4::ToClientBody::StateUpdated(update) => v5::ToClientBody::StateUpdated(update.into()),
412			v4::ToClientBody::WorkflowHistoryUpdated(update) => {
413				v5::ToClientBody::WorkflowHistoryUpdated(update.into())
414			}
415			v4::ToClientBody::RpcsListResponse(resp) => {
416				v5::ToClientBody::RpcsListResponse(resp.into())
417			}
418			v4::ToClientBody::TraceQueryResponse(resp) => {
419				v5::ToClientBody::TraceQueryResponse(resp.into())
420			}
421			v4::ToClientBody::QueueResponse(resp) => v5::ToClientBody::QueueResponse(resp.into()),
422			v4::ToClientBody::WorkflowHistoryResponse(resp) => {
423				v5::ToClientBody::WorkflowHistoryResponse(resp.into())
424			}
425			v4::ToClientBody::WorkflowReplayResponse(resp) => {
426				v5::ToClientBody::WorkflowReplayResponse(resp.into())
427			}
428			v4::ToClientBody::Error(error) => v5::ToClientBody::Error(error.into()),
429			// An older server that only spoke v4 has no tab config to report,
430			// so upgrade to an empty list. Clients treating this as "no custom
431			// tabs" is correct for a pre-tab-config-in-init runtime.
432			v4::ToClientBody::Init(init) => v5::ToClientBody::Init(v5::Init {
433				connections: convert_vec(init.connections),
434				state: init.state,
435				is_state_enabled: init.is_state_enabled,
436				rpcs: init.rpcs,
437				is_database_enabled: init.is_database_enabled,
438				queue_size: init.queue_size,
439				workflow_history: init.workflow_history,
440				is_workflow_enabled: init.is_workflow_enabled,
441				tab_config: Vec::new(),
442			}),
443			v4::ToClientBody::DatabaseSchemaResponse(resp) => {
444				v5::ToClientBody::DatabaseSchemaResponse(resp.into())
445			}
446			v4::ToClientBody::DatabaseTableRowsResponse(resp) => {
447				v5::ToClientBody::DatabaseTableRowsResponse(resp.into())
448			}
449		};
450
451		Ok(Self::V5(v5::ToClient { body }))
452	}
453
454	fn v5_to_v4(self) -> Result<Self> {
455		let Self::V5(data) = self else {
456			bail!("expected inspector protocol v5 ToClient")
457		};
458
459		let body = match data.body {
460			v5::ToClientBody::StateResponse(resp) => v4::ToClientBody::StateResponse(resp.into()),
461			v5::ToClientBody::ConnectionsResponse(resp) => {
462				v4::ToClientBody::ConnectionsResponse(resp.into())
463			}
464			v5::ToClientBody::ActionResponse(resp) => v4::ToClientBody::ActionResponse(resp.into()),
465			v5::ToClientBody::ConnectionsUpdated(update) => {
466				v4::ToClientBody::ConnectionsUpdated(update.into())
467			}
468			v5::ToClientBody::QueueUpdated(update) => v4::ToClientBody::QueueUpdated(update.into()),
469			v5::ToClientBody::StateUpdated(update) => v4::ToClientBody::StateUpdated(update.into()),
470			v5::ToClientBody::WorkflowHistoryUpdated(update) => {
471				v4::ToClientBody::WorkflowHistoryUpdated(update.into())
472			}
473			v5::ToClientBody::RpcsListResponse(resp) => {
474				v4::ToClientBody::RpcsListResponse(resp.into())
475			}
476			v5::ToClientBody::TraceQueryResponse(resp) => {
477				v4::ToClientBody::TraceQueryResponse(resp.into())
478			}
479			v5::ToClientBody::QueueResponse(resp) => v4::ToClientBody::QueueResponse(resp.into()),
480			v5::ToClientBody::WorkflowHistoryResponse(resp) => {
481				v4::ToClientBody::WorkflowHistoryResponse(resp.into())
482			}
483			v5::ToClientBody::WorkflowReplayResponse(resp) => {
484				v4::ToClientBody::WorkflowReplayResponse(resp.into())
485			}
486			v5::ToClientBody::Error(error) => v4::ToClientBody::Error(error.into()),
487			// v4 has no tab-config field; drop it. A v4 client falls back to
488			// the `/inspector/tab-config` HTTP fetch for the same data.
489			v5::ToClientBody::Init(init) => v4::ToClientBody::Init(v4::Init {
490				connections: convert_vec(init.connections),
491				state: init.state,
492				is_state_enabled: init.is_state_enabled,
493				rpcs: init.rpcs,
494				is_database_enabled: init.is_database_enabled,
495				queue_size: init.queue_size,
496				workflow_history: init.workflow_history,
497				is_workflow_enabled: init.is_workflow_enabled,
498			}),
499			v5::ToClientBody::DatabaseSchemaResponse(resp) => {
500				v4::ToClientBody::DatabaseSchemaResponse(resp.into())
501			}
502			v5::ToClientBody::DatabaseTableRowsResponse(resp) => {
503				v4::ToClientBody::DatabaseTableRowsResponse(resp.into())
504			}
505		};
506
507		Ok(Self::V4(v4::ToClient { body }))
508	}
509
510	fn v4_to_v3(self) -> Result<Self> {
511		let Self::V4(data) = self else {
512			bail!("expected inspector protocol v4 ToClient")
513		};
514
515		let body = match data.body {
516			v4::ToClientBody::StateResponse(resp) => v3::ToClientBody::StateResponse(resp.into()),
517			v4::ToClientBody::ConnectionsResponse(resp) => {
518				v3::ToClientBody::ConnectionsResponse(resp.into())
519			}
520			v4::ToClientBody::ActionResponse(resp) => v3::ToClientBody::ActionResponse(resp.into()),
521			v4::ToClientBody::ConnectionsUpdated(update) => {
522				v3::ToClientBody::ConnectionsUpdated(update.into())
523			}
524			v4::ToClientBody::QueueUpdated(update) => v3::ToClientBody::QueueUpdated(update.into()),
525			v4::ToClientBody::StateUpdated(update) => v3::ToClientBody::StateUpdated(update.into()),
526			v4::ToClientBody::WorkflowHistoryUpdated(update) => {
527				v3::ToClientBody::WorkflowHistoryUpdated(update.into())
528			}
529			v4::ToClientBody::RpcsListResponse(resp) => {
530				v3::ToClientBody::RpcsListResponse(resp.into())
531			}
532			v4::ToClientBody::TraceQueryResponse(resp) => {
533				v3::ToClientBody::TraceQueryResponse(resp.into())
534			}
535			v4::ToClientBody::QueueResponse(resp) => v3::ToClientBody::QueueResponse(resp.into()),
536			v4::ToClientBody::WorkflowHistoryResponse(resp) => {
537				v3::ToClientBody::WorkflowHistoryResponse(resp.into())
538			}
539			v4::ToClientBody::WorkflowReplayResponse(_) => {
540				v3::ToClientBody::Error(dropped_error(WORKFLOW_HISTORY_DROPPED_ERROR).into())
541			}
542			v4::ToClientBody::Error(error) => v3::ToClientBody::Error(error.into()),
543			v4::ToClientBody::Init(init) => v3::ToClientBody::Init(init.into()),
544			v4::ToClientBody::DatabaseSchemaResponse(resp) => {
545				v3::ToClientBody::DatabaseSchemaResponse(resp.into())
546			}
547			v4::ToClientBody::DatabaseTableRowsResponse(resp) => {
548				v3::ToClientBody::DatabaseTableRowsResponse(resp.into())
549			}
550		};
551
552		Ok(Self::V3(v3::ToClient { body }))
553	}
554
555	fn v3_to_v2(self) -> Result<Self> {
556		let Self::V3(data) = self else {
557			bail!("expected inspector protocol v3 ToClient")
558		};
559
560		let body = match data.body {
561			v3::ToClientBody::StateResponse(resp) => v2::ToClientBody::StateResponse(resp.into()),
562			v3::ToClientBody::ConnectionsResponse(resp) => {
563				v2::ToClientBody::ConnectionsResponse(resp.into())
564			}
565			v3::ToClientBody::ActionResponse(resp) => v2::ToClientBody::ActionResponse(resp.into()),
566			v3::ToClientBody::ConnectionsUpdated(update) => {
567				v2::ToClientBody::ConnectionsUpdated(update.into())
568			}
569			v3::ToClientBody::QueueUpdated(update) => v2::ToClientBody::QueueUpdated(update.into()),
570			v3::ToClientBody::StateUpdated(update) => v2::ToClientBody::StateUpdated(update.into()),
571			v3::ToClientBody::WorkflowHistoryUpdated(update) => {
572				v2::ToClientBody::WorkflowHistoryUpdated(update.into())
573			}
574			v3::ToClientBody::RpcsListResponse(resp) => {
575				v2::ToClientBody::RpcsListResponse(resp.into())
576			}
577			v3::ToClientBody::TraceQueryResponse(resp) => {
578				v2::ToClientBody::TraceQueryResponse(resp.into())
579			}
580			v3::ToClientBody::QueueResponse(resp) => v2::ToClientBody::QueueResponse(resp.into()),
581			v3::ToClientBody::WorkflowHistoryResponse(resp) => {
582				v2::ToClientBody::WorkflowHistoryResponse(resp.into())
583			}
584			v3::ToClientBody::Error(error) => v2::ToClientBody::Error(error.into()),
585			v3::ToClientBody::Init(init) => v2::ToClientBody::Init(init.into()),
586			v3::ToClientBody::DatabaseSchemaResponse(_)
587			| v3::ToClientBody::DatabaseTableRowsResponse(_) => {
588				v2::ToClientBody::Error(dropped_error(DATABASE_DROPPED_ERROR))
589			}
590		};
591
592		Ok(Self::V2(v2::ToClient { body }))
593	}
594
595	fn v2_to_v1(self) -> Result<Self> {
596		let Self::V2(data) = self else {
597			bail!("expected inspector protocol v2 ToClient")
598		};
599
600		let body = match data.body {
601			v2::ToClientBody::StateResponse(resp) => v1::ToClientBody::StateResponse(resp.into()),
602			v2::ToClientBody::ConnectionsResponse(resp) => {
603				v1::ToClientBody::ConnectionsResponse(resp.into())
604			}
605			v2::ToClientBody::ActionResponse(resp) => v1::ToClientBody::ActionResponse(resp.into()),
606			v2::ToClientBody::ConnectionsUpdated(update) => {
607				v1::ToClientBody::ConnectionsUpdated(update.into())
608			}
609			v2::ToClientBody::StateUpdated(update) => v1::ToClientBody::StateUpdated(update.into()),
610			v2::ToClientBody::RpcsListResponse(resp) => {
611				v1::ToClientBody::RpcsListResponse(resp.into())
612			}
613			v2::ToClientBody::Error(error) => v1::ToClientBody::Error(error.into()),
614			v2::ToClientBody::Init(init) => v1::ToClientBody::Init(v1::Init {
615				connections: init.connections.into_iter().map(Into::into).collect(),
616				events: Vec::new(),
617				state: init.state,
618				is_state_enabled: init.is_state_enabled,
619				rpcs: init.rpcs,
620				is_database_enabled: init.is_database_enabled,
621			}),
622			v2::ToClientBody::QueueUpdated(_) | v2::ToClientBody::QueueResponse(_) => {
623				v1::ToClientBody::Error(dropped_error(QUEUE_DROPPED_ERROR).into())
624			}
625			v2::ToClientBody::WorkflowHistoryUpdated(_)
626			| v2::ToClientBody::WorkflowHistoryResponse(_) => {
627				v1::ToClientBody::Error(dropped_error(WORKFLOW_HISTORY_DROPPED_ERROR).into())
628			}
629			v2::ToClientBody::TraceQueryResponse(_) => {
630				v1::ToClientBody::Error(dropped_error(TRACE_DROPPED_ERROR).into())
631			}
632		};
633
634		Ok(Self::V1(v1::ToClient { body }))
635	}
636}
637
638fn convert_vec<From, To>(values: Vec<From>) -> Vec<To>
639where
640	From: Into<To>,
641{
642	values.into_iter().map(Into::into).collect()
643}
644
645macro_rules! impl_same_fields_pair {
646	($left:ident, $right:ident, $ty:ident { $($field:ident),+ $(,)? }) => {
647		impl From<$left::$ty> for $right::$ty {
648			fn from(value: $left::$ty) -> Self {
649				Self {
650					$($field: value.$field),+
651				}
652			}
653		}
654
655		impl From<$right::$ty> for $left::$ty {
656			fn from(value: $right::$ty) -> Self {
657				Self {
658					$($field: value.$field),+
659				}
660			}
661		}
662	};
663}
664
665macro_rules! impl_connection_list_pair {
666	($left:ident, $right:ident, $ty:ident) => {
667		impl From<$left::$ty> for $right::$ty {
668			fn from(value: $left::$ty) -> Self {
669				Self {
670					connections: convert_vec(value.connections),
671				}
672			}
673		}
674
675		impl From<$right::$ty> for $left::$ty {
676			fn from(value: $right::$ty) -> Self {
677				Self {
678					connections: convert_vec(value.connections),
679				}
680			}
681		}
682	};
683}
684
685macro_rules! impl_connections_response_pair {
686	($left:ident, $right:ident) => {
687		impl From<$left::ConnectionsResponse> for $right::ConnectionsResponse {
688			fn from(value: $left::ConnectionsResponse) -> Self {
689				Self {
690					rid: value.rid,
691					connections: convert_vec(value.connections),
692				}
693			}
694		}
695
696		impl From<$right::ConnectionsResponse> for $left::ConnectionsResponse {
697			fn from(value: $right::ConnectionsResponse) -> Self {
698				Self {
699					rid: value.rid,
700					connections: convert_vec(value.connections),
701				}
702			}
703		}
704	};
705}
706
707macro_rules! impl_queue_status_pair {
708	($left:ident, $right:ident) => {
709		impl From<$left::QueueStatus> for $right::QueueStatus {
710			fn from(value: $left::QueueStatus) -> Self {
711				Self {
712					size: value.size,
713					max_size: value.max_size,
714					messages: convert_vec(value.messages),
715					truncated: value.truncated,
716				}
717			}
718		}
719
720		impl From<$right::QueueStatus> for $left::QueueStatus {
721			fn from(value: $right::QueueStatus) -> Self {
722				Self {
723					size: value.size,
724					max_size: value.max_size,
725					messages: convert_vec(value.messages),
726					truncated: value.truncated,
727				}
728			}
729		}
730	};
731}
732
733macro_rules! impl_queue_response_pair {
734	($left:ident, $right:ident) => {
735		impl From<$left::QueueResponse> for $right::QueueResponse {
736			fn from(value: $left::QueueResponse) -> Self {
737				Self {
738					rid: value.rid,
739					status: value.status.into(),
740				}
741			}
742		}
743
744		impl From<$right::QueueResponse> for $left::QueueResponse {
745			fn from(value: $right::QueueResponse) -> Self {
746				Self {
747					rid: value.rid,
748					status: value.status.into(),
749				}
750			}
751		}
752	};
753}
754
755macro_rules! impl_init_pair {
756	($left:ident, $right:ident) => {
757		impl From<$left::Init> for $right::Init {
758			fn from(value: $left::Init) -> Self {
759				Self {
760					connections: convert_vec(value.connections),
761					state: value.state,
762					is_state_enabled: value.is_state_enabled,
763					rpcs: value.rpcs,
764					is_database_enabled: value.is_database_enabled,
765					queue_size: value.queue_size,
766					workflow_history: value.workflow_history,
767					is_workflow_enabled: value.is_workflow_enabled,
768				}
769			}
770		}
771
772		impl From<$right::Init> for $left::Init {
773			fn from(value: $right::Init) -> Self {
774				Self {
775					connections: convert_vec(value.connections),
776					state: value.state,
777					is_state_enabled: value.is_state_enabled,
778					rpcs: value.rpcs,
779					is_database_enabled: value.is_database_enabled,
780					queue_size: value.queue_size,
781					workflow_history: value.workflow_history,
782					is_workflow_enabled: value.is_workflow_enabled,
783				}
784			}
785		}
786	};
787}
788
789macro_rules! impl_common_actor_pair {
790	($left:ident, $right:ident) => {
791		impl_same_fields_pair!($left, $right, PatchStateRequest { state });
792		impl_same_fields_pair!($left, $right, ActionRequest { id, name, args });
793		impl_same_fields_pair!($left, $right, StateRequest { id });
794		impl_same_fields_pair!($left, $right, ConnectionsRequest { id });
795		impl_same_fields_pair!($left, $right, RpcsListRequest { id });
796		impl_same_fields_pair!($left, $right, Connection { id, details });
797		impl_connections_response_pair!($left, $right);
798		impl_connection_list_pair!($left, $right, ConnectionsUpdated);
799		impl_same_fields_pair!(
800			$left,
801			$right,
802			StateResponse {
803				rid,
804				state,
805				is_state_enabled,
806			}
807		);
808		impl_same_fields_pair!($left, $right, ActionResponse { rid, output });
809		impl_same_fields_pair!($left, $right, StateUpdated { state });
810		impl_same_fields_pair!($left, $right, RpcsListResponse { rid, rpcs });
811		impl_same_fields_pair!($left, $right, Error { message });
812	};
813}
814
815macro_rules! impl_queue_workflow_pair {
816	($left:ident, $right:ident) => {
817		impl_same_fields_pair!(
818			$left,
819			$right,
820			TraceQueryRequest {
821				id,
822				start_ms,
823				end_ms,
824				limit,
825			}
826		);
827		impl_same_fields_pair!($left, $right, TraceQueryResponse { rid, payload });
828		impl_same_fields_pair!($left, $right, QueueRequest { id, limit });
829		impl_same_fields_pair!(
830			$left,
831			$right,
832			QueueMessageSummary {
833				id,
834				name,
835				created_at_ms,
836			}
837		);
838		impl_queue_status_pair!($left, $right);
839		impl_queue_response_pair!($left, $right);
840		impl_same_fields_pair!($left, $right, QueueUpdated { queue_size });
841		impl_same_fields_pair!($left, $right, WorkflowHistoryRequest { id });
842		impl_same_fields_pair!(
843			$left,
844			$right,
845			WorkflowHistoryResponse {
846				rid,
847				history,
848				is_workflow_enabled,
849			}
850		);
851		impl_same_fields_pair!($left, $right, WorkflowHistoryUpdated { history });
852		impl_init_pair!($left, $right);
853	};
854}
855
856macro_rules! impl_database_pair {
857	($left:ident, $right:ident) => {
858		impl_same_fields_pair!($left, $right, DatabaseSchemaRequest { id });
859		impl_same_fields_pair!($left, $right, DatabaseSchemaResponse { rid, schema });
860		impl_same_fields_pair!(
861			$left,
862			$right,
863			DatabaseTableRowsRequest {
864				id,
865				table,
866				limit,
867				offset,
868			}
869		);
870		impl_same_fields_pair!($left, $right, DatabaseTableRowsResponse { rid, result });
871	};
872}
873
874impl_common_actor_pair!(v1, v2);
875impl_common_actor_pair!(v2, v3);
876impl_common_actor_pair!(v3, v4);
877impl_common_actor_pair!(v4, v5);
878impl_queue_workflow_pair!(v2, v3);
879impl_queue_workflow_pair!(v3, v4);
880impl_database_pair!(v3, v4);
881impl_database_pair!(v4, v5);
882
883// v4 <-> v5 differ only in `Init` (v5 adds `tab_config`), which is converted
884// inline in the ToClient converters. Every other type is field-identical, so
885// generate the plain field-copy `From` impls here. `Init` is deliberately
886// excluded (no `impl_init_pair!`) because its shapes differ.
887impl_same_fields_pair!(
888	v4,
889	v5,
890	TraceQueryRequest {
891		id,
892		start_ms,
893		end_ms,
894		limit,
895	}
896);
897impl_same_fields_pair!(v4, v5, TraceQueryResponse { rid, payload });
898impl_same_fields_pair!(v4, v5, QueueRequest { id, limit });
899impl_same_fields_pair!(
900	v4,
901	v5,
902	QueueMessageSummary {
903		id,
904		name,
905		created_at_ms,
906	}
907);
908impl_queue_status_pair!(v4, v5);
909impl_queue_response_pair!(v4, v5);
910impl_same_fields_pair!(v4, v5, QueueUpdated { queue_size });
911impl_same_fields_pair!(v4, v5, WorkflowHistoryRequest { id });
912impl_same_fields_pair!(
913	v4,
914	v5,
915	WorkflowHistoryResponse {
916		rid,
917		history,
918		is_workflow_enabled,
919	}
920);
921impl_same_fields_pair!(v4, v5, WorkflowHistoryUpdated { history });
922impl_same_fields_pair!(v4, v5, WorkflowReplayRequest { id, entry_id });
923impl_same_fields_pair!(
924	v4,
925	v5,
926	WorkflowReplayResponse {
927		rid,
928		history,
929		is_workflow_enabled,
930	}
931);
932
933impl From<v4::ToServerBody> for v5::ToServerBody {
934	fn from(value: v4::ToServerBody) -> Self {
935		match value {
936			v4::ToServerBody::PatchStateRequest(req) => Self::PatchStateRequest(req.into()),
937			v4::ToServerBody::StateRequest(req) => Self::StateRequest(req.into()),
938			v4::ToServerBody::ConnectionsRequest(req) => Self::ConnectionsRequest(req.into()),
939			v4::ToServerBody::ActionRequest(req) => Self::ActionRequest(req.into()),
940			v4::ToServerBody::RpcsListRequest(req) => Self::RpcsListRequest(req.into()),
941			v4::ToServerBody::TraceQueryRequest(req) => Self::TraceQueryRequest(req.into()),
942			v4::ToServerBody::QueueRequest(req) => Self::QueueRequest(req.into()),
943			v4::ToServerBody::WorkflowHistoryRequest(req) => {
944				Self::WorkflowHistoryRequest(req.into())
945			}
946			v4::ToServerBody::WorkflowReplayRequest(req) => Self::WorkflowReplayRequest(req.into()),
947			v4::ToServerBody::DatabaseSchemaRequest(req) => Self::DatabaseSchemaRequest(req.into()),
948			v4::ToServerBody::DatabaseTableRowsRequest(req) => {
949				Self::DatabaseTableRowsRequest(req.into())
950			}
951		}
952	}
953}
954
955impl From<v5::ToServerBody> for v4::ToServerBody {
956	fn from(value: v5::ToServerBody) -> Self {
957		match value {
958			v5::ToServerBody::PatchStateRequest(req) => Self::PatchStateRequest(req.into()),
959			v5::ToServerBody::StateRequest(req) => Self::StateRequest(req.into()),
960			v5::ToServerBody::ConnectionsRequest(req) => Self::ConnectionsRequest(req.into()),
961			v5::ToServerBody::ActionRequest(req) => Self::ActionRequest(req.into()),
962			v5::ToServerBody::RpcsListRequest(req) => Self::RpcsListRequest(req.into()),
963			v5::ToServerBody::TraceQueryRequest(req) => Self::TraceQueryRequest(req.into()),
964			v5::ToServerBody::QueueRequest(req) => Self::QueueRequest(req.into()),
965			v5::ToServerBody::WorkflowHistoryRequest(req) => {
966				Self::WorkflowHistoryRequest(req.into())
967			}
968			v5::ToServerBody::WorkflowReplayRequest(req) => Self::WorkflowReplayRequest(req.into()),
969			v5::ToServerBody::DatabaseSchemaRequest(req) => Self::DatabaseSchemaRequest(req.into()),
970			v5::ToServerBody::DatabaseTableRowsRequest(req) => {
971				Self::DatabaseTableRowsRequest(req.into())
972			}
973		}
974	}
975}
976
977impl From<v4::ToServer> for v5::ToServer {
978	fn from(value: v4::ToServer) -> Self {
979		Self {
980			body: value.body.into(),
981		}
982	}
983}
984
985impl From<v5::ToServer> for v4::ToServer {
986	fn from(value: v5::ToServer) -> Self {
987		Self {
988			body: value.body.into(),
989		}
990	}
991}
992
993impl From<v2::ToServerBody> for v3::ToServerBody {
994	fn from(value: v2::ToServerBody) -> Self {
995		match value {
996			v2::ToServerBody::PatchStateRequest(req) => Self::PatchStateRequest(req.into()),
997			v2::ToServerBody::StateRequest(req) => Self::StateRequest(req.into()),
998			v2::ToServerBody::ConnectionsRequest(req) => Self::ConnectionsRequest(req.into()),
999			v2::ToServerBody::ActionRequest(req) => Self::ActionRequest(req.into()),
1000			v2::ToServerBody::RpcsListRequest(req) => Self::RpcsListRequest(req.into()),
1001			v2::ToServerBody::TraceQueryRequest(req) => Self::TraceQueryRequest(req.into()),
1002			v2::ToServerBody::QueueRequest(req) => Self::QueueRequest(req.into()),
1003			v2::ToServerBody::WorkflowHistoryRequest(req) => {
1004				Self::WorkflowHistoryRequest(req.into())
1005			}
1006		}
1007	}
1008}
1009
1010impl From<v2::ToServer> for v3::ToServer {
1011	fn from(value: v2::ToServer) -> Self {
1012		Self {
1013			body: value.body.into(),
1014		}
1015	}
1016}
1017
1018impl From<v2::ToClientBody> for v3::ToClientBody {
1019	fn from(value: v2::ToClientBody) -> Self {
1020		match value {
1021			v2::ToClientBody::StateResponse(resp) => Self::StateResponse(resp.into()),
1022			v2::ToClientBody::ConnectionsResponse(resp) => Self::ConnectionsResponse(resp.into()),
1023			v2::ToClientBody::ActionResponse(resp) => Self::ActionResponse(resp.into()),
1024			v2::ToClientBody::ConnectionsUpdated(update) => Self::ConnectionsUpdated(update.into()),
1025			v2::ToClientBody::QueueUpdated(update) => Self::QueueUpdated(update.into()),
1026			v2::ToClientBody::StateUpdated(update) => Self::StateUpdated(update.into()),
1027			v2::ToClientBody::WorkflowHistoryUpdated(update) => {
1028				Self::WorkflowHistoryUpdated(update.into())
1029			}
1030			v2::ToClientBody::RpcsListResponse(resp) => Self::RpcsListResponse(resp.into()),
1031			v2::ToClientBody::TraceQueryResponse(resp) => Self::TraceQueryResponse(resp.into()),
1032			v2::ToClientBody::QueueResponse(resp) => Self::QueueResponse(resp.into()),
1033			v2::ToClientBody::WorkflowHistoryResponse(resp) => {
1034				Self::WorkflowHistoryResponse(resp.into())
1035			}
1036			v2::ToClientBody::Error(error) => Self::Error(error.into()),
1037			v2::ToClientBody::Init(init) => Self::Init(init.into()),
1038		}
1039	}
1040}
1041
1042impl From<v2::ToClient> for v3::ToClient {
1043	fn from(value: v2::ToClient) -> Self {
1044		Self {
1045			body: value.body.into(),
1046		}
1047	}
1048}
1049
1050fn dropped_error(message: &str) -> v2::Error {
1051	v2::Error {
1052		message: message.to_owned(),
1053	}
1054}
1055
1056#[cfg(test)]
1057mod tests {
1058	use super::*;
1059
1060	#[test]
1061	fn v3_database_schema_request_keeps_meaning_when_upgrading_to_v4() {
1062		let request = ToServer::V3(v3::ToServer {
1063			body: v3::ToServerBody::DatabaseSchemaRequest(v3::DatabaseSchemaRequest {
1064				id: Uint(7),
1065			}),
1066		});
1067
1068		let ToServer::V4(upgraded) = ToServer::v3_to_v4(request).unwrap() else {
1069			panic!("expected v4 request")
1070		};
1071
1072		assert!(matches!(
1073			upgraded.body,
1074			v4::ToServerBody::DatabaseSchemaRequest(v4::DatabaseSchemaRequest { id }) if id == Uint(7)
1075		));
1076	}
1077
1078	fn v5_init_with_tabs() -> v5::Init {
1079		v5::Init {
1080			connections: Vec::new(),
1081			state: None,
1082			is_state_enabled: false,
1083			rpcs: Vec::new(),
1084			is_database_enabled: false,
1085			queue_size: Uint(0),
1086			workflow_history: None,
1087			is_workflow_enabled: false,
1088			tab_config: vec![v5::TabConfigEntry {
1089				id: "custom".to_owned(),
1090				label: Some("Custom".to_owned()),
1091				icon: Some("star".to_owned()),
1092				hidden: false,
1093			}],
1094		}
1095	}
1096
1097	#[test]
1098	fn v5_init_tab_config_drops_when_downgrading_to_v4() {
1099		let response = ToClient::V5(v5::ToClient {
1100			body: v5::ToClientBody::Init(v5_init_with_tabs()),
1101		});
1102
1103		let ToClient::V4(downgraded) = ToClient::v5_to_v4(response).unwrap() else {
1104			panic!("expected v4 response")
1105		};
1106
1107		// v4 has no tab_config field, so the tabs are dropped but the rest of
1108		// the Init survives the downgrade.
1109		assert!(matches!(
1110			downgraded.body,
1111			v4::ToClientBody::Init(v4::Init {
1112				is_workflow_enabled: false,
1113				..
1114			})
1115		));
1116	}
1117
1118	#[test]
1119	fn v4_init_upgrades_to_v5_with_empty_tab_config() {
1120		let response = ToClient::V4(v4::ToClient {
1121			body: v4::ToClientBody::Init(v4::Init {
1122				connections: Vec::new(),
1123				state: None,
1124				is_state_enabled: false,
1125				rpcs: Vec::new(),
1126				is_database_enabled: false,
1127				queue_size: Uint(0),
1128				workflow_history: None,
1129				is_workflow_enabled: false,
1130			}),
1131		});
1132
1133		let ToClient::V5(upgraded) = ToClient::v4_to_v5(response).unwrap() else {
1134			panic!("expected v5 response")
1135		};
1136
1137		let v5::ToClientBody::Init(init) = upgraded.body else {
1138			panic!("expected Init body")
1139		};
1140		assert!(init.tab_config.is_empty());
1141	}
1142
1143	#[test]
1144	fn v5_init_tab_config_round_trips_at_version_5() {
1145		let original = v5_init_with_tabs();
1146		let encoded = ToClient::V5(v5::ToClient {
1147			body: v5::ToClientBody::Init(original.clone()),
1148		})
1149		.serialize_with_embedded_version(5)
1150		.unwrap();
1151
1152		// `deserialize_with_embedded_version` upgrades to and returns the latest
1153		// wire type (v5::ToClient), not the versioned enum.
1154		let decoded =
1155			<ToClient as OwnedVersionedData>::deserialize_with_embedded_version(&encoded).unwrap();
1156		let v5::ToClientBody::Init(init) = decoded.body else {
1157			panic!("expected Init body")
1158		};
1159		assert_eq!(init.tab_config, original.tab_config);
1160	}
1161
1162	#[test]
1163	fn v4_workflow_replay_response_downgrades_to_v3_error() {
1164		let response = ToClient::V4(v4::ToClient {
1165			body: v4::ToClientBody::WorkflowReplayResponse(v4::WorkflowReplayResponse {
1166				rid: Uint(11),
1167				history: Some(b"workflow".to_vec()),
1168				is_workflow_enabled: true,
1169			}),
1170		});
1171
1172		let ToClient::V3(downgraded) = ToClient::v4_to_v3(response).unwrap() else {
1173			panic!("expected v3 response")
1174		};
1175
1176		assert_eq!(
1177			downgraded.body,
1178			v3::ToClientBody::Error(v3::Error {
1179				message: WORKFLOW_HISTORY_DROPPED_ERROR.to_owned(),
1180			})
1181		);
1182	}
1183}