Repository: holochain/wind-tunnel
Pull request #173: refactor(remote-signals): migrate scenario to default holochain
Author: login: jost-s
Created: 2025-04-27T00:07:18Z | Merged: 2025-04-30T13:22:07Z

### Summary



### TODO:
- [x] CHANGELOGs updated with appropriate info
- [x] All code changes are reflected in docs, including module-level docs


Diff:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index c6650abd1..c5cb5a158 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -227,20 +227,14 @@ jobs:
 
       - name: Smoke test - remote_signals
         run: |
-          set -x
-
-          # Start local network services
-          nix develop .#ci -c bash -c "hc-run-local-services --bootstrap-port 4422 --signal-port 4423 &"
-          # Start a TryCP instance
-          nix develop .#ci -c bash -c "source ./scripts/trycp.sh && start_trycp &"
+          # Start a sandbox conductor and run it in the background
+          nix develop .#ci -c bash -c "hc s clean && echo "1234" | hc s --piped create && echo "1234" | hc s --piped -f 8888 run &"
 
           # Run the scenario
-          RUST_LOG=warn CONDUCTOR_CONFIG="CI" MIN_PEERS=2 nix run .#remote_signals -- --targets targets-ci.yaml --instances-per-target 2 --duration 30 --no-progress
+          RUST_LOG=warn MIN_AGENTS=2 cargo run -p remote_signals -- --connection-string ws://localhost:8888 --agents 2 --duration 30 --no-progress
 
-          # Stop the TryCP instance
-          nix develop .#ci -c bash -c "source ./scripts/trycp.sh && stop_trycp"
-          # Stop local network services
-          pkill hc-run-local
+          # Stop sandbox conductor
+          pkill hc && pkill holochain && pkill lair-keystore
 
       - name: Smoke test - kitsune continuous flow
         run: |
diff --git a/Cargo.lock b/Cargo.lock
index 3fb6f61a2..a6a57452e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5184,11 +5184,11 @@ dependencies = [
  "anyhow",
  "happ_builder",
  "holochain_types",
+ "holochain_wind_tunnel_runner",
  "log",
  "rand 0.8.5",
  "remote_signal_integrity",
  "tokio",
- "trycp_wind_tunnel_runner",
 ]
 
 [[package]]
diff --git a/scenarios/remote_signals/Cargo.toml b/scenarios/remote_signals/Cargo.toml
index 7147dd638..b3de33f44 100644
--- a/scenarios/remote_signals/Cargo.toml
+++ b/scenarios/remote_signals/Cargo.toml
@@ -11,7 +11,7 @@ rand = { workspace = true }
 log = { workspace = true }
 
 holochain_types = { workspace = true }
-trycp_wind_tunnel_runner = { workspace = true }
+holochain_wind_tunnel_runner = { workspace = true }
 remote_signal_integrity = { workspace = true }
 
 [build-dependencies]
diff --git a/scenarios/remote_signals/README.md b/scenarios/remote_signals/README.md
index 1f9d947b7..1b1d70d14 100644
--- a/scenarios/remote_signals/README.md
+++ b/scenarios/remote_signals/README.md
@@ -14,34 +14,21 @@ Two custom metrics are recorded:
 - `wt.custom.remote_signal_round_trip`: The time in floating-point seconds from origin signal dispatch to origin receive of the remote side's response signal.
 - `wt.custom.remote_signal_timeout`: A counter (value 1) recorded every time there is a timeout waiting for the response signal. (Defaults to 20 seconds--see above)
 
-> [!WARNING]
-> This is a TryCP-based scenario and needs to be run differently to other scenarios.
-
 ### Waiting for peer discovery
 
-This scenario reads the environment variable `MIN_PEERS` and waits for at least that many peers to be available before
+This scenario reads the environment variable `MIN_AGENTS` and waits for at least that many agents to be available before
 starting the agent behaviour. It will wait up to two minutes then proceed regardless.
 
 The scenario is not able to check that you have configured more peers than the minimum you have set, so you should
 ensure that you have configured enough peers to meet the minimum.
 
-Note that the number of peers seen by each node includes itself. So having two nodes means that each node will 
-immediately see one peer after app installation.
-
-This configuration defaults to 2 peers.
+Note that the number of agents seen by each conductor includes itself. So having two conductors means that each will
+immediately see one agent after app installation.
 
 ### Suggested command
 
 You can run the scenario locally with the following command:
 
 ```bash
-RUST_LOG=info CONDUCTOR_CONFIG="CI" TRYCP_RUST_LOG="info" MIN_PEERS=2 cargo run --package remote_signals -- --targets targets-ci.yaml --instances-per-target 2 --duration 300
-```
-
-This assumes that `trycp_server` is running. See the script `scripts/trycp.sh` and run with `start_trycp`.
-
-To run the scenario against the current target list, you can run:
-
-```bash
-RUST_LOG=info MIN_PEERS=40 cargo run --package remote_signal_scenario -- --targets targets.yaml --duration 500
+RUST_LOG=info MIN_AGENTS=2 cargo run -p remote_signals -- --connection-string ws://localhost:8888 --agents 2 --duration 500
 ```
