1
//! `eval_form` driven through a real `rpc::Session` for every `RenderMode`.
2
//! List and report modes get the reply shape their natives produce as a string
3
//! literal (or `nil`), so no database is needed to reach each arm.
4

            
5
use rpc::{ScriptCtx, Session};
6

            
7
use super::*;
8

            
9
const REPORT: &str = r#""(:periods nil)""#;
10

            
11
23
async fn eval(form: &str, mode: RenderMode) -> Result<Rendered, CommandError> {
12
23
    let mut session = Session::new(ScriptCtx::new(Uuid::nil())).expect("Session::new");
13
23
    eval_form(&mut session, form, mode).await
14
23
}
15

            
16
17
async fn lines(form: &str, mode: RenderMode) -> Vec<String> {
17
17
    match eval(form, mode).await {
18
17
        Ok(Rendered::Lines(lines)) => lines,
19
        Ok(Rendered::Bytes(_)) => panic!("{form}: rendered bytes"),
20
        Ok(Rendered::Nothing) => panic!("{form}: rendered nothing"),
21
        Err(e) => panic!("{form}: {e}"),
22
    }
23
17
}
24

            
25
2
async fn error_message(form: &str) -> String {
26
2
    match eval(form, RenderMode::Scalar).await {
27
2
        Err(CommandError::Command(message)) => message,
28
        Err(other) => panic!("{form}: unexpected error {other}"),
29
        Ok(_) => panic!("{form}: expected a server error"),
30
    }
31
2
}
32

            
33
3
fn report_lines(rendered: Result<Rendered, CommandError>) -> Vec<String> {
34
3
    match rendered {
35
3
        Ok(Rendered::Lines(lines)) => lines,
36
        _ => panic!("report must render as text lines"),
37
    }
38
3
}
39

            
40
#[tokio::test]
41
1
async fn eval_form_scalar_renders_numbers_and_strings() {
42
1
    assert_eq!(lines("(+ 1 2)", RenderMode::Scalar).await, vec!["3"]);
43
1
    assert_eq!(lines(r#""hello""#, RenderMode::Scalar).await, vec!["hello"]);
44
1
}
45

            
46
#[tokio::test]
47
1
async fn eval_form_bool_success_decodes_t_nil_and_flags() {
48
1
    assert_eq!(lines("t", RenderMode::BoolSuccess).await, vec!["true"]);
49
1
    assert_eq!(lines("nil", RenderMode::BoolSuccess).await, vec!["false"]);
50
1
    assert_eq!(lines("0", RenderMode::BoolSuccess).await, vec!["false"]);
51
1
    assert_eq!(lines("1", RenderMode::BoolSuccess).await, vec!["true"]);
52
1
    assert!(eval(r#""yes""#, RenderMode::BoolSuccess).await.is_err());
53
1
}
54

            
55
#[tokio::test]
56
1
async fn eval_form_silent_renders_nothing() {
57
1
    assert!(matches!(
58
1
        eval("(+ 1 2)", RenderMode::Silent).await,
59
1
        Ok(Rendered::Nothing)
60
1
    ));
61
1
}
62

            
63
#[tokio::test]
64
1
async fn eval_form_config_value_extracts_string_bytes_and_unset() {
65
1
    assert_eq!(
66
1
        lines(r#""(:config-value \"x\")""#, RenderMode::ConfigValue).await,
67
1
        vec!["x"]
68
    );
69
1
    assert!(matches!(
70
1
        eval(r#""(:config-value #u8(1 2))""#, RenderMode::ConfigValue).await,
71
1
        Ok(Rendered::Bytes(ref b)) if b == &[1, 2]
72
    ));
73
1
    assert!(matches!(
74
1
        eval(r#""(:config-value nil)""#, RenderMode::ConfigValue).await,
75
1
        Ok(Rendered::Nothing)
76
1
    ));
77
1
}
78

            
79
#[tokio::test]
80
1
async fn eval_form_account_and_commodity_lists() {
81
1
    assert_eq!(
82
1
        lines(
83
1
            r#""((:account :id \"a\" :name \"Cash\"))""#,
84
1
            RenderMode::AccountList
85
1
        )
86
1
        .await,
87
1
        vec!["Cash"]
88
    );
89
1
    assert!(lines("nil", RenderMode::AccountList).await.is_empty());
90
1
    assert_eq!(
91
1
        lines(
92
1
            r#""((:commodity :id \"c\" :symbol \"USD\" :name \"US Dollar\"))""#,
93
1
            RenderMode::CommodityList
94
1
        )
95
1
        .await,
96
1
        vec!["USD - US Dollar"]
97
1
    );
98
1
}
99

            
100
#[tokio::test]
101
1
async fn eval_form_ssh_key_and_transaction_lists() {
102
1
    assert_eq!(
103
1
        lines(
104
1
            r#""((:ssh-key :fingerprint \"SHA256:x\" :key-type \"ed25519\" :created-at \"2026-01-01\" :name \"laptop\"))""#,
105
1
            RenderMode::SshKeyList
106
1
        )
107
1
        .await,
108
1
        vec!["laptop  [ed25519]  SHA256:x  created:2026-01-01  last-used:never"]
109
    );
110
1
    assert_eq!(
111
1
        lines(
112
1
            r#""((:transaction :id \"t-1\" :note \"lunch\" :post-date \"2026-06-27\"))""#,
113
1
            RenderMode::TransactionList
114
1
        )
115
1
        .await,
116
1
        vec!["lunch - 2026-06-27"]
117
    );
118
1
    assert!(lines("nil", RenderMode::TransactionList).await.is_empty());
119
1
}
120

            
121
#[tokio::test]
122
1
async fn eval_form_empty_balance_list_prints_the_sentinel() {
123
1
    assert_eq!(
124
1
        lines("nil", RenderMode::BalanceList).await,
125
1
        vec![NO_TRANSACTION_YET]
126
1
    );
127
1
}
128

            
129
#[tokio::test]
130
1
async fn eval_form_report_modes_render_text_charts() {
131
1
    let report = Value::String("(:periods nil)".into());
132
1
    let chart = "text".to_string();
133
1
    assert_eq!(
134
1
        lines(
135
1
            REPORT,
136
1
            RenderMode::ReportBalance {
137
1
                chart: chart.clone()
138
1
            }
139
1
        )
140
1
        .await,
141
1
        report_lines(render_report_balance(&report, &chart))
142
    );
143
1
    assert_eq!(
144
1
        lines(
145
1
            REPORT,
146
1
            RenderMode::ReportActivity {
147
1
                chart: chart.clone()
148
1
            }
149
1
        )
150
1
        .await,
151
1
        report_lines(render_report_activity(&report, &chart))
152
    );
153
1
    assert_eq!(
154
1
        lines(
155
1
            REPORT,
156
1
            RenderMode::ReportBreakdown {
157
1
                chart: chart.clone()
158
1
            }
159
1
        )
160
1
        .await,
161
1
        report_lines(render_report_breakdown(&report, &chart))
162
1
    );
163
1
}
164

            
165
#[tokio::test]
166
1
async fn eval_form_server_errors_surface_as_command_errors() {
167
1
    let raised = error_message(r#"(error 'boom "bad input")"#).await;
168
1
    assert!(
169
1
        raised.contains("boom") && raised.contains("bad input"),
170
        "{raised}"
171
    );
172
1
    let undefined = error_message("(no-such-function)").await;
173
1
    assert!(undefined.contains("[compile]"), "{undefined}");
174
1
}