Skip to main content

aion_server/
run.rs

1//! Run loop for the Aion workflow server: tracing initialization,
2//! configuration load, transport startup, and signal-driven graceful
3//! shutdown.
4//!
5//! This is the library entry point behind the `aion server` command. It
6//! preserves the operational contract of the former standalone
7//! `aion-server` binary: exit code 2 for configuration errors, the drain
8//! outcome's exit code on shutdown, and 130 when a second termination
9//! signal forces immediate exit.
10
11use std::process::ExitCode;
12
13use tracing::{error, info, warn};
14
15use crate::{
16    ServerConfig, ServerError, ServerState,
17    config::{CliOverrides, NamespaceMode, StoreBackend},
18    observability,
19    shutdown::ShutdownOutcome,
20};
21
22mod doors;
23mod outbox_commission;
24mod transports;
25
26use doors::{bind_and_claim, serve_until_shutdown};
27use outbox_commission::{
28    BackpressureSettings, maybe_spawn_cluster_supervisor, maybe_spawn_outbox_dispatcher,
29    rebuild_outbox_boot_state,
30};
31use transports::{serve_grpc, serve_http};
32
33/// Run the Aion workflow server until it shuts down, returning the process
34/// exit code.
35///
36/// Initializes the JSON tracing subscriber, loads and validates the merged
37/// configuration (file, environment, then `overrides`), serves the gRPC and
38/// HTTP transports, and drains gracefully after the first termination
39/// signal. Every failure is logged through tracing and mapped to the exit
40/// code contract above; the caller only has to exit with the returned code.
41pub async fn run(overrides: CliOverrides) -> ExitCode {
42    match run_server(overrides).await {
43        Ok(code) => code,
44        Err(error) => {
45            error!(%error, "aion-server failed");
46            if error.is_config() {
47                ExitCode::from(2)
48            } else {
49                ExitCode::FAILURE
50            }
51        }
52    }
53}
54
55/// The where-to-edit half of the missing `outbox.liminal_listen_address`
56/// refusal: a liminal outbox refusal must name the FILE to edit, not just the
57/// key — the operator reading it is exactly the operator who did not write
58/// the config (a scaffolded or setup-script home).
59fn liminal_address_hint(source: &crate::config::ConfigSource) -> String {
60    match source {
61        crate::config::ConfigSource::BuiltInDefaults => {
62            "set AION_OUTBOX_LIMINAL_LISTEN_ADDRESS, or add `liminal_listen_address = \
63             \"127.0.0.1:50061\"` to `[outbox]` in a config file"
64                .to_owned()
65        }
66        source => format!(
67            "add `liminal_listen_address = \"127.0.0.1:50061\"` to `[outbox]` in the {source}"
68        ),
69    }
70}
71
72async fn run_server(cli: CliOverrides) -> Result<ExitCode, ServerError> {
73    observability::tracing::init()?;
74
75    // #180: a boot that discovers no config anywhere first scaffolds
76    // `<AION_HOME>/config.toml` from the embedded template (claim-only-when-
77    // empty), then loads it — config LOAD itself stays pure and read-only.
78    let loaded = crate::config::load_or_scaffold(&cli)?;
79    loaded.resolution.ensure_private_home()?;
80    // Arm the death note as early as the home exists, so every later failure
81    // path — including config validation and state build — runs inside the
82    // ARMED/DISARMED bracket. Two anonymous server deaths on 2026-08-16 are
83    // why this exists; see the module docs for the exact coverage.
84    let death_note = crate::death_note::DeathNote::arm(&loaded.resolution.home)?;
85    let home = loaded.resolution.home.clone();
86    loaded.resolution.log_startup();
87    let liminal_address_hint = liminal_address_hint(&loaded.resolution.source);
88    let config = loaded.config;
89    reject_auth_without_feature(&config)?;
90    let store_backend = config.store.backend;
91    // Static shard assignment (SS-1): read the operator's pinned shard set from
92    // `[store] owned_shards`. Empty means own ALL shards (single-node default).
93    // The set is carried into `RuntimeConfig` by `into_parts` and applied to the
94    // `EngineBuilder` during state construction; surface it here so the boot
95    // banner records which shards this node serves. No election is performed.
96    let owned_shards = config.store.owned_shards.clone();
97    // Capture the outbox settings before `build` consumes `config`, so the
98    // (default-off) outbox dispatcher can be wired after state is up. The
99    // dispatcher shares the engine's already-opened haematite store via
100    // `state.outbox_store()`, so no store settings are needed.
101    let outbox_config = config.outbox.clone();
102    // Control-Plane Phase 2 (P2-Q2): capture the keyed-backpressure inputs — the
103    // generous platform-default ceiling and this node's owned-shard fraction —
104    // before `build` consumes `config`. On a single-node / own-all boot the fraction
105    // is 1, so per-node ceilings equal the cluster-wide quota and, with the generous
106    // default and no tenant override, the ceiling never engages (byte-identical claim).
107    let backpressure_settings = BackpressureSettings::from_config(&config);
108    // Capture the SS-5b failover supervisor knobs before `build` consumes config.
109    // Only a distributed haematite boot carries a `[store.cluster]` section; this
110    // is `None` for every single-node boot, so no supervisor is ever spawned.
111    let cluster_config = config.store.cluster.clone();
112    // Capture the managed-worker supervision policy before `build` consumes
113    // `config`. Resolution already happened during config validation, so this
114    // cannot surprise an operator at boot; it is re-read here because the
115    // policy is COMMISSIONED onto the supervisor built into state below, and a
116    // server without the section supervises nothing.
117    let supervision_policy = config.worker_supervision.resolve()?;
118    let state = ServerState::build(config).await?;
119    reject_tls_until_supported(&state)?;
120
121    let runtime = state.runtime_config();
122    let grpc_address = runtime.listen.grpc;
123    let http_address = runtime.listen.http;
124    let workflow_packages: Vec<String> = runtime
125        .workflow_packages
126        .iter()
127        .map(|path| path.display().to_string())
128        .collect();
129    // The revision, not just the version. A crate version cannot distinguish
130    // two builds from different commits of the same version, and that is the
131    // distinction an operator needs when deciding whether a restart restores
132    // what was running or substitutes something else (#123). The endpoint
133    // answers this too, but a crashed server leaves only its log.
134    let build = crate::build_identity::BuildIdentity::current();
135    // #139: the server-resolved workspace root (the aion home's `clones/`
136    // directory) that declared bodies expand `{workspace_root}` with. Reported
137    // here so composition points (setup.sh today, the workspace verb later)
138    // READ the value from the server that will use it instead of re-deriving
139    // it. An unresolvable root is reported as exactly that — never fabricated;
140    // a placeholder-bearing dispatch will refuse terminally with this reason.
141    // The rendering itself is `WorkspaceRoot::banner_value`, pinned by its own
142    // two-case test, so the banner and the tests cannot drift apart.
143    let workspace_root = state.workspace_root().banner_value();
144    info!(
145        version = env!("CARGO_PKG_VERSION"),
146        build = %build.line(),
147        commit = build.commit,
148        grpc_address = %grpc_address,
149        http_address = %http_address,
150        default_namespace = %runtime.default_namespace,
151        namespace_mode = namespace_mode_label(&runtime.namespace.mode),
152        store_backend = store_backend_label(store_backend),
153        auth_enabled = runtime.auth.enabled,
154        deploy_enabled = runtime.deploy.enabled,
155        metrics_enabled = runtime.metrics.enabled,
156        workspace_root = %workspace_root,
157        death_note = %death_note.path().display(),
158        workflow_package_count = workflow_packages.len(),
159        workflow_packages = ?workflow_packages,
160        owned_shards = ?owned_shards,
161        owns_all_shards = owned_shards.is_empty(),
162        "aion-server startup banner"
163    );
164    // #139 leg C: the assistant ships IN aion. The embedded document is
165    // installed here — after the engine has reloaded every persisted package,
166    // so the install can see what is already resident, and before the
167    // transports accept traffic, so the first caller finds it. It claims only a
168    // catalog holding no version of the assistant type; anything else is the
169    // operator's cut to make, and the outcome says so in the log either way.
170    crate::assistant::install_embedded_assistant_for_server(&state, &liminal_address_hint).await;
171    // #189 slice one: the built-in update check ships the same way, under the
172    // same only-the-empty-case install rule. Installing makes it STARTABLE
173    // and nothing else — no check runs without an explicit operator act.
174    crate::update_check::install_embedded_update_check_for_server(&state).await;
175    let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
176    // LSUB-4-1: a distributed haematite boot carries a `[store.cluster]` section.
177    // The single outbox dispatcher task is spawned in BOTH modes; the difference
178    // is only how ownership is enforced. Single-node (`None`) owns all shards by
179    // construction (`owned_shard_scope() == None`), so its claim sweeps see every
180    // row. Clustered (`Some`) relies on `claim_outbox_rows`' `owned_shard_scope()`
181    // filter — already seeded by `set_owned_shards` during `ServerState::build`,
182    // which runs before this point — so each node only ever claims rows on the
183    // shards it owns. Compute the flag here where the cluster section is in
184    // scope; pass it to the gate so the boot banner records the mode.
185    let outbox_clustered = cluster_config.is_some();
186    // Dormant by default: only when `outbox.enabled` is set does the
187    // non-replayed outbox dispatcher task start. With the flag off (the
188    // default) nothing here runs and server behaviour is unchanged.
189    // Hold the liminal worker listener (if any) for the server's lifetime: it is
190    // dropped at the end of `run_server`, after the serve `select!` completes, so
191    // its accept worker stops cleanly on shutdown via the listener's own `Drop`.
192    // #204/#253: rebuild the pause dispatch-hold and settle terminal
193    // workflows' stranded outbox rows BEFORE the dispatcher's first claim.
194    rebuild_outbox_boot_state(&state, &outbox_config).await;
195    let _outbox_worker_listener = maybe_spawn_outbox_dispatcher(
196        &state,
197        &outbox_config,
198        outbox_clustered,
199        backpressure_settings,
200        &shutdown_rx,
201        &liminal_address_hint,
202    )?;
203    // SS-5b: a distributed boot whose peers declare owned shards runs the cluster
204    // supervisor — automatic failover detection. A single-node boot spawns
205    // nothing here (the method returns `false`), so default behaviour is
206    // unchanged.
207    maybe_spawn_cluster_supervisor(&state, cluster_config.as_ref(), &shutdown_rx)?;
208    // #176: the worker heartbeat expiry sweeper is ALWAYS commissioned —
209    // dead-worker detection is a liveness correctness property, not an opt-in
210    // feature. It is the production caller of `fail_expired_workers`: a worker
211    // whose stream stays open while its process wedges (stops heartbeating
212    // without disconnecting) is expired, deregistered with the provable Timeout
213    // reason, and its in-flight tasks surface as TRANSPORT losses, re-dispatched
214    // attempt-neutrally rather than charged to the action's retry budget.
215    // Cadence derives from `worker.heartbeat_window` (quarter-window, clamped to
216    // [1s, window]; the default 30s window sweeps every 7.5s) — deliberately no
217    // separate config knob. It drains on the same shutdown watch as the
218    // transports; dropping the JoinHandle only detaches the task.
219    drop(state.spawn_heartbeat_sweeper(shutdown_rx.clone()));
220    commission_worker_supervision(&state, supervision_policy).await;
221    // Bind both listeners and claim the home FIRST: the pid record the stop
222    // verb trusts is written only once both doors are provably ours.
223    let doors = bind_and_claim(
224        &home,
225        grpc_address,
226        http_address,
227        build.commit,
228        state.runtime_config().drain_timeout,
229    )
230    .await?;
231    let identity_pid = doors.identity_pid;
232    let pid_file_guard = doors.pid_file_guard;
233    // Instant doors: the startup catch-up legs (owed timer fires, schedule
234    // catch-up) run as a background task CONCURRENT with the transports —
235    // the backlog has no upper bound, and a boot that blocks on it keeps the
236    // doors shut for the whole sweep (the 2026-08-24 estate outage shape:
237    // 37+ minutes of healthy catch-up with every listener refusing).
238    // Workflow-residency recovery already ran inside `ServerState::build`,
239    // so every surface the transports serve answers correctly while the
240    // catch-up drains behind them.
241    drop(state.spawn_startup_catchup(shutdown_rx.clone())?);
242    let mut grpc = tokio::spawn(serve_grpc(
243        state.clone(),
244        doors.grpc_listener,
245        doors.bound_grpc,
246        shutdown_rx.clone(),
247    ));
248    let mut http = tokio::spawn(serve_http(
249        state.clone(),
250        doors.http_listener,
251        doors.bound_http,
252        shutdown_rx,
253    ));
254
255    let report = serve_until_shutdown(&state, &shutdown_tx, &mut grpc, &mut http).await?;
256
257    let outcome = report.outcome;
258    let exit_code = outcome.exit_code();
259    // The rich outcome crosses the process boundary through the death note
260    // (one file, one writer); the exit code keeps the #207 contract.
261    death_note.record_outcome(&crate::control::outcome::OutcomeRecord::from_report(
262        identity_pid,
263        &report,
264    ));
265    death_note.disarm(&format!(
266        "clean run-loop exit: shutdown outcome {outcome:?}"
267    ));
268    drop(pid_file_guard);
269    Ok(exit_code)
270}
271
272/// Install the operator's supervision policy and converge the fleet.
273///
274/// Uncommissioned is a first-class but never SILENT state: a server with no
275/// `[worker_supervision]` section supervises nothing, and every deployment that
276/// wanted to be running is named in the warning, so the gap between "the
277/// operator deployed a worker" and "nothing is running it" is never quiet.
278async fn commission_worker_supervision(
279    state: &ServerState,
280    policy: Option<crate::worker::SupervisionPolicy>,
281) {
282    let supervisor = state.worker_supervisor();
283    let Some(policy) = policy else {
284        match supervisor.report().await {
285            Ok(report) => {
286                let wanted: Vec<&str> = report
287                    .workers
288                    .iter()
289                    .filter(|worker| worker.desired == aion_store::DesiredState::Running)
290                    .map(|worker| worker.name.as_str())
291                    .collect();
292                if wanted.is_empty() {
293                    info!("managed-worker supervision is not configured; no deployment wants it");
294                } else {
295                    warn!(
296                        deployments = wanted.join(", "),
297                        remedy = crate::worker::supervisor::UNCOMMISSIONED_REMEDY,
298                        "worker deployments want to be running but supervision is not configured"
299                    );
300                }
301            }
302            Err(error) => error!(
303                %error,
304                "managed-worker supervision is not configured and the deployment records \
305                 could not be read to say what that costs"
306            ),
307        }
308        return;
309    };
310    if !supervisor.commission(policy, crate::worker::ManagedExecutable::CurrentServer) {
311        error!("managed-worker supervision was already commissioned before boot completed");
312        return;
313    }
314    match supervisor.reconcile().await {
315        Ok(0) => info!("managed-worker supervision commissioned; no deployment wants to run"),
316        Ok(supervised) => info!(supervised, "managed-worker supervision commissioned"),
317        Err(error) => error!(%error, "managed-worker fleet could not be converged at boot"),
318    }
319}
320
321fn reject_auth_without_feature(config: &ServerConfig) -> Result<(), ServerError> {
322    if cfg!(not(feature = "auth")) && config.auth.enabled {
323        return Err(ServerError::Config {
324            message: "auth.enabled=true but binary compiled without auth feature".to_owned(),
325        });
326    }
327    Ok(())
328}
329
330fn reject_tls_until_supported(state: &ServerState) -> Result<(), ServerError> {
331    if state.runtime_config().tls.is_some() {
332        return Err(ServerError::Config {
333            message: "configured TLS material cannot be served until transport TLS is wired"
334                .to_owned(),
335        });
336    }
337    Ok(())
338}
339
340fn store_backend_label(backend: StoreBackend) -> &'static str {
341    match backend {
342        StoreBackend::Memory => "memory",
343        StoreBackend::Haematite => "haematite",
344    }
345}
346
347fn namespace_mode_label(mode: &NamespaceMode) -> &'static str {
348    match mode {
349        NamespaceMode::SharedEngine => "SharedEngine",
350        NamespaceMode::SingleTenant { .. } => "SingleTenant",
351    }
352}
353
354#[cfg(test)]
355mod tests {
356    #![allow(clippy::expect_used)]
357
358    use super::outbox_commission::{
359        BackpressureSettings, maybe_spawn_outbox_dispatcher, resolve_outbox_reconciler_config,
360    };
361    use crate::ServerState;
362    use crate::config::RuntimeConfig;
363    use crate::config::{OutboxConfig, OutboxTransport};
364    use aion_store::InMemoryStore;
365    use std::net::SocketAddr;
366    use std::time::Duration;
367
368    /// Own-all, generous-default backpressure settings for the gate tests (the
369    /// single-node default: fraction 1, so the ceiling never engages).
370    fn test_backpressure_settings() -> BackpressureSettings {
371        BackpressureSettings {
372            platform_default: crate::config::DEFAULT_MAX_IN_FLIGHT_ACTIVITIES,
373            fraction: crate::worker::OwnedShardFraction::own_all(),
374        }
375    }
376
377    /// A minimal `RuntimeConfig` for building an in-memory `ServerState` in unit
378    /// tests (mirrors `state.rs`'s test `runtime_config`).
379    fn runtime_config() -> RuntimeConfig {
380        use crate::config::{
381            AuthConfig, AuthoringConfig, DeployConfig, DevConfig, ListenConfig, MetricsConfig,
382            NamespaceConfig, NamespaceMode, OpsConsoleAssetSource, OpsConsoleConfig,
383            WebSocketConfig, WorkerConfig,
384        };
385        RuntimeConfig {
386            listen: ListenConfig {
387                grpc: SocketAddr::from(([127, 0, 0, 1], 50051)),
388                http: SocketAddr::from(([127, 0, 0, 1], 8080)),
389            },
390            tls: None,
391            auth: AuthConfig {
392                enabled: false,
393                jwks_url: None,
394                jwks_refresh_seconds: 300,
395            },
396            ops_console: OpsConsoleConfig {
397                source: OpsConsoleAssetSource::Embedded,
398            },
399            namespace: NamespaceConfig {
400                mode: NamespaceMode::SharedEngine,
401            },
402            worker: WorkerConfig {
403                heartbeat_window: Duration::from_secs(30),
404                ..WorkerConfig::default()
405            },
406            websocket: WebSocketConfig {
407                outbound_buffer_bound: 32,
408                event_broadcast_capacity: Some(64),
409                cluster_broadcast_capacity: Some(64),
410            },
411            workflow_packages: Vec::new(),
412            deploy: DeployConfig::default(),
413            authoring: AuthoringConfig::default(),
414            dev: DevConfig::default(),
415            outbox: OutboxConfig::default(),
416            observability: crate::config::ObservabilityConfig::with_flush_policy(64, 0),
417            mcp: crate::config::ResolvedMcpConfig::default(),
418            scheduler_threads: 1,
419            jit_threshold: None,
420            query_timeout: Some(Duration::from_secs(10)),
421            default_namespace: "default".to_owned(),
422            auto_create: crate::config::AutoCreate::Open,
423            max_in_flight_activities: crate::config::DEFAULT_MAX_IN_FLIGHT_ACTIVITIES,
424            drain_timeout: Duration::from_secs(30),
425            metrics: MetricsConfig { enabled: true },
426            owned_shards: Vec::new(),
427            cors_allowed_origins: Vec::new(),
428        }
429    }
430
431    /// An `OutboxConfig` with `enabled = true` and every required knob present, so
432    /// the only remaining gate is the store-backend / outbox-table availability.
433    fn enabled_outbox_config() -> OutboxConfig {
434        OutboxConfig {
435            enabled: true,
436            poll_interval_ms: Some(250),
437            batch_size: Some(64),
438            max_attempts: Some(5),
439            backoff_base_ms: Some(100),
440            backoff_multiplier: Some(2),
441            backoff_max_ms: Some(30_000),
442            reconcile_interval_ms: None,
443            reconcile_stale_after_ms: None,
444            transport: OutboxTransport::Grpc,
445            liminal_listen_address: None,
446        }
447    }
448
449    /// LSUB-4-2 / LSUB-4-6 (Memory-backend guard): commissioning the outbox
450    /// dispatcher against the in-memory backend (which has no outbox table, so
451    /// `outbox_store()` is `None`) is a configuration error, and the message names
452    /// haematite as the required durable backend.
453    #[tokio::test]
454    async fn outbox_enabled_on_memory_backend_is_a_config_error() {
455        let state = ServerState::build_with_store(InMemoryStore::default(), runtime_config())
456            .await
457            .expect("build in-memory state");
458        let (_tx, rx) = tokio::sync::watch::channel(false);
459        let error = maybe_spawn_outbox_dispatcher(
460            &state,
461            &enabled_outbox_config(),
462            false,
463            test_backpressure_settings(),
464            &rx,
465            "set outbox.liminal_listen_address in the test config",
466        )
467        .expect_err("outbox.enabled on the memory backend must be a config error");
468        assert!(
469            error.is_config(),
470            "memory-backend outbox error must be Config"
471        );
472        let message = error.to_string();
473        assert!(
474            message.contains("store.backend=haematite"),
475            "message must name the durable backend, got: {message}"
476        );
477    }
478
479    /// LSUB-4-1 (Fork-B fast path): with the outbox disabled (the default), the
480    /// gate is a no-op even on a memory backend — nothing is spawned and no error
481    /// is produced, so a default single-node boot is unchanged.
482    #[tokio::test]
483    async fn disabled_outbox_is_a_noop_on_any_backend() {
484        let state = ServerState::build_with_store(InMemoryStore::default(), runtime_config())
485            .await
486            .expect("build in-memory state");
487        let (_tx, rx) = tokio::sync::watch::channel(false);
488        maybe_spawn_outbox_dispatcher(
489            &state,
490            &OutboxConfig::default(),
491            false,
492            test_backpressure_settings(),
493            &rx,
494            "set outbox.liminal_listen_address in the test config",
495        )
496        .expect("disabled outbox gate must be an infallible no-op");
497    }
498
499    /// LSUB-4-4: the reconciler config resolves to `None` unless BOTH knobs are
500    /// set — the condition under which the clustered-boot WARN fires.
501    #[test]
502    fn reconciler_config_absent_unless_both_knobs_set() {
503        let mut config = enabled_outbox_config();
504        // Neither knob: absent.
505        assert!(
506            resolve_outbox_reconciler_config(&config)
507                .expect("resolve")
508                .is_none()
509        );
510        // Only interval: still absent (the silent-backstop-absent default).
511        config.reconcile_interval_ms = Some(1_000);
512        assert!(
513            resolve_outbox_reconciler_config(&config)
514                .expect("resolve")
515                .is_none()
516        );
517        // Both set: present.
518        config.reconcile_stale_after_ms = Some(60_000);
519        assert!(
520            resolve_outbox_reconciler_config(&config)
521                .expect("resolve")
522                .is_some()
523        );
524    }
525
526    /// LSUB-PROD (13-6): the liminal transport requires `liminal_listen_address`.
527    /// Commissioning the dispatcher with `transport = liminal` but no listen
528    /// address is a configuration error naming the missing knob, rather than a
529    /// panic or a silent fall-through to gRPC. Built over haematite (so
530    /// the outbox-store gate passes and the missing-address check is actually
531    /// reached). (Feature-gated: the liminal arm of `build_liminal_row_dispatch`
532    /// only exists with `liminal-transport` on; in a feature-off build the same
533    /// selection is the missing-feature error instead, covered by the type system
534    /// rather than this test.)
535    #[cfg(feature = "liminal-transport")]
536    #[tokio::test]
537    async fn liminal_transport_requires_listen_address() {
538        use crate::config::{
539            RuntimeSection, ServerConfig, StoreBackend, StoreConfig, WebSocketConfig,
540        };
541
542        let data_dir = std::env::temp_dir().join(format!(
543            "aion-lsub-prod-listen-guard-{}-{}",
544            std::process::id(),
545            std::time::SystemTime::now()
546                .duration_since(std::time::UNIX_EPOCH)
547                .map(|elapsed| elapsed.as_nanos())
548                .unwrap_or_default()
549        ));
550        let mut outbox = enabled_outbox_config();
551        outbox.transport = OutboxTransport::Liminal;
552        outbox.liminal_listen_address = None;
553        let config = ServerConfig {
554            store: StoreConfig {
555                backend: StoreBackend::Haematite,
556                data_dir: Some(data_dir.to_string_lossy().into_owned()),
557                // Required, no default: the haematite boot path refuses a config
558                // that does not rule on the node cache's byte ceiling.
559                node_cache_budget: Some(haematite::NodeCacheBudget::Unlimited),
560                ..StoreConfig::default()
561            },
562            runtime: RuntimeSection {
563                scheduler_threads: 1,
564                jit_threshold: None,
565                query_timeout_ms: Some(10_000),
566            },
567            websocket: WebSocketConfig {
568                outbound_buffer_bound: 32,
569                event_broadcast_capacity: Some(64),
570                cluster_broadcast_capacity: Some(64),
571            },
572            outbox: outbox.clone(),
573            // Required, no default: the transcript drain's flush policy.
574            observability: crate::config::ObservabilityConfig::with_flush_policy(64, 0),
575            ..ServerConfig::default()
576        };
577        let state = ServerState::build(config)
578            .await
579            .expect("build haematite state");
580        let (_tx, rx) = tokio::sync::watch::channel(false);
581
582        let error = maybe_spawn_outbox_dispatcher(
583            &state,
584            &outbox,
585            false,
586            test_backpressure_settings(),
587            &rx,
588            "add `liminal_listen_address = \"127.0.0.1:50061\"` to `[outbox]` in the test config",
589        )
590        .expect_err("liminal transport without a listen address must be a config error");
591        assert!(
592            error.is_config(),
593            "missing-listen-address error must be Config"
594        );
595        assert!(
596            error.to_string().contains("liminal_listen_address"),
597            "error must name the missing knob, got: {error}"
598        );
599        // #180 review MAJ-4: the refusal must carry the caller's threaded
600        // where-to-edit hint, so the production message names the resolved
601        // config FILE, not just the key.
602        assert!(
603            error.to_string().contains("in the test config"),
604            "error must carry the threaded config-location hint, got: {error}"
605        );
606    }
607}
608
609/// LSUB-PROD (13-6): production-boot cross-node round-trip over the REAL wiring.
610///
611/// This is the proof that the production boot now does the full round-trip the
612/// retired stub could not. It drives the EXACT production commissioning function
613/// `run_server` calls — [`maybe_spawn_outbox_dispatcher`] — over a real
614/// [`ServerState`] built with `outbox.enabled`, `transport = liminal`, and a
615/// `liminal_listen_address`. That function lifts the full push wiring
616/// (`build_liminal_row_dispatch`): it hosts the liminal worker listener, builds
617/// [`RegistryLiminalDispatch`](crate::worker::RegistryLiminalDispatch) over the
618/// SAME registry the gRPC path uses and the SAME
619/// [`ServerOutboxDeliveryCallback`](crate::worker::ServerOutboxDeliveryCallback)
620/// (over the live engine), and spawns the real [`OutboxDispatcher`].
621///
622/// A REAL remote [`LiminalActivityWorker`](aion_worker::LiminalActivityWorker)
623/// connects IN to the listener and self-registers in-band. A `collect_four`
624/// fan-out is started over the REAL HTTP transport, which stages four pending
625/// outbox rows; the production-wired dispatcher claims and pushes each to the
626/// worker, the worker executes it, and its completion re-enters aion through the
627/// production engine callback — `record_fan_out_completion` — driving the
628/// workflow to a recorded terminal. The proof asserts BOTH: the worker observably
629/// executed the activities, AND the terminals were recorded in history (four
630/// `ActivityCompleted` + one `WorkflowCompleted`), which the stub's
631/// publish-and-mark-done path never achieved.
632#[cfg(all(test, feature = "liminal-transport"))]
633mod lsub_prod_xnode_e2e {
634    #![allow(clippy::expect_used)]
635
636    use std::net::SocketAddr;
637    use std::path::PathBuf;
638    use std::sync::Arc;
639    use std::sync::atomic::{AtomicUsize, Ordering};
640    use std::time::{Duration, Instant};
641
642    use aion_core::Event;
643    use aion_package::{
644        ActionContract, BeamModule, BeamSet, CURRENT_FORMAT_VERSION, DeclaredActivity, Manifest,
645        ManifestVersion, PackageBuilder, PackageContract, WorkerContract,
646    };
647    use aion_worker::{ActivityRegistry, LiminalActivityWorker, WorkerConfig};
648    use axum::body;
649    use axum::http::{Request, StatusCode};
650    use serde_json::json;
651    use tower::ServiceExt;
652
653    use super::{BackpressureSettings, maybe_spawn_outbox_dispatcher};
654    use crate::ServerState;
655    use crate::api::http::http_router;
656    use crate::config::{
657        OutboxConfig, OutboxTransport, RuntimeSection, ServerConfig, StoreBackend, StoreConfig,
658        WebSocketConfig,
659    };
660
661    type TestError = Box<dyn std::error::Error + Send + Sync>;
662
663    /// The `collect_four` fixture passes each member the JSON string `"in"` as
664    /// activity input, so the worker handler decodes a [`String`], not a struct.
665    type FanInput = String;
666
667    const NAMESPACE: &str = "default";
668    const TASK_QUEUE: &str = "default";
669    const OUTBOX_MODULE: &str = "aion_outbox_fixture";
670    const OUTBOX_BEAM: &[u8] = include_bytes!("../tests/fixtures/aion_outbox_fixture.beam");
671    const OUTBOX_SOURCE: &[u8] = include_bytes!("../tests/fixtures/aion_outbox_fixture.erl");
672    const FAN_OUT: usize = 4;
673    const FAN_ACTIVITY_TYPES: [&str; FAN_OUT] = ["fan:0", "fan:1", "fan:2", "fan:3"];
674    const POLL_DEADLINE: Duration = Duration::from_secs(20);
675    /// The one fan-out member the reconnect pin holds. Any of the four would do —
676    /// they are dispatched independently and served by identical handlers.
677    const HELD_ACTIVITY_TYPE: &str = FAN_ACTIVITY_TYPES[0];
678
679    fn test_error(message: impl std::fmt::Display) -> TestError {
680        message.to_string().into()
681    }
682
683    /// Reserve a loopback port and return it: the liminal listener binds this exact
684    /// address (the production path binds the configured `liminal_listen_address`,
685    /// so the test must commit to a concrete port the worker can also dial).
686    fn reserve_loopback_port() -> Result<SocketAddr, TestError> {
687        let listener = std::net::TcpListener::bind("127.0.0.1:0").map_err(test_error)?;
688        let address = listener.local_addr().map_err(test_error)?;
689        drop(listener);
690        Ok(address)
691    }
692
693    /// The fixture's queue-scoped `.v4` contract: the four `fan:N` activities
694    /// `collect_four` schedules, declared on the queue its worker actually polls.
695    ///
696    /// Why the archive cannot just carry the manifest-derived record: by design
697    /// `PackageContract::from_manifest` "never invents a queue", so a manifest's
698    /// bare activity names land in `unscoped_activities` — and this server boots
699    /// queue-routed, where an unscoped catalog is a terminal
700    /// `NO_QUEUE_DECLARATION` at start admission
701    /// (`aion::lifecycle::start_admission`). That refusal is EARNED: an unserved
702    /// queue would otherwise wait silently forever. So the derived record is
703    /// amended rather than bypassed — the same four names move out of
704    /// `unscoped_activities` and onto the queue that serves them — and the
705    /// package still loads through the production boot path with the `.v4`
706    /// identity `PackageBuilder` stamps over this exact contract.
707    ///
708    /// The action schemas come from the SAME generator the worker's typed
709    /// registry uses, for the SAME Rust types: `collect_four` passes each member
710    /// the JSON string `"in"` and the handler returns a [`String`]. Deriving both
711    /// sides from `activity_descriptor::<FanInput, String>` means the package's
712    /// declaration and the worker's advertisement cannot drift apart, so
713    /// registration admission (`WORKER_CONTRACT_MISMATCH`) compares two schemas
714    /// with one source.
715    fn fixture_contract(manifest: &Manifest) -> Result<PackageContract, TestError> {
716        let mut actions = Vec::with_capacity(FAN_ACTIVITY_TYPES.len());
717        for activity_type in FAN_ACTIVITY_TYPES {
718            let descriptor = aion_worker::activity_descriptor::<FanInput, String>(activity_type)
719                .map_err(test_error)?;
720            actions.push(ActionContract {
721                name: descriptor.name,
722                input_schema: descriptor.input_schema,
723                output_schema: descriptor.output_schema,
724                node: None,
725                timeout: None,
726                retry: None,
727                advisory: false,
728                // A typed `String -> String` handler serves these, not an agent
729                // harness — the fan fixture's shape merely coincides with an
730                // agent seam's, and marking it would route it somewhere no
731                // handler is.
732                agent: false,
733                // A connected worker serves this fixture's queue, so the
734                // declaration carries no body of its own.
735                body: None,
736            });
737        }
738        let mut contract = PackageContract::from_manifest(manifest);
739        contract.workers = vec![WorkerContract {
740            task_queue: TASK_QUEUE.to_owned(),
741            actions,
742        }];
743        contract.unscoped_activities.clear();
744        Ok(contract)
745    }
746
747    /// Build the `collect_four` package on disk so the production state-build path
748    /// loads it exactly as it loads operator-supplied `workflow_packages`.
749    fn write_package_archive(dir: &std::path::Path) -> Result<PathBuf, TestError> {
750        let beams =
751            BeamSet::new(vec![BeamModule::new(OUTBOX_MODULE, OUTBOX_BEAM)]).map_err(test_error)?;
752        let manifest = Manifest {
753            entry_module: OUTBOX_MODULE.to_owned(),
754            entry_function: "collect_four".to_owned(),
755            input_schema: json!({ "type": "object" }),
756            output_schema: json!({}),
757            timeout: Some(Duration::from_secs(30)),
758            // The four ordinals `collect_four` actually fans out. This manifest
759            // used to name one invented activity, `fixture_activity`, that the
760            // fixture never schedules and no worker ever served.
761            activities: FAN_ACTIVITY_TYPES
762                .iter()
763                .map(|activity_type| DeclaredActivity {
764                    activity_type: (*activity_type).to_owned(),
765                })
766                .collect(),
767            version: ManifestVersion::new("stamped-by-builder"),
768            format_version: CURRENT_FORMAT_VERSION,
769            additional_workflows: Vec::new(),
770        };
771        let contract = fixture_contract(&manifest)?;
772        let archive =
773            PackageBuilder::with_source(manifest, beams, [(OUTBOX_MODULE, OUTBOX_SOURCE.to_vec())])
774                .with_contract(contract)
775                .write_to_bytes()
776                .map_err(test_error)?;
777        let path = dir.join("collect_four.aion");
778        std::fs::write(&path, archive).map_err(test_error)?;
779        Ok(path)
780    }
781
782    /// A production-shaped `ServerConfig`: the haematite backend (so the boot store
783    /// path shares the leaf as the dispatcher's outbox store, exactly as
784    /// `ServerState::build` does in production), `outbox.enabled`,
785    /// `transport = liminal`, the reserved `liminal_listen_address`, and the
786    /// `collect_four` package. Built through `ServerState::build` (not
787    /// `build_with_store`), so this is the real boot store seam, not a test stand-in.
788    fn server_config(
789        data_dir: &std::path::Path,
790        package_path: PathBuf,
791        listen_address: SocketAddr,
792    ) -> ServerConfig {
793        ServerConfig {
794            store: StoreConfig {
795                backend: StoreBackend::Haematite,
796                data_dir: Some(data_dir.to_string_lossy().into_owned()),
797                // Required, no default: the haematite boot path refuses a config
798                // that does not rule on the node cache's byte ceiling.
799                node_cache_budget: Some(haematite::NodeCacheBudget::Unlimited),
800                ..StoreConfig::default()
801            },
802            runtime: RuntimeSection {
803                scheduler_threads: 1,
804                jit_threshold: None,
805                query_timeout_ms: Some(10_000),
806            },
807            websocket: WebSocketConfig {
808                outbound_buffer_bound: 32,
809                event_broadcast_capacity: Some(64),
810                cluster_broadcast_capacity: Some(64),
811            },
812            workflow_packages: vec![package_path],
813            outbox: OutboxConfig {
814                enabled: true,
815                poll_interval_ms: Some(20),
816                batch_size: Some(16),
817                max_attempts: Some(5),
818                backoff_base_ms: Some(50),
819                backoff_multiplier: Some(2),
820                backoff_max_ms: Some(1_000),
821                reconcile_interval_ms: None,
822                reconcile_stale_after_ms: None,
823                transport: OutboxTransport::Liminal,
824                liminal_listen_address: Some(listen_address.to_string()),
825            },
826            // Required, no default: the transcript drain's flush policy.
827            observability: crate::config::ObservabilityConfig::with_flush_policy(64, 0),
828            ..ServerConfig::default()
829        }
830    }
831
832    /// The remote worker self-describes for the fixture's pool `(default, default)`
833    /// and registers a handler for every `fan:N` activity type, counting executions
834    /// so the test proves it genuinely ran the pushed dispatches.
835    fn worker_config() -> Result<WorkerConfig, TestError> {
836        WorkerConfig::builder()
837            .endpoint("unused-direct-address")
838            .namespace(NAMESPACE)
839            .task_queue(TASK_QUEUE)
840            .identity("lsub-prod-worker")
841            .max_concurrency(4)
842            .reconnect_initial_backoff(Duration::from_millis(5))
843            .reconnect_max_backoff(Duration::from_millis(20))
844            .reconnect_max_attempts(3)
845            .build()
846            .map_err(test_error)
847    }
848
849    fn worker_registry(executions: &Arc<AtomicUsize>) -> Result<Arc<ActivityRegistry>, TestError> {
850        let mut registry = ActivityRegistry::new();
851        for activity_type in FAN_ACTIVITY_TYPES {
852            let executions = Arc::clone(executions);
853            // `register_activity_with_contract`, not `register_activity`: the
854            // bare form registers a handler with NO descriptor, so the worker
855            // advertises four names and zero typed contracts, and admission —
856            // which compares CONTRACTS — refuses the registration outright
857            // (`WORKER_CONTRACT_MISMATCH`). Deriving the advertisement from
858            // `<FanInput, String>` is what makes it the same source the
859            // package's `fixture_contract` declares from, so the two sides
860            // cannot drift.
861            registry = registry
862                .register_activity_with_contract(
863                    activity_type,
864                    move |_input: FanInput, _context| {
865                        let executions = Arc::clone(&executions);
866                        Box::pin(async move {
867                            executions.fetch_add(1, Ordering::SeqCst);
868                            Ok(activity_type.to_owned())
869                        })
870                    },
871                )
872                .map_err(test_error)?;
873        }
874        Ok(Arc::new(registry))
875    }
876
877    /// Spawns the remote worker on its own OS thread with a current-thread runtime
878    /// (the push receive is blocking), connecting IN to the production listener.
879    struct WorkerThread {
880        stop: Arc<std::sync::atomic::AtomicBool>,
881        handle: Option<std::thread::JoinHandle<()>>,
882    }
883
884    impl WorkerThread {
885        fn spawn(address: String, config: WorkerConfig, registry: Arc<ActivityRegistry>) -> Self {
886            let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
887            let thread_stop = Arc::clone(&stop);
888            let handle = std::thread::spawn(move || {
889                let runtime = match tokio::runtime::Builder::new_current_thread()
890                    .enable_all()
891                    .build()
892                {
893                    Ok(runtime) => runtime,
894                    Err(error) => {
895                        eprintln!("worker runtime build failed: {error}");
896                        return;
897                    }
898                };
899                runtime.block_on(async move {
900                    let worker = match LiminalActivityWorker::connect(&address, &config, registry) {
901                        Ok(worker) => worker,
902                        Err(error) => {
903                            eprintln!("worker connect failed: {error}");
904                            return;
905                        }
906                    };
907                    if let Err(error) = worker
908                        .serve_until(|| thread_stop.load(Ordering::SeqCst))
909                        .await
910                    {
911                        eprintln!("worker serve loop ended with error: {error}");
912                    }
913                });
914            });
915            Self {
916                stop,
917                handle: Some(handle),
918            }
919        }
920
921        /// Spawn the worker through [`aion_worker::serve_with_redial`] — the entry
922        /// point every REAL worker uses — so a broken link is survivable.
923        ///
924        /// [`Self::spawn`] uses `LiminalActivityWorker::serve_until`, which returns
925        /// the first transport error by design: a single-connection serve has no
926        /// survivor to migrate to. That is the right shape for a test whose link
927        /// never breaks, and the wrong instrument entirely for one whose link is
928        /// broken on purpose — a worker that dies at the break can only ever show
929        /// that outstanding work fails, whoever is at fault.
930        ///
931        /// The redial driver is SYNCHRONOUS and builds its own current-thread
932        /// runtime, so it runs on the bare thread rather than inside one.
933        fn spawn_redialing(
934            address: String,
935            config: WorkerConfig,
936            registry: Arc<ActivityRegistry>,
937            timing: aion_worker::RedialTiming,
938        ) -> Self {
939            let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
940            let thread_stop = Arc::clone(&stop);
941            let handle = std::thread::spawn(move || {
942                if let Err(error) = aion_worker::serve_with_redial(
943                    vec![address],
944                    &config,
945                    &registry,
946                    timing,
947                    &thread_stop,
948                    None,
949                    || {},
950                ) {
951                    eprintln!("redialing worker ended with error: {error}");
952                }
953            });
954            Self {
955                stop,
956                handle: Some(handle),
957            }
958        }
959
960        fn stop(mut self) {
961            self.stop.store(true, Ordering::SeqCst);
962            if let Some(handle) = self.handle.take() {
963                handle.join().ok();
964            }
965        }
966    }
967
968    fn count_completed(history: &[Event]) -> usize {
969        history
970            .iter()
971            .filter(|event| matches!(event, Event::ActivityCompleted { .. }))
972            .count()
973    }
974
975    fn count_workflow_completed(history: &[Event]) -> usize {
976        history
977            .iter()
978            .filter(|event| matches!(event, Event::WorkflowCompleted { .. }))
979            .count()
980    }
981
982    async fn wait_for_history<F>(
983        store: &dyn aion_store::ReadableEventStore,
984        workflow_id: &aion_core::WorkflowId,
985        description: &str,
986        predicate: F,
987    ) -> Result<Vec<Event>, TestError>
988    where
989        F: Fn(&[Event]) -> bool,
990    {
991        let deadline = Instant::now() + POLL_DEADLINE;
992        loop {
993            let history = store.read_history(workflow_id).await.map_err(test_error)?;
994            if predicate(&history) {
995                return Ok(history);
996            }
997            if Instant::now() > deadline {
998                return Err(test_error(format!(
999                    "timed out waiting for {description}: {history:#?}"
1000                )));
1001            }
1002            tokio::time::sleep(Duration::from_millis(25)).await;
1003        }
1004    }
1005
1006    /// Start the loaded `collect_four` workflow over the REAL HTTP transport.
1007    async fn start_over_http(router: &axum::Router) -> Result<aion_core::WorkflowId, TestError> {
1008        let build_request = || -> Result<Request<body::Body>, TestError> {
1009            Request::builder()
1010                .uri("/workflows/start")
1011                .method("POST")
1012                .header("content-type", "application/json")
1013                .header("x-aion-subject", "ci")
1014                .header("x-aion-namespaces", NAMESPACE)
1015                .body(body::Body::from(
1016                    serde_json::to_vec(&json!({
1017                        "namespace": NAMESPACE,
1018                        "workflow_type": OUTBOX_MODULE,
1019                        "input": { "fixture": "input" },
1020                    }))
1021                    .map_err(test_error)?,
1022                ))
1023                .map_err(test_error)
1024        };
1025        let response = router
1026            .clone()
1027            .oneshot(build_request()?)
1028            .await
1029            .map_err(test_error)?;
1030        let status = response.status();
1031        let bytes = body::to_bytes(response.into_body(), usize::MAX)
1032            .await
1033            .map_err(test_error)?
1034            .to_vec();
1035        if status != StatusCode::OK {
1036            return Err(test_error(format!(
1037                "workflow start over HTTP must succeed, got {status}: {}",
1038                String::from_utf8_lossy(&bytes)
1039            )));
1040        }
1041        let body: serde_json::Value = serde_json::from_slice(&bytes).map_err(test_error)?;
1042        // The HTTP wire contract (`clean_dtos::StartWorkflowResponse`) serializes
1043        // `workflow_id` as a plain UUID string, not a nested `{ uuid }` object.
1044        let workflow_id = body["workflow_id"]
1045            .as_str()
1046            .ok_or_else(|| test_error("start response missing workflow id"))?
1047            .parse::<uuid::Uuid>()
1048            .map_err(test_error)?;
1049        Ok(aion_core::WorkflowId::new(workflow_id))
1050    }
1051
1052    /// How long a freshly connected worker needs before the dispatch path may
1053    /// select it, DERIVED from the same two facts the server derives it from.
1054    ///
1055    /// A worker is dispatch-ineligible until it serves an OPENING PROBATION:
1056    /// [`Reachability::is_proved`] requires `DISPATCH_PROBATION_PINGS` consecutive
1057    /// answered liveness pings, at the probe's cadence of
1058    /// [`sweep_interval`](crate::worker::sweep_interval)`(heartbeat_window)`. The
1059    /// constant's own documentation states the cost — *"at the probe's cadence a
1060    /// fresh worker is undispatchable for K cadences while its first dispatches
1061    /// park"* — so this is designed behaviour a test must wait out, not a delay to
1062    /// be shortened.
1063    ///
1064    /// One extra cadence is allowed because the first round lands at an arbitrary
1065    /// offset inside the first interval: the worker connects between rounds, so it
1066    /// can miss up to one whole cadence before its first answer is even counted.
1067    ///
1068    /// # Why this is not a raised timeout
1069    ///
1070    /// It was 5 seconds, fixed, and that is how this test became one of four
1071    /// documented carriers of a load-sensitive flake
1072    /// (`gate-logs/lock-race-attribution/VERDICT.md`). The mechanism, measured:
1073    /// `dispatch_ineligible` starts EMPTY and `select_worker` filters only against
1074    /// what the probe has published, so a run in which **no probe round lands
1075    /// inside the window** selects the worker immediately and passes, while a run
1076    /// in which one does correctly withholds it for ~2 cadences and fails. On the
1077    /// default 30s window that is 7.5s per cadence against a 5s wait.
1078    ///
1079    /// 🔴 The passing runs were the WRONG ones. They dispatched to a worker that
1080    /// had not served its probation — a path production does not permit, because
1081    /// production parks those dispatches. Waiting for genuine eligibility makes
1082    /// this test MORE production-shaped, not more lenient, and that is the reason
1083    /// to do it. Raising a bound until a flake stops is how a liveness bug gets
1084    /// buried; deriving the bound from the mechanism that sets it is not the same
1085    /// act, and the register warns about the first for good reason.
1086    fn eligibility_patience(config: &ServerConfig) -> Duration {
1087        let cadence = crate::worker::sweep_interval(config.worker.heartbeat_window);
1088        cadence * (crate::worker::heartbeat::DISPATCH_PROBATION_PINGS + 1)
1089    }
1090
1091    /// Wait until the worker's in-band registration lands in the SAME registry the
1092    /// dispatch path selects from, with every fan-out activity type eligible.
1093    ///
1094    /// On the deadline this reports the state that DISCRIMINATES the worlds a
1095    /// missed registration can be in, because the bare sentence it replaced —
1096    /// "worker never registered in-band for the pool" — is equally true in at
1097    /// least three of them, and they want different fixes:
1098    ///
1099    /// 1. the liminal listener never bound, so nothing could dial in;
1100    /// 2. the worker never connected, or died dialling;
1101    /// 3. it connected and registration was merely slow;
1102    /// 4. it connected, registered correctly, and the SELECTOR refused it anyway —
1103    ///    because the liveness probe published it as unreachable, or because it is
1104    ///    not indexed for the activity type it advertises.
1105    ///
1106    /// The fourth was not in the first version of this report, and it is the world
1107    /// a real occurrence turned out to be in: the listener was bound, a worker was
1108    /// registered under the right namespace and queue advertising all four activity
1109    /// types, and every `select_worker` still returned nothing. A report that
1110    /// cannot separate "not registered" from "registered and refused" names the
1111    /// wrong half of the system.
1112    ///
1113    /// That is not a hypothetical distinction here. This module's e2e is one of
1114    /// four documented carriers of a load-sensitive flake
1115    /// (`gate-logs/lock-race-attribution/VERDICT.md`), it fails through THIS wait,
1116    /// and the reason the carrier has never been explained is that the failure
1117    /// named the fact and withheld the cause.
1118    async fn wait_for_registration(
1119        registry: &crate::worker::ConnectedWorkerRegistry,
1120        heartbeat: &crate::worker::HeartbeatTracker,
1121        listen_address: SocketAddr,
1122        patience: Duration,
1123    ) -> Result<(), TestError> {
1124        let deadline = Instant::now() + patience;
1125        loop {
1126            let now = Instant::now();
1127            let mut ready = true;
1128            for activity_type in FAN_ACTIVITY_TYPES {
1129                let Some(worker) = registry
1130                    .select_worker(NAMESPACE, TASK_QUEUE, activity_type, None)
1131                    .map_err(test_error)?
1132                else {
1133                    ready = false;
1134                    break;
1135                };
1136                if !heartbeat
1137                    .is_dispatch_reachable(worker.id(), now)
1138                    .map_err(test_error)?
1139                {
1140                    ready = false;
1141                    break;
1142                }
1143            }
1144            if ready {
1145                return Ok(());
1146            }
1147            if Instant::now() > deadline {
1148                return Err(test_error(format!(
1149                    "worker never registered in-band for the pool within {patience:?}{}",
1150                    registration_diagnosis(registry, listen_address)
1151                )));
1152            }
1153            tokio::time::sleep(Duration::from_millis(10)).await;
1154        }
1155    }
1156
1157    /// The discriminator behind [`wait_for_registration`]'s failure: enough of the
1158    /// world to tell those three apart, gathered at the moment of the failure.
1159    fn registration_diagnosis(
1160        registry: &crate::worker::ConnectedWorkerRegistry,
1161        listen_address: SocketAddr,
1162    ) -> String {
1163        let mut lines = vec![String::from("--- registration diagnosis ---")];
1164        // World 1, PROBED rather than assumed. The port was reserved by binding a
1165        // listener and dropping it, so losing the race for it is a real
1166        // possibility rather than a theoretical one, and it is indistinguishable
1167        // from every other failure unless something asks.
1168        lines.push(
1169            match std::net::TcpStream::connect_timeout(&listen_address, Duration::from_millis(500))
1170            {
1171                Ok(stream) => {
1172                    drop(stream);
1173                    format!("listener {listen_address}: ACCEPTS — the port is bound and dialable")
1174                }
1175                Err(error) => format!(
1176                    "listener {listen_address}: NOT connectable ({error}) — nothing could have \
1177                     registered, so this is not a timing problem"
1178                ),
1179            },
1180        );
1181        // Worlds 2 and 3: did any worker arrive at all, and if one did, what does
1182        // the registry hold for it against what the dispatch path asks of it? A
1183        // worker present under a different pool or advertising different activity
1184        // types is a contract mismatch wearing a timeout's clothes.
1185        match registry.all_workers() {
1186            Err(error) => lines.push(format!("registry: UNREADABLE ({error})")),
1187            Ok(workers) if workers.is_empty() => lines.push(String::from(
1188                "registry: EMPTY — no worker of any pool registered, so no connection ever \
1189                 completed an in-band registration",
1190            )),
1191            Ok(workers) => {
1192                lines.push(format!("registry: {} worker(s) registered", workers.len()));
1193                for worker in &workers {
1194                    lines.push(format!(
1195                        "  id={:?} namespaces={:?} task_queue={:?} node={:?} types={:?}",
1196                        worker.id(),
1197                        worker.namespaces(),
1198                        worker.task_queue(),
1199                        worker.node(),
1200                        worker.activity_types()
1201                    ));
1202                }
1203            }
1204        }
1205        lines.push(format!(
1206            "asked of it: namespace={NAMESPACE:?} task_queue={TASK_QUEUE:?}"
1207        ));
1208        // The liveness probe's reachability verdict. `select_worker` skips every
1209        // worker in this set, so a registered, correctly-advertised worker that is
1210        // listed here is refused for a reason nothing else in this report shows.
1211        lines.push(match registry.dispatch_ineligible() {
1212            Ok(ineligible) if ineligible.is_empty() => {
1213                String::from("dispatch-ineligible: none — reachability is not refusing anyone")
1214            }
1215            Ok(ineligible) => format!(
1216                "dispatch-ineligible: {ineligible:?} — the liveness probe has published these \
1217                 as unreachable and select_worker skips them"
1218            ),
1219            Err(error) => format!("dispatch-ineligible: UNREADABLE ({error})"),
1220        });
1221        // Which of the four the selector could not satisfy, and — the part that
1222        // discriminates — the pool census beside each refusal.
1223        //
1224        // `select_worker` filters on THREE things: the activity index for
1225        // `(namespace, task_queue) + activity_type`, the node pin, and the
1226        // dispatch-ineligible set. The census counts the first two and does NOT
1227        // apply the third, so the pair of answers separates the remaining worlds
1228        // that a registry dump alone leaves fused:
1229        //
1230        // - census serves it, selector refuses  ⇒ REACHABILITY, not registration;
1231        // - census serves 0 for the activity    ⇒ the worker is in the pool but not
1232        //   indexed for this activity type;
1233        // - census serves 0 for the pool        ⇒ it is not in this pool at all,
1234        //   whatever `all_workers` shows.
1235        //
1236        // Written after the bare registry dump above failed to close a real case:
1237        // it proved the listener was bound and a worker with all four activity
1238        // types was registered, and still could not say why every selection
1239        // returned nothing.
1240        for activity_type in FAN_ACTIVITY_TYPES {
1241            let outcome = match registry.select_worker(NAMESPACE, TASK_QUEUE, activity_type, None) {
1242                Ok(Some(handle)) => format!("worker {:?}", handle.id()),
1243                Ok(None) => String::from("NO worker"),
1244                Err(error) => format!("error: {error}"),
1245            };
1246            let census = match registry.pool_census(NAMESPACE, TASK_QUEUE, activity_type, None) {
1247                Ok(census) => format!(
1248                    "in_pool={} serving_activity={} compatible={} last_compatible_age={:?}",
1249                    census.workers_in_pool,
1250                    census.workers_serving_activity,
1251                    census.compatible_workers,
1252                    census.last_compatible_poller_age
1253                ),
1254                Err(error) => format!("census UNREADABLE ({error})"),
1255            };
1256            lines.push(format!(
1257                "select_worker({activity_type}) -> {outcome}  [census: {census}]"
1258            ));
1259        }
1260        format!("\n  {}", lines.join("\n  "))
1261    }
1262
1263    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1264    async fn production_boot_dispatches_executes_and_records_over_liminal() -> Result<(), TestError>
1265    {
1266        let dir = crate::test_support::private_tempdir().map_err(test_error)?;
1267        let db_path = dir.path().join("aion.db");
1268        let package_path = write_package_archive(dir.path())?;
1269        // The production path binds the CONFIGURED listen address, so commit to a
1270        // concrete reserved loopback port the worker can also dial.
1271        let listen_address = reserve_loopback_port()?;
1272
1273        // (A) Build a real ServerState through the production boot path
1274        // (ServerState::build over a haematite ServerConfig): outbox enabled,
1275        // transport = liminal, the listen address set, collect_four loaded. This
1276        // shares the haematite leaf as the dispatcher's outbox store (the real boot
1277        // store seam) and installs the production ServerOutboxDeliveryCallback over
1278        // the live engine (gated on outbox.enabled).
1279        let config = server_config(&db_path, package_path, listen_address);
1280        let outbox_config = config.outbox.clone();
1281        // Captured before `build` consumes the config: the wait below is derived
1282        // from the very window this server is about to run its liveness probe on.
1283        let patience = eligibility_patience(&config);
1284        let state = ServerState::build(config).await.map_err(test_error)?;
1285
1286        // (B) Drive the EXACT production commissioning function run_server calls:
1287        // it hosts the liminal listener, builds RegistryLiminalDispatch over the
1288        // shared registry + engine callback, and spawns the real OutboxDispatcher.
1289        // Hold the returned listener guard for the test's lifetime, exactly as
1290        // run_server holds it.
1291        let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
1292        // Own-all, generous-default backpressure (single-node e2e): fraction 1 and
1293        // the platform default, so the ceiling never engages — the claim behaves
1294        // exactly as before, proving the production path is byte-identical on default.
1295        let backpressure_settings = BackpressureSettings {
1296            platform_default: crate::config::DEFAULT_MAX_IN_FLIGHT_ACTIVITIES,
1297            fraction: crate::worker::OwnedShardFraction::own_all(),
1298        };
1299        let listener_guard = maybe_spawn_outbox_dispatcher(
1300            &state,
1301            &outbox_config,
1302            false,
1303            backpressure_settings,
1304            &shutdown_rx,
1305            "set outbox.liminal_listen_address in the test config",
1306        )
1307        .map_err(test_error)?;
1308
1309        // (C) A REAL remote worker connects IN to the production listener and
1310        // self-registers in-band for the fixture's pool.
1311        let executions = Arc::new(AtomicUsize::new(0));
1312        let worker = WorkerThread::spawn(
1313            listen_address.to_string(),
1314            worker_config()?,
1315            worker_registry(&executions)?,
1316        );
1317
1318        // Wait until the in-band registration landed in the SAME registry the
1319        // dispatch path selects from (every fan-out activity type is eligible).
1320        let registry = state.worker_registry().clone();
1321        if let Err(error) = wait_for_registration(
1322            &registry,
1323            state.heartbeat_tracker(),
1324            listen_address,
1325            patience,
1326        )
1327        .await
1328        {
1329            worker.stop();
1330            return Err(error);
1331        }
1332
1333        // (D) Start collect_four over the REAL HTTP transport: the engine stages
1334        // four pending outbox rows; the production-wired dispatcher claims and
1335        // pushes each to the worker.
1336        let router = http_router(state.clone()).map_err(test_error)?;
1337        let workflow_id = start_over_http(&router).await?;
1338
1339        // (E) THE PROOF: the worker executed all four activities AND every terminal
1340        // was recorded through the production engine callback (record_fan_out_completion)
1341        // — four ActivityCompleted + one WorkflowCompleted in durable history. This
1342        // is the full round-trip the retired stub never achieved.
1343        let reader = state.engine().map_err(test_error)?.store();
1344        let settled =
1345            wait_for_history(reader.as_ref(), &workflow_id, "fan-out settled", |events| {
1346                count_completed(events) == FAN_OUT && count_workflow_completed(events) == 1
1347            })
1348            .await?;
1349        assert_eq!(
1350            count_completed(&settled),
1351            FAN_OUT,
1352            "every fan-out member must record a terminal through the production callback"
1353        );
1354        assert_eq!(
1355            count_workflow_completed(&settled),
1356            1,
1357            "the workflow must complete exactly once"
1358        );
1359        assert_eq!(
1360            executions.load(Ordering::SeqCst),
1361            FAN_OUT,
1362            "the remote worker must have executed every pushed dispatch exactly once"
1363        );
1364
1365        // Teardown: stop the dispatcher + worker, drop the listener guard (its Drop
1366        // stops the accept worker), shut the engine down so durable appends finish.
1367        shutdown_tx.send(true).ok();
1368        worker.stop();
1369        drop(listener_guard);
1370        state.shutdown().map_err(test_error)?;
1371        Ok(())
1372    }
1373
1374    /// One dispatch as the WORKER saw it: the identity the server sent it under,
1375    /// and when it arrived.
1376    #[derive(Clone, Debug)]
1377    struct SeenDispatch {
1378        activity_type: String,
1379        activity_id: String,
1380        attempt: u32,
1381        at: Instant,
1382    }
1383
1384    /// A loopback TCP relay the test can BREAK, sitting between the worker and the
1385    /// production liminal listener.
1386    ///
1387    /// The worker dials this instead of the listener, so the test owns a socket it
1388    /// can shut from the outside. That is the only way to make a REAL
1389    /// [`LiminalActivityWorker`] lose its connection mid-flight without reaching
1390    /// inside either the worker or the server — and a link broken from the inside
1391    /// would be a different experiment, because the code under test would be the
1392    /// code doing the breaking.
1393    ///
1394    /// # Why this is not the relay in `tests/dead_man_switch_e2e.rs`
1395    ///
1396    /// That file has `WedgeableRelay`, which can both wedge and sever, and this is
1397    /// deliberately not it. The two cannot be one, for a structural reason rather
1398    /// than a matter of taste: an integration test links this crate as an ordinary
1399    /// dependency, so it can see neither `#[cfg(test)] pub(crate) mod test_support`
1400    /// nor the private `maybe_spawn_outbox_dispatcher` this harness is built on,
1401    /// and `src/` cannot see `tests/`. Sharing one instrument would mean exporting
1402    /// a public, feature-gated test surface from a production crate.
1403    ///
1404    /// So the split is stated rather than hidden, and this half is a strict subset:
1405    /// it only severs. Wedging — which leaves both sockets open and merely discards
1406    /// bytes, so writes keep succeeding into the kernel buffer — is a DIFFERENT
1407    /// instrument answering a different question. #69 is about a broken link, not
1408    /// a silent one.
1409    struct SeverableRelay {
1410        address: SocketAddr,
1411        /// Every relayed socket, held so [`Self::sever`] can break them.
1412        sockets: Arc<std::sync::Mutex<Vec<std::net::TcpStream>>>,
1413        stop: Arc<std::sync::atomic::AtomicBool>,
1414        handle: Option<std::thread::JoinHandle<()>>,
1415    }
1416
1417    impl SeverableRelay {
1418        /// Bind a loopback port and relay every accepted connection to `upstream`.
1419        fn spawn(upstream: SocketAddr) -> Result<Self, TestError> {
1420            let listener = std::net::TcpListener::bind("127.0.0.1:0").map_err(test_error)?;
1421            let address = listener.local_addr().map_err(test_error)?;
1422            // Non-blocking accept so the relay can be shut down deterministically
1423            // rather than by parking a thread in `accept` until something happens
1424            // to connect. Accepted sockets are put back into blocking mode
1425            // explicitly: on this platform they would otherwise inherit the flag
1426            // and every pump would spin on `WouldBlock`.
1427            listener.set_nonblocking(true).map_err(test_error)?;
1428            let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
1429            let sockets: Arc<std::sync::Mutex<Vec<std::net::TcpStream>>> =
1430                Arc::new(std::sync::Mutex::new(Vec::new()));
1431            let accept_stop = Arc::clone(&stop);
1432            let accept_sockets = Arc::clone(&sockets);
1433            let handle = std::thread::spawn(move || {
1434                while !accept_stop.load(Ordering::SeqCst) {
1435                    match listener.accept() {
1436                        Ok((downstream, _)) => {
1437                            if let Err(error) =
1438                                Self::relay_one(&downstream, upstream, &accept_sockets)
1439                            {
1440                                // The worker redials, so a connection this relay
1441                                // fails to carry surfaces as a slower recovery
1442                                // rather than as a wrong answer — but silence here
1443                                // would make that indistinguishable from the
1444                                // server never pushing, which is exactly the
1445                                // confusion this pin exists to resolve.
1446                                eprintln!("relay could not carry a connection: {error}");
1447                            }
1448                        }
1449                        Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
1450                            std::thread::sleep(Duration::from_millis(2));
1451                        }
1452                        Err(error) => {
1453                            eprintln!("relay accept failed: {error}");
1454                            return;
1455                        }
1456                    }
1457                }
1458            });
1459            Ok(Self {
1460                address,
1461                sockets,
1462                stop,
1463                handle: Some(handle),
1464            })
1465        }
1466
1467        /// Dial upstream for one accepted connection and pump both directions.
1468        fn relay_one(
1469            downstream: &std::net::TcpStream,
1470            upstream: SocketAddr,
1471            sockets: &Arc<std::sync::Mutex<Vec<std::net::TcpStream>>>,
1472        ) -> Result<(), TestError> {
1473            downstream.set_nonblocking(false).map_err(test_error)?;
1474            let up = std::net::TcpStream::connect(upstream).map_err(test_error)?;
1475            let down_read = downstream.try_clone().map_err(test_error)?;
1476            let down_write = downstream.try_clone().map_err(test_error)?;
1477            let up_read = up.try_clone().map_err(test_error)?;
1478            let up_write = up.try_clone().map_err(test_error)?;
1479            let held = downstream.try_clone().map_err(test_error)?;
1480            let mut parked = sockets
1481                .lock()
1482                .map_err(|_| test_error("relay socket register poisoned"))?;
1483            parked.push(held);
1484            parked.push(up);
1485            drop(parked);
1486            for (from, to) in [(down_read, up_write), (up_read, down_write)] {
1487                std::thread::spawn(move || Self::pump(from, to));
1488            }
1489            Ok(())
1490        }
1491
1492        /// Copy one direction until the connection ends.
1493        ///
1494        /// A read or write error here IS the severed link in the expected case, and
1495        /// in every case it means the peer this pump exists to serve is gone: there
1496        /// is no party left to propagate to, so ending the pump is the handling,
1497        /// not an omission of it.
1498        fn pump(mut from: std::net::TcpStream, mut to: std::net::TcpStream) {
1499            use std::io::{Read, Write};
1500            let mut buffer = [0_u8; 8192];
1501            loop {
1502                match from.read(&mut buffer) {
1503                    Ok(0) | Err(_) => return,
1504                    Ok(read) => {
1505                        if to.write_all(&buffer[..read]).is_err() {
1506                            return;
1507                        }
1508                    }
1509                }
1510            }
1511        }
1512
1513        const fn address(&self) -> SocketAddr {
1514            self.address
1515        }
1516
1517        /// BREAK every relayed socket, and report how many were broken.
1518        ///
1519        /// The count is returned, and asserted non-zero by the caller, so that a
1520        /// sever which severed nothing can never masquerade as a measurement — the
1521        /// pin would otherwise pass by never having run its own experiment.
1522        fn sever(&self) -> Result<usize, TestError> {
1523            let mut parked = self
1524                .sockets
1525                .lock()
1526                .map_err(|_| test_error("relay socket register poisoned"))?;
1527            let mut severed = 0;
1528            for socket in parked.iter() {
1529                if socket.shutdown(std::net::Shutdown::Both).is_ok() {
1530                    severed += 1;
1531                }
1532            }
1533            parked.clear();
1534            Ok(severed)
1535        }
1536
1537        fn shutdown(mut self) {
1538            self.stop.store(true, Ordering::SeqCst);
1539            if let Some(handle) = self.handle.take() {
1540                handle.join().ok();
1541            }
1542        }
1543    }
1544
1545    /// Registry for the reconnect pin: every dispatch is RECORDED with the identity
1546    /// the server sent it under, and [`HELD_ACTIVITY_TYPE`]'s FIRST dispatch holds
1547    /// — the work is finished, its reply is not yet on the wire — until released.
1548    ///
1549    /// Only the first is held. A blanket hold would stall the re-delivery this pin
1550    /// exists to observe, and the pin would then measure its own instrument.
1551    fn recording_registry(
1552        seen: &Arc<std::sync::Mutex<Vec<SeenDispatch>>>,
1553        release: &Arc<std::sync::atomic::AtomicBool>,
1554    ) -> Result<Arc<ActivityRegistry>, TestError> {
1555        let mut registry = ActivityRegistry::new();
1556        for activity_type in FAN_ACTIVITY_TYPES {
1557            let seen = Arc::clone(seen);
1558            let release = Arc::clone(release);
1559            let arrivals = Arc::new(AtomicUsize::new(0));
1560            registry = registry
1561                .register_activity_with_contract(
1562                    activity_type,
1563                    move |_input: FanInput, context: &aion_worker::ActivityContext| {
1564                        let seen = Arc::clone(&seen);
1565                        let release = Arc::clone(&release);
1566                        let arrivals = Arc::clone(&arrivals);
1567                        let record = SeenDispatch {
1568                            activity_type: activity_type.to_owned(),
1569                            activity_id: context.activity_id().to_string(),
1570                            attempt: context.attempt(),
1571                            at: Instant::now(),
1572                        };
1573                        Box::pin(async move {
1574                            // Recorded BEFORE the hold: a dispatch that arrives and
1575                            // is never answered must still be visible, or the pin
1576                            // cannot tell "never re-delivered" from "re-delivered
1577                            // and lost again".
1578                            match seen.lock() {
1579                                Ok(mut log) => log.push(record),
1580                                Err(_) => {
1581                                    return Err(aion_worker::ActivityFailure::terminal(
1582                                        "the pin's dispatch log is poisoned, so this run can \
1583                                         observe nothing — failing loudly rather than \
1584                                         returning a result no assertion could trust",
1585                                    ));
1586                                }
1587                            }
1588                            let first = arrivals.fetch_add(1, Ordering::SeqCst) == 0;
1589                            if activity_type == HELD_ACTIVITY_TYPE && first {
1590                                while !release.load(Ordering::SeqCst) {
1591                                    tokio::time::sleep(Duration::from_millis(5)).await;
1592                                }
1593                            }
1594                            Ok(activity_type.to_owned())
1595                        })
1596                    },
1597                )
1598                .map_err(test_error)?;
1599        }
1600        Ok(Arc::new(registry))
1601    }
1602
1603    fn dispatches_of(
1604        seen: &Arc<std::sync::Mutex<Vec<SeenDispatch>>>,
1605        activity_type: &str,
1606    ) -> Result<Vec<SeenDispatch>, TestError> {
1607        let log = seen
1608            .lock()
1609            .map_err(|_| test_error("the pin's dispatch log is poisoned"))?;
1610        Ok(log
1611            .iter()
1612            .filter(|record| record.activity_type == activity_type)
1613            .cloned()
1614            .collect())
1615    }
1616
1617    fn dispatch_log(seen: &Arc<std::sync::Mutex<Vec<SeenDispatch>>>) -> String {
1618        match seen.lock() {
1619            Ok(log) => format!("{:#?}", *log),
1620            Err(_) => String::from("<poisoned>"),
1621        }
1622    }
1623
1624    /// aion #69 at the ENGINE level: what the system DOES after an activity's
1625    /// completion is lost to a broken link.
1626    ///
1627    /// # What this measures, and why the transport-level pin cannot
1628    ///
1629    /// #69's existing red-first pin lives on its fix branch rather than here (it
1630    /// is red on purpose and lands with the fix), and it establishes that the
1631    /// completion is DISCARDED: the server abandons the correlated reply-wait the
1632    /// moment the delivering connection closes. It drives `WorkerDelivery`
1633    /// directly, with no engine, no store and no workflow behind it, so it can say
1634    /// nothing at all about what happens NEXT. That gap is the whole severity of
1635    /// #69: "the work is repeated once" and "the work is lost" are priced very
1636    /// differently, and nothing in-tree could tell them apart.
1637    ///
1638    /// So this pin observes four things, and asserts only what must hold in EVERY
1639    /// world — including the one a #69 fix creates:
1640    ///
1641    /// - **O4, ASSERTED** — the workflow still reaches a recorded terminal. This is
1642    ///   the invariant: a broken link must not cost the workflow. It is not a weak
1643    ///   assertion, because `collect_four` consumes all four members, so the
1644    ///   workflow cannot complete while any member's work is missing;
1645    /// - **O1, REPORTED** — whether the held activity is dispatched a SECOND time.
1646    ///   This is the MECHANISM, and the mechanism is what a fix changes: a fix that
1647    ///   carries the completion across the reconnect would produce NO re-delivery,
1648    ///   and a pin asserting one would read that fix as a regression.
1649    ///   regression;
1650    /// - **O2, asserted CONDITIONALLY** — if a re-delivery happened it must carry
1651    ///   the activity's OWN identity. That is what makes the finished work
1652    ///   discarded rather than recovered; a re-delivery under a different identity
1653    ///   is a different defect and must not pass quietly;
1654    /// - **O3, REPORTED** — the elapsed time from the break to the re-delivery, as
1655    ///   a NUMBER asserted against nothing. No threshold is invented here: the
1656    ///   right bound is a conversation to have with the measurement in hand.
1657    ///
1658    /// ⚠️ **O3 is recovery LATENCY, and latency is not COST.** The number is
1659    /// measured on a fixture activity that is a pure `String -> String`, so its
1660    /// repeat costs microseconds. The real cost of a repeat is the repeated
1661    /// activity's own runtime plus its repeated SIDE EFFECTS, which this pin does
1662    /// not measure and structurally cannot: #69's own exhibit was an *agent*
1663    /// activity, whose repeat is minutes of compute and files written twice.
1664    /// Quote the finding — *repeated work, not lost work, one repeat per in-flight
1665    /// activity* — rather than the milliseconds, which carry their premise (a
1666    /// trivial activity) only for as long as someone remembers to attach it.
1667    ///
1668    /// The settle-wait below is bounded by [`POLL_DEADLINE`], so this pin cannot
1669    /// hang; but that bound is ~100x the observed recovery, so it is a liveness
1670    /// guard and NOT a latency guard. A large latency regression would still pass
1671    /// here, reported in O3 and asserted by nothing — deliberately, because the
1672    /// correct bound is not derivable from the samples taken so far.
1673    ///
1674    /// Executions are REPORTED, never asserted equal to the fan-out. A transport
1675    /// that can lose a reply gives at-least-once delivery, so the sibling test's
1676    /// `executions == FAN_OUT` is the wrong shape here and must not be copied
1677    /// across.
1678    ///
1679    /// # The world this models
1680    ///
1681    /// One server process with its transport-loss ledger live in memory, a worker
1682    /// that redials the SAME address, and a SINGLE loss — well inside
1683    /// `TRANSPORT_LOSS_BUDGET_WINDOWS`. It is NOT a server restart and NOT budget
1684    /// exhaustion, both of which are different worlds with different recoveries.
1685    /// The re-delivery this venue can produce is the outbox dispatcher's re-claim
1686    /// under the `max_attempts`/backoff this test's config sets, not the #266
1687    /// recovery replay — which is what gives O3's number a slot to mean anything in.
1688    ///
1689    /// The relay's own accept poll (2ms) sits inside the measured elapsed.
1690    ///
1691    /// ⚠️ This pin shares a venue with
1692    /// `production_boot_dispatches_executes_and_records_over_liminal`, one of four
1693    /// documented carriers of a load-sensitive flake — 2/24 on a base that
1694    /// predates it (`gate-logs/lock-race-attribution/VERDICT.md`). It inherits that
1695    /// sensitivity, and a red here should be read against that register first.
1696    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1697    async fn a_completion_lost_to_a_severed_link_is_re_dispatched_and_the_workflow_settles()
1698    -> Result<(), TestError> {
1699        let dir = crate::test_support::private_tempdir().map_err(test_error)?;
1700        let db_path = dir.path().join("aion.db");
1701        let package_path = write_package_archive(dir.path())?;
1702        let listen_address = reserve_loopback_port()?;
1703
1704        let config = server_config(&db_path, package_path, listen_address);
1705        let outbox_config = config.outbox.clone();
1706        // Captured before `build` consumes the config: the wait below is derived
1707        // from the very window this server is about to run its liveness probe on.
1708        let patience = eligibility_patience(&config);
1709        let state = ServerState::build(config).await.map_err(test_error)?;
1710        let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
1711        let backpressure_settings = BackpressureSettings {
1712            platform_default: crate::config::DEFAULT_MAX_IN_FLIGHT_ACTIVITIES,
1713            fraction: crate::worker::OwnedShardFraction::own_all(),
1714        };
1715        let listener_guard = maybe_spawn_outbox_dispatcher(
1716            &state,
1717            &outbox_config,
1718            false,
1719            backpressure_settings,
1720            &shutdown_rx,
1721            "set outbox.liminal_listen_address in the test config",
1722        )
1723        .map_err(test_error)?;
1724
1725        // The worker dials the RELAY, which carries it to the production listener.
1726        let relay = SeverableRelay::spawn(listen_address)?;
1727        let seen = Arc::new(std::sync::Mutex::new(Vec::new()));
1728        let release = Arc::new(std::sync::atomic::AtomicBool::new(false));
1729        // The redial timings are the ones this module's `worker_config` already
1730        // declares, read off it rather than re-chosen here: a reconnect pin that
1731        // picked its own recovery timings would be measuring a world of its own.
1732        let config = worker_config()?;
1733        let timing = aion_worker::RedialTiming::new(
1734            config.reconnect.initial_backoff,
1735            config.reconnect.max_backoff,
1736        );
1737        let worker = WorkerThread::spawn_redialing(
1738            relay.address().to_string(),
1739            config,
1740            recording_registry(&seen, &release)?,
1741            timing,
1742        );
1743
1744        let outcome =
1745            observe_reconnect(&state, &relay, &seen, &release, listen_address, patience).await;
1746
1747        // Teardown runs on EVERY path, including a failing one: a leaked worker
1748        // thread or listener poisons whatever runs next, and this venue is already
1749        // load-sensitive enough without the pin adding to it.
1750        shutdown_tx.send(true).ok();
1751        release.store(true, Ordering::SeqCst);
1752        worker.stop();
1753        relay.shutdown();
1754        drop(listener_guard);
1755        state.shutdown().map_err(test_error)?;
1756        outcome
1757    }
1758
1759    /// The measurement behind
1760    /// [`a_completion_lost_to_a_severed_link_is_re_dispatched_and_the_workflow_settles`],
1761    /// split out so its many early returns cannot skip the harness teardown.
1762    /// Wait until the held member is dispatched and holding — the moment the link
1763    /// can be broken — and report how many of its siblings had already settled.
1764    ///
1765    /// The split at the break is REPORTED, never required. An earlier draft
1766    /// demanded that the other three settle first, for a single-variable
1767    /// experiment. Measured across runs it simply varies: the four pushes land
1768    /// within microseconds of each other and which records a terminal first is a
1769    /// race, so requiring a particular split would fail the pin for a reason that
1770    /// has nothing to do with what it measures.
1771    async fn await_held_dispatch(
1772        reader: &dyn aion_store::ReadableEventStore,
1773        workflow_id: &aion_core::WorkflowId,
1774        seen: &Arc<std::sync::Mutex<Vec<SeenDispatch>>>,
1775    ) -> Result<(SeenDispatch, usize), TestError> {
1776        let deadline = Instant::now() + POLL_DEADLINE;
1777        loop {
1778            if let Some(first) = dispatches_of(seen, HELD_ACTIVITY_TYPE)?.first() {
1779                let at_the_break = reader.read_history(workflow_id).await.map_err(test_error)?;
1780                return Ok((first.clone(), count_completed(&at_the_break)));
1781            }
1782            if Instant::now() > deadline {
1783                let history = reader.read_history(workflow_id).await.map_err(test_error)?;
1784                return Err(test_error(format!(
1785                    "{HELD_ACTIVITY_TYPE} was never dispatched at all within {POLL_DEADLINE:?}, \
1786                     so there was no held completion to lose and this run measured nothing.\n\
1787                     dispatch log: {}\nhistory: {history:#?}",
1788                    dispatch_log(seen),
1789                )));
1790            }
1791            tokio::time::sleep(Duration::from_millis(25)).await;
1792        }
1793    }
1794
1795    async fn observe_reconnect(
1796        state: &ServerState,
1797        relay: &SeverableRelay,
1798        seen: &Arc<std::sync::Mutex<Vec<SeenDispatch>>>,
1799        release: &Arc<std::sync::atomic::AtomicBool>,
1800        listen_address: SocketAddr,
1801        patience: Duration,
1802    ) -> Result<(), TestError> {
1803        wait_for_registration(
1804            state.worker_registry(),
1805            state.heartbeat_tracker(),
1806            listen_address,
1807            patience,
1808        )
1809        .await?;
1810
1811        let router = http_router(state.clone()).map_err(test_error)?;
1812        let workflow_id = start_over_http(&router).await?;
1813        let reader = state.engine().map_err(test_error)?.store();
1814
1815        let (first, settled_before) =
1816            await_held_dispatch(reader.as_ref(), &workflow_id, seen).await?;
1817
1818        // BREAK the link while the finished work is still holding its reply.
1819        let severed = relay.sever()?;
1820        let severed_at = Instant::now();
1821        if severed == 0 {
1822            return Err(test_error(
1823                "the relay severed NOTHING, so no link was ever broken and this run measured \
1824                 nothing — a pass here would have been an artefact of the instrument",
1825            ));
1826        }
1827        // Release the hold: the worker now writes its reply into a dead socket.
1828        release.store(true, Ordering::SeqCst);
1829
1830        // O4 FIRST, because it is the INVARIANT: a broken link must not cost the
1831        // workflow. Every other observable here describes the MECHANISM by which
1832        // that holds, and the mechanism is exactly what a #69 fix is expected to
1833        // change — so asserting today's mechanism would make the fix read as a
1834        // regression, and would be asserting the enumeration rather than the
1835        // invariant.
1836        //
1837        // O4 is load-bearing rather than weak because `collect_four` CONSUMES all
1838        // four members: the workflow cannot reach a completed terminal while any
1839        // member's work is missing, so "the workflow settled" is not a state that
1840        // silently lost work can also produce.
1841        let settled = wait_for_history(
1842            reader.as_ref(),
1843            &workflow_id,
1844            "the workflow to settle after the severed link",
1845            |events| count_completed(events) == FAN_OUT && count_workflow_completed(events) == 1,
1846        )
1847        .await
1848        .map_err(|error| {
1849            test_error(format!(
1850                "O4 FAILED — the workflow did not settle after the link broke ({severed} \
1851                 socket(s) severed), so the lost completion cost the workflow rather than \
1852                 costing a repeat of the work.\n{error}\ndispatch log: {}",
1853                dispatch_log(seen),
1854            ))
1855        })?;
1856        assert_eq!(
1857            count_completed(&settled),
1858            FAN_OUT,
1859            "every fan-out member must still record a terminal after the link broke"
1860        );
1861        assert_eq!(
1862            count_workflow_completed(&settled),
1863            1,
1864            "the workflow must complete exactly once even though a completion was lost"
1865        );
1866
1867        // O1/O2/O3 — the MECHANISM, reported. O2 is asserted only CONDITIONALLY:
1868        // if a re-delivery happened it must have carried the activity's own
1869        // identity, because a re-delivery under a different identity would be a
1870        // different defect entirely and must not pass quietly. If no re-delivery
1871        // happened, the completion survived the reconnect — which is what a fixed
1872        // #69 looks like, and this pin should report it, not fail on it.
1873        let held = dispatches_of(seen, HELD_ACTIVITY_TYPE)?;
1874        match held.get(1) {
1875            None => println!(
1876                "aion#69 — {HELD_ACTIVITY_TYPE} ({}) was NOT re-dispatched and the workflow \
1877                 still settled, so the held completion survived the break; {settled_before} of \
1878                 {FAN_OUT} members had settled when it broke, {severed} socket(s) severed",
1879                first.activity_id,
1880            ),
1881            Some(second) => {
1882                if second.activity_id != first.activity_id {
1883                    return Err(test_error(format!(
1884                        "O2 FAILED — the re-delivery carried a DIFFERENT activity identity. The \
1885                         first dispatch was {} (attempt {}) and the second was {} (attempt {}), \
1886                         so the work was not re-run under its own identity and #69's framing \
1887                         does not describe what happened here.",
1888                        first.activity_id, first.attempt, second.activity_id, second.attempt,
1889                    )));
1890                }
1891                let recovery = second.at.saturating_duration_since(severed_at);
1892                println!(
1893                    "aion#69 O3 — re-delivery of {} ({}) took {}ms from the link breaking; \
1894                     first attempt {}, second attempt {}; {settled_before} of {FAN_OUT} members \
1895                     had already recorded a terminal when the link broke; {severed} socket(s) \
1896                     severed",
1897                    HELD_ACTIVITY_TYPE,
1898                    first.activity_id,
1899                    recovery.as_millis(),
1900                    first.attempt,
1901                    second.attempt,
1902                );
1903            }
1904        }
1905
1906        let all = seen
1907            .lock()
1908            .map_err(|_| test_error("the pin's dispatch log is poisoned"))?
1909            .len();
1910        println!(
1911            "aion#69 — {all} dispatch(es) served for {FAN_OUT} activities; the transport is \
1912             at-least-once, so the excess is the repeated work a broken link costs"
1913        );
1914        Ok(())
1915    }
1916}