Problem: the hexagonal-architecture plan (plans/2026-08-15-hexagonal-
architecture.md) needs its foundation — an accepted ADR, the target
directory tree, and machine-checked dependency rules — before any
service extraction starts. Also folds the four outstanding review
findings (F3.1/F5/F6/F7) into the plan: ObservationService owns the
bounded probe-concurrency contract (scheduler.go:133), Phase 9 gates
ExecutionService+PolicyService ≥ 90% with a gating-matrix test,
per-phase abort criteria, and the §3.2 internal/config note.
Change:
- docs/adr/0016-hexagonal-ports-adapters.md records context, decision,
and consequences of the ports & adapters migration.
- internal/domain → internal/core/domain (mechanical import rewrite,
20 files), new internal/core/{ports,app}, internal/adapters trees
with package docs.
- .golangci.yml: depguard rules for §3.1 (core purity, no agent-client
tech in core, nomos isolation — the nomos rules self-activate when
internal/nomos exists in Phase 8). Config migrated to golangci-lint
v2 format so it loads at all (the v1 config errored under v2, masked
by CI's advisory continue-on-error). Verified depguard fires on a
planted openai-go import in internal/core/app.
- CONTRIBUTING.md layout section now shows the core/adapters tree.
Risk: import path churn is mechanical and tests pass unchanged; the
lint config migration surfaces the pre-existing 400-issue baseline
(advisory in CI, unchanged policy) — new/moved packages lint clean.
Verification: go vet ./..., make test (race, core/domain at 100%
coverage), make generate-check, golangci-lint on internal/core/... and
internal/adapters/... — 0 issues; depguard violation probe confirmed.
121 lines
4.4 KiB
Go
121 lines
4.4 KiB
Go
package ontology
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/dtoro/oikos/internal/core/domain"
|
|
)
|
|
|
|
func fixtureTree() *TypeTree {
|
|
return &TypeTree{
|
|
Types: map[string]TypeInfo{
|
|
"entity": {IsAbstract: true},
|
|
"compute-entity": {Parent: "entity", IsAbstract: true},
|
|
"machine": {Parent: "compute-entity", IsAbstract: true},
|
|
"proxmox-host": {Parent: "machine", LifecycleID: "infrastructure"},
|
|
"lxc": {Parent: "compute-entity", LifecycleID: "infrastructure"},
|
|
"service": {Parent: "entity", LifecycleID: "infrastructure"},
|
|
"document": {Parent: "entity"}, // no lifecycle
|
|
},
|
|
RelTypes: map[string]RelTypeInfo{
|
|
"hosts": {SourceType: "machine", TargetType: "compute-entity", Cardinality: "one-to-many"},
|
|
"provides": {SourceType: "compute-entity", TargetType: "service", Cardinality: "one-to-many"},
|
|
"depends-on": {SourceType: "service", TargetType: "service", Cardinality: "many-to-many"},
|
|
"documents": {SourceType: "document", TargetType: "entity", Cardinality: "many-to-one"},
|
|
},
|
|
Lifecycles: map[string]LifecycleInfo{
|
|
"infrastructure": {
|
|
States: map[string]bool{"planned": true, "active": true, "destroyed": true},
|
|
DefaultState: "active",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestIsAWalksHierarchy(t *testing.T) {
|
|
tree := fixtureTree()
|
|
cases := []struct {
|
|
typ, target string
|
|
want bool
|
|
}{
|
|
{"proxmox-host", "machine", true},
|
|
{"proxmox-host", "compute-entity", true},
|
|
{"proxmox-host", "entity", true},
|
|
{"proxmox-host", "proxmox-host", true},
|
|
{"lxc", "machine", false},
|
|
{"service", "compute-entity", false},
|
|
{"nonexistent", "entity", false},
|
|
}
|
|
for _, c := range cases {
|
|
if got := tree.IsA(c.typ, c.target); got != c.want {
|
|
t.Errorf("IsA(%q, %q) = %v, want %v", c.typ, c.target, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateEntityRejectsAbstract(t *testing.T) {
|
|
tree := fixtureTree()
|
|
for _, abstract := range []string{"entity", "compute-entity", "machine"} {
|
|
if err := tree.ValidateEntity(abstract, ""); !errors.Is(err, domain.ErrAbstractType) {
|
|
t.Errorf("ValidateEntity(%q) = %v, want ErrAbstractType", abstract, err)
|
|
}
|
|
}
|
|
if err := tree.ValidateEntity("lxc", "active"); err != nil {
|
|
t.Errorf("ValidateEntity(lxc, active) = %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateEntityStates(t *testing.T) {
|
|
tree := fixtureTree()
|
|
if err := tree.ValidateEntity("lxc", "flying"); !errors.Is(err, domain.ErrInvalidTransition) {
|
|
t.Errorf("bad state = %v, want ErrInvalidTransition", err)
|
|
}
|
|
// state on a type without a lifecycle is rejected
|
|
if err := tree.ValidateEntity("document", "active"); !errors.Is(err, domain.ErrInvalidTransition) {
|
|
t.Errorf("state without lifecycle = %v, want ErrInvalidTransition", err)
|
|
}
|
|
// unknown type
|
|
if err := tree.ValidateEntity("ghost", ""); !errors.Is(err, domain.ErrNotFound) {
|
|
t.Errorf("unknown type = %v, want ErrNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateEdgeHonorsInheritance(t *testing.T) {
|
|
tree := fixtureTree()
|
|
// proxmox-host is-a machine; lxc is-a compute-entity → valid
|
|
if err := tree.ValidateEdge("hosts", "proxmox-host", "lxc"); err != nil {
|
|
t.Errorf("hosts(proxmox-host→lxc) = %v, want nil", err)
|
|
}
|
|
// abstract endpoint declared, concrete descendant offered → valid
|
|
if err := tree.ValidateEdge("provides", "lxc", "service"); err != nil {
|
|
t.Errorf("provides(lxc→service) = %v, want nil", err)
|
|
}
|
|
// documents targets the root abstract 'entity' → anything is valid
|
|
if err := tree.ValidateEdge("documents", "document", "proxmox-host"); err != nil {
|
|
t.Errorf("documents(document→proxmox-host) = %v, want nil", err)
|
|
}
|
|
// service is not a machine → invalid source
|
|
if err := tree.ValidateEdge("hosts", "service", "lxc"); !errors.Is(err, domain.ErrInvalidEdge) {
|
|
t.Errorf("hosts(service→lxc) = %v, want ErrInvalidEdge", err)
|
|
}
|
|
// lxc is not a service → invalid target
|
|
if err := tree.ValidateEdge("depends-on", "service", "lxc"); !errors.Is(err, domain.ErrInvalidEdge) {
|
|
t.Errorf("depends-on(service→lxc) = %v, want ErrInvalidEdge", err)
|
|
}
|
|
// unknown relationship type
|
|
if err := tree.ValidateEdge("teleports", "lxc", "service"); !errors.Is(err, domain.ErrNotFound) {
|
|
t.Errorf("unknown rel type = %v, want ErrNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestDefaultState(t *testing.T) {
|
|
tree := fixtureTree()
|
|
if got := tree.DefaultState("lxc"); got != "active" {
|
|
t.Errorf("DefaultState(lxc) = %q, want active", got)
|
|
}
|
|
if got := tree.DefaultState("document"); got != "" {
|
|
t.Errorf("DefaultState(document) = %q, want empty", got)
|
|
}
|
|
}
|