diff --git a/scenarios/remote_signals/src/main.rs b/scenarios/remote_signals/src/main.rs
index 44a75a74c..3df87d003 100644
--- a/scenarios/remote_signals/src/main.rs
+++ b/scenarios/remote_signals/src/main.rs
@@ -1,15 +1,13 @@
 use anyhow::Context;
 use holochain_types::prelude::*;
+use holochain_wind_tunnel_runner::{prelude::*, scenario_happ_path};
 use rand::seq::SliceRandom;
 use rand::thread_rng;
 use remote_signal_integrity::TimedMessage;
 use std::collections::HashSet;
 use std::sync::{Arc, Mutex};
+use std::thread;
 use std::time::Duration;
-use trycp_wind_tunnel_runner::embed_conductor_config;
-use trycp_wind_tunnel_runner::prelude::*;
-
-embed_conductor_config!();
 
 #[derive(Debug)]
 pub struct ScenarioValues {
@@ -42,84 +40,71 @@ impl Default for ScenarioValues {
 
 impl UserValuesConstraint for ScenarioValues {}
 
+fn setup(ctx: &mut RunnerContext<HolochainRunnerContext>) -> HookResult {
+    configure_app_ws_url(ctx)?;
+    Ok(())
+}
+
 fn agent_setup(
-    ctx: &mut AgentContext<TryCPRunnerContext, TryCPAgentContext<ScenarioValues>>,
+    ctx: &mut AgentContext<HolochainRunnerContext, HolochainAgentContext<ScenarioValues>>,
 ) -> HookResult {
-    connect_trycp_client(ctx)?;
-    reset_trycp_remote(ctx)?;
+    install_app(
+        ctx,
+        scenario_happ_path!("remote_signal"),
+        &"remote_signal".to_string(),
+    )?;
 
     let pending_set = ctx.get().scenario_values.pending_set.clone();
-
-    let client = ctx.get().trycp_client();
-    let agent_name = ctx.agent_name().to_string();
     let reporter = ctx.runner_context().reporter();
+    let client = ctx.get().app_client();
 
     ctx.runner_context()
         .executor()
         .execute_in_place(async move {
-            client
-                .configure_player(agent_name.clone(), conductor_config().to_string(), None)
-                .await?;
-
-            client.startup(agent_name.clone(), None).await?;
-
             let _rcv_task = tokio::task::spawn(async move {
-                loop {
-                    let msg: Signal = ExternIO(
-                        client
-                            .recv_signal()
-                            .await
-                            .expect("signal receiver ended")
-                            .data,
-                    )
-                    .decode()
-                    .unwrap();
-                    let msg = match msg {
-                        Signal::App { signal, .. } => {
-                            let msg: Vec<u8> = signal.into_inner().decode().unwrap();
-                            let msg: TimedMessage = ExternIO(msg).decode().unwrap();
-                            msg
-                        }
-                        _ => continue,
-                    };
-
-                    pending_set.lock().unwrap().remove(&msg.to_request());
-
-                    match msg {
-                        TimedMessage::TimedRequest { .. } => (),
-                        TimedMessage::TimedResponse { requested_at, .. } => {
-                            let dispatch_time_s = requested_at.as_micros() as f64 / 1_000_000.0;
-                            let receive_time_s = Timestamp::now().as_micros() as f64 / 1_000_000.0;
-
-                            reporter.add_custom(
-                                ReportMetric::new("remote_signal_round_trip")
-                                    .with_field("value", receive_time_s - dispatch_time_s),
-                            );
+                client
+                    .on_signal(move |signal| {
+                        let msg = match signal {
+                            Signal::App { signal, .. } => {
+                                let msg: Vec<u8> = signal.into_inner().decode().unwrap();
+                                let msg: TimedMessage = ExternIO(msg).decode().unwrap();
+                                msg
+                            }
+                            _ => return,
+                        };
+
+                        pending_set.lock().unwrap().remove(&msg.to_request());
+
+                        match msg {
+                            TimedMessage::TimedRequest { .. } => (),
+                            TimedMessage::TimedResponse { requested_at, .. } => {
+                                let dispatch_time_s = requested_at.as_micros() as f64 / 1_000_000.0;
+                                let receive_time_s =
+                                    Timestamp::now().as_micros() as f64 / 1_000_000.0;
+
+                                reporter.add_custom(
+                                    ReportMetric::new("remote_signal_round_trip")
+                                        .with_field("value", receive_time_s - dispatch_time_s),
+                                );
+                            }
                         }
-                    }
-                }
+                    })
+                    .await
+                    .expect("signal receiver ended");
             });
 
             Ok(())
         })?;
 
-    install_app(
-        ctx,
-        scenario_happ_path!("remote_signal"),
-        &"remote_signal".to_string(),
-    )?;
-    try_wait_for_min_peers(ctx, Duration::from_secs(120))?;
+    try_wait_for_min_agents(ctx, Duration::from_secs(120))?;
 
     Ok(())
 }
 
 fn agent_behaviour(
-    ctx: &mut AgentContext<TryCPRunnerContext, TryCPAgentContext<ScenarioValues>>,
+    ctx: &mut AgentContext<HolochainRunnerContext, HolochainAgentContext<ScenarioValues>>,
 ) -> HookResult {
-    let client = ctx.get().trycp_client();
-
-    let agent_name = ctx.agent_name().to_string();
-    let app_port = ctx.get().app_port();
+    let admin_ws_url = ctx.runner_context().get_connection_string().to_string();
     let cell_id = ctx.get().cell_id();
     let next_remote_signal_peer = ctx.get_mut().scenario_values.remote_signal_peers.pop();
     let signal_interval = ctx.get().scenario_values.signal_interval;
@@ -127,37 +112,34 @@ fn agent_behaviour(
     let pending_set = ctx.get().scenario_values.pending_set.clone();
     let reporter = ctx.runner_context().reporter();
 
-    let new_peers = ctx
-        .runner_context()
-        .executor()
-        .execute_in_place(async move {
-            let now = Timestamp::now();
-
-            let mut timeout_count = 0;
-
-            pending_set.lock().unwrap().retain(|msg| {
-                if now.as_millis() - msg.requested_at().as_millis()
-                    > response_timeout.as_millis() as i64
-                {
-                    timeout_count += 1;
-                    false
-                } else {
-                    true
-                }
-            });
+    let now = Timestamp::now();
 
-            while timeout_count > 0 {
-                reporter
-                    .add_custom(ReportMetric::new("remote_signal_timeout").with_field("value", 1));
-                timeout_count -= 1;
-            }
+    let mut timeout_count = 0;
 
-            match next_remote_signal_peer {
-                None => {
+    pending_set.lock().unwrap().retain(|msg| {
+        if now.as_millis() - msg.requested_at().as_millis() > response_timeout.as_millis() as i64 {
+            timeout_count += 1;
+            false
+        } else {
+            true
+        }
+    });
+
+    while timeout_count > 0 {
+        reporter.add_custom(ReportMetric::new("remote_signal_timeout").with_field("value", 1));
+        timeout_count -= 1;
+    }
+
+    let new_peers = match next_remote_signal_peer {
+        None => {
+            ctx.runner_context()
+                .executor()
+                .execute_in_place(async move {
+                    let admin_client = AdminWebsocket::connect(admin_ws_url, reporter).await?;
                     // No more agents available to signal, get a new list.
                     // This is also the initial condition.
-                    let mut new_peer_list = client
-                        .agent_info(agent_name, None, None)
+                    let mut new_peer_list = admin_client
+                        .agent_info(None)
                         .await
                         .context("Failed to get agent info")?
                         .into_iter()
@@ -166,39 +148,25 @@ fn agent_behaviour(
                         .collect::<Vec<_>>();
                     new_peer_list.shuffle(&mut thread_rng());
                     Ok(new_peer_list)
-                }
-                Some(agent_pub_key) => {
-                    let msg = TimedMessage::TimedRequest {
-                        requester: cell_id.agent_pubkey().clone(),
-                        responder: agent_pub_key.clone(),
-                        requested_at: Timestamp::now(),
-                    };
-                    pending_set.lock().unwrap().insert(msg.clone());
-                    // Send a remote signal to this agent
-                    client
-                        .call_zome(
-                            app_port,
-                            cell_id.clone(),
-                            "remote_signal",
-                            "signal_request",
-                            msg,
-                            // Better to keep this higher than the Kitsune timeout so that when this fails we get a
-                            // clear error back, rather than timing out here.
-                            Some(Duration::from_secs(80)),
-                        )
-                        .await
-                        .with_context(|| {
-                            format!("Failed to make remote signal to: {:?}", agent_pub_key)
-                        })?;
-
-                    // Don't hammer with signals
-                    tokio::time::sleep(signal_interval).await;
-
-                    // Add no new agents, that should only happen when we exhaust the list.
-                    Ok(Vec::with_capacity(0))
-                }
-            }
-        })?;
+                })?
+        }
+        Some(agent_pub_key) => {
+            let msg = TimedMessage::TimedRequest {
+                requester: cell_id.agent_pubkey().clone(),
+                responder: agent_pub_key.clone(),
+                requested_at: Timestamp::now(),
+            };
+            pending_set.lock().unwrap().insert(msg.clone());
+            // Send a remote signal to this agent
+            let _: () = call_zome(ctx, "remote_signal", "signal_request", msg)?;
+
+            // Don't hammer with signals
+            thread::sleep(signal_interval);
+
+            // Add no new agents, that should only happen when we exhaust the list.
+            Vec::with_capacity(0)
+        }
+    };
 
     ctx.get_mut()
         .scenario_values
@@ -208,36 +176,20 @@ fn agent_behaviour(
     Ok(())
 }
 
-fn agent_teardown(
-    ctx: &mut AgentContext<TryCPRunnerContext, TryCPAgentContext<ScenarioValues>>,
-) -> HookResult {
-    if let Err(e) = dump_logs(ctx) {
-        log::warn!("Failed to dump logs: {:?}", e);
-    }
-
-    // Best effort to remove data and cleanup.
-    // You should comment out this line if you want to examine the result of the scenario run!
-    let _ = reset_trycp_remote(ctx);
-
-    // Alternatively, you can just shut down the remote conductor instead of shutting it down and removing data.
-    // shutdown_remote(ctx)?;
-
-    disconnect_trycp_client(ctx)?;
-
-    Ok(())
-}
-
 fn main() -> WindTunnelResult<()> {
-    let builder = TryCPScenarioDefinitionBuilder::<
-        TryCPRunnerContext,
-        TryCPAgentContext<ScenarioValues>,
-    >::new_with_init(env!("CARGO_PKG_NAME"))?
-    .into_std()
+    let builder = ScenarioDefinitionBuilder::<
+        HolochainRunnerContext,
+        HolochainAgentContext<ScenarioValues>,
+    >::new_with_init(env!("CARGO_PKG_NAME"))
+    .use_setup(setup)
     .use_agent_setup(agent_setup)
     .use_agent_behaviour(agent_behaviour)
-    .use_agent_teardown(agent_teardown);
+    .use_agent_teardown(|ctx| {
+        uninstall_app(ctx, None).ok();
+        Ok(())
+    });
 
-    run_with_required_agents(builder, 1)?;
+    run(builder)?;
 
     Ok(())
 }


Reviews:
user: login: ThetaSinner
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-04-28T18:00:02Z
user: login: jost-s
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T18:02:09Z
user: login: jost-s
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T18:04:25Z
user: login: ThetaSinner
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-04-28T18:05:21Z
user: login: ThetaSinner
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-28T18:06:05Z
user: login: jost-s
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T18:46:23Z
user: login: cdunster
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T07:13:30Z
user: login: jost-s
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T11:37:06Z
user: login: cdunster
body: Another one bites the dust :smile: 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T13:22:06Z

Review comments:
diff_hunk: @@ -42,122 +40,110 @@ impl Default for ScenarioValues {
 
 impl UserValuesConstraint for ScenarioValues {}
 
+fn setup(ctx: &mut RunnerContext<HolochainRunnerContext>) -> HookResult {
+    configure_app_ws_url(ctx)?;
+    Ok(())
+}
+
 fn agent_setup(
-    ctx: &mut AgentContext<TryCPRunnerContext, TryCPAgentContext<ScenarioValues>>,
+    ctx: &mut AgentContext<HolochainRunnerContext, HolochainAgentContext<ScenarioValues>>,
 ) -> HookResult {
-    connect_trycp_client(ctx)?;
-    reset_trycp_remote(ctx)?;
+    install_app(
+        ctx,
+        scenario_happ_path!("remote_signal"),
+        &"remote_signal".to_string(),
+    )?;
 
     let pending_set = ctx.get().scenario_values.pending_set.clone();
-
-    let client = ctx.get().trycp_client();
-    let agent_name = ctx.agent_name().to_string();
     let reporter = ctx.runner_context().reporter();
+    let agent_index = ctx.agent_index();
+    let client = ctx.get().app_client();
 
     ctx.runner_context()
         .executor()
         .execute_in_place(async move {
-            client
-                .configure_player(agent_name.clone(), conductor_config().to_string(), None)
-                .await?;
-
-            client.startup(agent_name.clone(), None).await?;
-
             let _rcv_task = tokio::task::spawn(async move {
-                loop {
-                    let msg: Signal = ExternIO(
-                        client
-                            .recv_signal()
-                            .await
-                            .expect("signal receiver ended")
-                            .data,
-                    )
-                    .decode()
-                    .unwrap();
-                    let msg = match msg {
-                        Signal::App { signal, .. } => {
-                            let msg: Vec<u8> = signal.into_inner().decode().unwrap();
-                            let msg: TimedMessage = ExternIO(msg).decode().unwrap();
-                            msg
-                        }
-                        _ => continue,
-                    };
-
-                    pending_set.lock().unwrap().remove(&msg.to_request());
-
-                    match msg {
-                        TimedMessage::TimedRequest { .. } => (),
-                        TimedMessage::TimedResponse { requested_at, .. } => {
-                            let dispatch_time_s = requested_at.as_micros() as f64 / 1_000_000.0;
-                            let receive_time_s = Timestamp::now().as_micros() as f64 / 1_000_000.0;
-
-                            reporter.add_custom(
-                                ReportMetric::new("remote_signal_round_trip")
-                                    .with_field("value", receive_time_s - dispatch_time_s),
-                            );
+                // loop {
path: scenarios/remote_signals/src/main.rs
user: login: ThetaSinner
body: ```suggestion
```
created_at: 2025-04-28T17:58:36Z
author_association: MEMBER
diff_hunk: @@ -42,122 +40,110 @@ impl Default for ScenarioValues {
 
 impl UserValuesConstraint for ScenarioValues {}
 
+fn setup(ctx: &mut RunnerContext<HolochainRunnerContext>) -> HookResult {
+    configure_app_ws_url(ctx)?;
+    Ok(())
+}
+
 fn agent_setup(
-    ctx: &mut AgentContext<TryCPRunnerContext, TryCPAgentContext<ScenarioValues>>,
+    ctx: &mut AgentContext<HolochainRunnerContext, HolochainAgentContext<ScenarioValues>>,
 ) -> HookResult {
-    connect_trycp_client(ctx)?;
-    reset_trycp_remote(ctx)?;
+    install_app(
+        ctx,
+        scenario_happ_path!("remote_signal"),
+        &"remote_signal".to_string(),
+    )?;
 
     let pending_set = ctx.get().scenario_values.pending_set.clone();
-
-    let client = ctx.get().trycp_client();
-    let agent_name = ctx.agent_name().to_string();
     let reporter = ctx.runner_context().reporter();
+    let agent_index = ctx.agent_index();
+    let client = ctx.get().app_client();
 
     ctx.runner_context()
         .executor()
         .execute_in_place(async move {
-            client
-                .configure_player(agent_name.clone(), conductor_config().to_string(), None)
-                .await?;
-
-            client.startup(agent_name.clone(), None).await?;
-
             let _rcv_task = tokio::task::spawn(async move {
-                loop {
-                    let msg: Signal = ExternIO(
-                        client
-                            .recv_signal()
-                            .await
-                            .expect("signal receiver ended")
-                            .data,
-                    )
-                    .decode()
-                    .unwrap();
-                    let msg = match msg {
-                        Signal::App { signal, .. } => {
-                            let msg: Vec<u8> = signal.into_inner().decode().unwrap();
-                            let msg: TimedMessage = ExternIO(msg).decode().unwrap();
-                            msg
-                        }
-                        _ => continue,
-                    };
-
-                    pending_set.lock().unwrap().remove(&msg.to_request());
-
-                    match msg {
-                        TimedMessage::TimedRequest { .. } => (),
-                        TimedMessage::TimedResponse { requested_at, .. } => {
-                            let dispatch_time_s = requested_at.as_micros() as f64 / 1_000_000.0;
-                            let receive_time_s = Timestamp::now().as_micros() as f64 / 1_000_000.0;
-
-                            reporter.add_custom(
-                                ReportMetric::new("remote_signal_round_trip")
-                                    .with_field("value", receive_time_s - dispatch_time_s),
-                            );
+                // loop {
+                client
+                    .on_signal(move |signal| {
+                        println!("agent {:?} received a signal", agent_index);
path: scenarios/remote_signals/src/main.rs
user: login: ThetaSinner
body: Prefer `tracing` if this is useful, it's easier to filter out information you don't need
created_at: 2025-04-28T17:59:02Z
author_association: MEMBER
diff_hunk: @@ -42,122 +40,110 @@ impl Default for ScenarioValues {
 
 impl UserValuesConstraint for ScenarioValues {}
 
+fn setup(ctx: &mut RunnerContext<HolochainRunnerContext>) -> HookResult {
+    configure_app_ws_url(ctx)?;
+    Ok(())
+}
+
 fn agent_setup(
-    ctx: &mut AgentContext<TryCPRunnerContext, TryCPAgentContext<ScenarioValues>>,
+    ctx: &mut AgentContext<HolochainRunnerContext, HolochainAgentContext<ScenarioValues>>,
 ) -> HookResult {
-    connect_trycp_client(ctx)?;
-    reset_trycp_remote(ctx)?;
+    install_app(
+        ctx,
+        scenario_happ_path!("remote_signal"),
+        &"remote_signal".to_string(),
+    )?;
 
     let pending_set = ctx.get().scenario_values.pending_set.clone();
-
-    let client = ctx.get().trycp_client();
-    let agent_name = ctx.agent_name().to_string();
     let reporter = ctx.runner_context().reporter();
+    let agent_index = ctx.agent_index();
+    let client = ctx.get().app_client();
 
     ctx.runner_context()
         .executor()
         .execute_in_place(async move {
-            client
-                .configure_player(agent_name.clone(), conductor_config().to_string(), None)
-                .await?;
-
-            client.startup(agent_name.clone(), None).await?;
-
             let _rcv_task = tokio::task::spawn(async move {
-                loop {
-                    let msg: Signal = ExternIO(
-                        client
-                            .recv_signal()
-                            .await
-                            .expect("signal receiver ended")
-                            .data,
-                    )
-                    .decode()
-                    .unwrap();
-                    let msg = match msg {
-                        Signal::App { signal, .. } => {
-                            let msg: Vec<u8> = signal.into_inner().decode().unwrap();
-                            let msg: TimedMessage = ExternIO(msg).decode().unwrap();
-                            msg
-                        }
-                        _ => continue,
-                    };
-
-                    pending_set.lock().unwrap().remove(&msg.to_request());
-
-                    match msg {
-                        TimedMessage::TimedRequest { .. } => (),
-                        TimedMessage::TimedResponse { requested_at, .. } => {
-                            let dispatch_time_s = requested_at.as_micros() as f64 / 1_000_000.0;
-                            let receive_time_s = Timestamp::now().as_micros() as f64 / 1_000_000.0;
-
-                            reporter.add_custom(
-                                ReportMetric::new("remote_signal_round_trip")
-                                    .with_field("value", receive_time_s - dispatch_time_s),
-                            );
+                // loop {
+                client
+                    .on_signal(move |signal| {
+                        println!("agent {:?} received a signal", agent_index);
+                        let msg = match signal {
+                            Signal::App { signal, .. } => {
+                                let msg: Vec<u8> = signal.into_inner().decode().unwrap();
+                                let msg: TimedMessage = ExternIO(msg).decode().unwrap();
+                                msg
+                            }
+                            _ => return,
+                        };
+
+                        pending_set.lock().unwrap().remove(&msg.to_request());
+
+                        match msg {
+                            TimedMessage::TimedRequest { .. } => (),
+                            TimedMessage::TimedResponse { requested_at, .. } => {
+                                let dispatch_time_s = requested_at.as_micros() as f64 / 1_000_000.0;
+                                let receive_time_s =
+                                    Timestamp::now().as_micros() as f64 / 1_000_000.0;
+
+                                reporter.add_custom(
+                                    ReportMetric::new("remote_signal_round_trip")
+                                        .with_field("value", receive_time_s - dispatch_time_s),
+                                );
+                            }
                         }
-                    }
-                }
+                    })
+                    .await
+                    .expect("signal receiver ended");
+                // }
path: scenarios/remote_signals/src/main.rs
user: login: ThetaSinner
body: ```suggestion
```
created_at: 2025-04-28T17:59:25Z
author_association: MEMBER
diff_hunk: @@ -42,122 +40,110 @@ impl Default for ScenarioValues {
 
 impl UserValuesConstraint for ScenarioValues {}
 
+fn setup(ctx: &mut RunnerContext<HolochainRunnerContext>) -> HookResult {
+    configure_app_ws_url(ctx)?;
+    Ok(())
+}
+
 fn agent_setup(
-    ctx: &mut AgentContext<TryCPRunnerContext, TryCPAgentContext<ScenarioValues>>,
+    ctx: &mut AgentContext<HolochainRunnerContext, HolochainAgentContext<ScenarioValues>>,
 ) -> HookResult {
-    connect_trycp_client(ctx)?;
-    reset_trycp_remote(ctx)?;
+    install_app(
+        ctx,
+        scenario_happ_path!("remote_signal"),
+        &"remote_signal".to_string(),
+    )?;
 
     let pending_set = ctx.get().scenario_values.pending_set.clone();
-
-    let client = ctx.get().trycp_client();
-    let agent_name = ctx.agent_name().to_string();
     let reporter = ctx.runner_context().reporter();
+    let agent_index = ctx.agent_index();
+    let client = ctx.get().app_client();
 
     ctx.runner_context()
         .executor()
         .execute_in_place(async move {
-            client
-                .configure_player(agent_name.clone(), conductor_config().to_string(), None)
-                .await?;
-
-            client.startup(agent_name.clone(), None).await?;
-
             let _rcv_task = tokio::task::spawn(async move {
-                loop {
-                    let msg: Signal = ExternIO(
-                        client
-                            .recv_signal()
-                            .await
-                            .expect("signal receiver ended")
-                            .data,
-                    )
-                    .decode()
-                    .unwrap();
-                    let msg = match msg {
-                        Signal::App { signal, .. } => {
-                            let msg: Vec<u8> = signal.into_inner().decode().unwrap();
-                            let msg: TimedMessage = ExternIO(msg).decode().unwrap();
-                            msg
-                        }
-                        _ => continue,
-                    };
-
-                    pending_set.lock().unwrap().remove(&msg.to_request());
-
-                    match msg {
-                        TimedMessage::TimedRequest { .. } => (),
-                        TimedMessage::TimedResponse { requested_at, .. } => {
-                            let dispatch_time_s = requested_at.as_micros() as f64 / 1_000_000.0;
-                            let receive_time_s = Timestamp::now().as_micros() as f64 / 1_000_000.0;
-
-                            reporter.add_custom(
-                                ReportMetric::new("remote_signal_round_trip")
-                                    .with_field("value", receive_time_s - dispatch_time_s),
-                            );
+                // loop {
+                client
+                    .on_signal(move |signal| {
+                        println!("agent {:?} received a signal", agent_index);
path: scenarios/remote_signals/src/main.rs
user: login: jost-s
body: I knew there was a reason why I had this as draft =D
created_at: 2025-04-28T18:02:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,122 +40,110 @@ impl Default for ScenarioValues {
 
 impl UserValuesConstraint for ScenarioValues {}
 
+fn setup(ctx: &mut RunnerContext<HolochainRunnerContext>) -> HookResult {
+    configure_app_ws_url(ctx)?;
+    Ok(())
+}
+
 fn agent_setup(
-    ctx: &mut AgentContext<TryCPRunnerContext, TryCPAgentContext<ScenarioValues>>,
+    ctx: &mut AgentContext<HolochainRunnerContext, HolochainAgentContext<ScenarioValues>>,
 ) -> HookResult {
-    connect_trycp_client(ctx)?;
-    reset_trycp_remote(ctx)?;
+    install_app(
+        ctx,
+        scenario_happ_path!("remote_signal"),
+        &"remote_signal".to_string(),
+    )?;
 
     let pending_set = ctx.get().scenario_values.pending_set.clone();
-
-    let client = ctx.get().trycp_client();
-    let agent_name = ctx.agent_name().to_string();
     let reporter = ctx.runner_context().reporter();
+    let agent_index = ctx.agent_index();
+    let client = ctx.get().app_client();
 
     ctx.runner_context()
         .executor()
         .execute_in_place(async move {
-            client
-                .configure_player(agent_name.clone(), conductor_config().to_string(), None)
-                .await?;
-
-            client.startup(agent_name.clone(), None).await?;
-
             let _rcv_task = tokio::task::spawn(async move {
-                loop {
-                    let msg: Signal = ExternIO(
-                        client
-                            .recv_signal()
-                            .await
-                            .expect("signal receiver ended")
-                            .data,
-                    )
-                    .decode()
-                    .unwrap();
-                    let msg = match msg {
-                        Signal::App { signal, .. } => {
-                            let msg: Vec<u8> = signal.into_inner().decode().unwrap();
-                            let msg: TimedMessage = ExternIO(msg).decode().unwrap();
-                            msg
-                        }
-                        _ => continue,
-                    };
-
-                    pending_set.lock().unwrap().remove(&msg.to_request());
-
-                    match msg {
-                        TimedMessage::TimedRequest { .. } => (),
-                        TimedMessage::TimedResponse { requested_at, .. } => {
-                            let dispatch_time_s = requested_at.as_micros() as f64 / 1_000_000.0;
-                            let receive_time_s = Timestamp::now().as_micros() as f64 / 1_000_000.0;
-
-                            reporter.add_custom(
-                                ReportMetric::new("remote_signal_round_trip")
-                                    .with_field("value", receive_time_s - dispatch_time_s),
-                            );
+                // loop {
+                client
+                    .on_signal(move |signal| {
+                        println!("agent {:?} received a signal", agent_index);
path: scenarios/remote_signals/src/main.rs
user: login: jost-s
body: Now it's ready
created_at: 2025-04-28T18:04:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -8,5 +8,5 @@ function is not calling into Holochain other otherwise doing any work.
 ### Suggested command
 
 ```bash
-RUST_LOG=info cargo run --package zome_call_single_value -- --connection-string ws://localhost:8888 --duration 300
+RUST_LOG=warn cargo run --package zome_call_single_value -- --connection-string ws://localhost:8888 --duration 300
path: scenarios/zome_call_single_value/README.md
user: login: ThetaSinner
body: Don't feel strongly about this but when running locally, I'm generally happy with more information. But at info I usually end up tuning this anyway to get the modules I want logs from, so the default is use a placeholder anyway :)
created_at: 2025-04-28T18:06:05Z
author_association: MEMBER
diff_hunk: @@ -8,5 +8,5 @@ function is not calling into Holochain other otherwise doing any work.
 ### Suggested command
 
 ```bash
-RUST_LOG=info cargo run --package zome_call_single_value -- --connection-string ws://localhost:8888 --duration 300
+RUST_LOG=warn cargo run --package zome_call_single_value -- --connection-string ws://localhost:8888 --duration 300
path: scenarios/zome_call_single_value/README.md
user: login: jost-s
body: I absolutely don't care what's there. I just copied this across. I'll change it again.
created_at: 2025-04-28T18:46:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -239,20 +239,14 @@ jobs:
 
       - name: Smoke test - remote_signals
         run: |
-          set -x
-
-          # Start local network services
-          nix develop .#ci -c bash -c "hc-run-local-services --bootstrap-port 4422 --signal-port 4423 &"
-          # Start a TryCP instance
-          nix develop .#ci -c bash -c "source ./scripts/trycp.sh && start_trycp &"
+          # Start a sandbox conductor and run it in the background
+          nix develop .#ci -c bash -c "hc s clean && echo "1234" | hc s --piped create && echo "1234" | hc s --piped -f 8888 run &"
 
           # Run the scenario
-          RUST_LOG=warn CONDUCTOR_CONFIG="CI" MIN_PEERS=2 nix run .#remote_signals -- --targets targets-ci.yaml --instances-per-target 2 --duration 30 --no-progress
+          RUST_LOG=warn MIN_AGENTS=2 cargo run -p remote_signals -- --connection-string ws://localhost:8888 --agents 2 --duration 500 --no-progress
path: .github/workflows/test.yaml
user: login: cdunster
body: Why a duration of 500? I thought that running the scenarios on a single GH runner (i.e. the `test` workflow) should be quite quick. Also, why specifically 500? That's what, like 8 minutes and 20 seconds, seems random :grin:
created_at: 2025-04-29T07:01:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -239,20 +239,14 @@ jobs:
 
       - name: Smoke test - remote_signals
         run: |
-          set -x
-
-          # Start local network services
-          nix develop .#ci -c bash -c "hc-run-local-services --bootstrap-port 4422 --signal-port 4423 &"
-          # Start a TryCP instance
-          nix develop .#ci -c bash -c "source ./scripts/trycp.sh && start_trycp &"
+          # Start a sandbox conductor and run it in the background
+          nix develop .#ci -c bash -c "hc s clean && echo "1234" | hc s --piped create && echo "1234" | hc s --piped -f 8888 run &"
 
           # Run the scenario
-          RUST_LOG=warn CONDUCTOR_CONFIG="CI" MIN_PEERS=2 nix run .#remote_signals -- --targets targets-ci.yaml --instances-per-target 2 --duration 30 --no-progress
+          RUST_LOG=warn MIN_AGENTS=2 cargo run -p remote_signals -- --connection-string ws://localhost:8888 --agents 2 --duration 500 --no-progress
path: .github/workflows/test.yaml
user: login: jost-s
body: I have no idea. What are you asking me these things for? :joy: I must have copied that from somewhere while being mentally absent.
created_at: 2025-04-29T11:37:06Z
author_association: CONTRIBUTOR

PR comments:
user: login: cocogitto-bot[bot]
created_at: 2025-04-29T14:29:08Z
body: :heavy_check_mark: def31c594dea971b4dce7fe2f7bd5ab1d0ef22bf - Conventional commits check succeeded.
author_association: NONE