Public source validation / validate (push) Failing after 3m8s
137 lines
5.6 KiB
Go
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{}
|
|
}
|