BREAKING: 3-tier split step 2 + engine-bridge v3 EgBim lambdas wired

Completes the Generic / HmEG-aware / App-specific separation started in
f6b6e74. The legacy EgPlugin / EngineBridge / EngineBridge.Client /
EngineBridge.Probe modules are moved into their proper tiers, namespaces
and csproj/sln entries are renamed, and the HmegDirectStateProvider
lambdas are finally populated with real handles from the EgBim plugin
host. A new Recordingtest.Architecture.Tests project enforces the tier
rule at build time.

Moves (git mv + csproj/RootNamespace/AssemblyName rename + sln):

  src/Recordingtest.EgPlugin
    -> src/Sut/EgBim/Recordingtest.Sut.EgBim.PluginHost
  src/Recordingtest.EngineBridge
    -> src/Hmeg/Recordingtest.Hmeg.Catalog
  src/Recordingtest.EngineBridge.Client
    -> src/Hmeg/Recordingtest.Hmeg.Bridge.Client
  src/Recordingtest.EngineBridge.Probe
    -> src/Hmeg/Recordingtest.Hmeg.Catalog.Probe

  tests/Recordingtest.EgPlugin.Tests
    -> tests/Sut/EgBim/Recordingtest.Sut.EgBim.PluginHost.Tests
  tests/Recordingtest.EngineBridge.Tests
    -> tests/Hmeg/Recordingtest.Hmeg.Catalog.Tests
  tests/Recordingtest.EngineBridge.IntegrationTests
    -> tests/Hmeg/Recordingtest.Hmeg.Catalog.IntegrationTests

Namespace rename applied across all .cs files and csproj RootNamespace:

  Recordingtest.EgPlugin           -> Recordingtest.Sut.EgBim.PluginHost
  Recordingtest.EngineBridge       -> Recordingtest.Hmeg.Catalog
  Recordingtest.EngineBridge.Client -> Recordingtest.Hmeg.Bridge.Client
  Recordingtest.EngineBridge.Probe -> Recordingtest.Hmeg.Catalog.Probe

New: tests/Recordingtest.Architecture.Tests/

  DependencyGraphTests walks Assembly.GetReferencedAssemblies() for each
  tier and fails if a forbidden reference leaks in:
    - Generic modules must not reference HmEG or any app-specific DLL
    - HmEG-aware modules must not reference app-specific DLLs
    - Recordingtest.Hmeg.Bridge must reference HmEG (positive check)
  11 tests, all passing. Prevents future drift from CLAUDE.md §8.1.

Engine-bridge v3 wire-up (HmEgBridgePlugin.BuildProvider):

  Previously the HmegDirectStateProvider lambdas returned null and the
  chain fell through to reflection. They now call directly into the
  EditorPlugin base class that HmEgBridgePlugin inherits:

    spaceProvider    = () => RootSpace
                           // AppManager.ViewportManager.RootSpace
    viewportProvider = () => View
                           // EGViewport : Control, HmEGViewport
    documentPathProvider = () => AppManager?.FileManager?.CurrentFile

  Every lambda is wrapped in try/catch so plugin construction still
  cannot throw back into the SUT. Editor02.HmEGAppManager.dll added as
  a reference on Recordingtest.Sut.EgBim.PluginHost.csproj — app-
  specific tier, which is allowed by the architecture tests.

Entry points were confirmed from read-only review of the SUT sources at
  D:\GiteaAll\EG-BIM_Modeler\EditorPluginInterface\EditorPlugin.cs
  D:\GiteaAll\EG-BIM_Modeler\HmEGApplicationManagementLibrary\HmEGAppManager.cs
  D:\GiteaAll\EG-BIM_Modeler\HmEGApplicationManagementLibrary\SubManager\FileManager.cs

closing out Q1/Q2/Q6/Q7 from docs/hmeg-api-survey.md.

Tests: 115 -> 126 (+11 Architecture), 0 failures.

Next step: live verification of /scene /camera /selection with a real
SUT session; any discrepancy in HmegDirectStateProvider reflection will
be tightened after observing real HmEG camera field names.

Ref: #10 follow-up, #14 follow-up, docs/contracts/generic-sut-split.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
minsung
2026-04-09 10:39:13 +09:00
parent f6b6e7449e
commit 03fb504eea
36 changed files with 542 additions and 206 deletions

View File

