feat: 接入 Instance 扩展能力观测

This commit is contained in:
2026-09-20 20:41:38 +00:00
parent 6e6a89f15a
commit b5262c932f
3 changed files with 135 additions and 5 deletions
+3 -2
View File
@@ -20,8 +20,9 @@ Database 是 Ayatori 首批实际产品领域之一。第一个迁移切片只
`instance_extensions_test.go` 未进入本切片。 `instance_extensions_test.go` 未进入本切片。
代码被移动到 Ayatori 的 `internal/database/domain/instance`,测试 import 和文档链接相应更新; 代码被移动到 Ayatori 的 `internal/database/domain/instance`,测试 import 和文档链接相应更新;
领域行为未在本切片中扩展。后续 Ready/observation 行为应在 Ayatori 内先更新合同与测试,再继续 首个后续切片按已批准合同增加 Instance extension observation:观测与当前 target 绑定,进入重新
实现,不能把旧运行链路接回该模型。 验证或删除时失效,且支持判定不授权 Tenant provisioning。其余 Ready/observation 行为仍应先更新
合同与测试再实现,不能把旧运行链路接回该模型。
## 边界 ## 边界
+27 -3
View File
@@ -50,9 +50,10 @@ type Snapshot struct {
// Reconstitution does not establish live capability evidence, even for a Ready snapshot. // Reconstitution does not establish live capability evidence, even for a Ready snapshot.
// This initial slice deliberately exposes no operation that authorizes provisioning. // This initial slice deliberately exposes no operation that authorizes provisioning.
type Instance struct { type Instance struct {
target ObservationTarget target ObservationTarget
snapshot Snapshot snapshot Snapshot
deleting bool deleting bool
extensions ExtensionSupport
} }
func Reconstitute(target ObservationTarget, snapshot Snapshot, deleting bool) (*Instance, error) { 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.Phase = PhaseValidating
i.snapshot.Readiness = Unknown i.snapshot.Readiness = Unknown
i.extensions = ExtensionSupport{}
return nil return nil
} }
@@ -97,5 +99,27 @@ func (i *Instance) BeginDeletion() error {
} }
i.snapshot.Phase = PhaseDeleting i.snapshot.Phase = PhaseDeleting
i.snapshot.Readiness = Unknown i.snapshot.Readiness = Unknown
i.extensions = ExtensionSupport{}
return nil 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)
}
}