claude-code: undo a home removal through the mesh
A removal kept its copy but nothing could put it back without a shell on the machine. claude_code_home_restore puts a kept copy back when nothing is at its path and the copy matches the digests recorded at removal; claude_code_home_removed lists what is kept.
This commit is contained in:
@@ -208,3 +208,112 @@ func TestASymbolicLinkIsLeftAlone(t *testing.T) {
|
||||
t.Fatal("what the link points at was removed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestARemovalIsListedAndPutBack(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")
|
||||
_ = 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, _ := node(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user