summaryrefslogtreecommitdiff
path: root/spec/test_progress.lua
blob: a562789c72f0ee90ae17aaebd0cc6c33bc026b38 (plain)
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
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
local fake = require("spec.fake_ext")
package.loaded.panto = { text = fake.text }
local progress = require("subagents.progress")

local function board(id, collapsed)
    local component
    progress.claim({
        id = id,
        tool_name = "subagents.run",
        collapsed = collapsed,
        set_component = function(_, value)
            component = value
            return { invalidate = function() end, alive = function() return true end }
        end,
    })
    progress.bind({ tool_call_id = id })
    return component
end

local function plain(lines)
    return table.concat(lines, "\n"):gsub("\27%[[%d;]*m", "")
end

return {
    { "startup replay stays in transcript order while live calls pin", function()
        progress.reset()
        progress.begin_replay()
        local pins = {}
        local function claim(id)
            progress.claim({
                id = id,
                tool_name = "subagents.workflow",
                set_component = function()
                    return {
                        alive = function() return true end,
                        invalidate = function() end,
                        set_pinned = function(_, value) pins[#pins + 1] = value end,
                    }
                end,
            })
        end
        claim("historical")
        assert(#pins == 0, "replayed workflows must never pin")
        progress.begin_live_turn()
        claim("live")
        assert(pins[1] == true, "a live workflow pins while it runs")
        progress.reset()
        assert(pins[2] == false, "reset releases the live workflow")
    end },

    { "concurrent boards pin independently until their matching result", function()
        progress.reset()
        local pins = { a = {}, b = {} }
        local function claim(id)
            progress.claim({
                id = id,
                tool_name = "subagents.workflow",
                set_component = function()
                    return {
                        alive = function() return true end,
                        invalidate = function() end,
                        set_pinned = function(_, value)
                            pins[id][#pins[id] + 1] = value
                        end,
                    }
                end,
            })
        end
        claim("a")
        claim("b")
        assert(pins.a[1] == true and pins.b[1] == true, "each invocation pins its own handle")
        progress.settle({ id = "a" })
        assert(pins.a[2] == false, "the matching result restores transcript order")
        assert(#pins.b == 1, "another in-flight workflow remains pinned")
        progress.settle({ id = "missing" })
        assert(#pins.b == 1, "an unrelated or stale result is inert")
        progress.reset()
        assert(pins.b[2] == false, "reset safely releases unresolved boards")
    end },

    { "background workflows keep their board pinned after the outer tool settles", function()
        progress.reset()
        progress.begin_live_turn()
        local component, pins
        progress.claim({
            id = "background-call",
            tool_name = "subagents.lua",
            set_component = function(_, value)
                component, pins = value, {}
                return {
                    alive = function() return true end,
                    invalidate = function() end,
                    set_pinned = function(_, value) pins[#pins + 1] = value end,
                }
            end,
        })

        progress.bind({ tool_call_id = "background-call" })
        progress.card("worker", "child")
        progress.workflow_started("background-call")
        progress.settle({ id = "background-call" })
        progress.reset()
        assert(#pins == 1 and pins[1] == true, "turn boundaries must not unpin a running workflow")
        assert(component:render(80)[#component:render(80)] == "", "a running workflow retains its spacing")

        progress.workflow_finished("background-call")
        assert(#pins == 2 and pins[2] == false, "the board unpins when its final workflow settles")
        progress.reset()
    end },

    { "a pinned board leaves one blank line before the waiting indicator", function()
        progress.reset()
        local component, pins
        progress.claim({
            id = "spacing-call",
            tool_name = "subagents.workflow",
            collapsed = true,
            set_component = function(_, value)
                component = value
                pins = {}
                return {
                    alive = function() return true end,
                    invalidate = function() end,
                    set_pinned = function(_, value) pins[#pins + 1] = value end,
                }
            end,
        })
        progress.bind({ tool_call_id = "spacing-call" })
        local card = progress.card("worker", "child")
        card:event({ type = "block_start", block_type = "text", index = 1 })
        card:event({ type = "content_delta", index = 1, delta = "final line" })

        local compact = component:render(80)
        assert(pins[1] == true, "the board pins before it renders")
        assert(compact[#compact] == "", "pinned compact boards end with a blank line")
        assert(compact[#compact - 1]:find("final line", 1, true), "the gap follows the final compact line")

        progress.collapse({ collapsed = false })
        local expanded = component:render(80)
        assert(expanded[#expanded] == "", "pinned expanded boards end with a blank line")
        assert(expanded[#expanded - 1]:find("final line", 1, true), "the gap follows the final expanded line")

        progress.settle({ id = "spacing-call" })
        local historical = component:render(80)
        assert(historical[#historical] ~= "", "settled boards do not keep the spacing line")
        assert(pins[2] == false, "settling unpins the board")
        progress.reset()
    end },

    { "failed pin transitions do not change rendered spacing state", function()
        progress.reset()
        local component, pins, fail_unpin = nil, {}, true
        progress.claim({
            id = "spacing-lifecycle",
            tool_name = "subagents.run",
            set_component = function(_, value)
                component = value
                return {
                    alive = function() return true end,
                    invalidate = function() end,
                    set_pinned = function(_, value)
                        pins[#pins + 1] = value
                        if value == false and fail_unpin then error("cannot unpin") end
                    end,
                }
            end,
        })
        progress.bind({ tool_call_id = "spacing-lifecycle" })
        local card = progress.card("worker", "child")
        card:event({ type = "block_start", block_type = "text", index = 1 })
        card:event({ type = "content_delta", index = 1, delta = "final line" })
        local active = component:render(80)
        assert(active[#active] == "", "a successful pin enables the gap")

        progress.settle({ id = "spacing-lifecycle" })
        local still_pinned = component:render(80)
        assert(still_pinned[#still_pinned] == "", "a failed unpin keeps the board active")
        fail_unpin = false
        progress.settle({ id = "spacing-lifecycle" })
        local unpinned = component:render(80)
        assert(unpinned[#unpinned] ~= "", "a successful unpin removes the gap")
        assert(#pins == 3 and pins[1] == true and pins[2] == false and pins[3] == false,
            "unpin retries only after the failed lifecycle transition")
        progress.reset()
    end },

    { "concurrent UUIDv7 cards across boards show distinguishable id prefixes", function()
        progress.reset()
        local first_board = board("call-1", true)
        local first = "0198aaaa-aaaa-7aaa-8aaa-aaaaaaaaaaaa"
        progress.card("worker", first)
        local initial_line = first_board:render(120)[2]
        assert(initial_line:find("0198aaaa…", 1, true), initial_line)

        local second_board = board("call-2", true)
        local second = "0198aaaa-aaaa-7aab-8aaa-aaaaaaaaaaaa"
        progress.card("worker", second)

        local first_line = first_board:render(120)[2]
        local second_line = second_board:render(120)[2]
        assert(first_line ~= second_line, "distinct session ids must not render identical card headers")
        assert(first_line:find("0198aaaa%-aaaa%-7aaa"), first_line)
        assert(second_line:find("0198aaaa%-aaaa%-7aab"), second_line)
        progress.reset()
    end },

    { "finished cards sort above running cards in creation order", function()
        progress.reset()
        local component = board("call-sorting", true)
        local alpha = progress.card("alpha", "a")
        local beta = progress.card("beta", "b")
        local gamma = progress.card("gamma", "c")
        local delta = progress.card("delta", "d")

        gamma:done("completed")
        alpha:done("completed")
        delta:done("failed", "boom")

        local text = plain(component:render(80))
        local alpha_at = assert(text:find("alpha", 1, true))
        local gamma_at = assert(text:find("gamma", 1, true))
        local delta_at = assert(text:find("delta", 1, true))
        local beta_at = assert(text:find("beta", 1, true))
        assert(alpha_at < gamma_at and gamma_at < delta_at and delta_at < beta_at, text)
        progress.reset()
    end },

    { "Ctrl+O state expands and recollapses every existing board", function()
        progress.reset()
        local first = board("call-a", true)
        local a = progress.card("alpha", "a")
        for i = 1, 8 do
            a:event({ type = "block_start", block_type = "text", index = i })
            a:event({ type = "content_delta", index = i, delta = "line-" .. i .. "\n" })
            a:event({ type = "block_complete", block_type = "text", index = i, text = "line-" .. i })
        end
        local second = board("call-b", true)
        local b = progress.card("beta", "b")
        for i = 1, 8 do b:event({ type = "content_delta", index = 99, delta = "ignored" }) end
        for i = 1, 8 do
            b:event({ type = "block_start", block_type = "text", index = i })
            b:event({ type = "content_delta", index = i, delta = "beta-" .. i .. "\n" })
        end

        assert(#first:render(80) == 6, "blank + header + at most four recent lines")
        assert(#second:render(80) == 6, "each board is independently collapsed")
        progress.collapse({ collapsed = false })
        assert(#first:render(80) == 10, "expanded board should retain all eight lines")
        assert(#second:render(80) == 10, "global state should expand concurrent boards")
        progress.collapse({ collapsed = true })
        assert(#first:render(80) == 6 and #second:render(80) == 6)
        progress.reset()
    end },

    { "settled historical boards keep responding to Ctrl+O until their handles die", function()
        progress.reset()
        local alive = { a = true, b = true }
        local invalidations = { a = 0, b = 0 }
        local components = {}
        for _, id in ipairs({ "a", "b" }) do
            progress.claim({
                id = id,
                tool_name = "subagents.run",
                collapsed = true,
                set_component = function(_, component)
                    components[id] = component
                    return {
                        alive = function() return alive[id] end,
                        invalidate = function() invalidations[id] = invalidations[id] + 1 end,
                        set_pinned = function() end,
                    }
                end,
            })
            progress.bind({ tool_call_id = id })
            local card = progress.card(id, id)
            for i = 1, 6 do card:event({ type = "block_start", block_type = "text", index = i })
                card:event({ type = "content_delta", index = i, delta = id .. i .. "\n" }) end
            progress.settle({ id = id })
        end

        progress.reset() -- turn_end clears coroutine bindings, not historical components
        progress.collapse({ collapsed = false })
        assert(#components.a:render(80) == 8 and #components.b:render(80) == 8,
            "settled concurrent boards should both expand after turn_end")
        alive.a = false
        local a_invalidations = invalidations.a
        progress.collapse({ collapsed = true })
        assert(invalidations.a == a_invalidations, "dead historical handles should be pruned")
        assert(#components.b:render(80) == 6, "a live concurrent board should remain isolated")
        alive.b = false
        progress.reset()
    end },

    { "prompts are presentation-only in expanded mode and bounded", function()
        progress.reset()
        local component = board("call-prompts", true)
        local card = progress.card("inline", "child", nil, {
            prompt = "inspect this\nthen summarize",
            system_prompt = "INLINE-SYSTEM " .. string.rep("x", 64 * 1024),
        })
        card:event({ type = "block_start", block_type = "text", index = 1 })
        card:event({ type = "content_delta", index = 1, delta = "assistant line\n" })

        local compact = plain(component:render(100))
        assert(compact:find("assistant line", 1, true), compact)
        assert(not compact:find("inspect this", 1, true), compact)
        assert(not compact:find("INLINE-SYSTEM", 1, true), compact)

        progress.collapse({ collapsed = false })
        local expanded = plain(component:render(100))
        assert(expanded:find("system prompt: INLINE-SYSTEM", 1, true), expanded)
        assert(expanded:find("prompt: inspect this", 1, true), expanded)
        assert(expanded:find("then summarize", 1, true), expanded)
        assert(#card.system_prompt == 32 * 1024, "presentation metadata must be bounded")
        assert(#card.history == 1, "presentation metadata must not enter accumulated output history")
        progress.reset()
    end },

    { "subagents.lua boards always show the executed source with real newlines", function()
        if not pcall(require, "dkjson") then return "skip", "dkjson is not installed" end
        progress.reset()
        local source = "local w = subagents.workflows[\"workflow-1\"]\n\tif w then\n\t\treturn w.status\n\tend\nreturn \"gone\""
        local component
        progress.claim({
            id = "call-source",
            tool_name = "subagents.lua",
            collapsed = true,
            input = require("dkjson").encode({ source = source }),
            set_component = function(_, value)
                component = value
                return { invalidate = function() end, alive = function() return true end }
            end,
        })
        progress.bind({ tool_call_id = "call-source" })

        -- An inspection call starts no children, so the source is the only
        -- thing this entry can show.
        local collapsed = component:render(100)
        assert(collapsed[2]:find("subagents.lua", 1, true), plain(collapsed))
        assert(collapsed[3] == "    local w = subagents.workflows[\"workflow-1\"]", collapsed[3])
        assert(collapsed[4] == "      if w then", collapsed[4])
        assert(collapsed[#collapsed] == "    \226\128\166", "collapsed source is capped")
        assert(#collapsed == 2 + 4 + 1, plain(collapsed))

        progress.collapse({ collapsed = false })
        local expanded = plain(component:render(100))
        assert(expanded:find("return \"gone\"", 1, true), expanded)
        assert(not expanded:find("\\n", 1, true), "newlines must render as newlines")
        progress.reset()
    end },

    { "expanded subagents.lua source is never truncated", function()
        if not pcall(require, "dkjson") then return "skip", "dkjson is not installed" end
        progress.reset()
        local lines = {}
        for i = 1, 600 do lines[i] = "local v" .. i .. " = " .. i end
        lines[#lines + 1] = "return 'last-line'"
        local component
        progress.claim({
            id = "call-long-source",
            tool_name = "subagents.lua",
            collapsed = false,
            input = require("dkjson").encode({ source = table.concat(lines, "\n") }),
            set_component = function(_, value)
                component = value
                return { invalidate = function() end, alive = function() return true end }
            end,
        })
        local rendered = component:render(100)
        assert(rendered[#rendered] == "    return 'last-line'", rendered[#rendered])
        assert(#rendered == 2 + #lines, #rendered)
        progress.reset()
    end },

    { "expanded history includes streamed assistant text and available tool call fields", function()
        progress.reset()
        local component = board("call-tools", false)
        local card = progress.card("worker", "child")
        card:event({ type = "block_start", block_type = "text", index = 0 })
        card:event({ type = "content_delta", index = 0, delta = "assistant output" })
        card:event({ type = "block_complete", block_type = "text", index = 0, text = "assistant output" })
        card:event({ type = "block_start", block_type = "tool_use", index = 1 })
        card:event({ type = "tool_details", index = 1, id = "tool-7", name = "std.echo" })
        card:event({ type = "block_complete", block_type = "tool_use", index = 1,
            id = "tool-7", name = "std.echo", text = '{"text":"hello"}' })
        card:event({ type = "tool_dispatch_result", tool_results = {
            { tool_use_id = "tool-7", output = "hello\nworld", is_error = false },
            { tool_use_id = "tool-8", output = "boom", is_error = true },
        } })

        local text = plain(component:render(80))
        assert(text:find("assistant output", 1, true), text)
        assert(text:find("std.echo [tool-7]", 1, true), text)
        assert(text:find('input: {"text":"hello"}', 1, true), text)
        assert(text:find("result [tool-7]: hello", 1, true), text)
        assert(text:find("                 world", 1, true), text)
        assert(text:find("error [tool-8]: boom", 1, true), text)
        progress.reset()
    end },

    { "tool names use internal dotted spelling in progress labels", function()
        progress.reset()
        local component = board("call-tool-names", false)
        local card = progress.card("worker", "child")
        card:event({ type = "tool_details", index = 1, id = "wire", name = "std__shell" })
        card:event({ type = "tool_details", index = 2, id = "internal", name = "std.read" })
        card:event({ type = "block_complete", block_type = "tool_use", index = 3,
            id = "complete", name = "web__fetch" })

        local text = plain(component:render(80))
        assert(text:find("std.shell [wire]", 1, true), text)
        assert(text:find("std.read [internal]", 1, true), text)
        assert(text:find("web.fetch [complete]", 1, true), text)
        assert(not text:find("std__shell", 1, true), text)
        progress.reset()
    end },

    { "pathological single-line payloads are bounded before history ingestion", function()
        progress.reset()
        local component = board("call-huge", false)
        local card = progress.card("worker", "huge")
        local huge = "head\0" .. string.rep("x", 2 * 1024 * 1024)
        card:event({ type = "block_start", block_type = "text", index = 1 })
        card:event({ type = "content_delta", index = 1, delta = huge })
        card:event({ type = "tool_dispatch_result", tool_results = {
            { tool_use_id = "huge-tool", output = huge },
        } })

        assert(#card.history == 2)
        assert(#card.history[1] <= 4096 and #card.history[2] <= 4096,
            "assistant and tool lines must be bounded at ingestion")
        local text = plain(component:render(120))
        assert(text:find("head x", 1, true), "ordinary control sanitization should be preserved")
        progress.reset()
    end },

    { "rendering sanitizes controls, stays width-bound, and caps retained history", function()
        progress.reset()
        local component = board("call-bounds", false)
        local card = progress.card("bad\27label", "child")
        for i = 1, 600 do
            card:event({ type = "block_start", block_type = "text", index = i })
            card:event({ type = "content_delta", index = i,
                delta = string.format("history-%03d-abcdefghijklmnopqrstuvwxyz\n", i) })
        end
        local lines = component:render(16)
        local text = plain(lines)
        assert(not text:find("\27", 1, true), "control bytes must not reach the renderer")
        assert(not text:find("history%-001"), "old history should be evicted")
        assert(text:find("history%-600"), "recent history should remain")
        assert(#lines <= 4098, "rendering itself must remain bounded")
        for _, line in ipairs(lines) do
            assert(utf8.len(line) <= 16, "line exceeded component width: " .. line)
        end
        progress.reset()
    end },
}