Go 419 lines
package config
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestInit_CreatesWorkspaceTree(t *testing.T) {
root := newRepo(t)
write(t, root, "go.mod", "module x\n")
cfg, err := Load(root, "")
if err != nil {
t.Fatal(err)
}
rep, err := Init(cfg)
if err != nil {
t.Fatal(err)
}
for _, sub := range workspaceSubdirs {
info, err := os.Stat(filepath.Join(root, "tester", DefaultWorkspace, sub))
if err != nil {
t.Errorf("subdir %s missing: %v", sub, err)
continue
}
if !info.IsDir() {
t.Errorf("subdir %s is not a directory", sub)
}
}
if !rep.WroteReadme {
t.Error("expected WroteReadme=true on first init")
}
if !rep.GitignoreChanged {
t.Error("expected GitignoreChanged=true on first init")
}
if rep.AlreadyInit {
t.Error("expected AlreadyInit=false on first init")
}
if len(rep.CreatedDirs) != len(workspaceSubdirs) {
t.Errorf("CreatedDirs = %v, want %d entries", rep.CreatedDirs, len(workspaceSubdirs))
}
}
func TestInit_Idempotent(t *testing.T) {
root := newRepo(t)
write(t, root, "go.mod", "module x\n")
cfg, err := Load(root, "")
if err != nil {
t.Fatal(err)
}
if _, err := Init(cfg); err != nil {
t.Fatal(err)
}
rep, err := Init(cfg)
if err != nil {
t.Fatal(err)
}
if !rep.AlreadyInit {
t.Error("expected AlreadyInit=true on re-init")
}
if rep.WroteReadme {
t.Error("expected WroteReadme=false on re-init")
}
if rep.GitignoreChanged {
t.Error("expected GitignoreChanged=false on re-init")
}
if len(rep.CreatedDirs) != 0 {
t.Errorf("CreatedDirs on re-init = %v, want empty", rep.CreatedDirs)
}
}
func TestInit_AppendsToExistingGitignore(t *testing.T) {
root := newRepo(t)
write(t, root, ".gitignore", "node_modules/\n*.log\n")
cfg, err := Load(root, "")
if err != nil {
t.Fatal(err)
}
if _, err := Init(cfg); err != nil {
t.Fatal(err)
}
b, err := os.ReadFile(filepath.Join(root, ".gitignore"))
if err != nil {
t.Fatal(err)
}
s := string(b)
// Init now ignores the per-user dir (cfg.Username = "tester"), not the
// workspace leaf, so the appended line is /tester/.
wantLines := []string{"node_modules/", "*.log", "/tester/"}
for _, l := range wantLines {
if !strings.Contains(s, l+"\n") {
t.Errorf(".gitignore missing line %q. got:\n%s", l, s)
}
}
}
func TestInit_GitignoreNoTrailingNewline(t *testing.T) {
root := newRepo(t)
write(t, root, ".gitignore", "node_modules/")
cfg, err := Load(root, "")
if err != nil {
t.Fatal(err)
}
if _, err := Init(cfg); err != nil {
t.Fatal(err)
}
b, err := os.ReadFile(filepath.Join(root, ".gitignore"))
if err != nil {
t.Fatal(err)
}
want := "node_modules/\n/tester/\n"
if string(b) != want {
t.Errorf(".gitignore =\n%q\nwant\n%q", string(b), want)
}
}
func TestInit_CreatesGitignoreWhenMissing(t *testing.T) {
root := newRepo(t)
cfg, err := Load(root, "")
if err != nil {
t.Fatal(err)
}
if _, err := Init(cfg); err != nil {
t.Fatal(err)
}
b, err := os.ReadFile(filepath.Join(root, ".gitignore"))
if err != nil {
t.Fatal(err)
}
if string(b) != "/tester/\n" {
t.Errorf("created .gitignore =\n%q\nwant exactly /tester/\\n", string(b))
}
}
func TestInit_RecognisesExistingIgnoreVariants(t *testing.T) {
// Init ignores the per-user dir (cfg.Username = "tester"), so an
// existing equivalent line is one of the tester variants.
cases := []string{
"tester",
"tester/",
"/tester",
"/tester/",
}
for _, variant := range cases {
t.Run(variant, func(t *testing.T) {
root := newRepo(t)
write(t, root, ".gitignore", "# preamble\n"+variant+"\n")
cfg, err := Load(root, "")
if err != nil {
t.Fatal(err)
}
rep, err := Init(cfg)
if err != nil {
t.Fatal(err)
}
if rep.GitignoreChanged {
t.Errorf("variant %q caused gitignore append", variant)
}
// gitignore content must be unchanged
b, _ := os.ReadFile(filepath.Join(root, ".gitignore"))
if string(b) != "# preamble\n"+variant+"\n" {
t.Errorf("gitignore mutated:\n%s", string(b))
}
})
}
}
func TestInit_HonoursCustomWorkspaceName(t *testing.T) {
root := newRepo(t)
cfg, err := Load(root, ".workshop")
if err != nil {
t.Fatal(err)
}
if _, err := Init(cfg); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(root, "tester", ".workshop", "memory")); err != nil {
t.Errorf("custom workspace not created: %v", err)
}
b, _ := os.ReadFile(filepath.Join(root, ".gitignore"))
// Init ignores the per-user dir, not the workspace leaf, so even a
// custom workspace name yields /tester/.
if !strings.Contains(string(b), "/tester/\n") {
t.Errorf("gitignore missing /tester/: %s", string(b))
}
}
func TestInit_ErrorsWhenWorkspacePathIsAFile(t *testing.T) {
root := newRepo(t)
write(t, root, filepath.Join("tester", DefaultWorkspace), "i am a file, not a dir")
cfg, err := Load(root, "")
if err != nil {
t.Fatal(err)
}
if _, err := Init(cfg); err == nil {
t.Fatal("expected Init to error when workspace path is a file")
}
}
func TestIsInitialized(t *testing.T) {
root := newRepo(t)
cfg, err := Load(root, "")
if err != nil {
t.Fatal(err)
}
if IsInitialized(cfg) {
t.Error("expected IsInitialized=false before Init")
}
if _, err := Init(cfg); err != nil {
t.Fatal(err)
}
if !IsInitialized(cfg) {
t.Error("expected IsInitialized=true after Init")
}
}
func TestInit_PreservesExistingReadme(t *testing.T) {
root := newRepo(t)
wsDir := filepath.Join(root, "tester", DefaultWorkspace)
if err := os.MkdirAll(wsDir, 0o755); err != nil {
t.Fatal(err)
}
custom := "user wrote this README themselves"
write(t, wsDir, "README.md", custom)
cfg, err := Load(root, "")
if err != nil {
t.Fatal(err)
}
rep, err := Init(cfg)
if err != nil {
t.Fatal(err)
}
if rep.WroteReadme {
t.Error("expected WroteReadme=false when README already exists")
}
b, _ := os.ReadFile(filepath.Join(wsDir, "README.md"))
if string(b) != custom {
t.Errorf("README overwritten:\n%s", string(b))
}
}
func TestInit_ScaffoldsKnowledgeDirs(t *testing.T) {
root := newRepo(t)
cfg, err := Load(root, "")
if err != nil {
t.Fatal(err)
}
// A safe set plus one unsafe component that must be skipped, never
// written outside UserDir.
cfg.KnowledgeDirs = []string{"frontend", "backend", "../evil", "docs"}
rep, err := Init(cfg)
if err != nil {
t.Fatal(err)
}
for _, d := range []string{"frontend", "backend", "docs"} {
info, err := os.Stat(filepath.Join(root, "tester", d))
if err != nil || !info.IsDir() {
t.Errorf("knowledge dir %s not created under UserDir: %v", d, err)
}
}
if _, err := os.Stat(filepath.Join(root, "evil")); err == nil {
t.Error("unsafe knowledge dir \"../evil\" escaped UserDir")
}
if got := strings.Join(rep.CreatedKnowledgeDirs, ","); got != "frontend,backend,docs" {
t.Errorf("CreatedKnowledgeDirs = %q, want frontend,backend,docs", got)
}
// Idempotent: a second Init creates nothing new.
rep2, err := Init(cfg)
if err != nil {
t.Fatal(err)
}
if len(rep2.CreatedKnowledgeDirs) != 0 {
t.Errorf("re-init CreatedKnowledgeDirs = %v, want empty", rep2.CreatedKnowledgeDirs)
}
}
// --- H1.2: branch/edge coverage deepening (test-only) ---
// TestSafeDirComponent covers the empty/"."/".." reject (init.go:136-138)
// and the not-clean reject (139-141) arms by calling the validator directly.
func TestSafeDirComponent(t *testing.T) {
cases := []struct {
name string
want bool
}{
{"", false},
{".", false},
{"..", false},
{"a/b", false},
{"./x", false},
{"/abs", false},
{`a\b`, false},
{"frontend", true},
{"a.b", true},
{"a-b_c", true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := safeDirComponent(tc.name); got != tc.want {
t.Errorf("safeDirComponent(%q) = %v, want %v", tc.name, got, tc.want)
}
})
}
}
// TestInit_NilConfig covers the nil guard (init.go:45-47).
func TestInit_NilConfig(t *testing.T) {
if _, err := Init(nil); err == nil {
t.Fatal("Init(nil) = nil error, want error")
}
}
// TestIsInitialized_NilConfig covers the nil guard (init.go:119-121).
func TestIsInitialized_NilConfig(t *testing.T) {
if IsInitialized(nil) {
t.Fatal("IsInitialized(nil) = true, want false")
}
}
// TestIsInitialized_IncompleteWorkspace covers the false arm of the
// subdir-missing check (init.go:124): a workspace missing one canonical
// subdir is not initialised.
func TestIsInitialized_IncompleteWorkspace(t *testing.T) {
root := newRepo(t)
cfg, err := Load(root, "")
if err != nil {
t.Fatal(err)
}
if _, err := Init(cfg); err != nil {
t.Fatal(err)
}
if !IsInitialized(cfg) {
t.Fatal("expected IsInitialized=true after Init")
}
if err := os.RemoveAll(filepath.Join(cfg.Workspace, "memory")); err != nil {
t.Fatal(err)
}
if IsInitialized(cfg) {
t.Error("expected IsInitialized=false with a subdir removed")
}
}
// TestInit_ErrorsWhenSubdirPathIsAFile covers the loop error propagation
// (init.go:64-66) and the ensureDirCreated non-dir arm (165-167): a file
// occupying a canonical subdir path surfaces a clear error.
func TestInit_ErrorsWhenSubdirPathIsAFile(t *testing.T) {
root := newRepo(t)
wsDir := filepath.Join(root, "tester", DefaultWorkspace)
// "engine" is created first and succeeds; "memory" is the file in the way.
write(t, wsDir, "memory", "i am a file")
cfg, err := Load(root, "")
if err != nil {
t.Fatal(err)
}
_, err = Init(cfg)
if err == nil {
t.Fatal("expected Init to error when a subdir path is a file")
}
if !strings.Contains(err.Error(), "exists and is not a directory") {
t.Errorf("error = %q, want it to contain %q", err.Error(), "exists and is not a directory")
}
}
// TestInit_ErrorsWhenGitignoreIsADirectory covers the ensureIgnored
// ReadFile non-NotExist arm (init.go:187-189) and the Init wrap (101-103):
// the subdirs are created before .gitignore is touched, so a directory at
// the .gitignore path fails the ReadFile cleanly.
func TestInit_ErrorsWhenGitignoreIsADirectory(t *testing.T) {
root := newRepo(t)
if err := os.Mkdir(filepath.Join(root, ".gitignore"), 0o755); err != nil {
t.Fatal(err)
}
cfg, err := Load(root, "")
if err != nil {
t.Fatal(err)
}
_, err = Init(cfg)
if err == nil {
t.Fatal("expected Init to error when .gitignore is a directory")
}
if !strings.Contains(err.Error(), "update .gitignore") {
t.Errorf("error = %q, want it to contain %q", err.Error(), "update .gitignore")
}
}
// TestInit_EmptyUsernameIgnoresWorkspaceName covers the empty-username
// fallback (init.go:97-99), only reachable with Username=="" (which Load
// never produces). Built directly, not via a seam. UserDir is empty so the
// knowledge loop is skipped; ensureIgnored receives WorkspaceName.
func TestInit_EmptyUsernameIgnoresWorkspaceName(t *testing.T) {
root := newRepo(t)
cfg := &Config{
RepoRoot: root,
Username: "",
WorkspaceName: DefaultWorkspace,
Workspace: filepath.Join(root, DefaultWorkspace),
Profile: ProfileGeneric,
}
rep, err := Init(cfg)
if err != nil {
t.Fatalf("Init: %v", err)
}
b, err := os.ReadFile(rep.GitignorePath)
if err != nil {
t.Fatal(err)
}
want := "/" + DefaultWorkspace + "/"
if !strings.Contains(string(b), want) {
t.Errorf(".gitignore = %q, want it to contain %q", string(b), want)
}
}