@elvatis_com/openclaw-cli-bridge-elvatis 2.5.0 → 2.6.0

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.
@@ -0,0 +1,294 @@
1
+ /**
2
+ * Unit tests for ProviderSessionRegistry.
3
+ *
4
+ * Mocks the filesystem to prevent real file I/O.
5
+ */
6
+
7
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
8
+
9
+ // Mock config module — provide constants used by provider-sessions.ts
10
+ vi.mock("../src/config.js", async () => {
11
+ return {
12
+ PROVIDER_SESSIONS_FILE: "/tmp/test-sessions.json",
13
+ PROVIDER_SESSION_TTL_MS: 2 * 60 * 60 * 1000, // 2 hours
14
+ PROVIDER_SESSION_SWEEP_MS: 10 * 60 * 1000,
15
+ };
16
+ });
17
+
18
+ // Mock fs to prevent real file operations
19
+ const { mockReadFileSync, mockWriteFileSync, mockMkdirSync } = vi.hoisted(() => ({
20
+ mockReadFileSync: vi.fn(() => { throw new Error("ENOENT"); }),
21
+ mockWriteFileSync: vi.fn(),
22
+ mockMkdirSync: vi.fn(),
23
+ }));
24
+
25
+ vi.mock("node:fs", async (importOriginal) => {
26
+ const orig = await importOriginal<typeof import("node:fs")>();
27
+ return {
28
+ ...orig,
29
+ readFileSync: mockReadFileSync,
30
+ writeFileSync: mockWriteFileSync,
31
+ mkdirSync: mockMkdirSync,
32
+ };
33
+ });
34
+
35
+ import { ProviderSessionRegistry } from "../src/provider-sessions.js";
36
+
37
+ // ──────────────────────────────────────────────────────────────────────────────
38
+
39
+ describe("ProviderSessionRegistry", () => {
40
+ let registry: ProviderSessionRegistry;
41
+
42
+ beforeEach(() => {
43
+ mockReadFileSync.mockImplementation(() => { throw new Error("ENOENT"); });
44
+ mockWriteFileSync.mockClear();
45
+ mockMkdirSync.mockClear();
46
+ registry = new ProviderSessionRegistry();
47
+ });
48
+
49
+ afterEach(() => {
50
+ registry.stop();
51
+ });
52
+
53
+ // ── createSession ──────────────────────────────────────────────────────
54
+
55
+ describe("createSession()", () => {
56
+ it("creates a session with correct fields", () => {
57
+ const session = registry.createSession("claude", "cli-claude/claude-sonnet-4-6");
58
+ expect(session.id).toMatch(/^claude:session-[a-f0-9]+$/);
59
+ expect(session.provider).toBe("claude");
60
+ expect(session.modelAlias).toBe("cli-claude/claude-sonnet-4-6");
61
+ expect(session.state).toBe("active");
62
+ expect(session.runCount).toBe(0);
63
+ expect(session.timeoutCount).toBe(0);
64
+ expect(session.createdAt).toBeGreaterThan(0);
65
+ expect(session.updatedAt).toBe(session.createdAt);
66
+ });
67
+
68
+ it("creates unique session IDs", () => {
69
+ const s1 = registry.createSession("claude", "cli-claude/claude-sonnet-4-6");
70
+ const s2 = registry.createSession("claude", "cli-claude/claude-sonnet-4-6");
71
+ expect(s1.id).not.toBe(s2.id);
72
+ });
73
+
74
+ it("accepts optional metadata", () => {
75
+ const session = registry.createSession("claude", "cli-claude/claude-sonnet-4-6", {
76
+ meta: { profilePath: "/tmp/test" },
77
+ });
78
+ expect(session.meta.profilePath).toBe("/tmp/test");
79
+ });
80
+
81
+ it("flushes to disk after creation", () => {
82
+ registry.createSession("claude", "cli-claude/claude-sonnet-4-6");
83
+ expect(mockWriteFileSync).toHaveBeenCalled();
84
+ });
85
+ });
86
+
87
+ // ── getSession ─────────────────────────────────────────────────────────
88
+
89
+ describe("getSession()", () => {
90
+ it("returns the session by ID", () => {
91
+ const created = registry.createSession("gemini", "cli-gemini/gemini-2.5-pro");
92
+ const found = registry.getSession(created.id);
93
+ expect(found).toBe(created);
94
+ });
95
+
96
+ it("returns undefined for unknown ID", () => {
97
+ expect(registry.getSession("nonexistent")).toBeUndefined();
98
+ });
99
+ });
100
+
101
+ // ── findSession ────────────────────────────────────────────────────────
102
+
103
+ describe("findSession()", () => {
104
+ it("finds most recently updated matching session", () => {
105
+ const s1 = registry.createSession("claude", "cli-claude/claude-sonnet-4-6");
106
+ const s2 = registry.createSession("claude", "cli-claude/claude-sonnet-4-6");
107
+ // Force s2 to be newer so the comparison is deterministic
108
+ s2.updatedAt = s1.updatedAt + 1000;
109
+ const found = registry.findSession("claude", "cli-claude/claude-sonnet-4-6");
110
+ expect(found?.id).toBe(s2.id);
111
+ });
112
+
113
+ it("returns undefined when no match", () => {
114
+ registry.createSession("claude", "cli-claude/claude-sonnet-4-6");
115
+ expect(registry.findSession("gemini", "cli-gemini/gemini-2.5-pro")).toBeUndefined();
116
+ });
117
+
118
+ it("skips expired sessions", () => {
119
+ const s = registry.createSession("claude", "cli-claude/claude-sonnet-4-6");
120
+ s.state = "expired";
121
+ expect(registry.findSession("claude", "cli-claude/claude-sonnet-4-6")).toBeUndefined();
122
+ });
123
+ });
124
+
125
+ // ── ensureSession ──────────────────────────────────────────────────────
126
+
127
+ describe("ensureSession()", () => {
128
+ it("reuses existing active session", () => {
129
+ const s1 = registry.createSession("claude", "cli-claude/claude-sonnet-4-6");
130
+ const s2 = registry.ensureSession("claude", "cli-claude/claude-sonnet-4-6");
131
+ expect(s2.id).toBe(s1.id);
132
+ });
133
+
134
+ it("creates new session when none exists", () => {
135
+ const s = registry.ensureSession("claude", "cli-claude/claude-sonnet-4-6");
136
+ expect(s.id).toMatch(/^claude:session-/);
137
+ });
138
+
139
+ it("touches the existing session", () => {
140
+ const s = registry.createSession("claude", "cli-claude/claude-sonnet-4-6");
141
+ const originalUpdated = s.updatedAt;
142
+ // Small delay to ensure timestamp changes
143
+ s.updatedAt = originalUpdated - 1000;
144
+ registry.ensureSession("claude", "cli-claude/claude-sonnet-4-6");
145
+ expect(s.updatedAt).toBeGreaterThan(originalUpdated - 1000);
146
+ });
147
+ });
148
+
149
+ // ── touchSession ───────────────────────────────────────────────────────
150
+
151
+ describe("touchSession()", () => {
152
+ it("updates the timestamp", () => {
153
+ const s = registry.createSession("claude", "cli-claude/claude-sonnet-4-6");
154
+ const before = s.updatedAt;
155
+ s.updatedAt = before - 5000;
156
+ registry.touchSession(s.id);
157
+ expect(s.updatedAt).toBeGreaterThanOrEqual(before);
158
+ });
159
+
160
+ it("returns false for unknown session", () => {
161
+ expect(registry.touchSession("nonexistent")).toBe(false);
162
+ });
163
+
164
+ it("reactivates idle sessions", () => {
165
+ const s = registry.createSession("claude", "cli-claude/claude-sonnet-4-6");
166
+ s.state = "idle";
167
+ registry.touchSession(s.id);
168
+ expect(s.state).toBe("active");
169
+ });
170
+ });
171
+
172
+ // ── recordRun ──────────────────────────────────────────────────────────
173
+
174
+ describe("recordRun()", () => {
175
+ it("increments run count", () => {
176
+ const s = registry.createSession("claude", "cli-claude/claude-sonnet-4-6");
177
+ registry.recordRun(s.id, false);
178
+ expect(s.runCount).toBe(1);
179
+ expect(s.timeoutCount).toBe(0);
180
+ expect(s.state).toBe("idle");
181
+ });
182
+
183
+ it("increments timeout count when timedOut is true", () => {
184
+ const s = registry.createSession("claude", "cli-claude/claude-sonnet-4-6");
185
+ registry.recordRun(s.id, true);
186
+ expect(s.runCount).toBe(1);
187
+ expect(s.timeoutCount).toBe(1);
188
+ });
189
+
190
+ it("sets state to idle after run", () => {
191
+ const s = registry.createSession("claude", "cli-claude/claude-sonnet-4-6");
192
+ expect(s.state).toBe("active");
193
+ registry.recordRun(s.id, false);
194
+ expect(s.state).toBe("idle");
195
+ });
196
+ });
197
+
198
+ // ── deleteSession ──────────────────────────────────────────────────────
199
+
200
+ describe("deleteSession()", () => {
201
+ it("removes the session", () => {
202
+ const s = registry.createSession("claude", "cli-claude/claude-sonnet-4-6");
203
+ expect(registry.deleteSession(s.id)).toBe(true);
204
+ expect(registry.getSession(s.id)).toBeUndefined();
205
+ });
206
+
207
+ it("returns false for unknown ID", () => {
208
+ expect(registry.deleteSession("nonexistent")).toBe(false);
209
+ });
210
+ });
211
+
212
+ // ── stats ──────────────────────────────────────────────────────────────
213
+
214
+ describe("stats()", () => {
215
+ it("returns correct counts", () => {
216
+ const s1 = registry.createSession("claude", "cli-claude/claude-sonnet-4-6");
217
+ const s2 = registry.createSession("gemini", "cli-gemini/gemini-2.5-pro");
218
+ registry.recordRun(s2.id, false); // s2 → idle
219
+ const stats = registry.stats();
220
+ expect(stats.total).toBe(2);
221
+ expect(stats.active).toBe(1);
222
+ expect(stats.idle).toBe(1);
223
+ expect(stats.expired).toBe(0);
224
+ });
225
+ });
226
+
227
+ // ── sweep ──────────────────────────────────────────────────────────────
228
+
229
+ describe("sweep()", () => {
230
+ it("removes stale sessions", () => {
231
+ const s = registry.createSession("claude", "cli-claude/claude-sonnet-4-6");
232
+ // Make the session very old
233
+ s.updatedAt = Date.now() - 3 * 60 * 60 * 1000; // 3 hours ago
234
+ registry.sweep();
235
+ expect(registry.getSession(s.id)).toBeUndefined();
236
+ });
237
+
238
+ it("keeps recent sessions", () => {
239
+ const s = registry.createSession("claude", "cli-claude/claude-sonnet-4-6");
240
+ registry.sweep();
241
+ expect(registry.getSession(s.id)).toBeDefined();
242
+ });
243
+ });
244
+
245
+ // ── persistence ────────────────────────────────────────────────────────
246
+
247
+ describe("persistence", () => {
248
+ it("loads sessions from disk on construction", () => {
249
+ const stored = {
250
+ version: 1,
251
+ sessions: [{
252
+ id: "claude:session-abc123",
253
+ provider: "claude",
254
+ modelAlias: "cli-claude/claude-sonnet-4-6",
255
+ createdAt: Date.now() - 1000,
256
+ updatedAt: Date.now() - 500,
257
+ state: "idle" as const,
258
+ runCount: 3,
259
+ timeoutCount: 1,
260
+ meta: {},
261
+ }],
262
+ };
263
+ mockReadFileSync.mockReturnValue(JSON.stringify(stored));
264
+ const freshRegistry = new ProviderSessionRegistry();
265
+ const loaded = freshRegistry.getSession("claude:session-abc123");
266
+ expect(loaded).toBeDefined();
267
+ expect(loaded!.runCount).toBe(3);
268
+ freshRegistry.stop();
269
+ });
270
+
271
+ it("skips expired sessions on load", () => {
272
+ const stored = {
273
+ version: 1,
274
+ sessions: [{
275
+ id: "claude:session-old",
276
+ provider: "claude",
277
+ modelAlias: "cli-claude/claude-sonnet-4-6",
278
+ createdAt: Date.now() - 5 * 60 * 60 * 1000,
279
+ updatedAt: Date.now() - 5 * 60 * 60 * 1000, // 5 hours ago
280
+ state: "idle" as const,
281
+ runCount: 1,
282
+ timeoutCount: 0,
283
+ meta: {},
284
+ }],
285
+ };
286
+ mockReadFileSync.mockReturnValue(JSON.stringify(stored));
287
+ const freshRegistry = new ProviderSessionRegistry();
288
+ expect(freshRegistry.getSession("claude:session-old")).toBeUndefined();
289
+ freshRegistry.stop();
290
+ });
291
+ });
292
+ });
293
+
294
+ // Config module tests are in a separate file (config.test.ts) without mocks.
@@ -62,6 +62,20 @@ vi.mock("../src/workdir.js", () => ({
62
62
  sweepOrphanedWorkdirs: mockSweepOrphanedWorkdirs,
63
63
  }));
64
64
 
65
+ // Mock config module — provide all constants needed by session-manager.ts and cli-runner.ts
66
+ vi.mock("../src/config.js", async () => {
67
+ return {
68
+ SESSION_TTL_MS: 30 * 60 * 1000,
69
+ CLEANUP_INTERVAL_MS: 5 * 60 * 1000,
70
+ SESSION_KILL_GRACE_MS: 5_000,
71
+ DEFAULT_CLI_TIMEOUT_MS: 120_000,
72
+ TIMEOUT_GRACE_MS: 5_000,
73
+ MAX_MESSAGES: 20,
74
+ MAX_MSG_CHARS: 4_000,
75
+ MEDIA_TMP_DIR: "/tmp/cli-bridge-media",
76
+ };
77
+ });
78
+
65
79
  // Now import SessionManager (uses the mocked spawn)
66
80
  import { SessionManager } from "../src/session-manager.js";
67
81