package cmd_test // `kettle mirror` is the one command in the tree that has nothing to do with // issues, and the one that must never fail a caller. Both are tested here. import ( "encoding/json" "os" "path/filepath" "strings" "testing" ) func writeDoc(t *testing.T, dir, name, body string) { t.Helper() if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil { t.Fatal(err) } } func linkTarget(t *testing.T, path string) string { t.Helper() target, err := os.Readlink(path) if err != nil { t.Fatalf("%s is not a symlink: %v", path, err) } return target } // The tree it walks is the working directory, not a project: this must work in // a directory that has never seen `kettle init`, because the convention it // maintains has nothing to do with issues. func TestMirrorNeedsNoProject(t *testing.T) { dir := t.TempDir() writeDoc(t, dir, "AGENTS.md", "root\n") writeDoc(t, filepath.Join(dir, "cli"), "AGENTS.md", "cli\n") r := mustRun(t, dir, "mirror") if strings.Contains(r.out(), ".kettle") { t.Errorf("mirror asked for a project:\n%s", r.out()) } for _, sub := range []string{".", "cli"} { if got := linkTarget(t, filepath.Join(dir, sub, "CLAUDE.md")); got != "AGENTS.md" { t.Errorf("%s/CLAUDE.md points at %q, want a relative AGENTS.md", sub, got) } } // Idempotent, and it says so rather than printing nothing at all. again := mustRun(t, dir, "mirror") if !strings.Contains(again.stdout, "nothing to do") { t.Errorf("a canonical tree did not report itself clean:\n%s", again.out()) } } func TestMirrorTakesADirectoryArgument(t *testing.T) { dir := t.TempDir() elsewhere := filepath.Join(dir, "elsewhere") writeDoc(t, elsewhere, "AGENTS.md", "there\n") mustRun(t, dir, "mirror", elsewhere) if _, err := os.Lstat(filepath.Join(elsewhere, "CLAUDE.md")); err != nil { t.Errorf("the named directory was not repaired: %v", err) } if r := run(t, dir, "mirror", filepath.Join(dir, "nowhere")); r.code == 0 { t.Errorf("a directory that is not there must be refused:\n%s", r.out()) } } func TestMirrorCheckWritesNothingAndFailsLoudly(t *testing.T) { dir := t.TempDir() writeDoc(t, dir, "AGENTS.md", "root\n") r := run(t, dir, "mirror", "--check") if r.code != 1 { t.Fatalf("--check exited %d on a tree with work to do, want 1:\n%s", r.code, r.out()) } if !strings.Contains(r.stdout, "would: ") { t.Errorf("--check did not say what it would do:\n%s", r.out()) } if _, err := os.Lstat(filepath.Join(dir, "CLAUDE.md")); err == nil { t.Error("--check wrote to the tree it was asked about") } mustRun(t, dir, "mirror") if r := run(t, dir, "mirror", "--check"); r.code != 0 { t.Errorf("--check exited %d on a canonical tree:\n%s", r.code, r.out()) } } // The refusal, end to end. One of these two files is somebody's writing and no // rule in this binary knows which, so both survive and the operator is told. func TestMirrorRefusesTwoDifferentRealFiles(t *testing.T) { dir := t.TempDir() writeDoc(t, dir, "AGENTS.md", "mine\n") writeDoc(t, dir, "CLAUDE.md", "theirs\n") r := mustRun(t, dir, "mirror") if !strings.Contains(r.stdout, "conflict:") || !strings.Contains(r.stdout, "merge manually") { t.Errorf("the conflict was not reported:\n%s", r.out()) } if got := readFile(t, filepath.Join(dir, "AGENTS.md")); got != "mine\n" { t.Errorf("AGENTS.md was changed: %q", got) } if got := readFile(t, filepath.Join(dir, "CLAUDE.md")); got != "theirs\n" { t.Errorf("CLAUDE.md was changed: %q", got) } // A repair run reports the conflict and still exits 0 — the six branches it // could fix, it fixed. --check is the one that turns it into a failure. if r := run(t, dir, "mirror", "--check"); r.code != 1 { t.Errorf("--check exited %d on a conflict, want 1", r.code) } } // The hook form's whole contract: it cannot fail a Bash call. Every one of these // is a payload or a state that could plausibly arrive, and every one exits 0. func TestMirrorHookAlwaysExitsZero(t *testing.T) { cases := []struct { name string stdin string setup func(t *testing.T, dir string) }{ {name: "no payload at all", stdin: ""}, {name: "a payload that is not JSON", stdin: "not json at all"}, {name: "an empty object", stdin: "{}"}, {name: "a payload with keys it does not know", stdin: `{"cwd":".","tool_name":"Bash","future":{"x":1}}`}, { name: "a tree it cannot resolve", stdin: `{"cwd":"/nowhere/at/all"}`, setup: func(t *testing.T, dir string) { writeDoc(t, dir, "AGENTS.md", "root\n") }, }, { name: "a conflict it must not resolve", stdin: `{}`, setup: func(t *testing.T, dir string) { writeDoc(t, dir, "AGENTS.md", "mine\n") writeDoc(t, dir, "CLAUDE.md", "theirs\n") }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { dir := t.TempDir() if tc.setup != nil { tc.setup(t, dir) } r := runWith(t, dir, nil, tc.stdin, "mirror", "--hook") if r.code != 0 { t.Errorf("exited %d — a documentation helper may not break a Bash call:\n%s", r.code, r.out()) } }) } } // What it says when it did something, and what it says when it did not. func TestMirrorHookReportsOnlyWhenThereIsSomethingToSay(t *testing.T) { dir := t.TempDir() writeDoc(t, dir, "AGENTS.md", "root\n") r := runWith(t, dir, nil, "{}", "mirror", "--hook") var out struct { HookSpecificOutput struct { HookEventName string `json:"hookEventName"` AdditionalContext string `json:"additionalContext"` } `json:"hookSpecificOutput"` } if err := json.Unmarshal([]byte(r.stdout), &out); err != nil { t.Fatalf("the hook did not emit JSON: %v\n%s", err, r.out()) } if out.HookSpecificOutput.HookEventName != "PreToolUse" { t.Errorf("hookEventName is %q", out.HookSpecificOutput.HookEventName) } if !strings.Contains(out.HookSpecificOutput.AdditionalContext, "CLAUDE.md") { t.Errorf("the report says nothing about what it did:\n%s", out.HookSpecificOutput.AdditionalContext) } // Silence means the tree was already canonical. A hook that spoke on every // Bash call would be noise in every transcript. quiet := runWith(t, dir, nil, "{}", "mirror", "--hook") if strings.TrimSpace(quiet.stdout) != "" { t.Errorf("a canonical tree still produced output:\n%s", quiet.stdout) } } // CLAUDE_PROJECT_DIR is the harness's own answer to "which tree", and it wins // over the payload's cwd — a Bash call made from a subdirectory must still // repair the whole project rather than the corner it was made in. func TestMirrorHookPrefersTheProjectDirectory(t *testing.T) { dir := t.TempDir() sub := filepath.Join(dir, "deep", "inside") writeDoc(t, dir, "AGENTS.md", "root\n") writeDoc(t, sub, "AGENTS.md", "inside\n") r := runWith(t, sub, []string{"CLAUDE_PROJECT_DIR=" + dir}, `{"cwd":"`+sub+`"}`, "mirror", "--hook") if r.code != 0 { t.Fatalf("exited %d:\n%s", r.code, r.out()) } if _, err := os.Lstat(filepath.Join(dir, "CLAUDE.md")); err != nil { t.Errorf("the project root was not repaired: %v", err) } }