Files
ITWorx Pulse release export bd774932d5
Public source validation / validate (push) Failing after 3m8s
Publish ITWorx Pulse source
2026-09-03 02:09:19 +02:00

137 lines
5.6 KiB
Go

package reconciliation
import (
"testing"
"time"
)
func TestComposeRecreationKeepsLogicalEntityAndLinksEvent(t *testing.T) {
now := time.Date(2026, time.August, 1, 22, 0, 0, 0, time.UTC)
first, err := ReconcileContainers([]ContainerObservation{{
SourceID: "agent", RuntimeID: "runtime-a", Name: "pulse-api-1", Project: "pulse", Service: "api",
ImageDigest: "sha256:old", ObservedAt: now,
}}, nil, now)
if err != nil {
t.Fatal(err)
}
second, err := ReconcileContainers([]ContainerObservation{{
SourceID: "agent", RuntimeID: "runtime-b", Name: "pulse-api-1", Project: "pulse", Service: "api",
ImageDigest: "sha256:new", ObservedAt: now.Add(time.Minute),
}}, first.Aliases, now.Add(time.Minute))
if err != nil {
t.Fatal(err)
}
if len(second.Recreated) != 1 || second.Recreated[0].EntityID != first.Current[0].EntityID || second.Recreated[0].EventEntityID != first.Current[0].EntityID {
t.Fatalf("recreation did not preserve logical event identity: %+v", second)
}
if len(second.Aliases) != 2 || len(second.Current) != 1 || second.Current[0].RuntimeID != "runtime-b" {
t.Fatalf("runtime history not retained: %+v", second)
}
if second.Current[0].ImageDigest != "sha256:new" {
t.Fatalf("new runtime facts not applied: %+v", second.Current[0])
}
}
func TestReusedNameWithoutEvidenceDoesNotMerge(t *testing.T) {
now := time.Date(2026, time.August, 1, 22, 0, 0, 0, time.UTC)
first, err := ReconcileContainers([]ContainerObservation{{
SourceID: "agent", RuntimeID: "runtime-a", Name: "worker", ObservedAt: now,
}}, nil, now)
if err != nil {
t.Fatal(err)
}
second, err := ReconcileContainers([]ContainerObservation{{
SourceID: "agent", RuntimeID: "runtime-b", Name: "worker", ObservedAt: now.Add(time.Minute),
}}, first.Aliases, now.Add(time.Minute))
if err != nil {
t.Fatal(err)
}
if len(second.Current) != 1 || second.Current[0].EntityID == first.Current[0].EntityID {
t.Fatalf("reused name was merged without evidence: %+v", second)
}
if len(second.Recreated) != 0 {
t.Fatalf("name-only reuse created recreation: %+v", second.Recreated)
}
}
func TestIdempotentComposeSnapshotDoesNotDuplicateHistory(t *testing.T) {
now := time.Date(2026, time.August, 1, 22, 0, 0, 0, time.UTC)
obs := []ContainerObservation{{SourceID: "agent", RuntimeID: "runtime-a", Name: "api", Project: "pulse", Service: "api", ObservedAt: now}}
first, err := ReconcileContainers(obs, nil, now)
if err != nil {
t.Fatal(err)
}
second, err := ReconcileContainers(obs, first.Aliases, now)
if err != nil {
t.Fatal(err)
}
if len(second.Aliases) != 1 || len(second.Recreated) != 0 || len(second.Added) != 0 || len(second.Updated) != 1 {
t.Fatalf("repeat was not idempotent: %+v", second)
}
if first.Current[0].EntityID != second.Current[0].EntityID {
t.Fatal("logical ID changed on repeated observation")
}
}
func TestAmbiguousComposeIdentityRefusesMerge(t *testing.T) {
now := time.Date(2026, time.August, 1, 22, 0, 0, 0, time.UTC)
previous := []ContainerAlias{
{EntityID: "entity-a", SourceID: "agent", RuntimeID: "runtime-a", Project: "pulse", Service: "api", Active: true, FirstSeenAt: now, LastSeenAt: now},
{EntityID: "entity-b", SourceID: "agent", RuntimeID: "runtime-b", Project: "pulse", Service: "api", Active: true, FirstSeenAt: now, LastSeenAt: now},
}
result, err := ReconcileContainers([]ContainerObservation{{SourceID: "agent", RuntimeID: "runtime-c", Name: "api", Project: "pulse", Service: "api", ObservedAt: now.Add(time.Minute)}}, previous, now.Add(time.Minute))
if err != nil {
t.Fatal(err)
}
if len(result.Recreated) != 0 || len(result.Warnings) != 1 || result.Current[0].EntityID == "entity-a" || result.Current[0].EntityID == "entity-b" {
t.Fatalf("ambiguous identity merged: %+v", result)
}
}
func TestSupersededRuntimeAliasIsTombstonedOnce(t *testing.T) {
now := time.Date(2026, time.August, 1, 22, 0, 0, 0, time.UTC)
first, err := ReconcileContainers([]ContainerObservation{{
SourceID: "agent", RuntimeID: "runtime-a", Name: "pulse-api-1", Project: "pulse", Service: "api", ObservedAt: now,
}}, nil, now)
if err != nil {
t.Fatal(err)
}
if !first.Current[0].TombstonedAt.IsZero() {
t.Fatalf("active alias carries a tombstone time: %+v", first.Current[0])
}
recreated := now.Add(time.Minute).In(time.FixedZone("CEST", 2*60*60))
second, err := ReconcileContainers([]ContainerObservation{{
SourceID: "agent", RuntimeID: "runtime-b", Name: "pulse-api-1", Project: "pulse", Service: "api", ObservedAt: recreated,
}}, first.Aliases, recreated)
if err != nil {
t.Fatal(err)
}
old := aliasByRuntime(t, second.Aliases, "runtime-a")
if old.Active || !old.TombstonedAt.Equal(recreated) || old.TombstonedAt.Location() != time.UTC {
t.Fatalf("superseded alias was not tombstoned in UTC: %+v", old)
}
if current := aliasByRuntime(t, second.Aliases, "runtime-b"); !current.Active || !current.TombstonedAt.IsZero() {
t.Fatalf("current alias carries a tombstone time: %+v", current)
}
third, err := ReconcileContainers([]ContainerObservation{{
SourceID: "agent", RuntimeID: "runtime-b", Name: "pulse-api-1", Project: "pulse", Service: "api", ObservedAt: recreated.Add(time.Hour),
}}, second.Aliases, recreated.Add(time.Hour))
if err != nil {
t.Fatal(err)
}
if again := aliasByRuntime(t, third.Aliases, "runtime-a"); !again.TombstonedAt.Equal(recreated) {
t.Fatalf("tombstone time moved on a repeated snapshot: %+v", again)
}
}
func aliasByRuntime(t *testing.T, aliases []ContainerAlias, runtimeID string) ContainerAlias {
t.Helper()
for _, alias := range aliases {
if alias.RuntimeID == runtimeID {
return alias
}
}
t.Fatalf("alias %q not found in %+v", runtimeID, aliases)
return ContainerAlias{}
}