fix(inbound-api): surface compile warning on non-compiling method save

CompileAndRegister only swaps the cached handler on a successful compile, so a
management (or DB) save of a non-compiling inbound script silently kept the
previously-registered handler serving — stale results, still HTTP 200 — while
UpdateApiMethod reported success. A broken save thus looked like a no-op, with
the real Roslyn error only in the central log.

Surface the diagnostics instead: a new CompileAndRegister(method, out errors)
overload reports the compile/trust-model errors (the bool overload is kept for
existing callers), and HandleCreate/UpdateApiMethod return them as a top-level
compileWarning on the management result (null when the script compiled). The
save still persists and the previously registered version keeps serving — the
warning just stops a non-compiling save from looking like a success.

- InboundScriptExecutor: Compile returns its diagnostics alongside the handler.
- ManagementActor: TryCompileAndDescribe + flat ApiMethodResult projection so
  compileWarning rides at the top level without polluting the ApiMethod POCO.
- Component-InboundAPI.md: document the non-fatal-but-surfaced contract.
- Tests: executor diagnostics + non-compiling-update-keeps-previous-handler,
  plus an end-to-end ManagementActor test asserting compileWarning is returned.
This commit is contained in:
Joseph Doherty
2026-06-30 10:44:18 -04:00
parent 73ce2e4d0f
commit 67005ca4c0
5 changed files with 160 additions and 14 deletions
@@ -377,6 +377,37 @@ public class ManagementActorTests : TestKit, IDisposable
Assert.Contains("Designer", response.Message);
}
[Fact]
public void UpdateApiMethod_NonCompilingScript_SucceedsButReturnsCompileWarning()
{
// The save still persists, but a script that does not compile must surface a
// top-level compileWarning in the result rather than silently looking like a
// success — and the previously registered handler keeps serving (CompileAndRegister
// never replaces a working delegate with a broken one).
var existing = new Commons.Entities.InboundApi.ApiMethod("MyMethod", "return 1;")
{
Id = 7,
TimeoutSeconds = 10
};
var apiRepo = Substitute.For<IInboundApiRepository>();
apiRepo.GetApiMethodByIdAsync(7, Arg.Any<CancellationToken>()).Returns(existing);
apiRepo.SaveChangesAsync(Arg.Any<CancellationToken>()).Returns(1);
_services.AddScoped(_ => apiRepo);
_services.AddSingleton(sp => new ZB.MOM.WW.ScadaBridge.InboundAPI.InboundScriptExecutor(
NullLogger<ZB.MOM.WW.ScadaBridge.InboundAPI.InboundScriptExecutor>.Instance, sp));
var actor = CreateActor();
// Undefined symbol -> genuine Roslyn compile error.
var envelope = Envelope(
new UpdateApiMethodCommand(7, "return doesNotCompile;", 10, null, null), "Designer");
actor.Tell(envelope);
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(10));
Assert.Contains("compileWarning", response.JsonData);
Assert.Contains("failed to compile", response.JsonData);
}
[Fact]
public void AddTemplateAttribute_WithDeploymentRole_ReturnsUnauthorized()
{