chore: low-severity sweep — import session cap, full compile-error lists, leaf-name-fallback validation warning
#05-T24. Three low-severity cleanups, each TDD'd: - TransportOptions.MaxConcurrentImportSessions (8): BundleSessionStore.Open rejects a new session past the cap (evicting expired entries first), bounding the N×~200 MB decrypted-content footprint. Soft cap (count/add not atomic). - ScriptCompiler.TryCompile now joins ALL forbidden-API violations / compile errors (string.Join instead of [0]) so an operator sees every problem at once; the T15 verdict cache stores the joined string. - SemanticValidator emits an advisory warning when a CallScript target resolves ONLY via the composed leaf-name fallback (dynamic child path, not statically verified) instead of silently accepting it. Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -40,6 +40,19 @@ public class ScriptCompilerTests
|
||||
Assert.Contains("forbidden", result.Error, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryCompile_MultipleForbiddenApis_ReportsAll()
|
||||
{
|
||||
// #05-T24: the failure message must enumerate EVERY forbidden API, not
|
||||
// just the first, so an operator fixing one isn't surprised by the next.
|
||||
var result = _sut.TryCompile(
|
||||
"System.IO.File.ReadAllText(\"x\"); System.Diagnostics.Process.Start(\"y\");",
|
||||
"Test");
|
||||
Assert.True(result.IsFailure);
|
||||
Assert.Contains("System.IO", result.Error, StringComparison.Ordinal);
|
||||
Assert.Contains("Process", result.Error, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
// Namespace alias — the old substring scan never saw "System.IO." spelled out.
|
||||
[InlineData("using IO = System.IO; IO.File.ReadAllText(\"x\");")]
|
||||
|
||||
@@ -105,6 +105,50 @@ public class SemanticValidatorTests
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.CallTargetNotFound);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_CallScriptCompositionDelegatedByLeafName_EmitsAdvisoryWarning()
|
||||
{
|
||||
// #05-T24: a call target accepted ONLY by the composed leaf-name fallback
|
||||
// (dynamic child path, not statically verifiable) must surface an advisory
|
||||
// warning rather than being silently accepted.
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
Scripts =
|
||||
[
|
||||
new ResolvedScript { CanonicalName = "LeftMESReceiver.MoveIn", Code = "var x = 1;" },
|
||||
new ResolvedScript { CanonicalName = "MesMoveIn", Code = "await Children[side + \"MESReceiver\"].CallScript(\"MoveIn\", Parameters);" }
|
||||
]
|
||||
};
|
||||
|
||||
var result = _sut.Validate(config);
|
||||
|
||||
Assert.Contains(result.Warnings, w =>
|
||||
w.Category == ValidationCategory.CallTargetNotFound
|
||||
&& w.Message.Contains("leaf name only", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_CallScriptSameScopeSibling_EmitsNoLeafWarning()
|
||||
{
|
||||
// A same-scope sibling resolves directly (not the leaf fallback), so no
|
||||
// advisory warning is emitted.
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
Scripts =
|
||||
[
|
||||
new ResolvedScript { CanonicalName = "Target", Code = "var x = 1;" },
|
||||
new ResolvedScript { CanonicalName = "Caller", Code = "CallScript(\"Target\");" }
|
||||
]
|
||||
};
|
||||
|
||||
var result = _sut.Validate(config);
|
||||
|
||||
Assert.DoesNotContain(result.Warnings, w =>
|
||||
w.Message.Contains("leaf name only", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_CallScriptUnknownLeafName_StillReturnsError()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user