@agentick/gateway 0.0.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/LICENSE +21 -0
- package/README.md +477 -0
- package/dist/agent-registry.d.ts +51 -0
- package/dist/agent-registry.d.ts.map +1 -0
- package/dist/agent-registry.js +78 -0
- package/dist/agent-registry.js.map +1 -0
- package/dist/app-registry.d.ts +51 -0
- package/dist/app-registry.d.ts.map +1 -0
- package/dist/app-registry.js +78 -0
- package/dist/app-registry.js.map +1 -0
- package/dist/bin.d.ts +8 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +37 -0
- package/dist/bin.js.map +1 -0
- package/dist/gateway.d.ts +165 -0
- package/dist/gateway.d.ts.map +1 -0
- package/dist/gateway.js +1339 -0
- package/dist/gateway.js.map +1 -0
- package/dist/http-transport.d.ts +65 -0
- package/dist/http-transport.d.ts.map +1 -0
- package/dist/http-transport.js +517 -0
- package/dist/http-transport.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +162 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +16 -0
- package/dist/protocol.js.map +1 -0
- package/dist/session-manager.d.ts +101 -0
- package/dist/session-manager.d.ts.map +1 -0
- package/dist/session-manager.js +208 -0
- package/dist/session-manager.js.map +1 -0
- package/dist/testing.d.ts +92 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +129 -0
- package/dist/testing.js.map +1 -0
- package/dist/transport-protocol.d.ts +162 -0
- package/dist/transport-protocol.d.ts.map +1 -0
- package/dist/transport-protocol.js +16 -0
- package/dist/transport-protocol.js.map +1 -0
- package/dist/transport.d.ts +115 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +56 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +314 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +37 -0
- package/dist/types.js.map +1 -0
- package/dist/websocket-server.d.ts +87 -0
- package/dist/websocket-server.d.ts.map +1 -0
- package/dist/websocket-server.js +245 -0
- package/dist/websocket-server.js.map +1 -0
- package/dist/ws-transport.d.ts +17 -0
- package/dist/ws-transport.d.ts.map +1 -0
- package/dist/ws-transport.js +174 -0
- package/dist/ws-transport.js.map +1 -0
- package/package.json +51 -0
- package/src/__tests__/custom-methods.spec.ts +220 -0
- package/src/__tests__/gateway-methods.spec.ts +262 -0
- package/src/__tests__/gateway.spec.ts +404 -0
- package/src/__tests__/guards.spec.ts +235 -0
- package/src/__tests__/protocol.spec.ts +58 -0
- package/src/__tests__/session-manager.spec.ts +220 -0
- package/src/__tests__/ws-transport.spec.ts +246 -0
- package/src/app-registry.ts +103 -0
- package/src/bin.ts +38 -0
- package/src/gateway.ts +1712 -0
- package/src/http-transport.ts +623 -0
- package/src/index.ts +94 -0
- package/src/session-manager.ts +272 -0
- package/src/testing.ts +236 -0
- package/src/transport-protocol.ts +249 -0
- package/src/transport.ts +191 -0
- package/src/types.ts +392 -0
- package/src/websocket-server.ts +303 -0
- package/src/ws-transport.ts +205 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard Middleware Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for role-based and custom guard middleware behavior.
|
|
5
|
+
* Verifies GuardError is thrown with correct codes and type guards work.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, vi } from "vitest";
|
|
9
|
+
import { Context, type KernelContext, type UserContext } from "@agentick/kernel";
|
|
10
|
+
import { GuardError, isGuardError } from "@agentick/shared";
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Role guard behavior tests
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
describe("Role guard behavior", () => {
|
|
17
|
+
/**
|
|
18
|
+
* Simulates the role guard logic used in Gateway
|
|
19
|
+
*/
|
|
20
|
+
function checkRoles(roles: string[], userRoles: string[]): boolean {
|
|
21
|
+
return roles.some((r) => userRoles.includes(r));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
it("should pass when user has required role", () => {
|
|
25
|
+
expect(checkRoles(["admin"], ["admin", "user"])).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should pass when user has any of multiple required roles", () => {
|
|
29
|
+
expect(checkRoles(["admin", "moderator"], ["user", "moderator"])).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should fail when user lacks all required roles", () => {
|
|
33
|
+
expect(checkRoles(["admin", "moderator"], ["user"])).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should fail when user has no roles", () => {
|
|
37
|
+
expect(checkRoles(["admin"], [])).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should handle empty required roles (allows all)", () => {
|
|
41
|
+
expect(checkRoles([], ["user"])).toBe(false); // some() on empty returns false
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// Custom guard behavior tests
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
describe("Custom guard behavior", () => {
|
|
50
|
+
it("should allow when guard returns true", async () => {
|
|
51
|
+
const guard = (ctx: KernelContext) => true;
|
|
52
|
+
|
|
53
|
+
const user: UserContext = { id: "user-1" };
|
|
54
|
+
const ctx = Context.create({ user });
|
|
55
|
+
|
|
56
|
+
const result = await Context.run(ctx, () => guard(Context.get()));
|
|
57
|
+
expect(result).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should deny when guard returns false", async () => {
|
|
61
|
+
const guard = (ctx: KernelContext) => false;
|
|
62
|
+
|
|
63
|
+
const user: UserContext = { id: "user-1" };
|
|
64
|
+
const ctx = Context.create({ user });
|
|
65
|
+
|
|
66
|
+
const result = await Context.run(ctx, () => guard(Context.get()));
|
|
67
|
+
expect(result).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should support async guards", async () => {
|
|
71
|
+
const guard = async (ctx: KernelContext) => {
|
|
72
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
73
|
+
return ctx.user?.id === "allowed-user";
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const user1: UserContext = { id: "allowed-user" };
|
|
77
|
+
const ctx1 = Context.create({ user: user1 });
|
|
78
|
+
expect(await Context.run(ctx1, () => guard(Context.get()))).toBe(true);
|
|
79
|
+
|
|
80
|
+
const user2: UserContext = { id: "other-user" };
|
|
81
|
+
const ctx2 = Context.create({ user: user2 });
|
|
82
|
+
expect(await Context.run(ctx2, () => guard(Context.get()))).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should have access to user context", async () => {
|
|
86
|
+
const guard = (ctx: KernelContext) => {
|
|
87
|
+
return ctx.user?.id === "specific-user" && ctx.user?.roles?.includes("premium");
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const premiumUser: UserContext = { id: "specific-user", roles: ["user", "premium"] };
|
|
91
|
+
const ctx1 = Context.create({ user: premiumUser });
|
|
92
|
+
expect(await Context.run(ctx1, () => guard(Context.get()))).toBe(true);
|
|
93
|
+
|
|
94
|
+
const regularUser: UserContext = { id: "specific-user", roles: ["user"] };
|
|
95
|
+
const ctx2 = Context.create({ user: regularUser });
|
|
96
|
+
expect(await Context.run(ctx2, () => guard(Context.get()))).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should have access to metadata", async () => {
|
|
100
|
+
const guard = (ctx: KernelContext) => {
|
|
101
|
+
return ctx.metadata?.tenantId === "allowed-tenant";
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const ctx1 = Context.create({
|
|
105
|
+
user: { id: "1" },
|
|
106
|
+
metadata: { tenantId: "allowed-tenant" },
|
|
107
|
+
});
|
|
108
|
+
expect(await Context.run(ctx1, () => guard(Context.get()))).toBe(true);
|
|
109
|
+
|
|
110
|
+
const ctx2 = Context.create({
|
|
111
|
+
user: { id: "1" },
|
|
112
|
+
metadata: { tenantId: "other-tenant" },
|
|
113
|
+
});
|
|
114
|
+
expect(await Context.run(ctx2, () => guard(Context.get()))).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// Combined guards behavior tests
|
|
120
|
+
// ============================================================================
|
|
121
|
+
|
|
122
|
+
describe("Combined guards behavior", () => {
|
|
123
|
+
it("should check roles before custom guard", async () => {
|
|
124
|
+
const roleCheck = vi.fn((roles: string[], userRoles: string[]) =>
|
|
125
|
+
roles.some((r) => userRoles.includes(r)),
|
|
126
|
+
);
|
|
127
|
+
const customGuard = vi.fn((ctx: KernelContext) => true);
|
|
128
|
+
|
|
129
|
+
async function checkAllGuards(
|
|
130
|
+
roles: string[],
|
|
131
|
+
guard: (ctx: KernelContext) => boolean,
|
|
132
|
+
ctx: KernelContext,
|
|
133
|
+
): Promise<boolean> {
|
|
134
|
+
if (roles.length > 0) {
|
|
135
|
+
const userRoles = ctx.user?.roles ?? [];
|
|
136
|
+
if (!roleCheck(roles, userRoles)) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return guard(ctx);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const user: UserContext = { id: "user-1", roles: ["user"] };
|
|
144
|
+
const ctx = Context.create({ user });
|
|
145
|
+
|
|
146
|
+
const result = await Context.run(ctx, () =>
|
|
147
|
+
checkAllGuards(["admin"], customGuard, Context.get()),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
expect(result).toBe(false);
|
|
151
|
+
expect(roleCheck).toHaveBeenCalled();
|
|
152
|
+
expect(customGuard).not.toHaveBeenCalled();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should call custom guard after roles pass", async () => {
|
|
156
|
+
const customGuard = vi.fn((ctx: KernelContext) => ctx.user?.id === "allowed");
|
|
157
|
+
|
|
158
|
+
async function checkAllGuards(
|
|
159
|
+
roles: string[],
|
|
160
|
+
guard: (ctx: KernelContext) => boolean,
|
|
161
|
+
ctx: KernelContext,
|
|
162
|
+
): Promise<boolean> {
|
|
163
|
+
if (roles.length > 0) {
|
|
164
|
+
const userRoles = ctx.user?.roles ?? [];
|
|
165
|
+
if (!roles.some((r) => userRoles.includes(r))) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return guard(ctx);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const user: UserContext = { id: "allowed", roles: ["admin"] };
|
|
173
|
+
const ctx = Context.create({ user });
|
|
174
|
+
|
|
175
|
+
const result = await Context.run(ctx, () =>
|
|
176
|
+
checkAllGuards(["admin"], customGuard, Context.get()),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
expect(result).toBe(true);
|
|
180
|
+
expect(customGuard).toHaveBeenCalled();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ============================================================================
|
|
185
|
+
// GuardError integration tests
|
|
186
|
+
// ============================================================================
|
|
187
|
+
|
|
188
|
+
describe("GuardError integration", () => {
|
|
189
|
+
it("GuardError has correct error code", () => {
|
|
190
|
+
const err = new GuardError("test");
|
|
191
|
+
expect(err.code).toBe("GUARD_DENIED");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("GuardError.role() sets guardType", () => {
|
|
195
|
+
const err = GuardError.role(["admin"]);
|
|
196
|
+
expect(err.guardType).toBe("role");
|
|
197
|
+
expect(err.code).toBe("GUARD_DENIED");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("GuardError.denied() sets guardType to custom", () => {
|
|
201
|
+
const err = GuardError.denied("Nope");
|
|
202
|
+
expect(err.guardType).toBe("custom");
|
|
203
|
+
expect(err.code).toBe("GUARD_DENIED");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("isGuardError type guard works", () => {
|
|
207
|
+
expect(isGuardError(new GuardError("test"))).toBe(true);
|
|
208
|
+
expect(isGuardError(GuardError.role(["admin"]))).toBe(true);
|
|
209
|
+
expect(isGuardError(GuardError.denied("nope"))).toBe(true);
|
|
210
|
+
expect(isGuardError(new Error("Forbidden: something"))).toBe(false);
|
|
211
|
+
expect(isGuardError(null)).toBe(false);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("GuardError propagates through guard middleware", () => {
|
|
215
|
+
const guard = (_ctx: KernelContext) => {
|
|
216
|
+
throw GuardError.role(["admin"]);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const ctx = Context.create({ user: { id: "1" } });
|
|
220
|
+
|
|
221
|
+
expect(() => Context.run(ctx, () => guard(Context.get()))).toThrow(GuardError);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("should handle async guard throwing GuardError", async () => {
|
|
225
|
+
const guard = async (_ctx: KernelContext) => {
|
|
226
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
227
|
+
throw GuardError.denied("Access denied");
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const ctx = Context.create({ user: { id: "1" } });
|
|
231
|
+
|
|
232
|
+
await expect(Context.run(ctx, () => guard(Context.get()))).rejects.toThrow(GuardError);
|
|
233
|
+
await expect(Context.run(ctx, () => guard(Context.get()))).rejects.toThrow("Access denied");
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protocol Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for session key parsing and formatting.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from "vitest";
|
|
8
|
+
import { parseSessionKey, formatSessionKey, type SessionKey } from "../transport-protocol.js";
|
|
9
|
+
|
|
10
|
+
describe("parseSessionKey", () => {
|
|
11
|
+
it("parses simple session name with default agent", () => {
|
|
12
|
+
const result = parseSessionKey("main", "chat");
|
|
13
|
+
expect(result).toEqual({ appId: "chat", sessionName: "main" });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("parses agent-prefixed session key", () => {
|
|
17
|
+
const result = parseSessionKey("research:task-123", "chat");
|
|
18
|
+
expect(result).toEqual({ appId: "research", sessionName: "task-123" });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("preserves colons in session name after first", () => {
|
|
22
|
+
const result = parseSessionKey("slack:C012345:thread-xyz", "chat");
|
|
23
|
+
expect(result).toEqual({ appId: "slack", sessionName: "C012345:thread-xyz" });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("handles phone numbers in session name", () => {
|
|
27
|
+
const result = parseSessionKey("whatsapp:+1234567890", "chat");
|
|
28
|
+
expect(result).toEqual({ appId: "whatsapp", sessionName: "+1234567890" });
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("formatSessionKey", () => {
|
|
33
|
+
it("formats session key with agent prefix", () => {
|
|
34
|
+
const key: SessionKey = { appId: "chat", sessionName: "main" };
|
|
35
|
+
expect(formatSessionKey(key)).toBe("chat:main");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("formats session key with complex session name", () => {
|
|
39
|
+
const key: SessionKey = { appId: "slack", sessionName: "C012345:thread-xyz" };
|
|
40
|
+
expect(formatSessionKey(key)).toBe("slack:C012345:thread-xyz");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("parseSessionKey and formatSessionKey roundtrip", () => {
|
|
45
|
+
it("roundtrips simple key", () => {
|
|
46
|
+
const original = "chat:main";
|
|
47
|
+
const parsed = parseSessionKey(original, "default");
|
|
48
|
+
const formatted = formatSessionKey(parsed);
|
|
49
|
+
expect(formatted).toBe(original);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("roundtrips complex key", () => {
|
|
53
|
+
const original = "whatsapp:+1234567890";
|
|
54
|
+
const parsed = parseSessionKey(original, "default");
|
|
55
|
+
const formatted = formatSessionKey(parsed);
|
|
56
|
+
expect(formatted).toBe(original);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Manager Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
6
|
+
import { SessionManager } from "../session-manager.js";
|
|
7
|
+
import { AppRegistry } from "../app-registry.js";
|
|
8
|
+
import type { App } from "@agentick/core";
|
|
9
|
+
|
|
10
|
+
// Mock App for testing
|
|
11
|
+
function createMockApp(name: string): App {
|
|
12
|
+
return {
|
|
13
|
+
session: vi.fn().mockReturnValue({
|
|
14
|
+
id: `session-${Date.now()}`,
|
|
15
|
+
close: vi.fn(),
|
|
16
|
+
}),
|
|
17
|
+
run: vi.fn() as any,
|
|
18
|
+
send: vi.fn() as any,
|
|
19
|
+
close: vi.fn(),
|
|
20
|
+
sessions: [],
|
|
21
|
+
has: vi.fn(),
|
|
22
|
+
isHibernated: vi.fn(),
|
|
23
|
+
hibernate: vi.fn(),
|
|
24
|
+
hibernatedSessions: vi.fn(),
|
|
25
|
+
onSessionCreate: vi.fn(),
|
|
26
|
+
onSessionClose: vi.fn(),
|
|
27
|
+
} as unknown as App;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("SessionManager", () => {
|
|
31
|
+
let registry: AppRegistry;
|
|
32
|
+
let manager: SessionManager;
|
|
33
|
+
let chatApp: App;
|
|
34
|
+
let researchApp: App;
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
chatApp = createMockApp("chat");
|
|
38
|
+
researchApp = createMockApp("research");
|
|
39
|
+
registry = new AppRegistry({ chat: chatApp, research: researchApp }, "chat");
|
|
40
|
+
manager = new SessionManager(registry);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("getOrCreate", () => {
|
|
44
|
+
it("creates new session with default app", async () => {
|
|
45
|
+
const session = await manager.getOrCreate("main");
|
|
46
|
+
|
|
47
|
+
expect(session.state.id).toBe("chat:main");
|
|
48
|
+
expect(session.state.appId).toBe("chat");
|
|
49
|
+
expect(session.appInfo.id).toBe("chat");
|
|
50
|
+
expect(session.coreSession).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("creates new session with specified app", async () => {
|
|
54
|
+
const session = await manager.getOrCreate("research:task-1");
|
|
55
|
+
|
|
56
|
+
expect(session.state.id).toBe("research:task-1");
|
|
57
|
+
expect(session.state.appId).toBe("research");
|
|
58
|
+
expect(session.appInfo.id).toBe("research");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns existing session", async () => {
|
|
62
|
+
const session1 = await manager.getOrCreate("main");
|
|
63
|
+
const session2 = await manager.getOrCreate("chat:main");
|
|
64
|
+
|
|
65
|
+
expect(session1).toBe(session2);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("updates lastActivityAt on access", async () => {
|
|
69
|
+
const session1 = await manager.getOrCreate("main");
|
|
70
|
+
const firstActivity = session1.state.lastActivityAt;
|
|
71
|
+
|
|
72
|
+
// Small delay
|
|
73
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
74
|
+
|
|
75
|
+
const session2 = await manager.getOrCreate("chat:main");
|
|
76
|
+
expect(session2.state.lastActivityAt.getTime()).toBeGreaterThan(firstActivity.getTime());
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("get", () => {
|
|
81
|
+
it("returns existing session", async () => {
|
|
82
|
+
await manager.getOrCreate("main");
|
|
83
|
+
const session = manager.get("chat:main");
|
|
84
|
+
|
|
85
|
+
expect(session).toBeDefined();
|
|
86
|
+
expect(session?.state.id).toBe("chat:main");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("returns undefined for non-existent session", () => {
|
|
90
|
+
expect(manager.get("nonexistent")).toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("has", () => {
|
|
95
|
+
it("returns true for existing session", async () => {
|
|
96
|
+
await manager.getOrCreate("main");
|
|
97
|
+
expect(manager.has("chat:main")).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("returns false for non-existent session", () => {
|
|
101
|
+
expect(manager.has("nonexistent")).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("close", () => {
|
|
106
|
+
it("removes session", async () => {
|
|
107
|
+
await manager.getOrCreate("main");
|
|
108
|
+
expect(manager.has("chat:main")).toBe(true);
|
|
109
|
+
|
|
110
|
+
await manager.close("chat:main");
|
|
111
|
+
expect(manager.has("chat:main")).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("handles non-existent session gracefully", async () => {
|
|
115
|
+
await expect(manager.close("nonexistent")).resolves.toBeUndefined();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("reset", () => {
|
|
120
|
+
it("resets session state", async () => {
|
|
121
|
+
const session = await manager.getOrCreate("main");
|
|
122
|
+
session.state.messageCount = 10;
|
|
123
|
+
|
|
124
|
+
await manager.reset("chat:main");
|
|
125
|
+
|
|
126
|
+
expect(session.state.messageCount).toBe(0);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("handles non-existent session gracefully", async () => {
|
|
130
|
+
await expect(manager.reset("nonexistent")).resolves.toBeUndefined();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("subscription management", () => {
|
|
135
|
+
it("adds subscriber to session", async () => {
|
|
136
|
+
await manager.getOrCreate("main");
|
|
137
|
+
await manager.subscribe("chat:main", "client-1");
|
|
138
|
+
|
|
139
|
+
const subscribers = manager.getSubscribers("chat:main");
|
|
140
|
+
expect(subscribers.has("client-1")).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("removes subscriber from session", async () => {
|
|
144
|
+
await manager.getOrCreate("main");
|
|
145
|
+
await manager.subscribe("chat:main", "client-1");
|
|
146
|
+
manager.unsubscribe("chat:main", "client-1");
|
|
147
|
+
|
|
148
|
+
const subscribers = manager.getSubscribers("chat:main");
|
|
149
|
+
expect(subscribers.has("client-1")).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("removes subscriber from all sessions", async () => {
|
|
153
|
+
await manager.getOrCreate("main");
|
|
154
|
+
await manager.getOrCreate("research:task-1");
|
|
155
|
+
|
|
156
|
+
await manager.subscribe("chat:main", "client-1");
|
|
157
|
+
await manager.subscribe("research:task-1", "client-1");
|
|
158
|
+
|
|
159
|
+
manager.unsubscribeAll("client-1");
|
|
160
|
+
|
|
161
|
+
expect(manager.getSubscribers("chat:main").has("client-1")).toBe(false);
|
|
162
|
+
expect(manager.getSubscribers("research:task-1").has("client-1")).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("returns empty set for non-existent session", () => {
|
|
166
|
+
const subscribers = manager.getSubscribers("nonexistent");
|
|
167
|
+
expect(subscribers.size).toBe(0);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("message count", () => {
|
|
172
|
+
it("increments message count", async () => {
|
|
173
|
+
await manager.getOrCreate("main");
|
|
174
|
+
expect(manager.get("chat:main")?.state.messageCount).toBe(0);
|
|
175
|
+
|
|
176
|
+
manager.incrementMessageCount("chat:main");
|
|
177
|
+
expect(manager.get("chat:main")?.state.messageCount).toBe(1);
|
|
178
|
+
|
|
179
|
+
manager.incrementMessageCount("chat:main");
|
|
180
|
+
expect(manager.get("chat:main")?.state.messageCount).toBe(2);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("active state", () => {
|
|
185
|
+
it("sets active state", async () => {
|
|
186
|
+
await manager.getOrCreate("main");
|
|
187
|
+
expect(manager.get("chat:main")?.state.isActive).toBe(false);
|
|
188
|
+
|
|
189
|
+
manager.setActive("chat:main", true);
|
|
190
|
+
expect(manager.get("chat:main")?.state.isActive).toBe(true);
|
|
191
|
+
|
|
192
|
+
manager.setActive("chat:main", false);
|
|
193
|
+
expect(manager.get("chat:main")?.state.isActive).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe("listing", () => {
|
|
198
|
+
it("returns all session ids", async () => {
|
|
199
|
+
await manager.getOrCreate("main");
|
|
200
|
+
await manager.getOrCreate("research:task-1");
|
|
201
|
+
|
|
202
|
+
const ids = manager.ids();
|
|
203
|
+
expect(ids).toHaveLength(2);
|
|
204
|
+
expect(ids).toContain("chat:main");
|
|
205
|
+
expect(ids).toContain("research:task-1");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("returns sessions for specific app", async () => {
|
|
209
|
+
await manager.getOrCreate("main");
|
|
210
|
+
await manager.getOrCreate("chat:other");
|
|
211
|
+
await manager.getOrCreate("research:task-1");
|
|
212
|
+
|
|
213
|
+
const chatSessions = manager.forApp("chat");
|
|
214
|
+
expect(chatSessions).toHaveLength(2);
|
|
215
|
+
|
|
216
|
+
const researchSessions = manager.forApp("research");
|
|
217
|
+
expect(researchSessions).toHaveLength(1);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|