feat: 接入 Instance 扩展能力观测
Verify / test (pull_request) Successful in 4m16s
Verify / lint (pull_request) Failing after 4m10s

This commit is contained in:
2026-09-21 06:32:40 +00:00
parent 8ac6283573
commit 5a7b38ad26
3 changed files with 135 additions and 5 deletions
+27 -3
View File
@@ -50,9 +50,10 @@ type Snapshot struct {
// Reconstitution does not establish live capability evidence, even for a Ready snapshot.
// This initial slice deliberately exposes no operation that authorizes provisioning.
type Instance struct {
target ObservationTarget
snapshot Snapshot
deleting bool
target ObservationTarget
snapshot Snapshot
deleting bool
extensions ExtensionSupport
}
func Reconstitute(target ObservationTarget, snapshot Snapshot, deleting bool) (*Instance, error) {
@@ -83,6 +84,7 @@ func (i *Instance) BeginValidation() error {
}
i.snapshot.Phase = PhaseValidating
i.snapshot.Readiness = Unknown
i.extensions = ExtensionSupport{}
return nil
}
@@ -97,5 +99,27 @@ func (i *Instance) BeginDeletion() error {
}
i.snapshot.Phase = PhaseDeleting
i.snapshot.Readiness = Unknown
i.extensions = ExtensionSupport{}
return nil
}
// ObserveExtensions accepts facts only for this registration and configuration.
// Unobserved support clears a previous list after a failed read; the application
// reports the dependency failure separately. This does not establish readiness.
// Same-target freshness and Secret refresh are enforced by the application.
func (i *Instance) ObserveExtensions(target ObservationTarget, support ExtensionSupport) error {
if !i.target.Matches(target) {
return errors.New("extension observation target does not match instance")
}
if i.deleting {
return errors.New("cannot accept extension observations after deletion was requested")
}
i.extensions = support
return nil
}
// CheckExtensions checks support only; Accepted is not authorization to provision.
// The aggregate does not perform IO, mutate its snapshot or uninstall extensions.
func (i *Instance) CheckExtensions(requested ExtensionSet) ExtensionCheck {
return i.extensions.Check(requested)
}
@@ -0,0 +1,105 @@
/*
Copyright 2026.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package instance_test
import (
"testing"
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/instance"
)
// Acceptance: docs/database/domain-instance.md §2/§3, extension observations are
// bound to the current registration and definition and never authorize provisioning.
func TestInstanceAcceptsExtensionObservationForCurrentTarget(t *testing.T) {
value := lifecycleInstance(t, instance.Snapshot{Phase: instance.PhaseValidating}, false)
target := value.Target()
snapshot := value.Snapshot()
if err := value.ObserveExtensions(target, instance.ObserveExtensionSupport([]string{"pg_trgm"})); err != nil {
t.Fatal(err)
}
if got := value.CheckExtensions(instance.NewExtensionSet([]string{"pg_trgm"})); got.Decision != instance.ExtensionsAccepted {
t.Fatalf("CheckExtensions() = %v, want accepted", got)
}
if value.Snapshot() != snapshot {
t.Fatal("extension observation changed the persisted lifecycle snapshot")
}
}
func TestInstanceRejectsExtensionObservationForDifferentTarget(t *testing.T) {
value := lifecycleInstance(t, instance.Snapshot{Phase: instance.PhaseValidating}, false)
identity, _, definition := targetParts(t)
revision, err := instance.NewRevision(value.Target().Revision().Value() + 1)
if err != nil {
t.Fatal(err)
}
different, err := instance.NewObservationTarget(identity, revision, definition)
if err != nil {
t.Fatal(err)
}
if err := value.ObserveExtensions(different, instance.ObserveExtensionSupport([]string{"pg_trgm"})); err == nil {
t.Fatal("observation for a different target was accepted")
}
if got := value.CheckExtensions(instance.NewExtensionSet([]string{"pg_trgm"})); got.Decision != instance.ExtensionSupportUnobserved {
t.Fatalf("rejected observation changed support: %v", got)
}
}
func TestInstanceClearsExtensionObservationAcrossLifecycleBoundaries(t *testing.T) {
requested := instance.NewExtensionSet([]string{"pg_trgm"})
t.Run("validation", func(t *testing.T) {
value := lifecycleInstance(t, instance.Snapshot{Phase: instance.PhaseReady}, false)
if err := value.ObserveExtensions(value.Target(), instance.ObserveExtensionSupport([]string{"pg_trgm"})); err != nil {
t.Fatal(err)
}
if err := value.BeginValidation(); err != nil {
t.Fatal(err)
}
if got := value.CheckExtensions(requested); got.Decision != instance.ExtensionSupportUnobserved {
t.Fatalf("validation retained stale support: %v", got)
}
})
t.Run("deletion", func(t *testing.T) {
value := lifecycleInstance(t, instance.Snapshot{Phase: instance.PhaseReady}, true)
if err := value.ObserveExtensions(value.Target(), instance.ObserveExtensionSupport([]string{"pg_trgm"})); err == nil {
t.Fatal("deleting instance accepted a new observation")
}
if err := value.BeginDeletion(); err != nil {
t.Fatal(err)
}
if got := value.CheckExtensions(requested); got.Decision != instance.ExtensionSupportUnobserved {
t.Fatalf("deletion retained extension support: %v", got)
}
})
}
func TestInstanceCanExplicitlyInvalidateExtensionObservation(t *testing.T) {
value := lifecycleInstance(t, instance.Snapshot{Phase: instance.PhaseValidating}, false)
requested := instance.NewExtensionSet([]string{"pg_trgm"})
if err := value.ObserveExtensions(value.Target(), instance.ObserveExtensionSupport([]string{"pg_trgm"})); err != nil {
t.Fatal(err)
}
if err := value.ObserveExtensions(value.Target(), instance.ExtensionSupport{}); err != nil {
t.Fatal(err)
}
if got := value.CheckExtensions(requested); got.Decision != instance.ExtensionSupportUnobserved {
t.Fatalf("explicit invalidation retained stale support: %v", got)
}
}