Skip to main content

lean_rs_worker_protocol/
protocol.rs

1//! Length-delimited frame codec and message payload types for the
2//! parent ↔ child worker process boundary.
3//!
4//! ## Additive evolution
5//!
6//! Every public enum here is `#[non_exhaustive]` so the wire format can gain
7//! a new request, response, or message kind without forcing a semver-major
8//! bump on consumers. Most structs are also `#[non_exhaustive]` and expose
9//! `pub fn new(...)` constructors so the shapes can grow fields without
10//! breaking external builders. The exception is [`DataRow`], which is built
11//! so frequently with struct-literal syntax (tests, harnesses, fakes) that
12//! the ergonomic cost of `#[non_exhaustive]` outweighs the additive-evolution
13//! benefit; the wire schema for a data row is also already fixed by the
14//! stream contract.
15
16use std::collections::BTreeMap;
17use std::fmt;
18use std::io::{self, Read, Write};
19use std::time::Duration;
20
21use serde::{Deserialize, Serialize};
22use serde_json::Value;
23use serde_json::value::RawValue;
24
25use crate::types::{
26    LeanWorkerCapabilityMetadata, LeanWorkerDeclarationFilter, LeanWorkerDeclarationInspectionRequest,
27    LeanWorkerDeclarationInspectionResult, LeanWorkerDeclarationRow, LeanWorkerDeclarationSearch,
28    LeanWorkerDeclarationSearchResult, LeanWorkerDeclarationType, LeanWorkerDeclarationVerificationRequest,
29    LeanWorkerDeclarationVerificationResult, LeanWorkerDoctorReport, LeanWorkerElabOptions, LeanWorkerElabResult,
30    LeanWorkerKernelResult, LeanWorkerMetaResult, LeanWorkerMetaTransparency, LeanWorkerModuleQuery,
31    LeanWorkerModuleQueryBatchOutcome, LeanWorkerModuleQueryOutcome, LeanWorkerModuleQuerySelector,
32    LeanWorkerOutputBudgets, LeanWorkerProofAttemptRequest, LeanWorkerProofAttemptResult, LeanWorkerRendered,
33};
34
35/// Wire protocol version negotiated between parent and child during the
36/// handshake frame. Bump only on a breaking wire change.
37pub const PROTOCOL_VERSION: u16 = 8;
38
39/// Default per-frame size limit applied by the parent when no explicit cap is
40/// configured on the capability builder.
41///
42/// The cap is a parent-side policy decision negotiated to the child at
43/// handshake time via [`Message::ConfigureFrameLimit`]. Both [`write_frame`]
44/// and [`read_frame`] reject frames whose serialised JSON payload exceeds the
45/// cap passed in, so a runaway producer cannot make the peer allocate without
46/// bound. The cap is per-connection—set once at handshake, applied to every
47/// subsequent frame in both directions.
48pub const MAX_FRAME_BYTES: u32 = 1024 * 1024;
49
50/// Floor on the configurable frame cap. Trivial requests and the handshake
51/// itself must fit inside this; callers cannot configure smaller.
52pub const MIN_FRAME_BYTES: u32 = 64 * 1024;
53
54/// Ceiling on the configurable frame cap. Prevents callers from defeating the
55/// memory-safety policy by passing an absurdly large value.
56pub const MAX_FRAME_BYTES_HARD_CAP: u32 = 256 * 1024 * 1024;
57
58/// Versioned envelope around a single protocol [`Message`].
59#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
60#[non_exhaustive]
61pub struct Frame {
62    /// Protocol version the sender used. Receivers reject mismatches.
63    pub version: u16,
64    /// Inner message payload.
65    pub message: Message,
66}
67
68impl Frame {
69    /// Wrap `message` in a frame tagged with the current [`PROTOCOL_VERSION`].
70    #[must_use]
71    pub fn new(message: Message) -> Self {
72        Self {
73            version: PROTOCOL_VERSION,
74            message,
75        }
76    }
77}
78
79/// One protocol message exchanged over the worker boundary.
80#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
81#[serde(tag = "type", content = "body", rename_all = "snake_case")]
82#[non_exhaustive]
83pub enum Message {
84    /// Sent by the child immediately after spawn to advertise its version and
85    /// supported protocol revision.
86    Handshake {
87        /// `lean-rs-worker-child` package version.
88        worker_version: String,
89        /// Protocol version the child speaks. Parent rejects mismatches.
90        protocol_version: u16,
91    },
92    /// Sent by the parent immediately after the handshake frame to negotiate
93    /// the per-frame size cap for the remainder of this connection.
94    ///
95    /// The parent owns the memory-safety policy: it clamps the requested cap
96    /// into <code>[[MIN_FRAME_BYTES], [MAX_FRAME_BYTES_HARD_CAP]]</code>
97    /// before sending. The child installs the value as-is.
98    ConfigureFrameLimit {
99        /// Per-frame byte cap applied in both directions for this connection.
100        max_frame_bytes: u32,
101    },
102    /// Parent → child request frame.
103    Request(Request),
104    /// Child → parent terminal response for one request.
105    Response(Response),
106    /// Child → parent intermediate diagnostic frame.
107    Diagnostic(Diagnostic),
108    /// Child → parent intermediate progress frame.
109    ProgressTick(ProgressTick),
110    /// Child → parent streaming data row frame.
111    DataRow(DataRow),
112    /// Child → parent fatal exit notification carrying the captured stderr
113    /// just before the child process tears down.
114    FatalExit(FatalExit),
115}
116
117/// Parent-issued worker request body.
118#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
119#[serde(tag = "op", rename_all = "snake_case")]
120#[non_exhaustive]
121pub enum Request {
122    Health,
123    LoadFixtureCapability {
124        manifest_path: String,
125    },
126    CallFixtureMul {
127        manifest_path: String,
128        lhs: u64,
129        rhs: u64,
130    },
131    TriggerLeanPanic {
132        manifest_path: String,
133    },
134    OpenHostSession {
135        project_root: String,
136        mode: HostSessionMode,
137        imports: Vec<String>,
138    },
139    Elaborate {
140        source: String,
141        options: LeanWorkerElabOptions,
142    },
143    KernelCheck {
144        source: String,
145        options: LeanWorkerElabOptions,
146        progress: bool,
147    },
148    DeclarationKinds {
149        names: Vec<String>,
150        progress: bool,
151    },
152    DeclarationNames {
153        names: Vec<String>,
154        progress: bool,
155    },
156    RunDataStream {
157        export: String,
158        request_json: String,
159        progress: bool,
160    },
161    CapabilityMetadata {
162        export: String,
163        request_json: String,
164    },
165    CapabilityDoctor {
166        export: String,
167        request_json: String,
168    },
169    JsonCommand {
170        export: String,
171        request_json: String,
172    },
173    InferType {
174        source: String,
175        options: LeanWorkerElabOptions,
176    },
177    Whnf {
178        source: String,
179        options: LeanWorkerElabOptions,
180    },
181    IsDefEq {
182        lhs: String,
183        rhs: String,
184        transparency: LeanWorkerMetaTransparency,
185        options: LeanWorkerElabOptions,
186    },
187    Describe {
188        name: String,
189    },
190    SearchDeclarations {
191        search: LeanWorkerDeclarationSearch,
192    },
193    DeclarationType {
194        name: String,
195        max_bytes: usize,
196    },
197    InspectDeclaration {
198        request: LeanWorkerDeclarationInspectionRequest,
199    },
200    AttemptProof {
201        request: LeanWorkerProofAttemptRequest,
202        options: LeanWorkerElabOptions,
203        progress: bool,
204    },
205    VerifyDeclaration {
206        request: LeanWorkerDeclarationVerificationRequest,
207        options: LeanWorkerElabOptions,
208        progress: bool,
209    },
210    ListDeclarationsStrings {
211        filter: LeanWorkerDeclarationFilter,
212        progress: bool,
213    },
214    DescribeBulk {
215        names: Vec<String>,
216        progress: bool,
217    },
218    ProcessModuleQuery {
219        source: String,
220        query: LeanWorkerModuleQuery,
221        options: LeanWorkerElabOptions,
222    },
223    ProcessModuleQueryBatch {
224        source: String,
225        selectors: Vec<LeanWorkerModuleQuerySelector>,
226        budgets: LeanWorkerOutputBudgets,
227        options: LeanWorkerElabOptions,
228    },
229    ClearModuleSnapshotCache,
230    // Private harness requests that exercise streaming frame behavior.
231    // Not part of the public row sink API.
232    EmitTestRows {
233        streams: Vec<String>,
234    },
235    EmitTestRowsThenExit,
236    EmitTestRowsThenPanic,
237    Terminate,
238}
239
240/// How the worker child should load host-session capabilities.
241#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
242#[serde(tag = "kind", rename_all = "snake_case")]
243#[non_exhaustive]
244pub enum HostSessionMode {
245    /// Open a user capability dylib and the bundled host shims.
246    Capability {
247        package: String,
248        lib_name: String,
249        manifest_path: Option<String>,
250    },
251    /// Open only the bundled host shims.
252    ShimsOnly,
253}
254
255/// Child-issued terminal response body for one [`Request`].
256#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
257#[serde(tag = "status", rename_all = "snake_case")]
258#[non_exhaustive]
259pub enum Response {
260    HealthOk,
261    CapabilityLoaded,
262    U64 {
263        value: u64,
264    },
265    HostSessionOpened,
266    Elaboration {
267        outcome: LeanWorkerElabResult,
268    },
269    KernelCheck {
270        outcome: LeanWorkerKernelResult,
271    },
272    Strings {
273        values: Vec<String>,
274    },
275    StreamComplete {
276        summary: StreamSummary,
277    },
278    StreamExportFailed {
279        status_byte: u8,
280    },
281    StreamCallbackFailed {
282        status_byte: u8,
283        description: String,
284    },
285    StreamRowMalformed {
286        message: String,
287    },
288    CapabilityMetadata {
289        metadata: LeanWorkerCapabilityMetadata,
290    },
291    CapabilityDoctor {
292        report: LeanWorkerDoctorReport,
293    },
294    CapabilityMetadataMalformed {
295        message: String,
296    },
297    CapabilityDoctorMalformed {
298        message: String,
299    },
300    JsonCommand {
301        response_json: String,
302    },
303    MetaExpr {
304        result: LeanWorkerMetaResult<LeanWorkerRendered>,
305    },
306    MetaBool {
307        result: LeanWorkerMetaResult<bool>,
308    },
309    Declaration {
310        row: Option<LeanWorkerDeclarationRow>,
311    },
312    DeclarationSearch {
313        result: LeanWorkerDeclarationSearchResult,
314    },
315    DeclarationType {
316        row: Option<LeanWorkerDeclarationType>,
317    },
318    DeclarationInspection {
319        result: LeanWorkerDeclarationInspectionResult,
320    },
321    ProofAttempt {
322        result: LeanWorkerProofAttemptResult,
323    },
324    DeclarationVerification {
325        result: LeanWorkerDeclarationVerificationResult,
326    },
327    DeclarationBulk {
328        rows: Vec<LeanWorkerDeclarationRow>,
329    },
330    ProcessModuleQuery {
331        outcome: LeanWorkerModuleQueryOutcome,
332    },
333    ProcessModuleQueryBatch {
334        outcome: LeanWorkerModuleQueryBatchOutcome,
335    },
336    ModuleSnapshotCacheCleared {
337        result: crate::types::LeanWorkerModuleSnapshotCacheClearResult,
338    },
339    RowsComplete {
340        count: u64,
341    },
342    Terminating,
343    Error {
344        code: String,
345        message: String,
346    },
347}
348
349/// Intermediate diagnostic frame emitted by the child during a request.
350#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
351#[non_exhaustive]
352pub struct Diagnostic {
353    /// Stable diagnostic code identifier.
354    pub code: String,
355    /// Bounded human-readable diagnostic message.
356    pub message: String,
357}
358
359impl Diagnostic {
360    /// Build a diagnostic frame payload.
361    #[must_use]
362    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
363        Self {
364            code: code.into(),
365            message: message.into(),
366        }
367    }
368}
369
370/// Intermediate progress frame emitted by the child during a long-running
371/// request.
372#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
373#[non_exhaustive]
374pub struct ProgressTick {
375    /// Phase name the child is reporting progress for.
376    pub phase: String,
377    /// Items completed so far in this phase.
378    pub current: u64,
379    /// Total expected items in this phase, if known.
380    pub total: Option<u64>,
381}
382
383impl ProgressTick {
384    /// Build a progress-tick frame payload.
385    #[must_use]
386    pub fn new(phase: impl Into<String>, current: u64, total: Option<u64>) -> Self {
387        Self {
388            phase: phase.into(),
389            current,
390            total,
391        }
392    }
393}
394
395/// One row in a streaming response.
396///
397/// Construction goes through [`DataRowEmitter::next`] in the child runtime;
398/// direct struct-literal construction is permitted in tests and harnesses.
399/// This struct intentionally stays exhaustive: see the module-level note on
400/// additive evolution.
401#[derive(Clone, Debug, Deserialize, Serialize)]
402pub struct DataRow {
403    /// Logical stream this row belongs to.
404    pub stream: String,
405    /// Per-stream monotonically increasing sequence number.
406    pub sequence: u64,
407    /// Opaque JSON payload (deserialised lazily by the parent).
408    pub payload: Box<RawValue>,
409}
410
411impl PartialEq for DataRow {
412    fn eq(&self, other: &Self) -> bool {
413        self.stream == other.stream && self.sequence == other.sequence && self.payload.get() == other.payload.get()
414    }
415}
416
417impl Eq for DataRow {}
418
419/// Terminal stream-completion summary returned alongside a streaming response.
420#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
421#[non_exhaustive]
422pub struct StreamSummary {
423    /// Total rows emitted across all streams.
424    pub total_rows: u64,
425    /// Per-stream row counts at completion.
426    pub per_stream_counts: BTreeMap<String, u64>,
427    /// Child-side elapsed time in microseconds.
428    pub elapsed_micros: u64,
429    /// Optional downstream-defined terminal metadata.
430    pub metadata: Option<Value>,
431}
432
433impl StreamSummary {
434    /// Build a stream-completion summary, clamping the elapsed duration into
435    /// the `u64` micros field.
436    #[must_use]
437    pub fn new(
438        total_rows: u64,
439        per_stream_counts: BTreeMap<String, u64>,
440        elapsed: Duration,
441        metadata: Option<Value>,
442    ) -> Self {
443        Self {
444            total_rows,
445            per_stream_counts,
446            elapsed_micros: elapsed.as_micros().try_into().unwrap_or(u64::MAX),
447            metadata,
448        }
449    }
450}
451
452/// Stateful emitter that assigns per-stream sequence numbers and tracks the
453/// running row count for the terminal [`StreamSummary`].
454#[derive(Debug, Default)]
455#[non_exhaustive]
456pub struct DataRowEmitter {
457    sequences: BTreeMap<String, u64>,
458    count: u64,
459}
460
461impl DataRowEmitter {
462    /// Allocate the next [`DataRow`] for `stream`, advancing the per-stream
463    /// sequence and the overall count.
464    pub fn next(&mut self, stream: impl Into<String>, payload: Box<RawValue>) -> DataRow {
465        let stream = stream.into();
466        let sequence = self.sequences.entry(stream.clone()).or_insert(0);
467        let row = DataRow {
468            stream,
469            sequence: *sequence,
470            payload,
471        };
472        *sequence = sequence.saturating_add(1);
473        self.count = self.count.saturating_add(1);
474        row
475    }
476
477    #[cfg(test)]
478    fn emit(
479        &mut self,
480        writer: &mut impl Write,
481        stream: impl Into<String>,
482        payload: &Value,
483    ) -> Result<(), ProtocolError> {
484        let row = self.next(stream, serde_json::value::to_raw_value(payload)?);
485        write_frame(writer, Message::DataRow(row), MAX_FRAME_BYTES)
486    }
487
488    /// Total rows emitted across all streams.
489    #[must_use]
490    pub fn count(&self) -> u64 {
491        self.count
492    }
493
494    /// Snapshot of per-stream row counts.
495    #[must_use]
496    pub fn per_stream_counts(&self) -> BTreeMap<String, u64> {
497        self.sequences.clone()
498    }
499}
500
501/// Final frame the child writes before it tears down on an unrecoverable
502/// failure.
503#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
504#[non_exhaustive]
505pub struct FatalExit {
506    /// Stringified `ExitStatus` of the child process.
507    pub status: String,
508    /// Captured stderr tail at fatal-exit time.
509    pub stderr: String,
510}
511
512impl FatalExit {
513    /// Build a fatal-exit frame payload.
514    #[must_use]
515    pub fn new(status: impl Into<String>, stderr: impl Into<String>) -> Self {
516        Self {
517            status: status.into(),
518            stderr: stderr.into(),
519        }
520    }
521}
522
523/// Failure modes the codec can produce while reading or writing a frame.
524#[derive(Debug)]
525#[non_exhaustive]
526pub enum ProtocolError {
527    /// Underlying I/O failure (pipe closed, partial read, etc.).
528    Io(io::Error),
529    /// JSON serialisation or deserialisation failure.
530    Json(serde_json::Error),
531    /// A frame body exceeded [`MAX_FRAME_BYTES`].
532    FrameTooLarge {
533        /// Observed frame size in bytes.
534        len: u32,
535        /// Maximum allowed frame size.
536        max: u32,
537    },
538    /// Peer's frame version did not match this binary's [`PROTOCOL_VERSION`].
539    VersionMismatch {
540        /// Version this binary expected.
541        expected: u16,
542        /// Version the peer used.
543        actual: u16,
544    },
545}
546
547impl ProtocolError {
548    /// Whether the underlying I/O error indicates the peer's pipe was closed
549    /// (`UnexpectedEof`). Used by callers to distinguish a clean fatal exit
550    /// from a true protocol failure.
551    #[must_use]
552    pub fn is_eof(&self) -> bool {
553        matches!(self, Self::Io(err) if err.kind() == io::ErrorKind::UnexpectedEof)
554    }
555}
556
557impl fmt::Display for ProtocolError {
558    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
559        match self {
560            Self::Io(err) => write!(f, "worker protocol I/O failed: {err}"),
561            Self::Json(err) => write!(f, "worker protocol JSON decode failed: {err}"),
562            Self::FrameTooLarge { len, max } => {
563                write!(f, "worker protocol frame too large: {len} bytes exceeds {max}")
564            }
565            Self::VersionMismatch { expected, actual } => {
566                write!(
567                    f,
568                    "worker protocol version mismatch: expected {expected}, received {actual}"
569                )
570            }
571        }
572    }
573}
574
575impl std::error::Error for ProtocolError {}
576
577impl From<io::Error> for ProtocolError {
578    fn from(value: io::Error) -> Self {
579        Self::Io(value)
580    }
581}
582
583impl From<serde_json::Error> for ProtocolError {
584    fn from(value: serde_json::Error) -> Self {
585        Self::Json(value)
586    }
587}
588
589/// Serialise `message` as a length-delimited JSON frame to `writer`.
590///
591/// `max_frame_bytes` is the per-frame cap negotiated for this connection.
592/// Until the handshake completes, callers pass [`MAX_FRAME_BYTES`] as the
593/// default; afterwards the supervisor passes the
594/// [`Message::ConfigureFrameLimit`] value installed on the connection.
595///
596/// # Errors
597///
598/// Returns [`ProtocolError::FrameTooLarge`] if the serialised body would
599/// exceed `max_frame_bytes`, or the underlying [`ProtocolError::Io`] /
600/// [`ProtocolError::Json`] for codec failures.
601pub fn write_frame(writer: &mut impl Write, message: Message, max_frame_bytes: u32) -> Result<(), ProtocolError> {
602    let bytes = serde_json::to_vec(&Frame::new(message))?;
603    let len = u32::try_from(bytes.len()).map_err(|_| ProtocolError::FrameTooLarge {
604        len: u32::MAX,
605        max: max_frame_bytes,
606    })?;
607    if len > max_frame_bytes {
608        return Err(ProtocolError::FrameTooLarge {
609            len,
610            max: max_frame_bytes,
611        });
612    }
613    writer.write_all(&len.to_be_bytes())?;
614    writer.write_all(&bytes)?;
615    writer.flush()?;
616    Ok(())
617}
618
619/// Read one length-delimited JSON frame from `reader`.
620///
621/// `max_frame_bytes` is the per-frame cap negotiated for this connection. See
622/// [`write_frame`] for the back-compat default and post-handshake semantics.
623///
624/// # Errors
625///
626/// Returns [`ProtocolError::FrameTooLarge`] if the framed length exceeds
627/// `max_frame_bytes` (rejected before allocation),
628/// [`ProtocolError::VersionMismatch`] if the peer's version does not match
629/// [`PROTOCOL_VERSION`], or the underlying [`ProtocolError::Io`] /
630/// [`ProtocolError::Json`] for codec failures.
631pub fn read_frame(reader: &mut impl Read, max_frame_bytes: u32) -> Result<Frame, ProtocolError> {
632    let mut len_bytes = [0_u8; 4];
633    reader.read_exact(&mut len_bytes)?;
634    let len = u32::from_be_bytes(len_bytes);
635    if len > max_frame_bytes {
636        return Err(ProtocolError::FrameTooLarge {
637            len,
638            max: max_frame_bytes,
639        });
640    }
641    let mut bytes = vec![0_u8; len as usize];
642    reader.read_exact(&mut bytes)?;
643    let frame: Frame = serde_json::from_slice(&bytes)?;
644    if frame.version != PROTOCOL_VERSION {
645        return Err(ProtocolError::VersionMismatch {
646            expected: PROTOCOL_VERSION,
647            actual: frame.version,
648        });
649    }
650    Ok(frame)
651}
652
653#[cfg(test)]
654mod tests {
655    #![allow(clippy::expect_used, clippy::panic)]
656
657    use std::io::Cursor;
658
659    use serde_json::json;
660    use serde_json::value::RawValue;
661
662    use super::{
663        DataRow, DataRowEmitter, MAX_FRAME_BYTES, MAX_FRAME_BYTES_HARD_CAP, MIN_FRAME_BYTES, Message, ProtocolError,
664        Request, Response, read_frame, write_frame,
665    };
666    use crate::types::{
667        LeanWorkerDeclarationFilter, LeanWorkerDeclarationFlags, LeanWorkerDeclarationInspection,
668        LeanWorkerDeclarationInspectionFields, LeanWorkerDeclarationInspectionRequest,
669        LeanWorkerDeclarationInspectionResult, LeanWorkerDeclarationNameMatch, LeanWorkerDeclarationProofSearchFacts,
670        LeanWorkerDeclarationSearch, LeanWorkerDeclarationSearchBias, LeanWorkerDeclarationSearchFacts,
671        LeanWorkerDeclarationSearchPruning, LeanWorkerDeclarationSearchResult, LeanWorkerDeclarationSearchRow,
672        LeanWorkerDeclarationSearchScope, LeanWorkerDeclarationSearchTimings, LeanWorkerDeclarationTargetInfo,
673        LeanWorkerDeclarationVerificationFacts, LeanWorkerDeclarationVerificationRequest,
674        LeanWorkerDeclarationVerificationResult, LeanWorkerDeclarationVerificationStatus,
675        LeanWorkerDeclarationVerificationTarget, LeanWorkerElabFailure, LeanWorkerElabOptions,
676        LeanWorkerModuleCacheStatus, LeanWorkerModuleQuery, LeanWorkerModuleQueryBatchEnvelope,
677        LeanWorkerModuleQueryBatchItem, LeanWorkerModuleQueryBatchOutcome, LeanWorkerModuleQueryBatchResult,
678        LeanWorkerModuleQueryCacheFacts, LeanWorkerModuleQueryOutcome, LeanWorkerModuleQueryResult,
679        LeanWorkerModuleQuerySelector, LeanWorkerModuleQueryTimings, LeanWorkerModuleSourceSpan,
680        LeanWorkerOutputBudgets, LeanWorkerProofAttemptEnvelope, LeanWorkerProofAttemptRequest,
681        LeanWorkerProofAttemptResult, LeanWorkerProofAttemptRow, LeanWorkerProofAttemptStatus,
682        LeanWorkerProofCandidate, LeanWorkerProofEditTarget, LeanWorkerProofPositionSelector,
683        LeanWorkerProofPositionSummary, LeanWorkerProofStateResult, LeanWorkerRenderedInfo, LeanWorkerRendering,
684        LeanWorkerSorryPolicy, LeanWorkerSourceRange, LeanWorkerTypeAtResult,
685    };
686
687    fn raw_json(value: &serde_json::Value) -> Box<RawValue> {
688        serde_json::value::to_raw_value(value).expect("test JSON converts to raw value")
689    }
690
691    fn assert_frame_round_trips(message: &Message) {
692        let mut bytes = Vec::new();
693        write_frame(&mut bytes, message.clone(), MAX_FRAME_BYTES).expect("frame writes");
694        let frame = read_frame(&mut Cursor::new(bytes), MAX_FRAME_BYTES).expect("frame reads");
695        assert_eq!(&frame.message, message);
696    }
697
698    fn declaration_target_info_fixture(declaration_name: &str) -> LeanWorkerDeclarationTargetInfo {
699        let span = LeanWorkerModuleSourceSpan {
700            start_line: 1,
701            start_column: 1,
702            end_line: 1,
703            end_column: 10,
704        };
705        let short_name = declaration_name.rsplit('.').next().unwrap_or(declaration_name);
706        LeanWorkerDeclarationTargetInfo {
707            short_name: short_name.to_owned(),
708            declaration_name: declaration_name.to_owned(),
709            namespace_name: declaration_name
710                .strip_suffix(&format!(".{short_name}"))
711                .unwrap_or("")
712                .to_owned(),
713            declaration_kind: "theorem".to_owned(),
714            declaration_span: span.clone(),
715            name_span: span.clone(),
716            body_span: span,
717        }
718    }
719
720    fn verification_facts_fixture(
721        candidates: Vec<LeanWorkerDeclarationTargetInfo>,
722        axioms_available: bool,
723    ) -> LeanWorkerDeclarationVerificationFacts {
724        LeanWorkerDeclarationVerificationFacts {
725            target: None,
726            diagnostics: LeanWorkerElabFailure {
727                diagnostics: Vec::new(),
728                truncated: false,
729            },
730            unresolved_goals: Vec::new(),
731            contains_sorry: false,
732            contains_admit: false,
733            contains_sorry_ax: false,
734            axioms: Vec::new(),
735            axioms_truncated: false,
736            output_truncated: false,
737            candidates,
738            axioms_available,
739        }
740    }
741
742    #[test]
743    fn data_row_round_trips_through_length_delimited_frame() {
744        let row = DataRow {
745            stream: "rows".to_owned(),
746            sequence: 7,
747            payload: raw_json(&json!({ "name": "Nat.add", "score": 3 })),
748        };
749        let mut bytes = Vec::new();
750        write_frame(&mut bytes, Message::DataRow(row.clone()), MAX_FRAME_BYTES).expect("data row writes");
751        let frame = read_frame(&mut Cursor::new(bytes), MAX_FRAME_BYTES).expect("data row reads");
752        assert_eq!(frame.message, Message::DataRow(row));
753    }
754
755    #[test]
756    fn data_row_emitter_assigns_per_stream_sequences() {
757        let mut emitter = DataRowEmitter::default();
758        let mut bytes = Vec::new();
759        emitter
760            .emit(&mut bytes, "rows", &json!({ "i": 0 }))
761            .expect("first row writes");
762        emitter
763            .emit(&mut bytes, "warnings", &json!({ "i": 1 }))
764            .expect("second row writes");
765        emitter
766            .emit(&mut bytes, "rows", &json!({ "i": 2 }))
767            .expect("third row writes");
768        assert_eq!(emitter.count(), 3);
769
770        let mut cursor = Cursor::new(bytes);
771        let rows = [
772            read_frame(&mut cursor, MAX_FRAME_BYTES).expect("first row reads"),
773            read_frame(&mut cursor, MAX_FRAME_BYTES).expect("second row reads"),
774            read_frame(&mut cursor, MAX_FRAME_BYTES).expect("third row reads"),
775        ];
776        assert_eq!(
777            rows.map(|frame| frame.message),
778            [
779                Message::DataRow(DataRow {
780                    stream: "rows".to_owned(),
781                    sequence: 0,
782                    payload: raw_json(&json!({ "i": 0 })),
783                }),
784                Message::DataRow(DataRow {
785                    stream: "warnings".to_owned(),
786                    sequence: 0,
787                    payload: raw_json(&json!({ "i": 1 })),
788                }),
789                Message::DataRow(DataRow {
790                    stream: "rows".to_owned(),
791                    sequence: 1,
792                    payload: raw_json(&json!({ "i": 2 })),
793                }),
794            ],
795        );
796    }
797
798    #[test]
799    fn oversized_data_row_is_rejected_before_write() {
800        let row = DataRow {
801            stream: "rows".to_owned(),
802            sequence: 0,
803            payload: raw_json(&json!({ "blob": "x".repeat(MAX_FRAME_BYTES as usize) })),
804        };
805        let mut bytes = Vec::new();
806        let err =
807            write_frame(&mut bytes, Message::DataRow(row), MAX_FRAME_BYTES).expect_err("oversized frame is rejected");
808        match err {
809            ProtocolError::FrameTooLarge { len, max } => {
810                assert!(len > max);
811                assert_eq!(max, MAX_FRAME_BYTES);
812            }
813            other @ (ProtocolError::Io(_) | ProtocolError::Json(_) | ProtocolError::VersionMismatch { .. }) => {
814                panic!("expected FrameTooLarge, got {other:?}");
815            }
816        }
817    }
818
819    #[test]
820    fn oversized_data_row_is_rejected_before_read_allocation() {
821        let mut bytes = Vec::new();
822        bytes.extend_from_slice(&(MAX_FRAME_BYTES.saturating_add(1)).to_be_bytes());
823        let err = read_frame(&mut Cursor::new(bytes), MAX_FRAME_BYTES).expect_err("oversized frame is rejected");
824        match err {
825            ProtocolError::FrameTooLarge { len, max } => {
826                assert_eq!(len, MAX_FRAME_BYTES + 1);
827                assert_eq!(max, MAX_FRAME_BYTES);
828            }
829            other @ (ProtocolError::Io(_) | ProtocolError::Json(_) | ProtocolError::VersionMismatch { .. }) => {
830                panic!("expected FrameTooLarge, got {other:?}");
831            }
832        }
833    }
834
835    #[test]
836    fn larger_cap_accepts_frame_rejected_under_default() {
837        // A 2 MiB payload is rejected under MAX_FRAME_BYTES (1 MiB) but
838        // accepted when the cap is raised—proving the cap parameter is the
839        // only thing the codec consults.
840        let raised = MAX_FRAME_BYTES.saturating_mul(8);
841        let row = DataRow {
842            stream: "rows".to_owned(),
843            sequence: 0,
844            payload: raw_json(&json!({ "blob": "x".repeat(2 * MAX_FRAME_BYTES as usize) })),
845        };
846        let mut buf = Vec::new();
847        write_frame(&mut buf, Message::DataRow(row.clone()), raised).expect("oversize-under-default frame writes");
848        let frame = read_frame(&mut Cursor::new(buf), raised).expect("oversize-under-default frame reads");
849        assert_eq!(frame.message, Message::DataRow(row));
850    }
851
852    #[test]
853    fn frame_cap_bounds_constants_are_consistent() {
854        const { assert!(MIN_FRAME_BYTES <= MAX_FRAME_BYTES) };
855        const { assert!(MAX_FRAME_BYTES <= MAX_FRAME_BYTES_HARD_CAP) };
856    }
857
858    #[test]
859    fn malformed_frame_payload_is_protocol_error() {
860        let mut bytes = Vec::new();
861        bytes.extend_from_slice(&1_u32.to_be_bytes());
862        bytes.push(b'{');
863        let err = read_frame(&mut Cursor::new(bytes), MAX_FRAME_BYTES).expect_err("malformed JSON is rejected");
864        match err {
865            ProtocolError::Json(_) => {}
866            other @ (ProtocolError::Io(_)
867            | ProtocolError::FrameTooLarge { .. }
868            | ProtocolError::VersionMismatch { .. }) => {
869                panic!("expected Json error, got {other:?}");
870            }
871        }
872    }
873
874    #[test]
875    fn rows_complete_response_round_trips() {
876        let mut bytes = Vec::new();
877        write_frame(
878            &mut bytes,
879            Message::Response(Response::RowsComplete { count: 2 }),
880            MAX_FRAME_BYTES,
881        )
882        .expect("rows complete writes");
883        let frame = read_frame(&mut Cursor::new(bytes), MAX_FRAME_BYTES).expect("rows complete reads");
884        assert_eq!(frame.message, Message::Response(Response::RowsComplete { count: 2 }));
885    }
886
887    #[test]
888    fn declaration_search_request_and_response_round_trip() {
889        let request = Message::Request(Request::SearchDeclarations {
890            search: LeanWorkerDeclarationSearch {
891                name_fragment: Some("map".to_owned()),
892                name_match: LeanWorkerDeclarationNameMatch::Suffix,
893                kind: Some("theorem".to_owned()),
894                required_constants: vec!["List.map".to_owned()],
895                conclusion_head: Some("Eq".to_owned()),
896                scope_biases: vec![LeanWorkerDeclarationSearchBias {
897                    scope: LeanWorkerDeclarationSearchScope::Namespace,
898                    prefix: "List".to_owned(),
899                    strict: false,
900                    weight: 7,
901                }],
902                limit: 3,
903                filter: LeanWorkerDeclarationFilter {
904                    include_private: false,
905                    include_generated: false,
906                    include_internal: false,
907                },
908                include_source: false,
909            },
910        });
911        let mut bytes = Vec::new();
912        write_frame(&mut bytes, request.clone(), MAX_FRAME_BYTES).expect("declaration search request writes");
913        let frame = read_frame(&mut Cursor::new(bytes), MAX_FRAME_BYTES).expect("declaration search request reads");
914        assert_eq!(frame.message, request);
915
916        let response = Message::Response(Response::DeclarationSearch {
917            result: LeanWorkerDeclarationSearchResult {
918                declarations: vec![LeanWorkerDeclarationSearchRow {
919                    name: "List.map_map".to_owned(),
920                    kind: "theorem".to_owned(),
921                    module: Some("Init.Data.List.Lemmas".to_owned()),
922                    source: None,
923                    match_reason: "name,kind,required_constants,conclusion_head".to_owned(),
924                    score: 127,
925                    rank: 1,
926                    flags: LeanWorkerDeclarationFlags::default(),
927                }],
928                truncated: true,
929                facts: LeanWorkerDeclarationSearchFacts {
930                    declarations_scanned: 100,
931                    after_name_filter: 10,
932                    after_kind_filter: 8,
933                    after_required_constants_filter: 4,
934                    after_conclusion_filter: 2,
935                    after_scope_filter: 2,
936                    source_lookups: 0,
937                    broad_pruning: vec![LeanWorkerDeclarationSearchPruning {
938                        stage: "limit".to_owned(),
939                        reason: "broad_search_limit".to_owned(),
940                        count: 1,
941                    }],
942                    truncated: true,
943                    timings: LeanWorkerDeclarationSearchTimings {
944                        scan_micros: 1000,
945                        rank_micros: 50,
946                        source_micros: 0,
947                    },
948                },
949            },
950        });
951        let mut bytes = Vec::new();
952        write_frame(&mut bytes, response.clone(), MAX_FRAME_BYTES).expect("declaration search response writes");
953        let frame = read_frame(&mut Cursor::new(bytes), MAX_FRAME_BYTES).expect("declaration search response reads");
954        assert_eq!(frame.message, response);
955    }
956
957    #[test]
958    fn declaration_inspection_request_and_response_round_trip() {
959        let request = Message::Request(Request::InspectDeclaration {
960            request: LeanWorkerDeclarationInspectionRequest {
961                name: "List.map_map".to_owned(),
962                fields: LeanWorkerDeclarationInspectionFields {
963                    source: true,
964                    statement: true,
965                    docstring: true,
966                    attributes: true,
967                    flags: true,
968                    rendering: LeanWorkerRendering::Pretty,
969                },
970                budgets: LeanWorkerOutputBudgets {
971                    per_field_bytes: 128,
972                    total_bytes: 512,
973                },
974            },
975        });
976        let mut bytes = Vec::new();
977        write_frame(&mut bytes, request.clone(), MAX_FRAME_BYTES).expect("declaration inspection request writes");
978        let frame = read_frame(&mut Cursor::new(bytes), MAX_FRAME_BYTES).expect("declaration inspection request reads");
979        assert_eq!(frame.message, request);
980
981        let response = Message::Response(Response::DeclarationInspection {
982            result: LeanWorkerDeclarationInspectionResult::Found {
983                declaration: Box::new(LeanWorkerDeclarationInspection {
984                    name: "List.map_map".to_owned(),
985                    kind: "theorem".to_owned(),
986                    module: Some("Init.Data.List.Lemmas".to_owned()),
987                    source: Some(LeanWorkerSourceRange {
988                        file: "Init/Data/List/Lemmas.lean".to_owned(),
989                        start_line: 1,
990                        start_column: 1,
991                        end_line: 1,
992                        end_column: 10,
993                    }),
994                    statement: Some(LeanWorkerRenderedInfo {
995                        value: "forall ...".to_owned(),
996                        truncated: true,
997                    }),
998                    docstring: Some(LeanWorkerRenderedInfo {
999                        value: "doc".to_owned(),
1000                        truncated: false,
1001                    }),
1002                    attributes: vec!["simp".to_owned(), "rw".to_owned()],
1003                    proof_search: LeanWorkerDeclarationProofSearchFacts {
1004                        is_simp: true,
1005                        is_rw_candidate: true,
1006                        is_instance: false,
1007                        is_class: false,
1008                        class_name: None,
1009                    },
1010                    flags: LeanWorkerDeclarationFlags::default(),
1011                    statement_rendering: Some(LeanWorkerRendering::Pretty),
1012                }),
1013            },
1014        });
1015        let mut bytes = Vec::new();
1016        write_frame(&mut bytes, response.clone(), MAX_FRAME_BYTES).expect("declaration inspection response writes");
1017        let frame =
1018            read_frame(&mut Cursor::new(bytes), MAX_FRAME_BYTES).expect("declaration inspection response reads");
1019        assert_eq!(frame.message, response);
1020
1021        let not_found = Message::Response(Response::DeclarationInspection {
1022            result: LeanWorkerDeclarationInspectionResult::NotFound {
1023                name: "Missing.name".to_owned(),
1024            },
1025        });
1026        let mut bytes = Vec::new();
1027        write_frame(&mut bytes, not_found.clone(), MAX_FRAME_BYTES)
1028            .expect("declaration inspection not-found response writes");
1029        let frame = read_frame(&mut Cursor::new(bytes), MAX_FRAME_BYTES)
1030            .expect("declaration inspection not-found response reads");
1031        assert_eq!(frame.message, not_found);
1032
1033        let unsupported = Message::Response(Response::DeclarationInspection {
1034            result: LeanWorkerDeclarationInspectionResult::Unsupported,
1035        });
1036        let mut bytes = Vec::new();
1037        write_frame(&mut bytes, unsupported.clone(), MAX_FRAME_BYTES)
1038            .expect("declaration inspection unsupported response writes");
1039        let frame = read_frame(&mut Cursor::new(bytes), MAX_FRAME_BYTES)
1040            .expect("declaration inspection unsupported response reads");
1041        assert_eq!(frame.message, unsupported);
1042    }
1043
1044    #[test]
1045    fn module_query_request_and_response_round_trip() {
1046        let request = Message::Request(Request::ProcessModuleQuery {
1047            source: "def x := 1\n#check x\n".to_owned(),
1048            query: LeanWorkerModuleQuery::TypeAt { line: 2, column: 8 },
1049            options: LeanWorkerElabOptions::default(),
1050        });
1051        let mut bytes = Vec::new();
1052        write_frame(&mut bytes, request.clone(), MAX_FRAME_BYTES).expect("module query request writes");
1053        let frame = read_frame(&mut Cursor::new(bytes), MAX_FRAME_BYTES).expect("module query request reads");
1054        assert_eq!(frame.message, request);
1055
1056        let response = Message::Response(Response::ProcessModuleQuery {
1057            outcome: LeanWorkerModuleQueryOutcome::Ok {
1058                imports: Vec::new(),
1059                result: LeanWorkerModuleQueryResult::TypeAt(LeanWorkerTypeAtResult::Term {
1060                    span: LeanWorkerModuleSourceSpan {
1061                        start_line: 2,
1062                        start_column: 8,
1063                        end_line: 2,
1064                        end_column: 9,
1065                    },
1066                    expr: LeanWorkerRenderedInfo {
1067                        value: "x".to_owned(),
1068                        truncated: false,
1069                    },
1070                    type_str: LeanWorkerRenderedInfo {
1071                        value: "Nat".to_owned(),
1072                        truncated: false,
1073                    },
1074                    expected_type: None,
1075                }),
1076            },
1077        });
1078        let mut bytes = Vec::new();
1079        write_frame(&mut bytes, response.clone(), MAX_FRAME_BYTES).expect("module query response writes");
1080        let frame = read_frame(&mut Cursor::new(bytes), MAX_FRAME_BYTES).expect("module query response reads");
1081        assert_eq!(frame.message, response);
1082
1083        let unsupported = LeanWorkerModuleQueryOutcome::Unsupported;
1084        let json = serde_json::to_value(&unsupported).expect("unsupported serializes");
1085        assert_eq!(json, json!({ "status": "unsupported" }));
1086
1087        let diagnostics = LeanWorkerModuleQueryResult::Diagnostics(LeanWorkerElabFailure {
1088            diagnostics: Vec::new(),
1089            truncated: false,
1090        });
1091        let json = serde_json::to_value(&diagnostics).expect("diagnostics serializes");
1092        assert_eq!(
1093            json,
1094            json!({
1095                "result": "diagnostics",
1096                "body": {
1097                    "diagnostics": [],
1098                    "truncated": false
1099                }
1100            })
1101        );
1102    }
1103
1104    #[test]
1105    fn module_query_batch_request_and_response_round_trip() {
1106        let request = Message::Request(Request::ProcessModuleQueryBatch {
1107            source: "theorem t : True := by\n  trivial\n".to_owned(),
1108            selectors: vec![
1109                LeanWorkerModuleQuerySelector::Diagnostics {
1110                    id: "diagnostics".to_owned(),
1111                },
1112                LeanWorkerModuleQuerySelector::ProofState {
1113                    id: "state".to_owned(),
1114                    line: 2,
1115                    column: 4,
1116                },
1117            ],
1118            budgets: LeanWorkerOutputBudgets::default(),
1119            options: LeanWorkerElabOptions::default(),
1120        });
1121        let mut bytes = Vec::new();
1122        write_frame(&mut bytes, request.clone(), MAX_FRAME_BYTES).expect("module query batch request writes");
1123        let frame = read_frame(&mut Cursor::new(bytes), MAX_FRAME_BYTES).expect("module query batch request reads");
1124        assert_eq!(frame.message, request);
1125
1126        let response = Message::Response(Response::ProcessModuleQueryBatch {
1127            outcome: LeanWorkerModuleQueryBatchOutcome::Ok {
1128                imports: Vec::new(),
1129                result: LeanWorkerModuleQueryBatchEnvelope {
1130                    items: vec![LeanWorkerModuleQueryBatchItem::Ok {
1131                        id: "diagnostics".to_owned(),
1132                        result: Box::new(LeanWorkerModuleQueryBatchResult::Diagnostics(LeanWorkerElabFailure {
1133                            diagnostics: Vec::new(),
1134                            truncated: false,
1135                        })),
1136                    }],
1137                    total_truncated: false,
1138                },
1139                facts: LeanWorkerModuleQueryCacheFacts {
1140                    cache_status: LeanWorkerModuleCacheStatus::Miss,
1141                    timings: LeanWorkerModuleQueryTimings::zero(),
1142                    output_bytes: 0,
1143                    cache_entry_count: Some(1),
1144                    cache_approx_bytes: Some(1024),
1145                },
1146            },
1147        });
1148        let mut bytes = Vec::new();
1149        write_frame(&mut bytes, response.clone(), MAX_FRAME_BYTES).expect("module query batch response writes");
1150        let frame = read_frame(&mut Cursor::new(bytes), MAX_FRAME_BYTES).expect("module query batch response reads");
1151        assert_eq!(frame.message, response);
1152    }
1153
1154    #[test]
1155    fn proof_attempt_request_and_response_round_trip() {
1156        let span = LeanWorkerModuleSourceSpan {
1157            start_line: 1,
1158            start_column: 22,
1159            end_line: 2,
1160            end_column: 7,
1161        };
1162        let request = Message::Request(Request::AttemptProof {
1163            request: LeanWorkerProofAttemptRequest {
1164                source: "theorem t : True := by\n  trivial\n".to_owned(),
1165                edit: LeanWorkerProofEditTarget::Declaration {
1166                    name: "t".to_owned(),
1167                    position: LeanWorkerProofPositionSelector::Default,
1168                },
1169                candidates: vec![LeanWorkerProofCandidate {
1170                    id: "rfl".to_owned(),
1171                    text: "by trivial".to_owned(),
1172                }],
1173                budgets: LeanWorkerOutputBudgets::default(),
1174            },
1175            options: LeanWorkerElabOptions::default(),
1176            progress: true,
1177        });
1178        let mut bytes = Vec::new();
1179        write_frame(&mut bytes, request.clone(), MAX_FRAME_BYTES).expect("proof attempt request writes");
1180        let frame = read_frame(&mut Cursor::new(bytes), MAX_FRAME_BYTES).expect("proof attempt request reads");
1181        assert_eq!(frame.message, request);
1182
1183        let response = Message::Response(Response::ProofAttempt {
1184            result: LeanWorkerProofAttemptResult::Ok {
1185                imports: Vec::new(),
1186                result: LeanWorkerProofAttemptEnvelope {
1187                    candidates: vec![LeanWorkerProofAttemptRow {
1188                        id: "rfl".to_owned(),
1189                        status: LeanWorkerProofAttemptStatus::Closed,
1190                        candidate_text: LeanWorkerRenderedInfo {
1191                            value: "rfl".to_owned(),
1192                            truncated: false,
1193                        },
1194                        diagnostics: LeanWorkerElabFailure {
1195                            diagnostics: Vec::new(),
1196                            truncated: false,
1197                        },
1198                        downstream_diagnostics: LeanWorkerElabFailure {
1199                            diagnostics: Vec::new(),
1200                            truncated: false,
1201                        },
1202                        goals: Vec::new(),
1203                        declaration: Some(LeanWorkerDeclarationTargetInfo {
1204                            short_name: "t".to_owned(),
1205                            declaration_name: "t".to_owned(),
1206                            namespace_name: String::new(),
1207                            declaration_kind: "theorem".to_owned(),
1208                            declaration_span: span.clone(),
1209                            name_span: span.clone(),
1210                            body_span: span,
1211                        }),
1212                        proof_position: Some(LeanWorkerProofPositionSummary {
1213                            index: 0,
1214                            tactic: LeanWorkerRenderedInfo {
1215                                value: "trivial".to_owned(),
1216                                truncated: false,
1217                            },
1218                        }),
1219                        output_truncated: false,
1220                    }],
1221                    candidate_limit: 8,
1222                    candidates_truncated: false,
1223                },
1224            },
1225        });
1226        let mut bytes = Vec::new();
1227        write_frame(&mut bytes, response.clone(), MAX_FRAME_BYTES).expect("proof attempt response writes");
1228        let frame = read_frame(&mut Cursor::new(bytes), MAX_FRAME_BYTES).expect("proof attempt response reads");
1229        assert_eq!(frame.message, response);
1230    }
1231
1232    #[test]
1233    fn declaration_verification_request_and_response_round_trip() {
1234        let request = Message::Request(Request::VerifyDeclaration {
1235            request: LeanWorkerDeclarationVerificationRequest {
1236                source: "theorem t : True := by\n  trivial\n".to_owned(),
1237                target: LeanWorkerDeclarationVerificationTarget::Name { name: "t".to_owned() },
1238                sorry_policy: LeanWorkerSorryPolicy::Deny,
1239                report_axioms: true,
1240                budgets: LeanWorkerOutputBudgets::default(),
1241            },
1242            options: LeanWorkerElabOptions::default(),
1243            progress: false,
1244        });
1245        let mut bytes = Vec::new();
1246        write_frame(&mut bytes, request.clone(), MAX_FRAME_BYTES).expect("verification request writes");
1247        let frame = read_frame(&mut Cursor::new(bytes), MAX_FRAME_BYTES).expect("verification request reads");
1248        assert_eq!(frame.message, request);
1249
1250        let response = Message::Response(Response::DeclarationVerification {
1251            result: LeanWorkerDeclarationVerificationResult::Ok {
1252                verification_status: LeanWorkerDeclarationVerificationStatus::Accepted,
1253                facts: Box::new(LeanWorkerDeclarationVerificationFacts {
1254                    target: None,
1255                    diagnostics: LeanWorkerElabFailure {
1256                        diagnostics: Vec::new(),
1257                        truncated: false,
1258                    },
1259                    unresolved_goals: Vec::new(),
1260                    contains_sorry: false,
1261                    contains_admit: false,
1262                    contains_sorry_ax: false,
1263                    axioms: vec!["propext".to_owned(), "Classical.choice".to_owned()],
1264                    axioms_truncated: false,
1265                    output_truncated: false,
1266                    candidates: Vec::new(),
1267                    axioms_available: true,
1268                }),
1269                imports: Vec::new(),
1270            },
1271        });
1272        let mut bytes = Vec::new();
1273        write_frame(&mut bytes, response.clone(), MAX_FRAME_BYTES).expect("verification response writes");
1274        let frame = read_frame(&mut Cursor::new(bytes), MAX_FRAME_BYTES).expect("verification response reads");
1275        assert_eq!(frame.message, response);
1276    }
1277
1278    #[test]
1279    fn verification_needs_build_and_ambiguous_round_trip() {
1280        // NeedsBuild verdict: the enclosing MissingImports outcome names the
1281        // unbuilt modules; the status is the typed resolution verdict.
1282        let needs_build = Message::Response(Response::DeclarationVerification {
1283            result: LeanWorkerDeclarationVerificationResult::MissingImports {
1284                verification_status: LeanWorkerDeclarationVerificationStatus::NeedsBuild,
1285                facts: Box::new(verification_facts_fixture(Vec::new(), false)),
1286                imports: vec!["Mathlib.Tactic".to_owned()],
1287                missing: vec!["Mathlib.Unbuilt.Dep".to_owned()],
1288            },
1289        });
1290        assert_frame_round_trips(&needs_build);
1291
1292        // Ambiguous verdict carries the competing declarations.
1293        let ambiguous = Message::Response(Response::DeclarationVerification {
1294            result: LeanWorkerDeclarationVerificationResult::Ok {
1295                verification_status: LeanWorkerDeclarationVerificationStatus::Ambiguous,
1296                facts: Box::new(verification_facts_fixture(
1297                    vec![
1298                        declaration_target_info_fixture("A.foo"),
1299                        declaration_target_info_fixture("B.foo"),
1300                    ],
1301                    false,
1302                )),
1303                imports: Vec::new(),
1304            },
1305        });
1306        assert_frame_round_trips(&ambiguous);
1307    }
1308
1309    #[test]
1310    fn proof_state_ambiguous_and_needs_build_round_trip() {
1311        let ambiguous = Message::Response(Response::ProcessModuleQueryBatch {
1312            outcome: LeanWorkerModuleQueryBatchOutcome::Ok {
1313                result: LeanWorkerModuleQueryBatchEnvelope {
1314                    items: vec![LeanWorkerModuleQueryBatchItem::Ok {
1315                        id: "state".to_owned(),
1316                        result: Box::new(LeanWorkerModuleQueryBatchResult::ProofState(
1317                            LeanWorkerProofStateResult::Ambiguous {
1318                                candidates: vec![
1319                                    declaration_target_info_fixture("A.foo"),
1320                                    declaration_target_info_fixture("B.foo"),
1321                                ],
1322                            },
1323                        )),
1324                    }],
1325                    total_truncated: false,
1326                },
1327                imports: Vec::new(),
1328                facts: LeanWorkerModuleQueryCacheFacts::uncached(0),
1329            },
1330        });
1331        assert_frame_round_trips(&ambiguous);
1332
1333        let needs_build = Message::Response(Response::ProcessModuleQueryBatch {
1334            outcome: LeanWorkerModuleQueryBatchOutcome::Ok {
1335                result: LeanWorkerModuleQueryBatchEnvelope {
1336                    items: vec![LeanWorkerModuleQueryBatchItem::Ok {
1337                        id: "state".to_owned(),
1338                        result: Box::new(LeanWorkerModuleQueryBatchResult::ProofState(
1339                            LeanWorkerProofStateResult::NeedsBuild {
1340                                missing: vec!["Mathlib.Unbuilt.Dep".to_owned()],
1341                            },
1342                        )),
1343                    }],
1344                    total_truncated: false,
1345                },
1346                imports: Vec::new(),
1347                facts: LeanWorkerModuleQueryCacheFacts::uncached(0),
1348            },
1349        });
1350        assert_frame_round_trips(&needs_build);
1351    }
1352
1353    #[test]
1354    fn inspection_fields_default_rendering_is_pretty() {
1355        // A request serialized without an explicit `rendering` deserializes to
1356        // Pretty (the `#[serde(default)]`), so older callers get the useful
1357        // notation-aware form by default.
1358        let json = serde_json::json!({
1359            "source": true,
1360            "statement": true,
1361            "docstring": false,
1362            "attributes": false,
1363            "flags": false,
1364        });
1365        let fields: LeanWorkerDeclarationInspectionFields =
1366            serde_json::from_value(json).expect("fields without rendering deserialize");
1367        assert_eq!(fields.rendering, LeanWorkerRendering::Pretty);
1368    }
1369}