Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/reviews/convergence-boundary.receipt.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"reviewed_tree": "638048ded4504ed3df431637e803348cc2100340",
"program_fingerprint": "ddbd77f1cbc842b3dffcb8e54b53ddcc624a998fb9c5a9ace15028580ab87967"
"reviewed_tree": "cdfe3ed9fd234a119274ac44f54583e993b2e073",
"program_fingerprint": "2277c979a06ee984c09aa32b2ed3d8886f1ae685a647274185a71a75a1a3961c"
}
9 changes: 7 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,16 @@ jobs:
cache-dependency-path: boatstack/go.sum
- name: Build TypeScript frontends and documentation
run: npm ci && npm run test:flow-sdk && npm run docs:check
- name: Prove frontend canonical equivalence
# Required mode forbids the frontend-absent skip path: every
# frontend-dependent conformance proof (canonical equivalence, sugar
# equivalence, and the no-execution/import/expression boundaries) must
# actually run here, because the plain test jobs never install the
# frontend and would skip them silently.
- name: Prove frontend conformance in required mode
working-directory: boatstack
env:
BOATSTACK_REQUIRE_FLOW_FRONTEND: '1'
run: go test ./controlprogram -run 'TestTypeScriptDSLAndRawIRHaveOneCanonicalFingerprint|TestRepositoryOwnedSoftwareDeliveryFlowsShareOneRuntime|TestSoftwareDeliverySugar'
run: go test ./controlprogram -run 'TestTypeScriptDSLAndRawIRHaveOneCanonicalFingerprint|TestRepositoryOwnedSoftwareDeliveryFlowsShareOneRuntime|TestSoftwareDeliverySugar|TestTypeScriptFrontend|TestDomainNeutral'

component:
name: component-${{ matrix.name }}
Expand Down
330 changes: 330 additions & 0 deletions boatstack/delivery_controller_work_loop_test.go

Large diffs are not rendered by default.

121 changes: 121 additions & 0 deletions boatstack/flow/softwaredelivery/artifact_projection_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package softwaredelivery_test

import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"

"github.com/operatorstack/boatstack/boatstack/controlprogram"
softwareflow "github.com/operatorstack/boatstack/boatstack/flow/softwaredelivery"
"github.com/operatorstack/boatstack/boatstack/internal/hostprojection"
)

func artifactFactPredicate(facet, value string) controlprogram.Predicate {
return controlprogram.Predicate{Fact: &controlprogram.FactPredicate{Facet: facet, Statuses: []string{"known"}, Values: []string{value}}}
}

func artifactBoundaryProgram() controlprogram.Document {
mitigated := "mitigated"
return controlprogram.Document{
Schema: controlprogram.SchemaName, SchemaRevision: controlprogram.SchemaRevision,
Program: controlprogram.Program{ID: "incident-response", Version: "1", Description: "human text"},
Description: "incident control program",
Declarations: controlprogram.Declarations{
Capabilities: []string{"service.restart"}, Authorities: []string{"incident-commander"},
Effects: []string{"service.restart"}, Verifiers: []string{"healthcheck"}, InputResolvers: []string{"incident.input"},
},
Facets: []controlprogram.Facet{
{ID: "service", Kind: "enum", Values: []string{"healthy", "degraded"}, Description: "service health"},
{ID: "incident", Kind: "enum", Values: []string{"open", "mitigated"}},
},
Evidence: []controlprogram.Evidence{{ID: "healthcheck", Subject: "service", Kind: "observation", Description: "observed health"}},
Operators: []controlprogram.Operator{{
ID: "restart", Capabilities: []string{"service.restart"}, Authority: controlprogram.AuthorityRequirement{AnyOf: []string{"incident-commander"}},
Effects: []string{"service.restart"}, Verifier: "healthcheck", Recovery: "restart",
Description: "restart the service", ExecutionContext: "preserve",
StateEffect: &controlprogram.StateEffect{Kind: "assignments", Assignments: []controlprogram.StateAssignment{{Facet: "incident", Value: &mitigated}}},
}},
Transitions: []controlprogram.Transition{{
ID: "restart", Operator: "restart", Priority: 10,
Guard: artifactFactPredicate("incident", "open"), Target: artifactFactPredicate("incident", "mitigated"), Description: "restart service",
}},
Targets: []controlprogram.Target{{ID: "mitigated", Predicate: artifactFactPredicate("incident", "mitigated"), Description: "incident mitigated"}},
Entries: []controlprogram.Entry{{ID: "respond", Target: "mitigated", Description: "respond to incident", Inputs: []controlprogram.EntryInput{{ID: "incident", Type: "json", Required: true, Resolver: "incident.input", Config: json.RawMessage(`{"a":1}`)}}}},
}
}

