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