Skip to main content

aver/types/checker/
effect_classification.rs

1//! Oracle v1 effect classification.
2//!
3//! For each built-in effect method covered by `aver proof`, this module
4//! records:
5//!
6//! - Which proof dimension(s) it belongs to (snapshot / generative / output,
7//!   and the combination `generative + output` used by e.g. `Http.get`).
8//! - For snapshot and generative, the corresponding capability/oracle
9//!   signature that lifted specs bind via `given name: E.m = [...]`.
10//!
11//! Output-only effects (for example `Console.print`, `Time.sleep`, and
12//! terminal drawing calls) are classified but do not have an oracle signature:
13//! they append to the per-branch trace segment and are asserted about via the
14//! trace API, not by binding an oracle in `given`.
15//!
16//! The table is the single source of truth consumed by:
17//!
18//! - `given`-clause type inference (`given rnd: Random.int` → oracle type
19//!   `(BranchPath, Int, Int, Int) -> Int`).
20//! - Lifting of effectful function bodies at proof-export time.
21//! - Rejection diagnostics for unclassified effects.
22//!
23//! Source of runtime signatures: `src/services/*.rs` and `docs/services.md`.
24//! Keep this table synchronized with the real built-ins.
25
26use super::super::Type;
27use crate::types::branch_path;
28
29/// Proof dimension(s) an effect participates in. `!`-combinations are
30/// modelled directly rather than as flags for readability at call sites.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum EffectDimension {
33    /// Stable within a run. Modelled as a plain reader function.
34    Snapshot,
35    /// Fresh value per call. Modelled as a branch-indexed oracle.
36    Generative,
37    /// Trace-appending side-effect only. No oracle; assertions via trace API.
38    Output,
39    /// Both generative (response value from oracle) and output (request
40    /// emitted to trace). Used by request/operation-style effects such as
41    /// `Http.*`, mutating `Disk.*`, and one-shot `Tcp.*`.
42    GenerativeOutput,
43}
44
45/// Classification of one effect method. `runtime_params` and
46/// `runtime_return` mirror the surface signature at call sites in user
47/// code; oracle signatures are derived from them (see [`oracle_signature`]).
48#[derive(Debug, Clone)]
49pub struct EffectClassification {
50    pub method: &'static str,
51    pub dimension: EffectDimension,
52    pub runtime_params: &'static [RuntimeType],
53    pub runtime_return: RuntimeType,
54}
55
56/// Compact carrier for runtime signature components — kept separate from
57/// the full [`Type`] enum so the static table can live as a const array.
58/// Converted into [`Type`] on demand via [`runtime_type`].
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum RuntimeType {
61    Unknown,
62    Unit,
63    Int,
64    Float,
65    Str,
66    Bool,
67    OptionStr,
68    ListStr,
69    ResultUnitStr,
70    ResultStrStr,
71    ResultListStrStr,
72    HttpResponseResult,
73    /// `List<Header>` — the headers argument on `Http.post/put/patch`.
74    ListHeader,
75}
76
77impl RuntimeType {
78    fn as_type(self) -> Type {
79        match self {
80            RuntimeType::Unknown => Type::Unknown,
81            RuntimeType::Unit => Type::Unit,
82            RuntimeType::Int => Type::Int,
83            RuntimeType::Float => Type::Float,
84            RuntimeType::Str => Type::Str,
85            RuntimeType::Bool => Type::Bool,
86            RuntimeType::OptionStr => Type::Option(Box::new(Type::Str)),
87            RuntimeType::ListStr => Type::List(Box::new(Type::Str)),
88            RuntimeType::ResultUnitStr => Type::Result(Box::new(Type::Unit), Box::new(Type::Str)),
89            RuntimeType::ResultStrStr => Type::Result(Box::new(Type::Str), Box::new(Type::Str)),
90            RuntimeType::ResultListStrStr => Type::Result(
91                Box::new(Type::List(Box::new(Type::Str))),
92                Box::new(Type::Str),
93            ),
94            RuntimeType::HttpResponseResult => Type::Result(
95                Box::new(Type::Named("HttpResponse".to_string())),
96                Box::new(Type::Str),
97            ),
98            RuntimeType::ListHeader => Type::List(Box::new(Type::Named("Header".to_string()))),
99        }
100    }
101}
102
103fn runtime_type(rt: RuntimeType) -> Type {
104    rt.as_type()
105}
106
107/// Full classification table. This is the closed set for Oracle v1.
108const CLASSIFICATIONS: &[EffectClassification] = &[
109    // Snapshot
110    EffectClassification {
111        method: "Args.get",
112        dimension: EffectDimension::Snapshot,
113        runtime_params: &[],
114        runtime_return: RuntimeType::ListStr,
115    },
116    EffectClassification {
117        method: "Env.get",
118        dimension: EffectDimension::Snapshot,
119        runtime_params: &[RuntimeType::Str],
120        runtime_return: RuntimeType::OptionStr,
121    },
122    // Generative
123    EffectClassification {
124        method: "Random.int",
125        dimension: EffectDimension::Generative,
126        runtime_params: &[RuntimeType::Int, RuntimeType::Int],
127        runtime_return: RuntimeType::Int,
128    },
129    EffectClassification {
130        method: "Random.float",
131        dimension: EffectDimension::Generative,
132        runtime_params: &[],
133        runtime_return: RuntimeType::Float,
134    },
135    EffectClassification {
136        method: "Time.now",
137        dimension: EffectDimension::Generative,
138        runtime_params: &[],
139        runtime_return: RuntimeType::Str,
140    },
141    EffectClassification {
142        method: "Time.unixMs",
143        dimension: EffectDimension::Generative,
144        runtime_params: &[],
145        runtime_return: RuntimeType::Int,
146    },
147    EffectClassification {
148        method: "Disk.readText",
149        dimension: EffectDimension::Generative,
150        runtime_params: &[RuntimeType::Str],
151        runtime_return: RuntimeType::ResultStrStr,
152    },
153    EffectClassification {
154        method: "Disk.exists",
155        dimension: EffectDimension::Generative,
156        runtime_params: &[RuntimeType::Str],
157        runtime_return: RuntimeType::Bool,
158    },
159    EffectClassification {
160        method: "Disk.listDir",
161        dimension: EffectDimension::Generative,
162        runtime_params: &[RuntimeType::Str],
163        runtime_return: RuntimeType::ResultListStrStr,
164    },
165    EffectClassification {
166        method: "Console.readLine",
167        dimension: EffectDimension::Generative,
168        runtime_params: &[],
169        runtime_return: RuntimeType::ResultStrStr,
170    },
171    // Generative + output (Http)
172    EffectClassification {
173        method: "Http.get",
174        dimension: EffectDimension::GenerativeOutput,
175        runtime_params: &[RuntimeType::Str],
176        runtime_return: RuntimeType::HttpResponseResult,
177    },
178    EffectClassification {
179        method: "Http.head",
180        dimension: EffectDimension::GenerativeOutput,
181        runtime_params: &[RuntimeType::Str],
182        runtime_return: RuntimeType::HttpResponseResult,
183    },
184    EffectClassification {
185        method: "Http.delete",
186        dimension: EffectDimension::GenerativeOutput,
187        runtime_params: &[RuntimeType::Str],
188        runtime_return: RuntimeType::HttpResponseResult,
189    },
190    // Http.post/.put/.patch — four-arg form `(url, body, contentType, headers)`.
191    EffectClassification {
192        method: "Http.post",
193        dimension: EffectDimension::GenerativeOutput,
194        runtime_params: &[
195            RuntimeType::Str,
196            RuntimeType::Str,
197            RuntimeType::Str,
198            RuntimeType::ListHeader,
199        ],
200        runtime_return: RuntimeType::HttpResponseResult,
201    },
202    EffectClassification {
203        method: "Http.put",
204        dimension: EffectDimension::GenerativeOutput,
205        runtime_params: &[
206            RuntimeType::Str,
207            RuntimeType::Str,
208            RuntimeType::Str,
209            RuntimeType::ListHeader,
210        ],
211        runtime_return: RuntimeType::HttpResponseResult,
212    },
213    EffectClassification {
214        method: "Http.patch",
215        dimension: EffectDimension::GenerativeOutput,
216        runtime_params: &[
217            RuntimeType::Str,
218            RuntimeType::Str,
219            RuntimeType::Str,
220            RuntimeType::ListHeader,
221        ],
222        runtime_return: RuntimeType::HttpResponseResult,
223    },
224    // Disk writes/deletes are modelled like HTTP writes: the operation is
225    // emitted to the trace, and success/failure comes from the oracle. Oracle
226    // does not assert persistent filesystem state after the operation.
227    EffectClassification {
228        method: "Disk.writeText",
229        dimension: EffectDimension::GenerativeOutput,
230        runtime_params: &[RuntimeType::Str, RuntimeType::Str],
231        runtime_return: RuntimeType::ResultUnitStr,
232    },
233    EffectClassification {
234        method: "Disk.appendText",
235        dimension: EffectDimension::GenerativeOutput,
236        runtime_params: &[RuntimeType::Str, RuntimeType::Str],
237        runtime_return: RuntimeType::ResultUnitStr,
238    },
239    EffectClassification {
240        method: "Disk.delete",
241        dimension: EffectDimension::GenerativeOutput,
242        runtime_params: &[RuntimeType::Str],
243        runtime_return: RuntimeType::ResultUnitStr,
244    },
245    EffectClassification {
246        method: "Disk.deleteDir",
247        dimension: EffectDimension::GenerativeOutput,
248        runtime_params: &[RuntimeType::Str],
249        runtime_return: RuntimeType::ResultUnitStr,
250    },
251    EffectClassification {
252        method: "Disk.makeDir",
253        dimension: EffectDimension::GenerativeOutput,
254        runtime_params: &[RuntimeType::Str],
255        runtime_return: RuntimeType::ResultUnitStr,
256    },
257    // One-shot TCP operations — request is trace output, response comes from oracle.
258    EffectClassification {
259        method: "Tcp.send",
260        dimension: EffectDimension::GenerativeOutput,
261        runtime_params: &[RuntimeType::Str, RuntimeType::Int, RuntimeType::Str],
262        runtime_return: RuntimeType::ResultStrStr,
263    },
264    EffectClassification {
265        method: "Tcp.ping",
266        dimension: EffectDimension::GenerativeOutput,
267        runtime_params: &[RuntimeType::Str, RuntimeType::Int],
268        runtime_return: RuntimeType::ResultUnitStr,
269    },
270    // Output-only — no oracle signature, but classified for completeness.
271    EffectClassification {
272        method: "Console.print",
273        dimension: EffectDimension::Output,
274        runtime_params: &[RuntimeType::Unknown],
275        runtime_return: RuntimeType::Unit,
276    },
277    EffectClassification {
278        method: "Console.error",
279        dimension: EffectDimension::Output,
280        runtime_params: &[RuntimeType::Unknown],
281        runtime_return: RuntimeType::Unit,
282    },
283    EffectClassification {
284        method: "Console.warn",
285        dimension: EffectDimension::Output,
286        runtime_params: &[RuntimeType::Unknown],
287        runtime_return: RuntimeType::Unit,
288    },
289    EffectClassification {
290        method: "Time.sleep",
291        dimension: EffectDimension::Output,
292        runtime_params: &[RuntimeType::Int],
293        runtime_return: RuntimeType::Unit,
294    },
295    EffectClassification {
296        method: "Terminal.clear",
297        dimension: EffectDimension::Output,
298        runtime_params: &[],
299        runtime_return: RuntimeType::Unit,
300    },
301    EffectClassification {
302        method: "Terminal.moveTo",
303        dimension: EffectDimension::Output,
304        runtime_params: &[RuntimeType::Int, RuntimeType::Int],
305        runtime_return: RuntimeType::Unit,
306    },
307    EffectClassification {
308        method: "Terminal.print",
309        dimension: EffectDimension::Output,
310        runtime_params: &[RuntimeType::Unknown],
311        runtime_return: RuntimeType::Unit,
312    },
313    EffectClassification {
314        method: "Terminal.readKey",
315        dimension: EffectDimension::Generative,
316        runtime_params: &[],
317        runtime_return: RuntimeType::OptionStr,
318    },
319    EffectClassification {
320        method: "Terminal.hideCursor",
321        dimension: EffectDimension::Output,
322        runtime_params: &[],
323        runtime_return: RuntimeType::Unit,
324    },
325    EffectClassification {
326        method: "Terminal.showCursor",
327        dimension: EffectDimension::Output,
328        runtime_params: &[],
329        runtime_return: RuntimeType::Unit,
330    },
331    EffectClassification {
332        method: "Terminal.flush",
333        dimension: EffectDimension::Output,
334        runtime_params: &[],
335        runtime_return: RuntimeType::Unit,
336    },
337];
338
339/// Classify a built-in effect method, if it's in Oracle v1's closed set.
340pub fn classify(method: &str) -> Option<&'static EffectClassification> {
341    CLASSIFICATIONS.iter().find(|c| c.method == method)
342}
343
344/// Return `true` if the given name refers to an effect covered by Oracle v1.
345pub fn is_classified(method: &str) -> bool {
346    classify(method).is_some()
347}
348
349/// Oracle signature for use in lifted specs.
350///
351/// - Snapshot: capability reader — unchanged from runtime signature,
352///   wrapped in a function type. `Args.get` → `() -> List<String>`.
353/// - Generative / GenerativeOutput: branch-indexed oracle —
354///   `(BranchPath, Int, <runtime_params...>) -> <runtime_return>`.
355/// - Output: `None` — output effects don't bind oracles (trace API
356///   handles assertions about emissions).
357pub fn oracle_signature(method: &str) -> Option<Type> {
358    let c = classify(method)?;
359    match c.dimension {
360        EffectDimension::Output => None,
361        EffectDimension::Snapshot => {
362            let params: Vec<Type> = c.runtime_params.iter().copied().map(runtime_type).collect();
363            Some(Type::Fn(
364                params,
365                Box::new(runtime_type(c.runtime_return)),
366                vec![],
367            ))
368        }
369        EffectDimension::Generative | EffectDimension::GenerativeOutput => {
370            let mut params = vec![Type::Named(branch_path::TYPE_NAME.to_string()), Type::Int];
371            params.extend(c.runtime_params.iter().copied().map(runtime_type));
372            Some(Type::Fn(
373                params,
374                Box::new(runtime_type(c.runtime_return)),
375                vec![],
376            ))
377        }
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384
385    #[test]
386    fn classify_returns_none_for_unknown() {
387        assert!(classify("Nope.missing").is_none());
388        assert!(classify("Args.set").is_none());
389    }
390
391    #[test]
392    fn args_get_is_snapshot() {
393        let c = classify("Args.get").unwrap();
394        assert_eq!(c.dimension, EffectDimension::Snapshot);
395    }
396
397    #[test]
398    fn random_int_is_generative() {
399        let c = classify("Random.int").unwrap();
400        assert_eq!(c.dimension, EffectDimension::Generative);
401    }
402
403    #[test]
404    fn http_get_is_generative_output() {
405        let c = classify("Http.get").unwrap();
406        assert_eq!(c.dimension, EffectDimension::GenerativeOutput);
407    }
408
409    #[test]
410    fn disk_write_text_is_generative_output() {
411        let c = classify("Disk.writeText").unwrap();
412        assert_eq!(c.dimension, EffectDimension::GenerativeOutput);
413    }
414
415    #[test]
416    fn console_print_is_output() {
417        let c = classify("Console.print").unwrap();
418        assert_eq!(c.dimension, EffectDimension::Output);
419    }
420
421    #[test]
422    fn console_read_line_is_generative() {
423        let c = classify("Console.readLine").unwrap();
424        assert_eq!(c.dimension, EffectDimension::Generative);
425    }
426
427    #[test]
428    fn time_sleep_is_output() {
429        let c = classify("Time.sleep").unwrap();
430        assert_eq!(c.dimension, EffectDimension::Output);
431    }
432
433    #[test]
434    fn terminal_read_key_is_generative() {
435        let c = classify("Terminal.readKey").unwrap();
436        assert_eq!(c.dimension, EffectDimension::Generative);
437    }
438
439    #[test]
440    fn oracle_signature_for_random_int_is_branch_indexed() {
441        let sig = oracle_signature("Random.int").unwrap();
442        // (BranchPath, Int, Int, Int) -> Int
443        match sig {
444            Type::Fn(params, ret, _) => {
445                assert_eq!(params.len(), 4);
446                assert!(matches!(params[0], Type::Named(ref n) if n == "BranchPath"));
447                assert_eq!(params[1], Type::Int);
448                assert_eq!(params[2], Type::Int);
449                assert_eq!(params[3], Type::Int);
450                assert_eq!(*ret, Type::Int);
451            }
452            other => panic!("expected Fn, got {:?}", other),
453        }
454    }
455
456    #[test]
457    fn oracle_signature_for_random_float_is_branch_indexed_no_extra_args() {
458        let sig = oracle_signature("Random.float").unwrap();
459        // (BranchPath, Int) -> Float
460        match sig {
461            Type::Fn(params, ret, _) => {
462                assert_eq!(params.len(), 2);
463                assert!(matches!(params[0], Type::Named(ref n) if n == "BranchPath"));
464                assert_eq!(params[1], Type::Int);
465                assert_eq!(*ret, Type::Float);
466            }
467            other => panic!("expected Fn, got {:?}", other),
468        }
469    }
470
471    #[test]
472    fn oracle_signature_for_args_get_is_capability_reader() {
473        let sig = oracle_signature("Args.get").unwrap();
474        // () -> List<String>   (snapshot: not branch-indexed)
475        match sig {
476            Type::Fn(params, ret, _) => {
477                assert!(params.is_empty());
478                assert_eq!(*ret, Type::List(Box::new(Type::Str)));
479            }
480            other => panic!("expected Fn, got {:?}", other),
481        }
482    }
483
484    #[test]
485    fn oracle_signature_for_env_get_is_capability_reader() {
486        let sig = oracle_signature("Env.get").unwrap();
487        // String -> Option<String>
488        match sig {
489            Type::Fn(params, ret, _) => {
490                assert_eq!(params, vec![Type::Str]);
491                assert_eq!(*ret, Type::Option(Box::new(Type::Str)));
492            }
493            other => panic!("expected Fn, got {:?}", other),
494        }
495    }
496
497    #[test]
498    fn oracle_signature_for_http_get_is_branch_indexed() {
499        let sig = oracle_signature("Http.get").unwrap();
500        // (BranchPath, Int, String) -> Result<HttpResponse, String>
501        match sig {
502            Type::Fn(params, ret, _) => {
503                assert_eq!(params.len(), 3);
504                assert!(matches!(params[0], Type::Named(ref n) if n == "BranchPath"));
505                assert_eq!(params[1], Type::Int);
506                assert_eq!(params[2], Type::Str);
507                match *ret {
508                    Type::Result(ok, err) => {
509                        assert!(matches!(*ok, Type::Named(ref n) if n == "HttpResponse"));
510                        assert_eq!(*err, Type::Str);
511                    }
512                    other => panic!("expected Result, got {:?}", other),
513                }
514            }
515            other => panic!("expected Fn, got {:?}", other),
516        }
517    }
518
519    #[test]
520    fn oracle_signature_for_console_read_line_is_branch_indexed() {
521        let sig = oracle_signature("Console.readLine").unwrap();
522        // (BranchPath, Int) -> Result<String, String>
523        match sig {
524            Type::Fn(params, ret, _) => {
525                assert_eq!(params.len(), 2);
526                assert!(matches!(params[0], Type::Named(ref n) if n == "BranchPath"));
527                assert_eq!(params[1], Type::Int);
528                assert_eq!(*ret, Type::Result(Box::new(Type::Str), Box::new(Type::Str)));
529            }
530            other => panic!("expected Fn, got {:?}", other),
531        }
532    }
533
534    #[test]
535    fn oracle_signature_for_disk_list_dir_returns_result_list_string() {
536        let sig = oracle_signature("Disk.listDir").unwrap();
537        // (BranchPath, Int, String) -> Result<List<String>, String>
538        match sig {
539            Type::Fn(params, ret, _) => {
540                assert_eq!(params.len(), 3);
541                assert!(matches!(params[0], Type::Named(ref n) if n == "BranchPath"));
542                assert_eq!(params[1], Type::Int);
543                assert_eq!(params[2], Type::Str);
544                assert_eq!(
545                    *ret,
546                    Type::Result(
547                        Box::new(Type::List(Box::new(Type::Str))),
548                        Box::new(Type::Str)
549                    )
550                );
551            }
552            other => panic!("expected Fn, got {:?}", other),
553        }
554    }
555
556    #[test]
557    fn oracle_signature_for_tcp_ping_returns_result_unit_string() {
558        let sig = oracle_signature("Tcp.ping").unwrap();
559        // (BranchPath, Int, String, Int) -> Result<Unit, String>
560        match sig {
561            Type::Fn(params, ret, _) => {
562                assert_eq!(params.len(), 4);
563                assert!(matches!(params[0], Type::Named(ref n) if n == "BranchPath"));
564                assert_eq!(params[1], Type::Int);
565                assert_eq!(params[2], Type::Str);
566                assert_eq!(params[3], Type::Int);
567                assert_eq!(
568                    *ret,
569                    Type::Result(Box::new(Type::Unit), Box::new(Type::Str))
570                );
571            }
572            other => panic!("expected Fn, got {:?}", other),
573        }
574    }
575
576    #[test]
577    fn oracle_signature_for_disk_write_text_returns_result_unit_string() {
578        let sig = oracle_signature("Disk.writeText").unwrap();
579        // (BranchPath, Int, String, String) -> Result<Unit, String>
580        match sig {
581            Type::Fn(params, ret, _) => {
582                assert_eq!(params.len(), 4);
583                assert!(matches!(params[0], Type::Named(ref n) if n == "BranchPath"));
584                assert_eq!(params[1], Type::Int);
585                assert_eq!(params[2], Type::Str);
586                assert_eq!(params[3], Type::Str);
587                assert_eq!(
588                    *ret,
589                    Type::Result(Box::new(Type::Unit), Box::new(Type::Str))
590                );
591            }
592            other => panic!("expected Fn, got {:?}", other),
593        }
594    }
595
596    #[test]
597    fn oracle_signature_for_output_effect_is_none() {
598        assert!(oracle_signature("Console.print").is_none());
599        assert!(oracle_signature("Console.error").is_none());
600        assert!(oracle_signature("Console.warn").is_none());
601        assert!(oracle_signature("Time.sleep").is_none());
602        assert!(oracle_signature("Terminal.print").is_none());
603    }
604
605    #[test]
606    fn is_classified_covers_full_v1_set() {
607        for name in &[
608            "Args.get",
609            "Env.get",
610            "Random.int",
611            "Random.float",
612            "Time.now",
613            "Time.unixMs",
614            "Time.sleep",
615            "Disk.readText",
616            "Disk.exists",
617            "Disk.listDir",
618            "Disk.writeText",
619            "Disk.appendText",
620            "Disk.delete",
621            "Disk.deleteDir",
622            "Disk.makeDir",
623            "Console.readLine",
624            "Http.get",
625            "Http.head",
626            "Http.delete",
627            "Http.post",
628            "Http.put",
629            "Http.patch",
630            "Tcp.send",
631            "Tcp.ping",
632            "Console.print",
633            "Console.error",
634            "Console.warn",
635            "Terminal.clear",
636            "Terminal.moveTo",
637            "Terminal.print",
638            "Terminal.readKey",
639            "Terminal.hideCursor",
640            "Terminal.showCursor",
641            "Terminal.flush",
642        ] {
643            assert!(is_classified(name), "{} should be classified", name);
644        }
645    }
646
647    #[test]
648    fn oracle_signature_for_http_post_has_four_runtime_params() {
649        let sig = oracle_signature("Http.post").unwrap();
650        // (BranchPath, Int, Str, Str, Str, List<Header>) -> Result<HttpResponse, String>
651        match sig {
652            Type::Fn(params, ret, _) => {
653                assert_eq!(params.len(), 6);
654                assert!(matches!(params[0], Type::Named(ref n) if n == "BranchPath"));
655                assert_eq!(params[1], Type::Int);
656                assert_eq!(params[2], Type::Str);
657                assert_eq!(params[3], Type::Str);
658                assert_eq!(params[4], Type::Str);
659                match &params[5] {
660                    Type::List(inner) => {
661                        assert!(matches!(&**inner, Type::Named(n) if n == "Header"));
662                    }
663                    other => panic!("expected List<Header>, got {:?}", other),
664                }
665                match *ret {
666                    Type::Result(ok, err) => {
667                        assert!(matches!(*ok, Type::Named(ref n) if n == "HttpResponse"));
668                        assert_eq!(*err, Type::Str);
669                    }
670                    other => panic!("expected Result, got {:?}", other),
671                }
672            }
673            other => panic!("expected Fn, got {:?}", other),
674        }
675    }
676
677    #[test]
678    fn ambient_protocol_and_modal_effects_not_classified() {
679        // These remain replay-only in v1.
680        for name in &[
681            "Env.set",
682            "Tcp.connect",
683            "Tcp.writeLine",
684            "Tcp.readLine",
685            "Tcp.close",
686            "HttpServer.listen",
687            "Terminal.enableRawMode",
688            "Terminal.disableRawMode",
689            "Terminal.setColor",
690            "Terminal.resetColor",
691            "Terminal.size",
692        ] {
693            assert!(!is_classified(name), "{} should NOT be classified", name);
694        }
695    }
696}