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,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression suite for switchroom#3848 — the terminal worker-card frame must
|
|
3
|
+
* not be shed with the heartbeat repaints.
|
|
4
|
+
*
|
|
5
|
+
* #3847 made the edit-flood fuse class-aware and tagged EVERY worker-feed edit
|
|
6
|
+
* `cosmetic`. That is right for a liveness repaint (dropping one costs
|
|
7
|
+
* nothing — the next render carries full state) and wrong for the TERMINAL
|
|
8
|
+
* frame: it is the last frame the operator ever reads for that card, and
|
|
9
|
+
* nothing newer is coming to repaint it. Classed cosmetic, the single frame
|
|
10
|
+
* carrying the finished state competed in the same starved budget as a
|
|
11
|
+
* heartbeat, and under sustained cosmetic pressure it was the frame dropped —
|
|
12
|
+
* leaving the card frozen mid-run forever.
|
|
13
|
+
*
|
|
14
|
+
* Every test here asserts an OUTCOME (did the call reach the API / what class
|
|
15
|
+
* did it carry), under the exact pressure scenario that sheds it. Each one
|
|
16
|
+
* goes RED if the terminal frame is classed `cosmetic` again — at the feed
|
|
17
|
+
* (`{ terminal: … }` no longer passed), at the mapping
|
|
18
|
+
* (`workerFeedEditPriorityClass`), or at the fuse.
|
|
19
|
+
*/
|
|
20
|
+
import { describe, it, expect } from 'vitest'
|
|
21
|
+
import { readFileSync } from 'node:fs'
|
|
22
|
+
import { createEditFloodFuse, EDIT_FLOOD_FUSE_DEFAULTS } from '../edit-flood-fuse.js'
|
|
23
|
+
import { withOutboundClass } from '../outbound-class.js'
|
|
24
|
+
import type { Clock, PriorityClass } from '../send-gate.js'
|
|
25
|
+
import {
|
|
26
|
+
createWorkerActivityFeed,
|
|
27
|
+
workerFeedEditPriorityClass,
|
|
28
|
+
type BotApiForWorkerFeed,
|
|
29
|
+
type WorkerActivityView,
|
|
30
|
+
type WorkerFeedEditMeta,
|
|
31
|
+
} from '../worker-activity-feed.js'
|
|
32
|
+
|
|
33
|
+
const CHAT = '1001'
|
|
34
|
+
|
|
35
|
+
class FakeClock implements Clock {
|
|
36
|
+
private cur = 0
|
|
37
|
+
private seq = 0
|
|
38
|
+
private timers: { at: number; id: number; resolve: () => void }[] = []
|
|
39
|
+
|
|
40
|
+
now(): number { return this.cur }
|
|
41
|
+
|
|
42
|
+
sleep(ms: number): Promise<void> {
|
|
43
|
+
return new Promise<void>((resolve) => {
|
|
44
|
+
this.timers.push({ at: this.cur + ms, id: this.seq++, resolve })
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async advance(ms: number): Promise<void> {
|
|
49
|
+
const target = this.cur + ms
|
|
50
|
+
for (;;) {
|
|
51
|
+
await flush()
|
|
52
|
+
const due = this.timers.filter((t) => t.at <= target).sort((a, b) => a.at - b.at || a.id - b.id)
|
|
53
|
+
if (due.length === 0) break
|
|
54
|
+
const t = due[0]!
|
|
55
|
+
this.timers = this.timers.filter((x) => x !== t)
|
|
56
|
+
this.cur = t.at
|
|
57
|
+
t.resolve()
|
|
58
|
+
await flush()
|
|
59
|
+
}
|
|
60
|
+
this.cur = target
|
|
61
|
+
await flush()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function flush(): Promise<void> {
|
|
66
|
+
return new Promise((r) => setImmediate(r))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function drain(): Promise<void> {
|
|
70
|
+
for (let i = 0; i < 12; i++) await flush()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function view(step: string, elapsedMs: number, state: 'running' | 'done' = 'running'): WorkerActivityView {
|
|
74
|
+
return { description: 'ship the fix', lastTool: null, toolCount: 2, latestSummary: step, elapsedMs, state }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The gateway's worker-feed edit adapter, reproduced EXACTLY as
|
|
79
|
+
* `gateway.ts` wires it (see the `createWorkerActivityFeed({ bot: { … } })`
|
|
80
|
+
* block): map the feed's out-of-band `meta` to a priority class with
|
|
81
|
+
* `workerFeedEditPriorityClass`, publish it on the AsyncLocalStorage the fuse
|
|
82
|
+
* reads, then hand the call to the transformer.
|
|
83
|
+
*
|
|
84
|
+
* `withOutboundClass` is what the real send gate does with `priorityClass`;
|
|
85
|
+
* doing it here keeps this test on the production mapping rather than a
|
|
86
|
+
* hand-written class literal.
|
|
87
|
+
*/
|
|
88
|
+
function gatewayEditAdapter(
|
|
89
|
+
fuse: { apply: <R>(m: string, p: unknown, next: () => Promise<R>) => Promise<R> },
|
|
90
|
+
onLand: (messageId: number, cls: PriorityClass) => void,
|
|
91
|
+
) {
|
|
92
|
+
return (chatId: string, messageId: number, text: string, meta?: WorkerFeedEditMeta): Promise<unknown> => {
|
|
93
|
+
const cls: PriorityClass = workerFeedEditPriorityClass(meta)
|
|
94
|
+
return withOutboundClass(cls, () =>
|
|
95
|
+
fuse.apply('editMessageText', { chat_id: chatId, message_id: messageId, text }, async () => {
|
|
96
|
+
onLand(messageId, cls)
|
|
97
|
+
return true
|
|
98
|
+
}),
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const D = EDIT_FLOOD_FUSE_DEFAULTS
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Offset at which the frame under test is offered, relative to the pressure
|
|
107
|
+
* burst. Small and non-zero: the frame must arrive AFTER the chat's cosmetic
|
|
108
|
+
* allowance is spent (so it has to wait) but early enough that its own
|
|
109
|
+
* `maxDeferMs` deadline still falls inside the pressure window — which is
|
|
110
|
+
* precisely the "sustained cosmetic pressure" #3848 describes. Offering it at
|
|
111
|
+
* t=0 would race the burst's own late releases at the same instant.
|
|
112
|
+
*/
|
|
113
|
+
const OFFER_AT_MS = 1_000
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Saturate a chat's cosmetic allowance the way a busy agent does: N distinct
|
|
117
|
+
* worker cards all repainting at once. Both the per-chat cosmetic ceiling
|
|
118
|
+
* (`cosmeticPerChatMaxPerWindow`, 6/60s) and the bounded R1 late-release
|
|
119
|
+
* budget (`lateReleaseMaxPerWindow`, 2/60s) get spent — the latter matters
|
|
120
|
+
* because a LONE cosmetic frame is otherwise released late rather than
|
|
121
|
+
* dropped. That late-release budget is the partial mitigation #3848 records,
|
|
122
|
+
* and running it out is what turns the degradation into a guaranteed drop.
|
|
123
|
+
*
|
|
124
|
+
* Returns the still-pending burst; the caller advances the clock past
|
|
125
|
+
* `maxDeferMs` once, resolving the burst and the frame under test together.
|
|
126
|
+
*/
|
|
127
|
+
function startCosmeticPressure(
|
|
128
|
+
fuse: { apply: <R>(m: string, p: unknown, next: () => Promise<R>) => Promise<R> },
|
|
129
|
+
landed: number[],
|
|
130
|
+
): Promise<unknown>[] {
|
|
131
|
+
const total = D.cosmeticPerChatMaxPerWindow + D.lateReleaseMaxPerWindow
|
|
132
|
+
const pending: Promise<unknown>[] = []
|
|
133
|
+
for (let i = 0; i < total; i++) {
|
|
134
|
+
pending.push(
|
|
135
|
+
withOutboundClass('cosmetic', () =>
|
|
136
|
+
fuse.apply('editMessageText', { chat_id: CHAT, message_id: 9000 + i, text: `card ${i}` }, async () => {
|
|
137
|
+
landed.push(9000 + i)
|
|
138
|
+
return true
|
|
139
|
+
}),
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
return pending
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Offer ONE lone edit to a fresh message id under an already-exhausted
|
|
148
|
+
* cosmetic budget, as `cls`, and report whether it reached the API.
|
|
149
|
+
*/
|
|
150
|
+
async function offerLoneEditUnderPressure(cls: PriorityClass): Promise<boolean> {
|
|
151
|
+
const clock = new FakeClock()
|
|
152
|
+
const fuse = createEditFloodFuse({ clock })
|
|
153
|
+
const landed: number[] = []
|
|
154
|
+
const pressure = startCosmeticPressure(fuse, landed)
|
|
155
|
+
await clock.advance(OFFER_AT_MS)
|
|
156
|
+
|
|
157
|
+
let reachedApi = false
|
|
158
|
+
const call = withOutboundClass(cls, () =>
|
|
159
|
+
fuse.apply(
|
|
160
|
+
'editMessageText',
|
|
161
|
+
{ chat_id: CHAT, message_id: 4242, text: '✅ done · 12 tools' },
|
|
162
|
+
async () => { reachedApi = true; return true },
|
|
163
|
+
),
|
|
164
|
+
)
|
|
165
|
+
await clock.advance(D.maxDeferMs + 1_000)
|
|
166
|
+
await Promise.all([...pressure, call])
|
|
167
|
+
// Sanity: the pressure really did consume the whole cosmetic allowance.
|
|
168
|
+
expect(landed).toHaveLength(D.cosmeticPerChatMaxPerWindow + D.lateReleaseMaxPerWindow)
|
|
169
|
+
return reachedApi
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
describe('#3848 — a terminal worker-card frame survives an exhausted cosmetic budget', () => {
|
|
173
|
+
it('drops the terminal frame when it is classed cosmetic, and lands it when classed useful', async () => {
|
|
174
|
+
// Both halves run the SAME fuse config under the SAME pressure, so the
|
|
175
|
+
// ONLY difference is the class the terminal frame carries. The classes
|
|
176
|
+
// come from the production mapping, so reverting the promotion turns the
|
|
177
|
+
// second assertion into the first.
|
|
178
|
+
const asCosmetic = await offerLoneEditUnderPressure(workerFeedEditPriorityClass({ terminal: false }))
|
|
179
|
+
const asUseful = await offerLoneEditUnderPressure(workerFeedEditPriorityClass({ terminal: true }))
|
|
180
|
+
|
|
181
|
+
// The defect (#3848): with the cosmetic budget spent, the frame carrying
|
|
182
|
+
// the finished state never reaches Telegram — the card freezes mid-run.
|
|
183
|
+
expect(asCosmetic).toBe(false)
|
|
184
|
+
// The fix: the same frame, promoted because it is terminal, gets through.
|
|
185
|
+
expect(asUseful).toBe(true)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('still holds INTERMEDIATE repaints to the cosmetic ceiling after the fix', async () => {
|
|
189
|
+
// Guards the other direction: promoting the terminal frame must not
|
|
190
|
+
// promote the heartbeat stream, or #3847's whole flood defence is undone.
|
|
191
|
+
expect(workerFeedEditPriorityClass({ terminal: false })).toBe('cosmetic')
|
|
192
|
+
expect(await offerLoneEditUnderPressure('cosmetic')).toBe(false)
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
describe('#3848 — the REAL feed tags its terminal frame, through the REAL gateway mapping', () => {
|
|
197
|
+
/**
|
|
198
|
+
* End-to-end over the production pieces: `createWorkerActivityFeed` drives
|
|
199
|
+
* the gateway's own edit adapter into a real `createEditFloodFuse`. Nothing
|
|
200
|
+
* here hand-writes a class — the class comes from whatever the feed passes
|
|
201
|
+
* as `meta`. If the feed stops passing `{ terminal: … }`, or the adapter
|
|
202
|
+
* stops consulting it, the terminal recap is shed and this goes RED.
|
|
203
|
+
*/
|
|
204
|
+
it('lands the finish recap at the API while the chat cosmetic budget is exhausted', async () => {
|
|
205
|
+
const clock = new FakeClock()
|
|
206
|
+
const fuse = createEditFloodFuse({ clock })
|
|
207
|
+
const preLanded: number[] = []
|
|
208
|
+
const pressure = startCosmeticPressure(fuse, preLanded)
|
|
209
|
+
await clock.advance(OFFER_AT_MS)
|
|
210
|
+
|
|
211
|
+
const CARD = 777
|
|
212
|
+
const landedEdits: { messageId: number; cls: PriorityClass }[] = []
|
|
213
|
+
const edit = gatewayEditAdapter(fuse, (messageId, cls) => landedEdits.push({ messageId, cls }))
|
|
214
|
+
|
|
215
|
+
const bot: BotApiForWorkerFeed = {
|
|
216
|
+
sendMessage: async () => ({ message_id: CARD }),
|
|
217
|
+
editMessageText: (chatId, messageId, text, _opts, meta) => edit(chatId, messageId, text, meta),
|
|
218
|
+
}
|
|
219
|
+
const feed = createWorkerActivityFeed({
|
|
220
|
+
bot,
|
|
221
|
+
now: () => clock.now(),
|
|
222
|
+
minEditIntervalMs: 0,
|
|
223
|
+
elapsedRefreshMs: 0,
|
|
224
|
+
firstPaintMinMs: 0,
|
|
225
|
+
setInterval: () => 0,
|
|
226
|
+
clearInterval: () => {},
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
// First paint (a send — never dropped), then the worker finishes.
|
|
230
|
+
await feed.update('w1', CHAT, view('reading gateway.ts', 30_000))
|
|
231
|
+
await drain()
|
|
232
|
+
|
|
233
|
+
const finish = feed.finish('w1', view('opened the PR', 60_000, 'done'))
|
|
234
|
+
await clock.advance(D.maxDeferMs + 1_000)
|
|
235
|
+
await finish
|
|
236
|
+
await Promise.all(pressure)
|
|
237
|
+
await drain()
|
|
238
|
+
|
|
239
|
+
// The pressure really did spend the whole cosmetic allowance…
|
|
240
|
+
expect(preLanded).toHaveLength(D.cosmeticPerChatMaxPerWindow + D.lateReleaseMaxPerWindow)
|
|
241
|
+
// …and the terminal recap still reached Telegram, as `useful`.
|
|
242
|
+
expect(landedEdits.map((e) => e.messageId)).toContain(CARD)
|
|
243
|
+
expect(landedEdits.filter((e) => e.messageId === CARD).every((e) => e.cls === 'useful')).toBe(true)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('classes the running repaints cosmetic and only the finish frame useful', async () => {
|
|
247
|
+
const clock = new FakeClock()
|
|
248
|
+
const CARD = 888
|
|
249
|
+
const seen: { text: string; meta?: WorkerFeedEditMeta }[] = []
|
|
250
|
+
const bot: BotApiForWorkerFeed = {
|
|
251
|
+
sendMessage: async () => ({ message_id: CARD }),
|
|
252
|
+
editMessageText: async (_c, _m, text, _opts, meta) => { seen.push({ text, meta }); return true },
|
|
253
|
+
}
|
|
254
|
+
const feed = createWorkerActivityFeed({
|
|
255
|
+
bot,
|
|
256
|
+
now: () => clock.now(),
|
|
257
|
+
minEditIntervalMs: 0,
|
|
258
|
+
elapsedRefreshMs: 0,
|
|
259
|
+
firstPaintMinMs: 0,
|
|
260
|
+
setInterval: () => 0,
|
|
261
|
+
clearInterval: () => {},
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
await feed.update('w1', CHAT, view('first step', 10_000))
|
|
265
|
+
await drain()
|
|
266
|
+
await feed.update('w1', CHAT, view('second step', 20_000))
|
|
267
|
+
await drain()
|
|
268
|
+
await feed.update('w1', CHAT, view('third step', 30_000))
|
|
269
|
+
await drain()
|
|
270
|
+
const beforeFinish = seen.length
|
|
271
|
+
await feed.finish('w1', view('all done', 40_000, 'done'))
|
|
272
|
+
await drain()
|
|
273
|
+
|
|
274
|
+
// At least one intermediate repaint and exactly the finish frame after it.
|
|
275
|
+
expect(beforeFinish).toBeGreaterThan(0)
|
|
276
|
+
expect(seen.length).toBeGreaterThan(beforeFinish)
|
|
277
|
+
for (const e of seen.slice(0, beforeFinish)) {
|
|
278
|
+
expect(e.meta?.terminal).toBe(false)
|
|
279
|
+
expect(workerFeedEditPriorityClass(e.meta)).toBe('cosmetic')
|
|
280
|
+
}
|
|
281
|
+
for (const e of seen.slice(beforeFinish)) {
|
|
282
|
+
expect(e.meta?.terminal).toBe(true)
|
|
283
|
+
expect(workerFeedEditPriorityClass(e.meta)).toBe('useful')
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
describe('#3848 — gateway.ts actually wires the mapping', () => {
|
|
289
|
+
/**
|
|
290
|
+
* `gateway.ts` is not unit-importable (a single 24k-line module with a live
|
|
291
|
+
* bot at construction), so `gatewayEditAdapter` above can only MIRROR its
|
|
292
|
+
* adapter. That mirror would keep passing if someone hard-coded
|
|
293
|
+
* `priorityClass: 'cosmetic'` back into the real adapter, which is exactly
|
|
294
|
+
* the regression #3848 fixes. This asserts the real wiring at the source,
|
|
295
|
+
* deterministically, instead of trusting the mirror.
|
|
296
|
+
*/
|
|
297
|
+
const GATEWAY = new URL('../gateway/gateway.ts', import.meta.url)
|
|
298
|
+
|
|
299
|
+
it('passes the feed edit meta through workerFeedEditPriorityClass', () => {
|
|
300
|
+
const src = readFileSync(GATEWAY, 'utf8')
|
|
301
|
+
// The adapter must actually RECEIVE the meta argument from the feed.
|
|
302
|
+
const ANCHOR = 'editMessageText: (cid, mid, text, editOpts, editMeta) =>'
|
|
303
|
+
const at = src.indexOf(ANCHOR)
|
|
304
|
+
expect(at, `gateway.ts no longer declares the worker-feed edit adapter as \`${ANCHOR}\``).toBeGreaterThan(-1)
|
|
305
|
+
|
|
306
|
+
// Bound the slice to this one adapter: it ends at the `verb: 'worker-feed'`
|
|
307
|
+
// options object it passes to robustApiCall.
|
|
308
|
+
const verbAt = src.indexOf("verb: 'worker-feed'", at)
|
|
309
|
+
expect(verbAt).toBeGreaterThan(at)
|
|
310
|
+
const feedAdapter = src.slice(at, verbAt + 500)
|
|
311
|
+
|
|
312
|
+
expect(feedAdapter).toContain('priorityClass: workerFeedEditPriorityClass(editMeta)')
|
|
313
|
+
// The hard-coded class #3848 removed must not come back.
|
|
314
|
+
expect(feedAdapter).not.toContain("priorityClass: 'cosmetic'")
|
|
315
|
+
// And the symbol must be imported, or the above is dead source text.
|
|
316
|
+
expect(src).toContain('workerFeedEditPriorityClass')
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
describe('#3848 — the class mapping itself', () => {
|
|
321
|
+
it('maps terminal → useful and everything else → cosmetic', () => {
|
|
322
|
+
expect(workerFeedEditPriorityClass({ terminal: true })).toBe('useful')
|
|
323
|
+
expect(workerFeedEditPriorityClass({ terminal: false })).toBe('cosmetic')
|
|
324
|
+
// An adapter that forgets to forward `meta` must degrade to the SAFE
|
|
325
|
+
// class, not silently promote every repaint.
|
|
326
|
+
expect(workerFeedEditPriorityClass(undefined)).toBe('cosmetic')
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
it('returns a value assignable to the send gate PriorityClass union', () => {
|
|
330
|
+
// Compile-time half: this line fails `tsc` if the mapping ever returns a
|
|
331
|
+
// class the gate does not know (#3847's OutboundClass/PriorityClass lock).
|
|
332
|
+
const cls: PriorityClass = workerFeedEditPriorityClass({ terminal: true })
|
|
333
|
+
expect(['critical', 'useful', 'cosmetic']).toContain(cls)
|
|
334
|
+
})
|
|
335
|
+
})
|
|
@@ -253,7 +253,7 @@ describe('prose-silent background worker — end-to-end visibility harness (PR 2
|
|
|
253
253
|
expect(h.bot.sent).toHaveLength(1)
|
|
254
254
|
expect(h.feed.messageIdOf(h.agentId)).not.toBeNull()
|
|
255
255
|
expect(h.bot.sent[0].chatId).toBe('chat-42')
|
|
256
|
-
expect(h.bot.sent[0].text
|
|
256
|
+
expect(h.bot.sent[0].text.startsWith('🛠 **WORKER**')).toBe(true)
|
|
257
257
|
|
|
258
258
|
// (b) KEEPS UPDATING: later heartbeats edit the message with a climbing
|
|
259
259
|
// `· Ns` suffix so the still-alive worker visibly advances.
|