Skip to content

Fix stale hash/idempotency regressions and add sync-loop regression coverage#80

Open
Copilot wants to merge 5 commits intomainfrom
copilot/add-idempotency-regression-tests
Open

Fix stale hash/idempotency regressions and add sync-loop regression coverage#80
Copilot wants to merge 5 commits intomainfrom
copilot/add-idempotency-regression-tests

Conversation

Copy link
Copy Markdown

Copilot AI commented Apr 11, 2026

This PR resolves the stale-hash and idempotency issues behind the repeated sync-loop behavior reported in #78 and adds the regression coverage requested in #79.

What changed

  • preserved the hash cache for performance
  • fixed stale hash invalidation when entity state changes
  • updated project compare/apply to use effective property diffs instead of raw hash mismatch alone
  • added regression tests for:
    • stale cached hash after mutation
    • project compare/apply reporting no changes when no effective diff exists
    • idempotent sync/roundtrip behavior already covered in this branch

Issue coverage

Copilot AI linked an issue Apr 11, 2026 that may be closed by this pull request
5 tasks
Copilot AI changed the title [WIP] Add idempotency regression tests for disk/API sync flow Add idempotency regression coverage for YAML/CSV roundtrips and disk/API sync flow Apr 11, 2026
Copilot AI requested a review from BoBiene April 11, 2026 06:36
@BoBiene BoBiene changed the title Add idempotency regression coverage for YAML/CSV roundtrips and disk/API sync flow Fix stale hash/idempotency regressions and add sync-loop regression coverage Apr 11, 2026
@BoBiene BoBiene requested review from Copilot and rlabbeptc April 11, 2026 07:50
@BoBiene BoBiene marked this pull request as ready for review April 11, 2026 07:51
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes stale-hash/idempotency regressions that could cause repeated sync-loop behavior (#78) and adds regression coverage to prevent reintroductions (#79).

Changes:

  • Invalidate cached entity hashes on state mutation and normalize nested dynamic properties before hashing/diffing.
  • Update project compare/apply to trigger project-property updates based on effective property diffs (not raw hash mismatch alone).
  • Add serializer/storage/sync-loop idempotency regression tests (YAML/CSV roundtrip, compare/apply no-op, sync loop protection).

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
KepwareSync.Service/Kepware.SyncService.csproj Exposes internals to the test assembly to enable sync-service regression tests.
Kepware.Api/Model/BaseEntity.cs Adds hash invalidation on key mutations and ensures normalization occurs before hash/diff operations.
Kepware.Api/ClientHandler/ProjectApiHandler.cs Switches project-property update decision from hash mismatch to effective diff presence.
Kepware.Api.Test/Util/EntityCompareTests.cs Adds regression test for stale cached hash after entity mutation.
Kepware.Api.Test/Sync/SyncIdempotencyTests.cs Adds sync-loop/idempotency regression tests (net10-only).
Kepware.Api.Test/Serializer/RoundtripIdempotencyTests.cs Adds YAML/CSV roundtrip idempotency tests to ensure stable hashes/output.
Kepware.Api.Test/Kepware.Api.Test.csproj Adds net10-only reference to SyncService project for the new sync tests.

var targetProject = await roundtrippedProject.CloneAsync();
AssignOwners(targetProject);

var sourceTag = roundtrippedProject.Channels![0].Devices![0].Tags![0];
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sourceTag is declared but never used. With warnings treated as errors in Kepware.Api.Test.csproj, this will fail the build (CS0219). Remove the variable or use it (e.g., to derive the endpoint/expected diff).

Suggested change
var sourceTag = roundtrippedProject.Channels![0].Devices![0].Tags![0];

Copilot uses AI. Check for mistakes.
Comment on lines 113 to +125
/// Dynamic properties associated with the entity.
/// </summary>
[JsonExtensionData]
public Dictionary<string, JsonElement> DynamicProperties { get; set; } = new();
public Dictionary<string, JsonElement> DynamicProperties
{
get => _dynamicProperties;
set
{
_dynamicProperties = value ?? [];
_dynamicPropertiesNormalized = false;
InvalidateHash();
}
}
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DynamicProperties exposes a mutable Dictionary. If a caller mutates it directly (e.g., entity.DynamicProperties["k"] = ... / .Remove(...)) after Hash has been computed, the cached hash will not be invalidated, which can reintroduce stale-hash/idempotency issues. Consider returning an IReadOnlyDictionary (and mutating only through SetDynamicProperty/helpers) or wrapping mutations so they always call InvalidateHash().

Copilot uses AI. Check for mistakes.
Comment on lines 111 to 117
var result = new ProjectCompareAndApplyResult();
var projectPropertyDiff = sourceProject.GetUpdateDiff(projectFromApi);

if (sourceProject.Hash != projectFromApi.Hash)
if (projectPropertyDiff.Count > 0)
{
m_logger.LogInformation("Project properties has changed. Updating project properties...");
var projectPropertyFailure = await SetProjectPropertiesDetailedAsync(sourceProject, cancellationToken: cancellationToken).ConfigureAwait(false);
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

projectPropertyDiff is computed here but then SetProjectPropertiesDetailedAsync reloads the current project from the API and recomputes a diff again. Since you already have projectFromApi and the computed diff, consider avoiding the duplicate diff computation and extra GET by adding an overload that accepts the already-loaded current project (and/or the precomputed diff).

Copilot uses AI. Check for mistakes.
if (sourceProject.Hash != projectFromApi.Hash)
if (projectPropertyDiff.Count > 0)
{
m_logger.LogInformation("Project properties has changed. Updating project properties...");
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log message grammar: "Project properties has changed" should be "Project properties have changed" (or "Project properties changed") for clarity/searchability.

Suggested change
m_logger.LogInformation("Project properties has changed. Updating project properties...");
m_logger.LogInformation("Project properties have changed. Updating project properties...");

Copilot uses AI. Check for mistakes.
Comment on lines +121 to +129
public async Task CompareAndApply_ProjectHashMismatchWithoutProjectPropertyDiff_ShouldReportNoChanges()
{
ConfigureConnectedClient();

var sourceProject = CreateProjectPropertiesOnly(string.Empty);
sourceProject.Description = "Transient description";
_ = sourceProject.Hash;
sourceProject.Description = string.Empty;

Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test name implies it is exercising a "hash mismatch without effective diff" scenario, but the current arrange code doesn’t assert (or reliably create) a hash mismatch anymore now that Description mutations invalidate the cached hash. Either rename the test to reflect what it actually verifies (no-op compare/apply when there’s no effective diff), or adjust the setup to force a hash mismatch without an effective diff so the regression is truly covered.

Copilot generated this review using guidance from repository custom instructions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add idempotency regression tests for disk/API sync flow Continuous/Infinite Sync Loop when using KepwareToDiskAndSecondary direction

3 participants