switchroom 0.14.45 → 0.14.46
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/dist/agent-scheduler/index.js +80 -80
- package/dist/auth-broker/index.js +80 -80
- package/dist/cli/drive-write-pretool.mjs +10 -10
- package/dist/cli/notion-write-pretool.mjs +82 -82
- package/dist/cli/skill-validate-pretool.mjs +72 -72
- package/dist/cli/switchroom.js +499 -378
- package/dist/host-control/main.js +148 -148
- package/dist/vault/approvals/kernel-server.js +82 -82
- package/dist/vault/broker/server.js +83 -83
- package/examples/switchroom.yaml +26 -0
- package/package.json +1 -1
- package/telegram-plugin/dist/bridge/bridge.js +112 -112
- package/telegram-plugin/dist/gateway/gateway.js +249 -199
- package/telegram-plugin/dist/server.js +160 -160
- package/telegram-plugin/gateway/gateway.ts +58 -6
- package/telegram-plugin/gateway/vault-grant-inbound-builders.ts +17 -0
- package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +84 -0
|
@@ -2913,6 +2913,10 @@ interface PendingVaultRequestSave {
|
|
|
2913
2913
|
chat_id: string
|
|
2914
2914
|
/** Card message id (filled in after we send the card). */
|
|
2915
2915
|
card_message_id?: number
|
|
2916
|
+
/** Supergroup forum topic the agent was working in when it requested the
|
|
2917
|
+
* save — carried into the save-outcome inbound so the resumed reply lands
|
|
2918
|
+
* back in that topic, not General. */
|
|
2919
|
+
threadId?: number
|
|
2916
2920
|
/** Currently-suggested slug; may be renamed by the user. */
|
|
2917
2921
|
key: string
|
|
2918
2922
|
/** Storage shape — 'string' (default) or 'binary'. */
|
|
@@ -2954,6 +2958,10 @@ interface PendingVaultRequestAccess {
|
|
|
2954
2958
|
chat_id: string
|
|
2955
2959
|
/** Card message id (filled in after we send the card). */
|
|
2956
2960
|
card_message_id?: number
|
|
2961
|
+
/** Supergroup forum topic the agent was working in when it requested (the
|
|
2962
|
+
* card's originating thread). Carried into the grant-outcome inbound so the
|
|
2963
|
+
* resumed reply lands back in that topic, not General. */
|
|
2964
|
+
threadId?: number
|
|
2957
2965
|
/** Vault key the agent wants to read. */
|
|
2958
2966
|
key: string
|
|
2959
2967
|
/** 'read' (default) or 'write'. */
|
|
@@ -7080,6 +7088,8 @@ async function executeVaultRequestSave(args: Record<string, unknown>): Promise<{
|
|
|
7080
7088
|
// crashing the tool call.
|
|
7081
7089
|
const text = renderVaultRequestSaveCard(pending, agentSlug)
|
|
7082
7090
|
const threadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined
|
|
7091
|
+
// Remember the agent's working topic so the save-outcome inbound resumes in it.
|
|
7092
|
+
if (threadId != null) pending.threadId = threadId
|
|
7083
7093
|
const sent = await retryWithThreadFallback<{ message_id: number }>(
|
|
7084
7094
|
robustApiCall,
|
|
7085
7095
|
(tid) =>
|
|
@@ -7120,12 +7130,16 @@ interface PendingSecretRequest {
|
|
|
7120
7130
|
reason?: string
|
|
7121
7131
|
staged_at: number
|
|
7122
7132
|
card_message_id?: number
|
|
7133
|
+
/** Supergroup forum topic the agent was working in — carried into the
|
|
7134
|
+
* provide/decline/fail outcome inbounds so the resumed reply lands back
|
|
7135
|
+
* in that topic, not General. */
|
|
7136
|
+
threadId?: number
|
|
7123
7137
|
}
|
|
7124
7138
|
// stageId -> request (lives until tapped or TTL).
|
|
7125
7139
|
const pendingSecretRequests = new Map<string, PendingSecretRequest>()
|
|
7126
7140
|
// chat_id -> the armed capture: the operator's NEXT message in this chat is
|
|
7127
7141
|
// the value for `key`. Set when [Provide securely] is tapped.
|
|
7128
|
-
interface ArmedSecretCapture { key: string; agent: string; stageId: string; armed_at: number }
|
|
7142
|
+
interface ArmedSecretCapture { key: string; agent: string; stageId: string; armed_at: number; threadId?: number }
|
|
7129
7143
|
const armedSecretCaptures = new Map<string, ArmedSecretCapture>()
|
|
7130
7144
|
const PENDING_SECRET_REQUEST_TTL_MS = 30 * 60_000 // card lifetime
|
|
7131
7145
|
const ARMED_SECRET_CAPTURE_TTL_MS = 10 * 60_000 // window to send the value after tapping
|
|
@@ -7194,6 +7208,8 @@ async function executeRequestSecret(args: Record<string, unknown>): Promise<{ co
|
|
|
7194
7208
|
|
|
7195
7209
|
const text = renderSecretRequestCard(pending)
|
|
7196
7210
|
const threadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined
|
|
7211
|
+
// Remember the agent's working topic so the provide/decline/fail inbound resumes in it.
|
|
7212
|
+
if (threadId != null) pending.threadId = threadId
|
|
7197
7213
|
const sent = await retryWithThreadFallback<{ message_id: number }>(
|
|
7198
7214
|
robustApiCall,
|
|
7199
7215
|
(tid) =>
|
|
@@ -7269,12 +7285,19 @@ async function captureProvidedSecret(
|
|
|
7269
7285
|
const failMsg: InboundMessage = {
|
|
7270
7286
|
type: 'inbound',
|
|
7271
7287
|
chatId: chat_id,
|
|
7288
|
+
...(armed.threadId != null ? { threadId: armed.threadId } : {}),
|
|
7272
7289
|
messageId: fts,
|
|
7273
7290
|
user: 'vault-broker',
|
|
7274
7291
|
userId: 0,
|
|
7275
7292
|
ts: fts,
|
|
7276
7293
|
text: `⚠️ The secret you requested for \`vault:${armed.key}\` could NOT be saved (vault write failed). Do not assume it is available; tell the operator or try request_secret again.`,
|
|
7277
|
-
meta: {
|
|
7294
|
+
meta: {
|
|
7295
|
+
source: 'secret_provide_failed',
|
|
7296
|
+
agent: armed.agent,
|
|
7297
|
+
...(armed.threadId != null ? { message_thread_id: String(armed.threadId) } : {}),
|
|
7298
|
+
key: armed.key,
|
|
7299
|
+
stage_id: armed.stageId,
|
|
7300
|
+
},
|
|
7278
7301
|
}
|
|
7279
7302
|
const fdelivered = ipcServer.sendToAgent(armed.agent, failMsg)
|
|
7280
7303
|
if (fdelivered) markClaudeBusyForInbound(failMsg)
|
|
@@ -7294,6 +7317,7 @@ async function captureProvidedSecret(
|
|
|
7294
7317
|
const synthetic: InboundMessage = {
|
|
7295
7318
|
type: 'inbound',
|
|
7296
7319
|
chatId: chat_id,
|
|
7320
|
+
...(armed.threadId != null ? { threadId: armed.threadId } : {}),
|
|
7297
7321
|
messageId: ts,
|
|
7298
7322
|
user: 'vault-broker',
|
|
7299
7323
|
userId: 0,
|
|
@@ -7305,6 +7329,7 @@ async function captureProvidedSecret(
|
|
|
7305
7329
|
meta: {
|
|
7306
7330
|
source: 'secret_provided',
|
|
7307
7331
|
agent: armed.agent,
|
|
7332
|
+
...(armed.threadId != null ? { message_thread_id: String(armed.threadId) } : {}),
|
|
7308
7333
|
key: armed.key,
|
|
7309
7334
|
stage_id: armed.stageId,
|
|
7310
7335
|
},
|
|
@@ -7345,6 +7370,7 @@ async function handleSecretRequestCallback(ctx: Context, data: string): Promise<
|
|
|
7345
7370
|
agent: pending.agent,
|
|
7346
7371
|
stageId,
|
|
7347
7372
|
armed_at: Date.now(),
|
|
7373
|
+
...(pending.threadId != null ? { threadId: pending.threadId } : {}),
|
|
7348
7374
|
})
|
|
7349
7375
|
await ctx.answerCallbackQuery({ text: 'Send the value now — it auto-deletes.' }).catch(() => {})
|
|
7350
7376
|
if (pending.card_message_id != null) {
|
|
@@ -7377,12 +7403,19 @@ async function handleSecretRequestCallback(ctx: Context, data: string): Promise<
|
|
|
7377
7403
|
const synthetic: InboundMessage = {
|
|
7378
7404
|
type: 'inbound',
|
|
7379
7405
|
chatId: pending.chat_id,
|
|
7406
|
+
...(pending.threadId != null ? { threadId: pending.threadId } : {}),
|
|
7380
7407
|
messageId: ts,
|
|
7381
7408
|
user: 'vault-broker',
|
|
7382
7409
|
userId: 0,
|
|
7383
7410
|
ts,
|
|
7384
7411
|
text: `🚫 Operator declined your request for \`vault:${pending.key}\`. Proceed without it or ask how they'd like to handle the task.`,
|
|
7385
|
-
meta: {
|
|
7412
|
+
meta: {
|
|
7413
|
+
source: 'secret_declined',
|
|
7414
|
+
agent: pending.agent,
|
|
7415
|
+
...(pending.threadId != null ? { message_thread_id: String(pending.threadId) } : {}),
|
|
7416
|
+
key: pending.key,
|
|
7417
|
+
stage_id: stageId,
|
|
7418
|
+
},
|
|
7386
7419
|
}
|
|
7387
7420
|
const delivered = ipcServer.sendToAgent(pending.agent, synthetic)
|
|
7388
7421
|
if (delivered) markClaudeBusyForInbound(synthetic)
|
|
@@ -7530,6 +7563,8 @@ async function executeVaultRequestAccess(args: Record<string, unknown>): Promise
|
|
|
7530
7563
|
|
|
7531
7564
|
const text = renderVaultRequestAccessCard(pending)
|
|
7532
7565
|
const threadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined
|
|
7566
|
+
// Remember the agent's working topic so the grant-outcome inbound resumes in it.
|
|
7567
|
+
if (threadId != null) pending.threadId = threadId
|
|
7533
7568
|
// #1075: deleted-topic safe — fall back to main chat.
|
|
7534
7569
|
const sent = await retryWithThreadFallback<{ message_id: number }>(
|
|
7535
7570
|
robustApiCall,
|
|
@@ -14023,6 +14058,7 @@ async function performVaultAccessApproval(
|
|
|
14023
14058
|
scope: pending.scope,
|
|
14024
14059
|
chat_id: pending.chat_id,
|
|
14025
14060
|
ttl_seconds: pending.ttl_seconds,
|
|
14061
|
+
...(pending.threadId != null ? { threadId: pending.threadId } : {}),
|
|
14026
14062
|
},
|
|
14027
14063
|
grantId: id,
|
|
14028
14064
|
stageId,
|
|
@@ -14104,6 +14140,7 @@ async function handleVaultRequestAccessCallback(ctx: Context, data: string): Pro
|
|
|
14104
14140
|
scope: pending.scope,
|
|
14105
14141
|
chat_id: pending.chat_id,
|
|
14106
14142
|
ttl_seconds: pending.ttl_seconds,
|
|
14143
|
+
...(pending.threadId != null ? { threadId: pending.threadId } : {}),
|
|
14107
14144
|
},
|
|
14108
14145
|
stageId,
|
|
14109
14146
|
operatorId: senderId,
|
|
@@ -14278,7 +14315,12 @@ async function handleVaultRequestSaveCallback(ctx: Context, data: string): Promi
|
|
|
14278
14315
|
// tool returned "waiting for operator", the turn ended, and a
|
|
14279
14316
|
// Discard left the agent silently idle forever.
|
|
14280
14317
|
const discardInbound = buildVaultSaveDiscardedInbound({
|
|
14281
|
-
ctx: {
|
|
14318
|
+
ctx: {
|
|
14319
|
+
agent: pending.agent,
|
|
14320
|
+
key: pending.key,
|
|
14321
|
+
chat_id: pending.chat_id,
|
|
14322
|
+
...(pending.threadId != null ? { threadId: pending.threadId } : {}),
|
|
14323
|
+
},
|
|
14282
14324
|
stageId,
|
|
14283
14325
|
operatorId: senderId,
|
|
14284
14326
|
})
|
|
@@ -14401,7 +14443,12 @@ async function handleVaultRequestSaveCallback(ctx: Context, data: string): Promi
|
|
|
14401
14443
|
const failReason =
|
|
14402
14444
|
(write.output || 'vault write error').split('\n')[0]!.slice(0, 200)
|
|
14403
14445
|
const failInbound = buildVaultSaveFailedInbound({
|
|
14404
|
-
ctx: {
|
|
14446
|
+
ctx: {
|
|
14447
|
+
agent: pending.agent,
|
|
14448
|
+
key: pending.key,
|
|
14449
|
+
chat_id: pending.chat_id,
|
|
14450
|
+
...(pending.threadId != null ? { threadId: pending.threadId } : {}),
|
|
14451
|
+
},
|
|
14405
14452
|
stageId,
|
|
14406
14453
|
operatorId: senderId,
|
|
14407
14454
|
reason: failReason,
|
|
@@ -14432,7 +14479,12 @@ async function handleVaultRequestSaveCallback(ctx: Context, data: string): Promi
|
|
|
14432
14479
|
// task that was blocked on this credential (symmetric with the
|
|
14433
14480
|
// vra: approve path; buffered if the bridge is mid-reconnect).
|
|
14434
14481
|
const okInbound = buildVaultSaveCompletedInbound({
|
|
14435
|
-
ctx: {
|
|
14482
|
+
ctx: {
|
|
14483
|
+
agent: pending.agent,
|
|
14484
|
+
key: pending.key,
|
|
14485
|
+
chat_id: pending.chat_id,
|
|
14486
|
+
...(pending.threadId != null ? { threadId: pending.threadId } : {}),
|
|
14487
|
+
},
|
|
14436
14488
|
stageId,
|
|
14437
14489
|
operatorId: senderId,
|
|
14438
14490
|
})
|
|
@@ -34,6 +34,10 @@ export interface VaultGrantInboundContext {
|
|
|
34
34
|
chat_id: string
|
|
35
35
|
/** Seconds. For approved grants; ignored for deny. */
|
|
36
36
|
ttl_seconds: number
|
|
37
|
+
/** Supergroup forum topic (message_thread_id) the agent was working in
|
|
38
|
+
* when it requested the credential — so the resumed turn's reply lands
|
|
39
|
+
* back in that topic, not General. Undefined for DM / non-topic requests. */
|
|
40
|
+
threadId?: number
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
/**
|
|
@@ -62,6 +66,7 @@ export function buildVaultGrantApprovedInbound(opts: {
|
|
|
62
66
|
return {
|
|
63
67
|
type: 'inbound',
|
|
64
68
|
chatId: opts.ctx.chat_id,
|
|
69
|
+
...(opts.ctx.threadId != null ? { threadId: opts.ctx.threadId } : {}),
|
|
65
70
|
messageId: ts, // synthetic — no Telegram message id exists
|
|
66
71
|
user: 'vault-broker',
|
|
67
72
|
userId: 0,
|
|
@@ -76,6 +81,7 @@ export function buildVaultGrantApprovedInbound(opts: {
|
|
|
76
81
|
meta: {
|
|
77
82
|
source: 'vault_grant_approved',
|
|
78
83
|
agent: opts.ctx.agent,
|
|
84
|
+
...(opts.ctx.threadId != null ? { message_thread_id: String(opts.ctx.threadId) } : {}),
|
|
79
85
|
key: opts.ctx.key,
|
|
80
86
|
scope: opts.ctx.scope,
|
|
81
87
|
grant_id: opts.grantId,
|
|
@@ -103,6 +109,7 @@ export function buildVaultGrantDeniedInbound(opts: {
|
|
|
103
109
|
return {
|
|
104
110
|
type: 'inbound',
|
|
105
111
|
chatId: opts.ctx.chat_id,
|
|
112
|
+
...(opts.ctx.threadId != null ? { threadId: opts.ctx.threadId } : {}),
|
|
106
113
|
messageId: ts,
|
|
107
114
|
user: 'vault-broker',
|
|
108
115
|
userId: 0,
|
|
@@ -116,6 +123,7 @@ export function buildVaultGrantDeniedInbound(opts: {
|
|
|
116
123
|
meta: {
|
|
117
124
|
source: 'vault_grant_denied',
|
|
118
125
|
agent: opts.ctx.agent,
|
|
126
|
+
...(opts.ctx.threadId != null ? { message_thread_id: String(opts.ctx.threadId) } : {}),
|
|
119
127
|
key: opts.ctx.key,
|
|
120
128
|
scope: opts.ctx.scope,
|
|
121
129
|
stage_id: opts.stageId,
|
|
@@ -133,6 +141,9 @@ export interface VaultSaveInboundContext {
|
|
|
133
141
|
/** Telegram chat the save card lived in — keeps the synthesized
|
|
134
142
|
* resume-turn associated with the originating conversation. */
|
|
135
143
|
chat_id: string
|
|
144
|
+
/** Supergroup forum topic the agent was working in when it requested the
|
|
145
|
+
* save — so the resumed reply lands in that topic, not General. */
|
|
146
|
+
threadId?: number
|
|
136
147
|
}
|
|
137
148
|
|
|
138
149
|
/**
|
|
@@ -154,6 +165,7 @@ export function buildVaultSaveCompletedInbound(opts: {
|
|
|
154
165
|
return {
|
|
155
166
|
type: 'inbound',
|
|
156
167
|
chatId: opts.ctx.chat_id,
|
|
168
|
+
...(opts.ctx.threadId != null ? { threadId: opts.ctx.threadId } : {}),
|
|
157
169
|
messageId: ts,
|
|
158
170
|
user: 'vault-broker',
|
|
159
171
|
userId: 0,
|
|
@@ -165,6 +177,7 @@ export function buildVaultSaveCompletedInbound(opts: {
|
|
|
165
177
|
meta: {
|
|
166
178
|
source: 'vault_save_completed',
|
|
167
179
|
agent: opts.ctx.agent,
|
|
180
|
+
...(opts.ctx.threadId != null ? { message_thread_id: String(opts.ctx.threadId) } : {}),
|
|
168
181
|
key: opts.ctx.key,
|
|
169
182
|
stage_id: opts.stageId,
|
|
170
183
|
operator_id: opts.operatorId,
|
|
@@ -187,6 +200,7 @@ export function buildVaultSaveFailedInbound(opts: {
|
|
|
187
200
|
return {
|
|
188
201
|
type: 'inbound',
|
|
189
202
|
chatId: opts.ctx.chat_id,
|
|
203
|
+
...(opts.ctx.threadId != null ? { threadId: opts.ctx.threadId } : {}),
|
|
190
204
|
messageId: ts,
|
|
191
205
|
user: 'vault-broker',
|
|
192
206
|
userId: 0,
|
|
@@ -200,6 +214,7 @@ export function buildVaultSaveFailedInbound(opts: {
|
|
|
200
214
|
meta: {
|
|
201
215
|
source: 'vault_save_failed',
|
|
202
216
|
agent: opts.ctx.agent,
|
|
217
|
+
...(opts.ctx.threadId != null ? { message_thread_id: String(opts.ctx.threadId) } : {}),
|
|
203
218
|
key: opts.ctx.key,
|
|
204
219
|
stage_id: opts.stageId,
|
|
205
220
|
operator_id: opts.operatorId,
|
|
@@ -221,6 +236,7 @@ export function buildVaultSaveDiscardedInbound(opts: {
|
|
|
221
236
|
return {
|
|
222
237
|
type: 'inbound',
|
|
223
238
|
chatId: opts.ctx.chat_id,
|
|
239
|
+
...(opts.ctx.threadId != null ? { threadId: opts.ctx.threadId } : {}),
|
|
224
240
|
messageId: ts,
|
|
225
241
|
user: 'vault-broker',
|
|
226
242
|
userId: 0,
|
|
@@ -234,6 +250,7 @@ export function buildVaultSaveDiscardedInbound(opts: {
|
|
|
234
250
|
meta: {
|
|
235
251
|
source: 'vault_save_discarded',
|
|
236
252
|
agent: opts.ctx.agent,
|
|
253
|
+
...(opts.ctx.threadId != null ? { message_thread_id: String(opts.ctx.threadId) } : {}),
|
|
237
254
|
key: opts.ctx.key,
|
|
238
255
|
stage_id: opts.stageId,
|
|
239
256
|
operator_id: opts.operatorId,
|
|
@@ -16,7 +16,11 @@ import { describe, it, expect } from 'vitest'
|
|
|
16
16
|
import {
|
|
17
17
|
buildVaultGrantApprovedInbound,
|
|
18
18
|
buildVaultGrantDeniedInbound,
|
|
19
|
+
buildVaultSaveCompletedInbound,
|
|
20
|
+
buildVaultSaveFailedInbound,
|
|
21
|
+
buildVaultSaveDiscardedInbound,
|
|
19
22
|
type VaultGrantInboundContext,
|
|
23
|
+
type VaultSaveInboundContext,
|
|
20
24
|
} from '../gateway/vault-grant-inbound-builders.js'
|
|
21
25
|
|
|
22
26
|
const FIXED_NOW = 1_700_000_000_000
|
|
@@ -224,3 +228,83 @@ describe('approve vs deny shape invariants', () => {
|
|
|
224
228
|
expect(String(deny.meta?.source)).toMatch(/^vault_grant_denied$/)
|
|
225
229
|
})
|
|
226
230
|
})
|
|
231
|
+
|
|
232
|
+
// ── Supergroup topic routing (gap #2) ──────────────────────────────────────
|
|
233
|
+
//
|
|
234
|
+
// When the agent requested the credential from inside a forum topic, the
|
|
235
|
+
// grant/save-outcome inbound must carry that topic so the resumed turn's
|
|
236
|
+
// reply lands back in it — not General. The carrier is two-fold and BOTH
|
|
237
|
+
// halves are load-bearing:
|
|
238
|
+
// - top-level `threadId` → the gateway's per-topic busy-key / deliver-
|
|
239
|
+
// until-acked keying (markClaudeBusyForInbound reads it).
|
|
240
|
+
// - `meta.message_thread_id` (stringified) → rendered into the
|
|
241
|
+
// `<channel message_thread_id="…">` XML, which session-tail's
|
|
242
|
+
// parseChannelMeta re-extracts to set currentTurn.sessionThreadId, which
|
|
243
|
+
// the reply tool defaults to. Drop either and the reply mis-routes.
|
|
244
|
+
//
|
|
245
|
+
// DM / non-topic requests must leave BOTH absent (an empty-string or 0
|
|
246
|
+
// thread is a Telegram 400 "message thread not found").
|
|
247
|
+
describe('grant/save outcome topic routing', () => {
|
|
248
|
+
const SAVE_CTX: VaultSaveInboundContext = {
|
|
249
|
+
agent: 'marko',
|
|
250
|
+
key: 'brevo/api-key',
|
|
251
|
+
chat_id: '-1001234567890',
|
|
252
|
+
}
|
|
253
|
+
const THREAD = 4242
|
|
254
|
+
|
|
255
|
+
const grantBuilders = [
|
|
256
|
+
{
|
|
257
|
+
name: 'approved',
|
|
258
|
+
build: (ctx: VaultGrantInboundContext) =>
|
|
259
|
+
buildVaultGrantApprovedInbound({ ctx, grantId: 'vg_x', stageId: 's', operatorId: '1' }),
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: 'denied',
|
|
263
|
+
build: (ctx: VaultGrantInboundContext) =>
|
|
264
|
+
buildVaultGrantDeniedInbound({ ctx, stageId: 's', operatorId: '1' }),
|
|
265
|
+
},
|
|
266
|
+
]
|
|
267
|
+
const saveBuilders = [
|
|
268
|
+
{
|
|
269
|
+
name: 'save-completed',
|
|
270
|
+
build: (ctx: VaultSaveInboundContext) =>
|
|
271
|
+
buildVaultSaveCompletedInbound({ ctx, stageId: 's', operatorId: '1' }),
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
name: 'save-failed',
|
|
275
|
+
build: (ctx: VaultSaveInboundContext) =>
|
|
276
|
+
buildVaultSaveFailedInbound({ ctx, stageId: 's', operatorId: '1', reason: 'disk full' }),
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: 'save-discarded',
|
|
280
|
+
build: (ctx: VaultSaveInboundContext) =>
|
|
281
|
+
buildVaultSaveDiscardedInbound({ ctx, stageId: 's', operatorId: '1' }),
|
|
282
|
+
},
|
|
283
|
+
]
|
|
284
|
+
|
|
285
|
+
for (const { name, build } of grantBuilders) {
|
|
286
|
+
it(`${name}: threadId set → top-level threadId + meta.message_thread_id (stringified)`, () => {
|
|
287
|
+
const msg = build({ ...CTX_READ, threadId: THREAD })
|
|
288
|
+
expect(msg.threadId).toBe(THREAD)
|
|
289
|
+
expect(msg.meta?.message_thread_id).toBe(String(THREAD))
|
|
290
|
+
})
|
|
291
|
+
it(`${name}: threadId absent → both omitted (DM stays thread-less)`, () => {
|
|
292
|
+
const msg = build(CTX_READ)
|
|
293
|
+
expect(msg.threadId).toBeUndefined()
|
|
294
|
+
expect(msg.meta?.message_thread_id).toBeUndefined()
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
for (const { name, build } of saveBuilders) {
|
|
299
|
+
it(`${name}: threadId set → top-level threadId + meta.message_thread_id (stringified)`, () => {
|
|
300
|
+
const msg = build({ ...SAVE_CTX, threadId: THREAD })
|
|
301
|
+
expect(msg.threadId).toBe(THREAD)
|
|
302
|
+
expect(msg.meta?.message_thread_id).toBe(String(THREAD))
|
|
303
|
+
})
|
|
304
|
+
it(`${name}: threadId absent → both omitted (DM stays thread-less)`, () => {
|
|
305
|
+
const msg = build(SAVE_CTX)
|
|
306
|
+
expect(msg.threadId).toBeUndefined()
|
|
307
|
+
expect(msg.meta?.message_thread_id).toBeUndefined()
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
})
|