/ internal / daemon / project_init_test.go
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  }