From 4d03f00d253710cccc599a54d5231dcef1bd0b23 Mon Sep 17 00:00:00 2001 From: bigboateng Date: Wed, 12 Aug 2026 15:06:18 +0100 Subject: [PATCH 1/9] test: add reusable kernel conformance suite --- .github/tests/test_repository_contract.py | 44 +- README.md | 6 + boatstack/AGENTS.md | 9 + boatstack/kernel/conformance/conformance.go | 426 ++++++++++++++++ boatstack/kernel/conformance/integer.go | 397 +++++++++++++++ boatstack/kernel/runtime_test.go | 482 +----------------- .../2026-08-12-kernel-conformance-suite.md | 3 + 7 files changed, 887 insertions(+), 480 deletions(-) create mode 100644 boatstack/kernel/conformance/conformance.go create mode 100644 boatstack/kernel/conformance/integer.go create mode 100644 release-notes/2026-08-12-kernel-conformance-suite.md diff --git a/.github/tests/test_repository_contract.py b/.github/tests/test_repository_contract.py index f479add..40ca73d 100644 --- a/.github/tests/test_repository_contract.py +++ b/.github/tests/test_repository_contract.py @@ -410,7 +410,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", @@ -419,13 +422,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}") @@ -448,6 +455,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" / @@ -522,7 +560,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..b4c41b6 --- /dev/null +++ b/boatstack/kernel/conformance/conformance.go @@ -0,0 +1,426 @@ +// Package conformance provides a reusable verifier for kernel domains. +package conformance + +import ( + "context" + "fmt" + "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 + 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 + ConflictingObjective kernel.Objective + Authority kernel.Authority + BindTransition string + AdvanceTransitions []string + MaintenanceTransition string + RecoveryTransition string + ExtraCapability kernel.Capability + ChangeObservation func() + RebindObjective func(kernel.Objective) + 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("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("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("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.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.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.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) + before := fixture.Scenario.Snapshot() + fixture.Scenario.RebindObjective(fixture.Scenario.ConflictingObjective) + request.Objective = &fixture.Scenario.ConflictingObjective + _, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + after := fixture.Scenario.Snapshot() + if !kernel.IsStale(err) || effectCount(after, transition) != effectCount(before, transition) || after.CommitCount != before.CommitCount { + t.Fatalf("control-law objective-revision-freshness: error=%v before=%#v after=%#v", err, 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() + _, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + after := fixture.Scenario.Snapshot() + if !kernel.IsStale(err) || effectCount(after, transition) != effectCount(before, transition) || after.CommitCount != before.CommitCount { + t.Fatalf("control-law prescription-freshness: error=%v before=%#v after=%#v", err, 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 := runtime.Resolve(context.Background(), 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 !kernel.IsStale(err) || effectCount(after, transition) != effectCount(before, transition) || after.CommitCount != before.CommitCount { + t.Fatalf("control-law authority-denial: apply error=%v before=%#v after=%#v", err, 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) + before := fixture.Scenario.Snapshot() + resolution, err := runtime.Resolve(context.Background(), kernel.ResolveRequest{InstanceID: fixture.Scenario.InstanceID, Objective: &fixture.Scenario.Objective, Authority: authority, Requested: transition}) + after := fixture.Scenario.Snapshot() + if err != nil || resolution.Decision.Kind != kernel.Refused || effectCount(after, transition) != effectCount(before, transition) || after.CommitCount != before.CommitCount { + 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) + } + before := fixture.Scenario.Snapshot() + resolution, err := runtime.Resolve(context.Background(), kernel.ResolveRequest{InstanceID: fixture.Scenario.InstanceID, Objective: &fixture.Scenario.Objective, Authority: fixture.Scenario.Authority, Requested: transition}) + after := fixture.Scenario.Snapshot() + if err != nil || resolution.Decision.Kind != kernel.Frontier || effectCount(after, transition) != effectCount(before, transition) { + 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 := runtime.Resolve(ctx, 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 := runtime.Resolve(ctx, 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) + } + if _, err := runtime.Apply(ctx, kernel.ApplyRequest{ResolveRequest: targetedRequest, Prescription: *targeted.Prescription}); err != nil { + t.Fatalf("control-law prescription-soundness: %v", err) + } +} + +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) + _, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + interrupted := fixture.Scenario.Snapshot() + if !kernel.IsRecoveryRequired(err) || interrupted.State.Recovery == nil || interrupted.State.Recovery.TransitionID != transition || effectCount(interrupted, transition) != 1 { + t.Fatalf("control-law explicit-recovery: state=%#v error=%v", interrupted, err) + } + recovery := resolveAndApply(t, runtime, 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) 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) + var recovered any + func() { + defer func() { recovered = recover() }() + _, _ = runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + }() + after := fixture.Scenario.Snapshot() + if recovered == nil || after.State.Recovery == nil || after.State.Recovery.TransitionID != transition || effectCount(after, transition) != 1 { + t.Fatalf("control-law panic-recovery: panic=%v snapshot=%#v", recovered, after) + } + resolution, err := runtime.Resolve(context.Background(), 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) != 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) + _, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + after := fixture.Scenario.Snapshot() + if !kernel.IsRecoveryRequired(err) || after.State.Recovery == nil || after.CommitCount != 0 || len(after.Receipts) != 0 || effectCount(after, transition) != 1 { + t.Fatalf("control-law atomic-commit-recovery: snapshot=%#v error=%v", after, err) + } + resolution, resolveErr := runtime.Resolve(context.Background(), 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) != 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) + _, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + interrupted := fixture.Scenario.Snapshot() + if !kernel.IsRecoveryRequired(err) || interrupted.State.Recovery == nil { + t.Fatalf("control-law recovery-retry: initial state=%#v error=%v", interrupted.State, err) + } + 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 !kernel.IsRecoveryRequired(err) || failed.State.Recovery == nil || *failed.State.Recovery != original || effectCount(failed, transition) != 1 { + t.Fatalf("control-law recovery-obligation-preservation: state=%#v error=%v", failed.State, err) + } + resolveAndApply(t, runtime, fixture.Scenario, fixture.Scenario.RecoveryTransition, &fixture.Scenario.Objective) + settled := fixture.Scenario.Snapshot() + if settled.State.Recovery != nil || effectCount(settled, transition) != 1 { + 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) + request.InstanceID = other + _, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + after := fixture.Scenario.Snapshot() + if !kernel.IsStale(err) || effectCount(after, transition) != effectCount(before, transition) || after.CommitCount != before.CommitCount { + t.Fatalf("control-law prescription-instance-binding: error=%v snapshot=%#v", err, after) + } +} + +func (suite KernelConformance) concurrentApplyCommitsOnce(t *testing.T) { + fixture, runtime := suite.fresh(t, SetupConcurrentSameBase) + transition := fixture.Scenario.AdvanceTransitions[0] + request, prescription := resolve(t, runtime, fixture.Scenario, transition, &fixture.Scenario.Objective, fixture.Scenario.Authority) + start := make(chan struct{}) + results := make(chan error, 2) + for range 2 { + go func() { + <-start + _, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + results <- err + }() + } + close(start) + successes, refusals := 0, 0 + for range 2 { + if err := <-results; err == nil { + successes++ + } else { + refusals++ + } + } + after := fixture.Scenario.Snapshot() + if successes != 1 || refusals != 1 || after.CommitCount != 1 || len(after.Receipts) != 1 || effectCount(after, transition) != 1 { + t.Fatalf("control-law concurrent-cas: success/refusal=%d/%d snapshot=%#v", successes, refusals, after) + } +} + +func (suite KernelConformance) programReachesMarkedState(t *testing.T) { + fixture, runtime := suite.fresh(t, SetupUnbound) + transitions := append([]string{fixture.Scenario.BindTransition}, fixture.Scenario.AdvanceTransitions...) + for _, transition := range transitions { + receipt := resolveAndApply(t, runtime, fixture.Scenario, transition, &fixture.Scenario.Objective) + if receipt.Verification != "satisfied" || receipt.AttemptStateRevision != receipt.PriorStateRevision+1 || receipt.ResultStateRevision != receipt.AttemptStateRevision+1 { + t.Fatalf("control-law receipt-completeness: %#v", receipt) + } + } + resolution, err := runtime.Resolve(context.Background(), kernel.ResolveRequest{InstanceID: fixture.Scenario.InstanceID, Objective: &fixture.Scenario.Objective, Authority: fixture.Scenario.Authority}) + after := fixture.Scenario.Snapshot() + if err != nil || resolution.Decision.Kind != kernel.Marked || !fixture.Program.Marked(after.State.Mode) || len(after.Receipts) != len(transitions) { + t.Fatalf("control-law marked-reachability: decision=%#v snapshot=%#v error=%v", resolution.Decision, after, 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.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.ExtraCapability.Validate() != nil { + t.Fatal("kernel conformance fixture is incomplete") + } + 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 := runtime.Resolve(context.Background(), 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, scenario Scenario, transition string, objective *kernel.Objective) kernel.Receipt { + t.Helper() + request, prescription := resolve(t, runtime, scenario, transition, objective, scenario.Authority) + receipt, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + if err != nil { + t.Fatalf("apply %s: %v", transition, err) + } + return receipt +} + +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/integer.go b/boatstack/kernel/conformance/integer.go new file mode 100644 index 0000000..5e2487e --- /dev/null +++ b/boatstack/kernel/conformance/integer.go @@ -0,0 +1,397 @@ +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 + beginBarrier *twoPartyBarrier +} + +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 { + if s.beginBarrier != nil { + s.beginBarrier.wait() + } + 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) 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 +} + +type noOpLocker struct{} +type noOpLock struct{} + +func (noOpLocker) Acquire(context.Context, string) (kernel.Lock, error) { return noOpLock{}, nil } +func (noOpLock) Unlock() error { 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) + } + conflicting, err := kernel.NewObjective("other", 1, map[string]int{"value": 0}) + if err != nil { + panic(err) + } + state := kernel.ControlState{InstanceID: "counter-fixture", Program: program.Identity(), Mode: "unbound", Revision: 1} + value := 0 + switch setup { + case SetupBound, 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{"objective.bind", "counter.increment", "counter.reset"}, 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} + var locker kernel.Locker = &MemoryLocker{} + if setup == SetupConcurrentSameBase { + store.beginBarrier = newTwoPartyBarrier() + locker = noOpLocker{} + } + fixture := KernelConformance{ + Domain: domain, + Operator: IntegerOperator{Domain: domain}, + CapabilityClassifier: IntegerCapabilities{}, + Store: store, + Locker: locker, + Clock: FixedClock{Time: now}, + Program: program, + } + fixture.Scenario = Scenario{ + InstanceID: state.InstanceID, + Objective: objective, + ConflictingObjective: conflicting, + Authority: authority, + BindTransition: "objective.bind", + AdvanceTransitions: []string{"counter.increment-first", "counter.increment-second"}, + MaintenanceTransition: "counter.reset", + RecoveryTransition: "counter.recover", + ExtraCapability: "counter.audit", + ChangeObservation: domain.changeObservation, + RebindObjective: store.rebind, + InterruptNextOperator: domain.interruptNext, + PanicNextOperator: domain.panicNext, + FailNextCommit: store.failNextCommit, + RetargetInstance: store.retarget, + Snapshot: func() Snapshot { + current, commits := store.snapshot() + return Snapshot{State: current, Effects: domain.effectCounts(), Receipts: receipts.snapshot(), CommitCount: commits} + }, + } + fixture.New = func(_ testing.TB, requested Setup) KernelConformance { + return newIntegerFixture(requested) + } + return fixture +} + +type twoPartyBarrier struct { + mu sync.Mutex + arrivals int + ready chan struct{} +} + +func newTwoPartyBarrier() *twoPartyBarrier { + return &twoPartyBarrier{ready: make(chan struct{})} +} + +func (b *twoPartyBarrier) wait() { + b.mu.Lock() + b.arrivals++ + if b.arrivals == 2 { + close(b.ready) + } + b.mu.Unlock() + <-b.ready +} + +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. From 9a0054e3f0a6520f267509cdfb9755d7235f26c0 Mon Sep 17 00:00:00 2001 From: bigboateng Date: Wed, 12 Aug 2026 15:16:32 +0100 Subject: [PATCH 2/9] test: prove untargeted marked-state progress --- boatstack/kernel/conformance/conformance.go | 58 ++++++++++++++++-- .../kernel/conformance/conformance_test.go | 59 +++++++++++++++++++ 2 files changed, 111 insertions(+), 6 deletions(-) create mode 100644 boatstack/kernel/conformance/conformance_test.go diff --git a/boatstack/kernel/conformance/conformance.go b/boatstack/kernel/conformance/conformance.go index b4c41b6..97bd02a 100644 --- a/boatstack/kernel/conformance/conformance.go +++ b/boatstack/kernel/conformance/conformance.go @@ -3,6 +3,7 @@ package conformance import ( "context" + "encoding/json" "fmt" "testing" "time" @@ -335,20 +336,65 @@ func (suite KernelConformance) concurrentApplyCommitsOnce(t *testing.T) { func (suite KernelConformance) programReachesMarkedState(t *testing.T) { fixture, runtime := suite.fresh(t, SetupUnbound) - transitions := append([]string{fixture.Scenario.BindTransition}, fixture.Scenario.AdvanceTransitions...) - for _, transition := range transitions { - receipt := resolveAndApply(t, runtime, fixture.Scenario, transition, &fixture.Scenario.Objective) + receipts, err := runUntargetedToMarked(context.Background(), runtime, 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) } } - resolution, err := runtime.Resolve(context.Background(), kernel.ResolveRequest{InstanceID: fixture.Scenario.InstanceID, Objective: &fixture.Scenario.Objective, Authority: fixture.Scenario.Authority}) after := fixture.Scenario.Snapshot() - if err != nil || resolution.Decision.Kind != kernel.Marked || !fixture.Program.Marked(after.State.Mode) || len(after.Receipts) != len(transitions) { - t.Fatalf("control-law marked-reachability: decision=%#v snapshot=%#v error=%v", resolution.Decision, after, err) + if !fixture.Program.Marked(after.State.Mode) || len(after.Receipts) != len(receipts) { + t.Fatalf("control-law marked-reachability: snapshot=%#v", after) + } +} + +func runUntargetedToMarked(ctx context.Context, runtime kernel.Runtime, 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 := runtime.Resolve(ctx, 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) + } + 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) + } + 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) diff --git a/boatstack/kernel/conformance/conformance_test.go b/boatstack/kernel/conformance/conformance_test.go new file mode 100644 index 0000000..b093c6d --- /dev/null +++ b/boatstack/kernel/conformance/conformance_test.go @@ -0,0 +1,59 @@ +package conformance + +import ( + "context" + "strings" + "testing" + + "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.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.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 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 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 +} From db8196203ada4a1d1edf2cd7aed5524079d35d0c Mon Sep 17 00:00:00 2001 From: bigboateng Date: Wed, 12 Aug 2026 15:28:04 +0100 Subject: [PATCH 3/9] test: verify durable apply outcomes --- boatstack/kernel/conformance/conformance.go | 79 +++++++++--- .../kernel/conformance/conformance_test.go | 112 +++++++++++++++++- 2 files changed, 173 insertions(+), 18 deletions(-) diff --git a/boatstack/kernel/conformance/conformance.go b/boatstack/kernel/conformance/conformance.go index 97bd02a..ae5b6b1 100644 --- a/boatstack/kernel/conformance/conformance.go +++ b/boatstack/kernel/conformance/conformance.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "reflect" "testing" "time" @@ -90,7 +91,7 @@ func (suite KernelConformance) Run(t *testing.T) { func (suite KernelConformance) objectiveBinding(t *testing.T) { fixture, runtime := suite.fresh(t, SetupUnbound) before := fixture.Scenario.Snapshot() - receipt := resolveAndApply(t, runtime, fixture.Scenario, fixture.Scenario.BindTransition, &fixture.Scenario.Objective) + 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") @@ -103,7 +104,7 @@ func (suite KernelConformance) objectiveBinding(t *testing.T) { func (suite KernelConformance) objectiveAbsence(t *testing.T) { fixture, runtime := suite.fresh(t, SetupMaintenanceAbsent) before := fixture.Scenario.Snapshot() - resolveAndApply(t, runtime, fixture.Scenario, fixture.Scenario.MaintenanceTransition, &fixture.Scenario.ConflictingObjective) + 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") @@ -113,7 +114,7 @@ func (suite KernelConformance) objectiveAbsence(t *testing.T) { func (suite KernelConformance) maintenancePreservesExactBinding(t *testing.T) { fixture, runtime := suite.fresh(t, SetupMaintenanceBound) before := fixture.Scenario.Snapshot() - resolveAndApply(t, runtime, fixture.Scenario, fixture.Scenario.MaintenanceTransition, &fixture.Scenario.ConflictingObjective) + 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") @@ -208,9 +209,7 @@ func (suite KernelConformance) targetedAndUntargetedShareRelation(t *testing.T) 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) } - if _, err := runtime.Apply(ctx, kernel.ApplyRequest{ResolveRequest: targetedRequest, Prescription: *targeted.Prescription}); err != nil { - t.Fatalf("control-law prescription-soundness: %v", err) - } + applyAndRequireCommit(t, runtime, fixture.Program, fixture.Scenario, targetedRequest, *targeted.Prescription) } func (suite KernelConformance) interruptedOperatorRequiresExplicitRecovery(t *testing.T) { @@ -223,7 +222,7 @@ func (suite KernelConformance) interruptedOperatorRequiresExplicitRecovery(t *te if !kernel.IsRecoveryRequired(err) || interrupted.State.Recovery == nil || interrupted.State.Recovery.TransitionID != transition || effectCount(interrupted, transition) != 1 { t.Fatalf("control-law explicit-recovery: state=%#v error=%v", interrupted, err) } - recovery := resolveAndApply(t, runtime, fixture.Scenario, fixture.Scenario.RecoveryTransition, &fixture.Scenario.Objective) + 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) @@ -284,7 +283,7 @@ func (suite KernelConformance) failedRecoveryPreservesOriginalObligation(t *test if !kernel.IsRecoveryRequired(err) || failed.State.Recovery == nil || *failed.State.Recovery != original || effectCount(failed, transition) != 1 { t.Fatalf("control-law recovery-obligation-preservation: state=%#v error=%v", failed.State, err) } - resolveAndApply(t, runtime, fixture.Scenario, fixture.Scenario.RecoveryTransition, &fixture.Scenario.Objective) + resolveAndApply(t, runtime, fixture.Program, fixture.Scenario, fixture.Scenario.RecoveryTransition, &fixture.Scenario.Objective) settled := fixture.Scenario.Snapshot() if settled.State.Recovery != nil || effectCount(settled, transition) != 1 { t.Fatalf("control-law recovery-retry: state=%#v", settled.State) @@ -310,20 +309,28 @@ func (suite KernelConformance) concurrentApplyCommitsOnce(t *testing.T) { fixture, runtime := suite.fresh(t, SetupConcurrentSameBase) transition := fixture.Scenario.AdvanceTransitions[0] request, prescription := resolve(t, runtime, fixture.Scenario, transition, &fixture.Scenario.Objective, fixture.Scenario.Authority) + before := fixture.Scenario.Snapshot() start := make(chan struct{}) - results := make(chan error, 2) + type applyResult struct { + receipt kernel.Receipt + err error + } + results := make(chan applyResult, 2) for range 2 { go func() { <-start - _, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) - results <- err + receipt, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + results <- applyResult{receipt: receipt, err: err} }() } close(start) successes, refusals := 0, 0 + var winner kernel.Receipt for range 2 { - if err := <-results; err == nil { + result := <-results + if result.err == nil { successes++ + winner = result.receipt } else { refusals++ } @@ -332,11 +339,14 @@ func (suite KernelConformance) concurrentApplyCommitsOnce(t *testing.T) { if successes != 1 || refusals != 1 || after.CommitCount != 1 || len(after.Receipts) != 1 || effectCount(after, transition) != 1 { t.Fatalf("control-law concurrent-cas: success/refusal=%d/%d snapshot=%#v", successes, refusals, after) } + if err := committedOutcomeError(fixture.Program, before, after, winner); err != nil { + t.Fatalf("control-law concurrent-cas durable outcome: %v", err) + } } func (suite KernelConformance) programReachesMarkedState(t *testing.T) { fixture, runtime := suite.fresh(t, SetupUnbound) - receipts, err := runUntargetedToMarked(context.Background(), runtime, fixture.Scenario, len(fixture.Scenario.AdvanceTransitions)+1) + receipts, err := runUntargetedToMarked(context.Background(), runtime, fixture.Program, fixture.Scenario, len(fixture.Scenario.AdvanceTransitions)+1) if err != nil { t.Fatal(err) } @@ -351,7 +361,7 @@ func (suite KernelConformance) programReachesMarkedState(t *testing.T) { } } -func runUntargetedToMarked(ctx context.Context, runtime kernel.Runtime, scenario Scenario, maxTransitions int) ([]kernel.Receipt, error) { +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) @@ -377,10 +387,14 @@ func runUntargetedToMarked(ctx context.Context, runtime kernel.Runtime, scenario 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, before, scenario.Snapshot(), receipt); err != nil { + return nil, fmt.Errorf("control-law marked-reachability: apply %s: %w", resolution.Decision.Transition, err) + } receipts = append(receipts, receipt) } } @@ -427,16 +441,49 @@ func resolve(t testing.TB, runtime kernel.Runtime, scenario Scenario, transition return request, *resolution.Prescription } -func resolveAndApply(t testing.TB, runtime kernel.Runtime, scenario Scenario, transition string, objective *kernel.Objective) kernel.Receipt { +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", transition, err) + t.Fatalf("apply %s: %v", prescription.TransitionID, err) + } + if err := committedOutcomeError(program, before, scenario.Snapshot(), receipt); err != nil { + t.Fatalf("apply %s durable outcome: %v", prescription.TransitionID, err) } return receipt } +func committedOutcomeError(program kernel.Program, before, after Snapshot, 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") + } + durable := after.Receipts[len(after.Receipts)-1] + 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") + } + 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") + } + return nil +} + func effectCount(snapshot Snapshot, transition string) int { return snapshot.Effects[transition] } diff --git a/boatstack/kernel/conformance/conformance_test.go b/boatstack/kernel/conformance/conformance_test.go index b093c6d..c48208e 100644 --- a/boatstack/kernel/conformance/conformance_test.go +++ b/boatstack/kernel/conformance/conformance_test.go @@ -2,7 +2,9 @@ package conformance import ( "context" + "fmt" "strings" + "sync" "testing" "github.com/operatorstack/boatstack/boatstack/kernel" @@ -13,7 +15,7 @@ func TestMarkedStateCheckRejectsUntargetedPriorityCycle(t *testing.T) { targetedRuntime := mustRuntime(t, targeted) transitions := append([]string{targeted.Scenario.BindTransition}, targeted.Scenario.AdvanceTransitions...) for _, transition := range transitions { - resolveAndApply(t, targetedRuntime, targeted.Scenario, transition, &targeted.Scenario.Objective) + 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") @@ -21,12 +23,65 @@ func TestMarkedStateCheckRejectsUntargetedPriorityCycle(t *testing.T) { untargeted := integerPriorityCycleFixture() untargetedRuntime := mustRuntime(t, untargeted) - _, err := runUntargetedToMarked(context.Background(), untargetedRuntime, untargeted.Scenario, len(untargeted.Scenario.AdvanceTransitions)+1) + _, 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 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, before, fixture.Scenario.Snapshot(), returned); err == nil || !strings.Contains(err.Error(), "durable receipt differs") { + t.Fatalf("expected substituted durable receipt rejection, got %v", err) + } +} + +func TestCommittedOutcomeRejectsLosingApplyStateClobber(t *testing.T) { + fixture := newIntegerFixture(SetupConcurrentSameBase) + base := fixture.Store.(*MemoryStateStore) + base.beginBarrier = nil + clobber := newClobberAfterCommitStore(base) + fixture.Store = clobber + 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() + type result struct { + receipt kernel.Receipt + err error + } + results := make(chan result, 2) + for range 2 { + 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, before, fixture.Scenario.Snapshot(), winner); err == nil || !strings.Contains(err.Error(), "durable state differs") { + t.Fatalf("expected losing Apply state clobber rejection, got %v", err) + } +} + func integerPriorityCycleFixture() KernelConformance { fixture := newIntegerFixture(SetupUnbound) base, err := IntegerProgram() @@ -57,3 +112,56 @@ func mustRuntime(t *testing.T, fixture KernelConformance) kernel.Runtime { } return runtime } + +type substitutingReceiptStore struct { + kernel.Store + Receipt kernel.Receipt +} + +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 +} From 28957c9d5f6db4339ace3589d7e89b1873e217a3 Mon Sep 17 00:00:00 2001 From: bigboateng Date: Wed, 12 Aug 2026 15:36:55 +0100 Subject: [PATCH 4/9] test: isolate objective revision freshness --- boatstack/kernel/conformance/conformance.go | 10 +++++++--- boatstack/kernel/conformance/integer.go | 5 +++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/boatstack/kernel/conformance/conformance.go b/boatstack/kernel/conformance/conformance.go index ae5b6b1..776bb76 100644 --- a/boatstack/kernel/conformance/conformance.go +++ b/boatstack/kernel/conformance/conformance.go @@ -36,6 +36,7 @@ type Snapshot struct { type Scenario struct { InstanceID string Objective kernel.Objective + RevisedObjective kernel.Objective ConflictingObjective kernel.Objective Authority kernel.Authority BindTransition string @@ -126,11 +127,11 @@ func (suite KernelConformance) objectiveRevisionInvalidatesPrescription(t *testi 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.RebindObjective(fixture.Scenario.ConflictingObjective) - request.Objective = &fixture.Scenario.ConflictingObjective + fixture.Scenario.RebindObjective(fixture.Scenario.RevisedObjective) + request.Objective = &fixture.Scenario.RevisedObjective _, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) after := fixture.Scenario.Snapshot() - if !kernel.IsStale(err) || effectCount(after, transition) != effectCount(before, transition) || after.CommitCount != before.CommitCount { + if !kernel.IsStale(err) || effectCount(after, transition) != effectCount(before, transition) || after.CommitCount != before.CommitCount || len(after.Receipts) != len(before.Receipts) { t.Fatalf("control-law objective-revision-freshness: error=%v before=%#v after=%#v", err, before, after) } } @@ -428,6 +429,9 @@ func (suite KernelConformance) fixture(t testing.TB, setup Setup) KernelConforma 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.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.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") + } return fixture } diff --git a/boatstack/kernel/conformance/integer.go b/boatstack/kernel/conformance/integer.go index 5e2487e..b2a04e2 100644 --- a/boatstack/kernel/conformance/integer.go +++ b/boatstack/kernel/conformance/integer.go @@ -295,6 +295,10 @@ func newIntegerFixture(setup Setup) KernelConformance { 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) @@ -339,6 +343,7 @@ func newIntegerFixture(setup Setup) KernelConformance { fixture.Scenario = Scenario{ InstanceID: state.InstanceID, Objective: objective, + RevisedObjective: revised, ConflictingObjective: conflicting, Authority: authority, BindTransition: "objective.bind", From 5aabf7c6b8046787acfa7a2d4e02b344efa92d03 Mon Sep 17 00:00:00 2001 From: bigboateng Date: Wed, 12 Aug 2026 15:48:20 +0100 Subject: [PATCH 5/9] test: make conformance history-independent --- boatstack/kernel/conformance/conformance.go | 35 +++++++++++-------- .../kernel/conformance/conformance_test.go | 8 +++++ boatstack/kernel/conformance/integer.go | 16 ++++++++- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/boatstack/kernel/conformance/conformance.go b/boatstack/kernel/conformance/conformance.go index 776bb76..ecf489e 100644 --- a/boatstack/kernel/conformance/conformance.go +++ b/boatstack/kernel/conformance/conformance.go @@ -144,7 +144,7 @@ func (suite KernelConformance) stalePrescriptionPrecedesEffects(t *testing.T) { fixture.Scenario.ChangeObservation() _, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) after := fixture.Scenario.Snapshot() - if !kernel.IsStale(err) || effectCount(after, transition) != effectCount(before, transition) || after.CommitCount != before.CommitCount { + if !kernel.IsStale(err) || effectCount(after, transition) != effectCount(before, transition) || after.CommitCount != before.CommitCount || len(after.Receipts) != len(before.Receipts) { t.Fatalf("control-law prescription-freshness: error=%v before=%#v after=%#v", err, before, after) } } @@ -161,7 +161,7 @@ func (suite KernelConformance) authorityDenialFailsClosed(t *testing.T) { request.Authority = kernel.Authority{} _, err = runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) after := fixture.Scenario.Snapshot() - if !kernel.IsStale(err) || effectCount(after, transition) != effectCount(before, transition) || after.CommitCount != before.CommitCount { + if !kernel.IsStale(err) || effectCount(after, transition) != effectCount(before, transition) || after.CommitCount != before.CommitCount || len(after.Receipts) != len(before.Receipts) { t.Fatalf("control-law authority-denial: apply error=%v before=%#v after=%#v", err, before, after) } } @@ -175,7 +175,7 @@ func (suite KernelConformance) futureAuthorityFailsClosed(t *testing.T) { before := fixture.Scenario.Snapshot() resolution, err := runtime.Resolve(context.Background(), kernel.ResolveRequest{InstanceID: fixture.Scenario.InstanceID, Objective: &fixture.Scenario.Objective, Authority: authority, Requested: transition}) after := fixture.Scenario.Snapshot() - if err != nil || resolution.Decision.Kind != kernel.Refused || effectCount(after, transition) != effectCount(before, transition) || after.CommitCount != before.CommitCount { + if err != nil || resolution.Decision.Kind != kernel.Refused || effectCount(after, transition) != effectCount(before, transition) || after.CommitCount != before.CommitCount || len(after.Receipts) != len(before.Receipts) { t.Fatalf("control-law authority-time-validity: decision=%#v error=%v", resolution.Decision, err) } } @@ -191,7 +191,7 @@ func (suite KernelConformance) capabilityClassifierCannotBeWeakened(t *testing.T before := fixture.Scenario.Snapshot() resolution, err := runtime.Resolve(context.Background(), kernel.ResolveRequest{InstanceID: fixture.Scenario.InstanceID, Objective: &fixture.Scenario.Objective, Authority: fixture.Scenario.Authority, Requested: transition}) after := fixture.Scenario.Snapshot() - if err != nil || resolution.Decision.Kind != kernel.Frontier || effectCount(after, transition) != effectCount(before, transition) { + if err != nil || resolution.Decision.Kind != kernel.Frontier || effectCount(after, transition) != effectCount(before, transition) || after.CommitCount != before.CommitCount || len(after.Receipts) != len(before.Receipts) { t.Fatalf("control-law capability-non-weakening: decision=%#v error=%v", resolution.Decision, err) } } @@ -218,9 +218,10 @@ func (suite KernelConformance) interruptedOperatorRequiresExplicitRecovery(t *te 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 !kernel.IsRecoveryRequired(err) || interrupted.State.Recovery == nil || interrupted.State.Recovery.TransitionID != transition || effectCount(interrupted, transition) != 1 { + if !kernel.IsRecoveryRequired(err) || interrupted.State.Recovery == nil || interrupted.State.Recovery.TransitionID != transition || effectCount(interrupted, transition) != effectCount(before, transition)+1 || interrupted.CommitCount != before.CommitCount || len(interrupted.Receipts) != len(before.Receipts) { t.Fatalf("control-law explicit-recovery: state=%#v error=%v", interrupted, err) } recovery := resolveAndApply(t, runtime, fixture.Program, fixture.Scenario, fixture.Scenario.RecoveryTransition, &fixture.Scenario.Objective) @@ -235,17 +236,18 @@ func (suite KernelConformance) processPanicRequiresExplicitRecovery(t *testing.T 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 recovered == nil || after.State.Recovery == nil || after.State.Recovery.TransitionID != transition || effectCount(after, transition) != 1 { + if recovered == nil || after.State.Recovery == nil || after.State.Recovery.TransitionID != transition || effectCount(after, transition) != effectCount(before, transition)+1 || after.CommitCount != before.CommitCount || len(after.Receipts) != len(before.Receipts) { t.Fatalf("control-law panic-recovery: panic=%v snapshot=%#v", recovered, after) } resolution, err := runtime.Resolve(context.Background(), 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) != 1 { + 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) } } @@ -255,13 +257,14 @@ func (suite KernelConformance) atomicCommitFailureRequiresRecovery(t *testing.T) 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 !kernel.IsRecoveryRequired(err) || after.State.Recovery == nil || after.CommitCount != 0 || len(after.Receipts) != 0 || effectCount(after, transition) != 1 { + if !kernel.IsRecoveryRequired(err) || after.State.Recovery == nil || after.CommitCount != before.CommitCount || len(after.Receipts) != len(before.Receipts) || effectCount(after, transition) != effectCount(before, transition)+1 { t.Fatalf("control-law atomic-commit-recovery: snapshot=%#v error=%v", after, err) } resolution, resolveErr := runtime.Resolve(context.Background(), 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) != 1 { + 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) } } @@ -271,9 +274,10 @@ func (suite KernelConformance) failedRecoveryPreservesOriginalObligation(t *test 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 !kernel.IsRecoveryRequired(err) || interrupted.State.Recovery == nil { + if !kernel.IsRecoveryRequired(err) || interrupted.State.Recovery == nil || effectCount(interrupted, transition) != effectCount(before, transition)+1 || interrupted.CommitCount != before.CommitCount || len(interrupted.Receipts) != len(before.Receipts) { t.Fatalf("control-law recovery-retry: initial state=%#v error=%v", interrupted.State, err) } original := *interrupted.State.Recovery @@ -281,12 +285,12 @@ func (suite KernelConformance) failedRecoveryPreservesOriginalObligation(t *test fixture.Scenario.FailNextCommit() _, err = runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: recoveryRequest, Prescription: recoveryPrescription}) failed := fixture.Scenario.Snapshot() - if !kernel.IsRecoveryRequired(err) || failed.State.Recovery == nil || *failed.State.Recovery != original || effectCount(failed, transition) != 1 { + if !kernel.IsRecoveryRequired(err) || failed.State.Recovery == nil || *failed.State.Recovery != original || effectCount(failed, transition) != effectCount(interrupted, transition) || failed.CommitCount != interrupted.CommitCount || len(failed.Receipts) != len(interrupted.Receipts) { t.Fatalf("control-law recovery-obligation-preservation: state=%#v error=%v", failed.State, err) } resolveAndApply(t, runtime, fixture.Program, fixture.Scenario, fixture.Scenario.RecoveryTransition, &fixture.Scenario.Objective) settled := fixture.Scenario.Snapshot() - if settled.State.Recovery != nil || effectCount(settled, transition) != 1 { + if settled.State.Recovery != nil || effectCount(settled, transition) != effectCount(interrupted, transition) { t.Fatalf("control-law recovery-retry: state=%#v", settled.State) } } @@ -301,7 +305,7 @@ func (suite KernelConformance) prescriptionCannotReplayAcrossInstances(t *testin request.InstanceID = other _, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) after := fixture.Scenario.Snapshot() - if !kernel.IsStale(err) || effectCount(after, transition) != effectCount(before, transition) || after.CommitCount != before.CommitCount { + if !kernel.IsStale(err) || effectCount(after, transition) != effectCount(before, transition) || after.CommitCount != before.CommitCount || len(after.Receipts) != len(before.Receipts) { t.Fatalf("control-law prescription-instance-binding: error=%v snapshot=%#v", err, after) } } @@ -337,7 +341,7 @@ func (suite KernelConformance) concurrentApplyCommitsOnce(t *testing.T) { } } after := fixture.Scenario.Snapshot() - if successes != 1 || refusals != 1 || after.CommitCount != 1 || len(after.Receipts) != 1 || effectCount(after, transition) != 1 { + if successes != 1 || refusals != 1 || after.CommitCount != before.CommitCount+1 || len(after.Receipts) != len(before.Receipts)+1 || effectCount(after, transition) != effectCount(before, transition)+1 { t.Fatalf("control-law concurrent-cas: success/refusal=%d/%d snapshot=%#v", successes, refusals, after) } if err := committedOutcomeError(fixture.Program, before, after, winner); err != nil { @@ -347,6 +351,7 @@ func (suite KernelConformance) concurrentApplyCommitsOnce(t *testing.T) { 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) @@ -357,7 +362,7 @@ func (suite KernelConformance) programReachesMarkedState(t *testing.T) { } } after := fixture.Scenario.Snapshot() - if !fixture.Program.Marked(after.State.Mode) || len(after.Receipts) != len(receipts) { + if !fixture.Program.Marked(after.State.Mode) || len(after.Receipts) != len(before.Receipts)+len(receipts) { t.Fatalf("control-law marked-reachability: snapshot=%#v", after) } } diff --git a/boatstack/kernel/conformance/conformance_test.go b/boatstack/kernel/conformance/conformance_test.go index c48208e..16aa0f7 100644 --- a/boatstack/kernel/conformance/conformance_test.go +++ b/boatstack/kernel/conformance/conformance_test.go @@ -29,6 +29,14 @@ func TestMarkedStateCheckRejectsUntargetedPriorityCycle(t *testing.T) { } } +func TestIntegerBoundFixtureIncludesCommittedHistory(t *testing.T) { + fixture := newIntegerFixture(SetupBound) + snapshot := fixture.Scenario.Snapshot() + if snapshot.CommitCount != 1 || len(snapshot.Receipts) != 1 || snapshot.State.ObjectiveBinding == nil || !snapshot.State.ObjectiveBinding.Matches(fixture.Scenario.Objective) { + t.Fatalf("expected one committed objective-binding baseline, got %#v", snapshot) + } +} + func TestCommittedOutcomeRejectsDurableReceiptSubstitution(t *testing.T) { fixture := newIntegerFixture(SetupUnbound) runtime := mustRuntime(t, fixture) diff --git a/boatstack/kernel/conformance/integer.go b/boatstack/kernel/conformance/integer.go index b2a04e2..0dd645f 100644 --- a/boatstack/kernel/conformance/integer.go +++ b/boatstack/kernel/conformance/integer.go @@ -306,7 +306,7 @@ func newIntegerFixture(setup Setup) KernelConformance { state := kernel.ControlState{InstanceID: "counter-fixture", Program: program.Identity(), Mode: "unbound", Revision: 1} value := 0 switch setup { - case SetupBound, SetupConcurrentSameBase: + case SetupConcurrentSameBase: binding, bindErr := kernel.BindObjective(objective) if bindErr != nil { panic(bindErr) @@ -365,6 +365,20 @@ func newIntegerFixture(setup Setup) KernelConformance { 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) + } + request := kernel.ResolveRequest{InstanceID: state.InstanceID, Objective: &objective, Authority: authority, Requested: fixture.Scenario.BindTransition} + resolution, resolveErr := runtime.Resolve(context.Background(), request) + if resolveErr != nil || resolution.Prescription == nil { + panic(fmt.Sprintf("seed bound fixture: decision=%#v error=%v", resolution.Decision, resolveErr)) + } + if _, applyErr := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: *resolution.Prescription}); applyErr != nil { + panic(applyErr) + } + } return fixture } From e7df3b606ffbcad31ffd4b09ddb312c7aa213f68 Mon Sep 17 00:00:00 2001 From: bigboateng Date: Wed, 12 Aug 2026 16:15:51 +0100 Subject: [PATCH 6/9] test: close kernel conformance failure classes --- boatstack/kernel/conformance/conformance.go | 191 ++++++++++++++++-- .../kernel/conformance/conformance_test.go | 68 ++++++- boatstack/kernel/conformance/integer.go | 44 +++- 3 files changed, 272 insertions(+), 31 deletions(-) diff --git a/boatstack/kernel/conformance/conformance.go b/boatstack/kernel/conformance/conformance.go index ecf489e..b1d1365 100644 --- a/boatstack/kernel/conformance/conformance.go +++ b/boatstack/kernel/conformance/conformance.go @@ -26,6 +26,7 @@ const ( // 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 @@ -38,14 +39,18 @@ type Scenario struct { Objective kernel.Objective RevisedObjective kernel.Objective ConflictingObjective kernel.Objective + AlternateProgram kernel.ProgramIdentity 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() + RetargetProgram func(kernel.ProgramIdentity) InterruptNextOperator func() PanicNextOperator func() FailNextCommit func() @@ -75,12 +80,15 @@ func (suite KernelConformance) Run(t *testing.T) { 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) @@ -126,13 +134,39 @@ func (suite KernelConformance) objectiveRevisionInvalidatesPrescription(t *testi 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.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 !kernel.IsStale(err) || effectCount(after, transition) != effectCount(before, transition) || after.CommitCount != before.CommitCount || len(after.Receipts) != len(before.Receipts) { - t.Fatalf("control-law objective-revision-freshness: error=%v before=%#v after=%#v", err, before, after) + 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) + fixture.Scenario.RetargetProgram(fixture.Scenario.AlternateProgram) + 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 program-fingerprint-freshness: error=%v mutation=%v before=%#v after=%#v", err, unchangedErr, before, after) } } @@ -142,10 +176,11 @@ func (suite KernelConformance) stalePrescriptionPrecedesEffects(t *testing.T) { 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 !kernel.IsStale(err) || effectCount(after, transition) != effectCount(before, transition) || after.CommitCount != before.CommitCount || len(after.Receipts) != len(before.Receipts) { - t.Fatalf("control-law prescription-freshness: error=%v before=%#v after=%#v", err, before, after) + 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) } } @@ -161,8 +196,8 @@ func (suite KernelConformance) authorityDenialFailsClosed(t *testing.T) { request.Authority = kernel.Authority{} _, err = runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) after := fixture.Scenario.Snapshot() - if !kernel.IsStale(err) || effectCount(after, transition) != effectCount(before, transition) || after.CommitCount != before.CommitCount || len(after.Receipts) != len(before.Receipts) { - t.Fatalf("control-law authority-denial: apply error=%v before=%#v after=%#v", err, before, after) + 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) } } @@ -221,8 +256,8 @@ func (suite KernelConformance) interruptedOperatorRequiresExplicitRecovery(t *te before := fixture.Scenario.Snapshot() _, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) interrupted := fixture.Scenario.Snapshot() - if !kernel.IsRecoveryRequired(err) || interrupted.State.Recovery == nil || interrupted.State.Recovery.TransitionID != transition || effectCount(interrupted, transition) != effectCount(before, transition)+1 || interrupted.CommitCount != before.CommitCount || len(interrupted.Receipts) != len(before.Receipts) { - t.Fatalf("control-law explicit-recovery: state=%#v error=%v", interrupted, err) + 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() @@ -231,6 +266,26 @@ func (suite KernelConformance) interruptedOperatorRequiresExplicitRecovery(t *te } } +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 := runtime.Resolve(context.Background(), 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] @@ -243,8 +298,8 @@ func (suite KernelConformance) processPanicRequiresExplicitRecovery(t *testing.T _, _ = runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) }() after := fixture.Scenario.Snapshot() - if recovered == nil || after.State.Recovery == nil || after.State.Recovery.TransitionID != transition || effectCount(after, transition) != effectCount(before, transition)+1 || after.CommitCount != before.CommitCount || len(after.Receipts) != len(before.Receipts) { - t.Fatalf("control-law panic-recovery: panic=%v snapshot=%#v", recovered, after) + 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 := runtime.Resolve(context.Background(), 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 { @@ -260,8 +315,8 @@ func (suite KernelConformance) atomicCommitFailureRequiresRecovery(t *testing.T) before := fixture.Scenario.Snapshot() _, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) after := fixture.Scenario.Snapshot() - if !kernel.IsRecoveryRequired(err) || after.State.Recovery == nil || after.CommitCount != before.CommitCount || len(after.Receipts) != len(before.Receipts) || effectCount(after, transition) != effectCount(before, transition)+1 { - t.Fatalf("control-law atomic-commit-recovery: snapshot=%#v error=%v", after, err) + 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 := runtime.Resolve(context.Background(), 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 { @@ -277,16 +332,16 @@ func (suite KernelConformance) failedRecoveryPreservesOriginalObligation(t *test before := fixture.Scenario.Snapshot() _, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) interrupted := fixture.Scenario.Snapshot() - if !kernel.IsRecoveryRequired(err) || interrupted.State.Recovery == nil || effectCount(interrupted, transition) != effectCount(before, transition)+1 || interrupted.CommitCount != before.CommitCount || len(interrupted.Receipts) != len(before.Receipts) { - t.Fatalf("control-law recovery-retry: initial state=%#v error=%v", interrupted.State, err) + 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 !kernel.IsRecoveryRequired(err) || failed.State.Recovery == nil || *failed.State.Recovery != original || effectCount(failed, transition) != effectCount(interrupted, transition) || failed.CommitCount != interrupted.CommitCount || len(failed.Receipts) != len(interrupted.Receipts) { - t.Fatalf("control-law recovery-obligation-preservation: state=%#v error=%v", failed.State, err) + 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) } resolveAndApply(t, runtime, fixture.Program, fixture.Scenario, fixture.Scenario.RecoveryTransition, &fixture.Scenario.Objective) settled := fixture.Scenario.Snapshot() @@ -302,11 +357,12 @@ func (suite KernelConformance) prescriptionCannotReplayAcrossInstances(t *testin 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 !kernel.IsStale(err) || effectCount(after, transition) != effectCount(before, transition) || after.CommitCount != before.CommitCount || len(after.Receipts) != len(before.Receipts) { - t.Fatalf("control-law prescription-instance-binding: error=%v snapshot=%#v", err, after) + 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) } } @@ -431,12 +487,15 @@ func (suite KernelConformance) fixture(t testing.TB, setup Setup) KernelConforma 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.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.ExtraCapability.Validate() != nil { + 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.RetargetProgram == 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 } @@ -476,13 +535,19 @@ func committedOutcomeError(program kernel.Program, before, after Snapshot, retur 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") } - durable := after.Receipts[len(after.Receipts)-1] + 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 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") @@ -490,9 +555,93 @@ func committedOutcomeError(program kernel.Program, before, after Snapshot, retur 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) + } + 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 !reflect.DeepEqual(after.State, before.State) || !reflect.DeepEqual(after.Observation, before.Observation) || !reflect.DeepEqual(after.Effects, before.Effects) || after.CommitCount != before.CommitCount || !receiptHistoryEqual(after.Receipts, before.Receipts) { + return fmt.Errorf("refused Apply mutated control state, history, or effects") + } + return 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] } diff --git a/boatstack/kernel/conformance/conformance_test.go b/boatstack/kernel/conformance/conformance_test.go index 16aa0f7..af8ba47 100644 --- a/boatstack/kernel/conformance/conformance_test.go +++ b/boatstack/kernel/conformance/conformance_test.go @@ -32,8 +32,8 @@ func TestMarkedStateCheckRejectsUntargetedPriorityCycle(t *testing.T) { func TestIntegerBoundFixtureIncludesCommittedHistory(t *testing.T) { fixture := newIntegerFixture(SetupBound) snapshot := fixture.Scenario.Snapshot() - if snapshot.CommitCount != 1 || len(snapshot.Receipts) != 1 || snapshot.State.ObjectiveBinding == nil || !snapshot.State.ObjectiveBinding.Matches(fixture.Scenario.Objective) { - t.Fatalf("expected one committed objective-binding baseline, got %#v", 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) { + t.Fatalf("expected committed bind/advance/reset baseline, got %#v", snapshot) } } @@ -57,6 +57,44 @@ func TestCommittedOutcomeRejectsDurableReceiptSubstitution(t *testing.T) { } } +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, before, fixture.Scenario.Snapshot(), 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, before, fixture.Scenario.Snapshot(), 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) @@ -90,6 +128,23 @@ func TestCommittedOutcomeRejectsLosingApplyStateClobber(t *testing.T) { } } +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() @@ -126,6 +181,15 @@ type substitutingReceiptStore struct { Receipt kernel.Receipt } +type tornCommitStore struct{ *MemoryStateStore } + +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) } diff --git a/boatstack/kernel/conformance/integer.go b/boatstack/kernel/conformance/integer.go index 0dd645f..ad33e72 100644 --- a/boatstack/kernel/conformance/integer.go +++ b/boatstack/kernel/conformance/integer.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "sync" "testing" "time" @@ -230,6 +231,18 @@ func (s *MemoryStateStore) retarget(instanceID string) { 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() @@ -303,6 +316,11 @@ func newIntegerFixture(setup Setup) KernelConformance { if err != nil { panic(err) } + alternateProgram := program.Identity() + alternateProgram.Fingerprint = strings.Repeat("f", 64) + if alternateProgram.Fingerprint == program.Fingerprint { + alternateProgram.Fingerprint = strings.Repeat("e", 64) + } state := kernel.ControlState{InstanceID: "counter-fixture", Program: program.Identity(), Mode: "unbound", Revision: 1} value := 0 switch setup { @@ -345,21 +363,29 @@ func newIntegerFixture(setup Setup) KernelConformance { 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, + RetargetProgram: store.retargetProgram, InterruptNextOperator: domain.interruptNext, PanicNextOperator: domain.panicNext, FailNextCommit: store.failNextCommit, RetargetInstance: store.retarget, Snapshot: func() Snapshot { current, commits := store.snapshot() - return Snapshot{State: current, Effects: domain.effectCounts(), Receipts: receipts.snapshot(), CommitCount: commits} + 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 { @@ -370,13 +396,15 @@ func newIntegerFixture(setup Setup) KernelConformance { if runtimeErr != nil { panic(runtimeErr) } - request := kernel.ResolveRequest{InstanceID: state.InstanceID, Objective: &objective, Authority: authority, Requested: fixture.Scenario.BindTransition} - resolution, resolveErr := runtime.Resolve(context.Background(), request) - if resolveErr != nil || resolution.Prescription == nil { - panic(fmt.Sprintf("seed bound fixture: decision=%#v error=%v", resolution.Decision, resolveErr)) - } - if _, applyErr := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: *resolution.Prescription}); applyErr != nil { - panic(applyErr) + 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 From 9cda09a08910e1fa1e0798daa5599cce5d90191c Mon Sep 17 00:00:00 2001 From: bigboateng Date: Wed, 12 Aug 2026 16:56:09 +0100 Subject: [PATCH 7/9] test: bind complete conformance outcomes --- boatstack/kernel/conformance/conformance.go | 93 +++++++++---- .../kernel/conformance/conformance_test.go | 123 +++++++++++++++++- boatstack/kernel/conformance/integer.go | 2 +- 3 files changed, 189 insertions(+), 29 deletions(-) diff --git a/boatstack/kernel/conformance/conformance.go b/boatstack/kernel/conformance/conformance.go index b1d1365..70b4f14 100644 --- a/boatstack/kernel/conformance/conformance.go +++ b/boatstack/kernel/conformance/conformance.go @@ -189,7 +189,7 @@ func (suite KernelConformance) authorityDenialFailsClosed(t *testing.T) { 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 := runtime.Resolve(context.Background(), kernel.ResolveRequest{InstanceID: fixture.Scenario.InstanceID, Objective: &fixture.Scenario.Objective, Requested: transition}) + 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) } @@ -207,10 +207,8 @@ func (suite KernelConformance) futureAuthorityFailsClosed(t *testing.T) { authority := fixture.Scenario.Authority authority.Receipts = append([]kernel.AuthorityReceipt(nil), authority.Receipts...) authority.Receipts[0].IssuedAt = fixture.Clock.Now().Add(time.Second) - before := fixture.Scenario.Snapshot() - resolution, err := runtime.Resolve(context.Background(), kernel.ResolveRequest{InstanceID: fixture.Scenario.InstanceID, Objective: &fixture.Scenario.Objective, Authority: authority, Requested: transition}) - after := fixture.Scenario.Snapshot() - if err != nil || resolution.Decision.Kind != kernel.Refused || effectCount(after, transition) != effectCount(before, transition) || after.CommitCount != before.CommitCount || len(after.Receipts) != len(before.Receipts) { + 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) } } @@ -223,10 +221,9 @@ func (suite KernelConformance) capabilityClassifierCannotBeWeakened(t *testing.T if err != nil { t.Fatal(err) } - before := fixture.Scenario.Snapshot() - resolution, err := runtime.Resolve(context.Background(), kernel.ResolveRequest{InstanceID: fixture.Scenario.InstanceID, Objective: &fixture.Scenario.Objective, Authority: fixture.Scenario.Authority, Requested: transition}) - after := fixture.Scenario.Snapshot() - if err != nil || resolution.Decision.Kind != kernel.Frontier || effectCount(after, transition) != effectCount(before, transition) || after.CommitCount != before.CommitCount || len(after.Receipts) != len(before.Receipts) { + 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) } } @@ -235,13 +232,13 @@ 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 := runtime.Resolve(ctx, untargetedRequest) + 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 := runtime.Resolve(ctx, targetedRequest) + 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) } @@ -278,7 +275,7 @@ func (suite KernelConformance) recoveryAuthorityFailsClosed(t *testing.T) { t.Fatalf("control-law recovery-authority setup: error=%v attempt=%v", err, attemptErr) } limited := withoutCapability(fixture.Scenario.Authority, fixture.Scenario.RecoveryCapability) - resolution, err := runtime.Resolve(context.Background(), kernel.ResolveRequest{InstanceID: fixture.Scenario.InstanceID, Objective: &fixture.Scenario.Objective, Authority: limited}) + 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) @@ -301,7 +298,7 @@ func (suite KernelConformance) processPanicRequiresExplicitRecovery(t *testing.T 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 := runtime.Resolve(context.Background(), kernel.ResolveRequest{InstanceID: fixture.Scenario.InstanceID, Objective: &fixture.Scenario.Objective, Authority: fixture.Scenario.Authority}) + 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) } @@ -318,7 +315,7 @@ func (suite KernelConformance) atomicCommitFailureRequiresRecovery(t *testing.T) 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 := runtime.Resolve(context.Background(), kernel.ResolveRequest{InstanceID: fixture.Scenario.InstanceID, Objective: &fixture.Scenario.Objective, Authority: fixture.Scenario.Authority}) + 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) } @@ -343,7 +340,11 @@ func (suite KernelConformance) failedRecoveryPreservesOriginalObligation(t *test 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) } - resolveAndApply(t, runtime, fixture.Program, fixture.Scenario, fixture.Scenario.RecoveryTransition, &fixture.Scenario.Objective) + 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) @@ -400,7 +401,7 @@ func (suite KernelConformance) concurrentApplyCommitsOnce(t *testing.T) { if successes != 1 || refusals != 1 || after.CommitCount != before.CommitCount+1 || len(after.Receipts) != len(before.Receipts)+1 || effectCount(after, transition) != effectCount(before, transition)+1 { t.Fatalf("control-law concurrent-cas: success/refusal=%d/%d snapshot=%#v", successes, refusals, after) } - if err := committedOutcomeError(fixture.Program, before, after, winner); err != nil { + if err := committedOutcomeError(fixture.Program, before, after, prescription, winner); err != nil { t.Fatalf("control-law concurrent-cas durable outcome: %v", err) } } @@ -428,7 +429,7 @@ func runUntargetedToMarked(ctx context.Context, runtime kernel.Runtime, program seen := map[string]bool{} receipts := make([]kernel.Receipt, 0, maxTransitions) for step := 0; ; step++ { - resolution, err := runtime.Resolve(ctx, request) + resolution, err := resolveWithoutMutation(ctx, runtime, scenario, request) if err != nil { return nil, fmt.Errorf("control-law marked-reachability: resolve step %d: %w", step, err) } @@ -454,7 +455,7 @@ func runUntargetedToMarked(ctx context.Context, runtime kernel.Runtime, program if err != nil { return nil, fmt.Errorf("control-law marked-reachability: apply %s: %w", resolution.Decision.Transition, err) } - if err := committedOutcomeError(program, before, scenario.Snapshot(), receipt); err != nil { + if err := committedOutcomeError(program, 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) @@ -502,7 +503,7 @@ func (suite KernelConformance) fixture(t testing.TB, setup Setup) KernelConforma 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 := runtime.Resolve(context.Background(), request) + 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) } @@ -522,13 +523,13 @@ func applyAndRequireCommit(t testing.TB, runtime kernel.Runtime, program kernel. if err != nil { t.Fatalf("apply %s: %v", prescription.TransitionID, err) } - if err := committedOutcomeError(program, before, scenario.Snapshot(), receipt); err != nil { + if err := committedOutcomeError(program, before, scenario.Snapshot(), prescription, receipt); err != nil { t.Fatalf("apply %s durable outcome: %v", prescription.TransitionID, err) } return receipt } -func committedOutcomeError(program kernel.Program, before, after Snapshot, returned kernel.Receipt) error { +func committedOutcomeError(program kernel.Program, 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) } @@ -545,6 +546,18 @@ func committedOutcomeError(program kernel.Program, before, after Snapshot, retur 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") } @@ -587,12 +600,46 @@ func unresolvedAttemptError(before, after Snapshot, prescription kernel.Prescrip } func refusedApplyMutationError(before, after Snapshot, _ string) error { - if !reflect.DeepEqual(after.State, before.State) || !reflect.DeepEqual(after.Observation, before.Observation) || !reflect.DeepEqual(after.Effects, before.Effects) || after.CommitCount != before.CommitCount || !receiptHistoryEqual(after.Receipts, before.Receipts) { - return fmt.Errorf("refused Apply mutated control state, history, or effects") + 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 { diff --git a/boatstack/kernel/conformance/conformance_test.go b/boatstack/kernel/conformance/conformance_test.go index af8ba47..f1a5d4d 100644 --- a/boatstack/kernel/conformance/conformance_test.go +++ b/boatstack/kernel/conformance/conformance_test.go @@ -6,6 +6,7 @@ import ( "strings" "sync" "testing" + "time" "github.com/operatorstack/boatstack/boatstack/kernel" ) @@ -32,11 +33,49 @@ func TestMarkedStateCheckRejectsUntargetedPriorityCycle(t *testing.T) { 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) { + 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) @@ -52,7 +91,7 @@ func TestCommittedOutcomeRejectsDurableReceiptSubstitution(t *testing.T) { if err != nil { t.Fatal(err) } - if err := committedOutcomeError(fixture.Program, before, fixture.Scenario.Snapshot(), returned); err == nil || !strings.Contains(err.Error(), "durable receipt differs") { + if err := committedOutcomeError(fixture.Program, 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) } } @@ -71,7 +110,7 @@ func TestCommittedOutcomeRejectsPriorReceiptRewrite(t *testing.T) { receipts.mu.Lock() receipts.values[0] = returned receipts.mu.Unlock() - if err := committedOutcomeError(fixture.Program, before, fixture.Scenario.Snapshot(), returned); err == nil || !strings.Contains(err.Error(), "prior receipt history") { + if err := committedOutcomeError(fixture.Program, 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) } } @@ -90,7 +129,7 @@ func TestCommittedOutcomeRejectsUnrelatedEffect(t *testing.T) { domain.mu.Lock() domain.executions[fixture.Scenario.MaintenanceTransition]++ domain.mu.Unlock() - if err := committedOutcomeError(fixture.Program, before, fixture.Scenario.Snapshot(), returned); err == nil || !strings.Contains(err.Error(), "effect evidence") { + if err := committedOutcomeError(fixture.Program, before, fixture.Scenario.Snapshot(), prescription, returned); err == nil || !strings.Contains(err.Error(), "effect evidence") { t.Fatalf("expected unrelated effect rejection, got %v", err) } } @@ -123,11 +162,40 @@ func TestCommittedOutcomeRejectsLosingApplyStateClobber(t *testing.T) { winner = result.receipt } } - if err := committedOutcomeError(fixture.Program, before, fixture.Scenario.Snapshot(), winner); err == nil || !strings.Contains(err.Error(), "durable state differs") { + if err := committedOutcomeError(fixture.Program, 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, 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 TestUnresolvedAttemptRejectsTornTargetMode(t *testing.T) { fixture := newIntegerFixture(SetupBound) base := fixture.Store.(*MemoryStateStore) @@ -167,6 +235,41 @@ func integerPriorityCycleFixture() KernelConformance { 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) @@ -181,6 +284,16 @@ type substitutingReceiptStore struct { 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 } func (s tornCommitStore) CommitTransition(_ context.Context, _ uint64, target kernel.ControlState, _ kernel.Receipt) error { diff --git a/boatstack/kernel/conformance/integer.go b/boatstack/kernel/conformance/integer.go index ad33e72..d389b6c 100644 --- a/boatstack/kernel/conformance/integer.go +++ b/boatstack/kernel/conformance/integer.go @@ -340,7 +340,7 @@ func newIntegerFixture(setup Setup) KernelConformance { 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{"objective.bind", "counter.increment", "counter.reset"}, IssuedAt: now.Add(-time.Minute), ExpiresAt: now.Add(time.Hour)}}} + 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} From 06fe15993549d26297d5c25d9df7dc4a0d90c841 Mon Sep 17 00:00:00 2001 From: bigboateng Date: Wed, 12 Aug 2026 17:19:02 +0100 Subject: [PATCH 8/9] test: use independent conformance oracles --- boatstack/kernel/conformance/conformance.go | 109 +++++++++++++++--- .../kernel/conformance/conformance_test.go | 86 ++++++++++++-- boatstack/kernel/conformance/integer.go | 83 ++++++------- 3 files changed, 215 insertions(+), 63 deletions(-) diff --git a/boatstack/kernel/conformance/conformance.go b/boatstack/kernel/conformance/conformance.go index 70b4f14..00303bb 100644 --- a/boatstack/kernel/conformance/conformance.go +++ b/boatstack/kernel/conformance/conformance.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "reflect" + "sync" "testing" "time" @@ -39,7 +40,7 @@ type Scenario struct { Objective kernel.Objective RevisedObjective kernel.Objective ConflictingObjective kernel.Objective - AlternateProgram kernel.ProgramIdentity + AlternateProgram kernel.Program Authority kernel.Authority BindTransition string AdvanceTransitions []string @@ -50,7 +51,8 @@ type Scenario struct { ChangeObservation func() RebindObjective func(kernel.Objective) BumpStateRevision func() - RetargetProgram func(kernel.ProgramIdentity) + IndependentLocker func() kernel.Locker + VerifyCommitted func(Snapshot, Snapshot, kernel.Receipt) error InterruptNextOperator func() PanicNextOperator func() FailNextCommit func() @@ -161,11 +163,14 @@ func (suite KernelConformance) programFingerprintInvalidatesPrescription(t *test 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.RetargetProgram(fixture.Scenario.AlternateProgram) + 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 := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) + _, err = alternate.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) after := fixture.Scenario.Snapshot() - if unchangedErr := refusedApplyMutationError(before, after, transition); !kernel.IsStale(err) || unchangedErr != nil { + 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) } } @@ -368,17 +373,39 @@ func (suite KernelConformance) prescriptionCannotReplayAcrossInstances(t *testin } func (suite KernelConformance) concurrentApplyCommitsOnce(t *testing.T) { - fixture, runtime := suite.fresh(t, SetupConcurrentSameBase) + fixture, resolver := suite.fresh(t, SetupConcurrentSameBase) + if err := concurrentApplyError(fixture, resolver); err != nil { + t.Fatalf("control-law concurrent-cas: %v", err) + } +} + +func concurrentApplyError(fixture KernelConformance, resolver kernel.Runtime) error { transition := fixture.Scenario.AdvanceTransitions[0] - request, prescription := resolve(t, runtime, fixture.Scenario, transition, &fixture.Scenario.Objective, fixture.Scenario.Authority) + 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() + store := &sameBaseLoadStore{Store: fixture.Store, barrier: newTwoPartyBarrier()} + domain := &sameBaseObservationDomain{Domain: fixture.Domain, barrier: newTwoPartyBarrier()} + 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 range 2 { + for _, runtime := range []kernel.Runtime{runtimeA, runtimeB} { + runtime := runtime go func() { <-start receipt, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) @@ -399,11 +426,60 @@ func (suite KernelConformance) concurrentApplyCommitsOnce(t *testing.T) { } 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 { - t.Fatalf("control-law concurrent-cas: success/refusal=%d/%d snapshot=%#v", successes, refusals, after) + return fmt.Errorf("success/refusal=%d/%d snapshot=%#v", successes, refusals, after) } - if err := committedOutcomeError(fixture.Program, before, after, prescription, winner); err != nil { - t.Fatalf("control-law concurrent-cas durable outcome: %v", err) + 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{} +} + +func newTwoPartyBarrier() *twoPartyBarrier { + return &twoPartyBarrier{ready: make(chan struct{})} +} + +func (b *twoPartyBarrier) wait() { + b.mu.Lock() + b.arrivals++ + if b.arrivals == 2 { + close(b.ready) + } + b.mu.Unlock() + <-b.ready +} + +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) + if err != nil { + return kernel.ControlState{}, err + } + s.barrier.wait() + 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) + if err != nil { + return kernel.Observation{}, err + } + d.barrier.wait() + return observation, nil } func (suite KernelConformance) programReachesMarkedState(t *testing.T) { @@ -455,7 +531,7 @@ func runUntargetedToMarked(ctx context.Context, runtime kernel.Runtime, program if err != nil { return nil, fmt.Errorf("control-law marked-reachability: apply %s: %w", resolution.Decision.Transition, err) } - if err := committedOutcomeError(program, before, scenario.Snapshot(), *resolution.Prescription, receipt); err != nil { + 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) @@ -488,7 +564,7 @@ func (suite KernelConformance) fixture(t testing.TB, setup Setup) KernelConforma 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.RetargetProgram == 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 { + 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 { @@ -523,13 +599,13 @@ func applyAndRequireCommit(t testing.TB, runtime kernel.Runtime, program kernel. if err != nil { t.Fatalf("apply %s: %v", prescription.TransitionID, err) } - if err := committedOutcomeError(program, before, scenario.Snapshot(), prescription, receipt); err != nil { + 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, before, after Snapshot, prescription kernel.Prescription, returned kernel.Receipt) error { +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) } @@ -571,6 +647,9 @@ func committedOutcomeError(program kernel.Program, before, after Snapshot, presc 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 } diff --git a/boatstack/kernel/conformance/conformance_test.go b/boatstack/kernel/conformance/conformance_test.go index f1a5d4d..36d3dc5 100644 --- a/boatstack/kernel/conformance/conformance_test.go +++ b/boatstack/kernel/conformance/conformance_test.go @@ -91,7 +91,7 @@ func TestCommittedOutcomeRejectsDurableReceiptSubstitution(t *testing.T) { if err != nil { t.Fatal(err) } - if err := committedOutcomeError(fixture.Program, before, fixture.Scenario.Snapshot(), prescription, returned); err == nil || !strings.Contains(err.Error(), "durable receipt differs") { + 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) } } @@ -110,7 +110,7 @@ func TestCommittedOutcomeRejectsPriorReceiptRewrite(t *testing.T) { receipts.mu.Lock() receipts.values[0] = returned receipts.mu.Unlock() - if err := committedOutcomeError(fixture.Program, before, fixture.Scenario.Snapshot(), prescription, returned); err == nil || !strings.Contains(err.Error(), "prior receipt history") { + 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) } } @@ -129,7 +129,7 @@ func TestCommittedOutcomeRejectsUnrelatedEffect(t *testing.T) { domain.mu.Lock() domain.executions[fixture.Scenario.MaintenanceTransition]++ domain.mu.Unlock() - if err := committedOutcomeError(fixture.Program, before, fixture.Scenario.Snapshot(), prescription, returned); err == nil || !strings.Contains(err.Error(), "effect evidence") { + 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) } } @@ -137,19 +137,27 @@ func TestCommittedOutcomeRejectsUnrelatedEffect(t *testing.T) { func TestCommittedOutcomeRejectsLosingApplyStateClobber(t *testing.T) { fixture := newIntegerFixture(SetupConcurrentSameBase) base := fixture.Store.(*MemoryStateStore) - base.beginBarrier = nil clobber := newClobberAfterCommitStore(base) fixture.Store = clobber - runtime := mustRuntime(t, fixture) + resolver := mustRuntime(t, fixture) transition := fixture.Scenario.AdvanceTransitions[0] - request, prescription := resolve(t, runtime, fixture.Scenario, transition, &fixture.Scenario.Objective, fixture.Scenario.Authority) + 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 range 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} @@ -162,7 +170,7 @@ func TestCommittedOutcomeRejectsLosingApplyStateClobber(t *testing.T) { winner = result.receipt } } - if err := committedOutcomeError(fixture.Program, before, fixture.Scenario.Snapshot(), prescription, winner); err == nil || !strings.Contains(err.Error(), "durable state differs") { + 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) } } @@ -191,11 +199,38 @@ func TestCommittedOutcomeRejectsFalsePriorObservation(t *testing.T) { receipts.mu.Lock() receipts.values[len(receipts.values)-1] = returned receipts.mu.Unlock() - if err := committedOutcomeError(fixture.Program, before, fixture.Scenario.Snapshot(), prescription, returned); err == nil || !strings.Contains(err.Error(), "prior observation") { + 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 TestUnresolvedAttemptRejectsTornTargetMode(t *testing.T) { fixture := newIntegerFixture(SetupBound) base := fixture.Store.(*MemoryStateStore) @@ -296,6 +331,39 @@ func (s effectfulLoadStore) Load(ctx context.Context, instanceID string) (kernel 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 } + +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() diff --git a/boatstack/kernel/conformance/integer.go b/boatstack/kernel/conformance/integer.go index d389b6c..626501c 100644 --- a/boatstack/kernel/conformance/integer.go +++ b/boatstack/kernel/conformance/integer.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "strings" "sync" "testing" "time" @@ -175,7 +174,6 @@ type MemoryStateStore struct { receipts *MemoryReceipts commitFailures int commitCount int - beginBarrier *twoPartyBarrier } func (s *MemoryStateStore) Load(context.Context, string) (kernel.ControlState, error) { @@ -185,9 +183,6 @@ func (s *MemoryStateStore) Load(context.Context, string) (kernel.ControlState, e } func (s *MemoryStateStore) BeginEffect(_ context.Context, revision uint64, target kernel.ControlState) error { - if s.beginBarrier != nil { - s.beginBarrier.wait() - } s.mu.Lock() defer s.mu.Unlock() if s.state.Revision != revision { @@ -268,12 +263,6 @@ func (l memoryLock) Unlock() error { return nil } -type noOpLocker struct{} -type noOpLock struct{} - -func (noOpLocker) Acquire(context.Context, string) (kernel.Lock, error) { return noOpLock{}, nil } -func (noOpLock) Unlock() error { return nil } - // FixedClock returns one deterministic time. type FixedClock struct{ Time time.Time } @@ -316,10 +305,15 @@ func newIntegerFixture(setup Setup) KernelConformance { if err != nil { panic(err) } - alternateProgram := program.Identity() - alternateProgram.Fingerprint = strings.Repeat("f", 64) - if alternateProgram.Fingerprint == program.Fingerprint { - alternateProgram.Fingerprint = strings.Repeat("e", 64) + 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 @@ -344,17 +338,12 @@ func newIntegerFixture(setup Setup) KernelConformance { domain := &IntegerDomain{value: value, executions: map[string]int{}} receipts := &MemoryReceipts{} store := &MemoryStateStore{state: state, receipts: receipts} - var locker kernel.Locker = &MemoryLocker{} - if setup == SetupConcurrentSameBase { - store.beginBarrier = newTwoPartyBarrier() - locker = noOpLocker{} - } fixture := KernelConformance{ Domain: domain, Operator: IntegerOperator{Domain: domain}, CapabilityClassifier: IntegerCapabilities{}, Store: store, - Locker: locker, + Locker: &MemoryLocker{}, Clock: FixedClock{Time: now}, Program: program, } @@ -374,7 +363,10 @@ func newIntegerFixture(setup Setup) KernelConformance { ChangeObservation: domain.changeObservation, RebindObjective: store.rebind, BumpStateRevision: store.bumpRevision, - RetargetProgram: store.retargetProgram, + 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, @@ -410,24 +402,37 @@ func newIntegerFixture(setup Setup) KernelConformance { return fixture } -type twoPartyBarrier struct { - mu sync.Mutex - arrivals int - ready chan struct{} -} - -func newTwoPartyBarrier() *twoPartyBarrier { - return &twoPartyBarrier{ready: make(chan struct{})} -} - -func (b *twoPartyBarrier) wait() { - b.mu.Lock() - b.arrivals++ - if b.arrivals == 2 { - close(b.ready) +func verifyIntegerCommitted(program kernel.Program, before, after Snapshot, receipt kernel.Receipt) error { + var prior, result struct { + Value int `json:"value"` } - b.mu.Unlock() - <-b.ready + 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) + } + 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 { From 96367710109432fcd02cc3925c9e4a8314ee8bd7 Mon Sep 17 00:00:00 2001 From: bigboateng Date: Wed, 12 Aug 2026 17:31:38 +0100 Subject: [PATCH 9/9] test: close conformance oracle gaps --- boatstack/kernel/conformance/conformance.go | 107 ++++++++++++++++-- .../kernel/conformance/conformance_test.go | 79 +++++++++++++ boatstack/kernel/conformance/integer.go | 8 ++ 3 files changed, 185 insertions(+), 9 deletions(-) diff --git a/boatstack/kernel/conformance/conformance.go b/boatstack/kernel/conformance/conformance.go index 00303bb..1edecb0 100644 --- a/boatstack/kernel/conformance/conformance.go +++ b/boatstack/kernel/conformance/conformance.go @@ -96,6 +96,7 @@ func (suite KernelConformance) Run(t *testing.T) { 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) } @@ -379,6 +380,53 @@ func (suite KernelConformance) concurrentApplyCommitsOnce(t *testing.T) { } } +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} @@ -388,8 +436,10 @@ func concurrentApplyError(fixture KernelConformance, resolver kernel.Runtime) er } prescription := *resolution.Prescription before := fixture.Scenario.Snapshot() - store := &sameBaseLoadStore{Store: fixture.Store, barrier: newTwoPartyBarrier()} - domain := &sameBaseObservationDomain{Domain: fixture.Domain, barrier: newTwoPartyBarrier()} + 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 @@ -408,8 +458,20 @@ func concurrentApplyError(fixture KernelConformance, resolver kernel.Runtime) er runtime := runtime go func() { <-start - receipt, err := runtime.Apply(context.Background(), kernel.ApplyRequest{ResolveRequest: request, Prescription: prescription}) - results <- applyResult{receipt: receipt, err: err} + 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) @@ -438,20 +500,41 @@ 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{})} + return &twoPartyBarrier{ready: make(chan struct{}), canceled: make(chan struct{})} } -func (b *twoPartyBarrier) wait() { +func (b *twoPartyBarrier) wait() error { b.mu.Lock() b.arrivals++ if b.arrivals == 2 { close(b.ready) } + complete := b.arrivals >= 2 b.mu.Unlock() - <-b.ready + 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 { @@ -461,10 +544,13 @@ type sameBaseLoadStore struct { 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 } - s.barrier.wait() + if barrierErr != nil { + return kernel.ControlState{}, barrierErr + } return state, nil } @@ -475,10 +561,13 @@ type sameBaseObservationDomain struct { 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 } - d.barrier.wait() + if barrierErr != nil { + return kernel.Observation{}, barrierErr + } return observation, nil } diff --git a/boatstack/kernel/conformance/conformance_test.go b/boatstack/kernel/conformance/conformance_test.go index 36d3dc5..4e923cb 100644 --- a/boatstack/kernel/conformance/conformance_test.go +++ b/boatstack/kernel/conformance/conformance_test.go @@ -231,6 +231,47 @@ func TestConcurrentApplyRejectsStoreWithoutCompareAndCommit(t *testing.T) { } } +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) @@ -348,6 +389,44 @@ func (o noOpAdvanceOperator) Execute(_ context.Context, operation kernel.Operati 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() diff --git a/boatstack/kernel/conformance/integer.go b/boatstack/kernel/conformance/integer.go index 626501c..9cf5034 100644 --- a/boatstack/kernel/conformance/integer.go +++ b/boatstack/kernel/conformance/integer.go @@ -416,6 +416,14 @@ func verifyIntegerCommitted(program kernel.Program, before, after Snapshot, rece 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 {