package main // The person's own items in the home, read and removed on their word (novox/hq ADR 0216 rule 6: removing the // original is the person's act — the remove tool is that act made explicit). import ( "encoding/json" "os" "path/filepath" "strings" "testing" "time" ) var at = time.Date(2026, 10, 7, 9, 30, 0, 0, time.UTC) func TestEveryKindIsShownInFull(t *testing.T) { p, _ := node(t, "laptop") dir := filepath.Join(p.Home, ".claude") _ = os.MkdirAll(filepath.Join(dir, "rules"), 0o755) _ = os.MkdirAll(filepath.Join(dir, "skills", "old", "scripts"), 0o755) writeFile(t, filepath.Join(dir, "rules", "hal-era.md"), "# old rule\n") writeFile(t, filepath.Join(dir, "skills", "old", "SKILL.md"), "---\nname: old\n---\n") writeFile(t, filepath.Join(dir, "skills", "old", "scripts", "a.sh"), "#!/bin/sh\n") writeFile(t, filepath.Join(dir, "CLAUDE.md"), "# my memory\n") rule, err := ShowHome(p, "instructions", "hal-era") if err != nil || rule["content"] != "# old rule\n" || rule["placedByTheMesh"] != false { t.Fatalf("%v %v", rule, err) } if again, err := ShowHome(p, "rule", "hal-era.md"); err != nil || again["content"] != "# old rule\n" { t.Fatalf("a rule named as a person writes it: %v %v", again, err) } skill, err := ShowHome(p, KindSkill, "old") if files, _ := skill["files"].(map[string]string); err != nil || len(files) != 2 || files["scripts/a.sh"] != "#!/bin/sh\n" { t.Fatalf("%v %v", skill, err) } memory, err := ShowHome(p, KindMemory, "") if err != nil || memory["content"] != "# my memory\n" { t.Fatalf("%v %v", memory, err) } if _, err := ShowHome(p, KindAgent, "absent"); err == nil { t.Fatal("an absent item was shown") } for _, name := range []string{"../../etc/passwd", "..", ".credentials", "a/b"} { if _, err := ShowHome(p, KindInstructions, name); err == nil { t.Errorf("%q reached outside its kind's folder", name) } } if _, err := ShowHome(p, KindHook, "x"); err == nil { t.Fatal("a kind the home does not hold was taken") } // The status tool lists the memory beside the rest. found := false for _, it := range HomeItems(p, Config{}, Servers{}) { found = found || (it.Kind == KindMemory && it.Name == memoryName && !it.Placed) } if !found { t.Fatal("the status does not list the home's memory") } } func TestARemovalNeedsItsReason(t *testing.T) { p, _ := confinedNode(t, "laptop") dir := filepath.Join(p.Home, ".claude") _ = os.MkdirAll(filepath.Join(dir, "rules"), 0o755) writeFile(t, filepath.Join(dir, "rules", "keep.md"), "x") if _, err := RemoveHome(p, KindInstructions, "keep", " ", at); err == nil { t.Fatal("removed without a reason") } if _, err := os.Stat(filepath.Join(dir, "rules", "keep.md")); err != nil { t.Fatal("the file went although the removal was refused") } } func TestARemovalKeepsACopyAndLogsWhy(t *testing.T) { p, _ := confinedNode(t, "laptop") dir := filepath.Join(p.Home, ".claude") _ = os.MkdirAll(filepath.Join(dir, "rules"), 0o755) _ = os.MkdirAll(filepath.Join(dir, "skills", "old", "scripts"), 0o755) writeFile(t, filepath.Join(dir, "rules", "hal-era.md"), "# old rule\n") writeFile(t, filepath.Join(dir, "skills", "old", "SKILL.md"), "---\nname: old\n---\n") writeFile(t, filepath.Join(dir, "skills", "old", "scripts", "a.sh"), "#!/bin/sh\n") _ = os.Chmod(filepath.Join(dir, "skills", "old", "scripts", "a.sh"), 0o755) answer, err := RemoveHome(p, KindInstructions, "hal-era", "HAL is retired", at) if err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(dir, "rules", "hal-era.md")); !os.IsNotExist(err) { t.Fatal("the rule file is still there") } if _, err := os.Stat(filepath.Join(dir, "rules")); err != nil { t.Fatal("the kind's own folder went with it") } kept, _ := answer["keptAt"].(string) want := filepath.Join(p.State, RemovedDir, "2026-10-07", "093000-instructions-hal-era", "rules", "hal-era.md") if kept != want { t.Fatalf("kept at %s, not %s", kept, want) } if !strings.HasPrefix(kept, p.State) || strings.HasPrefix(kept, dir) { t.Fatal("the copy is inside the agent directory") } if raw, _ := os.ReadFile(kept); string(raw) != "# old rule\n" { t.Fatalf("the copy holds %q", raw) } // A second removal in the same second does not overwrite the first's copy. writeFile(t, filepath.Join(dir, "rules", "hal-era.md"), "# made again\n") second, err := RemoveHome(p, KindInstructions, "hal-era", "again", at) if err != nil || second["keptAt"] == kept { t.Fatalf("%v %v", second, err) } if raw, _ := os.ReadFile(kept); string(raw) != "# old rule\n" { t.Fatal("the first copy was overwritten") } // A skill goes as its whole folder, kept with its modes. skill, err := RemoveHome(p, KindSkill, "old", "superseded by the plugin's", at) if err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(dir, "skills", "old")); !os.IsNotExist(err) { t.Fatal("the skill's folder is still there") } script := filepath.Join(skill["keptAt"].(string), "scripts", "a.sh") if info, err := os.Stat(script); err != nil || info.Mode().Perm()&0o100 == 0 { t.Fatalf("the script's copy lost its executable bit: %v", err) } // The log holds every removal, each with its reason. raw, err := os.ReadFile(p.removedLog()) if err != nil { t.Fatal(err) } lines := strings.Split(strings.TrimSpace(string(raw)), "\n") if len(lines) != 3 { t.Fatalf("%d lines logged", len(lines)) } var r Removal if err := json.Unmarshal([]byte(lines[0]), &r); err != nil || r.Why != "HAL is retired" || r.Node != "laptop" || r.Kept != kept { t.Fatalf("%+v %v", r, err) } } func TestTheMemoryIsRemovedAndKept(t *testing.T) { p, _ := confinedNode(t, "laptop") dir := filepath.Join(p.Home, ".claude") writeFile(t, filepath.Join(dir, "CLAUDE.md"), "# HAL era\n") answer, err := RemoveHome(p, KindMemory, "", "the managed file says it now", at) if err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(dir, "CLAUDE.md")); !os.IsNotExist(err) { t.Fatal("the memory is still there") } if raw, _ := os.ReadFile(answer["keptAt"].(string)); string(raw) != "# HAL era\n" { t.Fatal("the memory was not kept") } } func TestWhatTheMeshPlacedIsRefused(t *testing.T) { p, w := confinedNode(t, "laptop") state, view := memConfig{}, NewConfigView(p) 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) } full := filepath.Join(p.Home, ".claude", "agents", "placed.md") if _, err := os.Stat(full); err != nil { t.Fatal("the home item was not placed") } shown, err := ShowHome(p, KindAgent, "placed") if err != nil || shown["placedByTheMesh"] != true { t.Fatalf("%v %v", shown, err) } if _, err := RemoveHome(p, KindAgent, "placed", "tidy", at); err == nil || !strings.Contains(err.Error(), "claude_code_agent_unregister") { t.Fatalf("what the mesh placed was not refused: %v", err) } if _, err := os.Stat(full); err != nil { t.Fatal("the mesh's item was removed") } if _, err := os.Stat(filepath.Join(p.State, RemovedDir)); !os.IsNotExist(err) { t.Fatal("a refused removal left a copy") } } func TestASymbolicLinkIsLeftAlone(t *testing.T) { p, _ := confinedNode(t, "laptop") dir := filepath.Join(p.Home, ".claude") elsewhere := t.TempDir() writeFile(t, filepath.Join(elsewhere, "target.md"), "not the home's") _ = os.MkdirAll(filepath.Join(dir, "rules"), 0o755) if err := os.Symlink(filepath.Join(elsewhere, "target.md"), filepath.Join(dir, "rules", "linked.md")); err != nil { t.Skip(err) } if _, err := RemoveHome(p, KindInstructions, "linked", "x", at); err == nil { t.Fatal("a symbolic link was removed") } if err := os.Symlink(elsewhere, filepath.Join(dir, "skills")); err != nil { t.Skip(err) } _ = os.MkdirAll(filepath.Join(elsewhere, "s"), 0o755) writeFile(t, filepath.Join(elsewhere, "s", "SKILL.md"), "x") if _, err := RemoveHome(p, KindSkill, "s", "x", at); err == nil { t.Fatal("a skill was removed through a linked folder") } if _, err := os.Stat(filepath.Join(elsewhere, "s", "SKILL.md")); err != nil { t.Fatal("what the link points at was removed") } } func TestARemovalIsListedAndPutBack(t *testing.T) { p, _ := confinedNode(t, "laptop") dir := filepath.Join(p.Home, ".claude") _ = os.MkdirAll(filepath.Join(dir, "rules"), 0o755) _ = os.MkdirAll(filepath.Join(dir, "skills", "old", "scripts"), 0o755) writeFile(t, filepath.Join(dir, "rules", "hal-era.md"), "# old rule\n") _ = os.Chmod(filepath.Join(dir, "rules", "hal-era.md"), 0o644) writeFile(t, filepath.Join(dir, "skills", "old", "SKILL.md"), "---\nname: old\n---\n") writeFile(t, filepath.Join(dir, "skills", "old", "scripts", "a.sh"), "#!/bin/sh\n") _ = os.Chmod(filepath.Join(dir, "skills", "old", "scripts", "a.sh"), 0o755) rule, err := RemoveHome(p, KindInstructions, "hal-era", "HAL is retired", at) if err != nil { t.Fatal(err) } skill, err := RemoveHome(p, KindSkill, "old", "superseded", at.Add(time.Minute)) if err != nil { t.Fatal(err) } kept, err := KeptRemovals(p) if err != nil || len(kept) != 2 || kept[0].Name != "old" || kept[1].Kept != rule["keptAt"] || kept[1].Restored != "" { t.Fatalf("%+v %v", kept, err) } back, err := RestoreHome(p, rule["keptAt"].(string), at.Add(time.Hour)) if err != nil { t.Fatal(err) } full := filepath.Join(dir, "rules", "hal-era.md") if raw, _ := os.ReadFile(full); string(raw) != "# old rule\n" || back["restored"] != full { t.Fatalf("%v: %q", back, raw) } if info, _ := os.Stat(full); info.Mode().Perm() != 0o644 { t.Fatalf("restored as %v, removed as 0644", info.Mode().Perm()) } if _, err := RestoreHome(p, skill["keptAt"].(string), at.Add(time.Hour)); err != nil { t.Fatal(err) } if info, err := os.Stat(filepath.Join(dir, "skills", "old", "scripts", "a.sh")); err != nil || info.Mode().Perm()&0o100 == 0 { t.Fatalf("the skill's script came back without its executable bit: %v", err) } // Listed as put back; logged with the removals. kept, _ = KeptRemovals(p) for _, k := range kept { if k.Restored == "" { t.Errorf("%s is not listed as put back", k.Kept) } } raw, _ := os.ReadFile(p.removedLog()) lines := strings.Split(strings.TrimSpace(string(raw)), "\n") var last Removal if len(lines) != 4 || json.Unmarshal([]byte(lines[2]), &last) != nil || last.Action != "restored" || last.Path != full { t.Fatalf("%d lines, %+v", len(lines), last) } // Something at the path now: refused, and left as it is. if _, err := RestoreHome(p, rule["keptAt"].(string), at); err == nil { t.Fatal("restored over what is there now") } if raw, _ := os.ReadFile(full); string(raw) != "# old rule\n" { t.Fatal("what was there was changed") } } func TestACopyThatChangedIsNotPutBack(t *testing.T) { p, _ := confinedNode(t, "laptop") dir := filepath.Join(p.Home, ".claude") _ = os.MkdirAll(filepath.Join(dir, "rules"), 0o755) _ = os.MkdirAll(filepath.Join(dir, "skills", "s"), 0o755) writeFile(t, filepath.Join(dir, "rules", "r.md"), "original") writeFile(t, filepath.Join(dir, "skills", "s", "SKILL.md"), "x") rule, _ := RemoveHome(p, KindInstructions, "r", "why", at) skill, _ := RemoveHome(p, KindSkill, "s", "why", at) writeFile(t, rule["keptAt"].(string), "tampered") if _, err := RestoreHome(p, rule["keptAt"].(string), at); err == nil { t.Fatal("a changed copy was put back") } if _, err := os.Stat(filepath.Join(dir, "rules", "r.md")); !os.IsNotExist(err) { t.Fatal("something was written from a changed copy") } // A file added to a kept skill: not what was removed. writeFile(t, filepath.Join(skill["keptAt"].(string), "extra.md"), "x") if _, err := RestoreHome(p, skill["keptAt"].(string), at); err == nil { t.Fatal("a copy with an extra file was put back") } if _, err := os.Stat(filepath.Join(dir, "skills", "s")); !os.IsNotExist(err) { t.Fatal("the skill's folder was made from a changed copy") } // A note pointing elsewhere than its own kind and name: refused. note := filepath.Join(filepath.Dir(filepath.Dir(rule["keptAt"].(string))), "removal.json") var r Removal _ = readJSON(note, &r) writeFile(t, rule["keptAt"].(string), "original") r.Path = "/etc/passwd" raw, _ := json.Marshal(r) writeFile(t, note, string(raw)) if _, err := RestoreHome(p, rule["keptAt"].(string), at); err == nil { t.Fatal("a note naming another path was followed") } // Nothing outside the module's kept copies. for _, kept := range []string{"", "/etc/passwd", filepath.Join(p.State, RemovedDir), filepath.Join(p.State, RemovedDir, "..", "config.json")} { if _, err := RestoreHome(p, kept, at); err == nil { t.Errorf("%q was taken as a kept copy", kept) } } }