diff --git a/.github/tests/test_repository_contract.py b/.github/tests/test_repository_contract.py index 3e5573e..92f26d0 100644 --- a/.github/tests/test_repository_contract.py +++ b/.github/tests/test_repository_contract.py @@ -415,7 +415,10 @@ def test_public_tree_excludes_private_context_and_v1_operating_guidance(self) -> def test_general_kernel_is_domain_neutral_and_owns_shared_control_laws(self) -> None: kernel = REPO / "boatstack" / "kernel" kernel_files = sorted(kernel.glob("*.go")) - source = "\n".join(path.read_text() for path in kernel_files) + production_files = [ + path for path in kernel_files if not path.name.endswith("_test.go") + ] + source = "\n".join(path.read_text() for path in production_files) for token in ( "git", "repository", "worktree", "branch", "pull request", "coding agent", "publication", @@ -424,13 +427,17 @@ def test_general_kernel_is_domain_neutral_and_owns_shared_control_laws(self) -> boatstack_packages = "github.com/operatorstack/boatstack/boatstack/" kernel_package = boatstack_packages + "kernel" + conformance_package = kernel_package + "/conformance" kernel_metadata = go_source_metadata(kernel_files) for path, metadata in zip(kernel_files, kernel_metadata, strict=True): + allowed = {kernel_package} + if path.name.endswith("_test.go"): + allowed.add(conformance_package) invalid = [ import_path for import_path in metadata["imports"] if import_path.startswith(boatstack_packages) - and import_path != kernel_package + and import_path not in allowed ] self.assertEqual([], invalid, f"kernel dependency direction: {path}") @@ -453,6 +460,37 @@ def test_general_kernel_is_domain_neutral_and_owns_shared_control_laws(self) -> f"kernel test fixture imports software delivery: {path}", ) + conformance_files = sorted((kernel / "conformance").glob("*.go")) + self.assertTrue(conformance_files) + conformance_source = "\n".join(path.read_text() for path in conformance_files) + for token in ( + "git", "repository", "worktree", "pull request", + "softwaredelivery", "subprocess", + ): + self.assertNotRegex( + conformance_source.lower(), + rf"\b{re.escape(token)}\b", + token, + ) + for path, metadata in zip( + conformance_files, + go_source_metadata(conformance_files), + strict=True, + ): + invalid = [ + import_path + for import_path in metadata["imports"] + if import_path.startswith(boatstack_packages) + and import_path != kernel_package + ] + invalid.extend( + import_path + for import_path in metadata["imports"] + if import_path in {"os", "os/exec", "path/filepath"} + ) + self.assertEqual([], invalid, f"kernel conformance dependency: {path}") + self.assertIn("conformance.IntegerFixture().Run(t)", test_source) + runtime = (kernel / "runtime.go").read_text() software_relation = ( REPO / "boatstack" / "internal" / "softwaredelivery" / @@ -527,7 +565,7 @@ def test_kernel_runtime_has_no_production_consumer_yet(self) -> None: path for path in sorted((REPO / "boatstack").rglob("*.go")) if not path.name.endswith("_test.go") - and path.parent != REPO / "boatstack" / "kernel" + and not path.is_relative_to(REPO / "boatstack" / "kernel") ] consumers = [ str(path.relative_to(REPO)) diff --git a/README.md b/README.md index 69a9e35..1a8752a 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,11 @@ language models, or coding-agent semantics. > ABI, configuration schema, generated skills, and state format may change > without a compatibility path. Audit it before using it on important work. +During alpha development, Boatstack does not preserve backward compatibility. +Breaking architecture changes update all in-tree consumers together instead of +adding compatibility shims. Existing local installations may need to be reset +or regenerated. + ## Try it Boatstack installs into an existing Git repository. The repository must have an @@ -107,6 +112,7 @@ control graph. The complete list is generated from the registry in the | **Transactions** | Prescriptions bind the exact control instance, state revision, program, objective binding, observation, transition, and authority. Apply rechecks that boundary before execution. | | **Verification and receipts** | Fresh postcondition verification, atomic state-and-receipt commits, and immutable transition facts. | | **Recovery** | A durable effect attempt precedes execution. Interrupted or uncertain outcomes enter explicit recovery instead of blindly repeating an effect. | +| **Conformance** | A reusable, domain-neutral suite verifies objective handling, authority, freshness, recovery, atomic commit, replay isolation, concurrency, and marked-state reachability against any explicitly mapped domain fixture. | ### Software delivery diff --git a/boatstack/AGENTS.md b/boatstack/AGENTS.md index d8af7ee..0baf5cf 100644 --- a/boatstack/AGENTS.md +++ b/boatstack/AGENTS.md @@ -2,6 +2,15 @@ Read this before opening a PR that touches anything under `boatstack/`. +## Alpha compatibility policy + +Boatstack does not preserve backward compatibility during alpha development. +Change existing interfaces, state, configuration, and generated surfaces when +the current architecture requires it. Update every in-tree consumer in the same +delivery. Do not add compatibility shims, dual paths, or migrations unless a +task explicitly requires one. Local alpha installations may be reset or +regenerated after a breaking change. + ## Every Boatstack PR REQUIRES a new release note CI runs `.github/scripts/release_notes.py check-policy` in the diff --git a/boatstack/kernel/conformance/conformance.go b/boatstack/kernel/conformance/conformance.go new file mode 100644 index 0000000..1edecb0 --- /dev/null +++ b/boatstack/kernel/conformance/conformance.go @@ -0,0 +1,892 @@ +// Package conformance provides a reusable verifier for kernel domains. +package conformance + +import ( + "context" + "encoding/json" + "fmt" + "reflect" + "sync" + "testing" + "time" + + "github.com/operatorstack/boatstack/boatstack/kernel" +) + +// Setup identifies the domain state required by one conformance case. +type Setup string + +const ( + SetupUnbound Setup = "unbound" + SetupBound Setup = "bound" + SetupMaintenanceAbsent Setup = "maintenance-absent" + SetupMaintenanceBound Setup = "maintenance-bound" + SetupConcurrentSameBase Setup = "concurrent-same-base" +) + +// Snapshot exposes only deterministic test evidence. It is not kernel state. +type Snapshot struct { + State kernel.ControlState + Observation kernel.Observation + Effects map[string]int + Receipts []kernel.Receipt + CommitCount int +} + +// Scenario maps domain-specific fixture operations onto domain-neutral laws. +// The suite never infers these roles from transition or operation names. +type Scenario struct { + InstanceID string + Objective kernel.Objective + RevisedObjective kernel.Objective + ConflictingObjective kernel.Objective + AlternateProgram kernel.Program + Authority kernel.Authority + BindTransition string + AdvanceTransitions []string + MaintenanceTransition string + RecoveryTransition string + RecoveryCapability kernel.Capability + ExtraCapability kernel.Capability + ChangeObservation func() + RebindObjective func(kernel.Objective) + BumpStateRevision func() + IndependentLocker func() kernel.Locker + VerifyCommitted func(Snapshot, Snapshot, kernel.Receipt) error + InterruptNextOperator func() + PanicNextOperator func() + FailNextCommit func() + RetargetInstance func(string) + Snapshot func() Snapshot +} + +// KernelConformance binds kernel ports to explicit scenario roles. New must +// return an isolated fixture for every requested Setup. +type KernelConformance struct { + Domain kernel.Domain + Operator kernel.Operator + CapabilityClassifier kernel.CapabilityClassifier + Store kernel.Store + Locker kernel.Locker + Clock kernel.Clock + Program kernel.Program + Scenario Scenario + New func(testing.TB, Setup) KernelConformance +} + +// Run exercises the kernel control laws against fresh domain fixtures. +func (suite KernelConformance) Run(t *testing.T) { + t.Helper() + + t.Run("objective_binding", suite.objectiveBinding) + t.Run("objective_absence", suite.objectiveAbsence) + t.Run("maintenance_preserves_exact_binding", suite.maintenancePreservesExactBinding) + t.Run("objective_revision_invalidates_prescription_before_effects", suite.objectiveRevisionInvalidatesPrescription) + t.Run("state_revision_invalidates_prescription_before_effects", suite.stateRevisionInvalidatesPrescription) + t.Run("program_fingerprint_invalidates_prescription_before_effects", suite.programFingerprintInvalidatesPrescription) + t.Run("stale_prescription_precedes_effects", suite.stalePrescriptionPrecedesEffects) + t.Run("authority_denial_fails_closed", suite.authorityDenialFailsClosed) + t.Run("future_authority_fails_closed", suite.futureAuthorityFailsClosed) + t.Run("capability_classifier_cannot_be_weakened", suite.capabilityClassifierCannotBeWeakened) + t.Run("targeted_and_untargeted_share_one_relation", suite.targetedAndUntargetedShareRelation) + t.Run("interrupted_operator_requires_explicit_recovery", suite.interruptedOperatorRequiresExplicitRecovery) + t.Run("recovery_authority_fails_closed", suite.recoveryAuthorityFailsClosed) + t.Run("process_panic_requires_explicit_recovery", suite.processPanicRequiresExplicitRecovery) + t.Run("atomic_commit_failure_requires_recovery_without_duplicate_effect", suite.atomicCommitFailureRequiresRecovery) + t.Run("failed_recovery_preserves_original_obligation", suite.failedRecoveryPreservesOriginalObligation) + t.Run("prescription_cannot_replay_across_instances", suite.prescriptionCannotReplayAcrossInstances) + t.Run("concurrent_apply_commits_once_before_loser_effect", suite.concurrentApplyCommitsOnce) + t.Run("commit_compare_and_swap_rejects_intervening_state", suite.commitCompareAndSwapRejectsIntervention) + t.Run("program_reaches_marked_state", suite.programReachesMarkedState) +} + +func (suite KernelConformance) objectiveBinding(t *testing.T) { + fixture, runtime := suite.fresh(t, SetupUnbound) + before := fixture.Scenario.Snapshot() + receipt := resolveAndApply(t, runtime, fixture.Program, fixture.Scenario, fixture.Scenario.BindTransition, &fixture.Scenario.Objective) + after := fixture.Scenario.Snapshot() + if after.State.ObjectiveBinding == nil || !after.State.ObjectiveBinding.Matches(fixture.Scenario.Objective) { + t.Fatal("control-law objective-binding: apply did not bind the exact objective") + } + if after.CommitCount != before.CommitCount+1 || len(after.Receipts) != len(before.Receipts)+1 || receipt.ObjectiveBinding == nil { + t.Fatalf("control-law objective-binding: commit/receipt evidence is incomplete: %#v", after) + } +} + +func (suite KernelConformance) objectiveAbsence(t *testing.T) { + fixture, runtime := suite.fresh(t, SetupMaintenanceAbsent) + before := fixture.Scenario.Snapshot() + resolveAndApply(t, runtime, fixture.Program, fixture.Scenario, fixture.Scenario.MaintenanceTransition, &fixture.Scenario.ConflictingObjective) + after := fixture.Scenario.Snapshot() + if before.State.ObjectiveBinding != nil || after.State.ObjectiveBinding != nil { + t.Fatal("control-law objective-absence: maintenance synthesized an objective binding") + } +} + +func (suite KernelConformance) maintenancePreservesExactBinding(t *testing.T) { + fixture, runtime := suite.fresh(t, SetupMaintenanceBound) + before := fixture.Scenario.Snapshot() + resolveAndApply(t, runtime, fixture.Program, fixture.Scenario, fixture.Scenario.MaintenanceTransition, &fixture.Scenario.ConflictingObjective) + after := fixture.Scenario.Snapshot() + if before.State.ObjectiveBinding == nil || after.State.ObjectiveBinding == nil || *after.State.ObjectiveBinding != *before.State.ObjectiveBinding { + t.Fatal("control-law objective-preservation: maintenance changed the exact binding") + } +} + +func (suite KernelConformance) objectiveRevisionInvalidatesPrescription(t *testing.T) { + fixture, runtime := suite.fresh(t, SetupBound) + transition := fixture.Scenario.AdvanceTransitions[0] + request, prescription := resolve(t, runtime, fixture.Scenario, transition, &fixture.Scenario.Objective, fixture.Scenario.Authority) + fixture.Scenario.RebindObjective(fixture.Scenario.RevisedObjective) + before := fixture.Scenario.Snapshot() + request.Objective = &fixture.Scenario.RevisedObjective + _, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + after := fixture.Scenario.Snapshot() + if unchangedErr := refusedApplyMutationError(before, after, transition); !kernel.IsStale(err) || unchangedErr != nil { + t.Fatalf("control-law objective-revision-freshness: error=%v mutation=%v before=%#v after=%#v", err, unchangedErr, before, after) + } +} + +func (suite KernelConformance) stateRevisionInvalidatesPrescription(t *testing.T) { + fixture, runtime := suite.fresh(t, SetupBound) + transition := fixture.Scenario.AdvanceTransitions[0] + request, prescription := resolve(t, runtime, fixture.Scenario, transition, &fixture.Scenario.Objective, fixture.Scenario.Authority) + fixture.Scenario.BumpStateRevision() + before := fixture.Scenario.Snapshot() + _, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + after := fixture.Scenario.Snapshot() + if unchangedErr := refusedApplyMutationError(before, after, transition); !kernel.IsStale(err) || unchangedErr != nil { + t.Fatalf("control-law state-revision-freshness: error=%v mutation=%v before=%#v after=%#v", err, unchangedErr, before, after) + } +} + +func (suite KernelConformance) programFingerprintInvalidatesPrescription(t *testing.T) { + fixture, runtime := suite.fresh(t, SetupBound) + transition := fixture.Scenario.AdvanceTransitions[0] + request, prescription := resolve(t, runtime, fixture.Scenario, transition, &fixture.Scenario.Objective, fixture.Scenario.Authority) + alternate, err := kernel.NewRuntime(fixture.Scenario.AlternateProgram, fixture.Domain, fixture.Operator, fixture.CapabilityClassifier, fixture.Store, fixture.Locker, fixture.Clock) + if err != nil { + t.Fatal(err) + } + before := fixture.Scenario.Snapshot() + _, err = alternate.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + after := fixture.Scenario.Snapshot() + if unchangedErr := refusedApplyMutationError(before, after, transition); err == nil || unchangedErr != nil { + t.Fatalf("control-law program-fingerprint-freshness: error=%v mutation=%v before=%#v after=%#v", err, unchangedErr, before, after) + } +} + +func (suite KernelConformance) stalePrescriptionPrecedesEffects(t *testing.T) { + fixture, runtime := suite.fresh(t, SetupBound) + transition := fixture.Scenario.AdvanceTransitions[0] + request, prescription := resolve(t, runtime, fixture.Scenario, transition, &fixture.Scenario.Objective, fixture.Scenario.Authority) + before := fixture.Scenario.Snapshot() + fixture.Scenario.ChangeObservation() + before = fixture.Scenario.Snapshot() + _, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + after := fixture.Scenario.Snapshot() + if unchangedErr := refusedApplyMutationError(before, after, transition); !kernel.IsStale(err) || unchangedErr != nil { + t.Fatalf("control-law prescription-freshness: error=%v mutation=%v before=%#v after=%#v", err, unchangedErr, before, after) + } +} + +func (suite KernelConformance) authorityDenialFailsClosed(t *testing.T) { + fixture, runtime := suite.fresh(t, SetupBound) + transition := fixture.Scenario.AdvanceTransitions[0] + request, prescription := resolve(t, runtime, fixture.Scenario, transition, &fixture.Scenario.Objective, fixture.Scenario.Authority) + before := fixture.Scenario.Snapshot() + resolution, err := resolveWithoutMutation(context.Background(), runtime, fixture.Scenario, kernel.ResolveRequest{InstanceID: fixture.Scenario.InstanceID, Objective: &fixture.Scenario.Objective, Requested: transition}) + if err != nil || resolution.Decision.Kind != kernel.Frontier { + t.Fatalf("control-law authority-denial: decision=%#v error=%v", resolution.Decision, err) + } + request.Authority = kernel.Authority{} + _, err = runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + after := fixture.Scenario.Snapshot() + if unchangedErr := refusedApplyMutationError(before, after, transition); !kernel.IsStale(err) || unchangedErr != nil { + t.Fatalf("control-law authority-denial: apply error=%v mutation=%v before=%#v after=%#v", err, unchangedErr, before, after) + } +} + +func (suite KernelConformance) futureAuthorityFailsClosed(t *testing.T) { + fixture, runtime := suite.fresh(t, SetupBound) + transition := fixture.Scenario.AdvanceTransitions[0] + authority := fixture.Scenario.Authority + authority.Receipts = append([]kernel.AuthorityReceipt(nil), authority.Receipts...) + authority.Receipts[0].IssuedAt = fixture.Clock.Now().Add(time.Second) + resolution, err := resolveWithoutMutation(context.Background(), runtime, fixture.Scenario, kernel.ResolveRequest{InstanceID: fixture.Scenario.InstanceID, Objective: &fixture.Scenario.Objective, Authority: authority, Requested: transition}) + if err != nil || resolution.Decision.Kind != kernel.Refused { + t.Fatalf("control-law authority-time-validity: decision=%#v error=%v", resolution.Decision, err) + } +} + +func (suite KernelConformance) capabilityClassifierCannotBeWeakened(t *testing.T) { + fixture := suite.fixture(t, SetupBound) + transition := fixture.Scenario.AdvanceTransitions[0] + classifier := strengtheningClassifier{base: fixture.CapabilityClassifier, transitionID: transition, capability: fixture.Scenario.ExtraCapability} + runtime, err := kernel.NewRuntime(fixture.Program, fixture.Domain, fixture.Operator, classifier, fixture.Store, fixture.Locker, fixture.Clock) + if err != nil { + t.Fatal(err) + } + authority := withoutCapability(fixture.Scenario.Authority, fixture.Scenario.ExtraCapability) + resolution, err := resolveWithoutMutation(context.Background(), runtime, fixture.Scenario, kernel.ResolveRequest{InstanceID: fixture.Scenario.InstanceID, Objective: &fixture.Scenario.Objective, Authority: authority, Requested: transition}) + if err != nil || resolution.Decision.Kind != kernel.Frontier { + t.Fatalf("control-law capability-non-weakening: decision=%#v error=%v", resolution.Decision, err) + } +} + +func (suite KernelConformance) targetedAndUntargetedShareRelation(t *testing.T) { + fixture, runtime := suite.fresh(t, SetupBound) + ctx := context.Background() + untargetedRequest := kernel.ResolveRequest{InstanceID: fixture.Scenario.InstanceID, Objective: &fixture.Scenario.Objective, Authority: fixture.Scenario.Authority} + untargeted, err := resolveWithoutMutation(ctx, runtime, fixture.Scenario, untargetedRequest) + if err != nil || untargeted.Decision.Kind != kernel.Prescribed || untargeted.Prescription == nil { + t.Fatalf("control-law canonical-relation: untargeted=%#v error=%v", untargeted.Decision, err) + } + targetedRequest := untargetedRequest + targetedRequest.Requested = untargeted.Decision.Transition + targeted, err := resolveWithoutMutation(ctx, runtime, fixture.Scenario, targetedRequest) + if err != nil || targeted.Decision.Kind != kernel.Prescribed || targeted.Prescription == nil || targeted.Prescription.ID != untargeted.Prescription.ID { + t.Fatalf("control-law canonical-relation: targeted=%#v error=%v", targeted.Decision, err) + } + applyAndRequireCommit(t, runtime, fixture.Program, fixture.Scenario, targetedRequest, *targeted.Prescription) +} + +func (suite KernelConformance) interruptedOperatorRequiresExplicitRecovery(t *testing.T) { + fixture, runtime := suite.fresh(t, SetupBound) + transition := fixture.Scenario.AdvanceTransitions[0] + fixture.Scenario.InterruptNextOperator() + request, prescription := resolve(t, runtime, fixture.Scenario, transition, &fixture.Scenario.Objective, fixture.Scenario.Authority) + before := fixture.Scenario.Snapshot() + _, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + interrupted := fixture.Scenario.Snapshot() + if attemptErr := unresolvedAttemptError(before, interrupted, prescription); !kernel.IsRecoveryRequired(err) || attemptErr != nil || effectCount(interrupted, transition) != effectCount(before, transition)+1 { + t.Fatalf("control-law explicit-recovery: state=%#v error=%v attempt=%v", interrupted, err, attemptErr) + } + recovery := resolveAndApply(t, runtime, fixture.Program, fixture.Scenario, fixture.Scenario.RecoveryTransition, &fixture.Scenario.Objective) + after := fixture.Scenario.Snapshot() + if after.State.Recovery != nil || recovery.TransitionID != fixture.Scenario.RecoveryTransition { + t.Fatalf("control-law explicit-recovery: recovery did not settle: %#v", after.State) + } +} + +func (suite KernelConformance) recoveryAuthorityFailsClosed(t *testing.T) { + fixture, runtime := suite.fresh(t, SetupBound) + transition := fixture.Scenario.AdvanceTransitions[0] + fixture.Scenario.InterruptNextOperator() + request, prescription := resolve(t, runtime, fixture.Scenario, transition, &fixture.Scenario.Objective, fixture.Scenario.Authority) + beforeAttempt := fixture.Scenario.Snapshot() + _, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + interrupted := fixture.Scenario.Snapshot() + if attemptErr := unresolvedAttemptError(beforeAttempt, interrupted, prescription); !kernel.IsRecoveryRequired(err) || attemptErr != nil { + t.Fatalf("control-law recovery-authority setup: error=%v attempt=%v", err, attemptErr) + } + limited := withoutCapability(fixture.Scenario.Authority, fixture.Scenario.RecoveryCapability) + resolution, err := resolveWithoutMutation(context.Background(), runtime, fixture.Scenario, kernel.ResolveRequest{InstanceID: fixture.Scenario.InstanceID, Objective: &fixture.Scenario.Objective, Authority: limited}) + afterDenied := fixture.Scenario.Snapshot() + if err != nil || resolution.Decision.Kind != kernel.Frontier || !reflect.DeepEqual(afterDenied, interrupted) { + t.Fatalf("control-law recovery-authority denial: decision=%#v error=%v before=%#v after=%#v", resolution.Decision, err, interrupted, afterDenied) + } + resolveAndApply(t, runtime, fixture.Program, fixture.Scenario, fixture.Scenario.RecoveryTransition, &fixture.Scenario.Objective) +} + +func (suite KernelConformance) processPanicRequiresExplicitRecovery(t *testing.T) { + fixture, runtime := suite.fresh(t, SetupBound) + transition := fixture.Scenario.AdvanceTransitions[0] + fixture.Scenario.PanicNextOperator() + request, prescription := resolve(t, runtime, fixture.Scenario, transition, &fixture.Scenario.Objective, fixture.Scenario.Authority) + before := fixture.Scenario.Snapshot() + var recovered any + func() { + defer func() { recovered = recover() }() + _, _ = runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + }() + after := fixture.Scenario.Snapshot() + if attemptErr := unresolvedAttemptError(before, after, prescription); recovered == nil || attemptErr != nil || effectCount(after, transition) != effectCount(before, transition)+1 { + t.Fatalf("control-law panic-recovery: panic=%v snapshot=%#v attempt=%v", recovered, after, attemptErr) + } + resolution, err := resolveWithoutMutation(context.Background(), runtime, fixture.Scenario, kernel.ResolveRequest{InstanceID: fixture.Scenario.InstanceID, Objective: &fixture.Scenario.Objective, Authority: fixture.Scenario.Authority}) + if err != nil || resolution.Decision.Kind != kernel.Prescribed || resolution.Decision.Transition != fixture.Scenario.RecoveryTransition || effectCount(fixture.Scenario.Snapshot(), transition) != effectCount(before, transition)+1 { + t.Fatalf("control-law panic-no-replay: decision=%#v error=%v", resolution.Decision, err) + } +} + +func (suite KernelConformance) atomicCommitFailureRequiresRecovery(t *testing.T) { + fixture, runtime := suite.fresh(t, SetupBound) + transition := fixture.Scenario.AdvanceTransitions[0] + fixture.Scenario.FailNextCommit() + request, prescription := resolve(t, runtime, fixture.Scenario, transition, &fixture.Scenario.Objective, fixture.Scenario.Authority) + before := fixture.Scenario.Snapshot() + _, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + after := fixture.Scenario.Snapshot() + if attemptErr := unresolvedAttemptError(before, after, prescription); !kernel.IsRecoveryRequired(err) || attemptErr != nil || effectCount(after, transition) != effectCount(before, transition)+1 { + t.Fatalf("control-law atomic-commit-recovery: snapshot=%#v error=%v attempt=%v", after, err, attemptErr) + } + resolution, resolveErr := resolveWithoutMutation(context.Background(), runtime, fixture.Scenario, kernel.ResolveRequest{InstanceID: fixture.Scenario.InstanceID, Objective: &fixture.Scenario.Objective, Authority: fixture.Scenario.Authority}) + if resolveErr != nil || resolution.Decision.Kind != kernel.Prescribed || resolution.Decision.Transition != fixture.Scenario.RecoveryTransition || effectCount(fixture.Scenario.Snapshot(), transition) != effectCount(before, transition)+1 { + t.Fatalf("control-law no-duplicate-effect: decision=%#v error=%v", resolution.Decision, resolveErr) + } +} + +func (suite KernelConformance) failedRecoveryPreservesOriginalObligation(t *testing.T) { + fixture, runtime := suite.fresh(t, SetupBound) + transition := fixture.Scenario.AdvanceTransitions[0] + fixture.Scenario.InterruptNextOperator() + request, prescription := resolve(t, runtime, fixture.Scenario, transition, &fixture.Scenario.Objective, fixture.Scenario.Authority) + before := fixture.Scenario.Snapshot() + _, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + interrupted := fixture.Scenario.Snapshot() + if attemptErr := unresolvedAttemptError(before, interrupted, prescription); !kernel.IsRecoveryRequired(err) || attemptErr != nil || effectCount(interrupted, transition) != effectCount(before, transition)+1 { + t.Fatalf("control-law recovery-retry: initial state=%#v error=%v attempt=%v", interrupted.State, err, attemptErr) + } + original := *interrupted.State.Recovery + recoveryRequest, recoveryPrescription := resolve(t, runtime, fixture.Scenario, fixture.Scenario.RecoveryTransition, &fixture.Scenario.Objective, fixture.Scenario.Authority) + fixture.Scenario.FailNextCommit() + _, err = runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: recoveryRequest, Prescription: recoveryPrescription}) + failed := fixture.Scenario.Snapshot() + if attemptErr := unresolvedAttemptError(interrupted, failed, recoveryPrescription); !kernel.IsRecoveryRequired(err) || attemptErr != nil || failed.State.Recovery == nil || *failed.State.Recovery != original || effectCount(failed, transition) != effectCount(interrupted, transition) { + t.Fatalf("control-law recovery-obligation-preservation: state=%#v error=%v attempt=%v", failed.State, err, attemptErr) + } + retryRequest, retryPrescription, resolveErr := resolveUntargetedRecovery(context.Background(), runtime, fixture.Scenario) + if resolveErr != nil { + t.Fatalf("control-law recovery-retry selection: %v", resolveErr) + } + applyAndRequireCommit(t, runtime, fixture.Program, fixture.Scenario, retryRequest, retryPrescription) + settled := fixture.Scenario.Snapshot() + if settled.State.Recovery != nil || effectCount(settled, transition) != effectCount(interrupted, transition) { + t.Fatalf("control-law recovery-retry: state=%#v", settled.State) + } +} + +func (suite KernelConformance) prescriptionCannotReplayAcrossInstances(t *testing.T) { + fixture, runtime := suite.fresh(t, SetupBound) + transition := fixture.Scenario.AdvanceTransitions[0] + request, prescription := resolve(t, runtime, fixture.Scenario, transition, &fixture.Scenario.Objective, fixture.Scenario.Authority) + before := fixture.Scenario.Snapshot() + other := fixture.Scenario.InstanceID + "-other" + fixture.Scenario.RetargetInstance(other) + before = fixture.Scenario.Snapshot() + request.InstanceID = other + _, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + after := fixture.Scenario.Snapshot() + if unchangedErr := refusedApplyMutationError(before, after, transition); !kernel.IsStale(err) || unchangedErr != nil { + t.Fatalf("control-law prescription-instance-binding: error=%v mutation=%v snapshot=%#v", err, unchangedErr, after) + } +} + +func (suite KernelConformance) concurrentApplyCommitsOnce(t *testing.T) { + fixture, resolver := suite.fresh(t, SetupConcurrentSameBase) + if err := concurrentApplyError(fixture, resolver); err != nil { + t.Fatalf("control-law concurrent-cas: %v", err) + } +} + +func (suite KernelConformance) commitCompareAndSwapRejectsIntervention(t *testing.T) { + fixture, resolver := suite.fresh(t, SetupBound) + if err := commitCompareAndSwapError(fixture, resolver); err != nil { + t.Fatalf("control-law commit-cas: %v", err) + } +} + +func commitCompareAndSwapError(fixture KernelConformance, resolver kernel.Runtime) error { + transition := fixture.Scenario.AdvanceTransitions[0] + request := kernel.ResolveRequest{InstanceID: fixture.Scenario.InstanceID, Objective: &fixture.Scenario.Objective, Authority: fixture.Scenario.Authority, Requested: transition} + resolution, err := resolveWithoutMutation(context.Background(), resolver, fixture.Scenario, request) + if err != nil || resolution.Decision.Kind != kernel.Prescribed || resolution.Prescription == nil { + return fmt.Errorf("resolve commit-CAS transition: decision=%#v error=%v", resolution.Decision, err) + } + operator := afterExecuteOperator{Operator: fixture.Operator, after: fixture.Scenario.BumpStateRevision} + runtime, err := kernel.NewRuntime(fixture.Program, fixture.Domain, operator, fixture.CapabilityClassifier, fixture.Store, fixture.Locker, fixture.Clock) + if err != nil { + return err + } + before := fixture.Scenario.Snapshot() + _, applyErr := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: *resolution.Prescription}) + after := fixture.Scenario.Snapshot() + if !kernel.IsRecoveryRequired(applyErr) { + return fmt.Errorf("intervening state was not rejected at commit: %v", applyErr) + } + if after.CommitCount != before.CommitCount || !receiptHistoryEqual(after.Receipts, before.Receipts) || effectCount(after, transition) != effectCount(before, transition)+1 { + return fmt.Errorf("commit refusal changed committed history or effect evidence: before=%#v after=%#v", before, after) + } + if after.State.Mode != before.State.Mode || after.State.Recovery == nil || after.State.Revision != before.State.Revision+2 { + return fmt.Errorf("commit refusal did not preserve the intervening unresolved state: before=%#v after=%#v", before.State, after.State) + } + return nil +} + +type afterExecuteOperator struct { + kernel.Operator + after func() +} + +func (o afterExecuteOperator) Execute(ctx context.Context, operation kernel.Operation) (kernel.Effect, error) { + effect, err := o.Operator.Execute(ctx, operation) + if err == nil { + o.after() + } + return effect, err +} + +func concurrentApplyError(fixture KernelConformance, resolver kernel.Runtime) error { + transition := fixture.Scenario.AdvanceTransitions[0] + request := kernel.ResolveRequest{InstanceID: fixture.Scenario.InstanceID, Objective: &fixture.Scenario.Objective, Authority: fixture.Scenario.Authority, Requested: transition} + resolution, err := resolveWithoutMutation(context.Background(), resolver, fixture.Scenario, request) + if err != nil || resolution.Decision.Kind != kernel.Prescribed || resolution.Prescription == nil { + return fmt.Errorf("resolve concurrent transition: decision=%#v error=%v", resolution.Decision, err) + } + prescription := *resolution.Prescription + before := fixture.Scenario.Snapshot() + storeBarrier := newTwoPartyBarrier() + domainBarrier := newTwoPartyBarrier() + store := &sameBaseLoadStore{Store: fixture.Store, barrier: storeBarrier} + domain := &sameBaseObservationDomain{Domain: fixture.Domain, barrier: domainBarrier} + runtimeA, err := kernel.NewRuntime(fixture.Program, domain, fixture.Operator, fixture.CapabilityClassifier, store, fixture.Scenario.IndependentLocker(), fixture.Clock) + if err != nil { + return err + } + runtimeB, err := kernel.NewRuntime(fixture.Program, domain, fixture.Operator, fixture.CapabilityClassifier, store, fixture.Scenario.IndependentLocker(), fixture.Clock) + if err != nil { + return err + } + start := make(chan struct{}) + type applyResult struct { + receipt kernel.Receipt + err error + } + results := make(chan applyResult, 2) + for _, runtime := range []kernel.Runtime{runtimeA, runtimeB} { + runtime := runtime + go func() { + <-start + result := applyResult{} + defer func() { + if recovered := recover(); recovered != nil { + storeBarrier.cancel() + domainBarrier.cancel() + result.err = fmt.Errorf("concurrent Apply panicked: %v", recovered) + } + results <- result + }() + result.receipt, result.err = runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + if result.err != nil { + storeBarrier.cancel() + domainBarrier.cancel() + } + }() + } + close(start) + successes, refusals := 0, 0 + var winner kernel.Receipt + for range 2 { + result := <-results + if result.err == nil { + successes++ + winner = result.receipt + } else { + refusals++ + } + } + after := fixture.Scenario.Snapshot() + if successes != 1 || refusals != 1 || after.CommitCount != before.CommitCount+1 || len(after.Receipts) != len(before.Receipts)+1 || effectCount(after, transition) != effectCount(before, transition)+1 { + return fmt.Errorf("success/refusal=%d/%d snapshot=%#v", successes, refusals, after) + } + if err := committedOutcomeError(fixture.Program, fixture.Scenario, before, after, prescription, winner); err != nil { + return fmt.Errorf("durable outcome: %w", err) + } + return nil +} + +type twoPartyBarrier struct { + mu sync.Mutex + arrivals int + ready chan struct{} + canceled chan struct{} + once sync.Once +} + +func newTwoPartyBarrier() *twoPartyBarrier { + return &twoPartyBarrier{ready: make(chan struct{}), canceled: make(chan struct{})} +} + +func (b *twoPartyBarrier) wait() error { + b.mu.Lock() + b.arrivals++ + if b.arrivals == 2 { + close(b.ready) + } + complete := b.arrivals >= 2 + b.mu.Unlock() + if complete { + return nil + } + select { + case <-b.ready: + return nil + case <-b.canceled: + b.mu.Lock() + complete = b.arrivals >= 2 + b.mu.Unlock() + if complete { + return nil + } + return fmt.Errorf("concurrent admission was canceled") + } +} + +func (b *twoPartyBarrier) cancel() { + b.once.Do(func() { close(b.canceled) }) +} + +type sameBaseLoadStore struct { + kernel.Store + barrier *twoPartyBarrier +} + +func (s *sameBaseLoadStore) Load(ctx context.Context, instanceID string) (kernel.ControlState, error) { + state, err := s.Store.Load(ctx, instanceID) + barrierErr := s.barrier.wait() + if err != nil { + return kernel.ControlState{}, err + } + if barrierErr != nil { + return kernel.ControlState{}, barrierErr + } + return state, nil +} + +type sameBaseObservationDomain struct { + kernel.Domain + barrier *twoPartyBarrier +} + +func (d *sameBaseObservationDomain) Observe(ctx context.Context, instanceID string) (kernel.Observation, error) { + observation, err := d.Domain.Observe(ctx, instanceID) + barrierErr := d.barrier.wait() + if err != nil { + return kernel.Observation{}, err + } + if barrierErr != nil { + return kernel.Observation{}, barrierErr + } + return observation, nil +} + +func (suite KernelConformance) programReachesMarkedState(t *testing.T) { + fixture, runtime := suite.fresh(t, SetupUnbound) + before := fixture.Scenario.Snapshot() + receipts, err := runUntargetedToMarked(context.Background(), runtime, fixture.Program, fixture.Scenario, len(fixture.Scenario.AdvanceTransitions)+1) + if err != nil { + t.Fatal(err) + } + for _, receipt := range receipts { + if receipt.Verification != "satisfied" || receipt.AttemptStateRevision != receipt.PriorStateRevision+1 || receipt.ResultStateRevision != receipt.AttemptStateRevision+1 { + t.Fatalf("control-law receipt-completeness: %#v", receipt) + } + } + after := fixture.Scenario.Snapshot() + if !fixture.Program.Marked(after.State.Mode) || len(after.Receipts) != len(before.Receipts)+len(receipts) { + t.Fatalf("control-law marked-reachability: snapshot=%#v", after) + } +} + +func runUntargetedToMarked(ctx context.Context, runtime kernel.Runtime, program kernel.Program, scenario Scenario, maxTransitions int) ([]kernel.Receipt, error) { + request := kernel.ResolveRequest{InstanceID: scenario.InstanceID, Objective: &scenario.Objective, Authority: scenario.Authority} + seen := map[string]bool{} + receipts := make([]kernel.Receipt, 0, maxTransitions) + for step := 0; ; step++ { + resolution, err := resolveWithoutMutation(ctx, runtime, scenario, request) + if err != nil { + return nil, fmt.Errorf("control-law marked-reachability: resolve step %d: %w", step, err) + } + if resolution.Decision.Kind == kernel.Marked { + return receipts, nil + } + if resolution.Decision.Kind != kernel.Prescribed || resolution.Prescription == nil { + return nil, fmt.Errorf("control-law marked-reachability: untargeted step %d returned %s: %s", step, resolution.Decision.Kind, resolution.Decision.Reason) + } + identity, err := progressIdentity(resolution) + if err != nil { + return nil, fmt.Errorf("control-law marked-reachability: identify step %d: %w", step, err) + } + if seen[identity] { + return nil, fmt.Errorf("control-law marked-reachability: untargeted resolution repeated control state before marked at step %d", step) + } + seen[identity] = true + if step == maxTransitions { + return nil, fmt.Errorf("control-law marked-reachability: untargeted resolution exceeded %d transitions before marked", maxTransitions) + } + before := scenario.Snapshot() + receipt, err := runtime.Apply(ctx, kernel.ApplyRequest{ResolveRequest: request, Prescription: *resolution.Prescription}) + if err != nil { + return nil, fmt.Errorf("control-law marked-reachability: apply %s: %w", resolution.Decision.Transition, err) + } + if err := committedOutcomeError(program, scenario, before, scenario.Snapshot(), *resolution.Prescription, receipt); err != nil { + return nil, fmt.Errorf("control-law marked-reachability: apply %s: %w", resolution.Decision.Transition, err) + } + receipts = append(receipts, receipt) + } +} + +func progressIdentity(resolution kernel.Resolution) (string, error) { + encoded, err := json.Marshal(struct { + Mode string `json:"mode"` + ObjectiveBinding *kernel.ObjectiveBinding `json:"objective_binding,omitempty"` + Recovery *kernel.RecoveryState `json:"recovery,omitempty"` + Observation string `json:"observation"` + }{resolution.State.Mode, resolution.State.ObjectiveBinding, resolution.State.Recovery, resolution.Observation.Fingerprint}) + return string(encoded), err +} + +func (suite KernelConformance) fresh(t *testing.T, setup Setup) (KernelConformance, kernel.Runtime) { + t.Helper() + fixture := suite.fixture(t, setup) + runtime, err := kernel.NewRuntime(fixture.Program, fixture.Domain, fixture.Operator, fixture.CapabilityClassifier, fixture.Store, fixture.Locker, fixture.Clock) + if err != nil { + t.Fatal(err) + } + return fixture, runtime +} + +func (suite KernelConformance) fixture(t testing.TB, setup Setup) KernelConformance { + t.Helper() + if suite.New == nil { + t.Fatal("kernel conformance requires a fresh fixture factory") + } + fixture := suite.New(t, setup) + if fixture.Domain == nil || fixture.Operator == nil || fixture.CapabilityClassifier == nil || fixture.Store == nil || fixture.Locker == nil || fixture.Clock == nil || fixture.Scenario.Snapshot == nil || fixture.Scenario.ChangeObservation == nil || fixture.Scenario.RebindObjective == nil || fixture.Scenario.BumpStateRevision == nil || fixture.Scenario.IndependentLocker == nil || fixture.Scenario.VerifyCommitted == nil || fixture.Scenario.InterruptNextOperator == nil || fixture.Scenario.PanicNextOperator == nil || fixture.Scenario.FailNextCommit == nil || fixture.Scenario.RetargetInstance == nil || fixture.Scenario.InstanceID == "" || fixture.Scenario.BindTransition == "" || len(fixture.Scenario.AdvanceTransitions) == 0 || fixture.Scenario.MaintenanceTransition == "" || fixture.Scenario.RecoveryTransition == "" || fixture.Scenario.RecoveryCapability.Validate() != nil || fixture.Scenario.ExtraCapability.Validate() != nil { + t.Fatal("kernel conformance fixture is incomplete") + } + if fixture.Scenario.RevisedObjective.Validate() != nil || fixture.Scenario.RevisedObjective.ID != fixture.Scenario.Objective.ID || fixture.Scenario.RevisedObjective.Revision <= fixture.Scenario.Objective.Revision || fixture.Scenario.RevisedObjective.Fingerprint == fixture.Scenario.Objective.Fingerprint { + t.Fatal("kernel conformance revised objective must preserve identity while increasing revision and changing fingerprint") + } + if fixture.Scenario.AlternateProgram.Validate() != nil || fixture.Scenario.AlternateProgram.ID != fixture.Program.ID || fixture.Scenario.AlternateProgram.Version != fixture.Program.Version || fixture.Scenario.AlternateProgram.Fingerprint == fixture.Program.Fingerprint { + t.Fatal("kernel conformance alternate program must preserve ID and version while changing fingerprint") + } + return fixture +} + +func resolve(t testing.TB, runtime kernel.Runtime, scenario Scenario, transition string, objective *kernel.Objective, authority kernel.Authority) (kernel.ResolveRequest, kernel.Prescription) { + t.Helper() + request := kernel.ResolveRequest{InstanceID: scenario.InstanceID, Objective: objective, Authority: authority, Requested: transition} + resolution, err := resolveWithoutMutation(context.Background(), runtime, scenario, request) + if err != nil || resolution.Decision.Kind != kernel.Prescribed || resolution.Prescription == nil { + t.Fatalf("resolve %s: decision=%#v error=%v", transition, resolution.Decision, err) + } + return request, *resolution.Prescription +} + +func resolveAndApply(t testing.TB, runtime kernel.Runtime, program kernel.Program, scenario Scenario, transition string, objective *kernel.Objective) kernel.Receipt { + t.Helper() + request, prescription := resolve(t, runtime, scenario, transition, objective, scenario.Authority) + return applyAndRequireCommit(t, runtime, program, scenario, request, prescription) +} + +func applyAndRequireCommit(t testing.TB, runtime kernel.Runtime, program kernel.Program, scenario Scenario, request kernel.ResolveRequest, prescription kernel.Prescription) kernel.Receipt { + t.Helper() + before := scenario.Snapshot() + receipt, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + if err != nil { + t.Fatalf("apply %s: %v", prescription.TransitionID, err) + } + if err := committedOutcomeError(program, scenario, before, scenario.Snapshot(), prescription, receipt); err != nil { + t.Fatalf("apply %s durable outcome: %v", prescription.TransitionID, err) + } + return receipt +} + +func committedOutcomeError(program kernel.Program, scenario Scenario, before, after Snapshot, prescription kernel.Prescription, returned kernel.Receipt) error { + if err := returned.Validate(); err != nil { + return fmt.Errorf("returned receipt is invalid: %w", err) + } + if after.CommitCount != before.CommitCount+1 || len(after.Receipts) != len(before.Receipts)+1 { + return fmt.Errorf("commit or durable receipt count did not advance exactly once") + } + if !receiptHistoryEqual(after.Receipts[:len(before.Receipts)], before.Receipts) { + return fmt.Errorf("successful Apply changed prior receipt history") + } + durable := after.Receipts[len(before.Receipts)] + if err := durable.Validate(); err != nil { + return fmt.Errorf("durable receipt is invalid: %w", err) + } + if !reflect.DeepEqual(durable, returned) { + return fmt.Errorf("durable receipt differs from returned receipt") + } + if returned.InstanceID != before.State.InstanceID || returned.Program != before.State.Program || returned.PrescriptionID != prescription.ID || returned.TransitionID != prescription.TransitionID { + return fmt.Errorf("receipt identity differs from the applied prescription and prior state") + } + if returned.PriorStateRevision != before.State.Revision || returned.AttemptStateRevision != before.State.Revision+1 || returned.ResultStateRevision != before.State.Revision+2 { + return fmt.Errorf("receipt revisions differ from the committed state sequence") + } + if returned.PriorObservation != before.Observation.Fingerprint || returned.PriorObservation != prescription.ExpectedSnapshotFingerprint { + return fmt.Errorf("receipt prior observation differs from the prescribed pre-transition observation") + } + if returned.AuthorityFingerprint != prescription.AuthorityFingerprint || !reflect.DeepEqual(returned.Capabilities, prescription.RequiredCapabilities) { + return fmt.Errorf("receipt authority differs from the applied prescription") + } + if err := after.Observation.Validate(); err != nil || after.Observation.Fingerprint != returned.ResultObservation { + return fmt.Errorf("durable receipt result observation differs from the domain") + } + transition, ok := program.Transition(returned.TransitionID) + if !ok { + return fmt.Errorf("returned receipt transition is absent from program") + } + if after.State.InstanceID != returned.InstanceID || after.State.Program != returned.Program || after.State.Revision != returned.ResultStateRevision || after.State.Mode != transition.TargetMode || after.State.Recovery != nil || !reflect.DeepEqual(after.State.ObjectiveBinding, returned.ObjectiveBinding) { + return fmt.Errorf("durable state differs from winning receipt outcome") + } + if err := exactEffectDelta(before.Effects, after.Effects, returned.TransitionID, 1); err != nil { + return fmt.Errorf("successful Apply effect evidence is invalid: %w", err) + } + if err := scenario.VerifyCommitted(before, after, returned); err != nil { + return fmt.Errorf("independent domain outcome verification failed: %w", err) + } + return nil +} + +func unresolvedAttemptError(before, after Snapshot, prescription kernel.Prescription) error { + if after.CommitCount != before.CommitCount || !receiptHistoryEqual(after.Receipts, before.Receipts) { + return fmt.Errorf("failed attempt changed committed history") + } + if err := exactEffectDelta(before.Effects, after.Effects, prescription.TransitionID, 1); err != nil { + return fmt.Errorf("failed attempt effect evidence is invalid: %w", err) + } + if err := after.State.Validate(); err != nil { + return fmt.Errorf("durable attempt state is invalid: %w", err) + } + if after.State.InstanceID != before.State.InstanceID || after.State.Program != before.State.Program || after.State.Mode != before.State.Mode || !reflect.DeepEqual(after.State.ObjectiveBinding, before.State.ObjectiveBinding) || after.State.Revision != before.State.Revision+1 { + return fmt.Errorf("durable attempt differs from pre-effect control state") + } + if before.State.Recovery != nil { + if !reflect.DeepEqual(after.State.Recovery, before.State.Recovery) { + return fmt.Errorf("durable attempt changed the existing recovery obligation") + } + return nil + } + if after.State.Recovery == nil || after.State.Recovery.PrescriptionID != prescription.ID || after.State.Recovery.TransitionID != prescription.TransitionID { + return fmt.Errorf("durable attempt does not bind the failed prescription") + } + return nil +} + +func refusedApplyMutationError(before, after Snapshot, _ string) error { + if err := unchangedSnapshotError(before, after); err != nil { + return fmt.Errorf("refused Apply mutated control state, history, or effects: %w", err) + } + return nil +} + +func unchangedSnapshotError(before, after Snapshot) error { + if !reflect.DeepEqual(after.State, before.State) || !reflect.DeepEqual(after.Observation, before.Observation) || after.CommitCount != before.CommitCount || !receiptHistoryEqual(after.Receipts, before.Receipts) { + return fmt.Errorf("control state, observation, or committed history changed") + } + if err := exactEffectDelta(before.Effects, after.Effects, "", 0); err != nil { + return fmt.Errorf("effect evidence changed: %w", err) + } + return nil +} + +func resolveWithoutMutation(ctx context.Context, runtime kernel.Runtime, scenario Scenario, request kernel.ResolveRequest) (kernel.Resolution, error) { + before := scenario.Snapshot() + resolution, err := runtime.Resolve(ctx, request) + if err != nil { + return resolution, err + } + if unchangedErr := unchangedSnapshotError(before, scenario.Snapshot()); unchangedErr != nil { + return resolution, fmt.Errorf("Resolve mutated deterministic fixture evidence: %w", unchangedErr) + } + return resolution, nil +} + +func resolveUntargetedRecovery(ctx context.Context, runtime kernel.Runtime, scenario Scenario) (kernel.ResolveRequest, kernel.Prescription, error) { + request := kernel.ResolveRequest{InstanceID: scenario.InstanceID, Objective: &scenario.Objective, Authority: scenario.Authority} + resolution, err := resolveWithoutMutation(ctx, runtime, scenario, request) + if err != nil { + return request, kernel.Prescription{}, err + } + if resolution.Decision.Kind != kernel.Prescribed || resolution.Prescription == nil || resolution.Decision.Transition != scenario.RecoveryTransition { + return request, kernel.Prescription{}, fmt.Errorf("untargeted recovery selected %q with decision %s, want %q", resolution.Decision.Transition, resolution.Decision.Kind, scenario.RecoveryTransition) + } + return request, *resolution.Prescription, nil +} + +func exactEffectDelta(before, after map[string]int, transition string, delta int) error { + keys := make(map[string]struct{}, len(before)+len(after)) + for key := range before { + keys[key] = struct{}{} + } + for key := range after { + keys[key] = struct{}{} + } + for key := range keys { + expected := before[key] + if key == transition { + expected += delta + } + if after[key] != expected { + return fmt.Errorf("transition %q changed from %d to %d, want %d", key, before[key], after[key], expected) + } + } + return nil +} + +func receiptHistoryEqual(left, right []kernel.Receipt) bool { + if len(left) != len(right) { + return false + } + for index := range left { + if !reflect.DeepEqual(left[index], right[index]) { + return false + } + } + return true +} + +func withoutCapability(authority kernel.Authority, removed kernel.Capability) kernel.Authority { + filtered := kernel.Authority{Receipts: make([]kernel.AuthorityReceipt, 0, len(authority.Receipts))} + for _, receipt := range authority.Receipts { + copy := receipt + copy.Capabilities = make([]kernel.Capability, 0, len(receipt.Capabilities)) + for _, capability := range receipt.Capabilities { + if capability != removed { + copy.Capabilities = append(copy.Capabilities, capability) + } + } + if len(copy.Capabilities) != 0 { + filtered.Receipts = append(filtered.Receipts, copy) + } + } + return filtered +} + +func effectCount(snapshot Snapshot, transition string) int { + return snapshot.Effects[transition] +} + +type strengtheningClassifier struct { + base kernel.CapabilityClassifier + transitionID string + capability kernel.Capability +} + +func (c strengtheningClassifier) RequiredCapabilities(transition kernel.Transition) ([]kernel.Capability, error) { + required, err := c.base.RequiredCapabilities(transition) + if err != nil { + return nil, err + } + if transition.ID == c.transitionID { + required = append(required, c.capability) + } + return required, nil +} + +func (s Setup) valid() bool { + switch s { + case SetupUnbound, SetupBound, SetupMaintenanceAbsent, SetupMaintenanceBound, SetupConcurrentSameBase: + return true + default: + return false + } +} + +func invalidSetup(setup Setup) error { + return fmt.Errorf("unknown conformance setup %q", setup) +} diff --git a/boatstack/kernel/conformance/conformance_test.go b/boatstack/kernel/conformance/conformance_test.go new file mode 100644 index 0000000..4e923cb --- /dev/null +++ b/boatstack/kernel/conformance/conformance_test.go @@ -0,0 +1,499 @@ +package conformance + +import ( + "context" + "fmt" + "strings" + "sync" + "testing" + "time" + + "github.com/operatorstack/boatstack/boatstack/kernel" +) + +func TestMarkedStateCheckRejectsUntargetedPriorityCycle(t *testing.T) { + targeted := integerPriorityCycleFixture() + targetedRuntime := mustRuntime(t, targeted) + transitions := append([]string{targeted.Scenario.BindTransition}, targeted.Scenario.AdvanceTransitions...) + for _, transition := range transitions { + resolveAndApply(t, targetedRuntime, targeted.Program, targeted.Scenario, transition, &targeted.Scenario.Objective) + } + if !targeted.Program.Marked(targeted.Scenario.Snapshot().State.Mode) { + t.Fatal("counterexample fixture must retain a targeted path to marked") + } + + untargeted := integerPriorityCycleFixture() + untargetedRuntime := mustRuntime(t, untargeted) + _, err := runUntargetedToMarked(context.Background(), untargetedRuntime, untargeted.Program, untargeted.Scenario, len(untargeted.Scenario.AdvanceTransitions)+1) + if err == nil || !strings.Contains(err.Error(), "repeated control state") { + t.Fatalf("expected untargeted priority cycle rejection, got %v", err) + } +} + +func TestIntegerBoundFixtureIncludesCommittedHistory(t *testing.T) { + fixture := newIntegerFixture(SetupBound) + snapshot := fixture.Scenario.Snapshot() + if snapshot.CommitCount != 3 || len(snapshot.Receipts) != 3 || effectCount(snapshot, fixture.Scenario.AdvanceTransitions[0]) != 1 || snapshot.State.Mode != "zero" || snapshot.State.ObjectiveBinding == nil || !snapshot.State.ObjectiveBinding.Matches(fixture.Scenario.Objective) || !authorityHasCapability(fixture.Scenario.Authority, fixture.Scenario.ExtraCapability) { + t.Fatalf("expected committed bind/advance/reset baseline, got %#v", snapshot) + } +} + +func TestResolveWithoutMutationRejectsEffectfulLoad(t *testing.T) { + fixture := newIntegerFixture(SetupBound) + base := fixture.Store.(*MemoryStateStore) + domain := fixture.Domain.(*IntegerDomain) + fixture.Store = effectfulLoadStore{Store: base, mutate: func() { + domain.mu.Lock() + defer domain.mu.Unlock() + domain.executions[fixture.Scenario.MaintenanceTransition]++ + }} + runtime := mustRuntime(t, fixture) + authority := fixture.Scenario.Authority + authority.Receipts = append([]kernel.AuthorityReceipt(nil), authority.Receipts...) + authority.Receipts[0].IssuedAt = fixture.Clock.Now().Add(time.Second) + _, err := resolveWithoutMutation(context.Background(), runtime, fixture.Scenario, kernel.ResolveRequest{InstanceID: fixture.Scenario.InstanceID, Objective: &fixture.Scenario.Objective, Authority: authority, Requested: fixture.Scenario.AdvanceTransitions[0]}) + if err == nil || !strings.Contains(err.Error(), "Resolve mutated") { + t.Fatalf("expected effectful authority refusal rejection, got %v", err) + } +} + +func TestUntargetedRecoveryRejectsPriorityCycle(t *testing.T) { + fixture := integerRecoveryCycleFixture() + runtime := mustRuntime(t, fixture) + transition := fixture.Scenario.AdvanceTransitions[0] + fixture.Scenario.InterruptNextOperator() + request, prescription := resolve(t, runtime, fixture.Scenario, transition, &fixture.Scenario.Objective, fixture.Scenario.Authority) + if _, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}); !kernel.IsRecoveryRequired(err) { + t.Fatalf("expected initial recovery obligation, got %v", err) + } + recoveryRequest, recoveryPrescription := resolve(t, runtime, fixture.Scenario, fixture.Scenario.RecoveryTransition, &fixture.Scenario.Objective, fixture.Scenario.Authority) + fixture.Scenario.FailNextCommit() + if _, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: recoveryRequest, Prescription: recoveryPrescription}); !kernel.IsRecoveryRequired(err) { + t.Fatalf("expected failed recovery obligation, got %v", err) + } + if _, _, err := resolveUntargetedRecovery(context.Background(), runtime, fixture.Scenario); err == nil || !strings.Contains(err.Error(), "counter.recover-cycle") { + t.Fatalf("expected untargeted recovery cycle rejection, got %v", err) + } +} + +func TestCommittedOutcomeRejectsDurableReceiptSubstitution(t *testing.T) { + fixture := newIntegerFixture(SetupUnbound) + runtime := mustRuntime(t, fixture) + previous := resolveAndApply(t, runtime, fixture.Program, fixture.Scenario, fixture.Scenario.BindTransition, &fixture.Scenario.Objective) + + base := fixture.Store.(*MemoryStateStore) + fixture.Store = substitutingReceiptStore{Store: base, Receipt: previous} + runtime = mustRuntime(t, fixture) + transition := fixture.Scenario.AdvanceTransitions[0] + request, prescription := resolve(t, runtime, fixture.Scenario, transition, &fixture.Scenario.Objective, fixture.Scenario.Authority) + before := fixture.Scenario.Snapshot() + returned, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + if err != nil { + t.Fatal(err) + } + if err := committedOutcomeError(fixture.Program, fixture.Scenario, before, fixture.Scenario.Snapshot(), prescription, returned); err == nil || !strings.Contains(err.Error(), "durable receipt differs") { + t.Fatalf("expected substituted durable receipt rejection, got %v", err) + } +} + +func TestCommittedOutcomeRejectsPriorReceiptRewrite(t *testing.T) { + fixture := newIntegerFixture(SetupBound) + runtime := mustRuntime(t, fixture) + transition := fixture.Scenario.AdvanceTransitions[0] + request, prescription := resolve(t, runtime, fixture.Scenario, transition, &fixture.Scenario.Objective, fixture.Scenario.Authority) + before := fixture.Scenario.Snapshot() + returned, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + if err != nil { + t.Fatal(err) + } + receipts := fixture.Store.(*MemoryStateStore).receipts + receipts.mu.Lock() + receipts.values[0] = returned + receipts.mu.Unlock() + if err := committedOutcomeError(fixture.Program, fixture.Scenario, before, fixture.Scenario.Snapshot(), prescription, returned); err == nil || !strings.Contains(err.Error(), "prior receipt history") { + t.Fatalf("expected prior receipt rewrite rejection, got %v", err) + } +} + +func TestCommittedOutcomeRejectsUnrelatedEffect(t *testing.T) { + fixture := newIntegerFixture(SetupBound) + runtime := mustRuntime(t, fixture) + transition := fixture.Scenario.AdvanceTransitions[0] + request, prescription := resolve(t, runtime, fixture.Scenario, transition, &fixture.Scenario.Objective, fixture.Scenario.Authority) + before := fixture.Scenario.Snapshot() + returned, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + if err != nil { + t.Fatal(err) + } + domain := fixture.Domain.(*IntegerDomain) + domain.mu.Lock() + domain.executions[fixture.Scenario.MaintenanceTransition]++ + domain.mu.Unlock() + if err := committedOutcomeError(fixture.Program, fixture.Scenario, before, fixture.Scenario.Snapshot(), prescription, returned); err == nil || !strings.Contains(err.Error(), "effect evidence") { + t.Fatalf("expected unrelated effect rejection, got %v", err) + } +} + +func TestCommittedOutcomeRejectsLosingApplyStateClobber(t *testing.T) { + fixture := newIntegerFixture(SetupConcurrentSameBase) + base := fixture.Store.(*MemoryStateStore) + clobber := newClobberAfterCommitStore(base) + fixture.Store = clobber + resolver := mustRuntime(t, fixture) + transition := fixture.Scenario.AdvanceTransitions[0] + request, prescription := resolve(t, resolver, fixture.Scenario, transition, &fixture.Scenario.Objective, fixture.Scenario.Authority) + runtimeA, err := kernel.NewRuntime(fixture.Program, fixture.Domain, fixture.Operator, fixture.CapabilityClassifier, fixture.Store, fixture.Scenario.IndependentLocker(), fixture.Clock) + if err != nil { + t.Fatal(err) + } + runtimeB, err := kernel.NewRuntime(fixture.Program, fixture.Domain, fixture.Operator, fixture.CapabilityClassifier, fixture.Store, fixture.Scenario.IndependentLocker(), fixture.Clock) + if err != nil { + t.Fatal(err) + } + before := fixture.Scenario.Snapshot() + type result struct { + receipt kernel.Receipt + err error + } + results := make(chan result, 2) + for _, runtime := range []kernel.Runtime{runtimeA, runtimeB} { + runtime := runtime + go func() { + receipt, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + results <- result{receipt: receipt, err: err} + }() + } + var winner kernel.Receipt + for range 2 { + result := <-results + if result.err == nil { + winner = result.receipt + } + } + if err := committedOutcomeError(fixture.Program, fixture.Scenario, before, fixture.Scenario.Snapshot(), prescription, winner); err == nil || !strings.Contains(err.Error(), "durable state differs") { + t.Fatalf("expected losing Apply state clobber rejection, got %v", err) + } +} + +func TestCommittedOutcomeRejectsFalsePriorObservation(t *testing.T) { + fixture := newIntegerFixture(SetupBound) + runtime := mustRuntime(t, fixture) + transition := fixture.Scenario.AdvanceTransitions[0] + request, prescription := resolve(t, runtime, fixture.Scenario, transition, &fixture.Scenario.Objective, fixture.Scenario.Authority) + before := fixture.Scenario.Snapshot() + returned, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + if err != nil { + t.Fatal(err) + } + returned.PriorObservation = strings.Repeat("f", 64) + if returned.PriorObservation == before.Observation.Fingerprint { + returned.PriorObservation = strings.Repeat("e", 64) + } + returned.ID = "" + identity, err := kernel.Fingerprint(returned) + if err != nil { + t.Fatal(err) + } + returned.ID = "rcp-" + identity + receipts := fixture.Store.(*MemoryStateStore).receipts + receipts.mu.Lock() + receipts.values[len(receipts.values)-1] = returned + receipts.mu.Unlock() + if err := committedOutcomeError(fixture.Program, fixture.Scenario, before, fixture.Scenario.Snapshot(), prescription, returned); err == nil || !strings.Contains(err.Error(), "prior observation") { + t.Fatalf("expected false prior observation rejection, got %v", err) + } +} + +func TestCommittedOutcomeRejectsNoOpAcceptedByDomainVerifier(t *testing.T) { + fixture := newIntegerFixture(SetupBound) + domain := fixture.Domain.(*IntegerDomain) + fixture.Domain = acceptingDomain{Domain: domain} + fixture.Operator = noOpAdvanceOperator{Domain: domain} + runtime := mustRuntime(t, fixture) + transition := fixture.Scenario.AdvanceTransitions[0] + request, prescription := resolve(t, runtime, fixture.Scenario, transition, &fixture.Scenario.Objective, fixture.Scenario.Authority) + before := fixture.Scenario.Snapshot() + returned, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + if err != nil { + t.Fatal(err) + } + if err := committedOutcomeError(fixture.Program, fixture.Scenario, before, fixture.Scenario.Snapshot(), prescription, returned); err == nil || !strings.Contains(err.Error(), "independent domain outcome verification failed") { + t.Fatalf("expected independent no-op rejection, got %v", err) + } +} + +func TestConcurrentApplyRejectsStoreWithoutCompareAndCommit(t *testing.T) { + fixture := newIntegerFixture(SetupConcurrentSameBase) + fixture.Store = &noCASStore{MemoryStateStore: fixture.Store.(*MemoryStateStore)} + resolver := mustRuntime(t, fixture) + if err := concurrentApplyError(fixture, resolver); err == nil { + t.Fatal("expected concurrent conformance to reject a store without revision compare-and-commit") + } +} + +func TestCommitCompareAndSwapRejectsBlindCommitStore(t *testing.T) { + fixture := newIntegerFixture(SetupBound) + fixture.Store = &blindCommitStore{MemoryStateStore: fixture.Store.(*MemoryStateStore)} + resolver := mustRuntime(t, fixture) + if err := commitCompareAndSwapError(fixture, resolver); err == nil { + t.Fatal("expected commit-CAS conformance to reject a blind CommitTransition") + } +} + +func TestCommittedOutcomeRejectsFabricatedEffectFacts(t *testing.T) { + fixture := newIntegerFixture(SetupBound) + fixture.Operator = fabricatingOperator{Operator: fixture.Operator} + runtime := mustRuntime(t, fixture) + transition := fixture.Scenario.AdvanceTransitions[0] + request, prescription := resolve(t, runtime, fixture.Scenario, transition, &fixture.Scenario.Objective, fixture.Scenario.Authority) + before := fixture.Scenario.Snapshot() + returned, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + if err != nil { + t.Fatal(err) + } + if err := committedOutcomeError(fixture.Program, fixture.Scenario, before, fixture.Scenario.Snapshot(), prescription, returned); err == nil || !strings.Contains(err.Error(), "receipt effect facts") { + t.Fatalf("expected fabricated effect fact rejection, got %v", err) + } +} + +func TestConcurrentApplyTerminatesWhenOneLoadFails(t *testing.T) { + fixture := newIntegerFixture(SetupConcurrentSameBase) + resolver := mustRuntime(t, fixture) + fixture.Store = &failOneLoadStore{Store: fixture.Store} + result := make(chan error, 1) + go func() { result <- concurrentApplyError(fixture, resolver) }() + select { + case err := <-result: + if err == nil { + t.Fatal("expected asymmetric Load failure to reject conformance") + } + case <-time.After(2 * time.Second): + t.Fatal("concurrent conformance deadlocked after one Load failure") + } +} + +func TestUnresolvedAttemptRejectsTornTargetMode(t *testing.T) { + fixture := newIntegerFixture(SetupBound) + base := fixture.Store.(*MemoryStateStore) + fixture.Store = tornCommitStore{MemoryStateStore: base} + runtime := mustRuntime(t, fixture) + transition := fixture.Scenario.AdvanceTransitions[0] + request, prescription := resolve(t, runtime, fixture.Scenario, transition, &fixture.Scenario.Objective, fixture.Scenario.Authority) + before := fixture.Scenario.Snapshot() + _, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + if !kernel.IsRecoveryRequired(err) { + t.Fatalf("expected recovery-required commit failure, got %v", err) + } + if err := unresolvedAttemptError(before, fixture.Scenario.Snapshot(), prescription); err == nil || !strings.Contains(err.Error(), "pre-effect control state") { + t.Fatalf("expected torn target-mode rejection, got %v", err) + } +} + +func integerPriorityCycleFixture() KernelConformance { + fixture := newIntegerFixture(SetupUnbound) + base, err := IntegerProgram() + if err != nil { + panic(err) + } + transitions := append([]kernel.Transition(nil), base.Transitions...) + for index := range transitions { + if transitions[index].ID == fixture.Scenario.MaintenanceTransition { + transitions[index].Priority = 5 + } + } + fixture.Program, err = kernel.CompileProgram(base.ID, base.Version, base.RuntimeCompatibility, base.InitialMode, base.MarkedModes, transitions) + if err != nil { + panic(err) + } + state, _ := fixture.Store.(*MemoryStateStore).snapshot() + state.Program = fixture.Program.Identity() + fixture.Store.(*MemoryStateStore).state = state + return fixture +} + +func integerRecoveryCycleFixture() KernelConformance { + fixture := newIntegerFixture(SetupBound) + base := fixture.Program + transitions := append([]kernel.Transition(nil), base.Transitions...) + for index := range transitions { + if transitions[index].ID == fixture.Scenario.RecoveryTransition { + transitions[index].Priority = 2 + } + } + transitions = append(transitions, kernel.Transition{ + ID: "counter.recover-cycle", SourceModes: []string{"zero", "one", "two"}, TargetMode: "zero", + ObjectiveScope: kernel.ObjectiveOptionalPreserve, ObjectiveMutation: kernel.PreserveObjective, + RequiredCapabilities: []kernel.Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, + Operation: "counter.reset", Priority: 1, Recovers: []string{"counter.increment-first", "counter.increment-second", "counter.reset"}, + }) + program, err := kernel.CompileProgram(base.ID, base.Version, base.RuntimeCompatibility, base.InitialMode, base.MarkedModes, transitions) + if err != nil { + panic(err) + } + fixture.Program = program + fixture.Store.(*MemoryStateStore).retargetProgram(program.Identity()) + return fixture +} + +func authorityHasCapability(authority kernel.Authority, wanted kernel.Capability) bool { + for _, receipt := range authority.Receipts { + for _, capability := range receipt.Capabilities { + if capability == wanted { + return true + } + } + } + return false +} + +func mustRuntime(t *testing.T, fixture KernelConformance) kernel.Runtime { + t.Helper() + runtime, err := kernel.NewRuntime(fixture.Program, fixture.Domain, fixture.Operator, fixture.CapabilityClassifier, fixture.Store, fixture.Locker, fixture.Clock) + if err != nil { + t.Fatal(err) + } + return runtime +} + +type substitutingReceiptStore struct { + kernel.Store + Receipt kernel.Receipt +} + +type effectfulLoadStore struct { + kernel.Store + mutate func() +} + +func (s effectfulLoadStore) Load(ctx context.Context, instanceID string) (kernel.ControlState, error) { + s.mutate() + return s.Store.Load(ctx, instanceID) +} + +type tornCommitStore struct{ *MemoryStateStore } + +type acceptingDomain struct{ kernel.Domain } + +func (acceptingDomain) Verify(context.Context, kernel.Evaluation, kernel.Effect, kernel.Observation) error { + return nil +} + +type noOpAdvanceOperator struct{ Domain *IntegerDomain } + +func (o noOpAdvanceOperator) Execute(_ context.Context, operation kernel.Operation) (kernel.Effect, error) { + o.Domain.mu.Lock() + defer o.Domain.mu.Unlock() + o.Domain.executions[operation.Transition.ID]++ + return kernel.Effect{Facts: []kernel.EffectFact{{Facet: "counter.value", Operation: operation.Transition.Operation, Fingerprint: fmt.Sprintf("value-%d", o.Domain.value)}}}, nil +} + +type noCASStore struct{ *MemoryStateStore } + +type blindCommitStore struct{ *MemoryStateStore } + +func (s *blindCommitStore) CommitTransition(_ context.Context, _ uint64, target kernel.ControlState, receipt kernel.Receipt) error { + s.mu.Lock() + defer s.mu.Unlock() + s.state = cloneState(target) + s.commitCount++ + s.receipts.append(receipt) + return nil +} + +type fabricatingOperator struct{ kernel.Operator } + +func (o fabricatingOperator) Execute(ctx context.Context, operation kernel.Operation) (kernel.Effect, error) { + effect, err := o.Operator.Execute(ctx, operation) + if err == nil && len(effect.Facts) > 0 { + effect.Facts[0].Fingerprint = "fabricated" + } + return effect, err +} + +type failOneLoadStore struct { + kernel.Store + mu sync.Mutex + failed bool +} + +func (s *failOneLoadStore) Load(ctx context.Context, instanceID string) (kernel.ControlState, error) { + s.mu.Lock() + if !s.failed { + s.failed = true + s.mu.Unlock() + return kernel.ControlState{}, fmt.Errorf("simulated asymmetric Load failure") + } + s.mu.Unlock() + return s.Store.Load(ctx, instanceID) +} + +func (s *noCASStore) BeginEffect(_ context.Context, _ uint64, target kernel.ControlState) error { + s.mu.Lock() + defer s.mu.Unlock() + s.state = cloneState(target) + return nil +} + +func (s *noCASStore) CommitTransition(_ context.Context, _ uint64, target kernel.ControlState, receipt kernel.Receipt) error { + s.mu.Lock() + defer s.mu.Unlock() + s.state = cloneState(target) + s.commitCount++ + s.receipts.append(receipt) + return nil +} + +func (s tornCommitStore) CommitTransition(_ context.Context, _ uint64, target kernel.ControlState, _ kernel.Receipt) error { + s.mu.Lock() + defer s.mu.Unlock() + s.state.Mode = target.Mode + return fmt.Errorf("simulated torn commit") +} + +func (s substitutingReceiptStore) CommitTransition(ctx context.Context, revision uint64, target kernel.ControlState, _ kernel.Receipt) error { + return s.Store.CommitTransition(ctx, revision, target, s.Receipt) +} + +type clobberAfterCommitStore struct { + base *MemoryStateStore + mu sync.Mutex + arrivals int + ready chan struct{} + committed chan struct{} + closeOnce sync.Once +} + +func newClobberAfterCommitStore(base *MemoryStateStore) *clobberAfterCommitStore { + return &clobberAfterCommitStore{base: base, ready: make(chan struct{}), committed: make(chan struct{})} +} + +func (s *clobberAfterCommitStore) Load(ctx context.Context, instanceID string) (kernel.ControlState, error) { + return s.base.Load(ctx, instanceID) +} + +func (s *clobberAfterCommitStore) BeginEffect(ctx context.Context, revision uint64, target kernel.ControlState) error { + s.mu.Lock() + s.arrivals++ + arrival := s.arrivals + if s.arrivals == 2 { + close(s.ready) + } + s.mu.Unlock() + <-s.ready + if arrival == 1 { + return s.base.BeginEffect(ctx, revision, target) + } + <-s.committed + s.base.mu.Lock() + s.base.state = cloneState(target) + s.base.mu.Unlock() + return fmt.Errorf("stale revision after clobber") +} + +func (s *clobberAfterCommitStore) CommitTransition(ctx context.Context, revision uint64, target kernel.ControlState, receipt kernel.Receipt) error { + err := s.base.CommitTransition(ctx, revision, target, receipt) + if err == nil { + s.closeOnce.Do(func() { close(s.committed) }) + } + return err +} diff --git a/boatstack/kernel/conformance/integer.go b/boatstack/kernel/conformance/integer.go new file mode 100644 index 0000000..9cf5034 --- /dev/null +++ b/boatstack/kernel/conformance/integer.go @@ -0,0 +1,457 @@ +package conformance + +import ( + "context" + "encoding/json" + "fmt" + "sync" + "testing" + "time" + + "github.com/operatorstack/boatstack/boatstack/kernel" +) + +// IntegerDomain is the reference non-software domain. +type IntegerDomain struct { + mu sync.Mutex + value int + executions map[string]int + interruptNextIncrement bool + panicNextIncrement bool +} + +func (d *IntegerDomain) Observe(context.Context, string) (kernel.Observation, error) { + d.mu.Lock() + defer d.mu.Unlock() + return kernel.NewObservation(struct { + Value int `json:"value"` + }{d.value}) +} + +func (d *IntegerDomain) Admissible(_ context.Context, evaluation kernel.Evaluation) (bool, string, error) { + var observed struct { + Value int `json:"value"` + } + if err := json.Unmarshal(evaluation.Observation.Value, &observed); err != nil { + return false, "", err + } + switch evaluation.Transition.Operation { + case "objective.bind": + return evaluation.State.ObjectiveBinding == nil && evaluation.Objective != nil, "objective is not yet bound", nil + case "counter.increment": + return evaluation.Objective != nil && observed.Value < 2, "exact objective is present and value is below target", nil + case "counter.reset": + return observed.Value > 0 || evaluation.State.Recovery != nil, "value is nonzero or recovery remains active", nil + default: + return false, "unknown transition", nil + } +} + +func (d *IntegerDomain) Verify(_ context.Context, evaluation kernel.Evaluation, _ kernel.Effect, target kernel.Observation) error { + var before, after struct { + Value int `json:"value"` + } + if err := json.Unmarshal(evaluation.Observation.Value, &before); err != nil { + return err + } + if err := json.Unmarshal(target.Value, &after); err != nil { + return err + } + switch evaluation.Transition.Operation { + case "objective.bind": + if before.Value != after.Value { + return fmt.Errorf("objective binding changed domain state") + } + case "counter.increment": + if after.Value != before.Value+1 { + return fmt.Errorf("increment postcondition failed") + } + case "counter.reset": + if after.Value != 0 { + return fmt.Errorf("reset postcondition failed") + } + } + return nil +} + +func (d *IntegerDomain) changeObservation() { + d.mu.Lock() + defer d.mu.Unlock() + d.value++ +} + +func (d *IntegerDomain) interruptNext() { + d.mu.Lock() + defer d.mu.Unlock() + d.interruptNextIncrement = true +} + +func (d *IntegerDomain) panicNext() { + d.mu.Lock() + defer d.mu.Unlock() + d.panicNextIncrement = true +} + +func (d *IntegerDomain) effectCounts() map[string]int { + d.mu.Lock() + defer d.mu.Unlock() + copy := make(map[string]int, len(d.executions)) + for transition, count := range d.executions { + copy[transition] = count + } + return copy +} + +// IntegerOperator applies the reference domain operations. +type IntegerOperator struct{ Domain *IntegerDomain } + +func (o IntegerOperator) Execute(_ context.Context, operation kernel.Operation) (kernel.Effect, error) { + o.Domain.mu.Lock() + defer o.Domain.mu.Unlock() + o.Domain.executions[operation.Transition.ID]++ + switch operation.Transition.Operation { + case "objective.bind": + case "counter.increment": + o.Domain.value++ + if o.Domain.interruptNextIncrement { + o.Domain.interruptNextIncrement = false + return kernel.Effect{}, fmt.Errorf("simulated interrupted operator") + } + if o.Domain.panicNextIncrement { + o.Domain.panicNextIncrement = false + panic("simulated process panic") + } + case "counter.reset": + o.Domain.value = 0 + default: + return kernel.Effect{}, fmt.Errorf("unknown operation") + } + facet := "counter.value" + if operation.Transition.Operation == "objective.bind" { + facet = "supervisor.objective" + } + return kernel.Effect{Facts: []kernel.EffectFact{{Facet: facet, Operation: operation.Transition.Operation, Fingerprint: fmt.Sprintf("value-%d", o.Domain.value)}}}, nil +} + +// IntegerCapabilities classifies the reference operations. +type IntegerCapabilities struct{} + +func (IntegerCapabilities) RequiredCapabilities(transition kernel.Transition) ([]kernel.Capability, error) { + switch transition.Operation { + case "objective.bind": + return []kernel.Capability{"objective.bind"}, nil + case "counter.increment": + return []kernel.Capability{"counter.increment"}, nil + case "counter.reset": + return []kernel.Capability{"counter.reset"}, nil + default: + return nil, fmt.Errorf("unclassified operation %q", transition.Operation) + } +} + +// MemoryReceipts records committed receipts for the reference store. +type MemoryReceipts struct { + mu sync.Mutex + values []kernel.Receipt +} + +func (r *MemoryReceipts) append(receipt kernel.Receipt) { + r.mu.Lock() + defer r.mu.Unlock() + r.values = append(r.values, receipt) +} + +func (r *MemoryReceipts) snapshot() []kernel.Receipt { + r.mu.Lock() + defer r.mu.Unlock() + return append([]kernel.Receipt(nil), r.values...) +} + +// MemoryStateStore is a revision-CAS reference Store. +type MemoryStateStore struct { + mu sync.Mutex + state kernel.ControlState + receipts *MemoryReceipts + commitFailures int + commitCount int +} + +func (s *MemoryStateStore) Load(context.Context, string) (kernel.ControlState, error) { + s.mu.Lock() + defer s.mu.Unlock() + return cloneState(s.state), nil +} + +func (s *MemoryStateStore) BeginEffect(_ context.Context, revision uint64, target kernel.ControlState) error { + s.mu.Lock() + defer s.mu.Unlock() + if s.state.Revision != revision { + return fmt.Errorf("stale revision") + } + s.state = cloneState(target) + return nil +} + +func (s *MemoryStateStore) CommitTransition(_ context.Context, revision uint64, target kernel.ControlState, receipt kernel.Receipt) error { + s.mu.Lock() + defer s.mu.Unlock() + if s.commitFailures > 0 { + s.commitFailures-- + return fmt.Errorf("simulated atomic transaction failure") + } + if s.state.Revision != revision { + return fmt.Errorf("stale revision") + } + s.state = cloneState(target) + s.commitCount++ + s.receipts.append(receipt) + return nil +} + +func (s *MemoryStateStore) snapshot() (kernel.ControlState, int) { + s.mu.Lock() + defer s.mu.Unlock() + return cloneState(s.state), s.commitCount +} + +func (s *MemoryStateStore) failNextCommit() { + s.mu.Lock() + defer s.mu.Unlock() + s.commitFailures++ +} + +func (s *MemoryStateStore) retarget(instanceID string) { + s.mu.Lock() + defer s.mu.Unlock() + s.state.InstanceID = instanceID +} + +func (s *MemoryStateStore) bumpRevision() { + s.mu.Lock() + defer s.mu.Unlock() + s.state.Revision++ +} + +func (s *MemoryStateStore) retargetProgram(program kernel.ProgramIdentity) { + s.mu.Lock() + defer s.mu.Unlock() + s.state.Program = program +} + +func (s *MemoryStateStore) rebind(objective kernel.Objective) { + s.mu.Lock() + defer s.mu.Unlock() + binding, err := kernel.BindObjective(objective) + if err != nil { + panic(err) + } + s.state.ObjectiveBinding = &binding +} + +// MemoryLocker serializes one control instance. +type MemoryLocker struct{ mu sync.Mutex } + +func (l *MemoryLocker) Acquire(context.Context, string) (kernel.Lock, error) { + l.mu.Lock() + return memoryLock{mu: &l.mu}, nil +} + +type memoryLock struct{ mu *sync.Mutex } + +func (l memoryLock) Unlock() error { + l.mu.Unlock() + return nil +} + +// FixedClock returns one deterministic time. +type FixedClock struct{ Time time.Time } + +func (c FixedClock) Now() time.Time { return c.Time } + +// IntegerProgram compiles the reference control program. +func IntegerProgram() (kernel.Program, error) { + return kernel.CompileProgram("integer-control", "1.0.0", "kernel-v1", "unbound", []string{"two"}, []kernel.Transition{ + {ID: "objective.bind", SourceModes: []string{"unbound"}, TargetMode: "zero", ObjectiveScope: kernel.ObjectiveNone, ObjectiveMutation: kernel.BindObjectiveMutation, RequiredCapabilities: []kernel.Capability{"objective.bind"}, OwnedFacets: []string{"supervisor.objective"}, Operation: "objective.bind", Priority: 5}, + {ID: "counter.increment-first", SourceModes: []string{"zero"}, TargetMode: "one", ObjectiveScope: kernel.ObjectiveBoundExact, ObjectiveMutation: kernel.PreserveObjective, RequiredCapabilities: []kernel.Capability{"counter.increment"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.increment", Priority: 10}, + {ID: "counter.increment-second", SourceModes: []string{"one"}, TargetMode: "two", ObjectiveScope: kernel.ObjectiveBoundExact, ObjectiveMutation: kernel.PreserveObjective, RequiredCapabilities: []kernel.Capability{"counter.increment"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.increment", Priority: 10}, + {ID: "counter.reset", SourceModes: []string{"one", "two"}, TargetMode: "zero", ObjectiveScope: kernel.ObjectiveOptionalPreserve, ObjectiveMutation: kernel.PreserveObjective, RequiredCapabilities: []kernel.Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.reset", Priority: 20}, + {ID: "objective.recover", SourceModes: []string{"unbound"}, TargetMode: "unbound", ObjectiveScope: kernel.ObjectiveOptionalPreserve, ObjectiveMutation: kernel.PreserveObjective, RequiredCapabilities: []kernel.Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.reset", Priority: 1, Recovers: []string{"objective.bind"}}, + {ID: "counter.recover", SourceModes: []string{"zero", "one", "two"}, TargetMode: "zero", ObjectiveScope: kernel.ObjectiveOptionalPreserve, ObjectiveMutation: kernel.PreserveObjective, RequiredCapabilities: []kernel.Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.reset", Priority: 1, Recovers: []string{"counter.increment-first", "counter.increment-second", "counter.reset"}}, + }) +} + +// IntegerFixture returns the reusable reference conformance suite. +func IntegerFixture() KernelConformance { + return newIntegerFixture(SetupUnbound) +} + +func newIntegerFixture(setup Setup) KernelConformance { + if !setup.valid() { + panic(invalidSetup(setup)) + } + program, err := IntegerProgram() + if err != nil { + panic(err) + } + objective, err := kernel.NewObjective("reach-two", 1, map[string]int{"value": 2}) + if err != nil { + panic(err) + } + revised, err := kernel.NewObjective("reach-two", 2, map[string]int{"value": 3}) + if err != nil { + panic(err) + } + conflicting, err := kernel.NewObjective("other", 1, map[string]int{"value": 0}) + if err != nil { + panic(err) + } + alternateTransitions := append([]kernel.Transition(nil), program.Transitions...) + for index := range alternateTransitions { + if alternateTransitions[index].ID == "counter.increment-first" { + alternateTransitions[index].TargetMode = "two" + } + } + alternateProgram, err := kernel.CompileProgram(program.ID, program.Version, program.RuntimeCompatibility, program.InitialMode, program.MarkedModes, alternateTransitions) + if err != nil { + panic(err) + } + state := kernel.ControlState{InstanceID: "counter-fixture", Program: program.Identity(), Mode: "unbound", Revision: 1} + value := 0 + switch setup { + case SetupConcurrentSameBase: + binding, bindErr := kernel.BindObjective(objective) + if bindErr != nil { + panic(bindErr) + } + state.Mode, state.ObjectiveBinding = "zero", &binding + case SetupMaintenanceAbsent: + state.Mode, value = "one", 1 + case SetupMaintenanceBound: + binding, bindErr := kernel.BindObjective(objective) + if bindErr != nil { + panic(bindErr) + } + state.Mode, state.ObjectiveBinding, value = "one", &binding, 1 + } + now := time.Date(2026, 8, 12, 10, 0, 0, 0, time.UTC) + authority := kernel.Authority{Receipts: []kernel.AuthorityReceipt{{ID: "human-counter", Subject: "fixture", Fingerprint: "fixture-authority", Capabilities: []kernel.Capability{"counter.audit", "counter.increment", "counter.reset", "objective.bind"}, IssuedAt: now.Add(-time.Minute), ExpiresAt: now.Add(time.Hour)}}} + domain := &IntegerDomain{value: value, executions: map[string]int{}} + receipts := &MemoryReceipts{} + store := &MemoryStateStore{state: state, receipts: receipts} + fixture := KernelConformance{ + Domain: domain, + Operator: IntegerOperator{Domain: domain}, + CapabilityClassifier: IntegerCapabilities{}, + Store: store, + Locker: &MemoryLocker{}, + Clock: FixedClock{Time: now}, + Program: program, + } + fixture.Scenario = Scenario{ + InstanceID: state.InstanceID, + Objective: objective, + RevisedObjective: revised, + ConflictingObjective: conflicting, + AlternateProgram: alternateProgram, + Authority: authority, + BindTransition: "objective.bind", + AdvanceTransitions: []string{"counter.increment-first", "counter.increment-second"}, + MaintenanceTransition: "counter.reset", + RecoveryTransition: "counter.recover", + RecoveryCapability: "counter.reset", + ExtraCapability: "counter.audit", + ChangeObservation: domain.changeObservation, + RebindObjective: store.rebind, + BumpStateRevision: store.bumpRevision, + IndependentLocker: func() kernel.Locker { return &MemoryLocker{} }, + VerifyCommitted: func(before, after Snapshot, receipt kernel.Receipt) error { + return verifyIntegerCommitted(program, before, after, receipt) + }, + InterruptNextOperator: domain.interruptNext, + PanicNextOperator: domain.panicNext, + FailNextCommit: store.failNextCommit, + RetargetInstance: store.retarget, + Snapshot: func() Snapshot { + current, commits := store.snapshot() + observation, observeErr := domain.Observe(context.Background(), current.InstanceID) + if observeErr != nil { + panic(observeErr) + } + return Snapshot{State: current, Observation: observation, Effects: domain.effectCounts(), Receipts: receipts.snapshot(), CommitCount: commits} + }, + } + fixture.New = func(_ testing.TB, requested Setup) KernelConformance { + return newIntegerFixture(requested) + } + if setup == SetupBound { + runtime, runtimeErr := kernel.NewRuntime(program, fixture.Domain, fixture.Operator, fixture.CapabilityClassifier, fixture.Store, fixture.Locker, fixture.Clock) + if runtimeErr != nil { + panic(runtimeErr) + } + for _, transition := range []string{fixture.Scenario.BindTransition, fixture.Scenario.AdvanceTransitions[0], fixture.Scenario.MaintenanceTransition} { + request := kernel.ResolveRequest{InstanceID: state.InstanceID, Objective: &objective, Authority: authority, Requested: transition} + resolution, resolveErr := runtime.Resolve(context.Background(), request) + if resolveErr != nil || resolution.Prescription == nil { + panic(fmt.Sprintf("seed bound fixture %s: decision=%#v error=%v", transition, resolution.Decision, resolveErr)) + } + if _, applyErr := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: *resolution.Prescription}); applyErr != nil { + panic(applyErr) + } + } + } + return fixture +} + +func verifyIntegerCommitted(program kernel.Program, before, after Snapshot, receipt kernel.Receipt) error { + var prior, result struct { + Value int `json:"value"` + } + if err := json.Unmarshal(before.Observation.Value, &prior); err != nil { + return err + } + if err := json.Unmarshal(after.Observation.Value, &result); err != nil { + return err + } + transition, ok := program.Transition(receipt.TransitionID) + if !ok { + return fmt.Errorf("unknown transition %q", receipt.TransitionID) + } + facet := "counter.value" + if transition.Operation == "objective.bind" { + facet = "supervisor.objective" + } + expectedFact := kernel.EffectFact{Facet: facet, Operation: transition.Operation, Fingerprint: fmt.Sprintf("value-%d", result.Value)} + if len(receipt.Effects) != 1 || receipt.Effects[0] != expectedFact { + return fmt.Errorf("receipt effect facts %#v differ from independent evidence %#v", receipt.Effects, expectedFact) + } + switch transition.Operation { + case "objective.bind": + if result.Value != prior.Value { + return fmt.Errorf("objective binding changed value from %d to %d", prior.Value, result.Value) + } + case "counter.increment": + if result.Value != prior.Value+1 { + return fmt.Errorf("increment changed value from %d to %d", prior.Value, result.Value) + } + case "counter.reset": + if result.Value != 0 { + return fmt.Errorf("reset left value at %d", result.Value) + } + default: + return fmt.Errorf("unsupported operation %q", transition.Operation) + } + return nil +} + +func cloneState(state kernel.ControlState) kernel.ControlState { + copy := state + if state.ObjectiveBinding != nil { + binding := *state.ObjectiveBinding + copy.ObjectiveBinding = &binding + } + if state.Recovery != nil { + recovery := *state.Recovery + copy.Recovery = &recovery + } + return copy +} diff --git a/boatstack/kernel/runtime_test.go b/boatstack/kernel/runtime_test.go index c66ce19..bb4ef79 100644 --- a/boatstack/kernel/runtime_test.go +++ b/boatstack/kernel/runtime_test.go @@ -1,483 +1,11 @@ -package kernel +package kernel_test import ( - "context" - "encoding/json" - "fmt" - "sync" "testing" - "time" -) - -type integerDomain struct { - mu sync.Mutex - value int - incrementExecutions int - failAfterIncrement bool - panicAfterIncrement bool -} - -func (d *integerDomain) Observe(context.Context, string) (Observation, error) { - d.mu.Lock() - defer d.mu.Unlock() - return NewObservation(struct { - Value int `json:"value"` - }{d.value}) -} - -func (d *integerDomain) Admissible(_ context.Context, evaluation Evaluation) (bool, string, error) { - var observed struct { - Value int `json:"value"` - } - if err := json.Unmarshal(evaluation.Observation.Value, &observed); err != nil { - return false, "", err - } - switch evaluation.Transition.Operation { - case "objective.bind": - return evaluation.State.ObjectiveBinding == nil && evaluation.Objective != nil, "objective is not yet bound", nil - case "counter.increment": - if evaluation.Objective == nil { - return false, "exact objective required", nil - } - return observed.Value < 2, "value is below objective", nil - case "counter.inspect": - return true, "inspection is always available", nil - case "counter.reset": - return observed.Value > 0 || evaluation.State.Recovery != nil, "value is nonzero or recovery remains active", nil - default: - return false, "unknown transition", nil - } -} - -func (d *integerDomain) Verify(_ context.Context, evaluation Evaluation, effect Effect, target Observation) error { - var before, after struct { - Value int `json:"value"` - } - if err := json.Unmarshal(evaluation.Observation.Value, &before); err != nil { - return err - } - if err := json.Unmarshal(target.Value, &after); err != nil { - return err - } - switch evaluation.Transition.Operation { - case "objective.bind": - if before.Value != after.Value { - return fmt.Errorf("objective binding changed domain state") - } - case "counter.increment": - if after.Value != before.Value+1 { - return fmt.Errorf("increment postcondition failed") - } - case "counter.reset": - if after.Value != 0 { - return fmt.Errorf("reset postcondition failed") - } - } - return nil -} - -type integerOperator struct{ domain *integerDomain } - -func (o integerOperator) Execute(_ context.Context, operation Operation) (Effect, error) { - o.domain.mu.Lock() - defer o.domain.mu.Unlock() - switch operation.Transition.Operation { - case "objective.bind": - case "counter.increment": - o.domain.incrementExecutions++ - o.domain.value++ - if o.domain.panicAfterIncrement { - o.domain.panicAfterIncrement = false - panic("simulated process panic") - } - if o.domain.failAfterIncrement { - o.domain.failAfterIncrement = false - return Effect{}, fmt.Errorf("simulated interrupted operator") - } - case "counter.reset": - o.domain.value = 0 - case "counter.inspect": - default: - return Effect{}, fmt.Errorf("unknown operation") - } - facet := "counter.value" - if operation.Transition.Operation == "objective.bind" { - facet = "supervisor.objective" - } - return Effect{Facts: []EffectFact{{Facet: facet, Operation: operation.Transition.Operation, Fingerprint: fmt.Sprintf("value-%d", o.domain.value)}}}, nil -} - -type memoryStateStore struct { - mu sync.Mutex - state ControlState - receipts *memoryReceipts - commitFailures int -} - -func (s *memoryStateStore) Load(context.Context, string) (ControlState, error) { - s.mu.Lock() - defer s.mu.Unlock() - return s.state, nil -} -func (s *memoryStateStore) BeginEffect(_ context.Context, revision uint64, target ControlState) error { - s.mu.Lock() - defer s.mu.Unlock() - if s.state.Revision != revision { - return fmt.Errorf("stale revision") - } - s.state = target - return nil -} -func (s *memoryStateStore) CommitTransition(_ context.Context, revision uint64, target ControlState, receipt Receipt) error { - s.mu.Lock() - defer s.mu.Unlock() - if s.commitFailures > 0 { - s.commitFailures-- - return fmt.Errorf("simulated atomic transaction failure") - } - if s.state.Revision != revision { - return fmt.Errorf("stale revision") - } - s.state = target - s.receipts.values = append(s.receipts.values, receipt) - return nil -} - -type memoryReceipts struct{ values []Receipt } - -type memoryLock struct{ mu *sync.Mutex } - -func (l memoryLock) Unlock() error { l.mu.Unlock(); return nil } - -type memoryLocker struct{ mu sync.Mutex } - -func (l *memoryLocker) Acquire(context.Context, string) (Lock, error) { - l.mu.Lock() - return memoryLock{&l.mu}, nil -} - -type fixedClock struct{ now time.Time } - -func (c fixedClock) Now() time.Time { return c.now } - -type integerCapabilities struct{} - -func (integerCapabilities) RequiredCapabilities(transition Transition) ([]Capability, error) { - switch transition.Operation { - case "objective.bind": - return []Capability{"objective.bind"}, nil - case "counter.increment": - return []Capability{"counter.increment"}, nil - case "counter.reset": - return []Capability{"counter.reset"}, nil - default: - return nil, fmt.Errorf("unclassified operation %q", transition.Operation) - } -} - -func integerProgram(t *testing.T) Program { - t.Helper() - program, err := CompileProgram("integer-control", "1.0.0", "kernel-v1", "unbound", []string{"two"}, []Transition{ - {ID: "objective.bind", SourceModes: []string{"unbound"}, TargetMode: "zero", ObjectiveScope: ObjectiveNone, ObjectiveMutation: BindObjectiveMutation, RequiredCapabilities: []Capability{"objective.bind"}, OwnedFacets: []string{"supervisor.objective"}, Operation: "objective.bind", Priority: 5}, - {ID: "counter.increment-first", SourceModes: []string{"zero"}, TargetMode: "one", ObjectiveScope: ObjectiveBoundExact, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.increment"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.increment", Priority: 10}, - {ID: "counter.increment-second", SourceModes: []string{"one"}, TargetMode: "two", ObjectiveScope: ObjectiveBoundExact, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.increment"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.increment", Priority: 10}, - {ID: "counter.reset", SourceModes: []string{"one", "two"}, TargetMode: "zero", ObjectiveScope: ObjectiveOptionalPreserve, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.reset", Priority: 20}, - {ID: "objective.recover", SourceModes: []string{"unbound"}, TargetMode: "unbound", ObjectiveScope: ObjectiveOptionalPreserve, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.reset", Priority: 1, Recovers: []string{"objective.bind"}}, - {ID: "counter.recover", SourceModes: []string{"zero", "one", "two"}, TargetMode: "zero", ObjectiveScope: ObjectiveOptionalPreserve, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.reset", Priority: 1, Recovers: []string{"counter.increment-first", "counter.increment-second", "counter.reset"}}, - }) - if err != nil { - t.Fatal(err) - } - return program -} - -func newIntegerRuntime(t *testing.T, bound bool) (Runtime, *memoryStateStore, *memoryReceipts, *integerDomain, Objective, Authority) { - t.Helper() - program := integerProgram(t) - objective, err := NewObjective("reach-two", 1, map[string]int{"value": 2}) - if err != nil { - t.Fatal(err) - } - state := ControlState{InstanceID: "counter-fixture", Program: program.Identity(), Mode: "unbound", Revision: 1} - if bound { - binding, _ := BindObjective(objective) - state.ObjectiveBinding = &binding - state.Mode = "zero" - } - receipts, domain := &memoryReceipts{}, &integerDomain{} - states := &memoryStateStore{state: state, receipts: receipts} - now := time.Date(2026, 8, 12, 10, 0, 0, 0, time.UTC) - authority := Authority{Receipts: []AuthorityReceipt{{ID: "human-counter", Subject: "fixture", Fingerprint: "fixture-authority", Capabilities: []Capability{"objective.bind", "counter.increment", "counter.reset"}, IssuedAt: now.Add(-time.Minute), ExpiresAt: now.Add(time.Hour)}}} - runtime, err := NewRuntime(program, domain, integerOperator{domain}, integerCapabilities{}, states, &memoryLocker{}, fixedClock{now}) - if err != nil { - t.Fatal(err) - } - return runtime, states, receipts, domain, objective, authority -} -func TestDeterministicNonSoftwareProgramReachesMarkedState(t *testing.T) { - runtime, states, receipts, _, objective, authority := newIntegerRuntime(t, false) - ctx := context.Background() - for _, transition := range []string{"objective.bind", "counter.increment-first", "counter.increment-second"} { - resolution, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: transition}) - if err != nil || resolution.Decision.Kind != Prescribed { - t.Fatalf("resolve %s: %#v %v", transition, resolution.Decision, err) - } - receipt, err := runtime.Apply(ctx, ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: transition}, Prescription: *resolution.Prescription}) - if err != nil { - t.Fatal(err) - } - if receipt.Verification != "satisfied" || receipt.Program.Fingerprint == "" || receipt.AttemptStateRevision != receipt.PriorStateRevision+1 || receipt.ResultStateRevision != receipt.AttemptStateRevision+1 { - t.Fatalf("incomplete receipt: %#v", receipt) - } - } - resolution, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority}) - if err != nil || resolution.Decision.Kind != Marked { - t.Fatalf("marked resolve: %#v %v", resolution.Decision, err) - } - if states.state.Revision != 7 || len(receipts.values) != 3 { - t.Fatalf("state/receipts = %d/%d", states.state.Revision, len(receipts.values)) - } -} - -func TestObjectiveRevisionInvalidatesPrescriptionBeforeEffects(t *testing.T) { - runtime, states, _, domain, objective, authority := newIntegerRuntime(t, true) - ctx := context.Background() - resolution, _ := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}) - revised, _ := NewObjective("reach-two", 2, map[string]int{"value": 3}) - binding, _ := BindObjective(revised) - states.state.ObjectiveBinding = &binding - _, err := runtime.Apply(ctx, ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-fixture", Objective: &revised, Authority: authority, Requested: "counter.increment-first"}, Prescription: *resolution.Prescription}) - if !IsStale(err) || domain.value != 0 { - t.Fatalf("err/value = %v/%d", err, domain.value) - } -} - -func TestAuthorityDenialAndObjectiveAbsenceFailClosed(t *testing.T) { - runtime, _, _, domain, objective, _ := newIntegerRuntime(t, true) - resolution, err := runtime.Resolve(context.Background(), ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Requested: "counter.increment-first"}) - if err != nil || resolution.Decision.Kind != Frontier { - t.Fatalf("decision/error = %#v/%v", resolution.Decision, err) - } - if domain.value != 0 { - t.Fatal("refused resolution mutated domain") - } -} - -func TestFutureAuthorityReceiptFailsClosedBeforeEffects(t *testing.T) { - runtime, _, _, domain, objective, authority := newIntegerRuntime(t, true) - authority.Receipts[0].IssuedAt = time.Date(2026, 8, 12, 10, 0, 1, 0, time.UTC) - resolution, err := runtime.Resolve(context.Background(), ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}) - if err != nil || resolution.Decision.Kind != Refused || domain.value != 0 || domain.incrementExecutions != 0 { - t.Fatalf("decision/value/executions/error = %#v/%d/%d/%v", resolution.Decision, domain.value, domain.incrementExecutions, err) - } -} - -func TestUntargetedAndTargetedResolutionShareOneRelation(t *testing.T) { - runtime, _, _, _, objective, authority := newIntegerRuntime(t, true) - ctx := context.Background() - untargeted, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority}) - if err != nil || untargeted.Decision.Kind != Prescribed { - t.Fatalf("untargeted: %#v %v", untargeted.Decision, err) - } - targeted, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: untargeted.Decision.Transition}) - if err != nil || targeted.Decision.Kind != Prescribed || targeted.Prescription.ID != untargeted.Prescription.ID { - t.Fatalf("targeted: %#v %v", targeted.Decision, err) - } - if _, err := runtime.Apply(ctx, ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: targeted.Decision.Transition}, Prescription: *targeted.Prescription}); err != nil { - t.Fatalf("prescribed transition was rejected by apply: %v", err) - } -} - -func TestObservationChangeMakesPrescriptionStaleBeforeOperator(t *testing.T) { - runtime, _, _, domain, objective, authority := newIntegerRuntime(t, true) - ctx := context.Background() - resolution, _ := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}) - domain.value = 1 - _, err := runtime.Apply(ctx, ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}, Prescription: *resolution.Prescription}) - if !IsStale(err) || domain.value != 1 { - t.Fatalf("err/value = %v/%d", err, domain.value) - } -} - -type strictCapabilities struct{ integerCapabilities } - -func (strictCapabilities) RequiredCapabilities(transition Transition) ([]Capability, error) { - base, err := integerCapabilities{}.RequiredCapabilities(transition) - if transition.Operation == "counter.increment" { - base = append(base, "counter.audit") - } - return base, err -} - -func TestTrustedCapabilityClassifierCannotBeWeakenedByProgram(t *testing.T) { - program := integerProgram(t) - objective, _ := NewObjective("reach-two", 1, map[string]int{"value": 2}) - binding, _ := BindObjective(objective) - receipts, domain := &memoryReceipts{}, &integerDomain{} - states := &memoryStateStore{state: ControlState{InstanceID: "counter-fixture", Program: program.Identity(), ObjectiveBinding: &binding, Mode: "zero", Revision: 1}, receipts: receipts} - now := time.Date(2026, 8, 12, 10, 0, 0, 0, time.UTC) - authority := Authority{Receipts: []AuthorityReceipt{{ID: "program-declared-only", Subject: "fixture", Fingerprint: "authority", Capabilities: []Capability{"counter.increment"}, IssuedAt: now.Add(-time.Minute)}}} - runtime, err := NewRuntime(program, domain, integerOperator{domain}, strictCapabilities{}, states, &memoryLocker{}, fixedClock{now}) - if err != nil { - t.Fatal(err) - } - resolution, err := runtime.Resolve(context.Background(), ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}) - if err != nil || resolution.Decision.Kind != Frontier || domain.value != 0 { - t.Fatalf("decision/value/error = %#v/%d/%v", resolution.Decision, domain.value, err) - } -} - -func TestOptionalMaintenancePreservesObjectiveAbsence(t *testing.T) { - runtime, states, _, domain, _, authority := newIntegerRuntime(t, false) - states.state.Mode, domain.value = "one", 1 - commandObjective, _ := NewObjective("unbound-command", 1, map[string]int{"value": 0}) - resolution, err := runtime.Resolve(context.Background(), ResolveRequest{InstanceID: "counter-fixture", Objective: &commandObjective, Authority: authority, Requested: "counter.reset"}) - if err != nil || resolution.Decision.Kind != Prescribed { - t.Fatalf("decision/error = %#v/%v", resolution.Decision, err) - } - _, err = runtime.Apply(context.Background(), ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-fixture", Objective: &commandObjective, Authority: authority, Requested: "counter.reset"}, Prescription: *resolution.Prescription}) - if err != nil || states.state.ObjectiveBinding != nil { - t.Fatalf("maintenance synthesized objective: %#v %v", states.state.ObjectiveBinding, err) - } -} - -func TestOptionalMaintenancePreservesExactBinding(t *testing.T) { - runtime, states, _, domain, objective, authority := newIntegerRuntime(t, true) - domain.value = 1 - states.state.Mode = "one" - conflicting, _ := NewObjective("other", 1, map[string]int{"value": 0}) - before := *states.state.ObjectiveBinding - resolution, err := runtime.Resolve(context.Background(), ResolveRequest{InstanceID: "counter-fixture", Objective: &conflicting, Authority: authority, Requested: "counter.reset"}) - if err != nil || resolution.Decision.Kind != Prescribed { - t.Fatalf("decision/error = %#v/%v", resolution.Decision, err) - } - _, err = runtime.Apply(context.Background(), ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-fixture", Objective: &conflicting, Authority: authority, Requested: "counter.reset"}, Prescription: *resolution.Prescription}) - if err != nil { - t.Fatal(err) - } - if states.state.ObjectiveBinding == nil || *states.state.ObjectiveBinding != before { - t.Fatal("maintenance changed objective binding") - } - _ = objective -} - -func TestInterruptedOperatorRequiresAndCompletesExplicitRecovery(t *testing.T) { - runtime, states, _, domain, objective, authority := newIntegerRuntime(t, true) - domain.failAfterIncrement = true - ctx := context.Background() - resolution, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}) - if err != nil || resolution.Decision.Kind != Prescribed { - t.Fatalf("resolve: %#v %v", resolution.Decision, err) - } - _, err = runtime.Apply(ctx, ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}, Prescription: *resolution.Prescription}) - if !IsRecoveryRequired(err) || states.state.Recovery == nil || states.state.Revision != 2 || domain.value != 1 { - t.Fatalf("recovery state: %#v value=%d err=%v", states.state, domain.value, err) - } - recovery, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority}) - if err != nil || recovery.Decision.Kind != Prescribed || recovery.Decision.Transition != "counter.recover" { - t.Fatalf("recovery resolve: %#v %v", recovery.Decision, err) - } - _, err = runtime.Apply(ctx, ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.recover"}, Prescription: *recovery.Prescription}) - if err != nil || states.state.Recovery != nil || states.state.Revision != 4 || domain.value != 0 { - t.Fatalf("recovery result: %#v value=%d err=%v", states.state, domain.value, err) - } -} - -func TestAtomicTransactionFailureEntersRecoveryWithoutDuplicateEffect(t *testing.T) { - runtime, store, receipts, domain, objective, authority := newIntegerRuntime(t, true) - store.commitFailures = 1 - ctx := context.Background() - resolution, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}) - if err != nil || resolution.Decision.Kind != Prescribed { - t.Fatalf("resolve: %#v %v", resolution.Decision, err) - } - _, err = runtime.Apply(ctx, ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}, Prescription: *resolution.Prescription}) - if !IsRecoveryRequired(err) { - t.Fatalf("apply error = %v", err) - } - if store.state.Mode != "zero" || store.state.Recovery == nil || len(receipts.values) != 0 { - t.Fatalf("non-atomic store result: state=%#v receipts=%d", store.state, len(receipts.values)) - } - if domain.value != 1 || domain.incrementExecutions != 1 { - t.Fatalf("operator value/executions = %d/%d", domain.value, domain.incrementExecutions) - } - recovery, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority}) - if err != nil || recovery.Decision.Kind != Prescribed || recovery.Decision.Transition != "counter.recover" { - t.Fatalf("recovery resolve: %#v %v", recovery.Decision, err) - } - if domain.incrementExecutions != 1 { - t.Fatalf("original operator ran %d times", domain.incrementExecutions) - } -} - -func TestProcessPanicLeavesDurableRecoveryBeforeEffectReplay(t *testing.T) { - runtime, store, _, domain, objective, authority := newIntegerRuntime(t, true) - domain.panicAfterIncrement = true - ctx := context.Background() - resolution, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}) - if err != nil || resolution.Decision.Kind != Prescribed { - t.Fatalf("resolve: %#v %v", resolution.Decision, err) - } - var recovered any - func() { - defer func() { recovered = recover() }() - _, _ = runtime.Apply(ctx, ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}, Prescription: *resolution.Prescription}) - }() - if recovered == nil || store.state.Recovery == nil || store.state.Recovery.TransitionID != "counter.increment-first" || store.state.Revision != 2 || domain.value != 1 || domain.incrementExecutions != 1 { - t.Fatalf("panic/recovery state: panic=%v state=%#v value=%d executions=%d", recovered, store.state, domain.value, domain.incrementExecutions) - } - next, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority}) - if err != nil || next.Decision.Kind != Prescribed || next.Decision.Transition != "counter.recover" || domain.incrementExecutions != 1 { - t.Fatalf("post-panic resolution: %#v executions=%d error=%v", next.Decision, domain.incrementExecutions, err) - } -} - -func TestFailedRecoveryAttemptPreservesOriginalRecoveryObligation(t *testing.T) { - runtime, store, _, domain, objective, authority := newIntegerRuntime(t, true) - domain.failAfterIncrement = true - ctx := context.Background() - - resolution, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}) - if err != nil || resolution.Decision.Kind != Prescribed { - t.Fatalf("resolve: %#v %v", resolution.Decision, err) - } - _, err = runtime.Apply(ctx, ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}, Prescription: *resolution.Prescription}) - if !IsRecoveryRequired(err) || store.state.Recovery == nil { - t.Fatalf("initial recovery: state=%#v err=%v", store.state, err) - } - original := *store.state.Recovery - - recovery, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority}) - if err != nil || recovery.Decision.Kind != Prescribed || recovery.Decision.Transition != "counter.recover" { - t.Fatalf("first recovery resolve: %#v %v", recovery.Decision, err) - } - store.commitFailures = 1 - _, err = runtime.Apply(ctx, ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.recover"}, Prescription: *recovery.Prescription}) - if !IsRecoveryRequired(err) || store.state.Recovery == nil || *store.state.Recovery != original || domain.value != 0 { - t.Fatalf("failed recovery attempt: state=%#v value=%d err=%v", store.state, domain.value, err) - } - - retry, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority}) - if err != nil || retry.Decision.Kind != Prescribed || retry.Decision.Transition != "counter.recover" { - t.Fatalf("recovery retry resolve: %#v %v", retry.Decision, err) - } - _, err = runtime.Apply(ctx, ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.recover"}, Prescription: *retry.Prescription}) - if err != nil || store.state.Recovery != nil || store.state.Mode != "zero" || store.state.Revision != 5 { - t.Fatalf("recovery retry result: state=%#v err=%v", store.state, err) - } -} + "github.com/operatorstack/boatstack/boatstack/kernel/conformance" +) -func TestPrescriptionCannotReplayAcrossControlInstances(t *testing.T) { - runtime, store, _, domain, objective, authority := newIntegerRuntime(t, true) - ctx := context.Background() - resolution, err := runtime.Resolve(ctx, ResolveRequest{InstanceID: "counter-fixture", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}) - if err != nil || resolution.Decision.Kind != Prescribed { - t.Fatalf("resolve: %#v %v", resolution.Decision, err) - } - store.state.InstanceID = "counter-other" - _, err = runtime.Apply(ctx, ApplyRequest{ResolveRequest: ResolveRequest{InstanceID: "counter-other", Objective: &objective, Authority: authority, Requested: "counter.increment-first"}, Prescription: *resolution.Prescription}) - if !IsStale(err) || domain.value != 0 || domain.incrementExecutions != 0 { - t.Fatalf("cross-instance apply: value=%d executions=%d err=%v", domain.value, domain.incrementExecutions, err) - } +func TestRuntimeConformance(t *testing.T) { + conformance.IntegerFixture().Run(t) } diff --git a/release-notes/2026-08-12-kernel-conformance-suite.md b/release-notes/2026-08-12-kernel-conformance-suite.md new file mode 100644 index 0000000..b4cb30c --- /dev/null +++ b/release-notes/2026-08-12-kernel-conformance-suite.md @@ -0,0 +1,3 @@ +### Add a reusable kernel conformance suite + +Domain authors can now verify kernel objective, authority, freshness, recovery, transaction, replay, concurrency, and marked-state laws with one reusable suite and a non-software reference fixture.