fluid-core/internal/observation/topology_test.go

224 lines
6.8 KiB
Go
Raw Normal View History

package observation
import (
"fmt"
"testing"
"time"
"github.com/tegwick/fluid-core/internal/contract"
)
var base = time.Date(2026, 9, 4, 10, 0, 0, 0, time.UTC)
func req(consumer string, offset time.Duration, method, route string, status int64) contract.FluidTelemetry {
cohort := contract.CohortID("coding-agents")
s := status
return contract.FluidTelemetry{
ID: fmt.Sprintf("tl-%s-%d", consumer, offset),
OccurredAt: base.Add(offset),
Kind: contract.FluidTelemetryKindRequest,
ConsumerRef: consumer,
Cohort: &cohort,
Request: &contract.FluidTelemetryRequest{Route: route, Method: method, Status: &s},
}
}
func errEv(consumer string, offset time.Duration, route string, class contract.FluidTelemetryErrorClass) contract.FluidTelemetry {
ev := req(consumer, offset, "GET", route, 400)
ev.Kind = contract.FluidTelemetryKindError
ev.Error = &contract.FluidTelemetryError{Class: class}
return ev
}
// TestDetectsInefficientUsagePattern reproduces the Blueprint section 33
// worked example: consumers listing everything to find one record.
func TestDetectsInefficientUsagePattern(t *testing.T) {
a := NewTopologyAnalyzer()
var events []contract.FluidTelemetry
for _, consumer := range []string{"c-1", "c-2", "c-3"} {
for chain := 0; chain < 2; chain++ {
start := time.Duration(chain) * time.Hour
events = append(events,
req(consumer, start, "GET", "/customers/{id}/invoices", 200),
req(consumer, start+time.Second, "GET", "/customers/{id}/invoices", 200),
req(consumer, start+2*time.Second, "GET", "/customers/{id}/invoices", 200),
)
}
}
patterns := a.Patterns(events)
if len(patterns) == 0 {
t.Fatal("no pattern detected in a clearly repeated shape")
}
p := patterns[0]
if p.Consumers != 3 {
t.Errorf("independent consumers = %d, want 3", p.Consumers)
}
if p.Count != 6 {
t.Errorf("occurrences = %d, want 6", p.Count)
}
// The repeated route is the signal that the consumer is compensating.
if p.MaxRepeats != 3 {
t.Errorf("max repeats = %d, want 3", p.MaxRepeats)
}
if p.RepeatedStep != "GET /customers/{id}/invoices" {
t.Errorf("repeated step = %q", p.RepeatedStep)
}
}
// TestOneConsumerRepeatingIsNotAPattern: a single client looping is a client
// bug; several independent consumers converging is interface pressure.
func TestOneConsumerRepeatingIsNotAPattern(t *testing.T) {
a := NewTopologyAnalyzer()
var events []contract.FluidTelemetry
for chain := 0; chain < 10; chain++ {
start := time.Duration(chain) * time.Hour
events = append(events,
req("c-1", start, "GET", "/entries", 200),
req("c-1", start+time.Second, "GET", "/entries", 200),
)
}
if patterns := a.Patterns(events); len(patterns) != 0 {
t.Errorf("a single consumer's loop was reported as a pattern: %+v", patterns)
}
}
func TestChainGapSplitsInteractions(t *testing.T) {
a := NewTopologyAnalyzer()
a.ChainGap = 10 * time.Second
events := []contract.FluidTelemetry{
req("c-1", 0, "GET", "/a", 200),
req("c-1", 2*time.Second, "GET", "/b", 200),
// Well past the gap: a new interaction, not a continuation.
req("c-1", time.Minute, "GET", "/c", 200),
}
interactions := a.Interactions(events)
if len(interactions) != 2 {
t.Fatalf("got %d interactions, want 2", len(interactions))
}
if len(interactions[0].Steps) != 2 || len(interactions[1].Steps) != 1 {
t.Errorf("steps split wrongly: %d and %d",
len(interactions[0].Steps), len(interactions[1].Steps))
}
}
func TestExplicitChainIDOverridesTiming(t *testing.T) {
a := NewTopologyAnalyzer()
a.ChainGap = time.Second
withChain := func(ev contract.FluidTelemetry, id string) contract.FluidTelemetry {
ev.Sequence = &contract.FluidTelemetrySequence{ChainID: id}
return ev
}
// Two calls an hour apart, but the consumer says they are one task.
events := []contract.FluidTelemetry{
withChain(req("c-1", 0, "GET", "/a", 200), "chain-1"),
withChain(req("c-1", time.Hour, "GET", "/b", 200), "chain-1"),
}
interactions := a.Interactions(events)
if len(interactions) != 1 {
t.Fatalf("an explicit chain id was split by timing: got %d interactions", len(interactions))
}
}
// TestRecoveredErrorIsDistinguished: invalid request, then a corrected retry,
// is a recoverable misunderstanding rather than a hard failure, and the two
// deserve different hypotheses.
func TestRecoveredErrorIsDistinguished(t *testing.T) {
a := NewTopologyAnalyzer()
var events []contract.FluidTelemetry
for _, consumer := range []string{"c-1", "c-2", "c-3"} {
for chain := 0; chain < 2; chain++ {
start := time.Duration(chain) * time.Hour
events = append(events,
errEv(consumer, start, "/entries", contract.FluidTelemetryErrorClassValidation),
req(consumer, start+time.Second, "GET", "/entries", 200),
)
}
}
patterns := a.Patterns(events)
if len(patterns) == 0 {
t.Fatal("no pattern detected")
}
if patterns[0].RecoveredError != contract.FluidTelemetryErrorClassValidation {
t.Errorf("recovered error = %q, want validation", patterns[0].RecoveredError)
}
}
func TestPatternsAreDeterministicallyOrdered(t *testing.T) {
a := NewTopologyAnalyzer()
var events []contract.FluidTelemetry
for _, consumer := range []string{"c-1", "c-2", "c-3"} {
for chain := 0; chain < 5; chain++ {
start := time.Duration(chain) * time.Hour
events = append(events, req(consumer, start, "GET", "/frequent", 200))
}
for chain := 0; chain < 2; chain++ {
start := time.Duration(chain+10) * time.Hour
events = append(events, req(consumer, start, "GET", "/rare", 200))
}
}
first := a.Patterns(events)
if len(first) < 2 {
t.Fatalf("expected two patterns, got %d", len(first))
}
// Biggest signal first, so an analyst reads the important thing first.
if first[0].Count < first[1].Count {
t.Error("patterns are not ordered by frequency")
}
for i := 0; i < 20; i++ {
again := a.Patterns(events)
for j := range first {
if again[j].Signature != first[j].Signature {
t.Fatal("pattern ordering varied between runs")
}
}
}
}
func TestCohortPopulationsRespectMinimumSize(t *testing.T) {
policy := testPolicy()
e := NewCohortEngine("unclassified", policy)
var events []contract.FluidTelemetry
// One cohort with a single consumer, one with plenty.
small := contract.CohortID("lone-partner")
for i := 0; i < 20; i++ {
ev := req("only-one", time.Duration(i)*time.Second, "GET", "/a", 200)
ev.Cohort = &small
events = append(events, ev)
}
big := contract.CohortID("agents")
for i := 0; i < 20; i++ {
ev := req(fmt.Sprintf("c-%d", i), time.Duration(i)*time.Second, "GET", "/a", 200)
ev.Cohort = &big
events = append(events, ev)
}
pops := e.Populations(events)
byCohort := map[contract.CohortID]Population{}
for _, p := range pops {
byCohort[p.Cohort] = p
}
if !byCohort[small].Suppressed {
t.Error("a cohort of one consumer was reportable")
}
if byCohort[big].Suppressed {
t.Error("a cohort of twenty consumers was suppressed")
}
}