assay-lua 0.17.9

General-purpose enhanced Lua runtime. Batteries-included scripting, automation, and web services.
Documentation
// Regression for issue #40 — older assay versions failed at coroutine
// resume with `error converting Lua nil to function` when a workflow
// handler called `os.date()` or `ctx:register_query()`. The v0.13.0
// rewrite moved workflow execution to pure-Lua `coroutine.create` (in
// `stdlib/engine/workflow/worker.lua`), which inherits globals from the
// parent state. This test pins that contract by exercising the same
// code paths the bug reported, without requiring a running engine.

mod common;

use common::run_lua;

#[tokio::test]
async fn workflow_coroutine_can_call_os_date_and_register_query() {
    let script = r#"
        -- Simulate the ctx the worker hands to a workflow function.
        -- register_query in the real ctx stores into _query_handlers; the
        -- shape mirrors stdlib/engine/workflow/ctx.lua.
        local ctx = {
            _query_handlers = {},
        }
        function ctx:register_query(name, fn)
            self._query_handlers[name] = fn
        end

        -- This is the exact pattern from the issue: a workflow function
        -- that calls register_query and os.date on first run.
        local function workflow(c, input)
            c:register_query("state", function() return { ok = true } end)
            local now = os.date("!%Y-%m-%dT%H:%M:%SZ")
            return { now = now, input = input }
        end

        local co = coroutine.create(function() return workflow(ctx, { hello = true }) end)
        local ok, result = coroutine.resume(co)

        assert.eq(ok, true)
        assert.eq(coroutine.status(co), "dead")
        assert.eq(type(result), "table")
        assert.eq(result.input.hello, true)
        assert.eq(result.now ~= nil and #result.now > 0, true)
        assert.not_nil(ctx._query_handlers.state)

        -- Query handler is callable from outside the coroutine.
        local snapshot = ctx._query_handlers.state()
        assert.eq(snapshot.ok, true)
    "#;
    run_lua(script).await.unwrap();
}

#[tokio::test]
async fn coroutine_can_yield_and_resume_with_args() {
    // Workflow ctx commands work via coroutine.yield -> host resumes with
    // the result. Verify that round-trip doesn't hit the "nil to function"
    // class of errors the issue described.
    let script = r#"
        local function workflow()
            local first = coroutine.yield({ type = "Step1" })
            local second = coroutine.yield({ type = "Step2", got = first })
            return { final = second }
        end

        local co = coroutine.create(workflow)
        local ok, cmd1 = coroutine.resume(co)
        assert.eq(ok, true)
        assert.eq(cmd1.type, "Step1")

        local ok2, cmd2 = coroutine.resume(co, "alpha")
        assert.eq(ok2, true)
        assert.eq(cmd2.type, "Step2")
        assert.eq(cmd2.got, "alpha")

        local ok3, result = coroutine.resume(co, "beta")
        assert.eq(ok3, true)
        assert.eq(result.final, "beta")
        assert.eq(coroutine.status(co), "dead")
    "#;
    run_lua(script).await.unwrap();
}

#[tokio::test]
async fn activity_retry_boundary_discards_failure_path_activity_results() {
    let script = r#"
        local ctx_mod = require("assay.engine.workflow.ctx")
        local history = {
            {
                seq = 2,
                event_type = "ActivityCompleted",
                payload = { activity_seq = 1, name = "prepare", result = { prepared = true } },
            },
            {
                seq = 4,
                event_type = "ActivityFailed",
                payload = { activity_seq = 2, name = "update_config", error = "rejected" },
            },
            {
                seq = 6,
                event_type = "ActivityCompleted",
                payload = { activity_seq = 3, name = "notify_failure", result = { sent = true } },
            },
            {
                seq = 8,
                event_type = "ActivityRetryRequested",
                payload = { activity_seq = 2, failed_event_seq = 4 },
            },
            {
                seq = 9,
                event_type = "ActivityCompleted",
                payload = { activity_seq = 2, name = "update_config", result = { commit = "abc123" } },
            },
        }
        local ctx = ctx_mod.make("wf-retry", history)
        local co = coroutine.create(function()
            local prepared = ctx:execute_activity("prepare", {})
            assert.eq(prepared.prepared, true)
            local update = ctx:execute_activity("update_config", {})
            assert.eq(update.commit, "abc123")
            return ctx:execute_activity("deploy", {})
        end)

        local ok, command = coroutine.resume(co)
        assert.eq(ok, true)
        assert.eq(coroutine.status(co), "suspended")
        assert.eq(command.type, "ScheduleActivity")
        assert.eq(command.seq, 3)
        assert.eq(command.name, "deploy")
    "#;
    run_lua(script).await.unwrap();
}