openclaw-multi-auto 1.4.5 → 1.4.7

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.
Files changed (100) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/canvas-host/a2ui/.bundle.hash +1 -1
  3. package/dist/plugin-sdk/mattermost.js +3 -3
  4. package/dist/plugin-sdk/signal.js +2 -2
  5. package/docs/browser-architecture.md +602 -0
  6. package/extensions/googlechat/node_modules/.bin/openclaw +2 -2
  7. package/extensions/memory-core/node_modules/.bin/openclaw +2 -2
  8. package/extensions/memory-lancedb/node_modules/.bin/openai +2 -2
  9. package/extensions/page-action-cache/dist/actions-executor.d.ts +62 -0
  10. package/extensions/page-action-cache/dist/actions-executor.d.ts.map +1 -0
  11. package/extensions/page-action-cache/dist/actions-executor.js +339 -0
  12. package/extensions/page-action-cache/dist/actions-executor.js.map +1 -0
  13. package/extensions/page-action-cache/dist/cache-invalidator.d.ts +70 -0
  14. package/extensions/page-action-cache/dist/cache-invalidator.d.ts.map +1 -0
  15. package/extensions/page-action-cache/dist/cache-invalidator.js +212 -0
  16. package/extensions/page-action-cache/dist/cache-invalidator.js.map +1 -0
  17. package/extensions/page-action-cache/dist/cache-store.d.ts +80 -0
  18. package/extensions/page-action-cache/dist/cache-store.d.ts.map +1 -0
  19. package/extensions/page-action-cache/dist/cache-store.js +361 -0
  20. package/extensions/page-action-cache/dist/cache-store.js.map +1 -0
  21. package/extensions/page-action-cache/dist/cache-strategy.d.ts +65 -0
  22. package/extensions/page-action-cache/dist/cache-strategy.d.ts.map +1 -0
  23. package/extensions/page-action-cache/dist/cache-strategy.js +237 -0
  24. package/extensions/page-action-cache/dist/cache-strategy.js.map +1 -0
  25. package/extensions/page-action-cache/dist/hooks-entry.d.ts +18 -0
  26. package/extensions/page-action-cache/dist/hooks-entry.d.ts.map +1 -0
  27. package/extensions/page-action-cache/dist/hooks-entry.js +27 -0
  28. package/extensions/page-action-cache/dist/hooks-entry.js.map +1 -0
  29. package/extensions/page-action-cache/dist/hooks.d.ts +10 -0
  30. package/extensions/page-action-cache/dist/hooks.d.ts.map +1 -0
  31. package/extensions/page-action-cache/dist/hooks.js +277 -0
  32. package/extensions/page-action-cache/dist/hooks.js.map +1 -0
  33. package/extensions/page-action-cache/dist/index.d.ts +24 -0
  34. package/extensions/page-action-cache/dist/index.d.ts.map +1 -0
  35. package/extensions/page-action-cache/dist/index.js +34 -0
  36. package/extensions/page-action-cache/dist/index.js.map +1 -0
  37. package/extensions/page-action-cache/dist/scenario-recognizer.d.ts +45 -0
  38. package/extensions/page-action-cache/dist/scenario-recognizer.d.ts.map +1 -0
  39. package/extensions/page-action-cache/dist/scenario-recognizer.js +213 -0
  40. package/extensions/page-action-cache/dist/scenario-recognizer.js.map +1 -0
  41. package/extensions/page-action-cache/dist/security-policy.d.ts +62 -0
  42. package/extensions/page-action-cache/dist/security-policy.d.ts.map +1 -0
  43. package/extensions/page-action-cache/dist/security-policy.js +219 -0
  44. package/extensions/page-action-cache/dist/security-policy.js.map +1 -0
  45. package/extensions/page-action-cache/dist/tools.d.ts +209 -0
  46. package/extensions/page-action-cache/dist/tools.d.ts.map +1 -0
  47. package/extensions/page-action-cache/dist/tools.js +383 -0
  48. package/extensions/page-action-cache/dist/tools.js.map +1 -0
  49. package/extensions/page-action-cache/dist/types.d.ts +336 -0
  50. package/extensions/page-action-cache/dist/types.d.ts.map +1 -0
  51. package/extensions/page-action-cache/dist/types.js +8 -0
  52. package/extensions/page-action-cache/dist/types.js.map +1 -0
  53. package/extensions/page-action-cache/dist/ux-enhancer.d.ts +60 -0
  54. package/extensions/page-action-cache/dist/ux-enhancer.d.ts.map +1 -0
  55. package/extensions/page-action-cache/dist/ux-enhancer.js +218 -0
  56. package/extensions/page-action-cache/dist/ux-enhancer.js.map +1 -0
  57. package/extensions/page-action-cache/dist/variable-resolver.d.ts +28 -0
  58. package/extensions/page-action-cache/dist/variable-resolver.d.ts.map +1 -0
  59. package/extensions/page-action-cache/dist/variable-resolver.js +201 -0
  60. package/extensions/page-action-cache/dist/variable-resolver.js.map +1 -0
  61. package/extensions/page-action-cache/docs/API.md +555 -0
  62. package/extensions/page-action-cache/docs/IMPLEMENTATION.md +1792 -0
  63. package/extensions/page-action-cache/docs/INTEGRATION.md +387 -0
  64. package/extensions/page-action-cache/docs/README.md +183 -0
  65. package/extensions/page-action-cache/index.ts +118 -0
  66. package/extensions/page-action-cache/node_modules/.bin/nlc +21 -0
  67. package/extensions/page-action-cache/node_modules/.bin/node-llama-cpp +21 -0
  68. package/extensions/page-action-cache/node_modules/.bin/openclaw +21 -0
  69. package/extensions/page-action-cache/node_modules/.bin/tsc +21 -0
  70. package/extensions/page-action-cache/node_modules/.bin/tsserver +21 -0
  71. package/extensions/page-action-cache/node_modules/.bin/vitest +21 -0
  72. package/extensions/page-action-cache/openclaw.plugin.json +208 -0
  73. package/extensions/page-action-cache/package.json +76 -0
  74. package/extensions/page-action-cache/scripts/npm_publish.sh +80 -0
  75. package/extensions/page-action-cache/skills/page-action-cache/SKILL.md +216 -0
  76. package/extensions/page-action-cache/src/actions-executor.ts +441 -0
  77. package/extensions/page-action-cache/src/cache-invalidator.ts +271 -0
  78. package/extensions/page-action-cache/src/cache-store.ts +457 -0
  79. package/extensions/page-action-cache/src/cache-strategy.ts +327 -0
  80. package/extensions/page-action-cache/src/hooks-entry.ts +114 -0
  81. package/extensions/page-action-cache/src/hooks.ts +332 -0
  82. package/extensions/page-action-cache/src/index.ts +104 -0
  83. package/extensions/page-action-cache/src/scenario-recognizer.ts +259 -0
  84. package/extensions/page-action-cache/src/security-policy.ts +268 -0
  85. package/extensions/page-action-cache/src/tools.ts +437 -0
  86. package/extensions/page-action-cache/src/types.ts +482 -0
  87. package/extensions/page-action-cache/src/ux-enhancer.ts +266 -0
  88. package/extensions/page-action-cache/src/variable-resolver.ts +258 -0
  89. package/extensions/page-action-cache/tests/actions-executor.test.ts +424 -0
  90. package/extensions/page-action-cache/tests/cache-store.test.ts +267 -0
  91. package/extensions/page-action-cache/tests/integration-test.ts +62 -0
  92. package/extensions/page-action-cache/tests/scenario-recognizer.test.ts +140 -0
  93. package/extensions/page-action-cache/tests/variable-resolver.test.ts +187 -0
  94. package/extensions/page-action-cache/tsconfig.json +39 -0
  95. package/package.json +1 -1
  96. package/scripts/create-instance.sh +26 -8
  97. package/scripts/npm_publish.sh +59 -1
  98. package/scripts/publish-extension.sh +343 -0
  99. package/ui/node_modules/.bin/vite +2 -2
  100. package/ui/node_modules/.bin/vitest +2 -2
