fix(transport): carry ExternalSystem TimeoutSeconds through bundles + diff (arch-review 2026-08-19 P7-1, row-27 shape)
TimeoutSeconds is the per-system HTTP timeout ExternalSystemClient reads at call time and is deployed to sites (proto field 6), but Transport bundles silently dropped it: the DTO never carried it, export never wrote it, the importer's Add path reset it to 0, the Overwrite path left the target's stale value untouched, and ArtifactDiff never compared it (so a bundle whose only change was a tuned timeout previewed as Identical). Adds TimeoutSeconds as an additive-optional trailing field on ExternalSystemDto (schemaVersion stays 1.1, bundleFormatVersion unchanged) and threads it through EntitySerializer (export + import), BundleImporter (Add and Overwrite), and ArtifactDiff.CompareExternalSystem. Old-bundle semantics pinned: an absent field (null) falls back to the entity default on Add and leaves the target's existing value untouched on Overwrite/diff. Beefed up the RoundTripEquivalenceTests external-system fixture with a non-default TimeoutSeconds so the reflection round-trip guard actually exercises the field instead of comparing default against default. Swept the rest of the ExternalSystemDto shape against ExternalSystemDefinition field-by-field; no other operative field is missing.
This commit is contained in:
@@ -340,6 +340,60 @@ public sealed class EntitySerializerTests
|
||||
Assert.Equal(TimeSpan.FromSeconds(15), rtSys.RetryDelay);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// arch-review 2026-08-19 P7-1 (row-27 shape): <c>TimeoutSeconds</c> is operative at
|
||||
/// runtime (the per-system HTTP timeout) but was silently dropped by the export/import
|
||||
/// DTO — same defect class as the earlier <c>MaxRetries</c>/<c>RetryDelay</c> holes this
|
||||
/// test's sibling covers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Roundtrip_external_system_preserves_timeout_seconds()
|
||||
{
|
||||
var sys = new ExternalSystemDefinition("billing", "https://billing/api", "Basic")
|
||||
{
|
||||
Id = 1,
|
||||
TimeoutSeconds = 120,
|
||||
};
|
||||
var aggregate = MakeEmptyAggregate() with { ExternalSystems = new[] { sys } };
|
||||
|
||||
var sut = new EntitySerializer();
|
||||
var dto = sut.ToBundleContent(aggregate);
|
||||
Assert.Equal(120, Assert.Single(dto.ExternalSystems).TimeoutSeconds);
|
||||
|
||||
var roundTripped = sut.FromBundleContent(dto);
|
||||
|
||||
var rtSys = Assert.Single(roundTripped.ExternalSystems);
|
||||
Assert.Equal("billing", rtSys.Name);
|
||||
Assert.Equal(120, rtSys.TimeoutSeconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pins the old-bundle semantics: a DTO with no <c>TimeoutSeconds</c> at all (the
|
||||
/// property default, as an old bundle deserializes it) falls back to the entity's own
|
||||
/// "0 = unset" default on import — it must not throw or coerce to some other sentinel.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromDto_with_null_TimeoutSeconds_yields_entity_default_zero()
|
||||
{
|
||||
var dto = new BundleContentDto(
|
||||
TemplateFolders: Array.Empty<TemplateFolderDto>(),
|
||||
Templates: Array.Empty<TemplateDto>(),
|
||||
SharedScripts: Array.Empty<SharedScriptDto>(),
|
||||
ExternalSystems: new[]
|
||||
{
|
||||
new ExternalSystemDto("erp", "https://x", "None", MaxRetries: 3, RetryDelay: TimeSpan.FromSeconds(5), Array.Empty<ExternalSystemMethodDto>(), Secrets: null),
|
||||
},
|
||||
DatabaseConnections: Array.Empty<DatabaseConnectionDto>(),
|
||||
NotificationLists: Array.Empty<NotificationListDto>(),
|
||||
SmtpConfigs: Array.Empty<SmtpConfigDto>(),
|
||||
ApiMethods: Array.Empty<ApiMethodDto>());
|
||||
|
||||
var aggregate = new EntitySerializer().FromBundleContent(dto);
|
||||
|
||||
var sys = Assert.Single(aggregate.ExternalSystems);
|
||||
Assert.Equal(0, sys.TimeoutSeconds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromDto_with_null_SecretsBlock_yields_entity_with_default_empty_secrets()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user