@johpaz/hive-core 1.0.7 → 1.0.10
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/package.json +10 -9
- package/src/agent/ethics.ts +70 -68
- package/src/agent/index.ts +48 -17
- package/src/agent/providers/index.ts +11 -5
- package/src/agent/soul.ts +19 -15
- package/src/agent/user.ts +19 -15
- package/src/agent/workspace.ts +6 -6
- package/src/agents/index.ts +4 -0
- package/src/agents/inter-agent-bus.test.ts +264 -0
- package/src/agents/inter-agent-bus.ts +279 -0
- package/src/agents/registry.test.ts +275 -0
- package/src/agents/registry.ts +273 -0
- package/src/agents/router.test.ts +229 -0
- package/src/agents/router.ts +251 -0
- package/src/agents/team-coordinator.test.ts +401 -0
- package/src/agents/team-coordinator.ts +480 -0
- package/src/canvas/canvas-manager.test.ts +159 -0
- package/src/canvas/canvas-manager.ts +219 -0
- package/src/canvas/canvas-tools.ts +189 -0
- package/src/canvas/index.ts +2 -0
- package/src/channels/whatsapp.ts +12 -12
- package/src/config/loader.ts +12 -9
- package/src/events/event-bus.test.ts +98 -0
- package/src/events/event-bus.ts +171 -0
- package/src/gateway/server.ts +131 -35
- package/src/index.ts +9 -1
- package/src/multi-agent/manager.ts +12 -12
- package/src/plugins/api.ts +129 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/loader.test.ts +285 -0
- package/src/plugins/loader.ts +363 -0
- package/src/resilience/circuit-breaker.test.ts +129 -0
- package/src/resilience/circuit-breaker.ts +223 -0
- package/src/security/google-chat.test.ts +219 -0
- package/src/security/google-chat.ts +269 -0
- package/src/security/index.ts +5 -0
- package/src/security/pairing.test.ts +302 -0
- package/src/security/pairing.ts +250 -0
- package/src/security/rate-limit.test.ts +239 -0
- package/src/security/rate-limit.ts +270 -0
- package/src/security/signal.test.ts +92 -0
- package/src/security/signal.ts +321 -0
- package/src/state/store.test.ts +190 -0
- package/src/state/store.ts +310 -0
- package/src/storage/sqlite.ts +3 -3
- package/src/tools/cron.ts +42 -2
- package/src/tools/dynamic-registry.test.ts +226 -0
- package/src/tools/dynamic-registry.ts +258 -0
- package/src/tools/fs.test.ts +127 -0
- package/src/tools/fs.ts +364 -0
- package/src/tools/index.ts +1 -0
- package/src/tools/read.ts +23 -19
- package/src/utils/logger.ts +112 -33
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from "child_process";
|
|
2
|
+
import { BaseChannel, type ChannelConfig, type IncomingMessage, type OutboundMessage } from "../channels/base.ts";
|
|
3
|
+
import { logger } from "../utils/logger.ts";
|
|
4
|
+
import { pairingService } from "./pairing.ts";
|
|
5
|
+
|
|
6
|
+
export interface SignalConfig extends ChannelConfig {
|
|
7
|
+
phoneNumber: string;
|
|
8
|
+
dataDir?: string;
|
|
9
|
+
signalCliPath?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface SignalMessage {
|
|
13
|
+
envelope: {
|
|
14
|
+
source: string;
|
|
15
|
+
sourceNumber?: string;
|
|
16
|
+
sourceName?: string;
|
|
17
|
+
timestamp: number;
|
|
18
|
+
dataMessage?: {
|
|
19
|
+
message?: string;
|
|
20
|
+
expiresInSeconds?: number;
|
|
21
|
+
};
|
|
22
|
+
syncMessage?: {
|
|
23
|
+
sentMessage?: {
|
|
24
|
+
destination?: string;
|
|
25
|
+
message?: string;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class SignalChannel extends BaseChannel {
|
|
32
|
+
name = "signal";
|
|
33
|
+
accountId: string;
|
|
34
|
+
config: SignalConfig;
|
|
35
|
+
|
|
36
|
+
private signalCli?: ChildProcess;
|
|
37
|
+
private log = logger.child("signal");
|
|
38
|
+
private messageBuffer = "";
|
|
39
|
+
private chatIdCache: Map<string, string> = new Map();
|
|
40
|
+
|
|
41
|
+
constructor(accountId: string, config: SignalConfig) {
|
|
42
|
+
super();
|
|
43
|
+
this.accountId = accountId;
|
|
44
|
+
this.config = {
|
|
45
|
+
...config,
|
|
46
|
+
dmPolicy: config.dmPolicy ?? "pairing",
|
|
47
|
+
allowFrom: config.allowFrom ?? [],
|
|
48
|
+
enabled: config.enabled ?? true,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async start(): Promise<void> {
|
|
53
|
+
if (!this.config.phoneNumber) {
|
|
54
|
+
throw new Error("Signal phone number not configured");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const signalCliPath = this.config.signalCliPath ?? "signal-cli";
|
|
58
|
+
const dataDir = this.config.dataDir;
|
|
59
|
+
|
|
60
|
+
const args = ["daemon", "--system"];
|
|
61
|
+
if (dataDir) {
|
|
62
|
+
args.push("--config", dataDir);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.log.info(`Starting signal-cli: ${signalCliPath} ${args.join(" ")}`);
|
|
66
|
+
|
|
67
|
+
this.signalCli = spawn(signalCliPath, args, {
|
|
68
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
this.signalCli.stdout?.on("data", (data: Buffer) => {
|
|
72
|
+
this.handleSignalOutput(data.toString());
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
this.signalCli.stderr?.on("data", (data: Buffer) => {
|
|
76
|
+
this.log.error(`signal-cli stderr: ${data.toString()}`);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
this.signalCli.on("error", (error: Error) => {
|
|
80
|
+
this.log.error(`signal-cli error: ${error.message}`);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
this.signalCli.on("exit", (code, signal) => {
|
|
84
|
+
this.log.warn(`signal-cli exited with code ${code}, signal ${signal}`);
|
|
85
|
+
this.running = false;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
89
|
+
|
|
90
|
+
this.running = true;
|
|
91
|
+
this.log.info(`Signal channel started for ${this.config.phoneNumber}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private handleSignalOutput(output: string): void {
|
|
95
|
+
this.messageBuffer += output;
|
|
96
|
+
|
|
97
|
+
const lines = this.messageBuffer.split("\n");
|
|
98
|
+
this.messageBuffer = lines.pop() ?? "";
|
|
99
|
+
|
|
100
|
+
for (const line of lines) {
|
|
101
|
+
if (line.trim()) {
|
|
102
|
+
this.parseSignalMessage(line);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private parseSignalMessage(line: string): void {
|
|
108
|
+
try {
|
|
109
|
+
const data: SignalMessage = JSON.parse(line);
|
|
110
|
+
|
|
111
|
+
if (data.envelope?.dataMessage?.message) {
|
|
112
|
+
const from = data.envelope.source;
|
|
113
|
+
const content = data.envelope.dataMessage.message;
|
|
114
|
+
const timestamp = data.envelope.timestamp;
|
|
115
|
+
|
|
116
|
+
this.handleIncomingMessage({
|
|
117
|
+
from,
|
|
118
|
+
content,
|
|
119
|
+
timestamp,
|
|
120
|
+
name: data.envelope.sourceName,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
this.log.debug(`Non-JSON output: ${line.slice(0, 100)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private async handleIncomingMessage(msg: {
|
|
129
|
+
from: string;
|
|
130
|
+
content: string;
|
|
131
|
+
timestamp: number;
|
|
132
|
+
name?: string;
|
|
133
|
+
}): Promise<void> {
|
|
134
|
+
const peerId = msg.from;
|
|
135
|
+
const kind = "direct";
|
|
136
|
+
|
|
137
|
+
if (msg.content === "/myid") {
|
|
138
|
+
await this.sendDirectMessage(
|
|
139
|
+
peerId,
|
|
140
|
+
`🆔 Tu Signal ID es: ${peerId}\n\n` +
|
|
141
|
+
`Para emparejar, usa el comando:\n` +
|
|
142
|
+
`\`hive pairing generate signal ${peerId}\``
|
|
143
|
+
);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (msg.content === "/pair" || msg.content.startsWith("/pair ")) {
|
|
148
|
+
const code = msg.content.split(" ")[1]?.trim();
|
|
149
|
+
|
|
150
|
+
if (!code) {
|
|
151
|
+
await this.sendDirectMessage(
|
|
152
|
+
peerId,
|
|
153
|
+
"🔐 Envía /pair CODIGO para emparejar.\n" +
|
|
154
|
+
"Solicita un código de emparejamiento al administrador."
|
|
155
|
+
);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const result = pairingService.approve(code);
|
|
160
|
+
if (result.success) {
|
|
161
|
+
await this.sendDirectMessage(peerId, "✅ ¡Emparejamiento exitoso! Ya puedes usar el bot.");
|
|
162
|
+
} else {
|
|
163
|
+
await this.sendDirectMessage(peerId, `❌ ${result.error}`);
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (this.config.dmPolicy === "pairing" && !pairingService.isAllowed("signal", peerId)) {
|
|
169
|
+
this.log.debug(`Message from unpaired user: ${peerId}`);
|
|
170
|
+
await this.sendDirectMessage(
|
|
171
|
+
peerId,
|
|
172
|
+
"⛔ No estás emparejado.\n\n" +
|
|
173
|
+
"Tu Signal ID: " + peerId + "\n\n" +
|
|
174
|
+
"Solicita un código de emparejamiento al administrador y envíalo con:\n" +
|
|
175
|
+
"/pair CODIGO"
|
|
176
|
+
);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!this.isUserAllowed(peerId)) {
|
|
181
|
+
this.log.debug(`Message from unauthorized user: ${peerId}`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const sessionId = this.formatSessionId(peerId, kind);
|
|
186
|
+
this.chatIdCache.set(sessionId, peerId);
|
|
187
|
+
|
|
188
|
+
const incomingMessage: IncomingMessage = {
|
|
189
|
+
sessionId,
|
|
190
|
+
channel: "signal",
|
|
191
|
+
accountId: this.accountId,
|
|
192
|
+
peerId,
|
|
193
|
+
peerKind: kind,
|
|
194
|
+
content: msg.content,
|
|
195
|
+
metadata: {
|
|
196
|
+
signal: {
|
|
197
|
+
phoneNumber: peerId,
|
|
198
|
+
name: msg.name,
|
|
199
|
+
timestamp: msg.timestamp,
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
await this.handleMessage(incomingMessage);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async stop(): Promise<void> {
|
|
208
|
+
if (this.signalCli) {
|
|
209
|
+
this.signalCli.kill("SIGTERM");
|
|
210
|
+
this.running = false;
|
|
211
|
+
this.log.info("Signal channel stopped");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async send(sessionId: string, message: OutboundMessage): Promise<void> {
|
|
216
|
+
const content = message.content ?? "";
|
|
217
|
+
|
|
218
|
+
if (!content || content.trim().length === 0) {
|
|
219
|
+
this.log.warn("Empty response, skipping send");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const phoneNumber = this.chatIdCache.get(sessionId) ?? this.extractPhoneFromSession(sessionId);
|
|
224
|
+
|
|
225
|
+
if (!phoneNumber) {
|
|
226
|
+
throw new Error(`Cannot determine phone number for session: ${sessionId}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
await this.sendDirectMessage(phoneNumber, content);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private async sendDirectMessage(phoneNumber: string, content: string): Promise<void> {
|
|
233
|
+
const chunks = this.chunkMessage(content, 2000);
|
|
234
|
+
|
|
235
|
+
for (const chunk of chunks) {
|
|
236
|
+
await this.executeCli([
|
|
237
|
+
"send",
|
|
238
|
+
"-a", this.config.phoneNumber,
|
|
239
|
+
"-m", chunk,
|
|
240
|
+
phoneNumber,
|
|
241
|
+
]);
|
|
242
|
+
|
|
243
|
+
if (chunks.length > 1) {
|
|
244
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private async executeCli(args: string[]): Promise<string> {
|
|
250
|
+
return new Promise((resolve, reject) => {
|
|
251
|
+
const signalCliPath = this.config.signalCliPath ?? "signal-cli";
|
|
252
|
+
const fullArgs = this.config.dataDir
|
|
253
|
+
? ["--config", this.config.dataDir, ...args]
|
|
254
|
+
: args;
|
|
255
|
+
|
|
256
|
+
const proc = spawn(signalCliPath, fullArgs, {
|
|
257
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
let stdout = "";
|
|
261
|
+
let stderr = "";
|
|
262
|
+
|
|
263
|
+
proc.stdout?.on("data", (data: Buffer) => {
|
|
264
|
+
stdout += data.toString();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
proc.stderr?.on("data", (data: Buffer) => {
|
|
268
|
+
stderr += data.toString();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
proc.on("close", (code) => {
|
|
272
|
+
if (code === 0) {
|
|
273
|
+
resolve(stdout);
|
|
274
|
+
} else {
|
|
275
|
+
reject(new Error(`signal-cli failed: ${stderr || stdout}`));
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
proc.on("error", (error) => {
|
|
280
|
+
reject(error);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private extractPhoneFromSession(sessionId: string): string | undefined {
|
|
286
|
+
const parts = sessionId.split(":");
|
|
287
|
+
return parts[4];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private chunkMessage(content: string, maxLength: number): string[] {
|
|
291
|
+
const chunks: string[] = [];
|
|
292
|
+
let remaining = content;
|
|
293
|
+
|
|
294
|
+
while (remaining.length > 0) {
|
|
295
|
+
if (remaining.length <= maxLength) {
|
|
296
|
+
chunks.push(remaining);
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let splitPoint = remaining.lastIndexOf("\n\n", maxLength);
|
|
301
|
+
if (splitPoint === -1 || splitPoint < maxLength * 0.5) {
|
|
302
|
+
splitPoint = remaining.lastIndexOf("\n", maxLength);
|
|
303
|
+
}
|
|
304
|
+
if (splitPoint === -1 || splitPoint < maxLength * 0.5) {
|
|
305
|
+
splitPoint = remaining.lastIndexOf(" ", maxLength);
|
|
306
|
+
}
|
|
307
|
+
if (splitPoint === -1 || splitPoint < maxLength * 0.5) {
|
|
308
|
+
splitPoint = maxLength;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
chunks.push(remaining.slice(0, splitPoint));
|
|
312
|
+
remaining = remaining.slice(splitPoint).trim();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return chunks;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function createSignalChannel(accountId: string, config: SignalConfig): SignalChannel {
|
|
320
|
+
return new SignalChannel(accountId, config);
|
|
321
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "bun:test";
|
|
2
|
+
import { StateStore } from "../state/store";
|
|
3
|
+
|
|
4
|
+
describe("StateStore", () => {
|
|
5
|
+
let store: StateStore;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
store = new StateStore({ maxSnapshots: 10 });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("should create session", () => {
|
|
12
|
+
const session = store.createSession({
|
|
13
|
+
id: "session-1",
|
|
14
|
+
agentId: "agent-1",
|
|
15
|
+
channel: "telegram",
|
|
16
|
+
userId: "user-1",
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expect(session.id).toBe("session-1");
|
|
20
|
+
expect(session.status).toBe("active");
|
|
21
|
+
expect(session.messageCount).toBe(0);
|
|
22
|
+
|
|
23
|
+
const state = store.getState();
|
|
24
|
+
expect(state.sessions.has("session-1")).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should update session", () => {
|
|
28
|
+
store.createSession({
|
|
29
|
+
id: "session-1",
|
|
30
|
+
agentId: "agent-1",
|
|
31
|
+
channel: "telegram",
|
|
32
|
+
userId: "user-1",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
store.updateSession("session-1", { status: "idle" });
|
|
36
|
+
|
|
37
|
+
const session = store.getState().sessions.get("session-1");
|
|
38
|
+
expect(session?.status).toBe("idle");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should increment message count", () => {
|
|
42
|
+
store.createSession({
|
|
43
|
+
id: "session-1",
|
|
44
|
+
agentId: "agent-1",
|
|
45
|
+
channel: "telegram",
|
|
46
|
+
userId: "user-1",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
store.incrementMessageCount("session-1");
|
|
50
|
+
store.incrementMessageCount("session-1");
|
|
51
|
+
|
|
52
|
+
const session = store.getState().sessions.get("session-1");
|
|
53
|
+
expect(session?.messageCount).toBe(2);
|
|
54
|
+
expect(store.getState().metrics.totalMessages).toBe(2);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should close session", () => {
|
|
58
|
+
store.createSession({
|
|
59
|
+
id: "session-1",
|
|
60
|
+
agentId: "agent-1",
|
|
61
|
+
channel: "telegram",
|
|
62
|
+
userId: "user-1",
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
store.closeSession("session-1");
|
|
66
|
+
|
|
67
|
+
const session = store.getState().sessions.get("session-1");
|
|
68
|
+
expect(session?.status).toBe("closed");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should save snapshots", () => {
|
|
72
|
+
store.createSession({
|
|
73
|
+
id: "session-1",
|
|
74
|
+
agentId: "agent-1",
|
|
75
|
+
channel: "telegram",
|
|
76
|
+
userId: "user-1",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const snapshots = store.getRecentSnapshots(1);
|
|
80
|
+
expect(snapshots.length).toBe(1);
|
|
81
|
+
expect(snapshots[0].action).toContain("Session created");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should limit snapshots", () => {
|
|
85
|
+
const smallStore = new StateStore({ maxSnapshots: 3 });
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < 5; i++) {
|
|
88
|
+
smallStore.createSession({
|
|
89
|
+
id: `session-${i}`,
|
|
90
|
+
agentId: "agent-1",
|
|
91
|
+
channel: "telegram",
|
|
92
|
+
userId: "user-1",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const snapshots = smallStore.getAllSnapshots();
|
|
97
|
+
expect(snapshots.length).toBe(3);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("should notify subscribers", () => {
|
|
101
|
+
let callCount = 0;
|
|
102
|
+
|
|
103
|
+
store.subscribe(() => {
|
|
104
|
+
callCount++;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
store.createSession({
|
|
108
|
+
id: "session-1",
|
|
109
|
+
agentId: "agent-1",
|
|
110
|
+
channel: "telegram",
|
|
111
|
+
userId: "user-1",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(callCount).toBe(1);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should track correlation id", () => {
|
|
118
|
+
store.setCorrelationId("corr-123");
|
|
119
|
+
|
|
120
|
+
store.createSession({
|
|
121
|
+
id: "session-1",
|
|
122
|
+
agentId: "agent-1",
|
|
123
|
+
channel: "telegram",
|
|
124
|
+
userId: "user-1",
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const snapshot = store.getRecentSnapshots(1)[0];
|
|
128
|
+
expect(snapshot.correlationId).toBe("corr-123");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should reset state", () => {
|
|
132
|
+
store.createSession({
|
|
133
|
+
id: "session-1",
|
|
134
|
+
agentId: "agent-1",
|
|
135
|
+
channel: "telegram",
|
|
136
|
+
userId: "user-1",
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
store.reset();
|
|
140
|
+
|
|
141
|
+
expect(store.getState().sessions.size).toBe(0);
|
|
142
|
+
expect(store.getState().metrics.totalSessions).toBe(0);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should export state as JSON", () => {
|
|
146
|
+
store.createSession({
|
|
147
|
+
id: "session-1",
|
|
148
|
+
agentId: "agent-1",
|
|
149
|
+
channel: "telegram",
|
|
150
|
+
userId: "user-1",
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const exported = store.export();
|
|
154
|
+
const parsed = JSON.parse(exported);
|
|
155
|
+
|
|
156
|
+
expect(parsed.sessions["session-1"]).toBeDefined();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should record tool calls", () => {
|
|
160
|
+
store.recordToolCall(100, true);
|
|
161
|
+
store.recordToolCall(200, false);
|
|
162
|
+
|
|
163
|
+
const metrics = store.getState().metrics;
|
|
164
|
+
expect(metrics.totalToolCalls).toBe(2);
|
|
165
|
+
expect(metrics.errors).toBe(1);
|
|
166
|
+
expect(metrics.averageResponseTime).toBe(150);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should provide stats", () => {
|
|
170
|
+
store.createSession({
|
|
171
|
+
id: "session-1",
|
|
172
|
+
agentId: "agent-1",
|
|
173
|
+
channel: "telegram",
|
|
174
|
+
userId: "user-1",
|
|
175
|
+
});
|
|
176
|
+
store.createSession({
|
|
177
|
+
id: "session-2",
|
|
178
|
+
agentId: "agent-1",
|
|
179
|
+
channel: "telegram",
|
|
180
|
+
userId: "user-1",
|
|
181
|
+
});
|
|
182
|
+
store.closeSession("session-2");
|
|
183
|
+
|
|
184
|
+
const stats = store.getStats();
|
|
185
|
+
|
|
186
|
+
expect(stats.sessionsCount).toBe(2);
|
|
187
|
+
expect(stats.snapshotsCount).toBeGreaterThan(0);
|
|
188
|
+
expect(stats.uptime).toBeGreaterThanOrEqual(0);
|
|
189
|
+
});
|
|
190
|
+
});
|