@@ -0,0 +1,424 @@
1
+ /**
2
+ * Actions Executor Tests
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, vi } from "vitest";
6
+ import { ActionsExecutor } from "../src/actions-executor.js";
7
+ import type { AtomicAction, PwAi, VariableMap } from "../src/types.js";
8
+
9
+ // Mock Playwright client
10
+ const mockPwAi = {
11
+ navigateViaPlaywright: vi.fn(),
12
+ clickViaPlaywright: vi.fn(),
13
+ typeViaPlaywright: vi.fn(),
14
+ pressKeyViaPlaywright: vi.fn(),
15
+ hoverViaPlaywright: vi.fn(),
16
+ screenshotViaPlaywright: vi.fn(),
17
+ evaluateViaPlaywright: vi.fn(),
18
+ } as unknown as PwAi;
19
+
20
+ describe("ActionsExecutor", () => {
21
+ let executor: ActionsExecutor;
22
+
23
+ beforeEach(() => {
24
+ executor = new ActionsExecutor("http://localhost:9222");
25
+ executor.setPlaywrightClient(mockPwAi);
26
+ vi.clearAllMocks();
27
+ });
28
+
29
+ describe("executeNavigate", () => {
30
+ it("should execute navigate action", async () => {
31
+ const action = {
32
+ type: "navigate" as const,
33
+ url: "https://example.com",
34
+ order: 1,
35
+ };
36
+
37
+ const result = await executor.execute(action);
38
+
39
+ expect(result.success).toBe(true);
40
+ expect(result.action).toBe("navigate");
41
+ expect(mockPwAi.navigateViaPlaywright).toHaveBeenCalledWith({
42
+ cdpUrl: "http://localhost:9222",
43
+ targetId: undefined,
44
+ url: "https://example.com",
45
+ waitUntil: "domcontentloaded",
46
+ timeout: undefined,
47
+ });
48
+ });
49
+
50
+ it("should use custom navigation policy", async () => {
51
+ const action = {
52
+ type: "navigate" as const,
53
+ url: "https://example.com",
54
+ order: 1,
55
+ navigationPolicy: {
56
+ waitUntil: "load" as const,
57
+ timeout: 30000,
58
+ },
59
+ };
60
+
61
+ const result = await executor.execute(action);
62
+
63
+ expect(result.success).toBe(true);
64
+ expect(mockPwAi.navigateViaPlaywright).toHaveBeenCalledWith({
65
+ cdpUrl: "http://localhost:9222",
66
+ targetId: undefined,
67
+ url: "https://example.com",
68
+ waitUntil: "load",
69
+ timeout: 30000,
70
+ });
71
+ });
72
+ });
73
+
74
+ describe("executeClick", () => {
75
+ it("should execute click action", async () => {
76
+ const action = {
77
+ type: "click" as const,
78
+ ref: "#submit-button",
79
+ order: 1,
80
+ };
81
+
82
+ const result = await executor.execute(action);
83
+
84
+ expect(result.success).toBe(true);
85
+ expect(result.action).toBe("click");
86
+ expect(mockPwAi.clickViaPlaywright).toHaveBeenCalledWith({
87
+ cdpUrl: "http://localhost:9222",
88
+ targetId: undefined,
89
+ ref: "#submit-button",
90
+ doubleClick: undefined,
91
+ button: "left",
92
+ modifiers: undefined,
93
+ timeoutMs: undefined,
94
+ });
95
+ });
96
+
97
+ it("should execute double click", async () => {
98
+ const action = {
99
+ type: "click" as const,
100
+ ref: "#button",
101
+ doubleClick: true,
102
+ order: 1,
103
+ };
104
+
105
+ const result = await executor.execute(action);
106
+
107
+ expect(result.success).toBe(true);
108
+ expect(mockPwAi.clickViaPlaywright).toHaveBeenCalledWith(
109
+ expect.objectContaining({
110
+ doubleClick: true,
111
+ })
112
+ );
113
+ });
114
+
115
+ it("should execute click with modifiers", async () => {
116
+ const action = {
117
+ type: "click" as const,
118
+ ref: "#button",
119
+ button: "left" as const,
120
+ modifiers: ["Control", "Shift"],
121
+ order: 1,
122
+ };
123
+
124
+ const result = await executor.execute(action);
125
+
126
+ expect(result.success).toBe(true);
127
+ expect(mockPwAi.clickViaPlaywright).toHaveBeenCalledWith(
128
+ expect.objectContaining({
129
+ button: "left",
130
+ modifiers: ["Control", "Shift"],
131
+ })
132
+ );
133
+ });
134
+ });
135
+
136
+ describe("executeType", () => {
137
+ it("should execute type action", async () => {
138
+ const action = {
139
+ type: "type" as const,
140
+ ref: "#username",
141
+ text: "alice",
142
+ order: 1,
143
+ };
144
+
145
+ const result = await executor.execute(action);
146
+
147
+ expect(result.success).toBe(true);
148
+ expect(result.action).toBe("type");
149
+ expect(mockPwAi.typeViaPlaywright).toHaveBeenCalledWith({
150
+ cdpUrl: "http://localhost:9222",
151
+ targetId: undefined,
152
+ ref: "#username",
153
+ text: "alice",
154
+ submit: undefined,
155
+ slowly: undefined,
156
+ timeoutMs: undefined,
157
+ });
158
+ });
159
+
160
+ it("should execute type with submit", async () => {
161
+ const action = {
162
+ type: "type" as const,
163
+ ref: "#username",
164
+ text: "alice",
165
+ submit: true,
166
+ order: 1,
167
+ };
168
+
169
+ const result = await executor.execute(action);
170
+
171
+ expect(result.success).toBe(true);
172
+ expect(mockPwAi.typeViaPlaywright).toHaveBeenCalledWith(
173
+ expect.objectContaining({
174
+ submit: true,
175
+ })
176
+ );
177
+ });
178
+
179
+ it("should execute type with slowly", async () => {
180
+ const action = {
181
+ type: "type" as const,
182
+ ref: "#username",
183
+ text: "alice",
184
+ slowly: true,
185
+ order: 1,
186
+ };
187
+
188
+ const result = await executor.execute(action);
189
+
190
+ expect(result.success).toBe(true);
191
+ expect(mockPwAi.typeViaPlaywright).toHaveBeenCalledWith(
192
+ expect.objectContaining({
193
+ slowly: true,
194
+ })
195
+ );
196
+ });
197
+ });
198
+
199
+ describe("executePress", () => {
200
+ it("should execute press action", async () => {
201
+ const action = {
202
+ type: "press" as const,
203
+ key: "Enter",
204
+ order: 1,
205
+ };
206
+
207
+ const result = await executor.execute(action);
208
+
209
+ expect(result.success).toBe(true);
210
+ expect(result.action).toBe("press");
211
+ expect(mockPwAi.pressKeyViaPlaywright).toHaveBeenCalledWith({
212
+ cdpUrl: "http://localhost:9222",
213
+ targetId: undefined,
214
+ key: "Enter",
215
+ delayMs: undefined,
216
+ });
217
+ });
218
+
219
+ it("should execute press with delay", async () => {
220
+ const action = {
221
+ type: "press" as const,
222
+ key: "Tab",
223
+ delayMs: 100,
224
+ order: 1,
225
+ };
226
+
227
+ const result = await executor.execute(action);
228
+
229
+ expect(result.success).toBe(true);
230
+ expect(mockPwAi.pressKeyViaPlaywright).toHaveBeenCalledWith(
231
+ expect.objectContaining({
232
+ delayMs: 100,
233
+ })
234
+ );
235
+ });
236
+ });
237
+
238
+ describe("executeHover", () => {
239
+ it("should execute hover action", async () => {
240
+ const action = {
241
+ type: "hover" as const,
242
+ ref: "#menu",
243
+ order: 1,
244
+ };
245
+
246
+ const result = await executor.execute(action);
247
+
248
+ expect(result.success).toBe(true);
249
+ expect(result.action).toBe("hover");
250
+ expect(mockPwAi.hoverViaPlaywright).toHaveBeenCalledWith({
251
+ cdpUrl: "http://localhost:9222",
252
+ targetId: undefined,
253
+ ref: "#menu",
254
+ timeoutMs: undefined,
255
+ });
256
+ });
257
+ });
258
+
259
+ describe("executeEvaluate", () => {
260
+ it("should execute evaluate action", async () => {
261
+ const action = {
262
+ type: "scroll" as const,
263
+ evaluate: {
264
+ code: "window.scrollTo(0, 500)",
265
+ },
266
+ order: 1,
267
+ };
268
+
269
+ const result = await executor.execute(action);
270
+
271
+ expect(result.success).toBe(true);
272
+ expect(result.action).toBe("scroll");
273
+ expect(mockPwAi.evaluateViaPlaywright).toHaveBeenCalledWith({
274
+ cdpUrl: "http://localhost:9222",
275
+ targetId: undefined,
276
+ code: "window.scrollTo(0, 500)",
277
+ args: undefined,
278
+ });
279
+ });
280
+ });
281
+
282
+ describe("variable replacement", () => {
283
+ it("should replace variable in type action", async () => {
284
+ const action = {
285
+ type: "type" as const,
286
+ ref: "#username",
287
+ variable: "username",
288
+ order: 1,
289
+ };
290
+
291
+ const variables: VariableMap = { username: "alice" };
292
+ const result = await executor.execute(action, variables);
293
+
294
+ expect(result.success).toBe(true);
295
+ expect(mockPwAi.typeViaPlaywright).toHaveBeenCalledWith(
296
+ expect.objectContaining({
297
+ text: "alice",
298
+ })
299
+ );
300
+ });
301
+
302
+ it("should replace variable in press action", async () => {
303
+ const action = {
304
+ type: "press" as const,
305
+ variable: "key",
306
+ order: 1,
307
+ };
308
+
309
+ const variables: VariableMap = { key: "Enter" };
310
+ const result = await executor.execute(action, variables);
311
+
312
+ expect(result.success).toBe(true);
313
+ expect(mockPwAi.pressKeyViaPlaywright).toHaveBeenCalledWith(
314
+ expect.objectContaining({
315
+ key: "Enter",
316
+ })
317
+ );
318
+ });
319
+
320
+ it("should not fail when variable is undefined", async () => {
321
+ const action = {
322
+ type: "type" as const,
323
+ ref: "#username",
324
+ variable: "username",
325
+ order: 1,
326
+ };
327
+
328
+ const result = await executor.execute(action);
329
+
330
+ // Should execute without error (variable undefined = skip)
331
+ expect(result.success).toBe(true);
332
+ });
333
+ });
334
+
335
+ describe("executeBatch", () => {
336
+ it("should execute multiple actions", async () => {
337
+ const actions = [
338
+ { type: "type" as const, ref: "#username", text: "alice", order: 1 },
339
+ { type: "type" as const, ref: "#password", text: "secret", order: 2 },
340
+ { type: "press" as const, key: "Enter", order: 3 },
341
+ ];
342
+
343
+ const results = await executor.executeBatch(actions);
344
+
345
+ expect(results).toHaveLength(3);
346
+ expect(results[0].success).toBe(true);
347
+ expect(results[1].success).toBe(true);
348
+ expect(results[2].success).toBe(true);
349
+ });
350
+
351
+ it("should support fromIndex and toIndex", async () => {
352
+ const actions = [
353
+ { type: "type" as const, ref: "#username", text: "alice", order: 1 },
354
+ { type: "type" as const, ref: "#password", text: "secret", order: 2 },
355
+ { type: "press" as const, key: "Enter", order: 3 },
356
+ ];
357
+
358
+ const results = await executor.executeBatch(actions, undefined, {
359
+ fromIndex: 1,
360
+ toIndex: 2,
361
+ });
362
+
363
+ expect(results).toHaveLength(2);
364
+ expect(mockPwAi.typeViaPlaywright).toHaveBeenCalledTimes(2);
365
+ expect(mockPwAi.pressKeyViaPlaywright).not.toHaveBeenCalled();
366
+ });
367
+
368
+ it("should stop on failure when atomic is true", async () => {
369
+ const actions = [
370
+ { type: "type" as const, ref: "#username", text: "alice", order: 1 },
371
+ { type: "type" as const, ref: "#password", text: "secret", order: 2 },
372
+ { type: "press" as const, key: "Enter", order: 3 },
373
+ ];
374
+
375
+ // Mock second action to fail
376
+ vi.mocked(mockPwAi.typeViaPlaywright).mockImplementationOnce(
377
+ async () => {
378
+ throw new Error("Element not found");
379
+ }
380
+ );
381
+
382
+ const results = await executor.executeBatch(actions, undefined, {
383
+ atomic: true,
384
+ });
385
+
386
+ expect(results).toHaveLength(2); // Only first two executed
387
+ expect(results[0].success).toBe(true);
388
+ expect(results[1].success).toBe(false);
389
+ expect(mockPwAi.pressKeyViaPlaywright).not.toHaveBeenCalled();
390
+ });
391
+ });
392
+
393
+ describe("formatActions", () => {
394
+ it("should format actions for display", () => {
395
+ const actions: AtomicAction[] = [
396
+ { type: "navigate" as const, url: "https://example.com", order: 1 },
397
+ { type: "type" as const, ref: "#username", text: "alice", order: 2 },
398
+ { type: "press" as const, key: "Enter", order: 3 },
399
+ ];
400
+
401
+ const formatted = executor.formatActions(actions);
402
+
403
+ expect(formatted).toContain("1. 导航到:https://example.com");
404
+ expect(formatted).toContain('2. 输入:#username="alice"');
405
+ expect(formatted).toContain("3. 按键:Enter");
406
+ });
407
+ });
408
+
409
+ describe("formatExecutionResults", () => {
410
+ it("should format execution results", () => {
411
+ const results = [
412
+ { action: "navigate", success: true, duration: 100 },
413
+ { action: "click", success: true, duration: 50 },
414
+ { action: "type", success: false, duration: 200, error: "Timeout" },
415
+ ];
416
+
417
+ const formatted = executor.formatExecutionResults(results);
418
+
419
+ expect(formatted).toContain("✅ 操作 1: navigate (100ms)");
420
+ expect(formatted).toContain("✅ 操作 2: click (50ms)");
421
+ expect(formatted).toContain("❌ 操作 3: type (200ms) - Timeout");
422
+ });
423
+ });
424
+ });
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Cache Store Tests
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
6
+ import { CacheStore } from "../src/cache-store.js";
7
+
8
+ describe("CacheStore", () => {
9
+ let cacheStore: CacheStore;
10
+
11
+ beforeEach(() => {
12
+ cacheStore = new CacheStore();
13
+ });
14
+
15
+ afterEach(() => {
16
+ cacheStore.clear();
17
+ });
18
+
19
+ describe("set and get", () => {
20
+ it("should save and retrieve cache entry", () => {
21
+ const url = "https://example.com/login";
22
+ const viewport = { width: 1920, height: 1080 };
23
+ const actions = [
24
+ { type: "type", ref: "#username", text: "test", order: 1 },
25
+ ];
26
+
27
+ const key = cacheStore.set(url, viewport, actions, {
28
+ scenario: "login",
29
+ cacheLevel: "L3",
30
+ });
31
+
32
+ expect(key).toBeDefined();
33
+ expect(typeof key).toBe("string");
34
+
35
+ const entry = cacheStore.get(url, viewport);
36
+ expect(entry).not.toBeNull();
37
+ expect(entry?.scenario).toBe("login");
38
+ expect(entry?.cacheLevel).toBe("L3");
39
+ expect(entry?.actions).toEqual(actions);
40
+ });
41
+
42
+ it("should return null for non-existent cache", () => {
43
+ const url = "https://example.com/nonexistent";
44
+ const viewport = { width: 1920, height: 1080 };
45
+
46
+ const entry = cacheStore.get(url, viewport);
47
+ expect(entry).toBeNull();
48
+ });
49
+
50
+ it("should normalize URL", () => {
51
+ const url1 = "https://example.com/login?param=value";
52
+ const url2 = "https://example.com/login";
53
+ const viewport = { width: 1920, height: 1080 };
54
+ const actions = [];
55
+
56
+ cacheStore.set(url1, viewport, actions, {
57
+ scenario: "login",
58
+ cacheLevel: "L3",
59
+ });
60
+
61
+ const entry = cacheStore.get(url2, viewport);
62
+ expect(entry).not.toBeNull();
63
+ });
64
+ });
65
+
66
+ describe("delete", () => {
67
+ it("should delete cache entry", () => {
68
+ const url = "https://example.com/login";
69
+ const viewport = { width: 1920, height: 1080 };
70
+ const actions = [];
71
+
72
+ cacheStore.set(url, viewport, actions, {
73
+ scenario: "login",
74
+ cacheLevel: "L3",
75
+ });
76
+
77
+ const deleted = cacheStore.delete(url, viewport);
78
+ expect(deleted).toBe(true);
79
+
80
+ const entry = cacheStore.get(url, viewport);
81
+ expect(entry).toBeNull();
82
+ });
83
+
84
+ it("should return false for non-existent cache", () => {
85
+ const url = "https://example.com/nonexistent";
86
+ const viewport = { width: 1920, height: 1080 };
87
+
88
+ const deleted = cacheStore.delete(url, viewport);
89
+ expect(deleted).toBe(false);
90
+ });
91
+ });
92
+
93
+ describe("clear", () => {
94
+ it("should clear all cache entries", () => {
95
+ const url1 = "https://example.com/login";
96
+ const url2 = "https://example.com/search";
97
+ const viewport = { width: 1920, height: 1080 };
98
+ const actions = [];
99
+
100
+ cacheStore.set(url1, viewport, actions, {
101
+ scenario: "login",
102
+ cacheLevel: "L3",
103
+ });
104
+ cacheStore.set(url2, viewport, actions, {
105
+ scenario: "search",
106
+ cacheLevel: "L3",
107
+ });
108
+
109
+ cacheStore.clear();
110
+
111
+ const stats = cacheStore.getStats();
112
+ expect(stats.totalEntries).toBe(0);
113
+
114
+ const entry1 = cacheStore.get(url1, viewport);
115
+ const entry2 = cacheStore.get(url2, viewport);
116
+ expect(entry1).toBeNull();
117
+ expect(entry2).toBeNull();
118
+ });
119
+ });
120
+
121
+ describe("stats", () => {
122
+ it("should track hits and misses", () => {
123
+ const url1 = "https://example.com/login";
124
+ const url2 = "https://example.com/nonexistent";
125
+ const viewport = { width: 1920, height: 1080 };
126
+ const actions = [];
127
+
128
+ cacheStore.set(url1, viewport, actions, {
129
+ scenario: "login",
130
+ cacheLevel: "L3",
131
+ });
132
+
133
+ // Hit
134
+ cacheStore.get(url1, viewport);
135
+
136
+ // Miss
137
+ cacheStore.get(url2, viewport);
138
+
139
+ const stats = cacheStore.getStats();
140
+ expect(stats.totalHits).toBe(1);
141
+ expect(stats.totalMisses).toBe(1);
142
+ expect(stats.hitRate).toBe(50);
143
+ });
144
+
145
+ it("should calculate hit rate correctly", () => {
146
+ const url = "https://example.com/login";
147
+ const viewport = { width: 1920, height: 1080 };
148
+ const actions = [];
149
+
150
+ cacheStore.set(url, viewport, actions, {
151
+ scenario: "login",
152
+ cacheLevel: "L3",
153
+ });
154
+
155
+ // 3 hits
156
+ cacheStore.get(url, viewport);
157
+ cacheStore.get(url, viewport);
158
+ cacheStore.get(url, viewport);
159
+
160
+ // 1 miss
161
+ cacheStore.get("https://example.com/nonexistent", viewport);
162
+
163
+ const stats = cacheStore.getStats();
164
+ expect(stats.hitRate).toBe(75);
165
+ });
166
+ });
167
+
168
+ describe("listEntries", () => {
169
+ it("should list entries sorted by last access time", () => {
170
+ const url1 = "https://example.com/login";
171
+ const url2 = "https://example.com/search";
172
+ const viewport = { width: 1920, height: 1080 };
173
+ const actions = [];
174
+
175
+ cacheStore.set(url1, viewport, actions, {
176
+ scenario: "login",
177
+ cacheLevel: "L3",
178
+ });
179
+
180
+ // Add delay to ensure different timestamps
181
+ return new Promise<void>((resolve) => {
182
+ setTimeout(() => {
183
+ cacheStore.set(url2, viewport, actions, {
184
+ scenario: "search",
185
+ cacheLevel: "L3",
186
+ });
187
+
188
+ const entries = cacheStore.listEntries();
189
+ expect(entries).toHaveLength(2);
190
+ expect(entries[0].scenario).toBe("search"); // Most recent
191
+ expect(entries[1].scenario).toBe("login");
192
+
193
+ resolve();
194
+ }, 10);
195
+ });
196
+ });
197
+ });
198
+
199
+ describe("cleanupExpired", () => {
200
+ it("should clean up expired cache entries", () => {
201
+ const url = "https://example.com/login";
202
+ const viewport = { width: 1920, height: 1080 };
203
+ const actions = [];
204
+
205
+ // Manually create an expired entry
206
+ const cacheStoreInstance = cacheStore as any;
207
+ const key = cacheStoreInstance.generateKey(url, viewport);
208
+
209
+ cacheStoreInstance.store.entries[key] = {
210
+ key,
211
+ url,
212
+ viewport,
213
+ cacheLevel: "L3" as const,
214
+ scenario: "login",
215
+ description: "Test",
216
+ actions,
217
+ createdAt: Date.now() - 100000000, // Very old
218
+ lastAccessTime: Date.now() - 100000000,
219
+ accessCount: 0,
220
+ expiresAt: Date.now() - 10000, // Expired
221
+ pageChangeDetection: {
222
+ hasChanged: false,
223
+ changeType: "none",
224
+ confidence: 100,
225
+ domHash: "",
226
+ lastCheckedAt: Date.now(),
227
+ },
228
+ source: "llm" as const,
229
+ successCount: 0,
230
+ failCount: 0,
231
+ avgExecutionTime: 0,
232
+ };
233
+
234
+ const cleaned = cacheStore.cleanupExpired();
235
+ expect(cleaned).toBe(1);
236
+
237
+ const entry = cacheStore.get(url, viewport);
238
+ expect(entry).toBeNull();
239
+ });
240
+ });
241
+
242
+ describe("updateExecutionStats", () => {
243
+ it("should update execution statistics", () => {
244
+ const url = "https://example.com/login";
245
+ const viewport = { width: 1920, height: 1080 };
246
+ const actions = [];
247
+
248
+ const key = cacheStore.set(url, viewport, actions, {
249
+ scenario: "login",
250
+ cacheLevel: "L3",
251
+ });
252
+
253
+ const results = [
254
+ { action: "click", success: true, duration: 100 },
255
+ { action: "type", success: true, duration: 200 },
256
+ { action: "click", success: false, duration: 50 },
257
+ ];
258
+
259
+ cacheStore.updateExecutionStats(key, results);
260
+
261
+ const entry = cacheStore.get(url, viewport);
262
+ expect(entry?.successCount).toBe(2);
263
+ expect(entry?.failCount).toBe(1);
264
+ expect(entry?.avgExecutionTime).toBeCloseTo(116.67, 1); // (100+200+50)/3
265
+ });
266
+ });
267
+ });