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

@@ -0,0 +1,149 @@
using Recordingtest.Bridge;
using Recordingtest.Sut.EgBim.PluginHost;
using Xunit;
namespace Recordingtest.Sut.EgBim.PluginHost.Tests;
public class ReflectionEngineStateProviderTests
{
private sealed class FakeAccessor : IAppManagerAccessor
{
public object? AppManager { get; set; } = new object();
public object? ActiveDocument { get; set; }
public object? ActiveViewport { get; set; }
public IReadOnlyList<string> SelectedIds { get; set; } = Array.Empty<string>();
public int ObjectCount { get; set; }
public string? DocumentPath { get; set; }
public (double[] Eye, double[] Target, double[] Up, double Fov)? Camera { get; set; }
public bool ThrowOnSelection { get; set; }
public bool ThrowOnCamera { get; set; }
public bool ThrowOnScene { get; set; }
public object? GetAppManager() => AppManager;
public object? GetActiveDocument() => ActiveDocument;
public object? GetActiveViewport() => ActiveViewport;
public IReadOnlyList<string> GetSelectedIds()
{
if (ThrowOnSelection) throw new InvalidOperationException("boom");
return SelectedIds;
}
public int GetObjectCount()
{
if (ThrowOnScene) throw new InvalidOperationException("boom");
return ObjectCount;
}
public string? GetDocumentPath()
{
if (ThrowOnScene) throw new InvalidOperationException("boom");
return DocumentPath;
}
public (double[] Eye, double[] Target, double[] Up, double Fov)? GetCameraTuple()
{
if (ThrowOnCamera) throw new InvalidOperationException("boom");
return Camera;
}
}
[Fact]
public void GetSelectedIds_Returns_Accessor_Values()
{
var fa = new FakeAccessor { SelectedIds = new[] { "a", "b" } };
var p = new ReflectionEngineStateProvider(fa);
var ids = p.GetSelectedIds();
Assert.Equal(new[] { "a", "b" }, ids);
}
[Fact]
public void GetSelectedIds_Swallows_Exception_Returns_Empty()
{
var fa = new FakeAccessor { ThrowOnSelection = true };
var p = new ReflectionEngineStateProvider(fa);
Assert.Empty(p.GetSelectedIds());
}
[Fact]
public void GetCamera_Returns_Accessor_Tuple()
{
var fa = new FakeAccessor
{
Camera = (
Eye: new double[] { 1, 2, 3 },
Target: new double[] { 4, 5, 6 },
Up: new double[] { 0, 0, 1 },
Fov: 60.0)
};
var p = new ReflectionEngineStateProvider(fa);
var c = p.GetCamera();
Assert.Equal(new double[] { 1, 2, 3 }, c.Eye);
Assert.Equal(new double[] { 4, 5, 6 }, c.Target);
Assert.Equal(60.0, c.Fov);
}
[Fact]
public void GetCamera_Null_Tuple_Returns_Default()
{
var fa = new FakeAccessor { Camera = null };
var p = new ReflectionEngineStateProvider(fa);
var c = p.GetCamera();
Assert.Equal(45.0, c.Fov);
Assert.Equal(new double[] { 0, 0, 1 }, c.Up);
}
[Fact]
public void GetCamera_Throwing_Accessor_Returns_Default_Not_Throw()
{
var fa = new FakeAccessor { ThrowOnCamera = true };
var p = new ReflectionEngineStateProvider(fa);
var c = p.GetCamera();
Assert.Equal(45.0, c.Fov);
}
[Fact]
public void GetScene_Returns_Accessor_Values()
{
var fa = new FakeAccessor { ObjectCount = 42, DocumentPath = "C:/x.hmeg" };
var p = new ReflectionEngineStateProvider(fa);
var s = p.GetScene();
Assert.Equal(42, s.ObjectCount);
Assert.Equal("C:/x.hmeg", s.DocumentPath);
}
[Fact]
public void GetScene_Throwing_Accessor_Returns_Empty_Default()
{
var fa = new FakeAccessor { ThrowOnScene = true };
var p = new ReflectionEngineStateProvider(fa);
var s = p.GetScene();
Assert.Equal(0, s.ObjectCount);
Assert.Null(s.DocumentPath);
}
[Fact]
public void Default_Reflection_Accessor_Without_Hmeg_Returns_Null_Safely()
{
// CI process has no Editor.AppManager.AppManager type loaded; the
// accessor must return null/empty/default without throwing.
var acc = new ReflectionAppManagerAccessor(seedRoot: new object());
Assert.Null(acc.GetAppManager());
Assert.Null(acc.GetActiveDocument());
Assert.Null(acc.GetActiveViewport());
Assert.Empty(acc.GetSelectedIds());
Assert.Equal(0, acc.GetObjectCount());
Assert.Null(acc.GetDocumentPath());
Assert.Null(acc.GetCameraTuple());
}
[Fact]
public void Provider_With_Default_Accessor_Without_Hmeg_Returns_Defaults()
{
// End-to-end fallback: provider built from a non-HmEG seed must
// produce the v2 safe defaults so /state continues to respond.
var p = new ReflectionEngineStateProvider(seedRoot: new object());
Assert.Empty(p.GetSelectedIds());
var c = p.GetCamera();
Assert.Equal(45.0, c.Fov);
var s = p.GetScene();
Assert.Equal(0, s.ObjectCount);
Assert.True(p.GetRenderComplete());
}
}