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:
@@ -170,6 +170,13 @@ public sealed class ArtifactDiff
|
||||
// previewed as "Identical". Mirrors CompareDatabaseConnection below.
|
||||
AddIfDifferent(changes, "MaxRetries", existing.MaxRetries, incoming.MaxRetries);
|
||||
AddIfDifferent(changes, "RetryDelay", existing.RetryDelay.ToString(), incoming.RetryDelay.ToString());
|
||||
// arch-review 2026-08-19 P7-1 (row-27 shape): TimeoutSeconds is operative at
|
||||
// runtime and the importer has always APPLIED it, but the diff omitted it — same
|
||||
// "silently changed and invisible to preview" defect as the MaxRetries fix above.
|
||||
// A null incoming value (bundle written before this field existed) compares as
|
||||
// "no change" — it mirrors BundleImporter's Overwrite fallback, which leaves the
|
||||
// target's existing timeout untouched in that case.
|
||||
AddIfDifferent(changes, "TimeoutSeconds", existing.TimeoutSeconds, incoming.TimeoutSeconds ?? existing.TimeoutSeconds);
|
||||
|
||||
// Secrets: presence-only comparison (we never echo the value in the diff).
|
||||
var existingHasSecret = !string.IsNullOrEmpty(existing.AuthConfiguration);
|
||||
|
||||
@@ -3090,6 +3090,9 @@ public sealed class BundleImporter : IBundleImporter
|
||||
existing.AuthConfiguration = dto.Secrets?.Values.TryGetValue("AuthConfiguration", out var auth) == true ? auth : null;
|
||||
existing.MaxRetries = dto.MaxRetries;
|
||||
existing.RetryDelay = dto.RetryDelay;
|
||||
// A bundle written before this field existed carries null — leave the
|
||||
// target's existing timeout untouched rather than resetting it to 0/unset.
|
||||
existing.TimeoutSeconds = dto.TimeoutSeconds ?? existing.TimeoutSeconds;
|
||||
await _externalRepo.UpdateExternalSystemAsync(existing, ct).ConfigureAwait(false);
|
||||
await _auditService.LogAsync(user, "Update", "ExternalSystem", existing.Id.ToString(), existing.Name,
|
||||
new { existing.Name, existing.EndpointUrl }, ct).ConfigureAwait(false);
|
||||
@@ -3123,6 +3126,9 @@ public sealed class BundleImporter : IBundleImporter
|
||||
AuthConfiguration = dto.Secrets?.Values.TryGetValue("AuthConfiguration", out var auth) == true ? auth : null,
|
||||
MaxRetries = dto.MaxRetries,
|
||||
RetryDelay = dto.RetryDelay,
|
||||
// Null (a bundle written before this field existed) falls back to the
|
||||
// entity's own "0 = unset" default.
|
||||
TimeoutSeconds = dto.TimeoutSeconds ?? 0,
|
||||
};
|
||||
return sys;
|
||||
}
|
||||
|
||||
@@ -252,7 +252,14 @@ public sealed record ExternalSystemDto(
|
||||
int MaxRetries,
|
||||
TimeSpan RetryDelay,
|
||||
IReadOnlyList<ExternalSystemMethodDto> Methods,
|
||||
SecretsBlock? Secrets);
|
||||
SecretsBlock? Secrets,
|
||||
// Per-system HTTP round-trip timeout (arch-review 2026-08-19 P7-1, row-27 shape).
|
||||
// Additive trailing field; null distinguishes a bundle written before this field
|
||||
// existed from a bundle that explicitly carries the entity's own "0 = unset"
|
||||
// sentinel. BundleImporter treats a null incoming value as "not specified by this
|
||||
// bundle": Add falls back to the entity default (0), Overwrite leaves the target's
|
||||
// existing value untouched.
|
||||
int? TimeoutSeconds = null);
|
||||
|
||||
public sealed record ExternalSystemMethodDto(
|
||||
string Name,
|
||||
|
||||
@@ -139,7 +139,8 @@ public sealed class EntitySerializer
|
||||
MaxRetries: sys.MaxRetries,
|
||||
RetryDelay: sys.RetryDelay,
|
||||
Methods: methods,
|
||||
Secrets: secrets);
|
||||
Secrets: secrets,
|
||||
TimeoutSeconds: sys.TimeoutSeconds);
|
||||
}).ToList(),
|
||||
DatabaseConnections: aggregate.DatabaseConnections.Select(dc => new DatabaseConnectionDto(
|
||||
Name: dc.Name,
|
||||
@@ -433,6 +434,9 @@ public sealed class EntitySerializer
|
||||
AuthConfiguration = dto.Secrets?.Values.TryGetValue("AuthConfiguration", out var auth) == true ? auth : null,
|
||||
MaxRetries = dto.MaxRetries,
|
||||
RetryDelay = dto.RetryDelay,
|
||||
// Null (a bundle written before this field existed) falls back to the
|
||||
// entity's own "0 = unset" default.
|
||||
TimeoutSeconds = dto.TimeoutSeconds ?? 0,
|
||||
};
|
||||
externalSystems.Add(sys);
|
||||
foreach (var m in dto.Methods)
|
||||
|
||||
Reference in New Issue
Block a user