Some checks failed
ci / build (push) Has been cancelled
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
448 lines
15 KiB
Go
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{}
|