Some checks failed
ci / build (push) Failing after 3h11m37s
Completes FLUID-WP-0005. Normalization and redaction live on one path, shared by the in-process emitter and the ingest endpoint: two paths with two normalizations would eventually disagree, and the disagreement would surface as a pressure finding that is really a pipeline bug. Telemetry kind is inferred from event shape rather than defaulting to "request", since an error filed as a request understates the interface's failure rate. A malformed event in a batch does not discard the rest. Feedback is stored as evidence and creates no pressure and no hypothesis on its own, per API Standards 15, with the consumer recorded as the actor so their untrusted status stays visible in the audit trail. The feedback endpoint is the only consumer-reachable part of the control plane. The observation endpoints are not served at all when no pseudonymization salt is configured, rather than served with a generated one: a salt that changed per run would make the same consumer look new every time and every cohort count wrong. Adds an end-to-end test driving real traffic through the gateway and confirming it becomes a classified pressure record, with no raw consumer identity reaching the store. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014KmVxhJ35tCo7rE7UnLwWu Assistant: claude-code Assistant-Model: opus Assistant-Process: 1116572@bnt-lap001 Assistant-Session: 8ba9bb93-a72a-4883-b189-2499cce5c400
154 lines
4.9 KiB
Go
154 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) }
|