Files
sgard/garden/targeting_test.go
Kyle Isom 589f76c10e Step 28: Machine identity and targeting core.
Entry gains Only/Never fields for per-machine targeting. Machine
identity = short hostname + os:<GOOS> + arch:<GOARCH> + tag:<name>.
Tags stored in local <repo>/tags file (added to .gitignore by init).
EntryApplies() matching: only=any-match, never=no-match, both=error.
13 tests covering matching, identity, tags CRUD, gitignore.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 22:47:02 -07:00

239 lines
5.6 KiB
Go

package garden
import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/kisom/sgard/manifest"
)
func TestEntryApplies_NoTargeting(t *testing.T) {
entry := &manifest.Entry{Path: "~/.bashrc"}
ok, err := EntryApplies(entry, []string{"vade", "os:linux", "arch:amd64"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !ok {
t.Error("entry with no targeting should always apply")
}
}
func TestEntryApplies_OnlyMatch(t *testing.T) {
entry := &manifest.Entry{Path: "~/.bashrc", Only: []string{"os:linux"}}
ok, err := EntryApplies(entry, []string{"vade", "os:linux", "arch:amd64"})
if err != nil {
t.Fatal(err)
}
if !ok {
t.Error("should match os:linux")
}
}
func TestEntryApplies_OnlyNoMatch(t *testing.T) {
entry := &manifest.Entry{Path: "~/.bashrc", Only: []string{"os:darwin"}}
ok, err := EntryApplies(entry, []string{"vade", "os:linux", "arch:amd64"})
if err != nil {
t.Fatal(err)
}
if ok {
t.Error("os:darwin should not match os:linux machine")
}
}
func TestEntryApplies_OnlyHostname(t *testing.T) {
entry := &manifest.Entry{Path: "~/.bashrc", Only: []string{"vade"}}
ok, err := EntryApplies(entry, []string{"vade", "os:linux", "arch:amd64"})
if err != nil {
t.Fatal(err)
}
if !ok {
t.Error("should match hostname vade")
}
}
func TestEntryApplies_OnlyTag(t *testing.T) {
entry := &manifest.Entry{Path: "~/.bashrc", Only: []string{"tag:work"}}
ok, err := EntryApplies(entry, []string{"vade", "os:linux", "tag:work"})
if err != nil {
t.Fatal(err)
}
if !ok {
t.Error("should match tag:work")
}
ok, err = EntryApplies(entry, []string{"vade", "os:linux"})
if err != nil {
t.Fatal(err)
}
if ok {
t.Error("should not match without tag:work")
}
}
func TestEntryApplies_NeverMatch(t *testing.T) {
entry := &manifest.Entry{Path: "~/.bashrc", Never: []string{"arch:arm64"}}
ok, err := EntryApplies(entry, []string{"vade", "os:linux", "arch:arm64"})
if err != nil {
t.Fatal(err)
}
if ok {
t.Error("should be excluded by never:arch:arm64")
}
}
func TestEntryApplies_NeverNoMatch(t *testing.T) {
entry := &manifest.Entry{Path: "~/.bashrc", Never: []string{"arch:arm64"}}
ok, err := EntryApplies(entry, []string{"vade", "os:linux", "arch:amd64"})
if err != nil {
t.Fatal(err)
}
if !ok {
t.Error("arch:amd64 machine should not be excluded by never:arch:arm64")
}
}
func TestEntryApplies_BothError(t *testing.T) {
entry := &manifest.Entry{
Path: "~/.bashrc",
Only: []string{"os:linux"},
Never: []string{"arch:arm64"},
}
_, err := EntryApplies(entry, []string{"vade", "os:linux", "arch:amd64"})
if err == nil {
t.Fatal("should error when both only and never are set")
}
}
func TestEntryApplies_CaseInsensitive(t *testing.T) {
entry := &manifest.Entry{Path: "~/.bashrc", Only: []string{"OS:Linux"}}
ok, err := EntryApplies(entry, []string{"vade", "os:linux", "arch:amd64"})
if err != nil {
t.Fatal(err)
}
if !ok {
t.Error("matching should be case-insensitive")
}
}
func TestEntryApplies_OnlyMultiple(t *testing.T) {
entry := &manifest.Entry{Path: "~/.bashrc", Only: []string{"os:darwin", "os:linux"}}
ok, err := EntryApplies(entry, []string{"vade", "os:linux", "arch:amd64"})
if err != nil {
t.Fatal(err)
}
if !ok {
t.Error("should match if any label in only matches")
}
}
func TestIdentity(t *testing.T) {
root := t.TempDir()
repoDir := filepath.Join(root, "repo")
g, err := Init(repoDir)
if err != nil {
t.Fatalf("Init: %v", err)
}
labels := g.Identity()
// Should contain os and arch.
found := make(map[string]bool)
for _, l := range labels {
found[l] = true
}
osLabel := "os:" + runtime.GOOS
archLabel := "arch:" + runtime.GOARCH
if !found[osLabel] {
t.Errorf("identity should contain %s", osLabel)
}
if !found[archLabel] {
t.Errorf("identity should contain %s", archLabel)
}
// Should contain a hostname (non-empty, no dots).
hostname := labels[0]
if hostname == "" || strings.Contains(hostname, ".") || strings.Contains(hostname, ":") {
t.Errorf("first label should be short hostname, got %q", hostname)
}
}
func TestTags(t *testing.T) {
root := t.TempDir()
repoDir := filepath.Join(root, "repo")
g, err := Init(repoDir)
if err != nil {
t.Fatalf("Init: %v", err)
}
// No tags initially.
if tags := g.LoadTags(); len(tags) != 0 {
t.Fatalf("expected no tags, got %v", tags)
}
// Add tags.
if err := g.SaveTag("work"); err != nil {
t.Fatalf("SaveTag: %v", err)
}
if err := g.SaveTag("desktop"); err != nil {
t.Fatalf("SaveTag: %v", err)
}
tags := g.LoadTags()
if len(tags) != 2 {
t.Fatalf("expected 2 tags, got %v", tags)
}
// Duplicate add is idempotent.
if err := g.SaveTag("work"); err != nil {
t.Fatalf("SaveTag duplicate: %v", err)
}
if tags := g.LoadTags(); len(tags) != 2 {
t.Fatalf("expected 2 tags after duplicate add, got %v", tags)
}
// Remove.
if err := g.RemoveTag("work"); err != nil {
t.Fatalf("RemoveTag: %v", err)
}
tags = g.LoadTags()
if len(tags) != 1 || tags[0] != "desktop" {
t.Fatalf("expected [desktop], got %v", tags)
}
// Tags appear in identity.
labels := g.Identity()
found := false
for _, l := range labels {
if l == "tag:desktop" {
found = true
}
}
if !found {
t.Errorf("identity should contain tag:desktop, got %v", labels)
}
}
func TestInitCreatesGitignoreWithTags(t *testing.T) {
root := t.TempDir()
repoDir := filepath.Join(root, "repo")
if _, err := Init(repoDir); err != nil {
t.Fatalf("Init: %v", err)
}
data, err := os.ReadFile(filepath.Join(repoDir, ".gitignore"))
if err != nil {
t.Fatalf("reading .gitignore: %v", err)
}
if !strings.Contains(string(data), "tags") {
t.Error(".gitignore should contain 'tags'")
}
}