From d237a0d310e783dc47a143b9b75c05137a3cd2f9 Mon Sep 17 00:00:00 2001 From: Bruno Baumgartner Date: Thu, 7 May 2026 22:38:00 +0200 Subject: [PATCH] fix: exclude audit metadata from snapshot hash --- .../usecases/export_snapshot_hash_test.go | 85 +++++++++++++++++++ internal/kernel/usecases/snapshot_hash.go | 6 +- 2 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 internal/kernel/usecases/export_snapshot_hash_test.go diff --git a/internal/kernel/usecases/export_snapshot_hash_test.go b/internal/kernel/usecases/export_snapshot_hash_test.go new file mode 100644 index 0000000..3271c35 --- /dev/null +++ b/internal/kernel/usecases/export_snapshot_hash_test.go @@ -0,0 +1,85 @@ +package usecases + +import ( + "testing" + + "digiemu-core/internal/kernel/ports" +) + +func TestSnapshotHashIgnoresCreatedAtUnix(t *testing.T) { + unit := ports.UnitDTO{ + ID: "unit-1", + Key: "demo", + Title: "Demo", + Description: "Same deterministic state", + HeadVersionID: "version-1", + } + + versionsA := []ports.VersionDTO{ + { + ID: "version-1", + Label: "stable-label", + PrevVersionID: "", + ContentHash: "CONTENT_HASH_ABC", + ActorID: "actor-1", + CreatedAtUnix: 1000, + }, + } + + versionsB := []ports.VersionDTO{ + { + ID: "version-1", + Label: "stable-label", + PrevVersionID: "", + ContentHash: "CONTENT_HASH_ABC", + ActorID: "actor-1", + CreatedAtUnix: 9999, + }, + } + + hashA := sha256HexFromLines(snapshotCanonicalLines(unit, versionsA)) + hashB := sha256HexFromLines(snapshotCanonicalLines(unit, versionsB)) + + if hashA != hashB { + t.Fatalf("CreatedAtUnix must be outside the deterministic hash boundary: hashA=%s hashB=%s", hashA, hashB) + } +} + +func TestSnapshotHashIgnoresGeneratedLabel(t *testing.T) { + unit := ports.UnitDTO{ + ID: "unit-1", + Key: "demo", + Title: "Demo", + Description: "Same deterministic state", + HeadVersionID: "version-1", + } + + versionsA := []ports.VersionDTO{ + { + ID: "version-1", + Label: "20260507T220000.000Z", + PrevVersionID: "", + ContentHash: "CONTENT_HASH_ABC", + ActorID: "actor-1", + CreatedAtUnix: 1000, + }, + } + + versionsB := []ports.VersionDTO{ + { + ID: "version-1", + Label: "20260507T230000.000Z", + PrevVersionID: "", + ContentHash: "CONTENT_HASH_ABC", + ActorID: "actor-1", + CreatedAtUnix: 1000, + }, + } + + hashA := sha256HexFromLines(snapshotCanonicalLines(unit, versionsA)) + hashB := sha256HexFromLines(snapshotCanonicalLines(unit, versionsB)) + + if hashA != hashB { + t.Fatalf("generated Label must be outside the deterministic hash boundary: hashA=%s hashB=%s", hashA, hashB) + } +} diff --git a/internal/kernel/usecases/snapshot_hash.go b/internal/kernel/usecases/snapshot_hash.go index bf80032..bcb83c5 100644 --- a/internal/kernel/usecases/snapshot_hash.go +++ b/internal/kernel/usecases/snapshot_hash.go @@ -17,7 +17,7 @@ import ( // Format (one record per line): // // UNIT||||<desc>|<headVersionID> -// VER|<id>|<label>|<prev>|<contentHash>|<actor>|<createdAtUnix> +// VER|<id>|<prev>|<contentHash> // AUD|<id>|<type>|<atUnix>|<actor>|<unit>|<ver>|<dataCanonical> func snapshotCanonicalLines(u ports.UnitDTO, vs []ports.VersionDTO) []string { lines := make([]string, 0, 1+len(vs)) @@ -27,8 +27,8 @@ func snapshotCanonicalLines(u ports.UnitDTO, vs []ports.VersionDTO) []string { )) for _, v := range vs { lines = append(lines, fmt.Sprintf( - "VER|%s|%s|%s|%s|%s|%d", - v.ID, v.Label, v.PrevVersionID, v.ContentHash, v.ActorID, v.CreatedAtUnix, + "VER|%s|%s|%s", + v.ID, v.PrevVersionID, v.ContentHash, )) } return lines