Skip to content
71 changes: 68 additions & 3 deletions acceptance/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"os"
"path/filepath"
"slices"
"strings"
"testing"

Expand Down Expand Up @@ -570,6 +571,63 @@ func TestInitCreatesHookFiles(t *testing.T) {
"expected hooks section in settings.json, got: %s", settingsContent)
}

// A repo initialized by the real binary ends up with a Stop hook that actually
// fires chunk validate. Asserting the hooks key merely exists is not enough: a
// repo can carry commit hooks and no Stop hook at all, which is how repos ended
// up looking configured while nothing validated at session end.
func TestInitWritesFiringStopHook(t *testing.T) {
workDir := gitrepo.SetupGitRepo(t, "my-org", "my-repo")
assert.NilError(t, os.WriteFile(filepath.Join(workDir, "go.mod"), []byte("module example.com/m\n"), 0o644))

env := testenv.NewTestEnv(t)
env.AnthropicKey = ""

result := binary.RunCLI(t, []string{"init"}, env, workDir)
assert.Equal(t, result.ExitCode, 0, "stdout: %s\nstderr: %s", result.Stdout, result.Stderr)

data, err := os.ReadFile(filepath.Join(workDir, ".claude", "settings.json"))
assert.NilError(t, err, "expected .claude/settings.json to exist")

var parsed map[string]interface{}
assert.NilError(t, json.Unmarshal(data, &parsed))
hooks, ok := parsed["hooks"].(map[string]interface{})
assert.Assert(t, ok, "expected a hooks object, got: %s", data)

// The commit hooks and the Stop hook are written by separate branches of the
// merge, so a repo can have one without the other. Both must be present.
commitCommands := hookCommands(t, hooks["PreToolUse"])
assert.Assert(t, len(commitCommands) > 0, "expected a PreToolUse commit hook, got: %s", data)

stopCommands := hookCommands(t, hooks["Stop"])
assert.Assert(t, slices.Contains(stopCommands, "chunk validate"),
"expected a Stop hook running chunk validate, got: %v", stopCommands)
}

// hookCommands flattens the hook entry commands across every group of one hook
// type, so a test can assert on what actually runs regardless of grouping.
func hookCommands(t *testing.T, groups interface{}) []string {
t.Helper()
list, _ := groups.([]interface{})
var commands []string
for _, g := range list {
group, ok := g.(map[string]interface{})
if !ok {
continue
}
entries, _ := group["hooks"].([]interface{})
for _, e := range entries {
entry, ok := e.(map[string]interface{})
if !ok {
continue
}
if cmd, ok := entry["command"].(string); ok {
commands = append(commands, cmd)
}
}
}
return commands
}

func TestInitHookExistingSettingsForceWritesExample(t *testing.T) {
workDir := gitrepo.SetupGitRepo(t, "my-org", "my-repo")
assert.NilError(t, os.WriteFile(filepath.Join(workDir, "go.mod"), []byte("module example.com/m\n"), 0o644))
Expand Down Expand Up @@ -627,10 +685,17 @@ func TestInitHookExistingSettingsWritesExample(t *testing.T) {
assert.Assert(t, strings.Contains(string(data), "existing"),
"expected original settings.json to be preserved without --force")

// Example should exist
examplePath := filepath.Join(claudeDir, "settings.example.json")
_, err = os.Stat(examplePath)
// Example should exist, and carry the Stop hook — it is what the user is told
// to copy, so an example without it hands them a half-configured repo.
exampleData, err := os.ReadFile(filepath.Join(claudeDir, "settings.example.json"))
assert.NilError(t, err, "expected settings.example.json to exist")

var example map[string]interface{}
assert.NilError(t, json.Unmarshal(exampleData, &example))
exampleHooks, ok := example["hooks"].(map[string]interface{})
assert.Assert(t, ok, "expected a hooks object in the example, got: %s", exampleData)
assert.Assert(t, slices.Contains(hookCommands(t, exampleHooks["Stop"]), "chunk validate"),
"expected the example to run chunk validate at session end, got: %s", exampleData)
}

// --- init never touches CircleCI ---
Expand Down
40 changes: 38 additions & 2 deletions internal/cmd/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

"github.com/CircleCI-Public/chunk-cli/internal/config"
"github.com/CircleCI-Public/chunk-cli/internal/iostream"
"github.com/CircleCI-Public/chunk-cli/internal/settings"
"github.com/CircleCI-Public/chunk-cli/internal/testing/fakes"
"github.com/CircleCI-Public/chunk-cli/internal/ui"
)
Expand All @@ -33,6 +34,30 @@ func testStreams() (iostream.Streams, *bytes.Buffer, *bytes.Buffer) {
return iostream.Streams{Out: &out, Err: &errOut}, &out, &errOut
}

// mergedHookCommands flattens the hook entry commands across every group of one
// hook type, so a test can assert on what actually runs regardless of grouping.
func mergedHookCommands(groups interface{}) []string {
list, _ := groups.([]interface{})
var commands []string
for _, g := range list {
group, ok := g.(map[string]interface{})
if !ok {
continue
}
entries, _ := group["hooks"].([]interface{})
for _, e := range entries {
entry, ok := e.(map[string]interface{})
if !ok {
continue
}
if cmd, ok := entry["command"].(string); ok {
commands = append(commands, cmd)
}
}
}
return commands
}

func TestWriteSettingsNewFile(t *testing.T) {
dir := t.TempDir()
streams, _, errOut := testStreams()
Expand Down Expand Up @@ -93,8 +118,19 @@ func TestWriteSettingsExistingMergeApplied(t *testing.T) {
assert.Assert(t, slices.Contains(allowStrs, "Read"))
assert.Assert(t, slices.Contains(allowStrs, "Bash(chunk:*)"))

// Hooks added.
assert.Assert(t, merged["hooks"] != nil)
// Hooks added — both kinds. "hooks != nil" would pass with only the commit
// hooks merged in, which is exactly the state that left repos looking
// configured while nothing validated at session end.
hooks, ok := merged["hooks"].(map[string]interface{})
assert.Assert(t, ok, "expected a hooks object, got: %s", data)

commitCommands := mergedHookCommands(hooks["PreToolUse"])
assert.Assert(t, slices.ContainsFunc(commitCommands, func(c string) bool {
return strings.Contains(c, "go test ./...")
}), "expected the commit hook to run the configured command, got: %v", commitCommands)

stopCommands := mergedHookCommands(hooks["Stop"])
assert.DeepEqual(t, stopCommands, []string{settings.StopCommand})

// No example file written.
_, statErr := os.Stat(filepath.Join(claudeDir, "settings.example.json"))
Expand Down
Loading