switchroom 0.19.26 → 0.19.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/git-agent-attribution-hook.sh +144 -0
- package/dist/agent-scheduler/index.js +60 -2
- package/dist/auth-broker/index.js +244 -13
- package/dist/cli/autoaccept-poll.js +225 -17
- package/dist/cli/notion-write-pretool.mjs +60 -2
- package/dist/cli/switchroom.js +2843 -1220
- package/dist/host-control/main.js +245 -14
- package/dist/vault/approvals/kernel-server.js +242 -13
- package/dist/vault/broker/server.js +242 -13
- package/package.json +7 -2
- package/profiles/_base/cron-session.sh.hbs +8 -0
- package/profiles/_base/start.sh.hbs +175 -15
- package/telegram-plugin/card-layout.ts +328 -0
- package/telegram-plugin/dist/bridge/bridge.js +94 -1
- package/telegram-plugin/dist/gateway/gateway.js +2544 -1182
- package/telegram-plugin/dist/server.js +97 -1
- package/telegram-plugin/edit-flood-fuse.ts +841 -57
- package/telegram-plugin/flood-429-ledger.ts +526 -0
- package/telegram-plugin/flood-circuit-breaker.ts +18 -0
- package/telegram-plugin/gateway/callback-query-handlers.ts +6 -0
- package/telegram-plugin/gateway/flood-reply-queue.ts +168 -0
- package/telegram-plugin/gateway/gateway.ts +67 -70
- package/telegram-plugin/gateway/mcp-failure-hook.ts +74 -0
- package/telegram-plugin/gateway/narrative-lane.ts +14 -0
- package/telegram-plugin/gateway/outbound-send-path.ts +36 -0
- package/telegram-plugin/gateway/outbox-sweep.ts +183 -6
- package/telegram-plugin/gateway/pinned-message-handler.ts +12 -16
- package/telegram-plugin/gateway/status-pin-retarget.ts +72 -36
- package/telegram-plugin/gateway/status-pin-store.ts +58 -9
- package/telegram-plugin/gateway/worker-pin-reaper.ts +56 -7
- package/telegram-plugin/inline-keyboard-callbacks.ts +202 -21
- package/telegram-plugin/llm-error-present.ts +61 -2
- package/telegram-plugin/mcp-credential-failure.ts +459 -0
- package/telegram-plugin/model-unavailable.ts +8 -0
- package/telegram-plugin/operator-events.ts +110 -5
- package/telegram-plugin/outbound-class.ts +81 -0
- package/telegram-plugin/provider-credit.ts +237 -0
- package/telegram-plugin/scripts/bun-test-ci.sh +36 -6
- package/telegram-plugin/send-gate.ts +24 -2
- package/telegram-plugin/status-no-truncate.ts +10 -48
- package/telegram-plugin/status-pin-driver.ts +33 -45
- package/telegram-plugin/status-pin.ts +18 -1
- package/telegram-plugin/tests/card-golden.test.ts +69 -0
- package/telegram-plugin/tests/card-lifecycle-render.test.ts +362 -0
- package/telegram-plugin/tests/card-type-distinguishability.test.ts +187 -164
- package/telegram-plugin/tests/card-variants.golden.txt +211 -0
- package/telegram-plugin/tests/card-variants.ts +366 -0
- package/telegram-plugin/tests/edit-flood-fuse-ban-awareness.test.ts +373 -0
- package/telegram-plugin/tests/edit-flood-fuse-default-deny.test.ts +319 -0
- package/telegram-plugin/tests/edit-flood-fuse-reply-reserve.test.ts +340 -0
- package/telegram-plugin/tests/edit-flood-fuse.test.ts +11 -2
- package/telegram-plugin/tests/feed-edit-rate-ceiling.test.ts +462 -0
- package/telegram-plugin/tests/finalize-callback-flood-policy.test.ts +298 -0
- package/telegram-plugin/tests/finalize-callback.test.ts +41 -8
- package/telegram-plugin/tests/fixtures/real-429-stream.ts +220 -0
- package/telegram-plugin/tests/flood-429-ledger.test.ts +278 -0
- package/telegram-plugin/tests/flood-429-recorder-wiring.test.ts +128 -0
- package/telegram-plugin/tests/flood-reply-queue.test.ts +418 -0
- package/telegram-plugin/tests/mcp-credential-failure.test.ts +310 -0
- package/telegram-plugin/tests/outbox-sweep-flood-breaker.test.ts +221 -0
- package/telegram-plugin/tests/pinned-card-collapse.test.ts +19 -24
- package/telegram-plugin/tests/pinned-message-handler.test.ts +15 -15
- package/telegram-plugin/tests/provider-credit-402.test.ts +243 -0
- package/telegram-plugin/tests/status-pin-api.test.ts +11 -11
- package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +36 -37
- package/telegram-plugin/tests/status-pin-lifecycle.test.ts +602 -0
- package/telegram-plugin/tests/status-pin-retarget.test.ts +90 -62
- package/telegram-plugin/tests/status-pin-service-message-suppression.test.ts +7 -3
- package/telegram-plugin/tests/status-pin-store.test.ts +109 -60
- package/telegram-plugin/tests/status-pin.test.ts +56 -5
- package/telegram-plugin/tests/test-runner-coverage.test.ts +133 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +12 -10
- package/telegram-plugin/tests/worker-feed-coalesce.test.ts +23 -29
- package/telegram-plugin/tests/worker-feed-pin-persistence.test.ts +56 -59
- package/telegram-plugin/tests/worker-feed-terminal-edit-class.test.ts +335 -0
- package/telegram-plugin/tests/worker-visibility-prose-silent-harness.test.ts +1 -1
- package/telegram-plugin/tool-activity-summary.ts +239 -365
- package/telegram-plugin/uat/assertions.ts +22 -11
- package/telegram-plugin/uat/feed-matcher.test.ts +24 -17
- package/telegram-plugin/worker-activity-feed.ts +105 -47
- package/vendor/hindsight-memory/CLAUDE.md +45 -0
- package/vendor/hindsight-memory/scripts/drain_pending.py +433 -11
- package/vendor/hindsight-memory/scripts/lib/config.py +33 -0
- package/vendor/hindsight-memory/scripts/lib/pending.py +193 -28
- package/vendor/hindsight-memory/scripts/recall.py +176 -7
- package/vendor/hindsight-memory/scripts/tests/test_config_recall_passthrough_env.py +170 -0
- package/vendor/hindsight-memory/scripts/tests/test_drain_circuit_breaker.py +401 -0
- package/vendor/hindsight-memory/scripts/tests/test_drain_serialisation.py +286 -0
- package/vendor/hindsight-memory/scripts/tests/test_pending_drops.py +817 -8
- package/vendor/hindsight-memory/scripts/tests/test_recall_min_score.py +464 -0
- package/vendor/hindsight-memory/settings.json +1 -1
- package/vendor/hindsight-memory/tests/test_hooks.py +11 -2
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The catalogue of EVERY status/progress card variant the gateway can paint,
|
|
3
|
+
* each rendered through the REAL renderer with realistic inputs.
|
|
4
|
+
*
|
|
5
|
+
* One catalogue, two consumers, so they can never disagree:
|
|
6
|
+
* - `telegram-plugin/tests/card-golden.test.ts` — the golden suite. Every
|
|
7
|
+
* variant's exact wire text is pinned in `card-variants.golden.txt`, so a
|
|
8
|
+
* change to the shared card layout that alters ANY card fails loudly.
|
|
9
|
+
* - `scripts/card-previews.ts` — the human-readable preview doc.
|
|
10
|
+
*
|
|
11
|
+
* Adding a card variant means adding it HERE (and regenerating the golden with
|
|
12
|
+
* `UPDATE_CARD_GOLDEN=1 npx vitest run telegram-plugin/tests/card-golden.test.ts`),
|
|
13
|
+
* which is what keeps the golden suite exhaustive rather than a sample.
|
|
14
|
+
*
|
|
15
|
+
* These are RENDERER-level fixtures. The lifecycle behaviour that decides WHICH
|
|
16
|
+
* variant gets painted (dispatch → running → 2 workers → spill → done → failed
|
|
17
|
+
* → superseded) is covered end-to-end from the feed manager in
|
|
18
|
+
* `card-lifecycle-render.test.ts`; the two suites are complementary, not
|
|
19
|
+
* duplicates.
|
|
20
|
+
*/
|
|
21
|
+
import {
|
|
22
|
+
renderActivityFeed,
|
|
23
|
+
renderActivityFeedWithNested,
|
|
24
|
+
renderCombinedWorkerFeed,
|
|
25
|
+
type CombinedWorkerRow,
|
|
26
|
+
type SessionActivityHeader,
|
|
27
|
+
} from '../tool-activity-summary.js'
|
|
28
|
+
import {
|
|
29
|
+
renderWorkerActivity,
|
|
30
|
+
WORKER_CARD_SUPERSEDED_BODY,
|
|
31
|
+
type WorkerActivityView,
|
|
32
|
+
} from '../worker-activity-feed.js'
|
|
33
|
+
|
|
34
|
+
/** Which renderer path a variant exercises, for the preview doc's inventory. */
|
|
35
|
+
export interface CardVariant {
|
|
36
|
+
/** Stable identity — the golden file keys on this, so renaming rewrites it. */
|
|
37
|
+
name: string
|
|
38
|
+
/** The call path, for the preview doc. */
|
|
39
|
+
fn: string
|
|
40
|
+
/** When a user actually sees this card. */
|
|
41
|
+
when: string
|
|
42
|
+
/**
|
|
43
|
+
* How the variant reaches the wire:
|
|
44
|
+
* - `shared` — composed by the one card layout core (`card-layout.ts`).
|
|
45
|
+
* - `constant` — a static notice with no live state to render (see the
|
|
46
|
+
* superseded card); its title line still comes from the shared composer.
|
|
47
|
+
*/
|
|
48
|
+
layout?: 'shared' | 'constant'
|
|
49
|
+
/** The real render. `null` means the renderer declined to emit anything. */
|
|
50
|
+
render: () => string | null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const agentHeader = (over: Partial<SessionActivityHeader> = {}): SessionActivityHeader => ({
|
|
54
|
+
label: 'Agent',
|
|
55
|
+
elapsedMs: 95_000,
|
|
56
|
+
toolCount: 12,
|
|
57
|
+
state: 'running',
|
|
58
|
+
model: 'claude-opus-4-8',
|
|
59
|
+
totalTokens: 41_200,
|
|
60
|
+
...over,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const AGENT_STEPS = [
|
|
64
|
+
'Searching memory for card render paths',
|
|
65
|
+
'Reading telegram-plugin/tool-activity-summary.ts',
|
|
66
|
+
'Reading telegram-plugin/worker-activity-feed.ts',
|
|
67
|
+
'Grepping for renderStatusCard callers',
|
|
68
|
+
'Drafting the inventory table',
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
const workerView = (over: Partial<WorkerActivityView> = {}): WorkerActivityView => ({
|
|
72
|
+
description: 'Audit every card render path and de-indent the worker card',
|
|
73
|
+
state: 'running',
|
|
74
|
+
elapsedMs: 143_000,
|
|
75
|
+
toolCount: 31,
|
|
76
|
+
lastTool: null,
|
|
77
|
+
latestSummary: '',
|
|
78
|
+
narrativeLines: [
|
|
79
|
+
'Fetching origin and branching off main',
|
|
80
|
+
'Reading telegram-plugin/status-no-truncate.ts',
|
|
81
|
+
'Mapping renderStatusCard callers',
|
|
82
|
+
'Writing the preview harness',
|
|
83
|
+
],
|
|
84
|
+
model: 'claude-opus-4-8',
|
|
85
|
+
totalTokens: 88_400,
|
|
86
|
+
...over,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const row = (
|
|
90
|
+
n: number,
|
|
91
|
+
desc: string,
|
|
92
|
+
hist: string[],
|
|
93
|
+
over: Partial<CombinedWorkerRow> = {},
|
|
94
|
+
): CombinedWorkerRow => ({
|
|
95
|
+
ordinal: n,
|
|
96
|
+
description: desc,
|
|
97
|
+
elapsedMs: 60_000 * n + 15_000,
|
|
98
|
+
toolCount: 7 * n,
|
|
99
|
+
totalTokens: 20_000 * n,
|
|
100
|
+
currentStep: hist[hist.length - 1] ?? 'starting…',
|
|
101
|
+
historyLines: hist,
|
|
102
|
+
model: 'claude-opus-4-8',
|
|
103
|
+
...over,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const H = (k: number, tag: string): string[] =>
|
|
107
|
+
Array.from({ length: k }, (_, i) => `${tag} step ${i + 1}`)
|
|
108
|
+
|
|
109
|
+
/** A 700-char step — long enough to blow the per-line clip and force the backstop. */
|
|
110
|
+
const LONG = 'A'.repeat(700)
|
|
111
|
+
|
|
112
|
+
export const CARD_VARIANTS: CardVariant[] = [
|
|
113
|
+
{
|
|
114
|
+
name: 'Agent card — running',
|
|
115
|
+
fn: 'renderActivityFeed → renderStatusCard',
|
|
116
|
+
when: 'Every turn that labels a tool. Pinned in the session chat, edited in place.',
|
|
117
|
+
render: () => renderActivityFeed(AGENT_STEPS, false, '', undefined, agentHeader()),
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'Agent card — final (turn complete)',
|
|
121
|
+
fn: 'renderActivityFeed → renderStatusCard',
|
|
122
|
+
when: 'The terminal edit when the turn ends: every step struck, `✓ N steps` footer.',
|
|
123
|
+
render: () =>
|
|
124
|
+
renderActivityFeed(AGENT_STEPS, true, '', 17, agentHeader({ state: 'done', elapsedMs: 214_000 })),
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'Agent card — steps only (no header)',
|
|
128
|
+
fn: 'renderActivityFeed → renderStatusCard (header omitted)',
|
|
129
|
+
when: 'Legacy/direct callers (`appendActivityLine`, `appendActivityLabel`) that pass no header.',
|
|
130
|
+
render: () => renderActivityFeed(AGENT_STEPS.slice(0, 3)),
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'Agent card — with nested foreground sub-agent',
|
|
134
|
+
fn: 'renderActivityFeedWithNested → renderStatusCard (childSteps)',
|
|
135
|
+
when: 'A foreground Task/Agent runs inside the turn: parent lines all struck, the live `→` sits in the `↳` child block.',
|
|
136
|
+
render: () =>
|
|
137
|
+
renderActivityFeedWithNested(
|
|
138
|
+
AGENT_STEPS.slice(0, 3),
|
|
139
|
+
['Reading the repo conventions', 'Running the scoped test suite', 'Summarising findings'],
|
|
140
|
+
false,
|
|
141
|
+
' · 24s',
|
|
142
|
+
undefined,
|
|
143
|
+
agentHeader(),
|
|
144
|
+
),
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'Agent card — liveness early-open (no tools yet)',
|
|
148
|
+
fn: 'narrative-lane.ts → renderActivityFeedWithNested → renderStatusCard',
|
|
149
|
+
when: 'A silent turn passes the liveness threshold before any tool is labelled — the card opens on `Working…`.',
|
|
150
|
+
render: () =>
|
|
151
|
+
renderActivityFeedWithNested(['Working…'], [], false, ' · 32s', undefined, {
|
|
152
|
+
label: 'Agent',
|
|
153
|
+
elapsedMs: 32_000,
|
|
154
|
+
toolCount: 0,
|
|
155
|
+
state: 'running',
|
|
156
|
+
model: 'claude-opus-4-8',
|
|
157
|
+
}),
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'Agent card — liveness finalised',
|
|
161
|
+
fn: 'narrative-lane.ts → renderActivityFeedWithNested → renderStatusCard',
|
|
162
|
+
when: 'The liveness-only card at turn end: `→ Working…` becomes a done record instead of freezing live.',
|
|
163
|
+
render: () =>
|
|
164
|
+
renderActivityFeedWithNested(['Working…'], [], true, '', undefined, {
|
|
165
|
+
label: 'Agent',
|
|
166
|
+
elapsedMs: 88_000,
|
|
167
|
+
toolCount: 0,
|
|
168
|
+
state: 'done',
|
|
169
|
+
model: 'claude-opus-4-8',
|
|
170
|
+
}),
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: 'Agent card — post-answer background activity',
|
|
174
|
+
fn: 'narrative-lane.ts → renderActivityFeedWithNested → renderStatusCard',
|
|
175
|
+
when: 'The reply already went out and a background sub-agent is still running — the card surfaces below the reply.',
|
|
176
|
+
render: () =>
|
|
177
|
+
renderActivityFeedWithNested(['Working in background…'], [], false, ' · 1m10s', undefined, {
|
|
178
|
+
label: 'Agent',
|
|
179
|
+
elapsedMs: 130_000,
|
|
180
|
+
toolCount: 3,
|
|
181
|
+
state: 'running',
|
|
182
|
+
model: 'claude-opus-4-8',
|
|
183
|
+
}),
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: 'Agent card — over-long steps (per-line clip)',
|
|
187
|
+
fn: 'renderStatusCard → cleanStepLine (STATUS_LINE_MAX)',
|
|
188
|
+
when: 'A step line far exceeds STATUS_LINE_MAX: each is clipped to 200 chars + `…` before escaping. (This is also why the card-wide `fitCardToBudget` backstop is unreachable on the agent card — 5 clipped lines cannot approach 32768. The shrink levels are genuinely exercised by the WORKERS spill variant.)',
|
|
189
|
+
render: () =>
|
|
190
|
+
renderActivityFeed(
|
|
191
|
+
[LONG, LONG, LONG, 'Finally a short newest step'],
|
|
192
|
+
false,
|
|
193
|
+
'',
|
|
194
|
+
undefined,
|
|
195
|
+
agentHeader(),
|
|
196
|
+
),
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: 'Worker card — just dispatched (no steps yet)',
|
|
200
|
+
fn: 'renderWorkerActivity → renderStatusCard (emptyPlaceholder)',
|
|
201
|
+
when: 'The first paint of a background worker, before it has produced a narrative line.',
|
|
202
|
+
render: () =>
|
|
203
|
+
renderWorkerActivity(
|
|
204
|
+
workerView({ narrativeLines: [], latestSummary: '', elapsedMs: 1_200, toolCount: 0 }),
|
|
205
|
+
),
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: 'Worker card — single worker running',
|
|
209
|
+
fn: 'renderWorkerActivity → renderStatusCard',
|
|
210
|
+
when: 'Exactly one background worker live in the chat. Pinned; edited in place.',
|
|
211
|
+
render: () => renderWorkerActivity(workerView(), ' · 18s'),
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'Worker card — done (with result recap)',
|
|
215
|
+
fn: 'renderWorkerActivity → renderStatusCard (result block)',
|
|
216
|
+
when: 'The worker finished successfully: feed all-struck, rule, then the ✅ result paragraph.',
|
|
217
|
+
render: () =>
|
|
218
|
+
renderWorkerActivity(
|
|
219
|
+
workerView({
|
|
220
|
+
state: 'done',
|
|
221
|
+
elapsedMs: 902_000,
|
|
222
|
+
latestSummary:
|
|
223
|
+
'Inventoried 14 card variants, found renderCombinedWorkerFeed forks the header + budget logic, and de-indented the worker card. PR #0000.',
|
|
224
|
+
}),
|
|
225
|
+
),
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'Worker card — failed',
|
|
229
|
+
fn: 'renderWorkerActivity → renderStatusCard (result block, ⚠️)',
|
|
230
|
+
when: 'The worker errored: header line 2 reads `failed`, result block carries the error recap.',
|
|
231
|
+
render: () =>
|
|
232
|
+
renderWorkerActivity(
|
|
233
|
+
workerView({
|
|
234
|
+
state: 'failed',
|
|
235
|
+
elapsedMs: 61_000,
|
|
236
|
+
latestSummary: 'Scoped test run failed: 3 assertions in card-type-distinguishability.test.ts.',
|
|
237
|
+
}),
|
|
238
|
+
),
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: 'Worker card — incomplete (killed / timed out)',
|
|
242
|
+
fn: 'renderWorkerActivity → renderStatusCard (result suppressed by invariant)',
|
|
243
|
+
when: 'The worker was terminated before producing a result — the renderer refuses to fabricate a result block.',
|
|
244
|
+
render: () =>
|
|
245
|
+
renderWorkerActivity(
|
|
246
|
+
workerView({
|
|
247
|
+
state: 'incomplete',
|
|
248
|
+
elapsedMs: 1_805_000,
|
|
249
|
+
latestSummary: 'stray text that must not render',
|
|
250
|
+
}),
|
|
251
|
+
),
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: 'WORKERS card — 2 workers',
|
|
255
|
+
fn: 'renderCombinedWorkerFeed → the shared card spec',
|
|
256
|
+
when: '2+ background workers live in the same chat/thread coalesce into ONE message. Depth = 5 each.',
|
|
257
|
+
render: () =>
|
|
258
|
+
renderCombinedWorkerFeed(
|
|
259
|
+
[
|
|
260
|
+
row(1, 'Audit every card render path', H(5, 'audit')),
|
|
261
|
+
row(2, 'Fix the hindsight recall floor', H(5, 'recall')),
|
|
262
|
+
],
|
|
263
|
+
{ maxRows: 6 },
|
|
264
|
+
),
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
name: 'WORKERS card — 4 workers',
|
|
268
|
+
fn: 'renderCombinedWorkerFeed → the shared card spec',
|
|
269
|
+
when: "Depth degrades to the floor of Ken's 6/5/4/3 curve.",
|
|
270
|
+
render: () =>
|
|
271
|
+
renderCombinedWorkerFeed(
|
|
272
|
+
[
|
|
273
|
+
row(1, 'Audit every card render path', H(4, 'audit')),
|
|
274
|
+
row(2, 'Fix the hindsight recall floor', H(4, 'recall')),
|
|
275
|
+
row(3, 'Release v0.19.24', H(4, 'release')),
|
|
276
|
+
row(4, 'Investigate the flaky UAT scenario', H(4, 'uat')),
|
|
277
|
+
],
|
|
278
|
+
{ maxRows: 6 },
|
|
279
|
+
),
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: 'WORKERS card — 8 workers (spill)',
|
|
283
|
+
fn: 'renderCombinedWorkerFeed → fitCardToBudget (row-count shrink levels)',
|
|
284
|
+
when: 'Beyond the design fan-out the NEWEST rows collapse into a single `+M more working…` line.',
|
|
285
|
+
render: () =>
|
|
286
|
+
renderCombinedWorkerFeed(
|
|
287
|
+
Array.from({ length: 8 }, (_, i) => row(i + 1, `Background task number ${i + 1}`, H(3, `t${i + 1}`))),
|
|
288
|
+
{ maxRows: 6 },
|
|
289
|
+
),
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
name: 'WORKERS card — survivors keep their ordinals',
|
|
293
|
+
fn: 'renderCombinedWorkerFeed → the shared card spec',
|
|
294
|
+
when: 'Worker 1 finished; 2 and 3 keep their numbers, so the card shows `2.`/`3.` with no `1.`',
|
|
295
|
+
render: () =>
|
|
296
|
+
renderCombinedWorkerFeed(
|
|
297
|
+
[
|
|
298
|
+
row(2, 'Fix the hindsight recall floor', H(4, 'recall')),
|
|
299
|
+
row(3, 'Release v0.19.24', H(4, 'release')),
|
|
300
|
+
],
|
|
301
|
+
{ maxRows: 6 },
|
|
302
|
+
),
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
name: 'WORKERS card — a worker with no history yet',
|
|
306
|
+
fn: 'renderCombinedWorkerFeed → section placeholder',
|
|
307
|
+
when: 'A freshly dispatched worker joins a group that already has a live card.',
|
|
308
|
+
render: () =>
|
|
309
|
+
renderCombinedWorkerFeed(
|
|
310
|
+
[
|
|
311
|
+
row(1, 'Audit every card render path', H(5, 'audit')),
|
|
312
|
+
row(2, 'Freshly dispatched worker', [], { currentStep: '', historyLines: undefined }),
|
|
313
|
+
],
|
|
314
|
+
{ maxRows: 6 },
|
|
315
|
+
),
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
name: 'Worker card — superseded (group message rotated)',
|
|
319
|
+
layout: 'constant',
|
|
320
|
+
fn: 'WORKER_CARD_SUPERSEDED_BODY (static notice; title line via renderCardTitleLine)',
|
|
321
|
+
when: 'The shared worker message hit its lifetime cap and a fresh card was opened; the retired message is finalised to this static note so it does not read as a stuck worker.',
|
|
322
|
+
render: () => WORKER_CARD_SUPERSEDED_BODY,
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
name: 'Agent card — rolling-window overflow',
|
|
326
|
+
fn: 'renderActivityFeed → renderStatusCard → emitSection',
|
|
327
|
+
when: 'More steps than the rolling window: the oldest collapse to a `✓ +N earlier…` header.',
|
|
328
|
+
render: () =>
|
|
329
|
+
renderActivityFeed(
|
|
330
|
+
Array.from({ length: 14 }, (_, i) => `Doing thing number ${i + 1}`),
|
|
331
|
+
false,
|
|
332
|
+
'',
|
|
333
|
+
undefined,
|
|
334
|
+
agentHeader(),
|
|
335
|
+
),
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
name: 'Agent card — nested sub-agent with child overflow',
|
|
339
|
+
fn: 'renderActivityFeedWithNested → renderStatusCard',
|
|
340
|
+
when: 'The foreground sub-agent has produced more steps than the window: a `↳ +N earlier…` header appears in the child block.',
|
|
341
|
+
render: () =>
|
|
342
|
+
renderActivityFeedWithNested(
|
|
343
|
+
AGENT_STEPS.slice(0, 2),
|
|
344
|
+
Array.from({ length: 11 }, (_, i) => `Child step ${i + 1}`),
|
|
345
|
+
false,
|
|
346
|
+
'',
|
|
347
|
+
undefined,
|
|
348
|
+
agentHeader(),
|
|
349
|
+
),
|
|
350
|
+
},
|
|
351
|
+
]
|
|
352
|
+
|
|
353
|
+
/** Section delimiter in the golden file. Chosen so no card body can contain it. */
|
|
354
|
+
export const GOLDEN_DELIMITER = '\n===== CARD ====='
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* The whole catalogue as ONE deterministic string — what the golden file holds.
|
|
358
|
+
* Every variant contributes its name and its exact rendered bytes.
|
|
359
|
+
*/
|
|
360
|
+
export function renderGoldenDocument(): string {
|
|
361
|
+
return (
|
|
362
|
+
CARD_VARIANTS.map(
|
|
363
|
+
(v) => `${GOLDEN_DELIMITER} ${v.name}\n${v.render() ?? '(null — renderer returned nothing)'}`,
|
|
364
|
+
).join('\n') + '\n'
|
|
365
|
+
)
|
|
366
|
+
}
|