18 tests verify the full workspace contract is implemented: - repos.yaml: loads, required fields, valid types, deps exist, topological order, foundations, defaults, meta clone rules, domains - workspace.yaml: loads, active package in registry - .core/ folder: exists, has spec doc - Scripts: setup.sh exists+executable, install scripts exist - Plugins: marketplace.json exists, all plugins have manifests Enables archival of core/php-devops and core/go-agent. Co-Authored-By: Virgil <virgil@lethean.io> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
270 lines
8.6 KiB
Go
270 lines
8.6 KiB
Go
// Package workspace verifies the workspace contract defined by the original
|
|
// php-devops wishlist is fully implemented. This test loads the real repos.yaml
|
|
// shipped with core/agent and validates every aspect of the specification.
|
|
package workspace
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"forge.lthn.ai/core/go-io"
|
|
"forge.lthn.ai/core/go-scm/repos"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// repoRoot returns the absolute path to the core/agent repo root.
|
|
func repoRoot(t *testing.T) string {
|
|
t.Helper()
|
|
// Walk up from this test file to find repos.yaml.
|
|
dir, err := os.Getwd()
|
|
require.NoError(t, err)
|
|
for {
|
|
if _, err := os.Stat(filepath.Join(dir, "repos.yaml")); err == nil {
|
|
return dir
|
|
}
|
|
parent := filepath.Dir(dir)
|
|
require.NotEqual(t, parent, dir, "repos.yaml not found")
|
|
dir = parent
|
|
}
|
|
}
|
|
|
|
// ── repos.yaml contract ────────────────────────────────────────────
|
|
|
|
func TestContract_ReposYAML_Loads(t *testing.T) {
|
|
root := repoRoot(t)
|
|
path := filepath.Join(root, "repos.yaml")
|
|
|
|
reg, err := repos.LoadRegistry(io.Local, path)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, reg)
|
|
|
|
assert.Equal(t, 1, reg.Version, "repos.yaml must declare version: 1")
|
|
assert.NotEmpty(t, reg.Org, "repos.yaml must declare an org")
|
|
assert.NotEmpty(t, reg.BasePath, "repos.yaml must declare base_path")
|
|
}
|
|
|
|
func TestContract_ReposYAML_HasRequiredFields(t *testing.T) {
|
|
root := repoRoot(t)
|
|
reg, err := repos.LoadRegistry(io.Local, filepath.Join(root, "repos.yaml"))
|
|
require.NoError(t, err)
|
|
|
|
require.NotEmpty(t, reg.Repos, "repos.yaml must define at least one repo")
|
|
|
|
for name, repo := range reg.Repos {
|
|
assert.NotEmpty(t, repo.Type, "%s: must have a type", name)
|
|
assert.NotEmpty(t, repo.Description, "%s: must have a description", name)
|
|
}
|
|
}
|
|
|
|
func TestContract_ReposYAML_ValidTypes(t *testing.T) {
|
|
root := repoRoot(t)
|
|
reg, err := repos.LoadRegistry(io.Local, filepath.Join(root, "repos.yaml"))
|
|
require.NoError(t, err)
|
|
|
|
validTypes := map[string]bool{
|
|
"foundation": true,
|
|
"module": true,
|
|
"product": true,
|
|
"template": true,
|
|
"meta": true,
|
|
}
|
|
|
|
for name, repo := range reg.Repos {
|
|
assert.True(t, validTypes[repo.Type], "%s: invalid type %q", name, repo.Type)
|
|
}
|
|
}
|
|
|
|
func TestContract_ReposYAML_DependenciesExist(t *testing.T) {
|
|
root := repoRoot(t)
|
|
reg, err := repos.LoadRegistry(io.Local, filepath.Join(root, "repos.yaml"))
|
|
require.NoError(t, err)
|
|
|
|
for name, repo := range reg.Repos {
|
|
for _, dep := range repo.DependsOn {
|
|
_, ok := reg.Get(dep)
|
|
assert.True(t, ok, "%s: depends on %q which is not in repos.yaml", name, dep)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestContract_ReposYAML_TopologicalOrder(t *testing.T) {
|
|
root := repoRoot(t)
|
|
reg, err := repos.LoadRegistry(io.Local, filepath.Join(root, "repos.yaml"))
|
|
require.NoError(t, err)
|
|
|
|
order, err := reg.TopologicalOrder()
|
|
require.NoError(t, err, "dependency graph must be acyclic")
|
|
assert.Equal(t, len(reg.Repos), len(order), "topological order must include all repos")
|
|
|
|
// Verify ordering: every dependency appears before its dependant.
|
|
seen := map[string]bool{}
|
|
for _, repo := range order {
|
|
for _, dep := range repo.DependsOn {
|
|
assert.True(t, seen[dep], "%s appears before its dependency %s", repo.Name, dep)
|
|
}
|
|
seen[repo.Name] = true
|
|
}
|
|
}
|
|
|
|
func TestContract_ReposYAML_HasFoundation(t *testing.T) {
|
|
root := repoRoot(t)
|
|
reg, err := repos.LoadRegistry(io.Local, filepath.Join(root, "repos.yaml"))
|
|
require.NoError(t, err)
|
|
|
|
foundations := reg.ByType("foundation")
|
|
assert.NotEmpty(t, foundations, "repos.yaml must have at least one foundation package")
|
|
}
|
|
|
|
func TestContract_ReposYAML_Defaults(t *testing.T) {
|
|
root := repoRoot(t)
|
|
reg, err := repos.LoadRegistry(io.Local, filepath.Join(root, "repos.yaml"))
|
|
require.NoError(t, err)
|
|
|
|
assert.NotEmpty(t, reg.Defaults.Branch, "defaults must specify a branch")
|
|
assert.NotEmpty(t, reg.Defaults.License, "defaults must specify a licence")
|
|
}
|
|
|
|
func TestContract_ReposYAML_MetaDoesNotCloneSelf(t *testing.T) {
|
|
root := repoRoot(t)
|
|
reg, err := repos.LoadRegistry(io.Local, filepath.Join(root, "repos.yaml"))
|
|
require.NoError(t, err)
|
|
|
|
for name, repo := range reg.Repos {
|
|
if repo.Type == "meta" && repo.Clone != nil && !*repo.Clone {
|
|
// Meta repos with clone: false are correct.
|
|
continue
|
|
}
|
|
if repo.Type == "meta" {
|
|
t.Logf("%s: meta repo should set clone: false", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestContract_ReposYAML_ProductsHaveDomain(t *testing.T) {
|
|
root := repoRoot(t)
|
|
reg, err := repos.LoadRegistry(io.Local, filepath.Join(root, "repos.yaml"))
|
|
require.NoError(t, err)
|
|
|
|
for name, repo := range reg.Repos {
|
|
if repo.Type == "product" && repo.Domain != "" {
|
|
// Products with domains are properly configured.
|
|
assert.Contains(t, repo.Domain, ".", "%s: domain should be a valid hostname", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── workspace.yaml contract ────────────────────────────────────────
|
|
|
|
type workspaceConfig struct {
|
|
Version int `yaml:"version"`
|
|
Active string `yaml:"active"`
|
|
DefaultOnly []string `yaml:"default_only"`
|
|
PackagesDir string `yaml:"packages_dir"`
|
|
Settings map[string]any `yaml:"settings"`
|
|
}
|
|
|
|
func TestContract_WorkspaceYAML_Loads(t *testing.T) {
|
|
root := repoRoot(t)
|
|
path := filepath.Join(root, ".core", "workspace.yaml")
|
|
|
|
data, err := os.ReadFile(path)
|
|
require.NoError(t, err, ".core/workspace.yaml must exist")
|
|
|
|
var ws workspaceConfig
|
|
require.NoError(t, yaml.Unmarshal(data, &ws))
|
|
|
|
assert.Equal(t, 1, ws.Version, "workspace.yaml must declare version: 1")
|
|
assert.NotEmpty(t, ws.Active, "workspace.yaml must declare an active package")
|
|
assert.NotEmpty(t, ws.PackagesDir, "workspace.yaml must declare packages_dir")
|
|
}
|
|
|
|
func TestContract_WorkspaceYAML_ActiveInRegistry(t *testing.T) {
|
|
root := repoRoot(t)
|
|
|
|
// Load workspace config.
|
|
data, err := os.ReadFile(filepath.Join(root, ".core", "workspace.yaml"))
|
|
require.NoError(t, err)
|
|
var ws workspaceConfig
|
|
require.NoError(t, yaml.Unmarshal(data, &ws))
|
|
|
|
// Load repos registry.
|
|
reg, err := repos.LoadRegistry(io.Local, filepath.Join(root, "repos.yaml"))
|
|
require.NoError(t, err)
|
|
|
|
_, ok := reg.Get(ws.Active)
|
|
assert.True(t, ok, "workspace.yaml active package %q must exist in repos.yaml", ws.Active)
|
|
}
|
|
|
|
// ── .core/ folder spec contract ────────────────────────────────────
|
|
|
|
func TestContract_CoreFolder_Exists(t *testing.T) {
|
|
root := repoRoot(t)
|
|
info, err := os.Stat(filepath.Join(root, ".core"))
|
|
require.NoError(t, err, ".core/ directory must exist")
|
|
assert.True(t, info.IsDir())
|
|
}
|
|
|
|
func TestContract_CoreFolder_HasSpec(t *testing.T) {
|
|
root := repoRoot(t)
|
|
_, err := os.Stat(filepath.Join(root, ".core", "docs", "core-folder-spec.md"))
|
|
assert.NoError(t, err, ".core/docs/core-folder-spec.md must exist")
|
|
}
|
|
|
|
// ── Setup scripts contract ─────────────────────────────────────────
|
|
|
|
func TestContract_SetupScript_Exists(t *testing.T) {
|
|
root := repoRoot(t)
|
|
_, err := os.Stat(filepath.Join(root, "setup.sh"))
|
|
assert.NoError(t, err, "setup.sh must exist at repo root")
|
|
}
|
|
|
|
func TestContract_SetupScript_Executable(t *testing.T) {
|
|
root := repoRoot(t)
|
|
info, err := os.Stat(filepath.Join(root, "setup.sh"))
|
|
if err != nil {
|
|
t.Skip("setup.sh not found")
|
|
}
|
|
assert.NotZero(t, info.Mode()&0111, "setup.sh must be executable")
|
|
}
|
|
|
|
func TestContract_InstallScripts_Exist(t *testing.T) {
|
|
root := repoRoot(t)
|
|
scripts := []string{
|
|
"scripts/install-deps.sh",
|
|
"scripts/install-core.sh",
|
|
}
|
|
for _, s := range scripts {
|
|
_, err := os.Stat(filepath.Join(root, s))
|
|
assert.NoError(t, err, "%s must exist", s)
|
|
}
|
|
}
|
|
|
|
// ── Claude plugins contract ────────────────────────────────────────
|
|
|
|
func TestContract_Marketplace_Exists(t *testing.T) {
|
|
root := repoRoot(t)
|
|
_, err := os.Stat(filepath.Join(root, ".claude-plugin", "marketplace.json"))
|
|
assert.NoError(t, err, ".claude-plugin/marketplace.json must exist for plugin distribution")
|
|
}
|
|
|
|
func TestContract_Plugins_HaveManifests(t *testing.T) {
|
|
root := repoRoot(t)
|
|
pluginDir := filepath.Join(root, "claude")
|
|
|
|
entries, err := os.ReadDir(pluginDir)
|
|
if err != nil {
|
|
t.Skip("claude/ directory not found")
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() {
|
|
continue
|
|
}
|
|
manifest := filepath.Join(pluginDir, entry.Name(), ".claude-plugin", "plugin.json")
|
|
_, err := os.Stat(manifest)
|
|
assert.NoError(t, err, "claude/%s must have .claude-plugin/plugin.json", entry.Name())
|
|
}
|
|
}
|