nodesRunningMe looked for printed lines in what is a JSON list, so every register tool's "also on" hint and every nodes: "all" named no machine.
472 lines
20 KiB
Go
472 lines
20 KiB
Go
package main
|
|
|
|
// The agent's configuration registered at three scopes (novox/hq ADR 0216): each test is one row of the
|
|
// record's "How it is checked".
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// memConfig is the `config` state as a map, shared by the nodes of a test the way the bus shares it.
|
|
type memConfig map[string]json.RawMessage
|
|
|
|
func (m memConfig) Put(key string, value any) error {
|
|
raw, err := json.Marshal(value)
|
|
m[key] = raw
|
|
return err
|
|
}
|
|
func (m memConfig) Delete(key string) error { delete(m, key); return nil }
|
|
func (m memConfig) Keys() ([]string, error) {
|
|
out := []string{}
|
|
for k := range m {
|
|
out = append(out, k)
|
|
}
|
|
return out, nil
|
|
}
|
|
func (m memConfig) Get(key string) (json.RawMessage, bool, error) {
|
|
raw, ok := m[key]
|
|
return raw, ok, nil
|
|
}
|
|
|
|
// deliver hands every key of the state to a node's view, as its watch would.
|
|
func deliver(m memConfig, v *ConfigView) {
|
|
for key, raw := range m {
|
|
var it Item
|
|
_ = json.Unmarshal(raw, &it)
|
|
v.Take(key, "put", &it)
|
|
}
|
|
}
|
|
|
|
func one(name, content string) map[string]string { return map[string]string{name + ".md": content} }
|
|
|
|
func pluginOf(t *testing.T, w map[string]string) map[string]PluginFile {
|
|
t.Helper()
|
|
var files map[string]PluginFile
|
|
if err := json.Unmarshal([]byte(w[MarketplaceDir+"/"]), &files); err != nil {
|
|
t.Fatalf("no marketplace written: %v", err)
|
|
}
|
|
return files
|
|
}
|
|
|
|
func TestEachKindLandsInItsOnePlace(t *testing.T) {
|
|
p, w := node(t, "laptop")
|
|
state, view := memConfig{}, NewConfigView(p)
|
|
register := func(it Item) {
|
|
t.Helper()
|
|
answer, err := Register(p, it, nil, false, state, view, writer(w))
|
|
if err != nil || answer["registered"] == false {
|
|
t.Fatalf("%s %s: %v %v", it.Kind, it.Name, answer, err)
|
|
}
|
|
}
|
|
register(Item{Kind: KindSkill, Name: "review", Scope: ScopeMesh,
|
|
Files: map[string]string{"SKILL.md": "---\nname: review\ndescription: d\n---\nbody", "scripts/run.sh": "#!/bin/sh\necho hi\n"}})
|
|
register(Item{Kind: KindAgent, Name: "reviewer", Scope: ScopeMesh, Files: one("reviewer", "---\nname: reviewer\n---\nx")})
|
|
register(Item{Kind: KindCommand, Name: "ship", Scope: ScopeMesh, Files: one("ship", "ship it")})
|
|
register(Item{Kind: KindOutputStyle, Name: "terse", Scope: ScopeMesh, Files: one("terse", "---\nname: terse\n---\nshort")})
|
|
register(Item{Kind: KindHook, Name: "guard", Scope: ScopeMesh, Event: "PreToolUse", Matcher: "Bash",
|
|
Command: "${HOOK_DIR}/guard.sh", Files: map[string]string{"guard.sh": "#!/bin/sh\nexit 0\n"}})
|
|
register(Item{Kind: KindInstructions, Name: "conventions", Scope: ScopeMesh, Files: one("conventions", "Commit in the imperative.")})
|
|
register(Item{Kind: KindSettings, Name: SettingsName, Scope: ScopeMesh,
|
|
Settings: map[string]any{"permissions": map[string]any{"deny": []any{"Bash(rm -rf:*)"}}}})
|
|
|
|
plugin := pluginOf(t, w)
|
|
root := "plugins/" + Plugin + "/"
|
|
for _, want := range []string{".claude-plugin/marketplace.json", root + ".claude-plugin/plugin.json",
|
|
root + "skills/review/SKILL.md", root + "skills/review/scripts/run.sh", root + "agents/reviewer.md",
|
|
root + "commands/ship.md", root + "output-styles/terse.md", root + "hooks/hooks.json", root + "hooks/guard/guard.sh"} {
|
|
if _, ok := plugin[want]; !ok {
|
|
t.Errorf("the plugin lacks %s", want)
|
|
}
|
|
}
|
|
if !plugin[root+"skills/review/scripts/run.sh"].Executable || !plugin[root+"hooks/guard/guard.sh"].Executable {
|
|
t.Error("a script is not executable")
|
|
}
|
|
var hooks struct {
|
|
Hooks map[string][]struct {
|
|
Matcher string `json:"matcher"`
|
|
Hooks []struct {
|
|
Command string `json:"command"`
|
|
} `json:"hooks"`
|
|
} `json:"hooks"`
|
|
}
|
|
_ = json.Unmarshal([]byte(plugin[root+"hooks/hooks.json"].Content), &hooks)
|
|
// The mesh's own guard on the shell first (novox/hq ADR 0245), then the hook registered.
|
|
if pre := hooks.Hooks["PreToolUse"]; len(pre) != 2 || pre[0].Matcher != GuardMatcher || pre[0].Hooks[0].Command != GuardCommand() ||
|
|
pre[1].Matcher != "Bash" || pre[1].Hooks[0].Command != `"${CLAUDE_PLUGIN_ROOT}/hooks/guard"/guard.sh` {
|
|
t.Errorf("the hook's command does not name its directory, quoted: %s", plugin[root+"hooks/hooks.json"].Content)
|
|
}
|
|
if !strings.Contains(w["CLAUDE.md"], "### conventions\n\nCommit in the imperative.") {
|
|
t.Errorf("the instruction section is not in the managed instruction file:\n%s", w["CLAUDE.md"])
|
|
}
|
|
var managed map[string]any
|
|
_ = json.Unmarshal([]byte(w["managed-settings.json"]), &managed)
|
|
if managed["permissions"] == nil || managed["enabledPlugins"].(map[string]any)[Plugin+"@"+Plugin] != true {
|
|
t.Errorf("managed settings: %v", managed)
|
|
}
|
|
if _, inPlugin := plugin[root+"settings.json"]; inPlugin {
|
|
t.Error("settings were put in the plugin, which drops them")
|
|
}
|
|
}
|
|
|
|
func TestAMeshItemReachesEveryNodeANodeItemOneAndAHomeItemOneHome(t *testing.T) {
|
|
a, wa := node(t, "laptop")
|
|
b, wb := node(t, "server")
|
|
state := memConfig{}
|
|
va, vb := NewConfigView(a), NewConfigView(b)
|
|
for _, r := range []struct {
|
|
it Item
|
|
nodes []string
|
|
}{
|
|
{Item{Kind: KindCommand, Name: "everywhere", Scope: ScopeMesh, Files: one("everywhere", "x")}, nil},
|
|
{Item{Kind: KindCommand, Name: "server-only", Scope: ScopeNode, Files: one("server-only", "x")}, []string{"server"}},
|
|
{Item{Kind: KindCommand, Name: "at-home", Scope: ScopeHome, Files: one("at-home", "x")}, []string{"laptop"}},
|
|
} {
|
|
if _, err := Register(a, r.it, r.nodes, false, state, va, writer(wa)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
deliver(state, vb)
|
|
if _, err := RenderNow(b, writer(wb)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
root := "plugins/" + Plugin + "/commands/"
|
|
pa, pb := pluginOf(t, wa), pluginOf(t, wb)
|
|
if _, ok := pa[root+"everywhere.md"]; !ok {
|
|
t.Error("the mesh item is missing on the laptop")
|
|
}
|
|
if _, ok := pb[root+"everywhere.md"]; !ok {
|
|
t.Error("the mesh item is missing on the server")
|
|
}
|
|
if _, ok := pa[root+"server-only.md"]; ok {
|
|
t.Error("the server's item reached the laptop")
|
|
}
|
|
if _, ok := pb[root+"server-only.md"]; !ok {
|
|
t.Error("the server's item is missing on the server")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(a.Home, ".claude", "commands", "at-home.md")); err != nil {
|
|
t.Error("the home item was not placed in the laptop's home")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(b.Home, ".claude", "commands", "at-home.md")); err == nil {
|
|
t.Error("the laptop's home item reached the server's home")
|
|
}
|
|
if _, ok := pa[root+"at-home.md"]; ok {
|
|
t.Error("a home item went into the plugin")
|
|
}
|
|
}
|
|
|
|
func TestTheMeshsOwnKeysCannotBeSetOrReplaced(t *testing.T) {
|
|
for _, key := range []string{"extraKnownMarketplaces", "enabledPlugins", "attribution", "apiKeyHelper"} {
|
|
it := Item{Kind: KindSettings, Name: SettingsName, Scope: ScopeMesh, Settings: map[string]any{key: true}}
|
|
if it.Problem() == "" {
|
|
t.Errorf("a registration could set %s", key)
|
|
}
|
|
}
|
|
out := Render(Facts{Console: "x"}, Settings{ManagedSettings: map[string]any{"enabledPlugins": map[string]any{Plugin + "@" + Plugin: false}}},
|
|
nil, "/h", nil, Config{})
|
|
var managed map[string]any
|
|
_ = json.Unmarshal([]byte(out["managed-settings.json"]), &managed)
|
|
if managed["enabledPlugins"].(map[string]any)[Plugin+"@"+Plugin] != true {
|
|
t.Error("the operator's setting turned the mesh's plugin off")
|
|
}
|
|
}
|
|
|
|
func TestSettingsLayMeshThenNodeAndJoinTheirLists(t *testing.T) {
|
|
c := Config{
|
|
Mesh: []Item{{Kind: KindSettings, Scope: ScopeMesh, Settings: map[string]any{
|
|
"permissions": map[string]any{"deny": []any{"A"}}, "model": "mesh-model"}}},
|
|
Node: []Item{{Kind: KindSettings, Scope: ScopeNode, Settings: map[string]any{
|
|
"permissions": map[string]any{"deny": []any{"A", "B"}}, "model": "node-model"}}},
|
|
}
|
|
out := Render(Facts{Console: "x"}, Settings{ManagedSettings: map[string]any{"permissions": map[string]any{"allow": []any{"C"}}}},
|
|
nil, "/h", nil, c)
|
|
var managed struct {
|
|
Permissions map[string][]string `json:"permissions"`
|
|
Model string `json:"model"`
|
|
}
|
|
_ = json.Unmarshal([]byte(out["managed-settings.json"]), &managed)
|
|
if strings.Join(managed.Permissions["deny"], ",") != "A,B" || strings.Join(managed.Permissions["allow"], ",") != "C" || managed.Model != "node-model" {
|
|
t.Fatalf("%+v", managed)
|
|
}
|
|
}
|
|
|
|
func TestTheHomeScopeOwnsOnlyWhatItPlaced(t *testing.T) {
|
|
p, w := node(t, "laptop")
|
|
state, view := memConfig{}, NewConfigView(p)
|
|
mine := filepath.Join(p.Home, ".claude", "agents", "mine.md")
|
|
_ = os.MkdirAll(filepath.Dir(mine), 0o755)
|
|
writeFile(t, mine, "the person's own")
|
|
|
|
answer, _ := Register(p, Item{Kind: KindAgent, Name: "mine", Scope: ScopeHome, Files: one("mine", "the mesh's")}, nil, false, state, view, writer(w))
|
|
if answer["registered"] != false {
|
|
t.Fatalf("a name the person uses was taken: %v", answer)
|
|
}
|
|
if raw, _ := os.ReadFile(mine); string(raw) != "the person's own" {
|
|
t.Fatal("the person's file was overwritten")
|
|
}
|
|
|
|
placed := filepath.Join(p.Home, ".claude", "agents", "placed.md")
|
|
if _, err := Register(p, Item{Kind: KindAgent, Name: "placed", Scope: ScopeHome, Files: one("placed", "v1")}, nil, false, state, view, writer(w)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if raw, _ := os.ReadFile(placed); string(raw) != "v1" {
|
|
t.Fatal("the home item was not placed")
|
|
}
|
|
if _, err := Register(p, Item{Kind: KindAgent, Name: "placed", Scope: ScopeHome}, nil, true, state, view, writer(w)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := os.Stat(placed); err == nil {
|
|
t.Fatal("unregistering did not remove what the mesh placed")
|
|
}
|
|
if _, err := os.Stat(mine); err != nil {
|
|
t.Fatal("unregistering removed the person's file")
|
|
}
|
|
|
|
// Changed by hand after it was placed: left alone when unregistered.
|
|
if _, err := Register(p, Item{Kind: KindAgent, Name: "edited", Scope: ScopeHome, Files: one("edited", "v1")}, nil, false, state, view, writer(w)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
edited := filepath.Join(p.Home, ".claude", "agents", "edited.md")
|
|
writeFile(t, edited, "changed by hand")
|
|
if _, err := Register(p, Item{Kind: KindAgent, Name: "edited", Scope: ScopeHome}, nil, true, state, view, writer(w)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if raw, _ := os.ReadFile(edited); string(raw) != "changed by hand" {
|
|
t.Fatal("a placed file changed by hand was removed")
|
|
}
|
|
}
|
|
|
|
func TestAnItemAboveTheLimitOrMisshapenIsRefused(t *testing.T) {
|
|
big := Item{Kind: KindSkill, Name: "big", Scope: ScopeMesh, Files: map[string]string{"SKILL.md": strings.Repeat("x", MaxItemBytes+1)}}
|
|
cases := map[string]Item{
|
|
"too big": big,
|
|
"a bad name": {Kind: KindAgent, Name: "Bad Name", Scope: ScopeMesh, Files: one("Bad Name", "x")},
|
|
"a skill without one": {Kind: KindSkill, Name: "s", Scope: ScopeMesh, Files: map[string]string{"other.md": "x"}},
|
|
"a path outside": {Kind: KindSkill, Name: "s", Scope: ScopeMesh, Files: map[string]string{"SKILL.md": "x", "../escape": "x"}},
|
|
"a hook at home": {Kind: KindHook, Name: "h", Scope: ScopeHome, Event: "Stop", Command: "true"},
|
|
"settings at home": {Kind: KindSettings, Name: SettingsName, Scope: ScopeHome, Settings: map[string]any{"model": "x"}},
|
|
"an unknown event": {Kind: KindHook, Name: "h", Scope: ScopeMesh, Event: "Whenever", Command: "true"},
|
|
}
|
|
for label, it := range cases {
|
|
if it.Problem() == "" {
|
|
t.Errorf("%s was accepted", label)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAHomeItemIsImportedAndTheStaleOnesAreNamed(t *testing.T) {
|
|
p, _ := node(t, "laptop")
|
|
dir := filepath.Join(p.Home, ".claude")
|
|
_ = os.MkdirAll(filepath.Join(dir, "skills", "old", "scripts"), 0o755)
|
|
writeFile(t, filepath.Join(dir, "skills", "old", "SKILL.md"), "---\nname: old\n---\nuse mcp__gone__do_it")
|
|
writeFile(t, filepath.Join(dir, "skills", "old", "scripts", "a.sh"), "#!/bin/sh\n")
|
|
it, err := ImportFromHome(p, KindSkill, "old")
|
|
if err != nil || len(it.Files) != 2 || it.Files["scripts/a.sh"] == "" {
|
|
t.Fatalf("%+v %v", it, err)
|
|
}
|
|
items := HomeItems(p, Config{Mesh: []Item{{Kind: KindSkill, Name: "old"}}}, Servers{})
|
|
if len(items) != 1 || len(items[0].Notes) != 2 {
|
|
t.Fatalf("%+v", items)
|
|
}
|
|
}
|
|
|
|
// The manifest lists exactly the tools the bundle serves: a tool missing from it is never announced.
|
|
func TestTheManifestListsEveryToolServed(t *testing.T) {
|
|
raw, err := os.ReadFile("../../module.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m struct {
|
|
Tools []string `json:"tools"`
|
|
}
|
|
_ = json.Unmarshal(raw, &m)
|
|
listed := map[string]bool{}
|
|
for _, n := range m.Tools {
|
|
listed[n] = true
|
|
}
|
|
p, _ := node(t, "laptop")
|
|
served := tools(p, nil, NewServerView(p), memConfig{}, NewConfigView(p))
|
|
for _, tool := range served {
|
|
if !listed[tool.Name] {
|
|
t.Errorf("%s is served and not in the manifest", tool.Name)
|
|
}
|
|
delete(listed, tool.Name)
|
|
}
|
|
for n := range listed {
|
|
t.Errorf("%s is in the manifest and not served", n)
|
|
}
|
|
}
|
|
|
|
// The tree writer's comparison: a directory holding exactly the files given, executable bits included.
|
|
func TestATreeIsTheSameOnlyWhenEveryFileAndModeIs(t *testing.T) {
|
|
dir := t.TempDir()
|
|
files := map[string]PluginFile{"a.md": {Content: "a"}, "s/run.sh": {Content: "#!/bin/sh\n", Executable: true}}
|
|
_ = os.MkdirAll(filepath.Join(dir, "s"), 0o755)
|
|
writeFile(t, filepath.Join(dir, "a.md"), "a")
|
|
writeFile(t, filepath.Join(dir, "s", "run.sh"), "#!/bin/sh\n")
|
|
if sameTree(dir, files) {
|
|
t.Fatal("a script without its executable bit counted as the same")
|
|
}
|
|
_ = os.Chmod(filepath.Join(dir, "s", "run.sh"), 0o755)
|
|
if !sameTree(dir, files) {
|
|
t.Fatal("the same tree counted as different")
|
|
}
|
|
writeFile(t, filepath.Join(dir, "extra.md"), "x")
|
|
if sameTree(dir, files) {
|
|
t.Fatal("an extra file counted as the same")
|
|
}
|
|
}
|
|
|
|
// A registration removed while a node was away is dropped when it is back: the watch hands over only what is
|
|
// there, so the view is pruned to the keys the state still holds.
|
|
func TestWhatWasRemovedWhileANodeWasAwayIsDropped(t *testing.T) {
|
|
p, w := node(t, "laptop")
|
|
state, view := memConfig{}, NewConfigView(p)
|
|
for _, name := range []string{"kept", "gone"} {
|
|
if _, err := Register(p, Item{Kind: KindCommand, Name: name, Scope: ScopeMesh, Files: one(name, "x")}, nil, false, state, view, writer(w)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
delete(state, "mesh.command.gone") // removed from another node while this one was off
|
|
back := NewConfigView(p) // the node starts again from what it last wrote
|
|
if len(back.Items()) != 2 {
|
|
t.Fatalf("the view did not start from what was last written: %v", back.Items())
|
|
}
|
|
keys, _ := state.Keys()
|
|
if !back.Prune(keys) || len(back.Items()) != 1 {
|
|
t.Fatalf("after pruning: %v", back.Items())
|
|
}
|
|
}
|
|
|
|
// The review's findings, each held by a test.
|
|
|
|
func TestAPathOrAKeyThatIsNotWhatItSaysIsRefused(t *testing.T) {
|
|
for label, files := range map[string]map[string]string{
|
|
"a dot": {"SKILL.md": "x", ".": "x"},
|
|
"an empty segment": {"SKILL.md": "x", "a//b": "x"},
|
|
"a parent segment": {"SKILL.md": "x", "a/../b": "x"},
|
|
"a file and directory": {"SKILL.md": "x", "a": "x", "a/b": "x"},
|
|
} {
|
|
if (Item{Kind: KindSkill, Name: "s", Scope: ScopeMesh, Files: files}).Problem() == "" {
|
|
t.Errorf("%s was accepted", label)
|
|
}
|
|
}
|
|
p, _ := node(t, "laptop")
|
|
v := NewConfigView(p)
|
|
home := Item{Kind: KindCommand, Name: "x", Scope: ScopeHome, Files: one("x", "y")}
|
|
if v.Take("mesh.command.x", "put", &home); len(v.Items()) != 0 {
|
|
t.Fatal("a mesh key holding a home item was taken")
|
|
}
|
|
for _, key := range []string{"mesh.command.x", "mesh.agent.x"} {
|
|
mesh := Item{Kind: KindCommand, Name: "x", Scope: ScopeMesh, Files: one("x", "y")}
|
|
v.Take(key, "put", &mesh)
|
|
}
|
|
if len(v.Items()) != 1 {
|
|
t.Fatalf("an item under a key of another kind was taken: %v", v.Items())
|
|
}
|
|
}
|
|
|
|
func TestAllIsEveryNodeAndANameThatCannotBeAKeyIsRefused(t *testing.T) {
|
|
nodes, err := ExpandNodes([]string{"all"}, func() ([]string, error) { return []string{"ace", "g14"}, nil })
|
|
if err != nil || strings.Join(nodes, ",") != "ace,g14" {
|
|
t.Fatalf("%v %v", nodes, err)
|
|
}
|
|
p, w := node(t, "laptop")
|
|
answer, _ := Register(p, Item{Kind: KindCommand, Name: "c", Scope: ScopeNode, Files: one("c", "x")}, []string{"a.b"}, false, memConfig{}, NewConfigView(p), writer(w))
|
|
if answer["registered"] != false {
|
|
t.Fatalf("a node name with a dot was used in a key: %v", answer)
|
|
}
|
|
if (Item{Kind: KindSettings, Name: SettingsName, Scope: ScopeMesh, Settings: map[string]any{"deniedMcpServers": []any{}}}).Problem() == "" {
|
|
t.Fatal("a registration could deny the mesh's console")
|
|
}
|
|
}
|
|
|
|
func TestTheOperatorsOwnPluginsAndMarketplacesAreKept(t *testing.T) {
|
|
out := Render(Facts{Console: "x"}, Settings{ManagedSettings: map[string]any{
|
|
"enabledPlugins": map[string]any{"theirs@market": true},
|
|
"extraKnownMarketplaces": map[string]any{"market": map[string]any{"source": map[string]any{"source": "github", "repo": "o/r"}}},
|
|
}}, nil, "/h", nil, Config{})
|
|
var managed struct {
|
|
Enabled map[string]any `json:"enabledPlugins"`
|
|
Known map[string]any `json:"extraKnownMarketplaces"`
|
|
}
|
|
_ = json.Unmarshal([]byte(out["managed-settings.json"]), &managed)
|
|
if managed.Enabled["theirs@market"] != true || managed.Enabled[Plugin+"@"+Plugin] != true || managed.Known["market"] == nil || managed.Known[Plugin] == nil {
|
|
t.Fatalf("%+v", managed)
|
|
}
|
|
}
|
|
|
|
func TestTheHomeLeavesThePersonsChoicesAlone(t *testing.T) {
|
|
p, _ := node(t, "laptop")
|
|
dir := filepath.Join(p.Home, ".claude")
|
|
// An identical file the person made is not taken over, so unregistering never removes it.
|
|
_ = os.MkdirAll(filepath.Join(dir, "agents"), 0o755)
|
|
writeFile(t, filepath.Join(dir, "agents", "same.md"), "x")
|
|
if _, left := PlaceHome(p, map[string]string{"agents/same.md": "x"}); len(left) != 1 {
|
|
t.Fatalf("an identical file of the person's was taken over: %v", left)
|
|
}
|
|
PlaceHome(p, map[string]string{})
|
|
if _, err := os.Stat(filepath.Join(dir, "agents", "same.md")); err != nil {
|
|
t.Fatal("the person's file was removed")
|
|
}
|
|
// A placed file the person deleted stays deleted while it is registered.
|
|
PlaceHome(p, map[string]string{"commands/c.md": "x"})
|
|
_ = os.Remove(filepath.Join(dir, "commands", "c.md"))
|
|
PlaceHome(p, map[string]string{"commands/c.md": "x"})
|
|
if _, err := os.Stat(filepath.Join(dir, "commands", "c.md")); err == nil {
|
|
t.Fatal("a file the person deleted was placed again")
|
|
}
|
|
// The kind's own directory stays when the mesh's last item in it goes.
|
|
PlaceHome(p, map[string]string{"skills/s/SKILL.md": "x"})
|
|
PlaceHome(p, map[string]string{})
|
|
if _, err := os.Stat(filepath.Join(dir, "skills", "s")); err == nil {
|
|
t.Fatal("the item's own directory was left")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, "skills")); err != nil {
|
|
t.Fatal("the kind's directory was removed")
|
|
}
|
|
// Never through a symbolic link.
|
|
elsewhere := t.TempDir()
|
|
if err := os.Symlink(elsewhere, filepath.Join(dir, "output-styles")); err != nil {
|
|
t.Skip("no symbolic links here")
|
|
}
|
|
PlaceHome(p, map[string]string{"output-styles/o.md": "x"})
|
|
if _, err := os.Stat(filepath.Join(elsewhere, "o.md")); err == nil {
|
|
t.Fatal("a file was written through a symbolic link")
|
|
}
|
|
}
|
|
|
|
func TestOneFileThatCannotBeWrittenDoesNotStopTheOthers(t *testing.T) {
|
|
p, _ := node(t, "laptop")
|
|
w := map[string]string{}
|
|
failing := func(name, content string) (string, error) {
|
|
if name == MarketplaceDir+"/" {
|
|
return "", os.ErrPermission
|
|
}
|
|
w[name] = content
|
|
return name + ": written", nil
|
|
}
|
|
if _, err := RenderNow(p, failing); err == nil {
|
|
t.Fatal("the failure was not reported")
|
|
}
|
|
if w["managed-settings.json"] == "" || w["managed-mcp.json"] == "" || w["CLAUDE.md"] == "" {
|
|
t.Fatalf("the other files were not written: %v", w)
|
|
}
|
|
}
|
|
|
|
// The answer says what this node wrote even when its own watch took the change first.
|
|
func TestTheAnswerSaysWhatWasWrittenWhenTheWatchWasFirst(t *testing.T) {
|
|
p, w := node(t, "laptop")
|
|
state, view := memConfig{}, NewConfigView(p)
|
|
it := Item{Kind: KindCommand, Name: "c", Scope: ScopeHome, Files: one("c", "x")}
|
|
first := it
|
|
view.Take(it.Key("laptop"), "put", &first) // the watch, first
|
|
answer, err := Register(p, it, nil, false, state, view, writer(w))
|
|
if err != nil || answer["rendered here"] == nil || answer["here"] != nil {
|
|
t.Fatalf("%v %v", answer, err)
|
|
}
|
|
}
|