1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
//! Issue #1973: a `#[state_machine(transitions(...))]` edge can declare an
//! `on_commit = <Job>` after-commit effect. Declaring one makes the model
//! generate a connection-taking `transition_{field}_to_on_conn` method that
//! validates the transition and enqueues the named job **transactionally** on
//! the caller's connection with a derived idempotency key. Guards and effects
//! compose on a single edge.
//!
//! This is a lightweight compilation + pure-validation contract test that runs
//! WITHOUT Docker (it never opens a connection): it constructs models by hand
//! and calls the generated pure validators, and references the connection-taking
//! method behind a never-called async fn so the generated effect codegen is
//! type-checked. Run it standalone with:
//!
//! ```text
//! cargo test -p autumn-web --test state_machine_on_commit
//! ```
#![cfg(feature = "db")]
use autumn_web::model;
use autumn_web::prelude::*;
use autumn_web::reexports::diesel_async::AsyncPgConnection;
use serde::{Deserialize, Serialize};
// The framework-provided transition context is this job's args. The derived
// idempotency key rides on the payload, so declaring the job
// `unique, by = ["idempotency_key"]` coalesces a retried transition.
#[derive(Debug, Clone, Serialize, Deserialize)]
struct EffectArgs {
idempotency_key: String,
}
#[job(name = "on_commit_send_shipped_email")]
#[allow(clippy::unused_async)] // stub handler: no real work in this codegen test
async fn send_shipped_email(_state: AppState, _args: EffectArgs) -> AutumnResult<()> {
Ok(())
}
#[job(name = "on_commit_announce_archive")]
#[allow(clippy::unused_async)] // stub handler: no real work in this codegen test
async fn announce_archive(_state: AppState, _args: EffectArgs) -> AutumnResult<()> {
Ok(())
}
diesel::table! {
on_commit_orders (id) {
id -> BigInt,
status -> Text,
}
}
#[model(table = "on_commit_orders")]
pub struct Order {
#[id]
pub id: i64,
#[state_machine(transitions(
pending -> processing,
processing -> shipped: on_commit = SendShippedEmailJob,
shipped -> archived: guard = "can_archive", on_commit = AnnounceArchiveJob,
))]
pub status: String,
}
impl Order {
// A stub guard for the codegen test; real guards do non-trivial work.
#[allow(clippy::missing_const_for_fn)]
fn can_archive(&self) -> bool {
!self.status.is_empty()
}
}
fn order(status: &str) -> Order {
Order {
id: 7,
status: status.to_string(),
}
}
// The pure validator is byte-for-byte unchanged by adding `on_commit`.
#[test]
fn pure_validator_still_governs_allowed_edges() {
assert!(order("pending").can_transition_status_to("processing"));
assert!(order("processing").can_transition_status_to("shipped"));
// The guard on the composed edge is still honoured.
assert!(order("shipped").can_transition_status_to("archived"));
// Undeclared edges remain denied.
assert!(!order("pending").can_transition_status_to("shipped"));
assert!(order("pending").transition_status_to("shipped").is_err());
assert_eq!(
order("processing")
.transition_status_to("shipped")
.expect("declared edge"),
"shipped"
);
}
// Compilation contract: the additive connection-taking method exists, takes a
// connection, and returns the new state. Never called (no runtime/DB here) — its
// mere existence type-checks the generated effect codegen end to end.
#[allow(dead_code)]
async fn _assert_on_conn_signature(
order: &Order,
conn: &mut AsyncPgConnection,
) -> AutumnResult<String> {
order.transition_status_to_on_conn(conn, "shipped").await
}
// The `TransitionEffect` payload the generated effect enqueues is public and
// serializable, and its idempotency key composes the transition context.
#[test]
fn transition_effect_payload_is_public_and_serializable() {
let effect = autumn_web::TransitionEffect {
model: "Order".to_string(),
field: "status".to_string(),
record_id: "7".to_string(),
from_state: "processing".to_string(),
to_state: "shipped".to_string(),
idempotency_key: "Order:status:7:processing:shipped".to_string(),
};
let json = serde_json::to_value(&effect).expect("serializes");
assert_eq!(json["idempotency_key"], "Order:status:7:processing:shipped");
let round: autumn_web::TransitionEffect = serde_json::from_value(json).expect("round-trips");
assert_eq!(round, effect);
}