/ contexa / LUA / test.lua
Swadhin Biswas lua: add 21-test suite covering models and workspace · 3/4/2026
260775a
294 lines · 9.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
--- contexa test suite
--- Run with: lua test.lua (requires luafilesystem)

-- Adjust package path to find the local contexa module
package.path = "./?.lua;./?/init.lua;" .. package.path

local workspace = require("contexa.workspace")
local models = require("contexa.models")

local test_count = 0
local pass_count = 0
local fail_count = 0

local function test(name, fn)
    test_count = test_count + 1
    local ok, err = pcall(fn)
    if ok then
        pass_count = pass_count + 1
        print(string.format("  PASS  %s", name))
    else
        fail_count = fail_count + 1
        print(string.format("  FAIL  %s: %s", name, err))
    end
end

local function assert_eq(a, b, msg)
    if a ~= b then
        error(string.format("%s: expected %q, got %q", msg or "assert_eq", tostring(b), tostring(a)), 2)
    end
end

local function assert_true(v, msg)
    if not v then error(msg or "assert_true failed", 2) end
end

local function assert_error(fn, msg)
    local ok, _ = pcall(fn)
    if ok then error(msg or "Expected error but none raised", 2) end
end

--- Create a unique temp directory for each test
local tmp_counter = 0
local function make_tmp()
    tmp_counter = tmp_counter + 1
    local dir = "/tmp/contexa_lua_test_" .. os.time() .. "_" .. tmp_counter
    os.execute('rm -rf "' .. dir .. '"')
    os.execute('mkdir -p "' .. dir .. '"')
    return dir
end

local function cleanup(dir)
    os.execute('rm -rf "' .. dir .. '"')
end

------------------------------------------------------------------------
print("contexa Lua test suite")
print(string.rep("=", 50))

------------------------------------------------------------------------
-- Model tests
------------------------------------------------------------------------
print("\n-- Models --")

