Skip to main content

monoloop_loop/transaction/
fake_support.rs

1//! Test encoders used by startup and exchange tests.
2
3use monoloop_contracts::{
4    Bytes, CanonicalMessage, DialectDescriptor, EncodedExchange, EncodingError,
5    ExchangeInputPolicy, InitialEncodeRequest, OutboundDialectEncoder,
6    ToolContinuationEncodeRequest,
7};
8
9/// Encoder that panics on encode (§22.2 coordinator-panic proofs).
10#[derive(Debug, Default)]
11pub struct PanicEncoder;
12
13impl OutboundDialectEncoder for PanicEncoder {
14    fn encode_initial(
15        &self,
16        _request: InitialEncodeRequest<'_>,
17    ) -> Result<EncodedExchange, EncodingError> {
18        panic!("PanicEncoder::encode_initial (§22.2)");
19    }
20
21    fn encode_tool_continuation(
22        &self,
23        _request: ToolContinuationEncodeRequest<'_>,
24    ) -> Result<EncodedExchange, EncodingError> {
25        panic!("PanicEncoder::encode_tool_continuation (§22.2)");
26    }
27}
28
29/// Encoder that rejects all encode calls.
30#[derive(Debug, Default)]
31pub struct RejectEncoder;
32
33impl OutboundDialectEncoder for RejectEncoder {
34    fn encode_initial(
35        &self,
36        _request: InitialEncodeRequest<'_>,
37    ) -> Result<EncodedExchange, EncodingError> {
38        Err(EncodingError::Unsupported("reject encoder"))
39    }
40
41    fn encode_tool_continuation(
42        &self,
43        _request: ToolContinuationEncodeRequest<'_>,
44    ) -> Result<EncodedExchange, EncodingError> {
45        Err(EncodingError::Unsupported("reject encoder"))
46    }
47}
48
49/// Encoder returning empty bytes.
50#[derive(Debug)]
51pub struct EmptyBytesEncoder {
52    /// Dialect stamped on the encoded exchange.
53    pub dialect: DialectDescriptor,
54}
55
56impl EmptyBytesEncoder {
57    /// Construct with an explicit dialect stamp.
58    pub fn new(dialect: DialectDescriptor) -> Self {
59        Self { dialect }
60    }
61}
62
63impl OutboundDialectEncoder for EmptyBytesEncoder {
64    fn encode_initial(
65        &self,
66        _request: InitialEncodeRequest<'_>,
67    ) -> Result<EncodedExchange, EncodingError> {
68        Ok(EncodedExchange {
69            bytes: Bytes::new(),
70            required_input_dialect: self.dialect.clone(),
71            input_policy: ExchangeInputPolicy::SendAndFinish,
72        })
73    }
74
75    fn encode_tool_continuation(
76        &self,
77        _request: ToolContinuationEncodeRequest<'_>,
78    ) -> Result<EncodedExchange, EncodingError> {
79        Ok(EncodedExchange {
80            bytes: Bytes::new(),
81            required_input_dialect: self.dialect.clone(),
82            input_policy: ExchangeInputPolicy::SendAndFinish,
83        })
84    }
85}
86
87/// Loop-owned **smoke** encoder for FakeConnector + [`DialectDescriptor::test_raw`].
88///
89/// Joins text parts as UTF-8 and ensures a trailing sentence terminator so the
90/// segmenter emits. Not a production Channel encoder and not a testkit fixture
91/// encoder — live hosts must use profile `*_channel_binding` encoders instead.
92#[derive(Debug, Default)]
93pub struct TestTextEncoder;
94
95impl OutboundDialectEncoder for TestTextEncoder {
96    fn encode_initial(
97        &self,
98        request: InitialEncodeRequest<'_>,
99    ) -> Result<EncodedExchange, EncodingError> {
100        let mut text = String::new();
101        for msg in request.input.messages() {
102            match msg {
103                CanonicalMessage::System { content, .. }
104                | CanonicalMessage::User { content, .. }
105                | CanonicalMessage::Tool { content, .. } => {
106                    for part in content {
107                        text.push_str(part.text());
108                        text.push(' ');
109                    }
110                }
111                CanonicalMessage::Assistant { content, .. } => {
112                    for part in content {
113                        text.push_str(part.text());
114                        text.push(' ');
115                    }
116                }
117            }
118        }
119        let trimmed = text.trim();
120        if trimmed.is_empty() {
121            return Err(EncodingError::UnrepresentableInput);
122        }
123        let mut body = trimmed.to_string();
124        if !body.ends_with('.') && !body.ends_with('!') && !body.ends_with('?') {
125            body.push('.');
126        }
127        body.push(' ');
128        Ok(EncodedExchange {
129            bytes: Bytes::from(body.into_bytes()),
130            required_input_dialect: DialectDescriptor::test_raw(),
131            input_policy: ExchangeInputPolicy::SendAndFinish,
132        })
133    }
134
135    fn encode_tool_continuation(
136        &self,
137        _request: ToolContinuationEncodeRequest<'_>,
138    ) -> Result<EncodedExchange, EncodingError> {
139        Err(EncodingError::Unsupported(
140            "TestTextEncoder has no tool continuation",
141        ))
142    }
143}