lua: add 21-test suite covering models and workspace
Swadhin Biswas committed 7 months ago
260775a
1 parent 2332079
commit 260775a
1 file changed
+293 -0
Showing 1 changed file+293-0
▼LUA/test.lua
+293-0
| @@ -0,0 +1,293 @@ | ||
1 | +--- contexa test suite | |
2 | +--- Run with: lua test.lua (requires luafilesystem) | |
3 | + | |
4 | +-- Adjust package path to find the local contexa module | |
5 | +package.path = "./?.lua;./?/init.lua;" .. package.path | |
6 | + | |
7 | +local workspace = require("contexa.workspace") | |
8 | +local models = require("contexa.models") | |
9 | + | |
10 | +local test_count = 0 | |
11 | +local pass_count = 0 | |
12 | +local fail_count = 0 | |
13 | + | |
14 | +local function test(name, fn) | |
15 | + test_count = test_count + 1 | |
16 | + local ok, err = pcall(fn) | |
17 | + if ok then | |
18 | + pass_count = pass_count + 1 | |
19 | + print(string.format(" PASS %s", name)) | |
20 | + else | |
21 | + fail_count = fail_count + 1 | |
22 | + print(string.format(" FAIL %s: %s", name, err)) | |
23 | + end | |
24 | +end | |
25 | + | |
26 | +local function assert_eq(a, b, msg) | |
27 | + if a ~= b then | |
28 | + error(string.format("%s: expected %q, got %q", msg or "assert_eq", tostring(b), tostring(a)), 2) | |
29 | + end | |
30 | +end | |
31 | + | |
32 | +local function assert_true(v, msg) | |
33 | + if not v then error(msg or "assert_true failed", 2) end | |
34 | +end | |
35 | + | |
36 | +local function assert_error(fn, msg) | |
37 | + local ok, _ = pcall(fn) | |
38 | + if ok then error(msg or "Expected error but none raised", 2) end | |
39 | +end | |
40 | + | |
41 | +--- Create a unique temp directory for each test | |
42 | +local tmp_counter = 0 | |
43 | +local function make_tmp() | |
44 | + tmp_counter = tmp_counter + 1 | |
45 | + local dir = "/tmp/contexa_lua_test_" .. os.time() .. "_" .. tmp_counter | |
46 | + os.execute('rm -rf "' .. dir .. '"') | |
47 | + os.execute('mkdir -p "' .. dir .. '"') | |
48 | + return dir | |
49 | +end | |
50 | + | |
51 | +local function cleanup(dir) | |
52 | + os.execute('rm -rf "' .. dir .. '"') | |
53 | +end | |
54 | + | |
55 | +------------------------------------------------------------------------ | |
56 | +print("contexa Lua test suite") | |
57 | +print(string.rep("=", 50)) | |
58 | + | |
59 | +------------------------------------------------------------------------ | |
60 | +-- Model tests | |
61 | +------------------------------------------------------------------------ | |
62 | +print("\n-- Models --") | |
63 | + | |
64 | +test("generate_id returns 8 hex chars", function() | |
65 | + local id = models.generate_id() | |
66 | + assert_eq(#id, 8, "length") | |
67 | + assert_true(id:match("^%x+$"), "should be hex") | |
68 | +end) | |
69 | + | |
70 | +test("timestamp returns ISO 8601", function() | |
71 | + local ts = models.timestamp() | |
72 | + assert_true(ts:match("^%d%d%d%d%-%d%d%-%d%dT"), "ISO format") | |
73 | +end) | |
74 | + | |
75 | +test("OTA to_markdown round-trip", function() | |
76 | + local rec = models.new_ota(1, "saw something", "thinking", "did something") | |
77 | + local md = models.ota_to_markdown(rec) | |
78 | + assert_true(md:find("### Step 1"), "has step header") | |
79 | + assert_true(md:find("saw something"), "has observation") | |
80 | + assert_true(md:find("thinking"), "has thought") | |
81 | + assert_true(md:find("did something"), "has action") | |
82 | +end) | |
83 | + | |
84 | +test("Commit to_markdown round-trip", function() | |
85 | + local rec = models.new_commit("main", "purpose", "prev", "contrib") | |
86 | + local md = models.commit_to_markdown(rec) | |
87 | + assert_true(md:find("## Commit"), "has commit header") | |
88 | + assert_true(md:find("purpose"), "has purpose") | |
89 | + assert_true(md:find("contrib"), "has contribution") | |
90 | +end) | |
91 | + | |
92 | +test("BranchMetadata YAML round-trip", function() | |
93 | + local meta = models.new_branch_metadata("test-branch", "test purpose", "main") | |
94 | + local yaml = models.metadata_to_yaml(meta) | |
95 | + local parsed = models.metadata_from_yaml(yaml) | |
96 | + assert_eq(parsed.name, "test-branch", "name") | |
97 | + assert_eq(parsed.purpose, "test purpose", "purpose") | |
98 | + assert_eq(parsed.created_from, "main", "created_from") | |
99 | + assert_eq(parsed.status, "active", "status") | |
100 | + assert_eq(parsed.merged_into, nil, "merged_into") | |
101 | +end) | |
102 | + | |
103 | +------------------------------------------------------------------------ | |
104 | +-- Workspace tests | |
105 | +------------------------------------------------------------------------ | |
106 | +print("\n-- Workspace --") | |
107 | + | |
108 | +test("init creates workspace", function() | |
109 | + local dir = make_tmp() | |
110 | + local ws = workspace.new(dir) | |
111 | + ws:init("Test roadmap") | |
112 | + assert_eq(ws:current_branch(), "main") | |
113 | + local branches = ws:list_branches() | |
114 | + assert_true(#branches >= 1, "has at least 1 branch") | |
115 | + cleanup(dir) | |
116 | +end) | |
117 | + | |
118 | +test("init fails if already exists", function() | |
119 | + local dir = make_tmp() | |
120 | + local ws = workspace.new(dir) | |
121 | + ws:init() | |
122 | + assert_error(function() ws:init() end, "should fail on double init") | |
123 | + cleanup(dir) | |
124 | +end) | |
125 | + | |
126 | +test("load attaches to existing workspace", function() | |
127 | + local dir = make_tmp() | |
128 | + local ws = workspace.new(dir) | |
129 | + ws:init() | |
130 | + local ws2 = workspace.new(dir) | |
131 | + ws2:load() | |
132 | + assert_eq(ws2:current_branch(), "main") | |
133 | + cleanup(dir) | |
134 | +end) | |
135 | + | |
136 | +test("load fails if no workspace", function() | |
137 | + local dir = make_tmp() | |
138 | + local ws = workspace.new(dir) | |
139 | + assert_error(function() ws:load() end, "should fail on missing workspace") | |
140 | + cleanup(dir) | |
141 | +end) | |
142 | + | |
143 | +test("log_ota appends records with incrementing steps", function() | |
144 | + local dir = make_tmp() | |
145 | + local ws = workspace.new(dir) | |
146 | + ws:init() | |
147 | + local r1 = ws:log_ota("obs1", "thought1", "act1") | |
148 | + assert_eq(r1.step, 1, "step 1") | |
149 | + local r2 = ws:log_ota("obs2", "thought2", "act2") | |
150 | + assert_eq(r2.step, 2, "step 2") | |
151 | + cleanup(dir) | |
152 | +end) | |
153 | + | |
154 | +test("commit creates record with auto previous_summary", function() | |
155 | + local dir = make_tmp() | |
156 | + local ws = workspace.new(dir) | |
157 | + ws:init() | |
158 | + local c1 = ws:commit("First contribution") | |
159 | + assert_true(c1.commit_id ~= "", "has commit_id") | |
160 | + assert_true(#c1.commit_id == 8, "8-char id") | |
161 | + -- Second commit should auto-fill previous_summary | |
162 | + local c2 = ws:commit("Second contribution") | |
163 | + assert_eq(c2.previous_progress_summary, "First contribution", "auto previous_summary") | |
164 | + cleanup(dir) | |
165 | +end) | |
166 | + | |
167 | +test("branch creates isolated workspace", function() | |
168 | + local dir = make_tmp() | |
169 | + local ws = workspace.new(dir) | |
170 | + ws:init() | |
171 | + ws:log_ota("obs", "thought", "action") | |
172 | + ws:branch("feature", "Test feature") | |
173 | + assert_eq(ws:current_branch(), "feature") | |
174 | + -- New branch should have empty OTA/commits | |
175 | + local ctx = ws:context("feature", 10) | |
176 | + assert_eq(#ctx.ota_records, 0, "fresh OTA log") | |
177 | + assert_eq(#ctx.commits, 0, "fresh commits") | |
178 | + cleanup(dir) | |
179 | +end) | |
180 | + | |
181 | +test("branch fails if duplicate name", function() | |
182 | + local dir = make_tmp() | |
183 | + local ws = workspace.new(dir) | |
184 | + ws:init() | |
185 | + ws:branch("dup", "purpose") | |
186 | + ws:switch_branch("main") | |
187 | + assert_error(function() ws:branch("dup", "purpose") end, "should fail on duplicate") | |
188 | + cleanup(dir) | |
189 | +end) | |
190 | + | |
191 | +test("switch_branch changes current branch", function() | |
192 | + local dir = make_tmp() | |
193 | + local ws = workspace.new(dir) | |
194 | + ws:init() | |
195 | + ws:branch("feature", "purpose") | |
196 | + ws:switch_branch("main") | |
197 | + assert_eq(ws:current_branch(), "main") | |
198 | + cleanup(dir) | |
199 | +end) | |
200 | + | |
201 | +test("switch_branch fails on non-existent branch", function() | |
202 | + local dir = make_tmp() | |
203 | + local ws = workspace.new(dir) | |
204 | + ws:init() | |
205 | + assert_error(function() ws:switch_branch("nope") end, "should fail") | |
206 | + cleanup(dir) | |
207 | +end) | |
208 | + | |
209 | +test("list_branches returns all branches", function() | |
210 | + local dir = make_tmp() | |
211 | + local ws = workspace.new(dir) | |
212 | + ws:init() | |
213 | + ws:branch("alpha", "purpose a") | |
214 | + ws:switch_branch("main") | |
215 | + ws:branch("beta", "purpose b") | |
216 | + local branches = ws:list_branches() | |
217 | + assert_true(#branches >= 3, "at least 3 branches") | |
218 | + cleanup(dir) | |
219 | +end) | |
220 | + | |
221 | +test("merge integrates branch into target", function() | |
222 | + local dir = make_tmp() | |
223 | + local ws = workspace.new(dir) | |
224 | + ws:init() | |
225 | + ws:branch("feature", "Add feature X") | |
226 | + ws:log_ota("obs", "thought", "action") | |
227 | + ws:commit("Implemented feature X") | |
228 | + local merge_rec = ws:merge("feature", nil, "main") | |
229 | + assert_eq(ws:current_branch(), "main", "switched to target") | |
230 | + assert_true(merge_rec.commit_id ~= "", "has merge commit") | |
231 | + -- Check source branch is marked merged | |
232 | + local ctx = ws:context("main", 10) | |
233 | + assert_true(#ctx.commits >= 1, "has merge commit on main") | |
234 | + cleanup(dir) | |
235 | +end) | |
236 | + | |
237 | +test("merge fails on non-existent source", function() | |
238 | + local dir = make_tmp() | |
239 | + local ws = workspace.new(dir) | |
240 | + ws:init() | |
241 | + assert_error(function() ws:merge("nope") end, "should fail") | |
242 | + cleanup(dir) | |
243 | +end) | |
244 | + | |
245 | +test("context returns correct data", function() | |
246 | + local dir = make_tmp() | |
247 | + local ws = workspace.new(dir) | |
248 | + ws:init("My roadmap") | |
249 | + ws:log_ota("obs1", "t1", "a1") | |
250 | + ws:log_ota("obs2", "t2", "a2") | |
251 | + ws:commit("Commit 1") | |
252 | + ws:commit("Commit 2") | |
253 | + ws:commit("Commit 3") | |
254 | + local ctx = ws:context("main", 2) | |
255 | + assert_eq(#ctx.commits, 2, "last 2 commits") | |
256 | + assert_eq(#ctx.ota_records, 2, "all OTA records") | |
257 | + assert_true(ctx.main_roadmap:find("My roadmap"), "has roadmap") | |
258 | + assert_true(ctx.metadata ~= nil, "has metadata") | |
259 | + assert_eq(ctx.metadata.name, "main", "metadata.name") | |
260 | + cleanup(dir) | |
261 | +end) | |
262 | + | |
263 | +test("context k larger than available commits", function() | |
264 | + local dir = make_tmp() | |
265 | + local ws = workspace.new(dir) | |
266 | + ws:init() | |
267 | + ws:commit("Only commit") | |
268 | + local ctx = ws:context("main", 100) | |
269 | + assert_eq(#ctx.commits, 1, "returns all available") | |
270 | + cleanup(dir) | |
271 | +end) | |
272 | + | |
273 | +test("context_summary renders markdown", function() | |
274 | + local dir = make_tmp() | |
275 | + local ws = workspace.new(dir) | |
276 | + ws:init("Roadmap text") | |
277 | + ws:log_ota("obs", "thought", "action") | |
278 | + ws:commit("My contribution") | |
279 | + local ctx = ws:context("main", 1) | |
280 | + local summary = models.context_summary(ctx) | |
281 | + assert_true(summary:find("CONTEXT"), "has CONTEXT header") | |
282 | + assert_true(summary:find("Roadmap"), "has roadmap") | |
283 | + assert_true(summary:find("My contribution"), "has commit") | |
284 | + cleanup(dir) | |
285 | +end) | |
286 | + | |
287 | +------------------------------------------------------------------------ | |
288 | +print(string.format("\n%s", string.rep("=", 50))) | |
289 | +print(string.format("Results: %d/%d passed, %d failed", pass_count, test_count, fail_count)) | |
290 | + | |
291 | +if fail_count > 0 then | |
292 | + os.exit(1) | |
293 | +end | |
0 | 294 | |