test("generate_id returns 8 hex chars", function()
    local id = models.generate_id()
    assert_eq(#id, 8, "length")
    assert_true(id:match("^%x+$"), "should be hex")
end)

test("timestamp returns ISO 8601", function()
    local ts = models.timestamp()
    assert_true(ts:match("^%d%d%d%d%-%d%d%-%d%dT"), "ISO format")
end)

test("OTA to_markdown round-trip", function()
    local rec = models.new_ota(1, "saw something", "thinking", "did something")
    local md = models.ota_to_markdown(rec)
    assert_true(md:find("### Step 1"), "has step header")
    assert_true(md:find("saw something"), "has observation")
    assert_true(md:find("thinking"), "has thought")
    assert_true(md:find("did something"), "has action")
end)

test("Commit to_markdown round-trip", function()
    local rec = models.new_commit("main", "purpose", "prev", "contrib")
    local md = models.commit_to_markdown(rec)
    assert_true(md:find("## Commit"), "has commit header")
    assert_true(md:find("purpose"), "has purpose")
    assert_true(md:find("contrib"), "has contribution")
end)

test("BranchMetadata YAML round-trip", function()
    local meta = models.new_branch_metadata("test-branch", "test purpose", "main")
    local yaml = models.metadata_to_yaml(meta)
    local parsed = models.metadata_from_yaml(yaml)
    assert_eq(parsed.name, "test-branch", "name")
    assert_eq(parsed.purpose, "test purpose", "purpose")
    assert_eq(parsed.created_from, "main", "created_from")
    assert_eq(parsed.status, "active", "status")
    assert_eq(parsed.merged_into, nil, "merged_into")
end)

------------------------------------------------------------------------
-- Workspace tests
------------------------------------------------------------------------
print("\n-- Workspace --")

test("init creates workspace", function()
    local dir = make_tmp()
    local ws = workspace.new(dir)
    ws:init("Test roadmap")
    assert_eq(ws:current_branch(), "main")
    local branches = ws:list_branches()
    assert_true(#branches >= 1, "has at least 1 branch")
    cleanup(dir)
end)

test("init fails if already exists", function()
    local dir = make_tmp()
    local ws = workspace.new(dir)
    ws:init()
    assert_error(function() ws:init() end, "should fail on double init")
    cleanup(dir)
end)

test("load attaches to existing workspace", function()
    local dir = make_tmp()
    local ws = workspace.new(dir)
    ws:init()
    local ws2 = workspace.new(dir)
    ws2:load()
    assert_eq(ws2:current_branch(), "main")
    cleanup(dir)
end)

test("load fails if no workspace", function()
    local dir = make_tmp()
    local ws = workspace.new(dir)
    assert_error(function() ws:load() end, "should fail on missing workspace")
    cleanup(dir)
end)

test("log_ota appends records with incrementing steps", function()
    local dir = make_tmp()
    local ws = workspace.new(dir)
    ws:init()
    local r1 = ws:log_ota("obs1", "thought1", "act1")
    assert_eq(r1.step, 1, "step 1")
    local r2 = ws:log_ota("obs2", "thought2", "act2")
    assert_eq(r2.step, 2, "step 2")
    cleanup(dir)
end)

test("commit creates record with auto previous_summary", function()
    local dir = make_tmp()
    local ws = workspace.new(dir)
    ws:init()
    local c1 = ws:commit("First contribution")
    assert_true(c1.commit_id ~= "", "has commit_id")
    assert_true(#c1.commit_id == 8, "8-char id")
    -- Second commit should auto-fill previous_summary
    local c2 = ws:commit("Second contribution")
    assert_eq(c2.previous_progress_summary, "First contribution", "auto previous_summary")
    cleanup(dir)
end)

test("branch creates isolated workspace", function()
    local dir = make_tmp()
    local ws = workspace.new(dir)
    ws:init()
    ws:log_ota("obs", "thought", "action")
    ws:branch("feature", "Test feature")
    assert_eq(ws:current_branch(), "feature")
    -- New branch should have empty OTA/commits
    local ctx = ws:context("feature", 10)
    assert_eq(#ctx.ota_records, 0, "fresh OTA log")
    assert_eq(#ctx.commits, 0, "fresh commits")
    cleanup(dir)
end)

test("branch fails if duplicate name", function()
    local dir = make_tmp()
    local ws = workspace.new(dir)
    ws:init()
    ws:branch("dup", "purpose")
    ws:switch_branch("main")
    assert_error(function() ws:branch("dup", "purpose") end, "should fail on duplicate")
    cleanup(dir)
end)

test("switch_branch changes current branch", function()
    local dir = make_tmp()
    local ws = workspace.new(dir)
    ws:init()
    ws:branch("feature", "purpose")
    ws:switch_branch("main")
    assert_eq(ws:current_branch(), "main")
    cleanup(dir)
end)

test("switch_branch fails on non-existent branch", function()
    local dir = make_tmp()
    local ws = workspace.new(dir)
    ws:init()
    assert_error(function() ws:switch_branch("nope") end, "should fail")
    cleanup(dir)
end)

test("list_branches returns all branches", function()
    local dir = make_tmp()
    local ws = workspace.new(dir)
    ws:init()
    ws:branch("alpha", "purpose a")
    ws:switch_branch("main")
    ws:branch("beta", "purpose b")
    local branches = ws:list_branches()
    assert_true(#branches >= 3, "at least 3 branches")
    cleanup(dir)
end)

test("merge integrates branch into target", function()
    local dir = make_tmp()
    local ws = workspace.new(dir)
    ws:init()
    ws:branch("feature", "Add feature X")
    ws:log_ota("obs", "thought", "action")
    ws:commit("Implemented feature X")
    local merge_rec = ws:merge("feature", nil, "main")
    assert_eq(ws:current_branch(), "main", "switched to target")
    assert_true(merge_rec.commit_id ~= "", "has merge commit")
    -- Check source branch is marked merged
    local ctx = ws:context("main", 10)
    assert_true(#ctx.commits >= 1, "has merge commit on main")
    cleanup(dir)
end)

test("merge fails on non-existent source", function()
    local dir = make_tmp()
    local ws = workspace.new(dir)
    ws:init()
    assert_error(function() ws:merge("nope") end, "should fail")
    cleanup(dir)
end)

test("context returns correct data", function()
    local dir = make_tmp()
    local ws = workspace.new(dir)
    ws:init("My roadmap")
    ws:log_ota("obs1", "t1", "a1")
    ws:log_ota("obs2", "t2", "a2")
    ws:commit("Commit 1")
    ws:commit("Commit 2")
    ws:commit("Commit 3")
    local ctx = ws:context("main", 2)
    assert_eq(#ctx.commits, 2, "last 2 commits")
    assert_eq(#ctx.ota_records, 2, "all OTA records")
    assert_true(ctx.main_roadmap:find("My roadmap"), "has roadmap")
    assert_true(ctx.metadata ~= nil, "has metadata")
    assert_eq(ctx.metadata.name, "main", "metadata.name")
    cleanup(dir)
end)

test("context k larger than available commits", function()
    local dir = make_tmp()
    local ws = workspace.new(dir)
    ws:init()
    ws:commit("Only commit")
    local ctx = ws:context("main", 100)
    assert_eq(#ctx.commits, 1, "returns all available")
    cleanup(dir)
end)

test("context_summary renders markdown", function()
    local dir = make_tmp()
    local ws = workspace.new(dir)
    ws:init("Roadmap text")
    ws:log_ota("obs", "thought", "action")
    ws:commit("My contribution")
    local ctx = ws:context("main", 1)
    local summary = models.context_summary(ctx)
    assert_true(summary:find("CONTEXT"), "has CONTEXT header")
    assert_true(summary:find("Roadmap"), "has roadmap")
    assert_true(summary:find("My contribution"), "has commit")
    cleanup(dir)
end)

------------------------------------------------------------------------
print(string.format("\n%s", string.rep("=", 50)))
print(string.format("Results: %d/%d passed, %d failed", pass_count, test_count, fail_count))

if fail_count > 0 then
    os.exit(1)
end