diff --git a/tests/Trax.Effect.Tests.Integration/Trax.Effect.Tests.Integration.csproj b/tests/Trax.Effect.Tests.Integration/Trax.Effect.Tests.Integration.csproj
index 1499469..afc0e90 100644
--- a/tests/Trax.Effect.Tests.Integration/Trax.Effect.Tests.Integration.csproj
+++ b/tests/Trax.Effect.Tests.Integration/Trax.Effect.Tests.Integration.csproj
@@ -15,6 +15,7 @@
+
diff --git a/tests/Trax.Effect.Tests.Integration/UnitTests/Services/EffectCoverageGapTests.cs b/tests/Trax.Effect.Tests.Integration/UnitTests/Services/EffectCoverageGapTests.cs
new file mode 100644
index 0000000..124b71a
--- /dev/null
+++ b/tests/Trax.Effect.Tests.Integration/UnitTests/Services/EffectCoverageGapTests.cs
@@ -0,0 +1,383 @@
+using System.Reflection;
+using FluentAssertions;
+using LanguageExt;
+using Microsoft.Extensions.DependencyInjection;
+using NSubstitute;
+using NUnit.Framework;
+using Trax.Core.Exceptions;
+using Trax.Core.Train;
+using Trax.Effect.Configuration.TraxEffectBuilder;
+using Trax.Effect.Data.InMemory.Extensions;
+using Trax.Effect.Extensions;
+using Trax.Effect.Models;
+using Trax.Effect.Models.Metadata;
+using Trax.Effect.Models.Metadata.DTOs;
+using Trax.Effect.Services.EffectJunction;
+using Trax.Effect.Services.EffectProvider;
+using Trax.Effect.Services.EffectProviderFactory;
+using Trax.Effect.Services.EffectRegistry;
+using Trax.Effect.Services.JunctionEffectProvider;
+using Trax.Effect.Services.JunctionEffectProviderFactory;
+using Trax.Effect.Services.JunctionEffectRunner;
+using Trax.Effect.Services.LifecycleHookRunner;
+using Trax.Effect.Services.ServiceTrain;
+using Trax.Effect.Services.TrainLifecycleHook;
+using Trax.Effect.Services.TrainLifecycleHookFactory;
+
+namespace Trax.Effect.Tests.Integration.UnitTests.Services;
+
+[TestFixture]
+public class EffectCoverageGapTests
+{
+ #region EffectJunction.RailwayJunction
+
+ [Test]
+ public async Task EffectJunction_RailwayJunction_NonServiceTrain_Throws()
+ {
+ var junction = new TestEffectJunction();
+ var nonServiceTrain = new NonServiceTrain();
+
+ Func act = async () =>
+ await junction.RailwayJunction(
+ Either.Right("in"),
+ nonServiceTrain
+ );
+
+ await act.Should().ThrowAsync().WithMessage("*non-ServiceTrain*");
+ }
+
+ [Test]
+ public async Task EffectJunction_RailwayJunction_NullMetadata_Throws()
+ {
+ var junction = new TestEffectJunction();
+ var train = new TestTrain();
+ // Leave Metadata null
+
+ Func act = async () =>
+ await junction.RailwayJunction(Either.Right("in"), train);
+
+ await act.Should().ThrowAsync().WithMessage("*Metadata cannot be null*");
+ }
+
+ [Test]
+ public async Task EffectJunction_RailwayJunction_HappyPath_PopulatesMetadata()
+ {
+ var junction = new TestEffectJunction();
+ var train = CreateTrain();
+
+ var result = await junction.RailwayJunction(
+ Either.Right("hello"),
+ train
+ );
+
+ result.IsRight.Should().BeTrue();
+ result.IfRight(v => v.Should().Be("hello-out"));
+ junction.Metadata.Should().NotBeNull();
+ junction.Metadata!.Name.Should().Be(nameof(TestEffectJunction));
+ junction.Metadata.HasRan.Should().BeTrue();
+ junction.Metadata.StartTimeUtc.Should().NotBeNull();
+ junction.Metadata.EndTimeUtc.Should().NotBeNull();
+ }
+
+ [Test]
+ public async Task EffectJunction_RailwayJunction_WithEffectRunner_CallsBeforeAndAfter()
+ {
+ var runner = Substitute.For();
+ var junction = new TestEffectJunction();
+ var train = CreateTrain(runner);
+
+ await junction.RailwayJunction(Either.Right("hi"), train);
+
+ await runner
+ .Received(1)
+ .BeforeJunctionExecution(
+ Arg.Any>(),
+ Arg.Any>(),
+ Arg.Any()
+ );
+ await runner
+ .Received(1)
+ .AfterJunctionExecution(
+ Arg.Any>(),
+ Arg.Any>(),
+ Arg.Any()
+ );
+ }
+
+ #endregion
+
+ #region JunctionEffectRunner.Before/AfterJunctionExecution
+
+ [Test]
+ public async Task JunctionEffectRunner_BeforeAndAfter_DispatchToActiveProviders()
+ {
+ var provider = Substitute.For();
+ var factory = Substitute.For();
+ factory.Create().Returns(provider);
+
+ var registry = Substitute.For();
+ registry.IsEnabled(Arg.Any()).Returns(true);
+
+ using var runner = new JunctionEffectRunner(new[] { factory }, registry);
+
+ var junction = new TestEffectJunction();
+ var train = CreateTrain();
+ var ct = CancellationToken.None;
+
+ await runner.BeforeJunctionExecution(junction, train, ct);
+ await runner.AfterJunctionExecution(junction, train, ct);
+
+ await provider.Received(1).BeforeJunctionExecution(junction, train, ct);
+ await provider.Received(1).AfterJunctionExecution(junction, train, ct);
+ }
+
+ #endregion
+
+ #region LifecycleHookRunner.OnStateChanged exception path
+
+ [Test]
+ public async Task LifecycleHookRunner_OnStateChanged_HookThrows_LogsAndContinues()
+ {
+ var hook1 = Substitute.For();
+ hook1
+ .OnStateChanged(Arg.Any(), Arg.Any())
+ .Returns(_ => throw new InvalidOperationException("hook1 fail"));
+ var hook2 = Substitute.For();
+
+ var f1 = Substitute.For();
+ f1.Create().Returns(hook1);
+ var f2 = Substitute.For();
+ f2.Create().Returns(hook2);
+
+ var registry = Substitute.For();
+ registry.IsEnabled(Arg.Any()).Returns(true);
+
+ using var runner = new LifecycleHookRunner(new[] { f1, f2 }, registry);
+
+ var metadata = CreateMetadata();
+
+ // Should not throw — hook1 fails, hook2 still runs.
+ await runner.OnStateChanged(metadata, CancellationToken.None);
+
+ await hook2.Received(1).OnStateChanged(metadata, Arg.Any());
+ }
+
+ #endregion
+
+ #region ServiceExtensions — AddEffect / AddJunctionEffect / AddLifecycleHook overloads
+
+ private static IServiceCollection ServicesWithEffectsConfigured(
+ Action configure
+ )
+ {
+ var services = new ServiceCollection();
+ services.AddLogging();
+ services.AddTrax(trax =>
+ trax.AddEffects(effects =>
+ {
+ effects.UseInMemory();
+ configure(effects);
+ return effects;
+ })
+ );
+ return services;
+ }
+
+ [Test]
+ public void AddEffect_WithFactoryInstance_RegistersFactoryAndRegistry()
+ {
+ var fakeFactory = new FakeEffectProviderFactory();
+
+ var services = ServicesWithEffectsConfigured(b => b.AddEffect(fakeFactory));
+
+ services.Should().Contain(d => d.ServiceType == typeof(IEffectProviderFactory));
+ using var provider = services.BuildServiceProvider();
+ var registry = provider.GetRequiredService();
+ registry.IsEnabled(typeof(FakeEffectProviderFactory)).Should().BeTrue();
+ registry.IsToggleable(typeof(FakeEffectProviderFactory)).Should().BeTrue();
+ }
+
+ [Test]
+ public void AddJunctionEffect_WithIAndConcreteFactory_RegistersAllThreeServiceTypes()
+ {
+ var instance = new FakeJunctionEffectProviderFactory();
+
+ var services = ServicesWithEffectsConfigured(b =>
+ b.AddJunctionEffect<
+ IFakeJunctionEffectProviderFactory,
+ FakeJunctionEffectProviderFactory
+ >(instance)
+ );
+
+ services.Should().Contain(d => d.ServiceType == typeof(FakeJunctionEffectProviderFactory));
+ services.Should().Contain(d => d.ServiceType == typeof(IJunctionEffectProviderFactory));
+ services.Should().Contain(d => d.ServiceType == typeof(IFakeJunctionEffectProviderFactory));
+ }
+
+ [Test]
+ public void AddJunctionEffect_TypeOnly_RegistersConcreteAndInterface()
+ {
+ var services = ServicesWithEffectsConfigured(b =>
+ b.AddJunctionEffect()
+ );
+
+ services.Should().Contain(d => d.ServiceType == typeof(FakeJunctionEffectProviderFactory));
+ services.Should().Contain(d => d.ServiceType == typeof(IJunctionEffectProviderFactory));
+ }
+
+ [Test]
+ public void AddJunctionEffect_TypeOnly_WithFactoryInstance_RegistersInterface()
+ {
+ var instance = new FakeJunctionEffectProviderFactory();
+
+ var services = ServicesWithEffectsConfigured(b => b.AddJunctionEffect(instance));
+
+ services.Should().Contain(d => d.ServiceType == typeof(IJunctionEffectProviderFactory));
+ }
+
+ [Test]
+ public void AddLifecycleHook_WithIAndConcreteFactory_RegistersAllThreeServiceTypes()
+ {
+ var factory = new FakeLifecycleHookFactory();
+
+ var services = ServicesWithEffectsConfigured(b =>
+ b.AddLifecycleHook(factory)
+ );
+
+ services.Should().Contain(d => d.ServiceType == typeof(FakeLifecycleHookFactory));
+ services.Should().Contain(d => d.ServiceType == typeof(ITrainLifecycleHookFactory));
+ services.Should().Contain(d => d.ServiceType == typeof(IFakeLifecycleHookFactory));
+ }
+
+ [Test]
+ public void AddLifecycleHook_WithFactoryInstance_RegistersFactory()
+ {
+ var factory = new FakeLifecycleHookFactory();
+
+ var services = ServicesWithEffectsConfigured(b => b.AddLifecycleHook(factory));
+
+ services.Should().Contain(d => d.ServiceType == typeof(ITrainLifecycleHookFactory));
+ }
+
+ [Test]
+ public void AddLifecycleHook_TypeIsNeitherHookNorFactory_Throws()
+ {
+ Action act = () => ServicesWithEffectsConfigured(b => b.AddLifecycleHook());
+
+ act.Should()
+ .Throw()
+ .WithMessage("*ITrainLifecycleHook or ITrainLifecycleHookFactory*");
+ }
+
+ [Test]
+ public void AddScopedTraxJunction_RuntimeTypes_RegistersInterfaceAsScoped()
+ {
+ var services = new ServiceCollection();
+ services.AddScopedTraxJunction(typeof(IFakeRoute), typeof(FakeRoute));
+
+ services
+ .Should()
+ .Contain(d =>
+ d.ServiceType == typeof(IFakeRoute) && d.Lifetime == ServiceLifetime.Scoped
+ );
+ }
+
+ [Test]
+ public void AddTransientTraxJunction_RuntimeTypes_RegistersInterfaceAsTransient()
+ {
+ var services = new ServiceCollection();
+ services.AddTransientTraxJunction(typeof(IFakeRoute), typeof(FakeRoute));
+
+ services
+ .Should()
+ .Contain(d =>
+ d.ServiceType == typeof(IFakeRoute) && d.Lifetime == ServiceLifetime.Transient
+ );
+ }
+
+ [Test]
+ public void AddSingletonTraxJunction_RuntimeTypes_RegistersInterfaceAsSingleton()
+ {
+ var services = new ServiceCollection();
+ services.AddSingletonTraxJunction(typeof(IFakeRoute), typeof(FakeRoute));
+
+ services
+ .Should()
+ .Contain(d =>
+ d.ServiceType == typeof(IFakeRoute) && d.Lifetime == ServiceLifetime.Singleton
+ );
+ }
+
+ #endregion
+
+ #region Test helpers / fakes
+
+ private static TestTrain CreateTrain(IJunctionEffectRunner? runner = null)
+ {
+ var train = new TestTrain();
+ if (runner is not null)
+ train.JunctionEffectRunner = runner;
+
+ var metadataProp = typeof(ServiceTrain).GetProperty(
+ "Metadata",
+ BindingFlags.Public | BindingFlags.Instance
+ );
+ metadataProp!.SetValue(train, CreateMetadata());
+
+ return train;
+ }
+
+ private static Metadata CreateMetadata() =>
+ Metadata.Create(
+ new CreateMetadata
+ {
+ Name = "TestTrain",
+ ExternalId = Guid.NewGuid().ToString("N"),
+ Input = null,
+ }
+ );
+
+ private class TestTrain : ServiceTrain
+ {
+ protected override Task> RunInternal(string input) =>
+ Task.FromResult>(input);
+ }
+
+ private class TestEffectJunction : EffectJunction
+ {
+ public override Task Run(string input) => Task.FromResult(input + "-out");
+ }
+
+ private class NonServiceTrain : Train
+ {
+ protected override Task> RunInternal(string input) =>
+ Task.FromResult>(input);
+ }
+
+ private class FakeEffectProviderFactory : IEffectProviderFactory
+ {
+ public IEffectProvider Create() => Substitute.For();
+ }
+
+ public interface IFakeJunctionEffectProviderFactory : IJunctionEffectProviderFactory;
+
+ public class FakeJunctionEffectProviderFactory : IFakeJunctionEffectProviderFactory
+ {
+ public IJunctionEffectProvider Create() => Substitute.For();
+ }
+
+ public interface IFakeLifecycleHookFactory : ITrainLifecycleHookFactory;
+
+ public class FakeLifecycleHookFactory : IFakeLifecycleHookFactory
+ {
+ public ITrainLifecycleHook Create() => Substitute.For();
+ }
+
+ public class RandomType { }
+
+ public interface IFakeRoute;
+
+ public class FakeRoute : IFakeRoute;
+
+ #endregion
+}