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:
@@ -243,6 +243,56 @@ public class InboundScriptExecutorTests
|
||||
Assert.True(_executor.CompileAndRegister(method));
|
||||
}
|
||||
|
||||
// --- Compile-diagnostics overload: surface errors so a non-compiling save is not a silent no-op ---
|
||||
|
||||
[Fact]
|
||||
public void CompileAndRegister_NonCompilingScript_ReportsErrors()
|
||||
{
|
||||
// A genuine Roslyn error (undefined symbol) must be reported via the out-param,
|
||||
// not just swallowed to a bare false. The Management Create/Update path turns
|
||||
// these into the caller-visible compileWarning.
|
||||
var method = new ApiMethod("broken", "return doesNotExist;") { Id = 1, TimeoutSeconds = 10 };
|
||||
|
||||
var ok = _executor.CompileAndRegister(method, out var errors);
|
||||
|
||||
Assert.False(ok);
|
||||
Assert.NotEmpty(errors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompileAndRegister_ValidScript_ReportsNoErrors()
|
||||
{
|
||||
var method = new ApiMethod("fine", "return 1;") { Id = 1, TimeoutSeconds = 10 };
|
||||
|
||||
var ok = _executor.CompileAndRegister(method, out var errors);
|
||||
|
||||
Assert.True(ok);
|
||||
Assert.Empty(errors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompileAndRegister_NonCompilingUpdate_KeepsPreviousHandler()
|
||||
{
|
||||
// Register a working handler, then "update" the same method name with a script
|
||||
// that does not compile. The broken save must NOT replace the working delegate —
|
||||
// the previously registered version keeps serving requests (this is exactly the
|
||||
// behaviour that makes a broken save non-fatal while the warning is surfaced).
|
||||
var good = new ApiMethod("evolving", "return 111;") { Id = 1, TimeoutSeconds = 10 };
|
||||
Assert.True(_executor.CompileAndRegister(good, out var goodErrors));
|
||||
Assert.Empty(goodErrors);
|
||||
|
||||
var broken = new ApiMethod("evolving", "this is not valid C#") { Id = 1, TimeoutSeconds = 10 };
|
||||
Assert.False(_executor.CompileAndRegister(broken, out var brokenErrors));
|
||||
Assert.NotEmpty(brokenErrors);
|
||||
|
||||
// The old (good) handler still serves — the broken script never went live.
|
||||
var result = await _executor.ExecuteAsync(
|
||||
broken, new Dictionary<string, object?>(), _route, TimeSpan.FromSeconds(10));
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Contains("111", result.ResultJson);
|
||||
}
|
||||
|
||||
// --- InboundAPI-002: lazy compile-and-fetch must be atomic, never KeyNotFoundException ---
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user