fluid-core/conformance/suite/conformance_test.go
tegwick 55363905bc
Some checks failed
ci / build (push) Has been cancelled
Add the conformance suite, echo fixture and integration guide
Completes FLUID-WP-0007. The seven minimal-conformance requirements and
the mechanically checkable architectural invariants are asserted as
tests rather than claimed in a README, because a conformance claim
nobody re-checks is one that quietly stops being true. Only the
checkable subset of the invariants is asserted; pretending a test can
settle the rest would be worse than leaving them to review.

TestFirstVerticalSlice runs all eleven steps of Blueprint 50 with no
human steps: two revisions, explicit routing, telemetry, a cohort
dimension, detected pressure, a hypothesis, a candidate, a 90/10
experiment, fitness comparison, promotion, and a complete audit trail.
Requests per completed task fall from 5.65 to 1.00 against a 1.20
target. A companion test runs the loop twice and requires the same
verdict, since a loop whose conclusion depended on run order would be
measuring the harness rather than the interface.

The failure-containment matrix covers Blueprint 34 directly: the data
plane keeps serving with the evidence store closed, with telemetry
wedged against a sink that never returns, after a failed build, after an
experiment rollback, and with the adaptive concurrency limit saturated.

Fixes a real bug the suite exposed. Drain closed the emitter outright,
so every request after the first flush emitted into a dead emitter and
was silently lost -- the kind of fault that makes a later measurement
quietly wrong rather than loudly broken. Emitter.Flush now waits for
delivery without stopping it.

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
2026-09-04 08:21:49 +02:00

448 lines
15 KiB
Go

