diff --git a/src/tests/TeamsISO.Engine.IntegrationTests/IntegrationTestsScaffold.cs b/src/tests/TeamsISO.Engine.IntegrationTests/IntegrationTestsScaffold.cs
index 6d33330..05ebdf6 100644
--- a/src/tests/TeamsISO.Engine.IntegrationTests/IntegrationTestsScaffold.cs
+++ b/src/tests/TeamsISO.Engine.IntegrationTests/IntegrationTestsScaffold.cs
@@ -1,11 +1,124 @@
+using System.Runtime.Versioning;
+using FluentAssertions;
+using Microsoft.Extensions.Logging.Abstractions;
+using TeamsISO.Engine.Interop;
+using TeamsISO.Engine.NdiInterop;
+
namespace TeamsISO.Engine.IntegrationTests;
-public class IntegrationTestsScaffold
+///
+/// Integration tests that talk to a real NDI runtime — gated behind
+/// requires=ndi so the default CI run skips them. Run locally with:
+/// dotnet test --filter requires=ndi
+///
+/// Pre-conditions:
+/// - Windows host
+/// - NDI 6 Runtime installed (NDI_RUNTIME_DIR_V6 set)
+/// - Network discovery permitted on the loopback / local subnet
+///
+[SupportedOSPlatform("windows")]
+public class NdiInteropIntegrationTests
{
- [Fact(Skip = "Phase A: integration tests require NDI runtime — added in Phase B.")]
+ private static NdiInteropPInvoke NewInterop() =>
+ new(NullLogger.Instance);
+
+ [Fact]
[Trait("requires", "ndi")]
- public void ScaffoldFactSkipsCleanly()
+ public void NdiRuntime_LoadsAndReportsVersion()
{
- Assert.True(true);
+ using var interop = NewInterop();
+
+ var version = interop.GetRuntimeVersion();
+
+ version.Should().NotBeNullOrEmpty();
+ version.Should().StartWith(NdiVersion.ExpectedRuntimeVersionPrefix,
+ because: "the engine probe asserts this prefix; if it ever drifts CI must catch it");
+ }
+
+ [Fact]
+ [Trait("requires", "ndi")]
+ public void Finder_DefaultGroups_CreatesAndDisposesCleanly()
+ {
+ using var interop = NewInterop();
+
+ using (var finder = interop.CreateFinder())
+ {
+ finder.Should().NotBeNull(because: "default-group finder must construct successfully");
+ // Snapshot any visible sources — exercises the path; we don't assert on count
+ // because the test environment's NDI sources are unknowable.
+ _ = interop.GetCurrentSources(finder);
+ }
+ }
+
+ [Theory]
+ [Trait("requires", "ndi")]
+ [InlineData("teamsiso-test-input")]
+ [InlineData("teamsiso-test-input,production")]
+ [InlineData(" teamsiso-test-input ")]
+ public void Finder_CustomGroups_DoesNotThrow(string groups)
+ {
+ using var interop = NewInterop();
+
+ var act = () =>
+ {
+ using var finder = interop.CreateFinder(groups);
+ _ = interop.GetCurrentSources(finder);
+ };
+
+ act.Should().NotThrow(because: $"groups='{groups}' must round-trip into NDIlib_find_create_v2 cleanly");
+ }
+
+ [Fact]
+ [Trait("requires", "ndi")]
+ public void Sender_DefaultGroups_CreatesAndDisposesCleanly()
+ {
+ using var interop = NewInterop();
+
+ using (var sender = interop.CreateSender("TEAMSISO_TEST_DEFAULT"))
+ {
+ sender.Should().NotBeNull();
+ }
+ }
+
+ [Fact]
+ [Trait("requires", "ndi")]
+ public void Sender_CustomGroups_CreatesAndDisposesCleanly()
+ {
+ using var interop = NewInterop();
+
+ using (var sender = interop.CreateSender("TEAMSISO_TEST_GROUPED", "teamsiso-test-output"))
+ {
+ sender.Should().NotBeNull();
+ }
+ }
+
+ [Fact]
+ [Trait("requires", "ndi")]
+ public async Task LoopbackDiscovery_FindsOurOwnSenderWithinFiveSeconds()
+ {
+ // End-to-end check: a sender we create on the local machine must be visible
+ // to a finder running in the same process, within a reasonable window. Catches
+ // network-layer regressions (firewall, mDNS, multicast disable).
+ using var interop = NewInterop();
+
+ var uniqueName = $"TEAMSISO_LOOP_{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
+ using var sender = interop.CreateSender(uniqueName);
+ using var finder = interop.CreateFinder();
+
+ var deadline = DateTime.UtcNow.AddSeconds(5);
+ bool found = false;
+ while (DateTime.UtcNow < deadline)
+ {
+ var sources = interop.GetCurrentSources(finder);
+ if (sources.Any(s => s.Contains(uniqueName, StringComparison.Ordinal)))
+ {
+ found = true;
+ break;
+ }
+ await Task.Delay(250);
+ }
+
+ found.Should().BeTrue(
+ because: $"local sender '{uniqueName}' must be discovered by a same-process finder within 5s");
}
}