On g14 the node's own watch took a home registration before the tool did, and the answer said 'already so' though the file had just been placed. Compare the view before and after, and render whenever the item applies here, as the MCP server registration already does.
471 lines
19 KiB
Go
471 lines
19 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)
|
|
if pre := hooks.Hooks["PreToolUse"]; len(pre) != 1 || pre[0].Matcher != "Bash" ||
|
|
pre[0].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)
|
|
}
|
|
}
|