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