claude-code: read and remove the person's own home items through the mesh
Hand-written items in an account's ~/.claude could only be removed over a shell on the machine. claude_code_home_show reads one in full (memory, ~/.claude/CLAUDE.md, included); claude_code_home_remove removes one on the person's word, with a required reason, refusing what the mesh placed and symbolic links, keeping a dated copy in the module's state and logging it.
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
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, _ := node(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, _ := 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")
|
||||
_ = 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, _ := node(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 := node(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, _ := node(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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user