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