@easyoref/agent 1.21.1
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/__tests__/clarify.test.ts +827 -0
- package/__tests__/config.test.ts +304 -0
- package/__tests__/enrichment.integration.test.ts +871 -0
- package/__tests__/graph.test.ts +661 -0
- package/dist/auth.d.ts +11 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +54 -0
- package/dist/auth.js.map +1 -0
- package/dist/dry-run.d.ts +12 -0
- package/dist/dry-run.d.ts.map +1 -0
- package/dist/dry-run.js +236 -0
- package/dist/dry-run.js.map +1 -0
- package/dist/extract.d.ts +180 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +210 -0
- package/dist/extract.js.map +1 -0
- package/dist/graph.d.ts +4083 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/graph.js +162 -0
- package/dist/graph.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/models.d.ts +7 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +18 -0
- package/dist/models.js.map +1 -0
- package/dist/nodes/clarify-node.d.ts +132 -0
- package/dist/nodes/clarify-node.d.ts.map +1 -0
- package/dist/nodes/clarify-node.js +118 -0
- package/dist/nodes/clarify-node.js.map +1 -0
- package/dist/nodes/clarify.d.ts +6 -0
- package/dist/nodes/clarify.d.ts.map +1 -0
- package/dist/nodes/clarify.js +124 -0
- package/dist/nodes/clarify.js.map +1 -0
- package/dist/nodes/edit-node.d.ts +71 -0
- package/dist/nodes/edit-node.d.ts.map +1 -0
- package/dist/nodes/edit-node.js +496 -0
- package/dist/nodes/edit-node.js.map +1 -0
- package/dist/nodes/edit.d.ts +6 -0
- package/dist/nodes/edit.d.ts.map +1 -0
- package/dist/nodes/edit.js +22 -0
- package/dist/nodes/edit.js.map +1 -0
- package/dist/nodes/extract-node.d.ts +174 -0
- package/dist/nodes/extract-node.d.ts.map +1 -0
- package/dist/nodes/extract-node.js +233 -0
- package/dist/nodes/extract-node.js.map +1 -0
- package/dist/nodes/extract.d.ts +6 -0
- package/dist/nodes/extract.d.ts.map +1 -0
- package/dist/nodes/extract.js +49 -0
- package/dist/nodes/extract.js.map +1 -0
- package/dist/nodes/filter-agent.d.ts +11 -0
- package/dist/nodes/filter-agent.d.ts.map +1 -0
- package/dist/nodes/filter-agent.js +60 -0
- package/dist/nodes/filter-agent.js.map +1 -0
- package/dist/nodes/filter-node.d.ts +9 -0
- package/dist/nodes/filter-node.d.ts.map +1 -0
- package/dist/nodes/filter-node.js +111 -0
- package/dist/nodes/filter-node.js.map +1 -0
- package/dist/nodes/filters.d.ts +13 -0
- package/dist/nodes/filters.d.ts.map +1 -0
- package/dist/nodes/filters.js +111 -0
- package/dist/nodes/filters.js.map +1 -0
- package/dist/nodes/message-node.d.ts +71 -0
- package/dist/nodes/message-node.d.ts.map +1 -0
- package/dist/nodes/message-node.js +491 -0
- package/dist/nodes/message-node.js.map +1 -0
- package/dist/nodes/message.d.ts +71 -0
- package/dist/nodes/message.d.ts.map +1 -0
- package/dist/nodes/message.js +496 -0
- package/dist/nodes/message.js.map +1 -0
- package/dist/nodes/vote-node.d.ts +13 -0
- package/dist/nodes/vote-node.d.ts.map +1 -0
- package/dist/nodes/vote-node.js +232 -0
- package/dist/nodes/vote-node.js.map +1 -0
- package/dist/nodes/vote.d.ts +13 -0
- package/dist/nodes/vote.d.ts.map +1 -0
- package/dist/nodes/vote.js +232 -0
- package/dist/nodes/vote.js.map +1 -0
- package/dist/queue.d.ts +15 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/queue.js +41 -0
- package/dist/queue.js.map +1 -0
- package/dist/redis.d.ts +8 -0
- package/dist/redis.d.ts.map +1 -0
- package/dist/redis.js +33 -0
- package/dist/redis.js.map +1 -0
- package/dist/runtime/auth.d.ts +11 -0
- package/dist/runtime/auth.d.ts.map +1 -0
- package/dist/runtime/auth.js +54 -0
- package/dist/runtime/auth.js.map +1 -0
- package/dist/runtime/dry-run.d.ts +12 -0
- package/dist/runtime/dry-run.d.ts.map +1 -0
- package/dist/runtime/dry-run.js +236 -0
- package/dist/runtime/dry-run.js.map +1 -0
- package/dist/runtime/queue.d.ts +15 -0
- package/dist/runtime/queue.d.ts.map +1 -0
- package/dist/runtime/queue.js +41 -0
- package/dist/runtime/queue.js.map +1 -0
- package/dist/runtime/redis.d.ts +8 -0
- package/dist/runtime/redis.d.ts.map +1 -0
- package/dist/runtime/redis.js +33 -0
- package/dist/runtime/redis.js.map +1 -0
- package/dist/runtime/worker.d.ts +14 -0
- package/dist/runtime/worker.d.ts.map +1 -0
- package/dist/runtime/worker.js +135 -0
- package/dist/runtime/worker.js.map +1 -0
- package/dist/tools/alert-history.d.ts +18 -0
- package/dist/tools/alert-history.d.ts.map +1 -0
- package/dist/tools/alert-history.js +98 -0
- package/dist/tools/alert-history.js.map +1 -0
- package/dist/tools/betterstack-log.d.ts +15 -0
- package/dist/tools/betterstack-log.d.ts.map +1 -0
- package/dist/tools/betterstack-log.js +80 -0
- package/dist/tools/betterstack-log.js.map +1 -0
- package/dist/tools/index.d.ts +44 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +20 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/read-sources.d.ts +15 -0
- package/dist/tools/read-sources.d.ts.map +1 -0
- package/dist/tools/read-sources.js +67 -0
- package/dist/tools/read-sources.js.map +1 -0
- package/dist/tools/resolve-area.d.ts +19 -0
- package/dist/tools/resolve-area.d.ts.map +1 -0
- package/dist/tools/resolve-area.js +147 -0
- package/dist/tools/resolve-area.js.map +1 -0
- package/dist/tools.d.ts +115 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +439 -0
- package/dist/tools.js.map +1 -0
- package/dist/worker.d.ts +14 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +135 -0
- package/dist/worker.js.map +1 -0
- package/package.json +26 -0
- package/src/graph.ts +200 -0
- package/src/index.ts +27 -0
- package/src/models.ts +20 -0
- package/src/nodes/clarify-node.ts +172 -0
- package/src/nodes/edit-node.ts +695 -0
- package/src/nodes/extract-node.ts +299 -0
- package/src/nodes/filter-node.ts +139 -0
- package/src/nodes/message.ts +695 -0
- package/src/nodes/vote-node.ts +354 -0
- package/src/nodes/vote.ts +354 -0
- package/src/runtime/auth.ts +63 -0
- package/src/runtime/dry-run.ts +303 -0
- package/src/runtime/queue.ts +53 -0
- package/src/runtime/redis.ts +38 -0
- package/src/runtime/worker.ts +167 -0
- package/src/tools/alert-history.ts +120 -0
- package/src/tools/betterstack-log.ts +102 -0
- package/src/tools/index.ts +23 -0
- package/src/tools/read-sources.ts +86 -0
- package/src/tools/resolve-area.ts +202 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for tool calling: tools.ts, clarify.ts, and shouldClarify routing.
|
|
3
|
+
*
|
|
4
|
+
* These tests mock external dependencies (GramJS, fetch, LLM) and verify:
|
|
5
|
+
* - Tool execution and error handling
|
|
6
|
+
* - ReAct loop flow (with/without tool calls)
|
|
7
|
+
* - Conditional edge routing logic
|
|
8
|
+
* - Contradiction detection
|
|
9
|
+
* - Area proximity resolution
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
VotedResultSchema,
|
|
14
|
+
type ValidatedExtraction,
|
|
15
|
+
type VotedResult,
|
|
16
|
+
} from "@easyoref/shared";
|
|
17
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
18
|
+
|
|
19
|
+
// ── Mocks (must be defined before imports) ─────────────
|
|
20
|
+
|
|
21
|
+
vi.mock("@easyoref/shared", async () => {
|
|
22
|
+
const actual = await vi.importActual("@easyoref/shared");
|
|
23
|
+
return {
|
|
24
|
+
...actual,
|
|
25
|
+
config: {
|
|
26
|
+
agent: {
|
|
27
|
+
model: "test-model",
|
|
28
|
+
apiKey: "test-key",
|
|
29
|
+
mcpTools: true,
|
|
30
|
+
clarifyFetchCount: 3,
|
|
31
|
+
confidenceThreshold: 0.6,
|
|
32
|
+
filterModel: "google/gemini-2.5-flash-lite",
|
|
33
|
+
extractModel: "google/gemini-3.1-flash-lite-preview",
|
|
34
|
+
channels: ["@idf_telegram", "@N12LIVE", "@kann_news"],
|
|
35
|
+
areaLabels: { הרצליה: "Герцлия" },
|
|
36
|
+
},
|
|
37
|
+
botToken: "",
|
|
38
|
+
orefApiUrl: "https://mock.oref.api/alerts",
|
|
39
|
+
orefHistoryUrl: "",
|
|
40
|
+
logtailToken: "test-logtail-token",
|
|
41
|
+
areas: ["הרצליה", "תל אביב - דרום העיר ויפו"],
|
|
42
|
+
language: "ru",
|
|
43
|
+
},
|
|
44
|
+
getRedis: vi.fn().mockReturnValue({
|
|
45
|
+
lpush: vi.fn(),
|
|
46
|
+
expire: vi.fn(),
|
|
47
|
+
}),
|
|
48
|
+
pushSessionPost: vi.fn(),
|
|
49
|
+
pushChannelPost: vi.fn(),
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
vi.mock("@easyoref/monitoring", () => ({
|
|
54
|
+
info: vi.fn(),
|
|
55
|
+
warn: vi.fn(),
|
|
56
|
+
error: vi.fn(),
|
|
57
|
+
debug: vi.fn(),
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
vi.mock("@easyoref/gramjs", () => ({
|
|
61
|
+
fetchRecentChannelPosts: vi.fn(),
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
// ── Helpers ────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
function makeVotedResult(overrides: Partial<VotedResult> = {}): VotedResult {
|
|
67
|
+
return VotedResultSchema.parse({
|
|
68
|
+
etaRefinedMinutes: undefined,
|
|
69
|
+
eta_citations: [],
|
|
70
|
+
countryOrigins: [{ name: "Lebanon", citations: [1] }],
|
|
71
|
+
rocketCountMin: 10,
|
|
72
|
+
rocketCountMax: 15,
|
|
73
|
+
rocket_citations: [1, 2],
|
|
74
|
+
rocket_confidence: 0.7,
|
|
75
|
+
isCassette: undefined,
|
|
76
|
+
isCassette_confidence: 0,
|
|
77
|
+
intercepted: 8,
|
|
78
|
+
interceptedQual: undefined,
|
|
79
|
+
interceptedConfidence: 0.6,
|
|
80
|
+
seaImpact: undefined,
|
|
81
|
+
seaImpactQual: undefined,
|
|
82
|
+
sea_confidence: 0,
|
|
83
|
+
openAreaImpact: undefined,
|
|
84
|
+
openAreaImpactQual: undefined,
|
|
85
|
+
open_area_confidence: 0,
|
|
86
|
+
hitsConfirmed: 1,
|
|
87
|
+
hits_citations: [2],
|
|
88
|
+
hits_confidence: 0.4,
|
|
89
|
+
casualties: undefined,
|
|
90
|
+
casualties_citations: [],
|
|
91
|
+
casualties_confidence: 0,
|
|
92
|
+
injuries: undefined,
|
|
93
|
+
injuries_citations: [],
|
|
94
|
+
injuries_confidence: 0,
|
|
95
|
+
confidence: 0.45,
|
|
96
|
+
sourcesCount: 2,
|
|
97
|
+
citedSources: [
|
|
98
|
+
{ index: 1, channel: "@idf_telegram", messageUrl: undefined },
|
|
99
|
+
{ index: 2, channel: "@N12LIVE", messageUrl: undefined },
|
|
100
|
+
],
|
|
101
|
+
...overrides,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function makeExtraction(
|
|
106
|
+
overrides: Partial<ValidatedExtraction> = {},
|
|
107
|
+
): ValidatedExtraction {
|
|
108
|
+
return {
|
|
109
|
+
channel: "@idf_telegram",
|
|
110
|
+
regionRelevance: 0.9,
|
|
111
|
+
sourceTrust: 0.8,
|
|
112
|
+
tone: "calm",
|
|
113
|
+
timeRelevance: 0.9,
|
|
114
|
+
countryOrigin: "Lebanon",
|
|
115
|
+
rocketCount: 12,
|
|
116
|
+
isCassette: undefined,
|
|
117
|
+
intercepted: 8,
|
|
118
|
+
interceptedQual: undefined,
|
|
119
|
+
seaImpact: undefined,
|
|
120
|
+
seaImpactQual: undefined,
|
|
121
|
+
openAreaImpact: undefined,
|
|
122
|
+
openAreaImpactQual: undefined,
|
|
123
|
+
hitsConfirmed: 1,
|
|
124
|
+
casualties: undefined,
|
|
125
|
+
injuries: undefined,
|
|
126
|
+
etaRefinedMinutes: undefined,
|
|
127
|
+
confidence: 0.7,
|
|
128
|
+
valid: true,
|
|
129
|
+
...overrides,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ═════════════════════════════════════════════════════════
|
|
134
|
+
// 1. readSourcesTool
|
|
135
|
+
// ═════════════════════════════════════════════════════════
|
|
136
|
+
|
|
137
|
+
describe("readSourcesTool", () => {
|
|
138
|
+
let readSourcesTool: typeof import("../src/tools/index.js").readSourcesTool;
|
|
139
|
+
let fetchRecentChannelPosts: ReturnType<typeof vi.fn>;
|
|
140
|
+
|
|
141
|
+
beforeEach(async () => {
|
|
142
|
+
vi.resetModules();
|
|
143
|
+
const toolsMod = await import("../src/tools/index.js");
|
|
144
|
+
readSourcesTool = toolsMod.readSourcesTool;
|
|
145
|
+
const gramjsMod = await import("@easyoref/gramjs");
|
|
146
|
+
fetchRecentChannelPosts = gramjsMod.fetchRecentChannelPosts as ReturnType<
|
|
147
|
+
typeof vi.fn
|
|
148
|
+
>;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
afterEach(() => vi.restoreAllMocks());
|
|
152
|
+
|
|
153
|
+
it("returns posts from channel", async () => {
|
|
154
|
+
fetchRecentChannelPosts.mockResolvedValueOnce([
|
|
155
|
+
{
|
|
156
|
+
text: "IDF reports 12 rockets launched",
|
|
157
|
+
ts: 1700000000,
|
|
158
|
+
messageUrl: "https://t.me/idf/100",
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
text: "Iron Dome intercepted majority",
|
|
162
|
+
ts: 1700000001,
|
|
163
|
+
messageUrl: "https://t.me/idf/101",
|
|
164
|
+
},
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
const result = await readSourcesTool.invoke({
|
|
168
|
+
channel: "@idf_telegram",
|
|
169
|
+
limit: 3,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const parsed = JSON.parse(result);
|
|
173
|
+
expect(parsed.channel).toBe("@idf_telegram");
|
|
174
|
+
expect(parsed.posts).toHaveLength(2);
|
|
175
|
+
expect(parsed.posts[0].text).toContain("12 rockets");
|
|
176
|
+
expect(parsed.count).toBe(2);
|
|
177
|
+
expect(fetchRecentChannelPosts).toHaveBeenCalledWith("@idf_telegram", 3);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("limits posts to clarifyFetchCount", async () => {
|
|
181
|
+
fetchRecentChannelPosts.mockResolvedValueOnce([
|
|
182
|
+
{ text: "Post1", ts: 1, messageUrl: undefined },
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
// limit=4 (max allowed by schema) should be capped to clarifyFetchCount=3
|
|
186
|
+
await readSourcesTool.invoke({ channel: "@test", limit: 4 });
|
|
187
|
+
expect(fetchRecentChannelPosts).toHaveBeenCalledWith("@test", 3);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("truncates long texts to 800 chars", async () => {
|
|
191
|
+
const longText = "A".repeat(1500);
|
|
192
|
+
fetchRecentChannelPosts.mockResolvedValueOnce([
|
|
193
|
+
{ text: longText, ts: 1, messageUrl: undefined },
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
const result = await readSourcesTool.invoke({
|
|
197
|
+
channel: "@test",
|
|
198
|
+
limit: 1,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const parsed = JSON.parse(result);
|
|
202
|
+
expect(parsed.posts[0].text.length).toBe(800);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("returns empty array when no posts", async () => {
|
|
206
|
+
fetchRecentChannelPosts.mockResolvedValueOnce([]);
|
|
207
|
+
|
|
208
|
+
const result = await readSourcesTool.invoke({
|
|
209
|
+
channel: "@empty",
|
|
210
|
+
limit: 2,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const parsed = JSON.parse(result);
|
|
214
|
+
expect(parsed.posts).toHaveLength(0);
|
|
215
|
+
expect(parsed.note).toContain("No recent posts");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("handles FLOOD error gracefully (retry: false)", async () => {
|
|
219
|
+
fetchRecentChannelPosts.mockRejectedValueOnce(new Error("FLOOD_WAIT_420"));
|
|
220
|
+
|
|
221
|
+
const result = await readSourcesTool.invoke({
|
|
222
|
+
channel: "@flooded",
|
|
223
|
+
limit: 1,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const parsed = JSON.parse(result);
|
|
227
|
+
expect(parsed.error).toContain("Failed to fetch");
|
|
228
|
+
expect(parsed.retry).toBe(false);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("handles generic error gracefully (retry: true)", async () => {
|
|
232
|
+
fetchRecentChannelPosts.mockRejectedValueOnce(new Error("Network timeout"));
|
|
233
|
+
|
|
234
|
+
const result = await readSourcesTool.invoke({
|
|
235
|
+
channel: "@broken",
|
|
236
|
+
limit: 1,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const parsed = JSON.parse(result);
|
|
240
|
+
expect(parsed.error).toContain("Network timeout");
|
|
241
|
+
expect(parsed.retry).toBe(true);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// ═════════════════════════════════════════════════════════
|
|
246
|
+
// 2. alertHistoryTool
|
|
247
|
+
// ═════════════════════════════════════════════════════════
|
|
248
|
+
|
|
249
|
+
describe("alertHistoryTool", () => {
|
|
250
|
+
let alertHistoryTool: typeof import("../src/tools/index.js").alertHistoryTool;
|
|
251
|
+
const originalFetch = globalThis.fetch;
|
|
252
|
+
|
|
253
|
+
beforeEach(async () => {
|
|
254
|
+
vi.resetModules();
|
|
255
|
+
const toolsMod = await import("../src/tools/index.js");
|
|
256
|
+
alertHistoryTool = toolsMod.alertHistoryTool;
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
afterEach(() => {
|
|
260
|
+
globalThis.fetch = originalFetch;
|
|
261
|
+
vi.restoreAllMocks();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("returns empty when no history", async () => {
|
|
265
|
+
globalThis.fetch = vi.fn().mockResolvedValueOnce({
|
|
266
|
+
ok: true,
|
|
267
|
+
text: async () => "",
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const result = await alertHistoryTool.invoke({
|
|
271
|
+
area: "תל אביב",
|
|
272
|
+
last_minutes: 30,
|
|
273
|
+
});
|
|
274
|
+
const parsed = JSON.parse(result);
|
|
275
|
+
expect(parsed.alerts).toEqual([]);
|
|
276
|
+
expect(parsed.note).toContain("No alert history");
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("filters history by area", async () => {
|
|
280
|
+
globalThis.fetch = vi.fn().mockResolvedValueOnce({
|
|
281
|
+
ok: true,
|
|
282
|
+
text: async () =>
|
|
283
|
+
JSON.stringify([
|
|
284
|
+
{
|
|
285
|
+
alertDate: "2024-01-15 10:30",
|
|
286
|
+
title: "ירי רקטות",
|
|
287
|
+
data: "תל אביב - דרום העיר ויפו",
|
|
288
|
+
category_desc: "Missiles",
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
alertDate: "2024-01-15 10:31",
|
|
292
|
+
title: "ירי רקטות",
|
|
293
|
+
data: "חיפה - מערב",
|
|
294
|
+
category_desc: "Missiles",
|
|
295
|
+
},
|
|
296
|
+
]),
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const result = await alertHistoryTool.invoke({
|
|
300
|
+
area: "תל אביב",
|
|
301
|
+
last_minutes: 30,
|
|
302
|
+
});
|
|
303
|
+
const parsed = JSON.parse(result);
|
|
304
|
+
expect(parsed.alerts).toHaveLength(1);
|
|
305
|
+
expect(parsed.total_in_period).toBe(2);
|
|
306
|
+
expect(parsed.relevant_count).toBe(1);
|
|
307
|
+
expect(parsed.alerts[0].date).toBe("2024-01-15 10:30");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("handles HTTP error", async () => {
|
|
311
|
+
globalThis.fetch = vi.fn().mockResolvedValueOnce({
|
|
312
|
+
ok: false,
|
|
313
|
+
status: 503,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const result = await alertHistoryTool.invoke({
|
|
317
|
+
area: "test",
|
|
318
|
+
last_minutes: 30,
|
|
319
|
+
});
|
|
320
|
+
const parsed = JSON.parse(result);
|
|
321
|
+
expect(parsed.error).toContain("503");
|
|
322
|
+
expect(parsed.retry).toBe(true);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("handles network failure", async () => {
|
|
326
|
+
globalThis.fetch = vi.fn().mockRejectedValueOnce(new Error("fetch failed"));
|
|
327
|
+
|
|
328
|
+
const result = await alertHistoryTool.invoke({
|
|
329
|
+
area: "test",
|
|
330
|
+
last_minutes: 30,
|
|
331
|
+
});
|
|
332
|
+
const parsed = JSON.parse(result);
|
|
333
|
+
expect(parsed.error).toContain("fetch failed");
|
|
334
|
+
expect(parsed.retry).toBe(true);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// ═════════════════════════════════════════════════════════
|
|
339
|
+
// 3. shouldClarify routing logic
|
|
340
|
+
// ═════════════════════════════════════════════════════════
|
|
341
|
+
|
|
342
|
+
describe("shouldClarify routing", () => {
|
|
343
|
+
// Test the pure routing logic by extracting the conditions
|
|
344
|
+
// (shouldClarify is not exported, so we test its logic directly)
|
|
345
|
+
|
|
346
|
+
function shouldClarify(state: {
|
|
347
|
+
clarifyAttempted: boolean;
|
|
348
|
+
mcpToolsEnabled: boolean;
|
|
349
|
+
votedResult: VotedResult | undefined;
|
|
350
|
+
confidenceThreshold: number;
|
|
351
|
+
}): "clarify" | "editMessage" {
|
|
352
|
+
if (state.clarifyAttempted) return "editMessage";
|
|
353
|
+
if (!state.mcpToolsEnabled) return "editMessage";
|
|
354
|
+
if (!state.votedResult) return "editMessage";
|
|
355
|
+
if (state.votedResult.confidence < state.confidenceThreshold)
|
|
356
|
+
return "clarify";
|
|
357
|
+
return "editMessage";
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
it("routes to clarify when confidence < threshold", () => {
|
|
361
|
+
const result = shouldClarify({
|
|
362
|
+
clarifyAttempted: false,
|
|
363
|
+
mcpToolsEnabled: true,
|
|
364
|
+
votedResult: makeVotedResult({ confidence: 0.4 }),
|
|
365
|
+
confidenceThreshold: 0.6,
|
|
366
|
+
});
|
|
367
|
+
expect(result).toBe("clarify");
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("routes to editMessage when confidence >= threshold", () => {
|
|
371
|
+
const result = shouldClarify({
|
|
372
|
+
clarifyAttempted: false,
|
|
373
|
+
mcpToolsEnabled: true,
|
|
374
|
+
votedResult: makeVotedResult({ confidence: 0.8 }),
|
|
375
|
+
confidenceThreshold: 0.6,
|
|
376
|
+
});
|
|
377
|
+
expect(result).toBe("editMessage");
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("routes to editMessage when already clarified", () => {
|
|
381
|
+
const result = shouldClarify({
|
|
382
|
+
clarifyAttempted: true,
|
|
383
|
+
mcpToolsEnabled: true,
|
|
384
|
+
votedResult: makeVotedResult({ confidence: 0.3 }),
|
|
385
|
+
confidenceThreshold: 0.6,
|
|
386
|
+
});
|
|
387
|
+
expect(result).toBe("editMessage");
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it("routes to editMessage when MCP tools disabled", () => {
|
|
391
|
+
const result = shouldClarify({
|
|
392
|
+
clarifyAttempted: false,
|
|
393
|
+
mcpToolsEnabled: false,
|
|
394
|
+
votedResult: makeVotedResult({ confidence: 0.3 }),
|
|
395
|
+
confidenceThreshold: 0.6,
|
|
396
|
+
});
|
|
397
|
+
expect(result).toBe("editMessage");
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("routes to editMessage when votedResult is undefined", () => {
|
|
401
|
+
const result = shouldClarify({
|
|
402
|
+
clarifyAttempted: false,
|
|
403
|
+
mcpToolsEnabled: true,
|
|
404
|
+
votedResult: undefined,
|
|
405
|
+
confidenceThreshold: 0.6,
|
|
406
|
+
});
|
|
407
|
+
expect(result).toBe("editMessage");
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("routes to clarify at exact boundary (0.59 < 0.6)", () => {
|
|
411
|
+
const result = shouldClarify({
|
|
412
|
+
clarifyAttempted: false,
|
|
413
|
+
mcpToolsEnabled: true,
|
|
414
|
+
votedResult: makeVotedResult({ confidence: 0.59 }),
|
|
415
|
+
confidenceThreshold: 0.6,
|
|
416
|
+
});
|
|
417
|
+
expect(result).toBe("clarify");
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("routes to editMessage at exact threshold (0.6 >= 0.6)", () => {
|
|
421
|
+
const result = shouldClarify({
|
|
422
|
+
clarifyAttempted: false,
|
|
423
|
+
mcpToolsEnabled: true,
|
|
424
|
+
votedResult: makeVotedResult({ confidence: 0.6 }),
|
|
425
|
+
confidenceThreshold: 0.6,
|
|
426
|
+
});
|
|
427
|
+
expect(result).toBe("editMessage");
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// ═════════════════════════════════════════════════════════
|
|
432
|
+
// 4. Contradiction detection
|
|
433
|
+
// ═════════════════════════════════════════════════════════
|
|
434
|
+
|
|
435
|
+
describe("contradiction detection", () => {
|
|
436
|
+
// Extracted from clarify.ts describeContradictions logic
|
|
437
|
+
function describeContradictions(
|
|
438
|
+
_extractions: ValidatedExtraction[],
|
|
439
|
+
voted: VotedResult,
|
|
440
|
+
): string {
|
|
441
|
+
const issues: string[] = [];
|
|
442
|
+
if (voted.countryOrigins && voted.countryOrigins.length > 1) {
|
|
443
|
+
const names = voted.countryOrigins.map((c) => c.name).join(", ");
|
|
444
|
+
issues.push(`Multiple origin countries reported: ${names}`);
|
|
445
|
+
}
|
|
446
|
+
if (
|
|
447
|
+
voted.rocketCountMin !== undefined &&
|
|
448
|
+
voted.rocketCountMax !== undefined &&
|
|
449
|
+
voted.rocketCountMax - voted.rocketCountMin > 3
|
|
450
|
+
) {
|
|
451
|
+
issues.push(
|
|
452
|
+
`Wide rocket count range: ${voted.rocketCountMin}–${voted.rocketCountMax}`,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
if (voted.interceptedConfidence < 0.5 && voted.intercepted !== undefined) {
|
|
456
|
+
issues.push(
|
|
457
|
+
`Intercepted count (${
|
|
458
|
+
voted.intercepted
|
|
459
|
+
}) has low confidence: ${voted.interceptedConfidence.toFixed(2)}`,
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
if (voted.hitsConfidence < 0.5 && voted.hitsConfirmed !== undefined) {
|
|
463
|
+
issues.push(
|
|
464
|
+
`Hits confirmed (${
|
|
465
|
+
voted.hitsConfirmed
|
|
466
|
+
}) has low confidence: ${voted.hitsConfidence.toFixed(2)}`,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
issues.push(`Overall confidence: ${voted.confidence}`);
|
|
470
|
+
issues.push(`Sources count: ${voted.sourcesCount}`);
|
|
471
|
+
return issues.join("\n");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
it("detects multiple country origins", () => {
|
|
475
|
+
const voted = makeVotedResult({
|
|
476
|
+
countryOrigins: [
|
|
477
|
+
{ name: "Lebanon", citations: [1] },
|
|
478
|
+
{ name: "Iran", citations: [2] },
|
|
479
|
+
],
|
|
480
|
+
});
|
|
481
|
+
const result = describeContradictions([], voted);
|
|
482
|
+
expect(result).toContain("Multiple origin countries");
|
|
483
|
+
expect(result).toContain("Lebanon");
|
|
484
|
+
expect(result).toContain("Iran");
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it("detects wide rocket count range", () => {
|
|
488
|
+
const voted = makeVotedResult({
|
|
489
|
+
rocketCountMin: 5,
|
|
490
|
+
rocketCountMax: 20,
|
|
491
|
+
});
|
|
492
|
+
const result = describeContradictions([], voted);
|
|
493
|
+
expect(result).toContain("Wide rocket count range: 5–20");
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it("detects low intercepted confidence", () => {
|
|
497
|
+
const voted = makeVotedResult({
|
|
498
|
+
intercepted: 5,
|
|
499
|
+
interceptedConfidence: 0.3,
|
|
500
|
+
});
|
|
501
|
+
const result = describeContradictions([], voted);
|
|
502
|
+
expect(result).toContain("Intercepted count (5) has low confidence: 0.30");
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("detects low hits confidence", () => {
|
|
506
|
+
const voted = makeVotedResult({
|
|
507
|
+
hitsConfirmed: 2,
|
|
508
|
+
hitsConfidence: 0.25,
|
|
509
|
+
});
|
|
510
|
+
const result = describeContradictions([], voted);
|
|
511
|
+
expect(result).toContain("Hits confirmed (2) has low confidence: 0.25");
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it("does not flag narrow rocket count range", () => {
|
|
515
|
+
const voted = makeVotedResult({
|
|
516
|
+
rocketCountMin: 10,
|
|
517
|
+
rocketCountMax: 12,
|
|
518
|
+
});
|
|
519
|
+
const result = describeContradictions([], voted);
|
|
520
|
+
expect(result).not.toContain("Wide rocket count range");
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it("always includes overall confidence and sources count", () => {
|
|
524
|
+
const voted = makeVotedResult({ confidence: 0.45, sourcesCount: 3 });
|
|
525
|
+
const result = describeContradictions([], voted);
|
|
526
|
+
expect(result).toContain("Overall confidence: 0.45");
|
|
527
|
+
expect(result).toContain("Sources count: 3");
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// ═════════════════════════════════════════════════════════
|
|
532
|
+
// 5. ClarifyOutput structure
|
|
533
|
+
// ═════════════════════════════════════════════════════════
|
|
534
|
+
|
|
535
|
+
describe("clarify output contract", () => {
|
|
536
|
+
it("ClarifyOutput has expected shape", () => {
|
|
537
|
+
// Type-level test: ensure the interface we expect
|
|
538
|
+
const output = {
|
|
539
|
+
newPosts: [{ channel: "@test", text: "test", ts: 1 }],
|
|
540
|
+
newExtractions: [makeExtraction()],
|
|
541
|
+
toolCallCount: 2,
|
|
542
|
+
clarified: true,
|
|
543
|
+
};
|
|
544
|
+
expect(output.newPosts).toHaveLength(1);
|
|
545
|
+
expect(output.newExtractions).toHaveLength(1);
|
|
546
|
+
expect(output.toolCallCount).toBe(2);
|
|
547
|
+
expect(output.clarified).toBe(true);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it("ClarifyInput has expected fields", () => {
|
|
551
|
+
const input = {
|
|
552
|
+
alertId: "test-1",
|
|
553
|
+
alertAreas: ["תל אביב"],
|
|
554
|
+
alertType: "red_alert" as const,
|
|
555
|
+
messageId: 123,
|
|
556
|
+
currentText: "text",
|
|
557
|
+
extractions: [makeExtraction()],
|
|
558
|
+
votedResult: makeVotedResult(),
|
|
559
|
+
};
|
|
560
|
+
expect(input.alertAreas).toContain("תל אביב");
|
|
561
|
+
expect(input.extractions).toHaveLength(1);
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// ═════════════════════════════════════════════════════════
|
|
566
|
+
// 6. clarifyTools array
|
|
567
|
+
// ═════════════════════════════════════════════════════════
|
|
568
|
+
|
|
569
|
+
describe("clarifyTools export", () => {
|
|
570
|
+
it("exports exactly 4 tools", async () => {
|
|
571
|
+
const { clarifyTools } = await import("../src/tools/index.js");
|
|
572
|
+
expect(clarifyTools).toHaveLength(4);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it("has correct tool names", async () => {
|
|
576
|
+
const { clarifyTools } = await import("../src/tools/index.js");
|
|
577
|
+
const names = clarifyTools.map((t) => t.name);
|
|
578
|
+
expect(names).toContain("read_telegram_sources");
|
|
579
|
+
expect(names).toContain("alert_history");
|
|
580
|
+
expect(names).toContain("resolve_area");
|
|
581
|
+
expect(names).toContain("betterstack_log");
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it("does not include old MCP-prefixed tool names", async () => {
|
|
585
|
+
const { clarifyTools } = await import("../src/tools/index.js");
|
|
586
|
+
const names = clarifyTools.map((t) => t.name);
|
|
587
|
+
expect(names).not.toContain("telegram_mtproto_mcp_read_sources");
|
|
588
|
+
expect(names).not.toContain("pikud_haoref_mcp");
|
|
589
|
+
expect(names).not.toContain("telegram_bot_mcp_read_target");
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
// ═════════════════════════════════════════════════════════
|
|
594
|
+
// 7. resolveAreaTool
|
|
595
|
+
// ═════════════════════════════════════════════════════════
|
|
596
|
+
|
|
597
|
+
describe("resolveAreaTool", () => {
|
|
598
|
+
let resolveAreaTool: typeof import("../src/tools/index.js").resolveAreaTool;
|
|
599
|
+
|
|
600
|
+
beforeEach(async () => {
|
|
601
|
+
vi.resetModules();
|
|
602
|
+
const toolsMod = await import("../src/tools/index.js");
|
|
603
|
+
resolveAreaTool = toolsMod.resolveAreaTool;
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
afterEach(() => vi.restoreAllMocks());
|
|
607
|
+
|
|
608
|
+
it("resolves direct match (תל אביב)", async () => {
|
|
609
|
+
const result = await resolveAreaTool.invoke({ location: "תל אביב" });
|
|
610
|
+
const parsed = JSON.parse(result);
|
|
611
|
+
expect(parsed.relevant).toBe(true);
|
|
612
|
+
expect(parsed.reasoning).toContain("directly matches");
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it("resolves same zone (פתח תקווה → הרצליה via שרון/גוש דן)", async () => {
|
|
616
|
+
const result = await resolveAreaTool.invoke({ location: "פתח תקווה" });
|
|
617
|
+
const parsed = JSON.parse(result);
|
|
618
|
+
expect(parsed.relevant).toBe(true);
|
|
619
|
+
expect(parsed.sameZone).toBeTruthy();
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it("resolves region keyword (מרכז → תל אביב)", async () => {
|
|
623
|
+
const result = await resolveAreaTool.invoke({ location: "מרכז" });
|
|
624
|
+
const parsed = JSON.parse(result);
|
|
625
|
+
expect(parsed.relevant).toBe(true);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it("rejects unrelated area (חיפה)", async () => {
|
|
629
|
+
const result = await resolveAreaTool.invoke({ location: "קריית שמונה" });
|
|
630
|
+
const parsed = JSON.parse(result);
|
|
631
|
+
expect(parsed.relevant).toBe(false);
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
// ═════════════════════════════════════════════════════════
|
|
636
|
+
// 8. resolveAreaProximity (unit)
|
|
637
|
+
// ═════════════════════════════════════════════════════════
|
|
638
|
+
|
|
639
|
+
describe("resolveAreaProximity", () => {
|
|
640
|
+
let resolveAreaProximity: typeof import("../src/tools/index.js")._resolveAreaProximity;
|
|
641
|
+
|
|
642
|
+
beforeEach(async () => {
|
|
643
|
+
vi.resetModules();
|
|
644
|
+
const toolsMod = await import("../src/tools/index.js");
|
|
645
|
+
resolveAreaProximity = toolsMod._resolveAreaProximity;
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
const monitored = ["הרצליה", "תל אביב - דרום העיר ויפו"];
|
|
649
|
+
|
|
650
|
+
it("direct match — exact monitored area", () => {
|
|
651
|
+
const r = resolveAreaProximity("הרצליה", monitored);
|
|
652
|
+
expect(r.relevant).toBe(true);
|
|
653
|
+
expect(r.sameZone).toBeUndefined();
|
|
654
|
+
expect(r.monitoredMatch).toContain("הרצליה");
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it("direct match — partial prefix (תל אביב)", () => {
|
|
658
|
+
const r = resolveAreaProximity("תל אביב", monitored);
|
|
659
|
+
expect(r.relevant).toBe(true);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it("zone match — פתח תקווה in גוש דן with תל אביב", () => {
|
|
663
|
+
const r = resolveAreaProximity("פתח תקווה", monitored);
|
|
664
|
+
expect(r.relevant).toBe(true);
|
|
665
|
+
expect(r.sameZone).toBe("גוש דן");
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it("zone match — רעננה in שרון with הרצליה", () => {
|
|
669
|
+
const r = resolveAreaProximity("רעננה", monitored);
|
|
670
|
+
expect(r.relevant).toBe(true);
|
|
671
|
+
expect(r.sameZone).toBe("שרון");
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it("region keyword — מרכז includes תל אביב", () => {
|
|
675
|
+
const r = resolveAreaProximity("מרכז", monitored);
|
|
676
|
+
expect(r.relevant).toBe(true);
|
|
677
|
+
expect(r.sameZone).toBe("מרכז");
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it("no match — קריית שמונה (north)", () => {
|
|
681
|
+
const r = resolveAreaProximity("קריית שמונה", monitored);
|
|
682
|
+
expect(r.relevant).toBe(false);
|
|
683
|
+
expect(r.sameZone).toBe("גליל עליון");
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it("no match — completely unknown area", () => {
|
|
687
|
+
const r = resolveAreaProximity("אום אל פחם", monitored);
|
|
688
|
+
expect(r.relevant).toBe(false);
|
|
689
|
+
expect(r.sameZone).toBeUndefined();
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it("zone match — בני ברק in גוש דן", () => {
|
|
693
|
+
const r = resolveAreaProximity("בני ברק", monitored);
|
|
694
|
+
expect(r.relevant).toBe(true);
|
|
695
|
+
expect(r.sameZone).toBe("גוש דן");
|
|
696
|
+
});
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
// ═════════════════════════════════════════════════════════
|
|
700
|
+
// 9. formatOrefDate
|
|
701
|
+
// ═════════════════════════════════════════════════════════
|
|
702
|
+
|
|
703
|
+
describe("formatOrefDate", () => {
|
|
704
|
+
it("formats date as DD.MM.YYYY", async () => {
|
|
705
|
+
const { _formatOrefDate } = await import("../src/tools/index.js");
|
|
706
|
+
const d = new Date("2024-03-09T12:00:00Z");
|
|
707
|
+
expect(_formatOrefDate(d)).toBe("09.03.2024");
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it("pads single digit day and month", async () => {
|
|
711
|
+
const { _formatOrefDate } = await import("../src/tools/index.js");
|
|
712
|
+
const d = new Date("2024-01-05T00:00:00Z");
|
|
713
|
+
expect(_formatOrefDate(d)).toBe("05.01.2024");
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
// ═════════════════════════════════════════════════════════
|
|
718
|
+
// 10. betterstackLogTool
|
|
719
|
+
// ═════════════════════════════════════════════════════════
|
|
720
|
+
|
|
721
|
+
describe("betterstackLogTool", () => {
|
|
722
|
+
let betterstackLogTool: typeof import("../src/tools/index.js").betterstackLogTool;
|
|
723
|
+
const originalFetch = globalThis.fetch;
|
|
724
|
+
|
|
725
|
+
beforeEach(async () => {
|
|
726
|
+
vi.resetModules();
|
|
727
|
+
const toolsMod = await import("../src/tools/index.js");
|
|
728
|
+
betterstackLogTool = toolsMod.betterstackLogTool;
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
afterEach(() => {
|
|
732
|
+
globalThis.fetch = originalFetch;
|
|
733
|
+
vi.restoreAllMocks();
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it("returns log events matching query", async () => {
|
|
737
|
+
globalThis.fetch = vi.fn().mockResolvedValueOnce({
|
|
738
|
+
ok: true,
|
|
739
|
+
json: async () => ({
|
|
740
|
+
results: [
|
|
741
|
+
{
|
|
742
|
+
timestamp: "2024-01-15T10:30:00Z",
|
|
743
|
+
message: "Alert processed",
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
timestamp: "2024-01-15T10:29:00Z",
|
|
747
|
+
message: "Enrichment started",
|
|
748
|
+
},
|
|
749
|
+
],
|
|
750
|
+
}),
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
const result = await betterstackLogTool.invoke({
|
|
754
|
+
query: "enrichment",
|
|
755
|
+
lastMinutes: 15,
|
|
756
|
+
});
|
|
757
|
+
const parsed = JSON.parse(result);
|
|
758
|
+
expect(parsed.logs).toHaveLength(2);
|
|
759
|
+
expect(parsed.logs[0].message).toBe("Alert processed");
|
|
760
|
+
expect(parsed.count).toBe(2);
|
|
761
|
+
expect(parsed.query).toBe("enrichment");
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it("returns empty when no matching logs", async () => {
|
|
765
|
+
globalThis.fetch = vi.fn().mockResolvedValueOnce({
|
|
766
|
+
ok: true,
|
|
767
|
+
json: async () => ({ results: [] }),
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
const result = await betterstackLogTool.invoke({
|
|
771
|
+
query: "nonexistent",
|
|
772
|
+
lastMinutes: 5,
|
|
773
|
+
});
|
|
774
|
+
const parsed = JSON.parse(result);
|
|
775
|
+
expect(parsed.logs).toHaveLength(0);
|
|
776
|
+
expect(parsed.count).toBe(0);
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
it("handles HTTP error", async () => {
|
|
780
|
+
globalThis.fetch = vi.fn().mockResolvedValueOnce({
|
|
781
|
+
ok: false,
|
|
782
|
+
status: 401,
|
|
783
|
+
text: async () => "Unauthorized",
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
const result = await betterstackLogTool.invoke({
|
|
787
|
+
query: "test",
|
|
788
|
+
lastMinutes: 10,
|
|
789
|
+
});
|
|
790
|
+
const parsed = JSON.parse(result);
|
|
791
|
+
expect(parsed.error).toContain("Invalid Better Stack credentials");
|
|
792
|
+
expect(parsed.hint).toBeDefined();
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
it("handles network failure", async () => {
|
|
796
|
+
globalThis.fetch = vi.fn().mockRejectedValueOnce(new Error("ECONNREFUSED"));
|
|
797
|
+
|
|
798
|
+
const result = await betterstackLogTool.invoke({
|
|
799
|
+
query: "test",
|
|
800
|
+
lastMinutes: 10,
|
|
801
|
+
});
|
|
802
|
+
const parsed = JSON.parse(result);
|
|
803
|
+
expect(parsed.error).toContain("ECONNREFUSED");
|
|
804
|
+
expect(parsed.retry).toBe(true);
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
it("handles missing token gracefully", async () => {
|
|
808
|
+
vi.resetModules();
|
|
809
|
+
vi.doMock("@easyoref/shared", async () => {
|
|
810
|
+
const actual = await vi.importActual("@easyoref/shared");
|
|
811
|
+
return {
|
|
812
|
+
...actual,
|
|
813
|
+
config: {
|
|
814
|
+
...(actual.config ?? {}),
|
|
815
|
+
logtailToken: "",
|
|
816
|
+
},
|
|
817
|
+
};
|
|
818
|
+
});
|
|
819
|
+
const toolsMod = await import("../src/tools/index.js");
|
|
820
|
+
const result = await toolsMod.betterstackLogTool.invoke({
|
|
821
|
+
query: "test",
|
|
822
|
+
lastMinutes: 10,
|
|
823
|
+
});
|
|
824
|
+
const parsed = JSON.parse(result);
|
|
825
|
+
expect(parsed.error).toContain("Better Stack token not configured");
|
|
826
|
+
});
|
|
827
|
+
});
|