Files
orca/internal/ns/resolve_test.go
T
Jon Chery 7bb31d4c09 feat(P0a2): namespace CRUD + inheritance engine (REQ-082)
P0a2 — Namespace inheritance resolver + orca ns CLI subcommands.

Resolver (REQ-082, internal/ns/resolve.go):
- Pure Resolve() function: DFS post-order chain assembly (most-specific
  first, _defaults implicit last D-185). Child-wins-scalar env merge, de-duped
  union constraints. Cycle detection with readable cycle path. Missing-parent
  + missing-_defaults + misordering (['_defaults','x']) rejection. Opt-out
  impossible (D-187). 89.6% coverage.

Parser (internal/ns/parse.go):
- ParseNSMd: hand-rolled YAML frontmatter (no yaml.v3 dep). Validates
  kind:Namespace + name, parses parents flow-array, inherits_env/secrets.
- ParseNSMdDir: walks root/*/ns.md, skips cluster/, requires _defaults.

CLI (internal/cli/ns.go, D-176):
- orca ns list/create/delete/inspect/validate. Inspect + validate use the
  resolver. Create refuses _defaults/cluster; delete refuses _defaults +
  non-empty namespaces. JSON output support. 85.2% coverage.
- Registered on rootCmd.

Tests: resolve_test.go (11 tests), parse_test.go (14 tests), ns_test.go
(21 tests). 18 packages pass, 20 bats pass, gofmt clean, verify-reqs 90
consistent.

---ci---
project: orca
phase: P0a2
milestone: v0.9
status: execute
---/ci---
2026-08-05 16:49:12 +00:00

232 lines
6.4 KiB
Go

package ns
import (
"strings"
"testing"
)
func TestResolveEmptyConfigs(t *testing.T) {
out, err := Resolve(map[string]*NSConfig{})
if err != nil {
t.Fatalf("Resolve empty: unexpected error: %v", err)
}
if len(out) != 0 {
t.Fatalf("Resolve empty: want empty map, got %d entries", len(out))
}
}
func TestResolveSingleNoParents(t *testing.T) {
cfgs := map[string]*NSConfig{
defaultsName: {Name: defaultsName, Env: map[string]string{"A": "1"}},
"x": {Name: "x", Env: map[string]string{"B": "2"}},
}
out, err := Resolve(cfgs)
if err != nil {
t.Fatalf("Resolve: %v", err)
}
r := out["x"]
if r == nil {
t.Fatal("missing resolved x")
}
if !eqSlice(r.Chain, []string{"x", defaultsName}) {
t.Errorf("chain = %v, want [x _defaults]", r.Chain)
}
if r.Env["A"] != "1" || r.Env["B"] != "2" {
t.Errorf("env = %v, want A=1 B=2", r.Env)
}
}
func TestResolveChildOverridesParentScalar(t *testing.T) {
cfgs := map[string]*NSConfig{
defaultsName: {Name: defaultsName, Env: map[string]string{"K": "parent"}},
"child": {Name: "child", Parents: []string{defaultsName}, Env: map[string]string{"K": "child"}},
}
out, err := Resolve(cfgs)
if err != nil {
t.Fatalf("Resolve: %v", err)
}
if got := out["child"].Env["K"]; got != "child" {
t.Errorf("child K = %q, want %q (child overrides parent)", got, "child")
}
}
func TestResolveArraysUnion(t *testing.T) {
cfgs := map[string]*NSConfig{
defaultsName: {Name: defaultsName, Constraints: []string{"a", "b"}},
"x": {Name: "x", Parents: []string{defaultsName}, Constraints: []string{"c", "a"}},
}
out, err := Resolve(cfgs)
if err != nil {
t.Fatalf("Resolve: %v", err)
}
c := out["x"].Constraints
// Union de-duped; most-specific (x) first.
if !eqSlice(c, []string{"c", "a", "b"}) {
t.Errorf("constraints = %v, want [c a b]", c)
}
}
func TestResolveDefaultsImplicitLast(t *testing.T) {
cfgs := map[string]*NSConfig{
defaultsName: {Name: defaultsName},
"mid": {Name: "mid", Parents: []string{defaultsName}},
"top": {Name: "top", Parents: []string{"mid"}},
}
out, err := Resolve(cfgs)
if err != nil {
t.Fatalf("Resolve: %v", err)
}
if !eqSlice(out["top"].Chain, []string{"top", "mid", defaultsName}) {
t.Errorf("top chain = %v, want [top mid _defaults]", out["top"].Chain)
}
if !eqSlice(out["mid"].Chain, []string{"mid", defaultsName}) {
t.Errorf("mid chain = %v, want [mid _defaults]", out["mid"].Chain)
}
}
func TestResolveDefaultsDedupExplicit(t *testing.T) {
cfgs := map[string]*NSConfig{
defaultsName: {Name: defaultsName, Env: map[string]string{"D": "1"}},
"x": {Name: "x", Parents: []string{defaultsName}},
}
out, err := Resolve(cfgs)
if err != nil {
t.Fatalf("Resolve: %v", err)
}
// _defaults appears exactly once.
count := 0
for _, c := range out["x"].Chain {
if c == defaultsName {
count++
}
}
if count != 1 {
t.Errorf("_defaults appears %d times in chain %v, want 1", count, out["x"].Chain)
}
}
func TestResolveMisorderingRejected(t *testing.T) {
cfgs := map[string]*NSConfig{
defaultsName: {Name: defaultsName},
"x": {Name: "x"},
"y": {Name: "y", Parents: []string{defaultsName, "x"}},
}
_, err := Resolve(cfgs)
if err == nil {
t.Fatal("expected misordering error, got nil")
}
if !strings.Contains(err.Error(), "must be the only parent") {
t.Errorf("error = %q, want misordering message", err.Error())
}
}
func TestResolveOptOutImpossible(t *testing.T) {
cfgs := map[string]*NSConfig{
defaultsName: {Name: defaultsName, Env: map[string]string{"ROOT": "1"}},
"x": {Name: "x", Parents: nil},
}
out, err := Resolve(cfgs)
if err != nil {
t.Fatalf("Resolve: %v", err)
}
r := out["x"]
last := r.Chain[len(r.Chain)-1]
if last != defaultsName {
t.Errorf("last chain element = %q, want %q (opt-out impossible)", last, defaultsName)
}
if r.Env["ROOT"] != "1" {
t.Errorf("env should inherit from _defaults: ROOT=%q", r.Env["ROOT"])
}
}
func TestResolveCycleDetection(t *testing.T) {
cfgs := map[string]*NSConfig{
defaultsName: {Name: defaultsName},
"a": {Name: "a", Parents: []string{"b"}},
"b": {Name: "b", Parents: []string{"a"}},
}
_, err := Resolve(cfgs)
if err == nil {
t.Fatal("expected cycle error, got nil")
}
if !strings.Contains(err.Error(), "cycle") {
t.Errorf("error = %q, want cycle message", err.Error())
}
}
func TestResolveMissingParent(t *testing.T) {
cfgs := map[string]*NSConfig{
defaultsName: {Name: defaultsName},
"a": {Name: "a", Parents: []string{"ghost"}},
}
_, err := Resolve(cfgs)
if err == nil {
t.Fatal("expected missing-parent error, got nil")
}
if !strings.Contains(err.Error(), "ghost") || !strings.Contains(err.Error(), "not found") {
t.Errorf("error = %q, want contains 'ghost' and 'not found'", err.Error())
}
}
func TestResolveMissingDefaults(t *testing.T) {
cfgs := map[string]*NSConfig{
"x": {Name: "x"},
}
_, err := Resolve(cfgs)
if err == nil {
t.Fatal("expected missing _defaults error, got nil")
}
if !strings.Contains(err.Error(), defaultsName) {
t.Errorf("error = %q, want contains %q", err.Error(), defaultsName)
}
}
func TestResolveChainOrderWithDiamond(t *testing.T) {
// Diamond: top -> {left, right} -> base; base -> _defaults.
cfgs := map[string]*NSConfig{
defaultsName: {Name: defaultsName, Env: map[string]string{"R": "r"}},
"base": {Name: "base", Parents: []string{defaultsName}, Env: map[string]string{"B": "b"}},
"left": {Name: "left", Parents: []string{"base"}, Env: map[string]string{"L": "l"}},
"right": {Name: "right", Parents: []string{"base"}, Env: map[string]string{"L": "r"}},
"top": {Name: "top", Parents: []string{"left", "right"}, Env: map[string]string{"T": "t"}},
}
out, err := Resolve(cfgs)
if err != nil {
t.Fatalf("Resolve: %v", err)
}
r := out["top"]
if r == nil {
t.Fatal("missing top")
}
// top first, _defaults last.
if r.Chain[0] != "top" || r.Chain[len(r.Chain)-1] != defaultsName {
t.Errorf("chain = %v, want top first and _defaults last", r.Chain)
}
// base appears exactly once (diamond de-duped).
count := 0
for _, c := range r.Chain {
if c == "base" {
count++
}
}
if count != 1 {
t.Errorf("base appears %d times in %v, want 1", count, r.Chain)
}
// top inherits R from _defaults.
if r.Env["R"] != "r" {
t.Errorf("top should inherit R=r, got %q", r.Env["R"])
}
}
func eqSlice(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}