Skip to main content

component_testing/
component_testing.rs

1//! Component testing pattern — testing Lua "components" that
2//! follow a request/response protocol.
3//!
4//! Mirrors the pattern used in orcs-cli where Lua components
5//! expose `on_request` / `on_signal` handlers.
6
7use mlua::prelude::*;
8
9fn main() {
10    let lua = Lua::new();
11    mlua_lspec::register(&lua).expect("register lspec");
12    mlua_lspec::register_doubles(&lua).expect("register doubles");
13
14    lua.load(
15        r#"
16        local describe, it, expect = lust.describe, lust.it, lust.expect
17
18        -- ================================================================
19        -- Component definition (the code under test)
20        -- ================================================================
21        local function create_counter_component()
22            local count = 0
23            return {
24                id = "counter",
25
26                on_request = function(req)
27                    if req.operation == "increment" then
28                        count = count + (req.payload.amount or 1)
29                        return { success = true, data = { count = count } }
30                    elseif req.operation == "get" then
31                        return { success = true, data = { count = count } }
32                    elseif req.operation == "reset" then
33                        count = 0
34                        return { success = true, data = { count = 0 } }
35                    end
36                    return { success = false, error = "unknown: " .. req.operation }
37                end,
38
39                on_signal = function(sig)
40                    if sig == "stop" then
41                        count = 0
42                        return "stopped"
43                    end
44                    return "ignored"
45                end,
46            }
47        end
48
49        -- ================================================================
50        -- Minimal harness for request/signal simulation
51        -- ================================================================
52        local function create_harness(component_fn)
53            local comp = component_fn()
54            return {
55                request = function(self, operation, payload)
56                    local resp = comp.on_request({
57                        operation = operation,
58                        payload = payload or {},
59                    })
60                    if resp.success then
61                        return resp.data
62                    end
63                    error(resp.error)
64                end,
65                signal = function(self, kind)
66                    return comp.on_signal(kind)
67                end,
68                id = function(self)
69                    return comp.id
70                end,
71            }
72        end
73
74        -- ================================================================
75        -- Tests
76        -- ================================================================
77
78        describe('counter component', function()
79            local h
80
81            lust.before(function()
82                h = create_harness(create_counter_component)
83            end)
84
85            describe('identity', function()
86                it('has correct id', function()
87                    expect(h:id()).to.equal("counter")
88                end)
89            end)
90
91            describe('increment', function()
92                it('increments by 1 by default', function()
93                    local result = h:request("increment")
94                    expect(result.count).to.equal(1)
95                end)
96
97                it('increments by custom amount', function()
98                    local result = h:request("increment", { amount = 5 })
99                    expect(result.count).to.equal(5)
100                end)
101
102                it('accumulates across calls', function()
103                    h:request("increment")
104                    h:request("increment")
105                    h:request("increment")
106                    local result = h:request("get")
107                    expect(result.count).to.equal(3)
108                end)
109            end)
110
111            describe('reset', function()
112                it('resets count to zero', function()
113                    h:request("increment")
114                    h:request("increment")
115                    h:request("reset")
116                    local result = h:request("get")
117                    expect(result.count).to.equal(0)
118                end)
119            end)
120
121            describe('error handling', function()
122                it('rejects unknown operations', function()
123                    expect(function()
124                        h:request("unknown_op")
125                    end).to.fail.with("unknown")
126                end)
127            end)
128
129            describe('signals', function()
130                it('stop signal resets state', function()
131                    h:request("increment")
132                    h:request("increment")
133                    local response = h:signal("stop")
134                    expect(response).to.equal("stopped")
135
136                    local result = h:request("get")
137                    expect(result.count).to.equal(0)
138                end)
139
140                it('unknown signal is ignored', function()
141                    local response = h:signal("unknown")
142                    expect(response).to.equal("ignored")
143                end)
144            end)
145        end)
146    "#,
147    )
148    .exec()
149    .expect("test execution failed");
150
151    let summary = mlua_lspec::collect_results(&lua).expect("collect results");
152
153    println!(
154        "{} passed, {} failed out of {} tests",
155        summary.passed, summary.failed, summary.total
156    );
157    for test in &summary.tests {
158        let icon = if test.passed { "PASS" } else { "FAIL" };
159        println!("  [{icon}] {}: {}", test.suite, test.name);
160    }
161
162    assert_eq!(summary.failed, 0, "all tests should pass");
163}