project_init_test.go
1 package daemon 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "net/http" 8 "os" 9 "path/filepath" 10 "strings" 11 "testing" 12 "time" 13 14 "context" 15 ) 16 17 func TestServer_ProjectInit_BasicInit(t *testing.T) { 18 shannonDir := t.TempDir() 19 projectDir := t.TempDir() 20 deps := &ServerDeps{ 21 ShannonDir: shannonDir, 22 SessionCache: NewSessionCache(shannonDir), 23 } 24 c := NewClient("ws://localhost:1/x", "", func(msg MessagePayload) string { return "" }, nil) 25 srv := NewServer(0, c, deps, "test") 26 ctx, cancel := context.WithCancel(context.Background()) 27 defer cancel() 28 29 go srv.Start(ctx) 30 time.Sleep(100 * time.Millisecond) 31 32 body := fmt.Sprintf(`{"cwd":%q}`, projectDir) 33 resp, err := http.Post( 34 fmt.Sprintf("http://127.0.0.1:%d/project/init", srv.Port()), 35 "application/json", 36 strings.NewReader(body), 37 ) 38 if err != nil { 39 t.Fatal(err) 40 } 41 defer resp.Body.Close() 42 43 if resp.StatusCode != http.StatusOK { 44 raw, _ := io.ReadAll(resp.Body) 45 t.Fatalf("expected 200, got %d: %s", resp.StatusCode, raw) 46 } 47 48 var result struct { 49 Created []string `json:"created"` 50 Existed []string `json:"existed"` 51 } 52 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 53 t.Fatalf("decode response: %v", err) 54 } 55 56 if len(result.Created) != 1 || result.Created[0] != ".shannon/" { 57 t.Errorf("created = %v, want [.shannon/]", result.Created) 58 } 59 if len(result.Existed) != 0 { 60 t.Errorf("existed = %v, want []", result.Existed) 61 } 62 63 if _, err := os.Stat(filepath.Join(projectDir, ".shannon")); err != nil { 64 t.Errorf(".shannon dir not created: %v", err) 65 } 66 } 67 68 func TestServer_ProjectInit_WithInstructions(t *testing.T) { 69 shannonDir := t.TempDir() 70 projectDir := t.TempDir() 71 deps := &ServerDeps{ 72 ShannonDir: shannonDir, 73 SessionCache: NewSessionCache(shannonDir), 74 } 75 c := NewClient("ws://localhost:1/x", "", func(msg MessagePayload) string { return "" }, nil) 76 srv := NewServer(0, c, deps, "test") 77 ctx, cancel := context.WithCancel(context.Background()) 78 defer cancel() 79 80 go srv.Start(ctx) 81 time.Sleep(100 * time.Millisecond) 82 83 body := fmt.Sprintf(`{"cwd":%q,"instructions":"# My Project\n\nDo good stuff."}`, projectDir) 84 resp, err := http.Post( 85 fmt.Sprintf("http://127.0.0.1:%d/project/init", srv.Port()), 86 "application/json", 87 strings.NewReader(body), 88 ) 89 if err != nil { 90 t.Fatal(err) 91 } 92 defer resp.Body.Close() 93 94 if resp.StatusCode != http.StatusOK { 95 raw, _ := io.ReadAll(resp.Body) 96 t.Fatalf("expected 200, got %d: %s", resp.StatusCode, raw) 97 } 98 99 var result struct { 100 Created []string `json:"created"` 101 Existed []string `json:"existed"` 102 } 103 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 104 t.Fatalf("decode response: %v", err) 105 } 106 107 wantCreated := map[string]bool{".shannon/": true, ".shannon/instructions.md": true} 108 for _, c := range result.Created { 109 if !wantCreated[c] { 110 t.Errorf("unexpected created entry: %q", c) 111 } 112 delete(wantCreated, c) 113 } 114 if len(wantCreated) > 0 { 115 t.Errorf("missing created entries: %v", wantCreated) 116 } 117 if len(result.Existed) != 0 { 118 t.Errorf("existed = %v, want []", result.Existed) 119 } 120 121 instPath := filepath.Join(projectDir, ".shannon", "instructions.md") 122 data, err := os.ReadFile(instPath) 123 if err != nil { 124 t.Fatalf("instructions.md not created: %v", err) 125 } 126 if string(data) != "# My Project\n\nDo good stuff." { 127 t.Errorf("instructions content = %q, unexpected", string(data)) 128 } 129 } 130 131 func TestServer_ProjectInit_RelativePath(t *testing.T) { 132 shannonDir := t.TempDir() 133 deps := &ServerDeps{ 134 ShannonDir: shannonDir, 135 SessionCache: NewSessionCache(shannonDir), 136 } 137 c := NewClient("ws://localhost:1/x", "", func(msg MessagePayload) string { return "" }, nil) 138 srv := NewServer(0, c, deps, "test") 139 ctx, cancel := context.WithCancel(context.Background()) 140 defer cancel() 141 142 go srv.Start(ctx) 143 time.Sleep(100 * time.Millisecond) 144 145 resp, err := http.Post( 146 fmt.Sprintf("http://127.0.0.1:%d/project/init", srv.Port()), 147 "application/json", 148 strings.NewReader(`{"cwd":"relative/path"}`), 149 ) 150 if err != nil { 151 t.Fatal(err) 152 } 153 defer resp.Body.Close() 154 155 if resp.StatusCode != http.StatusBadRequest { 156 t.Errorf("expected 400, got %d", resp.StatusCode) 157 } 158 } 159 160 func TestServer_ProjectInit_InsideShannonDir(t *testing.T) { 161 shannonDir := t.TempDir() 162 // Try to init inside the global shannon dir itself 163 subDir := filepath.Join(shannonDir, "subproject") 164 if err := os.MkdirAll(subDir, 0700); err != nil { 165 t.Fatalf("mkdir subDir: %v", err) 166 } 167 deps := &ServerDeps{ 168 ShannonDir: shannonDir, 169 SessionCache: NewSessionCache(shannonDir), 170 } 171 c := NewClient("ws://localhost:1/x", "", func(msg MessagePayload) string { return "" }, nil) 172 srv := NewServer(0, c, deps, "test") 173 ctx, cancel := context.WithCancel(context.Background()) 174 defer cancel() 175 176 go srv.Start(ctx) 177 time.Sleep(100 * time.Millisecond) 178 179 // Test 1: exact shannon dir 180 body := fmt.Sprintf(`{"cwd":%q}`, shannonDir) 181 resp, err := http.Post( 182 fmt.Sprintf("http://127.0.0.1:%d/project/init", srv.Port()), 183 "application/json", 184 strings.NewReader(body), 185 ) 186 if err != nil { 187 t.Fatal(err) 188 } 189 resp.Body.Close() 190 if resp.StatusCode != http.StatusBadRequest { 191 t.Errorf("exact shannonDir: expected 400, got %d", resp.StatusCode) 192 } 193 194 // Test 2: subdir inside shannon dir 195 body2 := fmt.Sprintf(`{"cwd":%q}`, subDir) 196 resp2, err := http.Post( 197 fmt.Sprintf("http://127.0.0.1:%d/project/init", srv.Port()), 198 "application/json", 199 strings.NewReader(body2), 200 ) 201 if err != nil { 202 t.Fatal(err) 203 } 204 resp2.Body.Close() 205 if resp2.StatusCode != http.StatusBadRequest { 206 t.Errorf("subDir of shannonDir: expected 400, got %d", resp2.StatusCode) 207 } 208 } 209 210 func TestServer_ProjectInit_Idempotent(t *testing.T) { 211 shannonDir := t.TempDir() 212 projectDir := t.TempDir() 213 deps := &ServerDeps{ 214 ShannonDir: shannonDir, 215 SessionCache: NewSessionCache(shannonDir), 216 } 217 c := NewClient("ws://localhost:1/x", "", func(msg MessagePayload) string { return "" }, nil) 218 srv := NewServer(0, c, deps, "test") 219 ctx, cancel := context.WithCancel(context.Background()) 220 defer cancel() 221 222 go srv.Start(ctx) 223 time.Sleep(100 * time.Millisecond) 224 225 // Pre-create .shannon dir and instructions.md with existing content 226 dotShannon := filepath.Join(projectDir, ".shannon") 227 if err := os.MkdirAll(dotShannon, 0700); err != nil { 228 t.Fatalf("mkdir: %v", err) 229 } 230 existingContent := "# Existing Instructions" 231 if err := os.WriteFile(filepath.Join(dotShannon, "instructions.md"), []byte(existingContent), 0600); err != nil { 232 t.Fatalf("write existing instructions: %v", err) 233 } 234 235 body := fmt.Sprintf(`{"cwd":%q,"instructions":"# New Instructions"}`, projectDir) 236 resp, err := http.Post( 237 fmt.Sprintf("http://127.0.0.1:%d/project/init", srv.Port()), 238 "application/json", 239 strings.NewReader(body), 240 ) 241 if err != nil { 242 t.Fatal(err) 243 } 244 defer resp.Body.Close() 245 246 if resp.StatusCode != http.StatusOK { 247 raw, _ := io.ReadAll(resp.Body) 248 t.Fatalf("expected 200, got %d: %s", resp.StatusCode, raw) 249 } 250 251 var result struct { 252 Created []string `json:"created"` 253 Existed []string `json:"existed"` 254 } 255 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 256 t.Fatalf("decode response: %v", err) 257 } 258 259 if len(result.Created) != 0 { 260 t.Errorf("created = %v, want [] (both already existed)", result.Created) 261 } 262 wantExisted := map[string]bool{".shannon/": true, ".shannon/instructions.md": true} 263 for _, e := range result.Existed { 264 if !wantExisted[e] { 265 t.Errorf("unexpected existed entry: %q", e) 266 } 267 delete(wantExisted, e) 268 } 269 if len(wantExisted) > 0 { 270 t.Errorf("missing existed entries: %v", wantExisted) 271 } 272 273 // Verify existing file was NOT overwritten 274 data, _ := os.ReadFile(filepath.Join(dotShannon, "instructions.md")) 275 if string(data) != existingContent { 276 t.Errorf("instructions.md was overwritten: got %q, want %q", string(data), existingContent) 277 } 278 }