package suite
import (
"context"
"crypto/ed25519"
"net/http"
"strings"
"testing"
"github.com/tegwick/fluid-core/internal/contract"
"github.com/tegwick/fluid-core/internal/evidence"
"github.com/tegwick/fluid-core/internal/publish"
"github.com/tegwick/fluid-core/internal/runtime"
"github.com/tegwick/fluid-core/internal/signing"
)
// ---------------------------------------------------------------------------
// FluidAPIStandards.md section 36 — minimal v0.1 conformance
//
// An implementation claiming core conformance MUST provide all seven of these.
// They are asserted here rather than claimed in a README, because a conformance
// claim nobody re-checks is a conformance claim that quietly stops being true.
// ---------------------------------------------------------------------------
// 1. A deterministic API contract.
func TestConformance01_DeterministicContract(t *testing.T) {
h := New(t)
// The contract is enforced, not merely declared: an undeclared path is
// refused rather than proxied through.
mustStatus(t, h.Call("c-1", http.MethodGet, "/v1/entries"), http.StatusOK, "declared path")
mustStatus(t, h.Call("c-1", http.MethodGet, "/v1/undeclared"), http.StatusBadRequest, "undeclared path")
mustStatus(t, h.Call("c-1", http.MethodDelete, "/v1/entries"), http.StatusBadRequest, "undeclared method")
// Determinism: the same request must produce the same outcome every time.
first := h.Call("c-1", http.MethodGet, "/v1/entries")
for i := 0; i < 20; i++ {
again := h.Call("c-1", http.MethodGet, "/v1/entries")
if again.Code != first.Code || again.Body.String() != first.Body.String() {
t.Fatal("identical requests produced different responses")
}
}
}
// 2. An identifiable active interface revision.
func TestConformance02_IdentifiableRevision(t *testing.T) {
h := New(t)
rec := h.Call("c-1", http.MethodGet, "/v1/entries")
if got := rec.Header().Get("X-FLUID-Revision"); got != "R-1" {
t.Errorf("response does not identify its revision: %q", got)
}
// The revision that served a request must also be identifiable afterwards,
// from evidence rather than from the response the consumer happened to keep.
h.Drain()
for _, ev := range h.Telemetry() {
if ev.Revision == nil || *ev.Revision == "" {
t.Fatal("a telemetry event does not name the revision that served it")
}
}
}
// 3. Interface telemetry.
func TestConformance03_Telemetry(t *testing.T) {
h := New(t)
for i := 0; i < 5; i++ {
h.Call("c-1", http.MethodGet, "/v1/entries")
}
h.Call("c-1", http.MethodGet, "/v1/undeclared")
h.Drain()
rows := h.Telemetry()
if len(rows) != 6 {
t.Fatalf("recorded %d telemetry events, want 6", len(rows))
}
var errors int
for _, ev := range rows {
if ev.Kind == contract.FluidTelemetryKindError {
errors++
}
if ev.Resolution == nil {
t.Error("an event does not record why its revision was chosen")
}
}
if errors != 1 {
t.Errorf("recorded %d error events, want 1", errors)
}
}
// 4. Declared interface evolutionary intent.
func TestConformance04_DeclaredIntent(t *testing.T) {
h := New(t)
ctx := context.Background()
active, err := h.Intents.Active(ctx)
if err != nil {
t.Fatalf("no active intent: %v", err)
}
if !active.Mode.Valid() {
t.Errorf("active intent declares no valid authority mode")
}
// Every revision must be bound to the intent version governing it.
for _, rev := range []contract.RevisionID{"R-1", "R-2"} {
governing, err := h.Intents.GoverningIntent(ctx, rev)
if err != nil {
t.Errorf("%s has no governing intent: %v", rev, err)
continue
}
if governing.Digest == "" {
t.Errorf("%s is bound to an intent with no digest", rev)
}
}
}
// 5. An auditable link from evidence to proposed change.
func TestConformance05_EvidenceToChangeIsAuditable(t *testing.T) {
h := New(t)
ctx := context.Background()
loop := RunFullLoop(t, h)
// Pressure -> hypothesis -> revision -> experiment -> decision must be
// traversable without guessing.
pressure, err := h.Store.Record(ctx, contract.KindPressure, string(loop.Pressure))
if err != nil {
t.Fatalf("pressure not retrievable: %v", err)
}
if len(pressure) == 0 {
t.Fatal("pressure record is empty")
}
hyp, err := h.Hypotheses.Get(ctx, loop.Hypothesis)
if err != nil {
t.Fatal(err)
}
if len(hyp.Observation.EvidenceRefs) == 0 {
t.Error("the hypothesis cites no evidence")
}
if len(hyp.CandidateRevisionRefs) == 0 {
t.Error("the hypothesis names no candidate revision")
}
if len(hyp.ExperimentRefs) == 0 {
t.Error("the hypothesis names no experiment")
}
// And the revision must carry events reaching back to the decision.
events := h.Events(string(loop.Candidate))
var sawDecision bool
for _, ev := range events {
if strings.HasPrefix(ev.EventType, "PROMOTION_DECIDED") {
sawDecision = true
if len(ev.Inputs) == 0 {
t.Error("the promotion decision cites no inputs")
}
}
}
if !sawDecision {
t.Error("no promotion decision is recorded against the candidate")
}
}
// 6. Explicit responsibility boundaries.
func TestConformance06_ResponsibilityBoundaries(t *testing.T) {
h := New(t)
// The interface owns representation and routing; the backend owns the data.
// When the backend is gone, the interface must report that rather than
// inventing a response.
h.StopAdapter("R-1")
rec := h.Call("c-1", http.MethodGet, "/v1/entries")
if rec.Code != http.StatusBadGateway && rec.Code != http.StatusGatewayTimeout {
t.Fatalf("status %d; a dead backend must surface as a gateway error", rec.Code)
}
if strings.Contains(rec.Body.String(), "127.0.0.1") {
t.Error("the backend address leaked into the error response")
}
}
// 7. Deterministic security enforcement independent of probabilistic decisions.
func TestConformance07_DeterministicSecurity(t *testing.T) {
h := New(t)
// An unsigned descriptor is refused by the router regardless of anything
// else about it.
unsigned := contract.Revision{
SchemaVersion: "0.1", ID: "R-rogue", Interface: Interface,
State: contract.RevisionStateStable,
Contract: contract.RevisionContract{Type: contract.RevisionContractTypeOpenapi, Digest: contract.Digest("sha256:" + strings.Repeat("9", 64))},
Runtime: contract.RevisionRuntime{Upstream: h.AdapterURL("R-1")},
Intent: contract.RevisionIntent{Version: "IEI-1"},
Policy: contract.RevisionPolicy{
Compatibility: contract.RevisionPolicyCompatibilityAdditive,
SecurityCheck: contract.RevisionPolicySecurityCheckPassed,
},
}
if err := h.Registry.PutRevision(unsigned); err == nil {
t.Fatal("an unsigned descriptor was accepted by the router")
}
// A signed descriptor whose content was altered afterwards must also fail.
sig, err := h.Signer.Sign(contract.RevisionDescriptorDocument{Revision: unsigned})
if err != nil {
t.Fatal(err)
}
tampered := unsigned
tampered.Runtime.Upstream = "http://attacker.invalid"
tampered.Signature = &contract.RevisionSignature{
Algorithm: contract.RevisionSignatureAlgorithm(sig.Algorithm),
KeyID: sig.KeyID, Value: sig.Value,
}
if err := h.Registry.PutRevision(tampered); err == nil {
t.Fatal("a tampered descriptor was accepted by the router")
}
}
// ---------------------------------------------------------------------------
// ArchitectureBlueprint.md section 55 — architectural invariants
//
// Only the mechanically checkable subset is asserted. The rest are design
// properties a test cannot settle, and pretending otherwise would be worse
// than leaving them to review.
// ---------------------------------------------------------------------------
// Invariant 1: runtime behaviour remains deterministic.
// Invariant 2: evolution can stop without stopping the API.
func TestInvariant02_EvolutionCanStopWithoutStoppingTheAPI(t *testing.T) {
h := New(t)
// Kill the entire observation and control plane: telemetry delivery first,
// then the evidence store itself.
h.Drain()
if err := h.Store.Close(); err != nil {
t.Fatal(err)
}
// The data plane must keep serving from cached published configuration.
for i := 0; i < 10; i++ {
rec := h.Call("c-1", http.MethodGet, "/v1/entries")
if rec.Code != http.StatusOK {
t.Fatalf("request %d returned %d after the control plane died", i, rec.Code)
}
}
}
// Invariant 3: every published revision is identifiable.
// Invariant 4: every revision has an explicit contract.
// Invariant 5: every revision is governed by a specific intent version.
func TestInvariant03to05_RevisionIdentityContractAndIntent(t *testing.T) {
h := New(t)
ctx := context.Background()
for _, id := range []contract.RevisionID{"R-1", "R-2"} {
d, err := h.Registry.Revision(id)
if err != nil {
t.Fatalf("%s is not registered: %v", id, err)
}
if d.ID == "" {
t.Errorf("%s has no identity", id)
}
if d.Contract.Digest == "" {
t.Errorf("%s has no contract digest", id)
}
if _, ok := h.Validator.Contract(d.Contract.Digest); !ok {
t.Errorf("%s names a contract the gateway cannot enforce", id)
}
if d.Intent.Version == "" {
t.Errorf("%s declares no governing intent", id)
}
if _, err := h.Intents.GoverningIntent(ctx, id); err != nil {
t.Errorf("%s has no recorded intent binding: %v", id, err)
}
}
}
// Invariant 7: every experiment has guardrails and stop conditions.
func TestInvariant07_ExperimentsAreBounded(t *testing.T) {
h := New(t)
ctx := context.Background()
unbounded := contract.FluidExperiment{
ID: "E-unbounded", HypothesisRefs: []contract.HypothesisID{"H-1"},
ControlRevision: "R-1", CandidateRevisions: []contract.RevisionID{"R-2"},
Allocation: map[string]contract.UnitInterval{"control": 0.9, "candidate": 0.1},
Metrics: contract.FluidExperimentMetrics{Primary: []string{"m"}},
// No stop conditions.
}
if _, err := h.Experiments.Design(ctx, unbounded, Operator); err == nil {
t.Error("an experiment with no stop condition was designed")
}
}
// Invariant 8: every promotion is auditable.
// Invariant 9: AI-generated artifacts are untrusted until verified.
func TestInvariant09_GeneratedArtifactsAreUntrustedUntilVerified(t *testing.T) {
h := New(t)
// A candidate that has not been through the pipeline cannot be published:
// Publish takes a Verified value and nothing else constructs one.
if err := h.Pipeline.Publish(context.Background(), publish.Verified{}); err == nil {
t.Fatal("an unverified value was published")
}
}
// Invariant 10: security policy remains deterministic.
func TestInvariant10_SecurityPolicyIsDeterministic(t *testing.T) {
h := New(t)
// The same descriptor must be judged identically every time.
d, err := h.Registry.Revision("R-1")
if err != nil {
t.Fatal(err)
}
first := h.Registry.CheckRoutable("R-1", "agents")
for i := 0; i < 50; i++ {
if got := h.Registry.CheckRoutable("R-1", "agents"); (got == nil) != (first == nil) {
t.Fatal("routability judgement varied between identical calls")
}
}
_ = d
}
// Invariant 13: multiple revisions may coexist.
func TestInvariant13_RevisionsCoexist(t *testing.T) {
h := New(t)
// Both revisions serve concurrently, distinguished only by resolution.
mustStatus(t, h.CallPinned("c-1", "R-1", http.MethodGet, "/v1/entries"), http.StatusOK, "R-1")
mustStatus(t, h.CallPinned("c-2", "R-2", http.MethodGet, "/v1/entries/latest"), http.StatusOK, "R-2 latest")
// And the convenience resource exists only in R-2, which is what makes the
// two revisions genuinely different contracts rather than a relabelling.
mustStatus(t, h.CallPinned("c-3", "R-1", http.MethodGet, "/v1/entries/latest"),
http.StatusBadRequest, "R-1 must not serve a path it does not declare")
}
// Invariant 14: failed experiments are normal and recoverable.
func TestInvariant14_FailedExperimentsAreRecoverable(t *testing.T) {
h := New(t)
ctx := context.Background()
loop := SeedHypothesis(t, h)
exp := DesignExperiment(t, h, loop.Hypothesis)
if _, _, err := h.Experiments.Start(ctx, exp, 2, "R-1", Operator); err != nil {
t.Fatal(err)
}
// Stopping returns a policy with no rules; installing it puts every
// consumer back on the known-good revision.
_, stopPolicy, err := h.Experiments.Stop(ctx, exp, 3, "R-1", Operator, "guardrail breached")
if err != nil {
t.Fatal(err)
}
if len(stopPolicy.Rules) != 0 {
t.Fatalf("the stop policy still carries %d rules", len(stopPolicy.Rules))
}
if err := h.Registry.PutPolicy(stopPolicy); err != nil {
t.Fatal(err)
}
for i := 0; i < 5; i++ {
rec := h.Call("c-1", http.MethodGet, "/v1/entries")
mustStatus(t, rec, http.StatusOK, "after rollback")
if got := rec.Header().Get("X-FLUID-Revision"); got != "R-1" {
t.Fatalf("traffic did not return to the control revision: %q", got)
}
}
}
// Invariant 16: adaptive cost is controlled independently of runtime
// availability. Exhausting the adaptive budget must not affect serving.
func TestInvariant16_AdaptiveBudgetDoesNotAffectRuntime(t *testing.T) {
h := New(t)
ctx := context.Background()
// Saturate the experiment concurrency limit, the framework's current
// expression of an adaptive budget.
h.Experiments.SetMaxParallel(1)
loop := SeedHypothesis(t, h)
first := DesignExperiment(t, h, loop.Hypothesis)
if _, _, err := h.Experiments.Start(ctx, first, 2, "R-1", Operator); err != nil {
t.Fatal(err)
}
second := DesignExperimentNamed(t, h, loop.Hypothesis, "E-second")
if _, _, err := h.Experiments.Start(ctx, second, 3, "R-1", Operator); err == nil {
t.Fatal("the concurrency limit did not bind")
}
// With adaptation blocked, the runtime is unaffected.
for i := 0; i < 5; i++ {
mustStatus(t, h.Call("c-1", http.MethodGet, "/v1/entries"), http.StatusOK,
"serving with the adaptive budget exhausted")
}
}
// TestSignedDescriptorsRoundTrip guards the property the whole trust chain
// rests on: what the pipeline signs is what the router verifies.
func TestSignedDescriptorsRoundTrip(t *testing.T) {
h := New(t)
d, err := h.Registry.Revision("R-2")
if err != nil {
t.Fatal(err)
}
if d.Signature == nil {
t.Fatal("a registered revision carries no signature")
}
// The registry already verified this on insert; re-verifying here confirms
// the signature travelled with the descriptor rather than being dropped.
verifier := signing.NewVerifier(map[string]ed25519.PublicKey{"conformance-key": h.PublicKey})
if err := verifier.Verify(contract.RevisionDescriptorDocument{Revision: d}, &signing.Signature{
Algorithm: string(d.Signature.Algorithm),
KeyID: d.Signature.KeyID,
Value: d.Signature.Value,
}); err != nil {
t.Fatalf("a registered descriptor does not verify: %v", err)
}
if d.Signature.Algorithm != contract.RevisionSignatureAlgorithmEd25519 {
t.Errorf("unexpected signature algorithm %q", d.Signature.Algorithm)
}
}
// TestTelemetryCarriesNoRawIdentity is the privacy boundary at the seam that
// matters: whatever a consumer sends, the store must not keep it.
func TestTelemetryCarriesNoRawIdentity(t *testing.T) {
h := New(t)
h.Call("bernd@example.com", http.MethodGet, "/v1/entries")
h.Drain()
for _, ev := range h.Telemetry() {
if strings.Contains(ev.ConsumerRef, "@") {
t.Fatalf("a raw consumer identity reached the store: %q", ev.ConsumerRef)
}
if ev.Redaction == nil {
t.Error("an event does not record its redaction status")
}
}
}
var _ = evidence.EventFilter{}
var _ = runtime.Request{}