func TestArtifactBindsEveryCanonicalHostProjectionExactly(t *testing.T) {
// control-law: runtime-admits-only-an-exact-source-lock-artifact-projection
// for every canonical host, including Cursor and Gemini, through the same
// artifact boundary that verifies committed projection bytes.
compiled, err := controlprogram.Compile(artifactBoundaryProgram(), nil)
if err != nil {
t.Fatal(err)
}
generated, err := softwareflow.GenerateProjections(compiled, hostprojection.CanonicalIDs())
if err != nil {
t.Fatal(err)
}
hostPaths := map[hostprojection.ID]string{}
for _, host := range hostprojection.CanonicalIDs() {
paths, err := hostprojection.FlowPaths(host, "incident-response-respond")
if err != nil {
t.Fatal(err)
}
for _, path := range paths {
if _, exists := generated[path]; !exists {
t.Fatalf("host %s projection %s was not generated", host, path)
}
if strings.HasSuffix(path, ".md") && !strings.HasSuffix(path, ".gitattributes") {
hostPaths[host] = path
}
}
}
repository := t.TempDir()
sourcePath, lockPath := "flow.ts", "package-lock.json"
source, lock := []byte("declarative source"), []byte("dependency lock")
files := map[string][]byte{sourcePath: source, lockPath: lock}
for path, content := range generated {
files[path] = content
}
for path, content := range files {
absolute := filepath.Join(repository, filepath.FromSlash(path))
if err := os.MkdirAll(filepath.Dir(absolute), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(absolute, content, 0o600); err != nil {
t.Fatal(err)
}
}
artifact, _, err := controlprogram.NewArtifact(compiled, controlprogram.ArtifactInput{
CompilerVersion: "compiler-1", SourcePath: sourcePath, Source: source,
DependencyLockPath: lockPath, DependencyLock: lock,
Projections: hostprojection.CanonicalIDs(), GeneratedProjections: generated,
})
if err != nil {
t.Fatal(err)
}
if _, err := controlprogram.CheckArtifact(repository, artifact, "compiler-1", nil, hostprojection.CanonicalIDs(), softwareflow.GenerateProjections); err != nil {
t.Fatalf("exact four-host artifact was refused: %v", err)
}
for _, host := range []hostprojection.ID{hostprojection.Cursor, hostprojection.Gemini} {
path := hostPaths[host]
absolute := filepath.Join(repository, filepath.FromSlash(path))
if err := os.WriteFile(absolute, append(generated[path], []byte("\ntampered")...), 0o600); err != nil {
t.Fatal(err)
}
_, err := controlprogram.CheckArtifact(repository, artifact, "compiler-1", nil, hostprojection.CanonicalIDs(), softwareflow.GenerateProjections)
if err == nil || !strings.Contains(err.Error(), path) || !strings.Contains(err.Error(), "does not match compiled program") {
t.Fatalf("tampered %s projection was admitted: %v", host, err)
}
if err := os.WriteFile(absolute, generated[path], 0o600); err != nil {
t.Fatal(err)
}
}
narrowed := append([]hostprojection.ID(nil), hostprojection.Codex, hostprojection.Claude)
if _, err := controlprogram.CheckArtifact(repository, artifact, "compiler-1", nil, narrowed, softwareflow.GenerateProjections); err == nil || !strings.Contains(err.Error(), "FLOW_PROJECTION_SELECTION_STALE") {
t.Fatalf("narrowed projection selection was admitted: %v", err)
}
}
77 changes: 77 additions & 0 deletions boatstack/internal/softwaredelivery/engine/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1095,3 +1095,80 @@ func TestOwnedExternalExecutionErrorRequiresRecoveryWithoutRollback(t *testing.T
t.Fatalf("ambiguous external error was collapsed: effects=%+v journal=%+v receipts=%d", effects, journal, len(receipts.values))
}
}

func TestForgedRepositoryAuthorityIsRefusedBeforeEffects(t *testing.T) {
// control-law: missing-expired-changed-or-forged-authority-fails-before-effects
now := time.Unix(30, 0).UTC()
observer := &sequenceObserver{items: []model.Observation{
observation(model.PhaseObserved, "source"),
observation(model.PhaseObserved, "source"),
}}
journal, effectsPort, receipts, lock := &fakeJournal{}, &fakeEffects{}, &memoryReceipts{}, &fakeLock{}
kernel, err := New(
testRegistry(t), syntheticObjectiveContracts(t), syntheticProgram,
observer, fixedClock{now}, fakeLocker{lock}, journal, effectsPort, receipts,
)
if err != nil {
t.Fatal(err)
}
apply := request(t, now)
apply.Authority.Receipts[0].Fingerprint = "forged-configuration-fingerprint"

resolved, err := kernel.Resolve(context.Background(), apply.ResolveRequest)
if err != nil {
t.Fatal(err)
}
if resolved.Decision.Kind != supervisor.DecisionRefused || !strings.Contains(resolved.Decision.Reason, "not bound to current configuration evidence") {
t.Fatalf("forged authority resolution = %+v, want configuration-evidence refusal", resolved.Decision)
}
if resolved.Prescription.ID != "" {
t.Fatalf("forged authority minted a prescription: %+v", resolved.Prescription)
}

if _, applyErr := kernel.Apply(context.Background(), apply); applyErr == nil {
t.Fatal("apply with forged repository authority succeeded")
}
if effectsPort.transition.ID != "" || effectsPort.executions != 0 || journal.begun != 0 || len(receipts.values) != 0 {
t.Fatalf("forged authority crossed the effect boundary: prepared=%q effects=%d journal=%d receipts=%d", effectsPort.transition.ID, effectsPort.executions, journal.begun, len(receipts.values))
}
}