@@ -2,7 +2,7 @@ using System.Net;
using System.Net.Sockets;
using System.Text;
namespace Recordingtest.EngineBridge.IntegrationTests;
namespace Recordingtest.Hmeg.Catalog.IntegrationTests;
public sealed class FakeBridgeServer : IDisposable
{

View File

@@ -1,7 +1,7 @@
using Recordingtest.EngineBridge.Client;
using Recordingtest.Hmeg.Bridge.Client;
using Xunit;
namespace Recordingtest.EngineBridge.IntegrationTests;
namespace Recordingtest.Hmeg.Catalog.IntegrationTests;
public class HmEgHttpSnapshotTests
{

View File

@@ -5,7 +5,8 @@
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
<RootNamespace>Recordingtest.EngineBridge.IntegrationTests</RootNamespace>
<RootNamespace>Recordingtest.Hmeg.Catalog.IntegrationTests</RootNamespace>
<AssemblyName>Recordingtest.Hmeg.Catalog.IntegrationTests</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
@@ -13,6 +14,6 @@
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Recordingtest.EngineBridge.Client\Recordingtest.EngineBridge.Client.csproj" />
<ProjectReference Include="..\..\..\src\Hmeg\Recordingtest.Hmeg.Bridge.Client\Recordingtest.Hmeg.Bridge.Client.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,9 +1,9 @@
using System.Reflection;
using System.Text.Json;
using Recordingtest.EngineBridge;
using Recordingtest.Hmeg.Catalog;
using Xunit;
namespace Recordingtest.EngineBridge.Tests;
namespace Recordingtest.Hmeg.Catalog.Tests;
public sealed class EngineBridgeTests
{

View File

@@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPackable>false</IsPackable>
<RootNamespace>Recordingtest.EngineBridge.Tests</RootNamespace>
<AssemblyName>Recordingtest.EngineBridge.Tests</AssemblyName>
<RootNamespace>Recordingtest.Hmeg.Catalog.Tests</RootNamespace>
<AssemblyName>Recordingtest.Hmeg.Catalog.Tests</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
@@ -10,6 +10,6 @@
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Recordingtest.EngineBridge\Recordingtest.EngineBridge.csproj" />
<ProjectReference Include="..\..\..\src\Hmeg\Recordingtest.Hmeg.Catalog\Recordingtest.Hmeg.Catalog.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,115 @@
using System.IO;
using System.Reflection;
using Xunit;
namespace Recordingtest.Architecture.Tests;
/// <summary>
/// Enforces the 3-tier separation rule from CLAUDE.md §8.1:
///
/// Generic → no SUT assembly references at all
/// HmEG-aware → HmEG.dll only, no per-app assembly
/// App-specific → anything (not checked here; not referenced by this project)
///
/// These tests walk <see cref="Assembly.GetReferencedAssemblies"/> on each
/// compiled output and fail if a forbidden reference appears. The test
/// project itself does NOT reference any Sut/* project, so a physical build
/// error would catch accidental upward references even before these tests
/// run — this file is the explicit contract.
/// </summary>
public class DependencyGraphTests
{
private static readonly string[] ForbiddenAppAssemblies =
{
"Editor03.PluginInterface",
"Editor02.HmEGAppManager",
"EditorCore",
};
private const string HmegAssembly = "HmEG";
private static readonly string[] GenericAssemblies =
{
"Recordingtest.Bridge.Abstractions",
"Recordingtest.Recorder",
"Recordingtest.Player",
"Recordingtest.Normalizer",
"Recordingtest.DiffReporter",
"Recordingtest.Runner",
"Recordingtest.SutProber",
};
private static readonly string[] HmegAwareAssemblies =
{
"Recordingtest.Hmeg.Bridge",
"Recordingtest.Hmeg.Catalog",
"Recordingtest.Hmeg.Bridge.Client",
};
[Theory]
[InlineData("Recordingtest.Bridge.Abstractions")]
[InlineData("Recordingtest.Recorder")]
[InlineData("Recordingtest.Player")]
[InlineData("Recordingtest.Normalizer")]
[InlineData("Recordingtest.DiffReporter")]
[InlineData("Recordingtest.Runner")]
[InlineData("Recordingtest.SutProber")]
public void Generic_Tier_Does_Not_Reference_Hmeg_Or_AppAssemblies(string assemblyName)
{
var refs = LoadReferences(assemblyName);
Assert.DoesNotContain(HmegAssembly, refs);
foreach (var app in ForbiddenAppAssemblies)
{
Assert.DoesNotContain(app, refs);
}
}
[Theory]
[InlineData("Recordingtest.Hmeg.Bridge")]
[InlineData("Recordingtest.Hmeg.Catalog")]
[InlineData("Recordingtest.Hmeg.Bridge.Client")]
public void HmegAware_Tier_Does_Not_Reference_AppAssemblies(string assemblyName)
{
var refs = LoadReferences(assemblyName);
foreach (var app in ForbiddenAppAssemblies)
{
Assert.DoesNotContain(app, refs);
}
}
[Fact]
public void Hmeg_Bridge_References_HmEG_Dll()
{
// HmEG-aware tier is expected to reference HmEG.dll. This is the
// positive check that pairs with the App-specific forbidden list.
var refs = LoadReferences("Recordingtest.Hmeg.Bridge");
Assert.Contains(HmegAssembly, refs);
}
private static IReadOnlySet<string> LoadReferences(string assemblyName)
{
// Locate the assembly via a type we know belongs to it. Each tier
// member has at least one public type; we use GenericAssemblies/
// HmegAwareAssemblies as lookups by name.
Assembly? asm = null;
foreach (var loaded in AppDomain.CurrentDomain.GetAssemblies())
{
if (loaded.GetName().Name == assemblyName)
{
asm = loaded;
break;
}
}
if (asm is null)
{
// Force-load by trying to resolve a type: the test project has a
// ProjectReference to the target, so the binary is next to ours.
var dir = Path.GetDirectoryName(typeof(DependencyGraphTests).Assembly.Location)!;
var path = Path.Combine(dir, assemblyName + ".dll");
asm = Assembly.LoadFrom(path);
}
return new HashSet<string>(
asm.GetReferencedAssemblies().Select(n => n.Name ?? string.Empty),
StringComparer.Ordinal);
}
}

View File

@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
<RootNamespace>Recordingtest.Architecture.Tests</RootNamespace>
<AssemblyName>Recordingtest.Architecture.Tests</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<!-- Reference every Generic-tier assembly so we can inspect its dependency
graph at test time. Add HmEG-aware here too; do NOT reference any
Sut/* project or app-specific DLL. -->
<ProjectReference Include="..\..\src\Recordingtest.Bridge.Abstractions\Recordingtest.Bridge.Abstractions.csproj" />
<ProjectReference Include="..\..\src\Recordingtest.Recorder\Recordingtest.Recorder.csproj" />
<ProjectReference Include="..\..\src\Recordingtest.Player\Recordingtest.Player.csproj" />
<ProjectReference Include="..\..\src\Recordingtest.Normalizer\Recordingtest.Normalizer.csproj" />
<ProjectReference Include="..\..\src\Recordingtest.DiffReporter\Recordingtest.DiffReporter.csproj" />
<ProjectReference Include="..\..\src\Recordingtest.Runner\Recordingtest.Runner.csproj" />
<ProjectReference Include="..\..\src\Recordingtest.SutProber\Recordingtest.SutProber.csproj" />
<ProjectReference Include="..\..\src\Hmeg\Recordingtest.Hmeg.Bridge\Recordingtest.Hmeg.Bridge.csproj" />
<ProjectReference Include="..\..\src\Hmeg\Recordingtest.Hmeg.Catalog\Recordingtest.Hmeg.Catalog.csproj" />
<ProjectReference Include="..\..\src\Hmeg\Recordingtest.Hmeg.Bridge.Client\Recordingtest.Hmeg.Bridge.Client.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,8 +1,8 @@
using Recordingtest.Bridge;
using Recordingtest.EgPlugin;
using Recordingtest.Sut.EgBim.PluginHost;
using Xunit;
namespace Recordingtest.EgPlugin.Tests;
namespace Recordingtest.Sut.EgBim.PluginHost.Tests;
public class ChainedEngineStateProviderTests
{

View File

@@ -5,7 +5,8 @@
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
<RootNamespace>Recordingtest.EgPlugin.Tests</RootNamespace>
<RootNamespace>Recordingtest.Sut.EgBim.PluginHost.Tests</RootNamespace>
<AssemblyName>Recordingtest.Sut.EgBim.PluginHost.Tests</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
@@ -13,6 +14,7 @@
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Recordingtest.EgPlugin\Recordingtest.EgPlugin.csproj" />
<ProjectReference Include="..\..\..\..\src\Sut\EgBim\Recordingtest.Sut.EgBim.PluginHost\Recordingtest.Sut.EgBim.PluginHost.csproj" />
<ProjectReference Include="..\..\..\..\src\Recordingtest.Bridge.Abstractions\Recordingtest.Bridge.Abstractions.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,8 +1,8 @@
using Recordingtest.Bridge;
using Recordingtest.EgPlugin;
using Recordingtest.Sut.EgBim.PluginHost;
using Xunit;
namespace Recordingtest.EgPlugin.Tests;
namespace Recordingtest.Sut.EgBim.PluginHost.Tests;
public class ReflectionEngineStateProviderTests
{

View File

@@ -1,9 +1,9 @@
using System.Net;
using Recordingtest.Bridge;
using Recordingtest.EgPlugin;
using Recordingtest.Sut.EgBim.PluginHost;
using Xunit;
namespace Recordingtest.EgPlugin.Tests;
namespace Recordingtest.Sut.EgBim.PluginHost.Tests;
public class StateRouterTests
{