# AppHost and with_app `AppHost` creates a scenario whose system under test is supplied by application deployments instead of an outer managed node topology. The core scenario engine models one `Application` and a uniform cluster of its nodes. For a composed stack containing a binary, an additional cluster, or several applications, start from `AppHost::scenario()` and register the stack with `.with_app(...)`. Workloads, expectations, run duration, and teardown follow the lifecycle in [Scenario Model and Lifecycle](scenario-model.md). --- ## The Zero-Node Scenario `AppHost::scenario()` returns a `ScenarioBuilder` seeded with `AppHostTopology`: | Type | Role | |------|------| | `AppHostTopology` | Deployment descriptor with `node_count() == 0`. The outer scenario manages no nodes. | | `AppHostEnv` | Null environment: `NodeClient = ()`, and `build_node_client` always errors. Clients come from app handles instead. | | `AppHostScenarioBuilder` | Alias for `ScenarioBuilder`. | | `AppHostLocalDeployer` | Alias for `ProcessDeployer` — the local deployer that executes the scenario. | Because the outer topology is empty, app deployments create the processes and clusters used by the run. ```rust,ignore use testing_framework_app::{AppHost, AppHostLocalDeployer, AppScenarioBuilderExt}; use testing_framework_core::scenario::Deployer; let mut scenario = AppHost::scenario() .with_app(KvLocalApp::nodes(3)) .with_run_duration(Duration::from_secs(5)) .with_workload(KvAppHostConvergence::new(3)) .build()?; let deployer = AppHostLocalDeployer::default(); let runner = deployer.deploy(&scenario).await?; runner.run(&mut scenario).await?; ``` The runnable `kvstore_app_host_convergence` binary uses this structure: ```bash cargo run -p kvstore-examples --bin kvstore_app_host_convergence ``` --- ## How with_app Runs `AppScenarioBuilderExt::with_app(app)` wraps your [`AppDeployment`](app-deployment.md) in an `AppDeploymentFactory` and registers it as a runtime extension factory, the same lifecycle hook covered in [Runtime Extensions](runtime-extensions.md). Going through the extension mechanism ties managed deployment cleanup to the scenario lifetime and makes exposed handles available during the run. ```mermaid flowchart LR B["with_app(app)"] --> F[AppDeploymentFactory] F -->|prepare| C[DeployContext] C -->|"deploy(root app)"| H["handles + cleanup"] H --> R[AppRuntime extension] R -->|require_app| W[Workloads] H:::hd R:::hd W:::sc classDef hd stroke:#4caf7d,stroke-width:2.5px; classDef sc stroke:#9b6dd6,stroke-width:2.5px; ``` During scenario preparation the factory: 1. Clones your app (this is why the factory requires `Clone`) and builds a fresh `DeployContext`. 2. Runs the root deployment's `deploy`, which may deploy and expose child apps. 3. Auto-exposes the returned root handle if the deployment did not expose one of that type itself (`!ctx.contains::()`). 4. Transfers the handle registry and cleanup stack into an `AppRuntime` extension. If any step fails, the partially built context is dropped and every resource deployed so far is released (see [Handle Ownership and Teardown](handles-teardown.md)). A scenario accepts one `with_app` registration. Every `AppDeploymentFactory` produces the same extension type (`AppRuntime`), and the runtime rejects duplicate extension types. A second registration fails during preparation with `duplicate runtime extension type registered: AppRuntime`. Compose several applications inside one root `AppDeployment` and expose the child handles from there, as shown in [Composing Heterogeneous Stacks](composing-stacks.md). --- ## with_app Outside AppHost `with_app` is defined for every scenario builder, not only `AppHostScenarioBuilder`. On a regular uniform-cluster scenario, an "existing cluster" preset can wrap the outer scenario's deployment and node clients in a typed handle without deploying another resource. The OpenRaft example uses this pattern: ```rust,ignore // examples/openraft_kv/testing/integration/src/scenario.rs fn with_existing_openraft_kv_app(app: OpenRaftKvExistingClusterApp) -> Self { OpenRaftKvScenarioBuilder::with_deployment(app.topology()) .with_app(app) .with_cluster_observer() } ``` Here the scenario still manages a uniform OpenRaft cluster, and the app layer just gives workloads a typed `OpenRaftKvCluster` handle over it. --- ## Retrieving Handles in Workloads Workloads never see the deploy context. They retrieve exposed handles through `AppRunContextExt`, implemented on `RunContext`: | Method | Returns | |--------|---------| | `app::()` | `Option` — default handle for `T`, if exposed | | `app_named::(name)` | `Option` — named handle for `T` | | `require_app::()` | `Result` — errors if missing | | `require_app_named::(name)` | `Result` — errors if missing | ```rust,ignore use testing_framework_app::AppRunContextExt; async fn start(&self, ctx: &RunContext) -> Result<(), DynError> { let cluster = ctx.require_app::>()?; cluster.restart_node("node-0").await?; cluster.wait_node_ready("node-0").await?; Ok(()) } ``` Workloads normally use the `require_*` variants so that a missing handle produces a typed error containing the requested handle type. Every retrieval clones the handle. Handles are normally small access values backed by `Arc`; scenario cleanup still determines managed resource lifetime. --- ## Where to Go Next - [AppDeployment and DeployContext](app-deployment.md): implementing the deployment itself. - [One Binary: LocalProcessApp](local-process-app.md) and [Uniform Child Clusters: LocalAppCluster](local-app-cluster.md): the two built-in building blocks. - [Backend Scope](app-backend-scope.md): why AppHost scenarios run on the local deployer today.