diff --git a/test/InProcessTestHost.Tests/ExternalEventTests.cs b/test/InProcessTestHost.Tests/ExternalEventTests.cs
new file mode 100644
index 00000000..7cbce7be
--- /dev/null
+++ b/test/InProcessTestHost.Tests/ExternalEventTests.cs
@@ -0,0 +1,140 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.DurableTask;
+using Microsoft.DurableTask.Client;
+using Microsoft.DurableTask.Testing;
+using Microsoft.DurableTask.Worker;
+using Xunit;
+
+namespace InProcessTestHost.Tests;
+
+///
+/// Tests for external event delivery via ,
+/// including the canonical "wait for an event with a timeout" pattern built on and a
+/// durable timer.
+///
+///
+/// A durable timer keeps an orchestration in the "Running" state until it expires or is cancelled. As documented on
+/// , all durable timers
+/// must either expire or be cancelled via their before the orchestrator
+/// can complete. When a timer is used only as a timeout (for example, with ), cancel it
+/// once the other task wins so the orchestration completes promptly instead of waiting for the timer to fire. See
+/// https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-timers.
+///
+public class ExternalEventTests
+{
+ [Fact]
+ // A plain external event is delivered to a waiting orchestration and its payload is returned.
+ public async Task WaitForExternalEvent_IsDelivered()
+ {
+ const string orchestratorName = "WaitForEventOrchestrator";
+
+ await using DurableTaskTestHost host = await DurableTaskTestHost.StartAsync(tasks =>
+ {
+ tasks.AddOrchestratorFunc(orchestratorName, async ctx =>
+ {
+ return await ctx.WaitForExternalEvent("MyEvent");
+ });
+ });
+
+ string instanceId = await host.Client.ScheduleNewOrchestrationInstanceAsync(orchestratorName);
+
+ using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30));
+
+ // Wait for the orchestration to start before raising the event.
+ await host.Client.WaitForInstanceStartAsync(instanceId, cts.Token);
+ await host.Client.RaiseEventAsync(instanceId, "MyEvent", "hello");
+
+ OrchestrationMetadata metadata = await host.Client.WaitForInstanceCompletionAsync(
+ instanceId, getInputsAndOutputs: true, cts.Token);
+
+ Assert.NotNull(metadata);
+ Assert.Equal(OrchestrationRuntimeStatus.Completed, metadata.RuntimeStatus);
+ Assert.Equal("hello", metadata.ReadOutputAs());
+ }
+
+ [Fact]
+ // Canonical wait-for-event-with-timeout: when the event wins the Task.WhenAny, cancelling the durable timer lets the
+ // orchestration complete promptly, well before the 5-minute timer would fire.
+ public async Task WaitForExternalEvent_WithTimeout_EventWins()
+ {
+ const string orchestratorName = "EventWithTimeoutOrchestrator";
+
+ await using DurableTaskTestHost host = await DurableTaskTestHost.StartAsync(tasks =>
+ {
+ tasks.AddOrchestratorFunc(orchestratorName, async ctx =>
+ {
+ using CancellationTokenSource timerCts = new();
+ using CancellationTokenSource eventCts = new();
+ Task eventTask = ctx.WaitForExternalEvent("MyEvent", eventCts.Token);
+ Task timerTask = ctx.CreateTimer(TimeSpan.FromMinutes(5), timerCts.Token);
+ Task winner = await Task.WhenAny(eventTask, timerTask);
+ if (winner == eventTask)
+ {
+ // Cancel the still-pending durable timer so the orchestration can complete now.
+ timerCts.Cancel();
+ return await eventTask;
+ }
+
+ // Cancel the losing external-event wait, mirroring the SDK's WaitForExternalEvent(name, timeout) helper.
+ eventCts.Cancel();
+ return "timeout";
+ });
+ });
+
+ string instanceId = await host.Client.ScheduleNewOrchestrationInstanceAsync(orchestratorName);
+
+ using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30));
+
+ // Wait for the orchestration to start (and begin waiting on the event) before raising it,
+ // while the 5-minute timer is still pending.
+ await host.Client.WaitForInstanceStartAsync(instanceId, cts.Token);
+ await host.Client.RaiseEventAsync(instanceId, "MyEvent", "event");
+
+ OrchestrationMetadata metadata = await host.Client.WaitForInstanceCompletionAsync(
+ instanceId, getInputsAndOutputs: true, cts.Token);
+
+ Assert.NotNull(metadata);
+ Assert.Equal(OrchestrationRuntimeStatus.Completed, metadata.RuntimeStatus);
+ Assert.Equal("event", metadata.ReadOutputAs());
+ }
+
+ [Fact]
+ // Same pattern with a short timer and no event raised: the timer wins and the orchestration returns "timeout".
+ public async Task WaitForExternalEvent_WithTimeout_TimerWins()
+ {
+ const string orchestratorName = "TimeoutWinsOrchestrator";
+
+ await using DurableTaskTestHost host = await DurableTaskTestHost.StartAsync(tasks =>
+ {
+ tasks.AddOrchestratorFunc(orchestratorName, async ctx =>
+ {
+ using CancellationTokenSource timerCts = new();
+ using CancellationTokenSource eventCts = new();
+ Task eventTask = ctx.WaitForExternalEvent("MyEvent", eventCts.Token);
+ Task timerTask = ctx.CreateTimer(TimeSpan.FromSeconds(2), timerCts.Token);
+ Task winner = await Task.WhenAny(eventTask, timerTask);
+ if (winner == eventTask)
+ {
+ timerCts.Cancel();
+ return await eventTask;
+ }
+
+ // The timer won: cancel the losing external-event wait so no wait is left outstanding.
+ eventCts.Cancel();
+ return "timeout";
+ });
+ });
+
+ string instanceId = await host.Client.ScheduleNewOrchestrationInstanceAsync(orchestratorName);
+
+ using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30));
+ OrchestrationMetadata metadata = await host.Client.WaitForInstanceCompletionAsync(
+ instanceId, getInputsAndOutputs: true, cts.Token);
+
+ Assert.NotNull(metadata);
+ Assert.Equal(OrchestrationRuntimeStatus.Completed, metadata.RuntimeStatus);
+ Assert.Equal("timeout", metadata.ReadOutputAs());
+ }
+}