155 lines
4.9 KiB
Go
155 lines
4.9 KiB
Go
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"fmt"
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"path/filepath"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
_ "modernc.org/sqlite"
|
||
|
|
|
||
|
|
"github.com/tegwick/fluid-core/internal/contract"
|
||
|
|
"github.com/tegwick/fluid-core/internal/evidence"
|
||
|
|
"github.com/tegwick/fluid-core/internal/observation"
|
||
|
|
"github.com/tegwick/fluid-core/internal/runtime"
|
||
|
|
)
|
||
|
|
|
||
|
|
// TestInsightLoopEndToEnd drives real traffic through the gateway and confirms
|
||
|
|
// the observation plane turns it into a pressure record.
|
||
|
|
//
|
||
|
|
// This is the Blueprint section 33 shape end to end: consumers repeatedly
|
||
|
|
// listing a collection to find one entry, observed at the gateway, redacted on
|
||
|
|
// ingest, and classified as inefficient usage without anyone naming it as such.
|
||
|
|
func TestInsightLoopEndToEnd(t *testing.T) {
|
||
|
|
ctx := context.Background()
|
||
|
|
dir := t.TempDir()
|
||
|
|
|
||
|
|
store, err := evidence.OpenSQLite(ctx, filepath.Join(dir, "e.db"))
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
defer store.Close()
|
||
|
|
|
||
|
|
policy := observation.DefaultRedactionPolicy([]byte("a-stable-salt-for-this-test-only"))
|
||
|
|
ingest, err := observation.NewIngest(store, "hall-publishing", policy)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
|
||
|
|
adapter := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
_, _ = w.Write([]byte(`[{"id":1},{"id":2}]`))
|
||
|
|
}))
|
||
|
|
defer adapter.Close()
|
||
|
|
|
||
|
|
reg := runtime.NewRegistry("hall-publishing")
|
||
|
|
d := contract.Revision{
|
||
|
|
SchemaVersion: "0.1", ID: "R-1", Interface: "hall-publishing",
|
||
|
|
State: contract.RevisionStateStable,
|
||
|
|
Contract: contract.RevisionContract{Type: contract.RevisionContractTypeOpenapi, Digest: contract.Digest("sha256:" + repeat("1", 64))},
|
||
|
|
Runtime: contract.RevisionRuntime{Upstream: adapter.URL},
|
||
|
|
Intent: contract.RevisionIntent{Version: "IEI-1"},
|
||
|
|
Policy: contract.RevisionPolicy{
|
||
|
|
Compatibility: contract.RevisionPolicyCompatibilityAdditive,
|
||
|
|
SecurityCheck: contract.RevisionPolicySecurityCheckPassed,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
if err := reg.PutRevision(d); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := reg.PutPolicy(contract.RoutingPolicy{
|
||
|
|
SchemaVersion: "0.1", Interface: "hall-publishing",
|
||
|
|
Generation: 1, DefaultRevision: "R-1",
|
||
|
|
}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
|
||
|
|
emitter := runtime.NewEmitter(ingest, runtime.EmitterOptions{Buffer: 4096, Workers: 2})
|
||
|
|
gw, err := runtime.NewGateway(runtime.GatewayOptions{
|
||
|
|
Interface: "hall-publishing",
|
||
|
|
Registry: reg,
|
||
|
|
Resolver: runtime.NewResolver(reg, false),
|
||
|
|
Connector: runtime.NewConnector(),
|
||
|
|
Emitter: emitter,
|
||
|
|
Cohorts: observation.NewCohortEngine("coding-agents", policy),
|
||
|
|
Response: runtime.ResponsePolicy{FeedbackPath: "/v1/feedback"},
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Four independent consumers, each listing the collection four times per
|
||
|
|
// task to find one entry: the Blueprint section 33 shape.
|
||
|
|
for c := 0; c < 4; c++ {
|
||
|
|
for chain := 0; chain < 3; chain++ {
|
||
|
|
for i := 0; i < 4; i++ {
|
||
|
|
req := httptest.NewRequest(http.MethodGet, "/v1/hall-entries", nil)
|
||
|
|
req.Header.Set("X-FLUID-Consumer", fmt.Sprintf("agent-%d", c))
|
||
|
|
req.Header.Set("X-FLUID-Correlation", fmt.Sprintf("c-%d-%d", c, chain))
|
||
|
|
rec := httptest.NewRecorder()
|
||
|
|
gw.ServeHTTP(rec, req)
|
||
|
|
if rec.Code != http.StatusOK {
|
||
|
|
t.Fatalf("request failed: %d", rec.Code)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
emitter.Close()
|
||
|
|
|
||
|
|
events, err := store.Telemetry(ctx, evidence.TelemetryFilter{InterfaceID: "hall-publishing"})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if len(events) != 48 {
|
||
|
|
t.Fatalf("recorded %d telemetry events, want 48", len(events))
|
||
|
|
}
|
||
|
|
|
||
|
|
// The raw consumer header must never have reached the store.
|
||
|
|
for _, ev := range events {
|
||
|
|
if ev.ConsumerRef == "agent-0" {
|
||
|
|
t.Fatal("a raw consumer identity reached the evidence store")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
classifier := observation.NewClassifier(
|
||
|
|
observation.DefaultClassifierOptions(), observation.NewTopologyAnalyzer())
|
||
|
|
findings := classifier.Classify(events)
|
||
|
|
if len(findings) == 0 {
|
||
|
|
t.Fatal("no pressure found in clearly inefficient traffic")
|
||
|
|
}
|
||
|
|
|
||
|
|
registry := observation.NewPressureRegistry(store, "hall-publishing")
|
||
|
|
recorded, err := registry.RecordAll(ctx, findings)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if len(recorded) == 0 {
|
||
|
|
t.Fatal("no pressure recorded")
|
||
|
|
}
|
||
|
|
|
||
|
|
var inefficient *contract.FluidPressure
|
||
|
|
for i := range recorded {
|
||
|
|
if recorded[i].Class == contract.PressureClassSuccessfulButInefficientUsage {
|
||
|
|
inefficient = &recorded[i]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if inefficient == nil {
|
||
|
|
t.Fatalf("inefficient usage not recorded; got %v", recorded)
|
||
|
|
}
|
||
|
|
if inefficient.Frequency == nil || *inefficient.Frequency.IndependentConsumers != 4 {
|
||
|
|
t.Errorf("independent consumers not recorded correctly: %+v", inefficient.Frequency)
|
||
|
|
}
|
||
|
|
if len(inefficient.EvidenceRefs) == 0 {
|
||
|
|
t.Error("pressure recorded with no evidence references")
|
||
|
|
}
|
||
|
|
|
||
|
|
t.Logf("pressure %s: %s (severity %.2f, confidence %.2f, %d consumers)",
|
||
|
|
inefficient.ID, inefficient.Summary,
|
||
|
|
float64(*inefficient.Severity), float64(*inefficient.Confidence),
|
||
|
|
*inefficient.Frequency.IndependentConsumers)
|
||
|
|
}
|
||
|
|
|
||
|
|
func repeat(s string, n int) string { return strings.Repeat(s, n) }
|