go: add 20 tests covering init, ota, commit, branch, merge, context
Swadhin Biswas committed 7 months ago
fee4079
1 parent 6011ef8
commit fee4079
1 file changed
+375 -0
Showing 1 changed file+375-0
▼GO/cortexa/workspace_test.go
+375-0
| @@ -0,0 +1,375 @@ | ||
1 | +package cortexa | |
2 | + | |
3 | +import ( | |
4 | + "os" | |
5 | + "path/filepath" | |
6 | + "strings" | |
7 | + "testing" | |
8 | +) | |
9 | + | |
10 | +// helper: creates a temp dir, initialises a Workspace, returns ws + cleanup func. | |
11 | +func setupWorkspace(t *testing.T, roadmap string) (*Workspace, func()) { | |
12 | + t.Helper() | |
13 | + dir := t.TempDir() | |
14 | + ws := New(dir) | |
15 | + if err := ws.Init(roadmap); err != nil { | |
16 | + t.Fatalf("Init: %v", err) | |
17 | + } | |
18 | + return ws, func() {} // t.TempDir handles cleanup | |
19 | +} | |
20 | + | |
21 | +// ------------------------------------------------------------------ // | |
22 | +// Init // | |
23 | +// ------------------------------------------------------------------ // | |
24 | + | |
25 | +func TestInit_CreatesGCCStructure(t *testing.T) { | |
26 | + ws, _ := setupWorkspace(t, "Build an AI agent") | |
27 | + | |
28 | + // .GCC/main.md exists | |
29 | + data, err := os.ReadFile(ws.mainMd()) | |
30 | + if err != nil { | |
31 | + t.Fatalf("main.md missing: %v", err) | |
32 | + } | |
33 | + if !strings.Contains(string(data), "Build an AI agent") { | |
34 | + t.Errorf("main.md missing roadmap content") | |
35 | + } | |
36 | + | |
37 | + // branches/main/ directory created | |
38 | + info, err := os.Stat(ws.branchDir(mainBranch)) | |
39 | + if err != nil || !info.IsDir() { | |
40 | + t.Errorf("expected branches/main directory") | |
41 | + } | |
42 | + | |
43 | + // log.md, commit.md, metadata.yaml present | |
44 | + for _, f := range []string{ws.logPath(mainBranch), ws.commitPath(mainBranch), ws.metaPath(mainBranch)} { | |
45 | + if _, err := os.Stat(f); err != nil { | |
46 | + t.Errorf("expected %s to exist", filepath.Base(f)) | |
47 | + } | |
48 | + } | |
49 | +} | |
50 | + | |
51 | +func TestInit_DoubleInit_Fails(t *testing.T) { | |
52 | + ws, _ := setupWorkspace(t, "test") | |
53 | + if err := ws.Init("again"); err == nil { | |
54 | + t.Errorf("expected error on double init") | |
55 | + } | |
56 | +} | |
57 | + | |
58 | +// ------------------------------------------------------------------ // | |
59 | +// LogOTA // | |
60 | +// ------------------------------------------------------------------ // | |
61 | + | |
62 | +func TestLogOTA_IncrementsStep(t *testing.T) { | |
63 | + ws, _ := setupWorkspace(t, "test") | |
64 | + | |
65 | + r1, err := ws.LogOTA("obs1", "thought1", "act1") | |
66 | + if err != nil { | |
67 | + t.Fatalf("LogOTA: %v", err) | |
68 | + } | |
69 | + if r1.Step != 1 { | |
70 | + t.Errorf("expected step 1, got %d", r1.Step) | |
71 | + } | |
72 | + | |
73 | + r2, err := ws.LogOTA("obs2", "thought2", "act2") | |
74 | + if err != nil { | |
75 | + t.Fatalf("LogOTA: %v", err) | |
76 | + } | |
77 | + if r2.Step != 2 { | |
78 | + t.Errorf("expected step 2, got %d", r2.Step) | |
79 | + } | |
80 | +} | |
81 | + | |
82 | +func TestLogOTA_AppendsToLogMd(t *testing.T) { | |
83 | + ws, _ := setupWorkspace(t, "test") | |
84 | + ws.LogOTA("observation text", "thinking hard", "do something") | |
85 | + | |
86 | + data := ws.read(ws.logPath(mainBranch)) | |
87 | + if !strings.Contains(data, "observation text") { | |
88 | + t.Errorf("log.md missing observation") | |
89 | + } | |
90 | + if !strings.Contains(data, "thinking hard") { | |
91 | + t.Errorf("log.md missing thought") | |
92 | + } | |
93 | +} | |
94 | + | |
95 | +// ------------------------------------------------------------------ // | |
96 | +// Commit // | |
97 | +// ------------------------------------------------------------------ // | |
98 | + | |
99 | +func TestCommit_WritesToCommitMd(t *testing.T) { | |
100 | + ws, _ := setupWorkspace(t, "test") | |
101 | + | |
102 | + c, err := ws.Commit("Finished setup phase", nil, nil) | |
103 | + if err != nil { | |
104 | + t.Fatalf("Commit: %v", err) | |
105 | + } | |
106 | + if c.CommitID == "" { | |
107 | + t.Error("expected non-empty commit ID") | |
108 | + } | |
109 | + if c.BranchName != mainBranch { | |
110 | + t.Errorf("expected branch %s, got %s", mainBranch, c.BranchName) | |
111 | + } | |
112 | + | |
113 | + data := ws.read(ws.commitPath(mainBranch)) | |
114 | + if !strings.Contains(data, "Finished setup phase") { | |
115 | + t.Error("commit.md missing contribution text") | |
116 | + } | |
117 | +} | |
118 | + | |
119 | +func TestCommit_AutoPreviousSummary(t *testing.T) { | |
120 | + ws, _ := setupWorkspace(t, "test") | |
121 | + | |
122 | + // First commit — no prior, should default | |
123 | + c1, _ := ws.Commit("First milestone", nil, nil) | |
124 | + if c1.PreviousProgressSummary != "Initial state — no prior commits." { | |
125 | + t.Errorf("unexpected first prev summary: %q", c1.PreviousProgressSummary) | |
126 | + } | |
127 | + | |
128 | + // Second commit — auto-uses first's contribution | |
129 | + c2, _ := ws.Commit("Second milestone", nil, nil) | |
130 | + if c2.PreviousProgressSummary != "First milestone" { | |
131 | + t.Errorf("expected auto previous summary 'First milestone', got %q", c2.PreviousProgressSummary) | |
132 | + } | |
133 | +} | |
134 | + | |
135 | +func TestCommit_UpdateRoadmap(t *testing.T) { | |
136 | + ws, _ := setupWorkspace(t, "test") | |
137 | + | |
138 | + update := "We pivoted to approach B" | |
139 | + ws.Commit("Pivot done", nil, &update) | |
140 | + | |
141 | + data := ws.read(ws.mainMd()) | |
142 | + if !strings.Contains(data, "We pivoted to approach B") { | |
143 | + t.Error("main.md should contain roadmap update") | |
144 | + } | |
145 | +} | |
146 | + | |
147 | +// ------------------------------------------------------------------ // | |
148 | +// Branch // | |
149 | +// ------------------------------------------------------------------ // | |
150 | + | |
151 | +func TestBranch_CreatesIsolatedWorkspace(t *testing.T) { | |
152 | + ws, _ := setupWorkspace(t, "test") | |
153 | + | |
154 | + if err := ws.Branch("experiment", "Try alternative approach"); err != nil { | |
155 | + t.Fatalf("Branch: %v", err) | |
156 | + } | |
157 | + if ws.CurrentBranch() != "experiment" { | |
158 | + t.Errorf("expected current branch 'experiment', got %q", ws.CurrentBranch()) | |
159 | + } | |
160 | + | |
161 | + // Branch should have its own log.md | |
162 | + info, err := os.Stat(ws.logPath("experiment")) | |
163 | + if err != nil { | |
164 | + t.Fatalf("experiment/log.md should exist: %v", err) | |
165 | + } | |
166 | + if info.Size() == 0 { | |
167 | + t.Error("log.md should not be empty (should have header)") | |
168 | + } | |
169 | + | |
170 | + // metadata.yaml should record purpose | |
171 | + meta, err := ws.parseMeta("experiment") | |
172 | + if err != nil { | |
173 | + t.Fatalf("parseMeta: %v", err) | |
174 | + } | |
175 | + if meta == nil || meta.Purpose != "Try alternative approach" { | |
176 | + t.Error("metadata should record purpose") | |
177 | + } | |
178 | +} | |
179 | + | |
180 | +func TestBranch_DuplicateName_Fails(t *testing.T) { | |
181 | + ws, _ := setupWorkspace(t, "test") | |
182 | + ws.Branch("exp", "first") | |
183 | + if err := ws.Branch("exp", "second"); err == nil { | |
184 | + t.Error("expected error creating duplicate branch") | |
185 | + } | |
186 | +} | |
187 | + | |
188 | +// ------------------------------------------------------------------ // | |
189 | +// Merge // | |
190 | +// ------------------------------------------------------------------ // | |
191 | + | |
192 | +func TestMerge_IntegratesBranchIntoTarget(t *testing.T) { | |
193 | + ws, _ := setupWorkspace(t, "test") | |
194 | + | |
195 | + // Work on main, then branch | |
196 | + ws.LogOTA("main obs", "main thought", "main action") | |
197 | + ws.Commit("Main setup done", nil, nil) | |
198 | + | |
199 | + ws.Branch("feature", "Add new capability") | |
200 | + ws.LogOTA("feature obs", "feature thought", "feature action") | |
201 | + ws.Commit("Feature implemented", nil, nil) | |
202 | + | |
203 | + // Merge back to main | |
204 | + mergeCommit, err := ws.Merge("feature", nil, mainBranch) | |
205 | + if err != nil { | |
206 | + t.Fatalf("Merge: %v", err) | |
207 | + } | |
208 | + | |
209 | + // Check merge commit | |
210 | + if !strings.Contains(mergeCommit.ThisCommitContribution, "feature") { | |
211 | + t.Error("merge commit should reference the merged branch") | |
212 | + } | |
213 | + if ws.CurrentBranch() != mainBranch { | |
214 | + t.Errorf("after merge, should be on %s, got %s", mainBranch, ws.CurrentBranch()) | |
215 | + } | |
216 | + | |
217 | + // OTA from feature should appear in main's log | |
218 | + data := ws.read(ws.logPath(mainBranch)) | |
219 | + if !strings.Contains(data, "feature obs") { | |
220 | + t.Error("main log should contain merged OTA from feature branch") | |
221 | + } | |
222 | + | |
223 | + // Feature branch metadata should say "merged" | |
224 | + meta, _ := ws.parseMeta("feature") | |
225 | + if meta == nil || meta.Status != "merged" { | |
226 | + t.Error("feature branch metadata should be status=merged") | |
227 | + } | |
228 | +} | |
229 | + | |
230 | +func TestMerge_NonexistentBranch_Fails(t *testing.T) { | |
231 | + ws, _ := setupWorkspace(t, "test") | |
232 | + _, err := ws.Merge("nope", nil, mainBranch) | |
233 | + if err == nil { | |
234 | + t.Error("expected error merging nonexistent branch") | |
235 | + } | |
236 | +} | |
237 | + | |
238 | +func TestMerge_NonexistentTarget_Fails(t *testing.T) { | |
239 | + ws, _ := setupWorkspace(t, "test") | |
240 | + ws.Branch("feature", "test") | |
241 | + _, err := ws.Merge("feature", nil, "nonexistent") | |
242 | + if err == nil { | |
243 | + t.Error("expected error merging into nonexistent target") | |
244 | + } | |
245 | +} | |
246 | + | |
247 | +// ------------------------------------------------------------------ // | |
248 | +// Context // | |
249 | +// ------------------------------------------------------------------ // | |
250 | + | |
251 | +func TestContext_ReturnsRoadmapAndCommits(t *testing.T) { | |
252 | + ws, _ := setupWorkspace(t, "Build a multi-agent system") | |
253 | + ws.LogOTA("o", "t", "a") | |
254 | + ws.Commit("Phase 1 complete", nil, nil) | |
255 | + ws.Commit("Phase 2 complete", nil, nil) | |
256 | + | |
257 | + // K=1 should return only last commit | |
258 | + ctx, err := ws.Context(nil, 1) | |
259 | + if err != nil { | |
260 | + t.Fatalf("Context: %v", err) | |
261 | + } | |
262 | + if !strings.Contains(ctx.MainRoadmap, "multi-agent system") { | |
263 | + t.Error("context should include roadmap") | |
264 | + } | |
265 | + if len(ctx.Commits) != 1 { | |
266 | + t.Errorf("K=1 should return 1 commit, got %d", len(ctx.Commits)) | |
267 | + } | |
268 | + if ctx.Commits[0].ThisCommitContribution != "Phase 2 complete" { | |
269 | + t.Error("K=1 should return the most recent commit") | |
270 | + } | |
271 | + | |
272 | + // K=5 (more than available) should return all | |
273 | + ctx2, _ := ws.Context(nil, 5) | |
274 | + if len(ctx2.Commits) != 2 { | |
275 | + t.Errorf("K=5 should return all 2 commits, got %d", len(ctx2.Commits)) | |
276 | + } | |
277 | +} | |
278 | + | |
279 | +func TestContext_NonexistentBranch_Fails(t *testing.T) { | |
280 | + ws, _ := setupWorkspace(t, "test") | |
281 | + nope := "nonexistent" | |
282 | + _, err := ws.Context(&nope, 1) | |
283 | + if err == nil { | |
284 | + t.Error("expected error for nonexistent branch") | |
285 | + } | |
286 | +} | |
287 | + | |
288 | +func TestContext_Summary(t *testing.T) { | |
289 | + ws, _ := setupWorkspace(t, "Project X") | |
290 | + ws.LogOTA("obs", "thought", "action") | |
291 | + ws.Commit("Done step 1", nil, nil) | |
292 | + | |
293 | + ctx, _ := ws.Context(nil, 1) | |
294 | + summary := ctx.Summary() | |
295 | + if !strings.Contains(summary, "CONTEXT") { | |
296 | + t.Error("summary should contain CONTEXT header") | |
297 | + } | |
298 | + if !strings.Contains(summary, mainBranch) { | |
299 | + t.Error("summary should mention branch name") | |
300 | + } | |
301 | +} | |
302 | + | |
303 | +// ------------------------------------------------------------------ // | |
304 | +// Helpers // | |
305 | +// ------------------------------------------------------------------ // | |
306 | + | |
307 | +func TestSwitchBranch(t *testing.T) { | |
308 | + ws, _ := setupWorkspace(t, "test") | |
309 | + ws.Branch("alt", "alternative path") | |
310 | + ws.SwitchBranch(mainBranch) | |
311 | + if ws.CurrentBranch() != mainBranch { | |
312 | + t.Errorf("expected main, got %s", ws.CurrentBranch()) | |
313 | + } | |
314 | +} | |
315 | + | |
316 | +func TestSwitchBranch_Nonexistent_Fails(t *testing.T) { | |
317 | + ws, _ := setupWorkspace(t, "test") | |
318 | + if err := ws.SwitchBranch("nope"); err == nil { | |
319 | + t.Error("expected error switching to nonexistent branch") | |
320 | + } | |
321 | +} | |
322 | + | |
323 | +func TestListBranches(t *testing.T) { | |
324 | + ws, _ := setupWorkspace(t, "test") | |
325 | + ws.Branch("alpha", "a") | |
326 | + ws.Branch("beta", "b") | |
327 | + | |
328 | + branches, err := ws.ListBranches() | |
329 | + if err != nil { | |
330 | + t.Fatalf("ListBranches: %v", err) | |
331 | + } | |
332 | + // Should have main, alpha, beta | |
333 | + if len(branches) != 3 { | |
334 | + t.Errorf("expected 3 branches, got %d: %v", len(branches), branches) | |
335 | + } | |
336 | +} | |
337 | + | |
338 | +// ------------------------------------------------------------------ // | |
339 | +// Models // | |
340 | +// ------------------------------------------------------------------ // | |
341 | + | |
342 | +func TestOTARecord_ToMarkdown(t *testing.T) { | |
343 | + r := OTARecord{ | |
344 | + Step: 1, | |
345 | + Timestamp: "2025-01-01T00:00:00Z", | |
346 | + Observation: "saw an error", | |
347 | + Thought: "need to fix it", | |
348 | + Action: "patched the code", | |
349 | + } | |
350 | + md := r.ToMarkdown() | |
351 | + if !strings.Contains(md, "Step 1") { | |
352 | + t.Error("markdown should contain step number") | |
353 | + } | |
354 | + if !strings.Contains(md, "saw an error") { | |
355 | + t.Error("markdown should contain observation") | |
356 | + } | |
357 | +} | |
358 | + | |
359 | +func TestCommitRecord_ToMarkdown(t *testing.T) { | |
360 | + c := CommitRecord{ | |
361 | + CommitID: "abc123", | |
362 | + BranchName: "main", | |
363 | + BranchPurpose: "primary", | |
364 | + PreviousProgressSummary: "none", | |
365 | + ThisCommitContribution: "added feature X", | |
366 | + Timestamp: "2025-01-01T00:00:00Z", | |
367 | + } | |
368 | + md := c.ToMarkdown() | |
369 | + if !strings.Contains(md, "abc123") { | |
370 | + t.Error("markdown should contain commit ID") | |
371 | + } | |
372 | + if !strings.Contains(md, "added feature X") { | |
373 | + t.Error("markdown should contain contribution") | |
374 | + } | |
375 | +} | |
0 | 376 | |