1use 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
37pub const PROTOCOL_VERSION: u16 = 10;
40
41pub const MAX_FRAME_BYTES: u32 = 1024 * 1024;
51
52pub const MIN_FRAME_BYTES: u32 = 64 * 1024;
55
56pub const MAX_FRAME_BYTES_HARD_CAP: u32 = 256 * 1024 * 1024;
59
60#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
62#[non_exhaustive]
63pub struct Frame {
64 pub version: u16,
66 pub message: Message,
68}
69
70impl Frame {
71 #[must_use]
73 pub fn new(message: Message) -> Self {
74 Self {
75 version: PROTOCOL_VERSION,
76 message,
77 }
78 }
79}
80
81#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
83#[serde(tag = "type", content = "body", rename_all = "snake_case")]
84#[non_exhaustive]
85pub enum Message {
86 Handshake {
89 worker_version: String,
91 protocol_version: u16,
93 },
94 ConfigureFrameLimit {
101 max_frame_bytes: u32,
103 },
104 Request(Request),
106 Response(Response),
108 Diagnostic(Diagnostic),
110 ProgressTick(ProgressTick),
112 DataRow(DataRow),
114 FatalExit(FatalExit),
117}
118
119#[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 EmitTestRows {
241 streams: Vec<String>,
242 },
243 EmitTestRowsThenExit,
244 EmitTestRowsThenPanic,
245 Terminate,
246}
247
248#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
250#[serde(tag = "kind", rename_all = "snake_case")]
251#[non_exhaustive]
252pub enum HostSessionMode {
253 Capability {
255 package: String,
256 lib_name: String,
257 manifest_path: Option<String>,
258 },
259 ShimsOnly,
261}
262
263#[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#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
364#[non_exhaustive]
365pub struct Diagnostic {
366 pub code: String,
368 pub message: String,
370}
371
372impl Diagnostic {
373 #[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#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
386#[non_exhaustive]
387pub struct ProgressTick {
388 pub phase: String,
390 pub current: u64,
392 pub total: Option<u64>,
394}
395
396impl ProgressTick {
397 #[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#[derive(Clone, Debug, Deserialize, Serialize)]
417pub struct DataRow {
418 pub stream: String,
420 pub sequence: u64,
422 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#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
436#[non_exhaustive]
437pub struct StreamSummary {
438 pub total_rows: u64,
440 pub per_stream_counts: BTreeMap<String, u64>,
442 pub elapsed_micros: u64,
444 pub metadata: Option<Value>,
446}
447
448impl StreamSummary {
449 #[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#[derive(Debug, Default)]
470#[non_exhaustive]
471pub struct DataRowEmitter {
472 sequences: BTreeMap<String, u64>,
473 count: u64,
474}
475
476impl DataRowEmitter {
477 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 #[must_use]
505 pub fn count(&self) -> u64 {
506 self.count
507 }
508
509 #[must_use]
511 pub fn per_stream_counts(&self) -> BTreeMap<String, u64> {
512 self.sequences.clone()
513 }
514}
515
516#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
519#[non_exhaustive]
520pub struct FatalExit {
521 pub status: String,
523 pub stderr: String,
525}
526
527impl FatalExit {
528 #[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#[derive(Debug)]
540#[non_exhaustive]
541pub enum ProtocolError {
542 Io(io::Error),
544 Json(serde_json::Error),
546 FrameTooLarge {
548 len: u32,
550 max: u32,
552 },
553 VersionMismatch {
555 expected: u16,
557 actual: u16,
559 },
560}
561
562impl ProtocolError {
563 #[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
604pub 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
634pub 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 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 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 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 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}