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.
- package/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/plugin-sdk/mattermost.js +3 -3
- package/dist/plugin-sdk/signal.js +2 -2
- package/docs/browser-architecture.md +602 -0
- package/extensions/googlechat/node_modules/.bin/openclaw +2 -2
- package/extensions/memory-core/node_modules/.bin/openclaw +2 -2
- package/extensions/memory-lancedb/node_modules/.bin/openai +2 -2
- package/extensions/page-action-cache/dist/actions-executor.d.ts +62 -0
- package/extensions/page-action-cache/dist/actions-executor.d.ts.map +1 -0
- package/extensions/page-action-cache/dist/actions-executor.js +339 -0
- package/extensions/page-action-cache/dist/actions-executor.js.map +1 -0
- package/extensions/page-action-cache/dist/cache-invalidator.d.ts +70 -0
- package/extensions/page-action-cache/dist/cache-invalidator.d.ts.map +1 -0
- package/extensions/page-action-cache/dist/cache-invalidator.js +212 -0
- package/extensions/page-action-cache/dist/cache-invalidator.js.map +1 -0
- package/extensions/page-action-cache/dist/cache-store.d.ts +80 -0
- package/extensions/page-action-cache/dist/cache-store.d.ts.map +1 -0
- package/extensions/page-action-cache/dist/cache-store.js +361 -0
- package/extensions/page-action-cache/dist/cache-store.js.map +1 -0
- package/extensions/page-action-cache/dist/cache-strategy.d.ts +65 -0
- package/extensions/page-action-cache/dist/cache-strategy.d.ts.map +1 -0
- package/extensions/page-action-cache/dist/cache-strategy.js +237 -0
- package/extensions/page-action-cache/dist/cache-strategy.js.map +1 -0
- package/extensions/page-action-cache/dist/hooks-entry.d.ts +18 -0
- package/extensions/page-action-cache/dist/hooks-entry.d.ts.map +1 -0
- package/extensions/page-action-cache/dist/hooks-entry.js +27 -0
- package/extensions/page-action-cache/dist/hooks-entry.js.map +1 -0
- package/extensions/page-action-cache/dist/hooks.d.ts +10 -0
- package/extensions/page-action-cache/dist/hooks.d.ts.map +1 -0
- package/extensions/page-action-cache/dist/hooks.js +277 -0
- package/extensions/page-action-cache/dist/hooks.js.map +1 -0
- package/extensions/page-action-cache/dist/index.d.ts +24 -0
- package/extensions/page-action-cache/dist/index.d.ts.map +1 -0
- package/extensions/page-action-cache/dist/index.js +34 -0
- package/extensions/page-action-cache/dist/index.js.map +1 -0
- package/extensions/page-action-cache/dist/scenario-recognizer.d.ts +45 -0
- package/extensions/page-action-cache/dist/scenario-recognizer.d.ts.map +1 -0
- package/extensions/page-action-cache/dist/scenario-recognizer.js +213 -0
- package/extensions/page-action-cache/dist/scenario-recognizer.js.map +1 -0
- package/extensions/page-action-cache/dist/security-policy.d.ts +62 -0
- package/extensions/page-action-cache/dist/security-policy.d.ts.map +1 -0
- package/extensions/page-action-cache/dist/security-policy.js +219 -0
- package/extensions/page-action-cache/dist/security-policy.js.map +1 -0
- package/extensions/page-action-cache/dist/tools.d.ts +209 -0
- package/extensions/page-action-cache/dist/tools.d.ts.map +1 -0
- package/extensions/page-action-cache/dist/tools.js +383 -0
- package/extensions/page-action-cache/dist/tools.js.map +1 -0
- package/extensions/page-action-cache/dist/types.d.ts +336 -0
- package/extensions/page-action-cache/dist/types.d.ts.map +1 -0
- package/extensions/page-action-cache/dist/types.js +8 -0
- package/extensions/page-action-cache/dist/types.js.map +1 -0
- package/extensions/page-action-cache/dist/ux-enhancer.d.ts +60 -0
- package/extensions/page-action-cache/dist/ux-enhancer.d.ts.map +1 -0
- package/extensions/page-action-cache/dist/ux-enhancer.js +218 -0
- package/extensions/page-action-cache/dist/ux-enhancer.js.map +1 -0
- package/extensions/page-action-cache/dist/variable-resolver.d.ts +28 -0
- package/extensions/page-action-cache/dist/variable-resolver.d.ts.map +1 -0
- package/extensions/page-action-cache/dist/variable-resolver.js +201 -0
- package/extensions/page-action-cache/dist/variable-resolver.js.map +1 -0
- package/extensions/page-action-cache/docs/API.md +555 -0
- package/extensions/page-action-cache/docs/IMPLEMENTATION.md +1792 -0
- package/extensions/page-action-cache/docs/INTEGRATION.md +387 -0
- package/extensions/page-action-cache/docs/README.md +183 -0
- package/extensions/page-action-cache/index.ts +118 -0
- package/extensions/page-action-cache/node_modules/.bin/nlc +21 -0
- package/extensions/page-action-cache/node_modules/.bin/node-llama-cpp +21 -0
- package/extensions/page-action-cache/node_modules/.bin/openclaw +21 -0
- package/extensions/page-action-cache/node_modules/.bin/tsc +21 -0
- package/extensions/page-action-cache/node_modules/.bin/tsserver +21 -0
- package/extensions/page-action-cache/node_modules/.bin/vitest +21 -0
- package/extensions/page-action-cache/openclaw.plugin.json +208 -0
- package/extensions/page-action-cache/package.json +76 -0
- package/extensions/page-action-cache/scripts/npm_publish.sh +80 -0
- package/extensions/page-action-cache/skills/page-action-cache/SKILL.md +216 -0
- package/extensions/page-action-cache/src/actions-executor.ts +441 -0
- package/extensions/page-action-cache/src/cache-invalidator.ts +271 -0
- package/extensions/page-action-cache/src/cache-store.ts +457 -0
- package/extensions/page-action-cache/src/cache-strategy.ts +327 -0
- package/extensions/page-action-cache/src/hooks-entry.ts +114 -0
- package/extensions/page-action-cache/src/hooks.ts +332 -0
- package/extensions/page-action-cache/src/index.ts +104 -0
- package/extensions/page-action-cache/src/scenario-recognizer.ts +259 -0
- package/extensions/page-action-cache/src/security-policy.ts +268 -0
- package/extensions/page-action-cache/src/tools.ts +437 -0
- package/extensions/page-action-cache/src/types.ts +482 -0
- package/extensions/page-action-cache/src/ux-enhancer.ts +266 -0
- package/extensions/page-action-cache/src/variable-resolver.ts +258 -0
- package/extensions/page-action-cache/tests/actions-executor.test.ts +424 -0
- package/extensions/page-action-cache/tests/cache-store.test.ts +267 -0
- package/extensions/page-action-cache/tests/integration-test.ts +62 -0
- package/extensions/page-action-cache/tests/scenario-recognizer.test.ts +140 -0
- package/extensions/page-action-cache/tests/variable-resolver.test.ts +187 -0
- package/extensions/page-action-cache/tsconfig.json +39 -0
- package/package.json +1 -1
- package/scripts/create-instance.sh +26 -8
- package/scripts/npm_publish.sh +59 -1
- package/scripts/publish-extension.sh +343 -0
- package/ui/node_modules/.bin/vite +2 -2
- 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
|
+
});
|