paperclip-plugin-telegram 0.2.0 → 0.2.2
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/README.md +68 -4
- package/dist/acp-bridge.d.ts +34 -0
- package/dist/acp-bridge.js +805 -0
- package/dist/acp-bridge.js.map +1 -0
- package/dist/adapter.d.ts +35 -0
- package/dist/adapter.js +75 -0
- package/dist/adapter.js.map +1 -0
- package/dist/command-registry.d.ts +3 -0
- package/dist/command-registry.js +273 -0
- package/dist/command-registry.js.map +1 -0
- package/dist/commands.d.ts +10 -0
- package/dist/commands.js +213 -0
- package/dist/commands.js.map +1 -0
- package/dist/constants.d.ts +44 -0
- package/dist/constants.js +48 -0
- package/dist/constants.js.map +1 -0
- package/dist/escalation.d.ts +41 -0
- package/dist/escalation.js +254 -0
- package/dist/escalation.js.map +1 -0
- package/dist/formatters.d.ts +13 -0
- package/dist/formatters.js +130 -0
- package/dist/formatters.js.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.d.ts +3 -0
- package/dist/manifest.js +230 -0
- package/dist/manifest.js.map +1 -0
- package/dist/media-pipeline.d.ts +46 -0
- package/dist/media-pipeline.js +161 -0
- package/dist/media-pipeline.js.map +1 -0
- package/dist/telegram-api.d.ts +28 -0
- package/dist/telegram-api.js +147 -0
- package/dist/telegram-api.js.map +1 -0
- package/dist/watch-registry.d.ts +9 -0
- package/dist/watch-registry.js +272 -0
- package/dist/watch-registry.js.map +1 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +548 -0
- package/dist/worker.js.map +1 -0
- package/package.json +7 -3
- package/src/acp-bridge.ts +0 -1273
- package/src/adapter.ts +0 -129
- package/src/command-registry.ts +0 -482
- package/src/commands.ts +0 -346
- package/src/constants.ts +0 -51
- package/src/escalation.ts +0 -421
- package/src/formatters.ts +0 -148
- package/src/manifest.ts +0 -246
- package/src/media-pipeline.ts +0 -234
- package/src/telegram-api.ts +0 -202
- package/src/watch-registry.ts +0 -369
- package/src/worker.ts +0 -783
- package/tests/acp-bridge.test.ts +0 -314
- package/tests/command-registry.test.ts +0 -283
- package/tests/commands.test.ts +0 -213
- package/tests/escalation.test.ts +0 -550
- package/tests/formatters.test.ts +0 -185
- package/tests/media-pipeline.test.ts +0 -324
- package/tests/telegram-api.test.ts +0 -108
- package/tests/watch-registry.test.ts +0 -404
- package/tsconfig.json +0 -16
- /package/{src/index.ts → dist/index.d.ts} +0 -0
package/tests/escalation.test.ts
DELETED
|
@@ -1,550 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { EscalationManager } from "../src/escalation.js";
|
|
3
|
-
import type { EscalationEvent } from "../src/escalation.js";
|
|
4
|
-
import type { PluginContext } from "@paperclipai/plugin-sdk";
|
|
5
|
-
|
|
6
|
-
let sentMessages: Array<{ chatId: string; text: string; options?: Record<string, unknown> }> = [];
|
|
7
|
-
let editedMessages: Array<{ chatId: string; messageId: number; text: string; options?: Record<string, unknown> }> = [];
|
|
8
|
-
let stateStore: Record<string, unknown> = {};
|
|
9
|
-
let emittedEvents: Array<{ event: string; companyId: string; payload: unknown }> = [];
|
|
10
|
-
|
|
11
|
-
vi.mock("../src/telegram-api.js", async () => {
|
|
12
|
-
const actual = await vi.importActual("../src/telegram-api.js") as Record<string, unknown>;
|
|
13
|
-
return {
|
|
14
|
-
...actual,
|
|
15
|
-
sendMessage: vi.fn(async (_ctx: unknown, _token: string, chatId: string, text: string, options?: Record<string, unknown>) => {
|
|
16
|
-
sentMessages.push({ chatId, text, options });
|
|
17
|
-
return 42;
|
|
18
|
-
}),
|
|
19
|
-
editMessage: vi.fn(async (_ctx: unknown, _token: string, chatId: string, messageId: number, text: string, options?: Record<string, unknown>) => {
|
|
20
|
-
editedMessages.push({ chatId, messageId, text, options });
|
|
21
|
-
return true;
|
|
22
|
-
}),
|
|
23
|
-
};
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
function mockCtx(): PluginContext {
|
|
27
|
-
return {
|
|
28
|
-
http: { fetch: vi.fn() },
|
|
29
|
-
metrics: { write: vi.fn() },
|
|
30
|
-
state: {
|
|
31
|
-
get: vi.fn(async (key: { stateKey: string }) => stateStore[key.stateKey] ?? null),
|
|
32
|
-
set: vi.fn(async (key: { stateKey: string }, value: unknown) => {
|
|
33
|
-
stateStore[key.stateKey] = value;
|
|
34
|
-
}),
|
|
35
|
-
},
|
|
36
|
-
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
37
|
-
events: {
|
|
38
|
-
emit: vi.fn((event: string, companyId: string, payload: unknown) => {
|
|
39
|
-
emittedEvents.push({ event, companyId, payload });
|
|
40
|
-
}),
|
|
41
|
-
},
|
|
42
|
-
agents: {
|
|
43
|
-
sessions: {
|
|
44
|
-
sendMessage: vi.fn(),
|
|
45
|
-
close: vi.fn(),
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
} as unknown as PluginContext;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function makeEvent(overrides: Partial<EscalationEvent> = {}): EscalationEvent {
|
|
52
|
-
return {
|
|
53
|
-
escalationId: "esc-001",
|
|
54
|
-
agentId: "agent-1",
|
|
55
|
-
companyId: "company-1",
|
|
56
|
-
reason: "low_confidence",
|
|
57
|
-
context: {
|
|
58
|
-
conversationHistory: [{ role: "user", text: "Help me" }],
|
|
59
|
-
agentReasoning: "I'm not sure about this",
|
|
60
|
-
suggestedActions: ["Forward to support"],
|
|
61
|
-
suggestedReply: "Let me connect you with a human.",
|
|
62
|
-
confidenceScore: 0.3,
|
|
63
|
-
},
|
|
64
|
-
timeout: {
|
|
65
|
-
durationMs: 60000,
|
|
66
|
-
defaultAction: "defer",
|
|
67
|
-
},
|
|
68
|
-
originChatId: "origin-chat-1",
|
|
69
|
-
originThreadId: "origin-thread-1",
|
|
70
|
-
originMessageId: "origin-msg-1",
|
|
71
|
-
transport: "native",
|
|
72
|
-
sessionId: "session-1",
|
|
73
|
-
...overrides,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
beforeEach(() => {
|
|
78
|
-
sentMessages = [];
|
|
79
|
-
editedMessages = [];
|
|
80
|
-
stateStore = {};
|
|
81
|
-
emittedEvents = [];
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe("EscalationManager.create", () => {
|
|
85
|
-
it("sends an escalation message with MarkdownV2 formatting", async () => {
|
|
86
|
-
const manager = new EscalationManager();
|
|
87
|
-
const ctx = mockCtx();
|
|
88
|
-
await manager.create(ctx, "token", makeEvent(), "esc-chat-1");
|
|
89
|
-
|
|
90
|
-
expect(sentMessages.length).toBe(1);
|
|
91
|
-
expect(sentMessages[0].chatId).toBe("esc-chat-1");
|
|
92
|
-
expect(sentMessages[0].text).toContain("Escalation");
|
|
93
|
-
expect(sentMessages[0].text).toContain("Low Confidence");
|
|
94
|
-
expect(sentMessages[0].options).toMatchObject({ parseMode: "MarkdownV2" });
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("includes confidence score percentage", async () => {
|
|
98
|
-
const manager = new EscalationManager();
|
|
99
|
-
const ctx = mockCtx();
|
|
100
|
-
await manager.create(ctx, "token", makeEvent({ context: {
|
|
101
|
-
conversationHistory: [],
|
|
102
|
-
agentReasoning: "test",
|
|
103
|
-
suggestedActions: [],
|
|
104
|
-
confidenceScore: 0.72,
|
|
105
|
-
}}), "esc-chat-1");
|
|
106
|
-
|
|
107
|
-
expect(sentMessages[0].text).toContain("72%");
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("omits confidence when not provided", async () => {
|
|
111
|
-
const manager = new EscalationManager();
|
|
112
|
-
const ctx = mockCtx();
|
|
113
|
-
await manager.create(ctx, "token", makeEvent({ context: {
|
|
114
|
-
conversationHistory: [],
|
|
115
|
-
agentReasoning: "test",
|
|
116
|
-
suggestedActions: [],
|
|
117
|
-
confidenceScore: undefined,
|
|
118
|
-
}}), "esc-chat-1");
|
|
119
|
-
|
|
120
|
-
expect(sentMessages[0].text).not.toContain("%");
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("includes suggested reply button when suggestedReply is provided", async () => {
|
|
124
|
-
const manager = new EscalationManager();
|
|
125
|
-
const ctx = mockCtx();
|
|
126
|
-
await manager.create(ctx, "token", makeEvent(), "esc-chat-1");
|
|
127
|
-
|
|
128
|
-
const keyboard = sentMessages[0].options?.inlineKeyboard as Array<Array<{ text: string; callback_data: string }>>;
|
|
129
|
-
expect(keyboard).toBeDefined();
|
|
130
|
-
// First row should be the suggested reply button
|
|
131
|
-
const suggestedBtn = keyboard[0].find((b: { text: string }) => b.text === "Send Suggested Reply");
|
|
132
|
-
expect(suggestedBtn).toBeDefined();
|
|
133
|
-
expect(suggestedBtn!.callback_data).toBe("esc_suggested_esc-001");
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("omits suggested reply button when no suggestedReply", async () => {
|
|
137
|
-
const manager = new EscalationManager();
|
|
138
|
-
const ctx = mockCtx();
|
|
139
|
-
await manager.create(ctx, "token", makeEvent({ context: {
|
|
140
|
-
conversationHistory: [],
|
|
141
|
-
agentReasoning: "test",
|
|
142
|
-
suggestedActions: [],
|
|
143
|
-
suggestedReply: undefined,
|
|
144
|
-
}}), "esc-chat-1");
|
|
145
|
-
|
|
146
|
-
const keyboard = sentMessages[0].options?.inlineKeyboard as Array<Array<{ text: string; callback_data: string }>>;
|
|
147
|
-
const allButtons = keyboard.flat();
|
|
148
|
-
expect(allButtons.find((b: { text: string }) => b.text === "Send Suggested Reply")).toBeUndefined();
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it("always includes Reply, Override, and Dismiss buttons", async () => {
|
|
152
|
-
const manager = new EscalationManager();
|
|
153
|
-
const ctx = mockCtx();
|
|
154
|
-
await manager.create(ctx, "token", makeEvent(), "esc-chat-1");
|
|
155
|
-
|
|
156
|
-
const keyboard = sentMessages[0].options?.inlineKeyboard as Array<Array<{ text: string; callback_data: string }>>;
|
|
157
|
-
const allButtons = keyboard.flat();
|
|
158
|
-
expect(allButtons.find((b: { text: string }) => b.text === "Reply")).toBeDefined();
|
|
159
|
-
expect(allButtons.find((b: { text: string }) => b.text === "Override")).toBeDefined();
|
|
160
|
-
expect(allButtons.find((b: { text: string }) => b.text === "Dismiss")).toBeDefined();
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("stores escalation state in ctx.state", async () => {
|
|
164
|
-
const manager = new EscalationManager();
|
|
165
|
-
const ctx = mockCtx();
|
|
166
|
-
await manager.create(ctx, "token", makeEvent(), "esc-chat-1");
|
|
167
|
-
|
|
168
|
-
const stored = stateStore["escalation_esc-001"] as Record<string, unknown>;
|
|
169
|
-
expect(stored).toBeDefined();
|
|
170
|
-
expect(stored.escalationId).toBe("esc-001");
|
|
171
|
-
expect(stored.status).toBe("pending");
|
|
172
|
-
expect(stored.agentId).toBe("agent-1");
|
|
173
|
-
expect(stored.reason).toBe("low_confidence");
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("adds escalation id to pending list", async () => {
|
|
177
|
-
const manager = new EscalationManager();
|
|
178
|
-
const ctx = mockCtx();
|
|
179
|
-
await manager.create(ctx, "token", makeEvent(), "esc-chat-1");
|
|
180
|
-
|
|
181
|
-
const pendingIds = stateStore["escalation_pending_ids"] as string[];
|
|
182
|
-
expect(pendingIds).toContain("esc-001");
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it("appends to existing pending list", async () => {
|
|
186
|
-
stateStore["escalation_pending_ids"] = ["esc-000"];
|
|
187
|
-
const manager = new EscalationManager();
|
|
188
|
-
const ctx = mockCtx();
|
|
189
|
-
await manager.create(ctx, "token", makeEvent(), "esc-chat-1");
|
|
190
|
-
|
|
191
|
-
const pendingIds = stateStore["escalation_pending_ids"] as string[];
|
|
192
|
-
expect(pendingIds).toEqual(["esc-000", "esc-001"]);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it("includes suggested actions in message", async () => {
|
|
196
|
-
const manager = new EscalationManager();
|
|
197
|
-
const ctx = mockCtx();
|
|
198
|
-
await manager.create(ctx, "token", makeEvent({ context: {
|
|
199
|
-
conversationHistory: [],
|
|
200
|
-
agentReasoning: "test",
|
|
201
|
-
suggestedActions: ["Action 1", "Action 2"],
|
|
202
|
-
}}), "esc-chat-1");
|
|
203
|
-
|
|
204
|
-
expect(sentMessages[0].text).toContain("Action 1");
|
|
205
|
-
expect(sentMessages[0].text).toContain("Action 2");
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it("maps all four escalation reasons to labels", async () => {
|
|
209
|
-
const manager = new EscalationManager();
|
|
210
|
-
|
|
211
|
-
for (const [reason, label] of [
|
|
212
|
-
["low_confidence", "Low Confidence"],
|
|
213
|
-
["explicit_request", "User Requested Human"],
|
|
214
|
-
["policy_violation", "Policy Violation"],
|
|
215
|
-
["unknown_intent", "Unknown Intent"],
|
|
216
|
-
] as const) {
|
|
217
|
-
sentMessages = [];
|
|
218
|
-
const ctx = mockCtx();
|
|
219
|
-
await manager.create(ctx, "token", makeEvent({ reason }), "chat");
|
|
220
|
-
expect(sentMessages[0].text).toContain(label.replace(/[_*[\]()~`>#+\-=|{}.!\\]/g, "\\$&"));
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it("stores transport and sessionId in escalation state", async () => {
|
|
225
|
-
const manager = new EscalationManager();
|
|
226
|
-
const ctx = mockCtx();
|
|
227
|
-
await manager.create(ctx, "token", makeEvent({ transport: "acp", sessionId: "sess-acp" }), "esc-chat-1");
|
|
228
|
-
|
|
229
|
-
const stored = stateStore["escalation_esc-001"] as Record<string, unknown>;
|
|
230
|
-
expect(stored.transport).toBe("acp");
|
|
231
|
-
expect(stored.sessionId).toBe("sess-acp");
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
describe("EscalationManager.handleCallback - callback data parsing", () => {
|
|
236
|
-
it("handles esc_suggested action with suggested reply", async () => {
|
|
237
|
-
const manager = new EscalationManager();
|
|
238
|
-
const ctx = mockCtx();
|
|
239
|
-
|
|
240
|
-
stateStore["escalation_esc-001"] = {
|
|
241
|
-
escalationId: "esc-001",
|
|
242
|
-
agentId: "agent-1",
|
|
243
|
-
companyId: "company-1",
|
|
244
|
-
reason: "low_confidence",
|
|
245
|
-
agentReasoning: "unsure",
|
|
246
|
-
suggestedReply: "Here is help",
|
|
247
|
-
suggestedActions: [],
|
|
248
|
-
escalationChatId: "esc-chat-1",
|
|
249
|
-
escalationMessageId: "42",
|
|
250
|
-
status: "pending",
|
|
251
|
-
createdAt: new Date().toISOString(),
|
|
252
|
-
timeoutAt: new Date(Date.now() + 60000).toISOString(),
|
|
253
|
-
defaultAction: "defer",
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
await manager.handleCallback(ctx, "token", "suggested", "esc-001", "user-1", "cbq-1", "esc-chat-1", 42);
|
|
257
|
-
|
|
258
|
-
// Should resolve and edit message
|
|
259
|
-
const stored = stateStore["escalation_esc-001"] as Record<string, unknown>;
|
|
260
|
-
expect(stored.status).toBe("resolved");
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it("handles esc_reply action by editing message to awaiting reply", async () => {
|
|
264
|
-
const manager = new EscalationManager();
|
|
265
|
-
const ctx = mockCtx();
|
|
266
|
-
|
|
267
|
-
stateStore["escalation_esc-001"] = {
|
|
268
|
-
escalationId: "esc-001",
|
|
269
|
-
agentId: "agent-1",
|
|
270
|
-
companyId: "company-1",
|
|
271
|
-
reason: "low_confidence",
|
|
272
|
-
agentReasoning: "unsure",
|
|
273
|
-
suggestedActions: [],
|
|
274
|
-
escalationChatId: "esc-chat-1",
|
|
275
|
-
escalationMessageId: "42",
|
|
276
|
-
status: "pending",
|
|
277
|
-
createdAt: new Date().toISOString(),
|
|
278
|
-
timeoutAt: new Date(Date.now() + 60000).toISOString(),
|
|
279
|
-
defaultAction: "defer",
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
await manager.handleCallback(ctx, "token", "reply", "esc-001", "user-1", "cbq-1", "esc-chat-1", 42);
|
|
283
|
-
|
|
284
|
-
expect(editedMessages.length).toBe(1);
|
|
285
|
-
expect(editedMessages[0].text).toContain("Awaiting your reply");
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
it("handles esc_dismiss action", async () => {
|
|
289
|
-
const manager = new EscalationManager();
|
|
290
|
-
const ctx = mockCtx();
|
|
291
|
-
|
|
292
|
-
stateStore["escalation_esc-001"] = {
|
|
293
|
-
escalationId: "esc-001",
|
|
294
|
-
agentId: "agent-1",
|
|
295
|
-
companyId: "company-1",
|
|
296
|
-
reason: "low_confidence",
|
|
297
|
-
agentReasoning: "test",
|
|
298
|
-
suggestedActions: [],
|
|
299
|
-
escalationChatId: "esc-chat-1",
|
|
300
|
-
escalationMessageId: "42",
|
|
301
|
-
status: "pending",
|
|
302
|
-
createdAt: new Date().toISOString(),
|
|
303
|
-
timeoutAt: new Date(Date.now() + 60000).toISOString(),
|
|
304
|
-
defaultAction: "defer",
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
await manager.handleCallback(ctx, "token", "dismiss", "esc-001", "user-1", "cbq-1", "esc-chat-1", 42);
|
|
308
|
-
|
|
309
|
-
const stored = stateStore["escalation_esc-001"] as Record<string, unknown>;
|
|
310
|
-
expect(stored.status).toBe("resolved");
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
it("ignores callback for non-pending escalation", async () => {
|
|
314
|
-
const manager = new EscalationManager();
|
|
315
|
-
const ctx = mockCtx();
|
|
316
|
-
|
|
317
|
-
stateStore["escalation_esc-001"] = {
|
|
318
|
-
escalationId: "esc-001",
|
|
319
|
-
status: "resolved",
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
await manager.handleCallback(ctx, "token", "dismiss", "esc-001", "user-1", "cbq-1", "esc-chat-1", 42);
|
|
323
|
-
|
|
324
|
-
// Nothing should happen
|
|
325
|
-
expect(editedMessages.length).toBe(0);
|
|
326
|
-
expect(emittedEvents.length).toBe(0);
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
it("ignores callback for non-existent escalation", async () => {
|
|
330
|
-
const manager = new EscalationManager();
|
|
331
|
-
const ctx = mockCtx();
|
|
332
|
-
|
|
333
|
-
await manager.handleCallback(ctx, "token", "dismiss", "nonexistent", "user-1", "cbq-1", "chat", 42);
|
|
334
|
-
|
|
335
|
-
expect(editedMessages.length).toBe(0);
|
|
336
|
-
});
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
describe("EscalationManager.checkTimeouts", () => {
|
|
340
|
-
it("times out escalation that has exceeded timeout", async () => {
|
|
341
|
-
const manager = new EscalationManager();
|
|
342
|
-
const ctx = mockCtx();
|
|
343
|
-
|
|
344
|
-
stateStore["escalation_pending_ids"] = ["esc-001"];
|
|
345
|
-
stateStore["escalation_esc-001"] = {
|
|
346
|
-
escalationId: "esc-001",
|
|
347
|
-
agentId: "agent-1",
|
|
348
|
-
companyId: "company-1",
|
|
349
|
-
reason: "low_confidence",
|
|
350
|
-
agentReasoning: "test",
|
|
351
|
-
suggestedActions: [],
|
|
352
|
-
escalationChatId: "esc-chat-1",
|
|
353
|
-
escalationMessageId: "42",
|
|
354
|
-
status: "pending",
|
|
355
|
-
createdAt: new Date(Date.now() - 120000).toISOString(),
|
|
356
|
-
timeoutAt: new Date(Date.now() - 60000).toISOString(), // already timed out
|
|
357
|
-
defaultAction: "defer",
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
await manager.checkTimeouts(ctx, "token");
|
|
361
|
-
|
|
362
|
-
const stored = stateStore["escalation_esc-001"] as Record<string, unknown>;
|
|
363
|
-
expect(stored.status).toBe("timed_out");
|
|
364
|
-
expect(editedMessages.length).toBe(1);
|
|
365
|
-
expect(editedMessages[0].text).toContain("Timed Out");
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
it("does not time out escalation that has not exceeded timeout", async () => {
|
|
369
|
-
const manager = new EscalationManager();
|
|
370
|
-
const ctx = mockCtx();
|
|
371
|
-
|
|
372
|
-
stateStore["escalation_pending_ids"] = ["esc-001"];
|
|
373
|
-
stateStore["escalation_esc-001"] = {
|
|
374
|
-
escalationId: "esc-001",
|
|
375
|
-
agentId: "agent-1",
|
|
376
|
-
companyId: "company-1",
|
|
377
|
-
reason: "low_confidence",
|
|
378
|
-
agentReasoning: "test",
|
|
379
|
-
suggestedActions: [],
|
|
380
|
-
escalationChatId: "esc-chat-1",
|
|
381
|
-
escalationMessageId: "42",
|
|
382
|
-
status: "pending",
|
|
383
|
-
createdAt: new Date().toISOString(),
|
|
384
|
-
timeoutAt: new Date(Date.now() + 60000).toISOString(), // future
|
|
385
|
-
defaultAction: "defer",
|
|
386
|
-
};
|
|
387
|
-
|
|
388
|
-
await manager.checkTimeouts(ctx, "token");
|
|
389
|
-
|
|
390
|
-
const stored = stateStore["escalation_esc-001"] as Record<string, unknown>;
|
|
391
|
-
expect(stored.status).toBe("pending");
|
|
392
|
-
expect(editedMessages.length).toBe(0);
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
it("auto-replies on timeout when defaultAction is auto_reply", async () => {
|
|
396
|
-
const manager = new EscalationManager();
|
|
397
|
-
const ctx = mockCtx();
|
|
398
|
-
|
|
399
|
-
stateStore["escalation_pending_ids"] = ["esc-001"];
|
|
400
|
-
stateStore["escalation_esc-001"] = {
|
|
401
|
-
escalationId: "esc-001",
|
|
402
|
-
agentId: "agent-1",
|
|
403
|
-
companyId: "company-1",
|
|
404
|
-
reason: "low_confidence",
|
|
405
|
-
agentReasoning: "test",
|
|
406
|
-
suggestedReply: "Auto response text",
|
|
407
|
-
suggestedActions: [],
|
|
408
|
-
escalationChatId: "esc-chat-1",
|
|
409
|
-
escalationMessageId: "42",
|
|
410
|
-
status: "pending",
|
|
411
|
-
createdAt: new Date(Date.now() - 120000).toISOString(),
|
|
412
|
-
timeoutAt: new Date(Date.now() - 60000).toISOString(),
|
|
413
|
-
defaultAction: "auto_reply",
|
|
414
|
-
originChatId: "origin-chat",
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
await manager.checkTimeouts(ctx, "token");
|
|
418
|
-
|
|
419
|
-
// Should have sent auto-reply to origin chat
|
|
420
|
-
expect(sentMessages.some(m => m.chatId === "origin-chat")).toBe(true);
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
it("removes timed-out escalation from pending list", async () => {
|
|
424
|
-
const manager = new EscalationManager();
|
|
425
|
-
const ctx = mockCtx();
|
|
426
|
-
|
|
427
|
-
stateStore["escalation_pending_ids"] = ["esc-001", "esc-002"];
|
|
428
|
-
stateStore["escalation_esc-001"] = {
|
|
429
|
-
escalationId: "esc-001",
|
|
430
|
-
agentId: "agent-1",
|
|
431
|
-
companyId: "company-1",
|
|
432
|
-
reason: "low_confidence",
|
|
433
|
-
agentReasoning: "test",
|
|
434
|
-
suggestedActions: [],
|
|
435
|
-
escalationChatId: "esc-chat-1",
|
|
436
|
-
escalationMessageId: "42",
|
|
437
|
-
status: "pending",
|
|
438
|
-
createdAt: new Date(Date.now() - 120000).toISOString(),
|
|
439
|
-
timeoutAt: new Date(Date.now() - 60000).toISOString(),
|
|
440
|
-
defaultAction: "defer",
|
|
441
|
-
};
|
|
442
|
-
stateStore["escalation_esc-002"] = {
|
|
443
|
-
escalationId: "esc-002",
|
|
444
|
-
status: "pending",
|
|
445
|
-
timeoutAt: new Date(Date.now() + 60000).toISOString(),
|
|
446
|
-
escalationChatId: "chat",
|
|
447
|
-
escalationMessageId: "99",
|
|
448
|
-
agentId: "a",
|
|
449
|
-
companyId: "c",
|
|
450
|
-
reason: "low_confidence",
|
|
451
|
-
agentReasoning: "x",
|
|
452
|
-
suggestedActions: [],
|
|
453
|
-
createdAt: new Date().toISOString(),
|
|
454
|
-
defaultAction: "defer",
|
|
455
|
-
};
|
|
456
|
-
|
|
457
|
-
await manager.checkTimeouts(ctx, "token");
|
|
458
|
-
|
|
459
|
-
const pendingIds = stateStore["escalation_pending_ids"] as string[];
|
|
460
|
-
expect(pendingIds).not.toContain("esc-001");
|
|
461
|
-
expect(pendingIds).toContain("esc-002");
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
it("does nothing when pending list is empty", async () => {
|
|
465
|
-
const manager = new EscalationManager();
|
|
466
|
-
const ctx = mockCtx();
|
|
467
|
-
|
|
468
|
-
await manager.checkTimeouts(ctx, "token");
|
|
469
|
-
|
|
470
|
-
expect(editedMessages.length).toBe(0);
|
|
471
|
-
expect(sentMessages.length).toBe(0);
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
it("emits escalation.timed_out event", async () => {
|
|
475
|
-
const manager = new EscalationManager();
|
|
476
|
-
const ctx = mockCtx();
|
|
477
|
-
|
|
478
|
-
stateStore["escalation_pending_ids"] = ["esc-001"];
|
|
479
|
-
stateStore["escalation_esc-001"] = {
|
|
480
|
-
escalationId: "esc-001",
|
|
481
|
-
agentId: "agent-1",
|
|
482
|
-
companyId: "company-1",
|
|
483
|
-
reason: "low_confidence",
|
|
484
|
-
agentReasoning: "test",
|
|
485
|
-
suggestedActions: [],
|
|
486
|
-
escalationChatId: "esc-chat-1",
|
|
487
|
-
escalationMessageId: "42",
|
|
488
|
-
status: "pending",
|
|
489
|
-
createdAt: new Date(Date.now() - 120000).toISOString(),
|
|
490
|
-
timeoutAt: new Date(Date.now() - 60000).toISOString(),
|
|
491
|
-
defaultAction: "defer",
|
|
492
|
-
};
|
|
493
|
-
|
|
494
|
-
await manager.checkTimeouts(ctx, "token");
|
|
495
|
-
|
|
496
|
-
expect(emittedEvents.some(e => e.event === "escalation.timed_out")).toBe(true);
|
|
497
|
-
});
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
describe("EscalationManager.respond", () => {
|
|
501
|
-
it("resolves a pending escalation", async () => {
|
|
502
|
-
const manager = new EscalationManager();
|
|
503
|
-
const ctx = mockCtx();
|
|
504
|
-
|
|
505
|
-
stateStore["escalation_esc-001"] = {
|
|
506
|
-
escalationId: "esc-001",
|
|
507
|
-
agentId: "agent-1",
|
|
508
|
-
companyId: "company-1",
|
|
509
|
-
reason: "low_confidence",
|
|
510
|
-
agentReasoning: "test",
|
|
511
|
-
suggestedActions: [],
|
|
512
|
-
escalationChatId: "esc-chat-1",
|
|
513
|
-
escalationMessageId: "42",
|
|
514
|
-
status: "pending",
|
|
515
|
-
createdAt: new Date().toISOString(),
|
|
516
|
-
timeoutAt: new Date(Date.now() + 60000).toISOString(),
|
|
517
|
-
defaultAction: "defer",
|
|
518
|
-
};
|
|
519
|
-
stateStore["escalation_pending_ids"] = ["esc-001"];
|
|
520
|
-
|
|
521
|
-
await manager.respond(ctx, "token", "esc-001", {
|
|
522
|
-
escalationId: "esc-001",
|
|
523
|
-
responderId: "user-1",
|
|
524
|
-
responseText: "Here is the answer",
|
|
525
|
-
action: "reply_to_customer",
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
const stored = stateStore["escalation_esc-001"] as Record<string, unknown>;
|
|
529
|
-
expect(stored.status).toBe("resolved");
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
it("ignores respond for non-pending escalation", async () => {
|
|
533
|
-
const manager = new EscalationManager();
|
|
534
|
-
const ctx = mockCtx();
|
|
535
|
-
|
|
536
|
-
stateStore["escalation_esc-001"] = {
|
|
537
|
-
escalationId: "esc-001",
|
|
538
|
-
status: "resolved",
|
|
539
|
-
};
|
|
540
|
-
|
|
541
|
-
await manager.respond(ctx, "token", "esc-001", {
|
|
542
|
-
escalationId: "esc-001",
|
|
543
|
-
responderId: "user-1",
|
|
544
|
-
responseText: "text",
|
|
545
|
-
action: "reply_to_customer",
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
expect(editedMessages.length).toBe(0);
|
|
549
|
-
});
|
|
550
|
-
});
|