func TestTargetedAndUntargetedResolutionShareOnePrescription(t *testing.T) {
// control-law: targeted-and-untargeted-resolution-use-one-canonical-selection-relation
now := time.Unix(30, 0).UTC()
observer := &sequenceObserver{items: []model.Observation{
observation(model.PhaseObserved, "source"),
observation(model.PhaseObserved, "source"),
}}
kernel, err := New(
testRegistry(t), syntheticObjectiveContracts(t), syntheticProgram,
observer, fixedClock{now},
fakeLocker{&fakeLock{}}, &fakeJournal{}, &fakeEffects{}, &memoryReceipts{},
)
if err != nil {
t.Fatal(err)
}
req := request(t, now).ResolveRequest
req.Requested = ""
untargeted, err := kernel.Resolve(context.Background(), req)
if err != nil {
t.Fatal(err)
}
if untargeted.Decision.Kind != supervisor.DecisionPrescribed || untargeted.Prescription.ID == "" {
t.Fatalf("untargeted resolution = %+v, want PRESCRIBED with a prescription", untargeted.Decision)
}
req.Requested = "test.advance"
targeted, err := kernel.Resolve(context.Background(), req)
if err != nil {
t.Fatal(err)
}
if targeted.Decision.Kind != supervisor.DecisionPrescribed || targeted.Prescription.ID == "" {
t.Fatalf("targeted resolution = %+v, want PRESCRIBED with a prescription", targeted.Decision)
}
if untargeted.Prescription.ID != targeted.Prescription.ID {
t.Fatalf("targeted and untargeted resolution derived different prescriptions: %q vs %q", untargeted.Prescription.ID, targeted.Prescription.ID)
}
if untargeted.Admission.TransitionID != targeted.Admission.TransitionID || untargeted.Admission.PrescriptionID != targeted.Admission.PrescriptionID {
t.Fatalf("targeted and untargeted resolution admitted different transitions: %+v vs %+v", untargeted.Admission, targeted.Admission)
}
}
25 changes: 25 additions & 0 deletions boatstack/kernel/relation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,31 @@ func TestRelationTargetedAndUntargetedUseSameCandidates(t *testing.T) {
}
}

func TestRelationExplicitOnlyCandidateNeverAdvancesImplicitly(t *testing.T) {
// control-law: explicit-only-transitions-never-become-implicit-progress
candidates := []RelationCandidate{
{ID: "explicit-control", Rank: 1, Priority: 1, Selectable: false},
{ID: "routine-advance", Rank: 2, Priority: 1, Selectable: true},
}
untargeted, trace := RelateWithTrace(RelationInput{Candidates: candidates})
if untargeted.Kind != Prescribed || untargeted.Transition != "routine-advance" {
t.Fatalf("untargeted selection = %#v", untargeted)
}
for _, candidate := range trace {
if candidate.TransitionID == "explicit-control" && candidate.Survived {
t.Fatalf("explicit-only candidate survived untargeted selection: %#v", trace)
}
}
targeted := Relate(RelationInput{Requested: "explicit-control", Candidates: candidates})
if targeted.Kind != Prescribed || targeted.Transition != "explicit-control" {
t.Fatalf("explicit request = %#v", targeted)
}
onlyExplicit := Relate(RelationInput{Candidates: candidates[:1]})
if onlyExplicit.Kind != Unresolved {
t.Fatalf("explicit-only field must not progress implicitly: %#v", onlyExplicit)
}
}

func TestRelationReportsEqualPreferenceAndMarkedState(t *testing.T) {
tied, trace := RelateWithTrace(RelationInput{Candidates: []RelationCandidate{
{ID: "a", Priority: 1, Selectable: true},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Software-delivery capabilities frozen as regression contracts

The delivery pipeline's load-bearing behaviors are now pinned by explicit regression tests at their real boundaries, so later refactors must preserve capabilities rather than merely compile. New contracts prove that an explicit-only transition never becomes implicit progress (skipped untargeted, prescribable when requested, unresolved when it is the only candidate), that targeted and untargeted resolution derive the same prescription through the engine's public resolve path, that forged repository authority is refused at resolve time and never reaches the journal or effect ports, that every canonical host projection is byte-bound into the control-program artifact and any tampered projection fails closed as stale, and that the foreground work loop suspends, binds answers to the exact requested revision, rejects stale answers, refuses apply while work is incomplete, and resumes only its originating execution through the delivery surface. CI now runs every frontend-dependent conformance proof in required mode, closing a gap where the no-execution boundary could skip silently when the frontend was not installed.
Loading