P1.2-P1.5: Complete Phase 1 core libraries

Four packages built in parallel:

- P1.2 runtime: Container runtime abstraction with podman implementation.
  Interface (Pull/Run/Stop/Remove/Inspect/List), ContainerSpec/ContainerInfo
  types, CLI arg building, version extraction from image tags. 2 tests.

- P1.3 servicedef: TOML service definition file parsing. Load/Write/LoadAll,
  validation (required fields, unique component names), proto conversion.
  5 tests.

- P1.4 config: CLI and agent config loading from TOML. Duration type for
  time fields, env var overrides (MCP_*/MCP_AGENT_*), required field
  validation, sensible defaults. 7 tests.

- P1.5 auth: MCIAS integration. Token validator with 30s SHA-256 cache,
  gRPC unary interceptor (admin role enforcement, audit logging),
  Login/LoadToken/SaveToken for CLI. 9 tests.

All packages pass build, vet, lint, and test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 11:36:12 -07:00
parent 6122123064
commit 15b8823810
14 changed files with 2347 additions and 4 deletions

View File

@@ -0,0 +1,289 @@
package servicedef
import (
"os"
"path/filepath"
"strings"
"testing"
)
func boolPtr(b bool) *bool { return &b }
func sampleDef() *ServiceDef {
return &ServiceDef{
Name: "metacrypt",
Node: "rift",
Active: boolPtr(true),
Components: []ComponentDef{
{
Name: "api",
Image: "mcr.svc.mcp.metacircular.net:8443/metacrypt:latest",
Network: "docker_default",
User: "0:0",
Restart: "unless-stopped",
Ports: []string{"127.0.0.1:18443:8443", "127.0.0.1:19443:9443"},
Volumes: []string{"/srv/metacrypt:/srv/metacrypt"},
},
{
Name: "web",
Image: "mcr.svc.mcp.metacircular.net:8443/metacrypt-web:latest",
Network: "docker_default",
User: "0:0",
Restart: "unless-stopped",
Ports: []string{"127.0.0.1:18080:8080"},
Volumes: []string{"/srv/metacrypt:/srv/metacrypt"},
Cmd: []string{"server", "--config", "/srv/metacrypt/metacrypt.toml"},
},
},
}
}
// compareComponents asserts that two component slices are equal field by field.
func compareComponents(t *testing.T, prefix string, got, want []ComponentDef) {
t.Helper()
if len(got) != len(want) {
t.Fatalf("%s components: got %d, want %d", prefix, len(got), len(want))
}
for i, wantC := range want {
gotC := got[i]
if gotC.Name != wantC.Name {
t.Fatalf("%s component[%d] name: got %q, want %q", prefix, i, gotC.Name, wantC.Name)
}
if gotC.Image != wantC.Image {
t.Fatalf("%s component[%d] image: got %q, want %q", prefix, i, gotC.Image, wantC.Image)
}
if gotC.Network != wantC.Network {
t.Fatalf("%s component[%d] network: got %q, want %q", prefix, i, gotC.Network, wantC.Network)
}
if gotC.User != wantC.User {
t.Fatalf("%s component[%d] user: got %q, want %q", prefix, i, gotC.User, wantC.User)
}
if gotC.Restart != wantC.Restart {
t.Fatalf("%s component[%d] restart: got %q, want %q", prefix, i, gotC.Restart, wantC.Restart)
}
compareStrSlice(t, prefix, i, "ports", gotC.Ports, wantC.Ports)
compareStrSlice(t, prefix, i, "volumes", gotC.Volumes, wantC.Volumes)
compareStrSlice(t, prefix, i, "cmd", gotC.Cmd, wantC.Cmd)
}
}
func compareStrSlice(t *testing.T, prefix string, idx int, field string, got, want []string) {
t.Helper()
if len(got) != len(want) {
t.Fatalf("%s component[%d] %s: got %d, want %d", prefix, idx, field, len(got), len(want))
}
for j := range want {
if got[j] != want[j] {
t.Fatalf("%s component[%d] %s[%d]: got %q, want %q", prefix, idx, field, j, got[j], want[j])
}
}
}
func TestLoadWrite(t *testing.T) {
def := sampleDef()
dir := t.TempDir()
path := filepath.Join(dir, "metacrypt.toml")
if err := Write(path, def); err != nil {
t.Fatalf("write: %v", err)
}
got, err := Load(path)
if err != nil {
t.Fatalf("load: %v", err)
}
if got.Name != def.Name {
t.Fatalf("name: got %q, want %q", got.Name, def.Name)
}
if got.Node != def.Node {
t.Fatalf("node: got %q, want %q", got.Node, def.Node)
}
if *got.Active != *def.Active {
t.Fatalf("active: got %v, want %v", *got.Active, *def.Active)
}
compareComponents(t, "load-write", got.Components, def.Components)
}
func TestValidation(t *testing.T) {
tests := []struct {
name string
def *ServiceDef
wantErr string
}{
{
name: "missing name",
def: &ServiceDef{
Node: "rift",
Components: []ComponentDef{{Name: "api", Image: "img:v1"}},
},
wantErr: "service name is required",
},
{
name: "missing node",
def: &ServiceDef{
Name: "svc",
Components: []ComponentDef{{Name: "api", Image: "img:v1"}},
},
wantErr: "service node is required",
},
{
name: "empty components",
def: &ServiceDef{
Name: "svc",
Node: "rift",
},
wantErr: "must have at least one component",
},
{
name: "duplicate component names",
def: &ServiceDef{
Name: "svc",
Node: "rift",
Components: []ComponentDef{
{Name: "api", Image: "img:v1"},
{Name: "api", Image: "img:v2"},
},
},
wantErr: "duplicate component name",
},
{
name: "component missing name",
def: &ServiceDef{
Name: "svc",
Node: "rift",
Components: []ComponentDef{{Image: "img:v1"}},
},
wantErr: "component name is required",
},
{
name: "component missing image",
def: &ServiceDef{
Name: "svc",
Node: "rift",
Components: []ComponentDef{{Name: "api"}},
},
wantErr: "image is required",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validate(tt.def)
if err == nil {
t.Fatal("expected validation error")
}
if got := err.Error(); !strings.Contains(got, tt.wantErr) {
t.Fatalf("error %q does not contain %q", got, tt.wantErr)
}
})
}
}
func TestLoadAll(t *testing.T) {
dir := t.TempDir()
// Write services in non-alphabetical order.
defs := []*ServiceDef{
{
Name: "mcr",
Node: "rift",
Active: boolPtr(true),
Components: []ComponentDef{{Name: "api", Image: "mcr:latest"}},
},
{
Name: "metacrypt",
Node: "rift",
Active: boolPtr(true),
Components: []ComponentDef{{Name: "api", Image: "metacrypt:latest"}},
},
{
Name: "mcias",
Node: "rift",
Active: boolPtr(false),
Components: []ComponentDef{{Name: "api", Image: "mcias:latest"}},
},
}
for _, d := range defs {
if err := Write(filepath.Join(dir, d.Name+".toml"), d); err != nil {
t.Fatalf("write %s: %v", d.Name, err)
}
}
// Write a non-TOML file that should be ignored.
if err := os.WriteFile(filepath.Join(dir, "README.md"), []byte("# ignore"), 0o600); err != nil {
t.Fatalf("write readme: %v", err)
}
got, err := LoadAll(dir)
if err != nil {
t.Fatalf("load all: %v", err)
}
if len(got) != 3 {
t.Fatalf("count: got %d, want 3", len(got))
}
wantOrder := []string{"mcias", "mcr", "metacrypt"}
for i, name := range wantOrder {
if got[i].Name != name {
t.Fatalf("order[%d]: got %q, want %q", i, got[i].Name, name)
}
}
}
func TestActiveDefault(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "svc.toml")
content := `name = "svc"
node = "rift"
[[components]]
name = "api"
image = "img:latest"
`
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
def, err := Load(path)
if err != nil {
t.Fatalf("load: %v", err)
}
if def.Active == nil {
t.Fatal("active should not be nil")
}
if !*def.Active {
t.Fatal("active should default to true")
}
}
func TestProtoConversion(t *testing.T) {
def := sampleDef()
spec := ToProto(def)
if spec.Name != def.Name {
t.Fatalf("proto name: got %q, want %q", spec.Name, def.Name)
}
if !spec.Active {
t.Fatal("proto active should be true")
}
if len(spec.Components) != len(def.Components) {
t.Fatalf("proto components: got %d, want %d", len(spec.Components), len(def.Components))
}
got := FromProto(spec, def.Node)
if got.Name != def.Name {
t.Fatalf("round-trip name: got %q, want %q", got.Name, def.Name)
}
if got.Node != def.Node {
t.Fatalf("round-trip node: got %q, want %q", got.Node, def.Node)
}
if *got.Active != *def.Active {
t.Fatalf("round-trip active: got %v, want %v", *got.Active, *def.Active)
}
compareComponents(t, "round-trip", got.Components, def.Components)
}