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) } }