@openqa/cli 1.0.12 → 1.1.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.
- package/dist/agent/index.js +1 -1
- package/dist/agent/index.js.map +1 -1
- package/dist/cli/index.js +566 -1633
- package/dist/cli/server.js +533 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,1640 +1,588 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
2
11
|
|
|
3
|
-
//
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import { Tracer } from "@orka-js/observability";
|
|
12
|
-
import { EventEmitter as EventEmitter3 } from "events";
|
|
12
|
+
// node_modules/tsup/assets/esm_shims.js
|
|
13
|
+
import path from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
var init_esm_shims = __esm({
|
|
16
|
+
"node_modules/tsup/assets/esm_shims.js"() {
|
|
17
|
+
"use strict";
|
|
18
|
+
}
|
|
19
|
+
});
|
|
13
20
|
|
|
14
21
|
// database/index.ts
|
|
15
22
|
import { Low } from "lowdb";
|
|
16
23
|
import { JSONFile } from "lowdb/node";
|
|
17
24
|
import { dirname } from "path";
|
|
18
|
-
import { fileURLToPath } from "url";
|
|
25
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
19
26
|
import { mkdirSync } from "fs";
|
|
20
|
-
var __filename
|
|
21
|
-
var
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const adapter = new JSONFile(this.dbPath);
|
|
32
|
-
this.db = new Low(adapter, {
|
|
33
|
-
config: {},
|
|
34
|
-
test_sessions: [],
|
|
35
|
-
actions: [],
|
|
36
|
-
bugs: [],
|
|
37
|
-
kanban_tickets: []
|
|
38
|
-
});
|
|
39
|
-
this.db.read();
|
|
40
|
-
if (!this.db.data) {
|
|
41
|
-
this.db.data = {
|
|
42
|
-
config: {},
|
|
43
|
-
test_sessions: [],
|
|
44
|
-
actions: [],
|
|
45
|
-
bugs: [],
|
|
46
|
-
kanban_tickets: []
|
|
47
|
-
};
|
|
48
|
-
this.db.write();
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
async ensureInitialized() {
|
|
52
|
-
if (!this.db) {
|
|
53
|
-
this.initialize();
|
|
54
|
-
}
|
|
55
|
-
await this.db.read();
|
|
56
|
-
}
|
|
57
|
-
async getConfig(key) {
|
|
58
|
-
await this.ensureInitialized();
|
|
59
|
-
return this.db.data.config[key] || null;
|
|
60
|
-
}
|
|
61
|
-
async setConfig(key, value) {
|
|
62
|
-
await this.ensureInitialized();
|
|
63
|
-
this.db.data.config[key] = value;
|
|
64
|
-
await this.db.write();
|
|
65
|
-
}
|
|
66
|
-
async getAllConfig() {
|
|
67
|
-
await this.ensureInitialized();
|
|
68
|
-
return this.db.data.config;
|
|
69
|
-
}
|
|
70
|
-
async createSession(id, metadata) {
|
|
71
|
-
await this.ensureInitialized();
|
|
72
|
-
const session = {
|
|
73
|
-
id,
|
|
74
|
-
started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
75
|
-
status: "running",
|
|
76
|
-
total_actions: 0,
|
|
77
|
-
bugs_found: 0,
|
|
78
|
-
metadata: metadata ? JSON.stringify(metadata) : void 0
|
|
79
|
-
};
|
|
80
|
-
this.db.data.test_sessions.push(session);
|
|
81
|
-
await this.db.write();
|
|
82
|
-
return session;
|
|
83
|
-
}
|
|
84
|
-
async getSession(id) {
|
|
85
|
-
await this.ensureInitialized();
|
|
86
|
-
return this.db.data.test_sessions.find((s) => s.id === id) || null;
|
|
87
|
-
}
|
|
88
|
-
async updateSession(id, updates) {
|
|
89
|
-
await this.ensureInitialized();
|
|
90
|
-
const index = this.db.data.test_sessions.findIndex((s) => s.id === id);
|
|
91
|
-
if (index !== -1) {
|
|
92
|
-
this.db.data.test_sessions[index] = { ...this.db.data.test_sessions[index], ...updates };
|
|
93
|
-
await this.db.write();
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
async getRecentSessions(limit = 10) {
|
|
97
|
-
await this.ensureInitialized();
|
|
98
|
-
return this.db.data.test_sessions.sort((a, b) => new Date(b.started_at).getTime() - new Date(a.started_at).getTime()).slice(0, limit);
|
|
99
|
-
}
|
|
100
|
-
async createAction(action) {
|
|
101
|
-
await this.ensureInitialized();
|
|
102
|
-
const newAction = {
|
|
103
|
-
id: `action_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
104
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
105
|
-
...action
|
|
106
|
-
};
|
|
107
|
-
this.db.data.actions.push(newAction);
|
|
108
|
-
await this.db.write();
|
|
109
|
-
return newAction;
|
|
110
|
-
}
|
|
111
|
-
async getSessionActions(sessionId) {
|
|
112
|
-
await this.ensureInitialized();
|
|
113
|
-
return this.db.data.actions.filter((a) => a.session_id === sessionId).sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
114
|
-
}
|
|
115
|
-
async createBug(bug) {
|
|
116
|
-
await this.ensureInitialized();
|
|
117
|
-
const newBug = {
|
|
118
|
-
id: `bug_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
119
|
-
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
120
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
121
|
-
...bug
|
|
122
|
-
};
|
|
123
|
-
this.db.data.bugs.push(newBug);
|
|
124
|
-
await this.db.write();
|
|
125
|
-
return newBug;
|
|
126
|
-
}
|
|
127
|
-
async updateBug(id, updates) {
|
|
128
|
-
await this.ensureInitialized();
|
|
129
|
-
const index = this.db.data.bugs.findIndex((b) => b.id === id);
|
|
130
|
-
if (index !== -1) {
|
|
131
|
-
this.db.data.bugs[index] = {
|
|
132
|
-
...this.db.data.bugs[index],
|
|
133
|
-
...updates,
|
|
134
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
135
|
-
};
|
|
136
|
-
await this.db.write();
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
async getAllBugs() {
|
|
140
|
-
await this.ensureInitialized();
|
|
141
|
-
return this.db.data.bugs.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
142
|
-
}
|
|
143
|
-
async getBugsByStatus(status) {
|
|
144
|
-
await this.ensureInitialized();
|
|
145
|
-
return this.db.data.bugs.filter((b) => b.status === status).sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
146
|
-
}
|
|
147
|
-
async createKanbanTicket(ticket) {
|
|
148
|
-
await this.ensureInitialized();
|
|
149
|
-
const newTicket = {
|
|
150
|
-
id: `ticket_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
151
|
-
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
152
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
153
|
-
...ticket
|
|
154
|
-
};
|
|
155
|
-
this.db.data.kanban_tickets.push(newTicket);
|
|
156
|
-
await this.db.write();
|
|
157
|
-
return newTicket;
|
|
158
|
-
}
|
|
159
|
-
async updateKanbanTicket(id, updates) {
|
|
160
|
-
await this.ensureInitialized();
|
|
161
|
-
const index = this.db.data.kanban_tickets.findIndex((t) => t.id === id);
|
|
162
|
-
if (index !== -1) {
|
|
163
|
-
this.db.data.kanban_tickets[index] = {
|
|
164
|
-
...this.db.data.kanban_tickets[index],
|
|
165
|
-
...updates,
|
|
166
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
167
|
-
};
|
|
168
|
-
await this.db.write();
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
async getKanbanTickets() {
|
|
172
|
-
await this.ensureInitialized();
|
|
173
|
-
return this.db.data.kanban_tickets.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
174
|
-
}
|
|
175
|
-
async getKanbanTicketsByColumn(column) {
|
|
176
|
-
await this.ensureInitialized();
|
|
177
|
-
return this.db.data.kanban_tickets.filter((t) => t.column === column).sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
178
|
-
}
|
|
179
|
-
async close() {
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
// agent/config/index.ts
|
|
184
|
-
import { config as dotenvConfig } from "dotenv";
|
|
185
|
-
dotenvConfig();
|
|
186
|
-
var ConfigManager = class {
|
|
187
|
-
db = null;
|
|
188
|
-
envConfig;
|
|
189
|
-
constructor(dbPath) {
|
|
190
|
-
this.envConfig = this.loadFromEnv();
|
|
191
|
-
}
|
|
192
|
-
loadFromEnv() {
|
|
193
|
-
return {
|
|
194
|
-
llm: {
|
|
195
|
-
provider: process.env.LLM_PROVIDER || "openai",
|
|
196
|
-
apiKey: process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY,
|
|
197
|
-
model: process.env.LLM_MODEL,
|
|
198
|
-
baseUrl: process.env.OLLAMA_BASE_URL
|
|
199
|
-
},
|
|
200
|
-
saas: {
|
|
201
|
-
url: process.env.SAAS_URL || "",
|
|
202
|
-
authType: process.env.SAAS_AUTH_TYPE || "none",
|
|
203
|
-
username: process.env.SAAS_USERNAME,
|
|
204
|
-
password: process.env.SAAS_PASSWORD
|
|
205
|
-
},
|
|
206
|
-
github: process.env.GITHUB_TOKEN ? {
|
|
207
|
-
token: process.env.GITHUB_TOKEN,
|
|
208
|
-
owner: process.env.GITHUB_OWNER || "",
|
|
209
|
-
repo: process.env.GITHUB_REPO || ""
|
|
210
|
-
} : void 0,
|
|
211
|
-
agent: {
|
|
212
|
-
intervalMs: parseInt(process.env.AGENT_INTERVAL_MS || "3600000"),
|
|
213
|
-
maxIterations: parseInt(process.env.AGENT_MAX_ITERATIONS || "20"),
|
|
214
|
-
autoStart: process.env.AGENT_AUTO_START === "true"
|
|
215
|
-
},
|
|
216
|
-
web: {
|
|
217
|
-
port: parseInt(process.env.WEB_PORT || "3000"),
|
|
218
|
-
host: process.env.WEB_HOST || "0.0.0.0"
|
|
219
|
-
},
|
|
220
|
-
database: {
|
|
221
|
-
path: process.env.DB_PATH || "./data/openqa.db"
|
|
222
|
-
},
|
|
223
|
-
notifications: {
|
|
224
|
-
slack: process.env.SLACK_WEBHOOK_URL,
|
|
225
|
-
discord: process.env.DISCORD_WEBHOOK_URL
|
|
27
|
+
var __filename, __dirname, OpenQADatabase;
|
|
28
|
+
var init_database = __esm({
|
|
29
|
+
"database/index.ts"() {
|
|
30
|
+
"use strict";
|
|
31
|
+
init_esm_shims();
|
|
32
|
+
__filename = fileURLToPath2(import.meta.url);
|
|
33
|
+
__dirname = dirname(__filename);
|
|
34
|
+
OpenQADatabase = class {
|
|
35
|
+
constructor(dbPath = "./data/openqa.json") {
|
|
36
|
+
this.dbPath = dbPath;
|
|
37
|
+
this.initialize();
|
|
226
38
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const dbConfig = await this.getDB().getAllConfig();
|
|
250
|
-
const merged = { ...this.envConfig };
|
|
251
|
-
for (const [key, value] of Object.entries(dbConfig)) {
|
|
252
|
-
const keys = key.split(".");
|
|
253
|
-
let obj = merged;
|
|
254
|
-
for (let i = 0; i < keys.length - 1; i++) {
|
|
255
|
-
if (!obj[keys[i]]) obj[keys[i]] = {};
|
|
256
|
-
obj = obj[keys[i]];
|
|
257
|
-
}
|
|
258
|
-
obj[keys[keys.length - 1]] = value;
|
|
259
|
-
}
|
|
260
|
-
return merged;
|
|
261
|
-
}
|
|
262
|
-
async getConfig() {
|
|
263
|
-
return await this.getAll();
|
|
264
|
-
}
|
|
265
|
-
// Synchronous version that only uses env vars (no DB)
|
|
266
|
-
getConfigSync() {
|
|
267
|
-
return this.envConfig;
|
|
268
|
-
}
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
// agent/tools/browser.ts
|
|
272
|
-
import { chromium } from "playwright";
|
|
273
|
-
import { mkdirSync as mkdirSync2 } from "fs";
|
|
274
|
-
import { join as join2 } from "path";
|
|
275
|
-
var BrowserTools = class {
|
|
276
|
-
browser = null;
|
|
277
|
-
page = null;
|
|
278
|
-
db;
|
|
279
|
-
sessionId;
|
|
280
|
-
screenshotDir = "./data/screenshots";
|
|
281
|
-
constructor(db, sessionId) {
|
|
282
|
-
this.db = db;
|
|
283
|
-
this.sessionId = sessionId;
|
|
284
|
-
mkdirSync2(this.screenshotDir, { recursive: true });
|
|
285
|
-
}
|
|
286
|
-
async initialize() {
|
|
287
|
-
this.browser = await chromium.launch({ headless: true });
|
|
288
|
-
const context = await this.browser.newContext({
|
|
289
|
-
viewport: { width: 1920, height: 1080 },
|
|
290
|
-
userAgent: "OpenQA/1.0 (Automated Testing Agent)"
|
|
291
|
-
});
|
|
292
|
-
this.page = await context.newPage();
|
|
293
|
-
}
|
|
294
|
-
getTools() {
|
|
295
|
-
return [
|
|
296
|
-
{
|
|
297
|
-
name: "navigate_to_page",
|
|
298
|
-
description: "Navigate to a specific URL in the application",
|
|
299
|
-
parameters: {
|
|
300
|
-
type: "object",
|
|
301
|
-
properties: {
|
|
302
|
-
url: { type: "string", description: "The URL to navigate to" }
|
|
303
|
-
},
|
|
304
|
-
required: ["url"]
|
|
305
|
-
},
|
|
306
|
-
execute: async ({ url }) => {
|
|
307
|
-
if (!this.page) await this.initialize();
|
|
308
|
-
try {
|
|
309
|
-
await this.page.goto(url, { waitUntil: "networkidle" });
|
|
310
|
-
const title = await this.page.title();
|
|
311
|
-
this.db.createAction({
|
|
312
|
-
session_id: this.sessionId,
|
|
313
|
-
type: "navigate",
|
|
314
|
-
description: `Navigated to ${url}`,
|
|
315
|
-
input: url,
|
|
316
|
-
output: `Page title: ${title}`
|
|
317
|
-
});
|
|
318
|
-
return `Successfully navigated to ${url}. Page title: "${title}"`;
|
|
319
|
-
} catch (error) {
|
|
320
|
-
return `Failed to navigate: ${error.message}`;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
},
|
|
324
|
-
{
|
|
325
|
-
name: "click_element",
|
|
326
|
-
description: "Click on an element using a CSS selector",
|
|
327
|
-
parameters: {
|
|
328
|
-
type: "object",
|
|
329
|
-
properties: {
|
|
330
|
-
selector: { type: "string", description: "CSS selector of the element to click" }
|
|
331
|
-
},
|
|
332
|
-
required: ["selector"]
|
|
333
|
-
},
|
|
334
|
-
execute: async ({ selector }) => {
|
|
335
|
-
if (!this.page) return "Browser not initialized. Navigate to a page first.";
|
|
336
|
-
try {
|
|
337
|
-
await this.page.click(selector, { timeout: 5e3 });
|
|
338
|
-
this.db.createAction({
|
|
339
|
-
session_id: this.sessionId,
|
|
340
|
-
type: "click",
|
|
341
|
-
description: `Clicked element: ${selector}`,
|
|
342
|
-
input: selector
|
|
343
|
-
});
|
|
344
|
-
return `Successfully clicked element: ${selector}`;
|
|
345
|
-
} catch (error) {
|
|
346
|
-
return `Failed to click element: ${error.message}`;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
},
|
|
350
|
-
{
|
|
351
|
-
name: "fill_input",
|
|
352
|
-
description: "Fill an input field with text",
|
|
353
|
-
parameters: {
|
|
354
|
-
type: "object",
|
|
355
|
-
properties: {
|
|
356
|
-
selector: { type: "string", description: "CSS selector of the input field" },
|
|
357
|
-
text: { type: "string", description: "Text to fill in the input" }
|
|
358
|
-
},
|
|
359
|
-
required: ["selector", "text"]
|
|
360
|
-
},
|
|
361
|
-
execute: async ({ selector, text }) => {
|
|
362
|
-
if (!this.page) return "Browser not initialized. Navigate to a page first.";
|
|
363
|
-
try {
|
|
364
|
-
await this.page.fill(selector, text);
|
|
365
|
-
this.db.createAction({
|
|
366
|
-
session_id: this.sessionId,
|
|
367
|
-
type: "fill",
|
|
368
|
-
description: `Filled input ${selector}`,
|
|
369
|
-
input: `${selector} = ${text}`
|
|
370
|
-
});
|
|
371
|
-
return `Successfully filled input ${selector} with text`;
|
|
372
|
-
} catch (error) {
|
|
373
|
-
return `Failed to fill input: ${error.message}`;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
},
|
|
377
|
-
{
|
|
378
|
-
name: "take_screenshot",
|
|
379
|
-
description: "Take a screenshot of the current page for evidence",
|
|
380
|
-
parameters: {
|
|
381
|
-
type: "object",
|
|
382
|
-
properties: {
|
|
383
|
-
name: { type: "string", description: "Name for the screenshot file" }
|
|
384
|
-
},
|
|
385
|
-
required: ["name"]
|
|
386
|
-
},
|
|
387
|
-
execute: async ({ name }) => {
|
|
388
|
-
if (!this.page) return "Browser not initialized. Navigate to a page first.";
|
|
389
|
-
try {
|
|
390
|
-
const filename = `${Date.now()}_${name}.png`;
|
|
391
|
-
const path = join2(this.screenshotDir, filename);
|
|
392
|
-
await this.page.screenshot({ path, fullPage: true });
|
|
393
|
-
this.db.createAction({
|
|
394
|
-
session_id: this.sessionId,
|
|
395
|
-
type: "screenshot",
|
|
396
|
-
description: `Screenshot: ${name}`,
|
|
397
|
-
screenshot_path: path
|
|
398
|
-
});
|
|
399
|
-
return `Screenshot saved: ${path}`;
|
|
400
|
-
} catch (error) {
|
|
401
|
-
return `Failed to take screenshot: ${error.message}`;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
},
|
|
405
|
-
{
|
|
406
|
-
name: "get_page_content",
|
|
407
|
-
description: "Get the text content of the current page",
|
|
408
|
-
parameters: {
|
|
409
|
-
type: "object",
|
|
410
|
-
properties: {}
|
|
411
|
-
},
|
|
412
|
-
execute: async () => {
|
|
413
|
-
if (!this.page) return "Browser not initialized. Navigate to a page first.";
|
|
414
|
-
try {
|
|
415
|
-
const content = await this.page.textContent("body");
|
|
416
|
-
return content?.slice(0, 1e3) || "No content found";
|
|
417
|
-
} catch (error) {
|
|
418
|
-
return `Failed to get content: ${error.message}`;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
},
|
|
422
|
-
{
|
|
423
|
-
name: "check_console_errors",
|
|
424
|
-
description: "Check for JavaScript console errors on the page",
|
|
425
|
-
parameters: {
|
|
426
|
-
type: "object",
|
|
427
|
-
properties: {}
|
|
428
|
-
},
|
|
429
|
-
execute: async () => {
|
|
430
|
-
if (!this.page) return "Browser not initialized. Navigate to a page first.";
|
|
431
|
-
const errors = [];
|
|
432
|
-
this.page.on("console", (msg) => {
|
|
433
|
-
if (msg.type() === "error") {
|
|
434
|
-
errors.push(msg.text());
|
|
435
|
-
}
|
|
436
|
-
});
|
|
437
|
-
await this.page.waitForTimeout(2e3);
|
|
438
|
-
if (errors.length > 0) {
|
|
439
|
-
return `Found ${errors.length} console errors:
|
|
440
|
-
${errors.join("\n")}`;
|
|
441
|
-
}
|
|
442
|
-
return "No console errors detected";
|
|
39
|
+
db = null;
|
|
40
|
+
initialize() {
|
|
41
|
+
const dir = dirname(this.dbPath);
|
|
42
|
+
mkdirSync(dir, { recursive: true });
|
|
43
|
+
const adapter = new JSONFile(this.dbPath);
|
|
44
|
+
this.db = new Low(adapter, {
|
|
45
|
+
config: {},
|
|
46
|
+
test_sessions: [],
|
|
47
|
+
actions: [],
|
|
48
|
+
bugs: [],
|
|
49
|
+
kanban_tickets: []
|
|
50
|
+
});
|
|
51
|
+
this.db.read();
|
|
52
|
+
if (!this.db.data) {
|
|
53
|
+
this.db.data = {
|
|
54
|
+
config: {},
|
|
55
|
+
test_sessions: [],
|
|
56
|
+
actions: [],
|
|
57
|
+
bugs: [],
|
|
58
|
+
kanban_tickets: []
|
|
59
|
+
};
|
|
60
|
+
this.db.write();
|
|
443
61
|
}
|
|
444
62
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
if (this.browser) {
|
|
449
|
-
await this.browser.close();
|
|
450
|
-
this.browser = null;
|
|
451
|
-
this.page = null;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
};
|
|
455
|
-
|
|
456
|
-
// agent/tools/github.ts
|
|
457
|
-
import { Octokit } from "@octokit/rest";
|
|
458
|
-
var GitHubTools = class {
|
|
459
|
-
octokit = null;
|
|
460
|
-
db;
|
|
461
|
-
sessionId;
|
|
462
|
-
config;
|
|
463
|
-
constructor(db, sessionId, config) {
|
|
464
|
-
this.db = db;
|
|
465
|
-
this.sessionId = sessionId;
|
|
466
|
-
this.config = config;
|
|
467
|
-
if (config.token) {
|
|
468
|
-
this.octokit = new Octokit({ auth: config.token });
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
getTools() {
|
|
472
|
-
return [
|
|
473
|
-
{
|
|
474
|
-
name: "create_github_issue",
|
|
475
|
-
description: "Create a GitHub issue when a critical bug is found. Use this for bugs that require developer attention.",
|
|
476
|
-
parameters: {
|
|
477
|
-
type: "object",
|
|
478
|
-
properties: {
|
|
479
|
-
title: { type: "string", description: "Issue title (concise and descriptive)" },
|
|
480
|
-
body: { type: "string", description: "Detailed description with steps to reproduce" },
|
|
481
|
-
severity: { type: "string", enum: ["low", "medium", "high", "critical"], description: "Bug severity" },
|
|
482
|
-
labels: { type: "array", items: { type: "string" }, description: "Labels for the issue" },
|
|
483
|
-
screenshot_path: { type: "string", description: "Path to screenshot evidence" }
|
|
484
|
-
},
|
|
485
|
-
required: ["title", "body", "severity"]
|
|
486
|
-
},
|
|
487
|
-
execute: async ({ title, body, severity, labels = [], screenshot_path }) => {
|
|
488
|
-
if (!this.octokit || !this.config.owner || !this.config.repo) {
|
|
489
|
-
return "GitHub not configured. Please set GITHUB_TOKEN, GITHUB_OWNER, and GITHUB_REPO.";
|
|
490
|
-
}
|
|
491
|
-
try {
|
|
492
|
-
const severityLabel = `severity: ${severity}`;
|
|
493
|
-
const allLabels = ["automated-qa", severityLabel, ...labels];
|
|
494
|
-
const issueBody = `## \u{1F916} Automated QA Report
|
|
495
|
-
|
|
496
|
-
${body}
|
|
497
|
-
|
|
498
|
-
---
|
|
499
|
-
|
|
500
|
-
**Severity:** ${severity.toUpperCase()}
|
|
501
|
-
**Detected by:** OpenQA Agent
|
|
502
|
-
**Session ID:** ${this.sessionId}
|
|
503
|
-
${screenshot_path ? `**Screenshot:** ${screenshot_path}` : ""}
|
|
504
|
-
|
|
505
|
-
*This issue was automatically created by OpenQA during automated testing.*`;
|
|
506
|
-
const issue = await this.octokit.rest.issues.create({
|
|
507
|
-
owner: this.config.owner,
|
|
508
|
-
repo: this.config.repo,
|
|
509
|
-
title: `[QA] ${title}`,
|
|
510
|
-
body: issueBody,
|
|
511
|
-
labels: allLabels
|
|
512
|
-
});
|
|
513
|
-
this.db.createAction({
|
|
514
|
-
session_id: this.sessionId,
|
|
515
|
-
type: "github_issue",
|
|
516
|
-
description: `Created GitHub issue: ${title}`,
|
|
517
|
-
input: JSON.stringify({ title, severity }),
|
|
518
|
-
output: issue.data.html_url
|
|
519
|
-
});
|
|
520
|
-
const bug = this.db.createBug({
|
|
521
|
-
session_id: this.sessionId,
|
|
522
|
-
title,
|
|
523
|
-
description: body,
|
|
524
|
-
severity,
|
|
525
|
-
status: "open",
|
|
526
|
-
github_issue_url: issue.data.html_url,
|
|
527
|
-
screenshot_path
|
|
528
|
-
});
|
|
529
|
-
return `\u2705 GitHub issue created successfully!
|
|
530
|
-
URL: ${issue.data.html_url}
|
|
531
|
-
Issue #${issue.data.number}`;
|
|
532
|
-
} catch (error) {
|
|
533
|
-
return `\u274C Failed to create GitHub issue: ${error.message}`;
|
|
534
|
-
}
|
|
63
|
+
async ensureInitialized() {
|
|
64
|
+
if (!this.db) {
|
|
65
|
+
this.initialize();
|
|
535
66
|
}
|
|
67
|
+
await this.db.read();
|
|
536
68
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
tags: JSON.stringify(allTags),
|
|
575
|
-
screenshot_url: screenshot_path
|
|
576
|
-
});
|
|
577
|
-
this.db.createAction({
|
|
578
|
-
session_id: this.sessionId,
|
|
579
|
-
type: "kanban_ticket",
|
|
580
|
-
description: `Created Kanban ticket: ${title}`,
|
|
581
|
-
input: JSON.stringify({ title, priority, column }),
|
|
582
|
-
output: ticket.id
|
|
583
|
-
});
|
|
584
|
-
return `\u2705 Kanban ticket created successfully!
|
|
585
|
-
ID: ${ticket.id}
|
|
586
|
-
Column: ${column}
|
|
587
|
-
Priority: ${priority}`;
|
|
588
|
-
} catch (error) {
|
|
589
|
-
return `\u274C Failed to create Kanban ticket: ${error.message}`;
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
},
|
|
593
|
-
{
|
|
594
|
-
name: "update_kanban_ticket",
|
|
595
|
-
description: "Update an existing Kanban ticket (move columns, change priority, etc.)",
|
|
596
|
-
parameters: {
|
|
597
|
-
type: "object",
|
|
598
|
-
properties: {
|
|
599
|
-
ticket_id: { type: "string", description: "ID of the ticket to update" },
|
|
600
|
-
column: { type: "string", enum: ["backlog", "to-do", "in-progress", "done"], description: "New column" },
|
|
601
|
-
priority: { type: "string", enum: ["low", "medium", "high", "critical"], description: "New priority" }
|
|
602
|
-
},
|
|
603
|
-
required: ["ticket_id"]
|
|
604
|
-
},
|
|
605
|
-
execute: async ({ ticket_id, column, priority }) => {
|
|
606
|
-
try {
|
|
607
|
-
const updates = {};
|
|
608
|
-
if (column) updates.column = column;
|
|
609
|
-
if (priority) updates.priority = priority;
|
|
610
|
-
this.db.updateKanbanTicket(ticket_id, updates);
|
|
611
|
-
return `\u2705 Kanban ticket ${ticket_id} updated successfully!`;
|
|
612
|
-
} catch (error) {
|
|
613
|
-
return `\u274C Failed to update Kanban ticket: ${error.message}`;
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
},
|
|
617
|
-
{
|
|
618
|
-
name: "get_kanban_board",
|
|
619
|
-
description: "Get all tickets from the Kanban board to see current status",
|
|
620
|
-
parameters: {
|
|
621
|
-
type: "object",
|
|
622
|
-
properties: {}
|
|
623
|
-
},
|
|
624
|
-
execute: async () => {
|
|
625
|
-
try {
|
|
626
|
-
const tickets = this.db.getKanbanTickets();
|
|
627
|
-
const byColumn = {
|
|
628
|
-
backlog: tickets.filter((t) => t.column === "backlog"),
|
|
629
|
-
"to-do": tickets.filter((t) => t.column === "to-do"),
|
|
630
|
-
"in-progress": tickets.filter((t) => t.column === "in-progress"),
|
|
631
|
-
done: tickets.filter((t) => t.column === "done")
|
|
632
|
-
};
|
|
633
|
-
const summary = `
|
|
634
|
-
\u{1F4CA} Kanban Board Status:
|
|
635
|
-
- Backlog: ${byColumn.backlog.length} tickets
|
|
636
|
-
- To Do: ${byColumn["to-do"].length} tickets
|
|
637
|
-
- In Progress: ${byColumn["in-progress"].length} tickets
|
|
638
|
-
- Done: ${byColumn.done.length} tickets
|
|
639
|
-
|
|
640
|
-
Total: ${tickets.length} tickets
|
|
641
|
-
`.trim();
|
|
642
|
-
return summary;
|
|
643
|
-
} catch (error) {
|
|
644
|
-
return `\u274C Failed to get Kanban board: ${error.message}`;
|
|
645
|
-
}
|
|
69
|
+
async getConfig(key) {
|
|
70
|
+
await this.ensureInitialized();
|
|
71
|
+
return this.db.data.config[key] || null;
|
|
72
|
+
}
|
|
73
|
+
async setConfig(key, value) {
|
|
74
|
+
await this.ensureInitialized();
|
|
75
|
+
this.db.data.config[key] = value;
|
|
76
|
+
await this.db.write();
|
|
77
|
+
}
|
|
78
|
+
async getAllConfig() {
|
|
79
|
+
await this.ensureInitialized();
|
|
80
|
+
return this.db.data.config;
|
|
81
|
+
}
|
|
82
|
+
async createSession(id, metadata) {
|
|
83
|
+
await this.ensureInitialized();
|
|
84
|
+
const session = {
|
|
85
|
+
id,
|
|
86
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
87
|
+
status: "running",
|
|
88
|
+
total_actions: 0,
|
|
89
|
+
bugs_found: 0,
|
|
90
|
+
metadata: metadata ? JSON.stringify(metadata) : void 0
|
|
91
|
+
};
|
|
92
|
+
this.db.data.test_sessions.push(session);
|
|
93
|
+
await this.db.write();
|
|
94
|
+
return session;
|
|
95
|
+
}
|
|
96
|
+
async getSession(id) {
|
|
97
|
+
await this.ensureInitialized();
|
|
98
|
+
return this.db.data.test_sessions.find((s) => s.id === id) || null;
|
|
99
|
+
}
|
|
100
|
+
async updateSession(id, updates) {
|
|
101
|
+
await this.ensureInitialized();
|
|
102
|
+
const index = this.db.data.test_sessions.findIndex((s) => s.id === id);
|
|
103
|
+
if (index !== -1) {
|
|
104
|
+
this.db.data.test_sessions[index] = { ...this.db.data.test_sessions[index], ...updates };
|
|
105
|
+
await this.db.write();
|
|
646
106
|
}
|
|
647
107
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
// agent/webhooks/git-listener.ts
|
|
653
|
-
import { EventEmitter } from "events";
|
|
654
|
-
import { Octokit as Octokit2 } from "@octokit/rest";
|
|
655
|
-
var GitListener = class extends EventEmitter {
|
|
656
|
-
config;
|
|
657
|
-
octokit = null;
|
|
658
|
-
lastCommitSha = null;
|
|
659
|
-
lastPipelineId = null;
|
|
660
|
-
pollInterval = null;
|
|
661
|
-
isRunning = false;
|
|
662
|
-
constructor(config) {
|
|
663
|
-
super();
|
|
664
|
-
this.config = {
|
|
665
|
-
branch: "main",
|
|
666
|
-
pollIntervalMs: 6e4,
|
|
667
|
-
gitlabUrl: "https://gitlab.com",
|
|
668
|
-
...config
|
|
669
|
-
};
|
|
670
|
-
if (config.provider === "github" && config.token) {
|
|
671
|
-
this.octokit = new Octokit2({ auth: config.token });
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
async start() {
|
|
675
|
-
if (this.isRunning) return;
|
|
676
|
-
this.isRunning = true;
|
|
677
|
-
console.log(`\u{1F517} GitListener started for ${this.config.provider}/${this.config.owner}/${this.config.repo}`);
|
|
678
|
-
await this.checkInitialState();
|
|
679
|
-
this.pollInterval = setInterval(() => {
|
|
680
|
-
this.poll().catch(console.error);
|
|
681
|
-
}, this.config.pollIntervalMs);
|
|
682
|
-
}
|
|
683
|
-
stop() {
|
|
684
|
-
this.isRunning = false;
|
|
685
|
-
if (this.pollInterval) {
|
|
686
|
-
clearInterval(this.pollInterval);
|
|
687
|
-
this.pollInterval = null;
|
|
688
|
-
}
|
|
689
|
-
console.log("\u{1F517} GitListener stopped");
|
|
690
|
-
}
|
|
691
|
-
async checkInitialState() {
|
|
692
|
-
try {
|
|
693
|
-
if (this.config.provider === "github") {
|
|
694
|
-
await this.checkGitHubState();
|
|
695
|
-
} else {
|
|
696
|
-
await this.checkGitLabState();
|
|
108
|
+
async getRecentSessions(limit = 10) {
|
|
109
|
+
await this.ensureInitialized();
|
|
110
|
+
return this.db.data.test_sessions.sort((a, b) => new Date(b.started_at).getTime() - new Date(a.started_at).getTime()).slice(0, limit);
|
|
697
111
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
112
|
+
async createAction(action) {
|
|
113
|
+
await this.ensureInitialized();
|
|
114
|
+
const newAction = {
|
|
115
|
+
id: `action_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
116
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
117
|
+
...action
|
|
118
|
+
};
|
|
119
|
+
this.db.data.actions.push(newAction);
|
|
120
|
+
await this.db.write();
|
|
121
|
+
return newAction;
|
|
708
122
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
}
|
|
713
|
-
async checkGitHubState() {
|
|
714
|
-
if (!this.octokit) return;
|
|
715
|
-
const { data: commits } = await this.octokit.repos.listCommits({
|
|
716
|
-
owner: this.config.owner,
|
|
717
|
-
repo: this.config.repo,
|
|
718
|
-
sha: this.config.branch,
|
|
719
|
-
per_page: 1
|
|
720
|
-
});
|
|
721
|
-
if (commits.length > 0) {
|
|
722
|
-
this.lastCommitSha = commits[0].sha;
|
|
723
|
-
}
|
|
724
|
-
try {
|
|
725
|
-
const { data: runs } = await this.octokit.actions.listWorkflowRunsForRepo({
|
|
726
|
-
owner: this.config.owner,
|
|
727
|
-
repo: this.config.repo,
|
|
728
|
-
branch: this.config.branch,
|
|
729
|
-
per_page: 1
|
|
730
|
-
});
|
|
731
|
-
if (runs.workflow_runs.length > 0) {
|
|
732
|
-
this.lastPipelineId = runs.workflow_runs[0].id.toString();
|
|
123
|
+
async getSessionActions(sessionId) {
|
|
124
|
+
await this.ensureInitialized();
|
|
125
|
+
return this.db.data.actions.filter((a) => a.session_id === sessionId).sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
733
126
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
for (const commit of commits) {
|
|
746
|
-
if (this.lastCommitSha && commit.sha === this.lastCommitSha) break;
|
|
747
|
-
const isMerge = commit.parents && commit.parents.length > 1;
|
|
748
|
-
const event = {
|
|
749
|
-
type: isMerge ? "merge" : "push",
|
|
750
|
-
provider: "github",
|
|
751
|
-
branch: this.config.branch,
|
|
752
|
-
commit: commit.sha,
|
|
753
|
-
author: commit.commit.author?.name || "unknown",
|
|
754
|
-
message: commit.commit.message,
|
|
755
|
-
timestamp: new Date(commit.commit.author?.date || Date.now())
|
|
756
|
-
};
|
|
757
|
-
this.emit("git-event", event);
|
|
758
|
-
if (isMerge) {
|
|
759
|
-
this.emit("merge", event);
|
|
760
|
-
console.log(`\u{1F500} Merge detected on ${this.config.branch}: ${commit.sha.slice(0, 7)}`);
|
|
127
|
+
async createBug(bug) {
|
|
128
|
+
await this.ensureInitialized();
|
|
129
|
+
const newBug = {
|
|
130
|
+
id: `bug_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
131
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
132
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
133
|
+
...bug
|
|
134
|
+
};
|
|
135
|
+
this.db.data.bugs.push(newBug);
|
|
136
|
+
await this.db.write();
|
|
137
|
+
return newBug;
|
|
761
138
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
branch: this.config.branch,
|
|
771
|
-
per_page: 5
|
|
772
|
-
});
|
|
773
|
-
for (const run of runs.workflow_runs) {
|
|
774
|
-
if (this.lastPipelineId && run.id.toString() === this.lastPipelineId) break;
|
|
775
|
-
if (run.status === "completed") {
|
|
776
|
-
const event = {
|
|
777
|
-
type: run.conclusion === "success" ? "pipeline_success" : "pipeline_failure",
|
|
778
|
-
provider: "github",
|
|
779
|
-
branch: this.config.branch,
|
|
780
|
-
commit: run.head_sha,
|
|
781
|
-
author: run.actor?.login || "unknown",
|
|
782
|
-
message: run.name || "",
|
|
783
|
-
timestamp: new Date(run.updated_at || Date.now()),
|
|
784
|
-
pipelineId: run.id.toString(),
|
|
785
|
-
pipelineStatus: run.conclusion || void 0
|
|
139
|
+
async updateBug(id, updates) {
|
|
140
|
+
await this.ensureInitialized();
|
|
141
|
+
const index = this.db.data.bugs.findIndex((b) => b.id === id);
|
|
142
|
+
if (index !== -1) {
|
|
143
|
+
this.db.data.bugs[index] = {
|
|
144
|
+
...this.db.data.bugs[index],
|
|
145
|
+
...updates,
|
|
146
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
786
147
|
};
|
|
787
|
-
this.
|
|
788
|
-
if (run.conclusion === "success") {
|
|
789
|
-
this.emit("pipeline-success", event);
|
|
790
|
-
console.log(`\u2705 Pipeline success: ${run.name} (${run.id})`);
|
|
791
|
-
} else {
|
|
792
|
-
this.emit("pipeline-failure", event);
|
|
793
|
-
console.log(`\u274C Pipeline failure: ${run.name} (${run.id})`);
|
|
794
|
-
}
|
|
148
|
+
await this.db.write();
|
|
795
149
|
}
|
|
796
150
|
}
|
|
797
|
-
|
|
798
|
-
this.
|
|
151
|
+
async getAllBugs() {
|
|
152
|
+
await this.ensureInitialized();
|
|
153
|
+
return this.db.data.bugs.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
799
154
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
async checkGitLabState() {
|
|
804
|
-
const headers = { "PRIVATE-TOKEN": this.config.token };
|
|
805
|
-
const projectPath = encodeURIComponent(`${this.config.owner}/${this.config.repo}`);
|
|
806
|
-
const baseUrl = this.config.gitlabUrl;
|
|
807
|
-
try {
|
|
808
|
-
const commitsRes = await fetch(
|
|
809
|
-
`${baseUrl}/api/v4/projects/${projectPath}/repository/commits?ref_name=${this.config.branch}&per_page=1`,
|
|
810
|
-
{ headers }
|
|
811
|
-
);
|
|
812
|
-
const commits = await commitsRes.json();
|
|
813
|
-
if (commits.length > 0) {
|
|
814
|
-
this.lastCommitSha = commits[0].id;
|
|
155
|
+
async getBugsByStatus(status) {
|
|
156
|
+
await this.ensureInitialized();
|
|
157
|
+
return this.db.data.bugs.filter((b) => b.status === status).sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
815
158
|
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
159
|
+
async createKanbanTicket(ticket) {
|
|
160
|
+
await this.ensureInitialized();
|
|
161
|
+
const newTicket = {
|
|
162
|
+
id: `ticket_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
163
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
164
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
165
|
+
...ticket
|
|
166
|
+
};
|
|
167
|
+
this.db.data.kanban_tickets.push(newTicket);
|
|
168
|
+
await this.db.write();
|
|
169
|
+
return newTicket;
|
|
823
170
|
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
171
|
+
async updateKanbanTicket(id, updates) {
|
|
172
|
+
await this.ensureInitialized();
|
|
173
|
+
const index = this.db.data.kanban_tickets.findIndex((t) => t.id === id);
|
|
174
|
+
if (index !== -1) {
|
|
175
|
+
this.db.data.kanban_tickets[index] = {
|
|
176
|
+
...this.db.data.kanban_tickets[index],
|
|
177
|
+
...updates,
|
|
178
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
179
|
+
};
|
|
180
|
+
await this.db.write();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async getKanbanTickets() {
|
|
184
|
+
await this.ensureInitialized();
|
|
185
|
+
return this.db.data.kanban_tickets.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
186
|
+
}
|
|
187
|
+
async getKanbanTicketsByColumn(column) {
|
|
188
|
+
await this.ensureInitialized();
|
|
189
|
+
return this.db.data.kanban_tickets.filter((t) => t.column === column).sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
190
|
+
}
|
|
191
|
+
async close() {
|
|
192
|
+
}
|
|
193
|
+
};
|
|
827
194
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// agent/config/index.ts
|
|
198
|
+
import { config as dotenvConfig } from "dotenv";
|
|
199
|
+
var ConfigManager;
|
|
200
|
+
var init_config = __esm({
|
|
201
|
+
"agent/config/index.ts"() {
|
|
202
|
+
"use strict";
|
|
203
|
+
init_esm_shims();
|
|
204
|
+
init_database();
|
|
205
|
+
dotenvConfig();
|
|
206
|
+
ConfigManager = class {
|
|
207
|
+
db = null;
|
|
208
|
+
envConfig;
|
|
209
|
+
constructor(dbPath) {
|
|
210
|
+
this.envConfig = this.loadFromEnv();
|
|
211
|
+
}
|
|
212
|
+
loadFromEnv() {
|
|
213
|
+
return {
|
|
214
|
+
llm: {
|
|
215
|
+
provider: process.env.LLM_PROVIDER || "openai",
|
|
216
|
+
apiKey: process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY,
|
|
217
|
+
model: process.env.LLM_MODEL,
|
|
218
|
+
baseUrl: process.env.OLLAMA_BASE_URL
|
|
219
|
+
},
|
|
220
|
+
saas: {
|
|
221
|
+
url: process.env.SAAS_URL || "",
|
|
222
|
+
authType: process.env.SAAS_AUTH_TYPE || "none",
|
|
223
|
+
username: process.env.SAAS_USERNAME,
|
|
224
|
+
password: process.env.SAAS_PASSWORD
|
|
225
|
+
},
|
|
226
|
+
github: process.env.GITHUB_TOKEN ? {
|
|
227
|
+
token: process.env.GITHUB_TOKEN,
|
|
228
|
+
owner: process.env.GITHUB_OWNER || "",
|
|
229
|
+
repo: process.env.GITHUB_REPO || ""
|
|
230
|
+
} : void 0,
|
|
231
|
+
agent: {
|
|
232
|
+
intervalMs: parseInt(process.env.AGENT_INTERVAL_MS || "3600000"),
|
|
233
|
+
maxIterations: parseInt(process.env.AGENT_MAX_ITERATIONS || "20"),
|
|
234
|
+
autoStart: process.env.AGENT_AUTO_START === "true"
|
|
235
|
+
},
|
|
236
|
+
web: {
|
|
237
|
+
port: parseInt(process.env.WEB_PORT || "4242"),
|
|
238
|
+
host: process.env.WEB_HOST || "0.0.0.0"
|
|
239
|
+
},
|
|
240
|
+
database: {
|
|
241
|
+
path: process.env.DB_PATH || "./data/openqa.db"
|
|
242
|
+
},
|
|
243
|
+
notifications: {
|
|
244
|
+
slack: process.env.SLACK_WEBHOOK_URL,
|
|
245
|
+
discord: process.env.DISCORD_WEBHOOK_URL
|
|
246
|
+
}
|
|
849
247
|
};
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
248
|
+
}
|
|
249
|
+
getDB() {
|
|
250
|
+
if (!this.db) {
|
|
251
|
+
this.db = new OpenQADatabase("./data/openqa.json");
|
|
854
252
|
}
|
|
253
|
+
return this.db;
|
|
855
254
|
}
|
|
856
|
-
|
|
857
|
-
|
|
255
|
+
async get(key) {
|
|
256
|
+
const dbValue = await this.getDB().getConfig(key);
|
|
257
|
+
if (dbValue) return dbValue;
|
|
258
|
+
const keys = key.split(".");
|
|
259
|
+
let value = this.envConfig;
|
|
260
|
+
for (const k of keys) {
|
|
261
|
+
value = value?.[k];
|
|
262
|
+
}
|
|
263
|
+
return value?.toString() || null;
|
|
858
264
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
)
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
commit: pipeline.sha,
|
|
872
|
-
author: pipeline.user?.name || "unknown",
|
|
873
|
-
message: `Pipeline #${pipeline.id}`,
|
|
874
|
-
timestamp: new Date(pipeline.updated_at),
|
|
875
|
-
pipelineId: pipeline.id.toString(),
|
|
876
|
-
pipelineStatus: pipeline.status
|
|
877
|
-
};
|
|
878
|
-
this.emit("git-event", event);
|
|
879
|
-
if (pipeline.status === "success") {
|
|
880
|
-
this.emit("pipeline-success", event);
|
|
881
|
-
console.log(`\u2705 Pipeline success: #${pipeline.id}`);
|
|
882
|
-
} else {
|
|
883
|
-
this.emit("pipeline-failure", event);
|
|
884
|
-
console.log(`\u274C Pipeline failure: #${pipeline.id}`);
|
|
265
|
+
async set(key, value) {
|
|
266
|
+
await this.getDB().setConfig(key, value);
|
|
267
|
+
}
|
|
268
|
+
async getAll() {
|
|
269
|
+
const dbConfig = await this.getDB().getAllConfig();
|
|
270
|
+
const merged = { ...this.envConfig };
|
|
271
|
+
for (const [key, value] of Object.entries(dbConfig)) {
|
|
272
|
+
const keys = key.split(".");
|
|
273
|
+
let obj = merged;
|
|
274
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
275
|
+
if (!obj[keys[i]]) obj[keys[i]] = {};
|
|
276
|
+
obj = obj[keys[i]];
|
|
885
277
|
}
|
|
278
|
+
obj[keys[keys.length - 1]] = value;
|
|
886
279
|
}
|
|
280
|
+
return merged;
|
|
887
281
|
}
|
|
888
|
-
|
|
889
|
-
|
|
282
|
+
async getConfig() {
|
|
283
|
+
return await this.getAll();
|
|
890
284
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
}
|
|
895
|
-
async setupWebhook(webhookUrl) {
|
|
896
|
-
if (this.config.provider === "github") {
|
|
897
|
-
return this.setupGitHubWebhook(webhookUrl);
|
|
898
|
-
} else {
|
|
899
|
-
return this.setupGitLabWebhook(webhookUrl);
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
async setupGitHubWebhook(webhookUrl) {
|
|
903
|
-
if (!this.octokit) throw new Error("GitHub not configured");
|
|
904
|
-
const { data } = await this.octokit.repos.createWebhook({
|
|
905
|
-
owner: this.config.owner,
|
|
906
|
-
repo: this.config.repo,
|
|
907
|
-
config: {
|
|
908
|
-
url: webhookUrl,
|
|
909
|
-
content_type: "json"
|
|
910
|
-
},
|
|
911
|
-
events: ["push", "pull_request", "workflow_run"]
|
|
912
|
-
});
|
|
913
|
-
return data.id.toString();
|
|
914
|
-
}
|
|
915
|
-
async setupGitLabWebhook(webhookUrl) {
|
|
916
|
-
const headers = {
|
|
917
|
-
"PRIVATE-TOKEN": this.config.token,
|
|
918
|
-
"Content-Type": "application/json"
|
|
919
|
-
};
|
|
920
|
-
const projectPath = encodeURIComponent(`${this.config.owner}/${this.config.repo}`);
|
|
921
|
-
const res = await fetch(
|
|
922
|
-
`${this.config.gitlabUrl}/api/v4/projects/${projectPath}/hooks`,
|
|
923
|
-
{
|
|
924
|
-
method: "POST",
|
|
925
|
-
headers,
|
|
926
|
-
body: JSON.stringify({
|
|
927
|
-
url: webhookUrl,
|
|
928
|
-
push_events: true,
|
|
929
|
-
merge_requests_events: true,
|
|
930
|
-
pipeline_events: true
|
|
931
|
-
})
|
|
285
|
+
// Synchronous version that only uses env vars (no DB)
|
|
286
|
+
getConfigSync() {
|
|
287
|
+
return this.envConfig;
|
|
932
288
|
}
|
|
933
|
-
);
|
|
934
|
-
const data = await res.json();
|
|
935
|
-
return data.id.toString();
|
|
936
|
-
}
|
|
937
|
-
};
|
|
938
|
-
|
|
939
|
-
// agent/specialists/index.ts
|
|
940
|
-
import { ReActAgent } from "@orka-js/agent";
|
|
941
|
-
import { OpenAIAdapter } from "@orka-js/openai";
|
|
942
|
-
import { AnthropicAdapter } from "@orka-js/anthropic";
|
|
943
|
-
import { EventEmitter as EventEmitter2 } from "events";
|
|
944
|
-
var SPECIALIST_PROMPTS = {
|
|
945
|
-
"form-tester": `You are a Form Testing Specialist. Your mission:
|
|
946
|
-
- Find all forms on the page (login, signup, contact, search, etc.)
|
|
947
|
-
- Test form validation (empty fields, invalid formats, boundary values)
|
|
948
|
-
- Test error messages and user feedback
|
|
949
|
-
- Test form submission success/failure scenarios
|
|
950
|
-
- Check for proper field types (email, password, phone)
|
|
951
|
-
- Test autofill behavior
|
|
952
|
-
- Report any form-related bugs with clear reproduction steps`,
|
|
953
|
-
"security-scanner": `You are a Security Scanner Specialist. Your mission:
|
|
954
|
-
- Identify potential security vulnerabilities
|
|
955
|
-
- Check for exposed sensitive data in page source
|
|
956
|
-
- Look for insecure HTTP resources on HTTPS pages
|
|
957
|
-
- Check for missing security headers
|
|
958
|
-
- Identify potential CSRF vulnerabilities
|
|
959
|
-
- Check for information disclosure in error messages
|
|
960
|
-
- Look for hardcoded credentials or API keys
|
|
961
|
-
- Report security issues with severity ratings`,
|
|
962
|
-
"sql-injection": `You are a SQL Injection Testing Specialist. Your mission:
|
|
963
|
-
- Identify input fields that might interact with databases
|
|
964
|
-
- Test common SQL injection payloads (', ", --, ;, OR 1=1, etc.)
|
|
965
|
-
- Test for blind SQL injection (time-based, boolean-based)
|
|
966
|
-
- Check URL parameters for injection vulnerabilities
|
|
967
|
-
- Test search fields, login forms, and filters
|
|
968
|
-
- Document any successful injections with exact payloads
|
|
969
|
-
- Rate severity based on data exposure risk`,
|
|
970
|
-
"xss-tester": `You are an XSS (Cross-Site Scripting) Testing Specialist. Your mission:
|
|
971
|
-
- Find all user input fields that reflect content
|
|
972
|
-
- Test for reflected XSS (<script>, onerror, onload, etc.)
|
|
973
|
-
- Test for stored XSS in comments, profiles, messages
|
|
974
|
-
- Check for DOM-based XSS vulnerabilities
|
|
975
|
-
- Test various encoding bypasses
|
|
976
|
-
- Check if Content-Security-Policy is properly configured
|
|
977
|
-
- Document successful XSS with exact payloads`,
|
|
978
|
-
"component-tester": `You are a UI Component Testing Specialist. Your mission:
|
|
979
|
-
- Test all interactive components (buttons, dropdowns, modals, tabs)
|
|
980
|
-
- Verify component states (hover, active, disabled, loading)
|
|
981
|
-
- Test responsive behavior at different viewport sizes
|
|
982
|
-
- Check for broken layouts or overlapping elements
|
|
983
|
-
- Test keyboard navigation and focus management
|
|
984
|
-
- Verify animations and transitions work correctly
|
|
985
|
-
- Report visual bugs with screenshots`,
|
|
986
|
-
"accessibility-tester": `You are an Accessibility Testing Specialist. Your mission:
|
|
987
|
-
- Check for proper ARIA labels and roles
|
|
988
|
-
- Verify keyboard navigation works for all interactive elements
|
|
989
|
-
- Check color contrast ratios
|
|
990
|
-
- Verify images have alt text
|
|
991
|
-
- Test screen reader compatibility
|
|
992
|
-
- Check for proper heading hierarchy
|
|
993
|
-
- Verify focus indicators are visible
|
|
994
|
-
- Report WCAG violations with severity`,
|
|
995
|
-
"performance-tester": `You are a Performance Testing Specialist. Your mission:
|
|
996
|
-
- Measure page load times
|
|
997
|
-
- Identify slow-loading resources
|
|
998
|
-
- Check for render-blocking resources
|
|
999
|
-
- Monitor network requests and response times
|
|
1000
|
-
- Identify memory leaks or excessive DOM nodes
|
|
1001
|
-
- Check for unnecessary re-renders
|
|
1002
|
-
- Test under simulated slow network conditions
|
|
1003
|
-
- Report performance issues with metrics`,
|
|
1004
|
-
"api-tester": `You are an API Testing Specialist. Your mission:
|
|
1005
|
-
- Monitor network requests made by the application
|
|
1006
|
-
- Test API error handling
|
|
1007
|
-
- Check for proper authentication on API calls
|
|
1008
|
-
- Verify API response formats
|
|
1009
|
-
- Test rate limiting behavior
|
|
1010
|
-
- Check for exposed internal APIs
|
|
1011
|
-
- Verify proper HTTP methods are used
|
|
1012
|
-
- Report API issues with request/response details`,
|
|
1013
|
-
"auth-tester": `You are an Authentication Testing Specialist. Your mission:
|
|
1014
|
-
- Test login with valid/invalid credentials
|
|
1015
|
-
- Test password reset flow
|
|
1016
|
-
- Check session management (timeout, persistence)
|
|
1017
|
-
- Test logout functionality
|
|
1018
|
-
- Check for session fixation vulnerabilities
|
|
1019
|
-
- Test remember me functionality
|
|
1020
|
-
- Verify proper access control on protected pages
|
|
1021
|
-
- Test multi-factor authentication if present`,
|
|
1022
|
-
"navigation-tester": `You are a Navigation Testing Specialist. Your mission:
|
|
1023
|
-
- Test all navigation links and menus
|
|
1024
|
-
- Verify breadcrumbs work correctly
|
|
1025
|
-
- Test browser back/forward behavior
|
|
1026
|
-
- Check for broken links (404s)
|
|
1027
|
-
- Test deep linking and URL sharing
|
|
1028
|
-
- Verify redirects work properly
|
|
1029
|
-
- Test pagination and infinite scroll
|
|
1030
|
-
- Report navigation issues with affected URLs`
|
|
1031
|
-
};
|
|
1032
|
-
var SpecialistAgentManager = class extends EventEmitter2 {
|
|
1033
|
-
agents = /* @__PURE__ */ new Map();
|
|
1034
|
-
agentStatuses = /* @__PURE__ */ new Map();
|
|
1035
|
-
db;
|
|
1036
|
-
sessionId;
|
|
1037
|
-
llmConfig;
|
|
1038
|
-
browserTools;
|
|
1039
|
-
constructor(db, sessionId, llmConfig, browserTools) {
|
|
1040
|
-
super();
|
|
1041
|
-
this.db = db;
|
|
1042
|
-
this.sessionId = sessionId;
|
|
1043
|
-
this.llmConfig = llmConfig;
|
|
1044
|
-
this.browserTools = browserTools;
|
|
1045
|
-
}
|
|
1046
|
-
createLLMAdapter() {
|
|
1047
|
-
if (this.llmConfig.provider === "anthropic") {
|
|
1048
|
-
return new AnthropicAdapter({
|
|
1049
|
-
apiKey: this.llmConfig.apiKey,
|
|
1050
|
-
model: this.llmConfig.model || "claude-3-5-sonnet-20241022"
|
|
1051
|
-
});
|
|
1052
|
-
}
|
|
1053
|
-
return new OpenAIAdapter({
|
|
1054
|
-
apiKey: this.llmConfig.apiKey,
|
|
1055
|
-
model: this.llmConfig.model || "gpt-4"
|
|
1056
|
-
});
|
|
1057
|
-
}
|
|
1058
|
-
createSpecialist(type, customPrompt) {
|
|
1059
|
-
const agentId = `${type}_${Date.now()}`;
|
|
1060
|
-
const systemPrompt = customPrompt || SPECIALIST_PROMPTS[type];
|
|
1061
|
-
const agent = new ReActAgent({
|
|
1062
|
-
llm: this.createLLMAdapter(),
|
|
1063
|
-
tools: this.browserTools.getTools(),
|
|
1064
|
-
maxIterations: 15,
|
|
1065
|
-
systemPrompt: `${systemPrompt}
|
|
1066
|
-
|
|
1067
|
-
IMPORTANT RULES:
|
|
1068
|
-
- Take screenshots as evidence for any bug found
|
|
1069
|
-
- Create Kanban tickets for all findings
|
|
1070
|
-
- Create GitHub issues for critical/high severity bugs
|
|
1071
|
-
- Be thorough but efficient
|
|
1072
|
-
- Stop when you've tested the main scenarios for your specialty`
|
|
1073
|
-
});
|
|
1074
|
-
this.agents.set(agentId, agent);
|
|
1075
|
-
const status = {
|
|
1076
|
-
id: agentId,
|
|
1077
|
-
type,
|
|
1078
|
-
status: "idle",
|
|
1079
|
-
progress: 0,
|
|
1080
|
-
findings: 0,
|
|
1081
|
-
actions: 0
|
|
1082
289
|
};
|
|
1083
|
-
this.agentStatuses.set(agentId, status);
|
|
1084
|
-
this.emit("agent-created", status);
|
|
1085
|
-
return agentId;
|
|
1086
|
-
}
|
|
1087
|
-
async runSpecialist(agentId, targetUrl) {
|
|
1088
|
-
const agent = this.agents.get(agentId);
|
|
1089
|
-
const status = this.agentStatuses.get(agentId);
|
|
1090
|
-
if (!agent || !status) {
|
|
1091
|
-
throw new Error(`Agent ${agentId} not found`);
|
|
1092
|
-
}
|
|
1093
|
-
status.status = "running";
|
|
1094
|
-
status.startedAt = /* @__PURE__ */ new Date();
|
|
1095
|
-
status.progress = 0;
|
|
1096
|
-
this.emit("agent-started", status);
|
|
1097
|
-
try {
|
|
1098
|
-
const result = await agent.run(
|
|
1099
|
-
`Test the application at ${targetUrl}. Focus on your specialty area. Report all findings.`
|
|
1100
|
-
);
|
|
1101
|
-
status.status = "completed";
|
|
1102
|
-
status.completedAt = /* @__PURE__ */ new Date();
|
|
1103
|
-
status.progress = 100;
|
|
1104
|
-
this.emit("agent-completed", { ...status, result });
|
|
1105
|
-
} catch (error) {
|
|
1106
|
-
status.status = "failed";
|
|
1107
|
-
status.completedAt = /* @__PURE__ */ new Date();
|
|
1108
|
-
this.emit("agent-failed", { ...status, error: error.message });
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
async runAllSpecialists(targetUrl, types) {
|
|
1112
|
-
const agentTypes = types || [
|
|
1113
|
-
"form-tester",
|
|
1114
|
-
"security-scanner",
|
|
1115
|
-
"component-tester",
|
|
1116
|
-
"navigation-tester"
|
|
1117
|
-
];
|
|
1118
|
-
const agentIds = agentTypes.map((type) => this.createSpecialist(type));
|
|
1119
|
-
for (const agentId of agentIds) {
|
|
1120
|
-
await this.runSpecialist(agentId, targetUrl);
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
async runSecuritySuite(targetUrl) {
|
|
1124
|
-
const securityTypes = [
|
|
1125
|
-
"security-scanner",
|
|
1126
|
-
"sql-injection",
|
|
1127
|
-
"xss-tester",
|
|
1128
|
-
"auth-tester"
|
|
1129
|
-
];
|
|
1130
|
-
await this.runAllSpecialists(targetUrl, securityTypes);
|
|
1131
|
-
}
|
|
1132
|
-
getAgentStatus(agentId) {
|
|
1133
|
-
return this.agentStatuses.get(agentId);
|
|
1134
|
-
}
|
|
1135
|
-
getAllStatuses() {
|
|
1136
|
-
return Array.from(this.agentStatuses.values());
|
|
1137
290
|
}
|
|
1138
|
-
|
|
1139
|
-
const status = this.agentStatuses.get(agentId);
|
|
1140
|
-
if (status && status.status === "running") {
|
|
1141
|
-
status.status = "failed";
|
|
1142
|
-
status.completedAt = /* @__PURE__ */ new Date();
|
|
1143
|
-
this.emit("agent-stopped", status);
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
stopAll() {
|
|
1147
|
-
for (const [agentId] of this.agents) {
|
|
1148
|
-
this.stopAgent(agentId);
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
};
|
|
291
|
+
});
|
|
1152
292
|
|
|
1153
|
-
//
|
|
1154
|
-
var
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
type: "test-scenario",
|
|
1173
|
-
enabled: true,
|
|
1174
|
-
priority: 2,
|
|
1175
|
-
prompt: `Test mobile responsiveness:
|
|
1176
|
-
- Test at 375px width (iPhone)
|
|
1177
|
-
- Test at 768px width (tablet)
|
|
1178
|
-
- Check for horizontal scrolling issues
|
|
1179
|
-
- Verify touch targets are large enough
|
|
1180
|
-
- Check navigation menu behavior on mobile
|
|
1181
|
-
- Report any responsive design issues`,
|
|
1182
|
-
triggers: ["mobile", "responsive", "viewport"]
|
|
1183
|
-
},
|
|
1184
|
-
{
|
|
1185
|
-
name: "E-commerce Flow",
|
|
1186
|
-
description: "Test complete e-commerce purchase flow",
|
|
1187
|
-
type: "workflow",
|
|
1188
|
-
enabled: false,
|
|
1189
|
-
priority: 3,
|
|
1190
|
-
prompt: `Test e-commerce flow:
|
|
1191
|
-
- Browse products
|
|
1192
|
-
- Add items to cart
|
|
1193
|
-
- Verify cart updates correctly
|
|
1194
|
-
- Test checkout process
|
|
1195
|
-
- Test payment form validation
|
|
1196
|
-
- Verify order confirmation
|
|
1197
|
-
- Report any issues in the purchase flow`,
|
|
1198
|
-
triggers: ["shop", "cart", "checkout", "payment", "ecommerce"]
|
|
1199
|
-
},
|
|
1200
|
-
{
|
|
1201
|
-
name: "Dark Mode Testing",
|
|
1202
|
-
description: "Test dark mode if available",
|
|
1203
|
-
type: "custom-check",
|
|
1204
|
-
enabled: true,
|
|
1205
|
-
priority: 4,
|
|
1206
|
-
prompt: `Test dark mode:
|
|
1207
|
-
- Look for dark mode toggle
|
|
1208
|
-
- Switch between light and dark modes
|
|
1209
|
-
- Check for contrast issues in dark mode
|
|
1210
|
-
- Verify all text is readable
|
|
1211
|
-
- Check images and icons visibility
|
|
1212
|
-
- Report any dark mode specific bugs`,
|
|
1213
|
-
triggers: ["dark", "theme", "mode"]
|
|
1214
|
-
},
|
|
1215
|
-
{
|
|
1216
|
-
name: "Error Handling",
|
|
1217
|
-
description: "Test application error handling",
|
|
1218
|
-
type: "test-scenario",
|
|
1219
|
-
enabled: true,
|
|
1220
|
-
priority: 1,
|
|
1221
|
-
prompt: `Test error handling:
|
|
1222
|
-
- Try accessing non-existent pages (404)
|
|
1223
|
-
- Submit forms with invalid data
|
|
1224
|
-
- Test with network errors (if possible)
|
|
1225
|
-
- Check error message clarity
|
|
1226
|
-
- Verify errors don't expose sensitive info
|
|
1227
|
-
- Test recovery from error states
|
|
1228
|
-
- Report poor error handling`,
|
|
1229
|
-
triggers: ["error", "404", "exception"]
|
|
1230
|
-
},
|
|
1231
|
-
{
|
|
1232
|
-
name: "Rate Limiting Check",
|
|
1233
|
-
description: "Test for rate limiting on sensitive endpoints",
|
|
1234
|
-
type: "custom-check",
|
|
1235
|
-
enabled: true,
|
|
1236
|
-
priority: 2,
|
|
1237
|
-
prompt: `Test rate limiting:
|
|
1238
|
-
- Attempt multiple rapid login attempts
|
|
1239
|
-
- Test API endpoints for rate limiting
|
|
1240
|
-
- Check for CAPTCHA on repeated failures
|
|
1241
|
-
- Verify account lockout mechanisms
|
|
1242
|
-
- Report missing rate limiting as security issue`,
|
|
1243
|
-
triggers: ["rate", "limit", "brute", "ddos"]
|
|
1244
|
-
}
|
|
1245
|
-
];
|
|
1246
|
-
var SkillManager = class {
|
|
1247
|
-
db;
|
|
1248
|
-
skills = /* @__PURE__ */ new Map();
|
|
1249
|
-
constructor(db) {
|
|
1250
|
-
this.db = db;
|
|
1251
|
-
this.loadSkills();
|
|
1252
|
-
}
|
|
1253
|
-
loadSkills() {
|
|
1254
|
-
DEFAULT_SKILLS.forEach((skill) => {
|
|
1255
|
-
this.createSkill(skill);
|
|
293
|
+
// cli/server.ts
|
|
294
|
+
var server_exports = {};
|
|
295
|
+
__export(server_exports, {
|
|
296
|
+
startWebServer: () => startWebServer
|
|
297
|
+
});
|
|
298
|
+
import express from "express";
|
|
299
|
+
import { WebSocketServer } from "ws";
|
|
300
|
+
import chalk from "chalk";
|
|
301
|
+
async function startWebServer() {
|
|
302
|
+
const config = new ConfigManager();
|
|
303
|
+
const cfg = config.getConfigSync();
|
|
304
|
+
const db = new OpenQADatabase("./data/openqa.json");
|
|
305
|
+
const app = express();
|
|
306
|
+
app.use(express.json());
|
|
307
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
308
|
+
app.get("/api/status", async (req, res) => {
|
|
309
|
+
res.json({
|
|
310
|
+
isRunning: true,
|
|
311
|
+
target: cfg.saas.url || "Not configured"
|
|
1256
312
|
});
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
const
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
};
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
313
|
+
});
|
|
314
|
+
app.get("/api/sessions", async (req, res) => {
|
|
315
|
+
const limit = parseInt(req.query.limit) || 10;
|
|
316
|
+
const sessions = await db.getRecentSessions(limit);
|
|
317
|
+
res.json(sessions);
|
|
318
|
+
});
|
|
319
|
+
app.get("/api/sessions/:id/actions", async (req, res) => {
|
|
320
|
+
const actions = await db.getSessionActions(req.params.id);
|
|
321
|
+
res.json(actions);
|
|
322
|
+
});
|
|
323
|
+
app.get("/api/bugs", async (req, res) => {
|
|
324
|
+
const status = req.query.status;
|
|
325
|
+
const bugs = status ? await db.getBugsByStatus(status) : await db.getAllBugs();
|
|
326
|
+
res.json(bugs);
|
|
327
|
+
});
|
|
328
|
+
app.get("/api/kanban/tickets", async (req, res) => {
|
|
329
|
+
const column = req.query.column;
|
|
330
|
+
const tickets = column ? await db.getKanbanTicketsByColumn(column) : await db.getKanbanTickets();
|
|
331
|
+
res.json(tickets);
|
|
332
|
+
});
|
|
333
|
+
app.patch("/api/kanban/tickets/:id", async (req, res) => {
|
|
334
|
+
const { id } = req.params;
|
|
335
|
+
const updates = req.body;
|
|
336
|
+
await db.updateKanbanTicket(id, updates);
|
|
337
|
+
res.json({ success: true });
|
|
338
|
+
});
|
|
339
|
+
app.get("/api/config", (req, res) => {
|
|
340
|
+
res.json(cfg);
|
|
341
|
+
});
|
|
342
|
+
app.post("/api/config", async (req, res) => {
|
|
343
|
+
const { key, value } = req.body;
|
|
344
|
+
await config.set(key, value);
|
|
345
|
+
res.json({ success: true });
|
|
346
|
+
});
|
|
347
|
+
app.get("/", (req, res) => {
|
|
348
|
+
res.send(`
|
|
349
|
+
<!DOCTYPE html>
|
|
350
|
+
<html>
|
|
351
|
+
<head>
|
|
352
|
+
<title>OpenQA - Dashboard</title>
|
|
353
|
+
<style>
|
|
354
|
+
body { font-family: system-ui; max-width: 1200px; margin: 40px auto; padding: 20px; background: #0f172a; color: #e2e8f0; }
|
|
355
|
+
h1 { color: #38bdf8; }
|
|
356
|
+
.card { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 20px; margin: 20px 0; }
|
|
357
|
+
.status { display: inline-block; padding: 4px 12px; border-radius: 4px; font-size: 14px; }
|
|
358
|
+
.status.running { background: #10b981; color: white; }
|
|
359
|
+
.status.idle { background: #f59e0b; color: white; }
|
|
360
|
+
a { color: #38bdf8; text-decoration: none; }
|
|
361
|
+
a:hover { text-decoration: underline; }
|
|
362
|
+
nav { margin: 20px 0; }
|
|
363
|
+
nav a { margin-right: 20px; }
|
|
364
|
+
</style>
|
|
365
|
+
</head>
|
|
366
|
+
<body>
|
|
367
|
+
<h1>\u{1F916} OpenQA Dashboard</h1>
|
|
368
|
+
<nav>
|
|
369
|
+
<a href="/">Dashboard</a>
|
|
370
|
+
<a href="/kanban">Kanban</a>
|
|
371
|
+
<a href="/config">Config</a>
|
|
372
|
+
</nav>
|
|
373
|
+
<div class="card">
|
|
374
|
+
<h2>Status</h2>
|
|
375
|
+
<p>Agent: <span class="status idle">Idle</span></p>
|
|
376
|
+
<p>Target: ${cfg.saas.url || "Not configured"}</p>
|
|
377
|
+
<p>Auto-start: ${cfg.agent.autoStart ? "Enabled" : "Disabled"}</p>
|
|
378
|
+
</div>
|
|
379
|
+
<div class="card">
|
|
380
|
+
<h2>Quick Links</h2>
|
|
381
|
+
<ul>
|
|
382
|
+
<li><a href="/kanban">View Kanban Board</a></li>
|
|
383
|
+
<li><a href="/config">Configure OpenQA</a></li>
|
|
384
|
+
</ul>
|
|
385
|
+
</div>
|
|
386
|
+
<div class="card">
|
|
387
|
+
<h2>Getting Started</h2>
|
|
388
|
+
<p>Configure your SaaS application target and start testing:</p>
|
|
389
|
+
<ol>
|
|
390
|
+
<li>Set SAAS_URL environment variable or use the <a href="/config">Config page</a></li>
|
|
391
|
+
<li>Enable auto-start: <code>export AGENT_AUTO_START=true</code></li>
|
|
392
|
+
<li>Restart OpenQA</li>
|
|
393
|
+
</ol>
|
|
394
|
+
</div>
|
|
395
|
+
</body>
|
|
396
|
+
</html>
|
|
397
|
+
`);
|
|
398
|
+
});
|
|
399
|
+
app.get("/kanban", (req, res) => {
|
|
400
|
+
res.send(`
|
|
401
|
+
<!DOCTYPE html>
|
|
402
|
+
<html>
|
|
403
|
+
<head>
|
|
404
|
+
<title>OpenQA - Kanban Board</title>
|
|
405
|
+
<style>
|
|
406
|
+
body { font-family: system-ui; max-width: 1400px; margin: 40px auto; padding: 20px; background: #0f172a; color: #e2e8f0; }
|
|
407
|
+
h1 { color: #38bdf8; }
|
|
408
|
+
nav { margin: 20px 0; }
|
|
409
|
+
nav a { color: #38bdf8; text-decoration: none; margin-right: 20px; }
|
|
410
|
+
nav a:hover { text-decoration: underline; }
|
|
411
|
+
.board { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-top: 30px; }
|
|
412
|
+
.column { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 15px; }
|
|
413
|
+
.column h3 { margin-top: 0; color: #38bdf8; }
|
|
414
|
+
.ticket { background: #334155; padding: 12px; margin: 10px 0; border-radius: 6px; border-left: 3px solid #38bdf8; }
|
|
415
|
+
.ticket h4 { margin: 0 0 8px 0; font-size: 14px; }
|
|
416
|
+
.ticket p { margin: 0; font-size: 12px; color: #94a3b8; }
|
|
417
|
+
</style>
|
|
418
|
+
</head>
|
|
419
|
+
<body>
|
|
420
|
+
<h1>\u{1F4CB} Kanban Board</h1>
|
|
421
|
+
<nav>
|
|
422
|
+
<a href="/">Dashboard</a>
|
|
423
|
+
<a href="/kanban">Kanban</a>
|
|
424
|
+
<a href="/config">Config</a>
|
|
425
|
+
</nav>
|
|
426
|
+
<div class="board">
|
|
427
|
+
<div class="column">
|
|
428
|
+
<h3>Backlog</h3>
|
|
429
|
+
<p style="color: #64748b;">No tickets yet</p>
|
|
430
|
+
</div>
|
|
431
|
+
<div class="column">
|
|
432
|
+
<h3>To Do</h3>
|
|
433
|
+
<p style="color: #64748b;">No tickets yet</p>
|
|
434
|
+
</div>
|
|
435
|
+
<div class="column">
|
|
436
|
+
<h3>In Progress</h3>
|
|
437
|
+
<p style="color: #64748b;">No tickets yet</p>
|
|
438
|
+
</div>
|
|
439
|
+
<div class="column">
|
|
440
|
+
<h3>Done</h3>
|
|
441
|
+
<p style="color: #64748b;">No tickets yet</p>
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
<p style="margin-top: 40px; color: #64748b;">Tickets will appear here when the agent starts finding bugs and creating tasks.</p>
|
|
445
|
+
</body>
|
|
446
|
+
</html>
|
|
447
|
+
`);
|
|
448
|
+
});
|
|
449
|
+
app.get("/config", (req, res) => {
|
|
450
|
+
res.send(`
|
|
451
|
+
<!DOCTYPE html>
|
|
452
|
+
<html>
|
|
453
|
+
<head>
|
|
454
|
+
<title>OpenQA - Configuration</title>
|
|
455
|
+
<style>
|
|
456
|
+
body { font-family: system-ui; max-width: 800px; margin: 40px auto; padding: 20px; background: #0f172a; color: #e2e8f0; }
|
|
457
|
+
h1 { color: #38bdf8; }
|
|
458
|
+
nav { margin: 20px 0; }
|
|
459
|
+
nav a { color: #38bdf8; text-decoration: none; margin-right: 20px; }
|
|
460
|
+
nav a:hover { text-decoration: underline; }
|
|
461
|
+
.section { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 20px; margin: 20px 0; }
|
|
462
|
+
.section h2 { margin-top: 0; color: #38bdf8; font-size: 18px; }
|
|
463
|
+
.config-item { margin: 15px 0; }
|
|
464
|
+
.config-item label { display: block; margin-bottom: 5px; color: #94a3b8; font-size: 14px; }
|
|
465
|
+
.config-item .value { background: #334155; padding: 8px 12px; border-radius: 4px; font-family: monospace; font-size: 14px; }
|
|
466
|
+
code { background: #334155; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
|
467
|
+
</style>
|
|
468
|
+
</head>
|
|
469
|
+
<body>
|
|
470
|
+
<h1>\u2699\uFE0F Configuration</h1>
|
|
471
|
+
<nav>
|
|
472
|
+
<a href="/">Dashboard</a>
|
|
473
|
+
<a href="/kanban">Kanban</a>
|
|
474
|
+
<a href="/config">Config</a>
|
|
475
|
+
</nav>
|
|
476
|
+
|
|
477
|
+
<div class="section">
|
|
478
|
+
<h2>SaaS Target</h2>
|
|
479
|
+
<div class="config-item">
|
|
480
|
+
<label>URL</label>
|
|
481
|
+
<div class="value">${cfg.saas.url || "Not configured"}</div>
|
|
482
|
+
</div>
|
|
483
|
+
<div class="config-item">
|
|
484
|
+
<label>Auth Type</label>
|
|
485
|
+
<div class="value">${cfg.saas.authType}</div>
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
1316
488
|
|
|
1317
|
-
|
|
489
|
+
<div class="section">
|
|
490
|
+
<h2>LLM Configuration</h2>
|
|
491
|
+
<div class="config-item">
|
|
492
|
+
<label>Provider</label>
|
|
493
|
+
<div class="value">${cfg.llm.provider}</div>
|
|
494
|
+
</div>
|
|
495
|
+
<div class="config-item">
|
|
496
|
+
<label>Model</label>
|
|
497
|
+
<div class="value">${cfg.llm.model || "default"}</div>
|
|
498
|
+
</div>
|
|
499
|
+
</div>
|
|
1318
500
|
|
|
1319
|
-
|
|
501
|
+
<div class="section">
|
|
502
|
+
<h2>Agent Settings</h2>
|
|
503
|
+
<div class="config-item">
|
|
504
|
+
<label>Auto-start</label>
|
|
505
|
+
<div class="value">${cfg.agent.autoStart ? "Enabled" : "Disabled"}</div>
|
|
506
|
+
</div>
|
|
507
|
+
<div class="config-item">
|
|
508
|
+
<label>Interval</label>
|
|
509
|
+
<div class="value">${cfg.agent.intervalMs}ms</div>
|
|
510
|
+
</div>
|
|
511
|
+
<div class="config-item">
|
|
512
|
+
<label>Max Iterations</label>
|
|
513
|
+
<div class="value">${cfg.agent.maxIterations}</div>
|
|
514
|
+
</div>
|
|
515
|
+
</div>
|
|
1320
516
|
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
517
|
+
<div class="section">
|
|
518
|
+
<h2>How to Configure</h2>
|
|
519
|
+
<p>Set environment variables before starting OpenQA:</p>
|
|
520
|
+
<pre style="background: #334155; padding: 15px; border-radius: 6px; overflow-x: auto;"><code>export SAAS_URL="https://your-app.com"
|
|
521
|
+
export AGENT_AUTO_START=true
|
|
522
|
+
export LLM_PROVIDER=openai
|
|
523
|
+
export OPENAI_API_KEY="your-key"
|
|
524
|
+
|
|
525
|
+
openqa start</code></pre>
|
|
526
|
+
</div>
|
|
527
|
+
</body>
|
|
528
|
+
</html>
|
|
529
|
+
`);
|
|
530
|
+
});
|
|
531
|
+
const server = app.listen(cfg.web.port, cfg.web.host, () => {
|
|
532
|
+
console.log(chalk.cyan("\n\u{1F4CA} OpenQA Status:"));
|
|
533
|
+
console.log(chalk.white(` Agent: ${cfg.agent.autoStart ? "Auto-start enabled" : "Idle"}`));
|
|
534
|
+
console.log(chalk.white(` Target: ${cfg.saas.url || "Not configured"}`));
|
|
535
|
+
console.log(chalk.white(` Dashboard: http://localhost:${cfg.web.port}`));
|
|
536
|
+
console.log(chalk.white(` Kanban: http://localhost:${cfg.web.port}/kanban`));
|
|
537
|
+
console.log(chalk.white(` Config: http://localhost:${cfg.web.port}/config`));
|
|
538
|
+
console.log(chalk.gray("\nPress Ctrl+C to stop\n"));
|
|
539
|
+
if (!cfg.agent.autoStart) {
|
|
540
|
+
console.log(chalk.yellow("\u{1F4A1} Auto-start disabled. Agent is idle."));
|
|
541
|
+
console.log(chalk.cyan(" Set AGENT_AUTO_START=true to enable autonomous mode\n"));
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
server.on("upgrade", (request, socket, head) => {
|
|
545
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
546
|
+
wss.emit("connection", ws, request);
|
|
1336
547
|
});
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
importSkills(json) {
|
|
1343
|
-
const imported = JSON.parse(json);
|
|
1344
|
-
let count = 0;
|
|
1345
|
-
imported.forEach((skill) => {
|
|
1346
|
-
const newSkill = this.createSkill({
|
|
1347
|
-
name: skill.name,
|
|
1348
|
-
description: skill.description,
|
|
1349
|
-
type: skill.type,
|
|
1350
|
-
enabled: skill.enabled,
|
|
1351
|
-
priority: skill.priority,
|
|
1352
|
-
prompt: skill.prompt,
|
|
1353
|
-
triggers: skill.triggers
|
|
1354
|
-
});
|
|
1355
|
-
if (newSkill) count++;
|
|
548
|
+
});
|
|
549
|
+
wss.on("connection", (ws) => {
|
|
550
|
+
console.log("WebSocket client connected");
|
|
551
|
+
ws.on("close", () => {
|
|
552
|
+
console.log("WebSocket client disconnected");
|
|
1356
553
|
});
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
var OpenQAAgent = class extends EventEmitter3 {
|
|
1363
|
-
agent = null;
|
|
1364
|
-
db;
|
|
1365
|
-
config;
|
|
1366
|
-
browserTools = null;
|
|
1367
|
-
sessionId = "";
|
|
1368
|
-
isRunning = false;
|
|
1369
|
-
intervalId = null;
|
|
1370
|
-
// New v2 features
|
|
1371
|
-
gitListener = null;
|
|
1372
|
-
specialistManager = null;
|
|
1373
|
-
skillManager;
|
|
1374
|
-
constructor(configPath) {
|
|
1375
|
-
super();
|
|
1376
|
-
this.config = new ConfigManager(configPath);
|
|
1377
|
-
this.db = new OpenQADatabase("./data/openqa.json");
|
|
1378
|
-
this.skillManager = new SkillManager(this.db);
|
|
1379
|
-
}
|
|
1380
|
-
createLLMAdapter() {
|
|
1381
|
-
const cfg = this.config.getConfigSync();
|
|
1382
|
-
switch (cfg.llm.provider) {
|
|
1383
|
-
case "anthropic":
|
|
1384
|
-
return new AnthropicAdapter2({
|
|
1385
|
-
apiKey: cfg.llm.apiKey || process.env.ANTHROPIC_API_KEY,
|
|
1386
|
-
model: cfg.llm.model || "claude-3-5-sonnet-20241022"
|
|
1387
|
-
});
|
|
1388
|
-
case "openai":
|
|
1389
|
-
default:
|
|
1390
|
-
return new OpenAIAdapter2({
|
|
1391
|
-
apiKey: cfg.llm.apiKey || process.env.OPENAI_API_KEY,
|
|
1392
|
-
model: cfg.llm.model || "gpt-4"
|
|
1393
|
-
});
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
async initialize(triggerType = "manual", triggerData) {
|
|
1397
|
-
const cfg = this.config.getConfigSync();
|
|
1398
|
-
this.sessionId = `session_${Date.now()}`;
|
|
1399
|
-
await this.db.createSession(this.sessionId, {
|
|
1400
|
-
config: cfg,
|
|
1401
|
-
started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1402
|
-
trigger_type: triggerType,
|
|
1403
|
-
trigger_data: triggerData ? JSON.stringify(triggerData) : null
|
|
554
|
+
});
|
|
555
|
+
process.on("SIGTERM", () => {
|
|
556
|
+
console.log("Received SIGTERM, shutting down gracefully...");
|
|
557
|
+
server.close(() => {
|
|
558
|
+
process.exit(0);
|
|
1404
559
|
});
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
goal: "Test the SaaS application comprehensively and identify bugs",
|
|
1420
|
-
tools: allTools,
|
|
1421
|
-
tracer,
|
|
1422
|
-
maxIterations: cfg.agent.maxIterations,
|
|
1423
|
-
systemPrompt: `You are OpenQA, an autonomous QA testing agent - intelligent and thorough like a senior QA engineer.
|
|
1424
|
-
|
|
1425
|
-
Your mission:
|
|
1426
|
-
1. **Systematically test the SaaS application** at ${cfg.saas.url}
|
|
1427
|
-
2. **Create comprehensive test flows** - think like a real user AND a security expert
|
|
1428
|
-
3. **Identify bugs and issues** - UI bugs, console errors, broken flows, UX issues, security vulnerabilities
|
|
1429
|
-
4. **Report findings appropriately**:
|
|
1430
|
-
- Use create_github_issue for critical bugs requiring developer attention
|
|
1431
|
-
- Use create_kanban_ticket for QA tracking, minor issues, or improvements
|
|
1432
|
-
- You can create BOTH for critical bugs
|
|
1433
|
-
5. **Learn from previous sessions** - avoid repeating the same tests
|
|
1434
|
-
6. **Spawn specialist agents** when needed for deep testing (security, forms, etc.)
|
|
1435
|
-
|
|
1436
|
-
Testing strategy:
|
|
1437
|
-
- Start with core user flows (signup, login, main features)
|
|
1438
|
-
- Test edge cases and error handling
|
|
1439
|
-
- Check for console errors and network issues
|
|
1440
|
-
- Test security (SQL injection, XSS, auth bypass)
|
|
1441
|
-
- Test forms thoroughly (validation, edge cases)
|
|
1442
|
-
- Take screenshots as evidence
|
|
1443
|
-
- Document steps to reproduce clearly
|
|
1444
|
-
|
|
1445
|
-
Reporting guidelines:
|
|
1446
|
-
- **Critical/High severity** \u2192 GitHub issue + Kanban ticket
|
|
1447
|
-
- **Medium severity** \u2192 Kanban ticket (optionally GitHub if it blocks users)
|
|
1448
|
-
- **Low severity/Improvements** \u2192 Kanban ticket only
|
|
1449
|
-
|
|
1450
|
-
${skillPrompt}
|
|
1451
|
-
|
|
1452
|
-
Always provide clear, actionable information with steps to reproduce. Think step by step like a human QA expert.`
|
|
1453
|
-
};
|
|
1454
|
-
this.agent = new ReActAgent2(agentConfig, llm, memory);
|
|
1455
|
-
this.specialistManager = new SpecialistAgentManager(
|
|
1456
|
-
this.db,
|
|
1457
|
-
this.sessionId,
|
|
1458
|
-
{ provider: cfg.llm.provider, apiKey: cfg.llm.apiKey || "" },
|
|
1459
|
-
this.browserTools
|
|
1460
|
-
);
|
|
1461
|
-
this.specialistManager.on("agent-created", (status) => this.emit("specialist-created", status));
|
|
1462
|
-
this.specialistManager.on("agent-started", (status) => this.emit("specialist-started", status));
|
|
1463
|
-
this.specialistManager.on("agent-completed", (data) => this.emit("specialist-completed", data));
|
|
1464
|
-
this.specialistManager.on("agent-failed", (data) => this.emit("specialist-failed", data));
|
|
1465
|
-
console.log(`\u2705 OpenQA Agent initialized (Session: ${this.sessionId})`);
|
|
1466
|
-
}
|
|
1467
|
-
async runSession() {
|
|
1468
|
-
if (!this.agent) {
|
|
1469
|
-
await this.initialize();
|
|
1470
|
-
}
|
|
1471
|
-
const cfg = this.config.getConfig();
|
|
1472
|
-
console.log(`\u{1F680} Starting test session for ${cfg.saas.url}`);
|
|
1473
|
-
try {
|
|
1474
|
-
const result = await this.agent.run(
|
|
1475
|
-
`Continue testing the application at ${cfg.saas.url}. Review previous findings, create new test scenarios, and report any issues discovered. Focus on areas not yet tested.`
|
|
1476
|
-
);
|
|
1477
|
-
this.db.updateSession(this.sessionId, {
|
|
1478
|
-
status: "completed",
|
|
1479
|
-
ended_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1480
|
-
});
|
|
1481
|
-
console.log("\u2705 Test session completed:", result);
|
|
1482
|
-
return result;
|
|
1483
|
-
} catch (error) {
|
|
1484
|
-
console.error("\u274C Session error:", error);
|
|
1485
|
-
this.db.updateSession(this.sessionId, {
|
|
1486
|
-
status: "failed",
|
|
1487
|
-
ended_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1488
|
-
});
|
|
1489
|
-
throw error;
|
|
1490
|
-
} finally {
|
|
1491
|
-
if (this.browserTools) {
|
|
1492
|
-
await this.browserTools.close();
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
async startAutonomous() {
|
|
1497
|
-
if (this.isRunning) {
|
|
1498
|
-
console.log("\u26A0\uFE0F Agent is already running");
|
|
1499
|
-
return;
|
|
1500
|
-
}
|
|
1501
|
-
this.isRunning = true;
|
|
1502
|
-
const cfg = this.config.getConfig();
|
|
1503
|
-
console.log(`\u{1F916} OpenQA Agent starting in autonomous mode`);
|
|
1504
|
-
console.log(`\u{1F4CD} Target: ${cfg.saas.url}`);
|
|
1505
|
-
console.log(`\u23F1\uFE0F Interval: ${cfg.agent.intervalMs}ms (${cfg.agent.intervalMs / 1e3 / 60} minutes)`);
|
|
1506
|
-
await this.startGitListener();
|
|
1507
|
-
const runLoop = async () => {
|
|
1508
|
-
if (!this.isRunning) return;
|
|
1509
|
-
try {
|
|
1510
|
-
await this.runSession();
|
|
1511
|
-
} catch (error) {
|
|
1512
|
-
console.error("Session failed, will retry on next interval");
|
|
1513
|
-
}
|
|
1514
|
-
if (this.isRunning) {
|
|
1515
|
-
this.sessionId = `session_${Date.now()}`;
|
|
1516
|
-
this.agent = null;
|
|
1517
|
-
this.browserTools = null;
|
|
1518
|
-
this.intervalId = setTimeout(runLoop, cfg.agent.intervalMs);
|
|
1519
|
-
}
|
|
1520
|
-
};
|
|
1521
|
-
await runLoop();
|
|
1522
|
-
}
|
|
1523
|
-
stop() {
|
|
1524
|
-
console.log("\u{1F6D1} Stopping OpenQA Agent...");
|
|
1525
|
-
this.isRunning = false;
|
|
1526
|
-
if (this.intervalId) {
|
|
1527
|
-
clearTimeout(this.intervalId);
|
|
1528
|
-
this.intervalId = null;
|
|
1529
|
-
}
|
|
1530
|
-
if (this.gitListener) {
|
|
1531
|
-
this.gitListener.stop();
|
|
1532
|
-
this.gitListener = null;
|
|
1533
|
-
}
|
|
1534
|
-
if (this.specialistManager) {
|
|
1535
|
-
this.specialistManager.stopAll();
|
|
1536
|
-
}
|
|
1537
|
-
if (this.browserTools) {
|
|
1538
|
-
this.browserTools.close();
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
// Git integration
|
|
1542
|
-
async startGitListener() {
|
|
1543
|
-
const cfg = this.config.getConfig();
|
|
1544
|
-
if (cfg.github?.token && cfg.github?.owner && cfg.github?.repo) {
|
|
1545
|
-
this.gitListener = new GitListener({
|
|
1546
|
-
provider: "github",
|
|
1547
|
-
token: cfg.github.token,
|
|
1548
|
-
owner: cfg.github.owner,
|
|
1549
|
-
repo: cfg.github.repo,
|
|
1550
|
-
branch: "main",
|
|
1551
|
-
pollIntervalMs: 6e4
|
|
1552
|
-
});
|
|
1553
|
-
} else if (this.config.get("gitlab.token") && this.config.get("gitlab.project")) {
|
|
1554
|
-
const [owner, repo] = (this.config.get("gitlab.project") || "").split("/");
|
|
1555
|
-
this.gitListener = new GitListener({
|
|
1556
|
-
provider: "gitlab",
|
|
1557
|
-
token: this.config.get("gitlab.token") || "",
|
|
1558
|
-
owner,
|
|
1559
|
-
repo,
|
|
1560
|
-
branch: "main",
|
|
1561
|
-
pollIntervalMs: 6e4,
|
|
1562
|
-
gitlabUrl: this.config.get("gitlab.url") || "https://gitlab.com"
|
|
1563
|
-
});
|
|
1564
|
-
}
|
|
1565
|
-
if (this.gitListener) {
|
|
1566
|
-
this.gitListener.on("merge", async (event) => {
|
|
1567
|
-
console.log(`\u{1F500} Merge detected! Starting test session...`);
|
|
1568
|
-
this.emit("git-merge", event);
|
|
1569
|
-
this.sessionId = `session_${Date.now()}`;
|
|
1570
|
-
this.agent = null;
|
|
1571
|
-
this.browserTools = null;
|
|
1572
|
-
await this.runSession();
|
|
1573
|
-
});
|
|
1574
|
-
this.gitListener.on("pipeline-success", async (event) => {
|
|
1575
|
-
console.log(`\u2705 Pipeline success! Starting test session...`);
|
|
1576
|
-
this.emit("git-pipeline-success", event);
|
|
1577
|
-
this.sessionId = `session_${Date.now()}`;
|
|
1578
|
-
this.agent = null;
|
|
1579
|
-
this.browserTools = null;
|
|
1580
|
-
await this.runSession();
|
|
1581
|
-
});
|
|
1582
|
-
await this.gitListener.start();
|
|
1583
|
-
console.log(`\u{1F517} Git listener started for ${this.gitListener ? "repository" : "none"}`);
|
|
1584
|
-
}
|
|
1585
|
-
}
|
|
1586
|
-
// Specialist agents management
|
|
1587
|
-
async runSecurityScan() {
|
|
1588
|
-
if (!this.specialistManager) {
|
|
1589
|
-
await this.initialize();
|
|
1590
|
-
}
|
|
1591
|
-
const cfg = this.config.getConfig();
|
|
1592
|
-
await this.specialistManager.runSecuritySuite(cfg.saas.url);
|
|
1593
|
-
}
|
|
1594
|
-
async runSpecialist(type) {
|
|
1595
|
-
if (!this.specialistManager) {
|
|
1596
|
-
await this.initialize();
|
|
1597
|
-
}
|
|
1598
|
-
const cfg = this.config.getConfig();
|
|
1599
|
-
const agentId = this.specialistManager.createSpecialist(type);
|
|
1600
|
-
await this.specialistManager.runSpecialist(agentId, cfg.saas.url);
|
|
1601
|
-
}
|
|
1602
|
-
getSpecialistStatuses() {
|
|
1603
|
-
return this.specialistManager?.getAllStatuses() || [];
|
|
1604
|
-
}
|
|
1605
|
-
// Skills management
|
|
1606
|
-
getSkills() {
|
|
1607
|
-
return this.skillManager.getAllSkills();
|
|
1608
|
-
}
|
|
1609
|
-
createSkill(data) {
|
|
1610
|
-
return this.skillManager.createSkill(data);
|
|
1611
|
-
}
|
|
1612
|
-
updateSkill(id, updates) {
|
|
1613
|
-
return this.skillManager.updateSkill(id, updates);
|
|
1614
|
-
}
|
|
1615
|
-
deleteSkill(id) {
|
|
1616
|
-
return this.skillManager.deleteSkill(id);
|
|
1617
|
-
}
|
|
1618
|
-
toggleSkill(id) {
|
|
1619
|
-
return this.skillManager.toggleSkill(id);
|
|
1620
|
-
}
|
|
1621
|
-
getStatus() {
|
|
1622
|
-
return {
|
|
1623
|
-
isRunning: this.isRunning,
|
|
1624
|
-
sessionId: this.sessionId,
|
|
1625
|
-
config: this.config.getConfig(),
|
|
1626
|
-
gitListenerActive: !!this.gitListener,
|
|
1627
|
-
specialists: this.getSpecialistStatuses(),
|
|
1628
|
-
skills: this.skillManager.getEnabledSkills().length
|
|
1629
|
-
};
|
|
560
|
+
});
|
|
561
|
+
process.on("SIGINT", () => {
|
|
562
|
+
console.log("\nShutting down gracefully...");
|
|
563
|
+
server.close(() => {
|
|
564
|
+
process.exit(0);
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
var init_server = __esm({
|
|
569
|
+
"cli/server.ts"() {
|
|
570
|
+
"use strict";
|
|
571
|
+
init_esm_shims();
|
|
572
|
+
init_config();
|
|
573
|
+
init_database();
|
|
1630
574
|
}
|
|
1631
|
-
};
|
|
575
|
+
});
|
|
1632
576
|
|
|
1633
577
|
// cli/index.ts
|
|
578
|
+
init_esm_shims();
|
|
579
|
+
init_config();
|
|
580
|
+
init_database();
|
|
581
|
+
import { Command } from "commander";
|
|
1634
582
|
import { spawn } from "child_process";
|
|
1635
583
|
import { writeFileSync as writeFileSync2, readFileSync as readFileSync2, existsSync, unlinkSync } from "fs";
|
|
1636
|
-
import { join as
|
|
1637
|
-
import
|
|
584
|
+
import { join as join2 } from "path";
|
|
585
|
+
import chalk2 from "chalk";
|
|
1638
586
|
import ora from "ora";
|
|
1639
587
|
var program = new Command();
|
|
1640
588
|
var PID_FILE = "./data/openqa.pid";
|
|
@@ -1646,46 +594,31 @@ program.command("start").description("Start the OpenQA agent and web UI").option
|
|
|
1646
594
|
const pid = readFileSync2(PID_FILE, "utf-8").trim();
|
|
1647
595
|
try {
|
|
1648
596
|
process.kill(parseInt(pid), 0);
|
|
1649
|
-
spinner.fail(
|
|
1650
|
-
console.log(
|
|
1651
|
-
console.log(
|
|
597
|
+
spinner.fail(chalk2.red("OpenQA is already running"));
|
|
598
|
+
console.log(chalk2.yellow(`PID: ${pid}`));
|
|
599
|
+
console.log(chalk2.cyan('Run "openqa stop" to stop it first'));
|
|
1652
600
|
process.exit(1);
|
|
1653
601
|
} catch {
|
|
1654
602
|
unlinkSync(PID_FILE);
|
|
1655
603
|
}
|
|
1656
604
|
}
|
|
1657
605
|
if (options.daemon) {
|
|
1658
|
-
const child = spawn("node", [
|
|
606
|
+
const child = spawn("node", [join2(process.cwd(), "dist/cli/daemon.js")], {
|
|
1659
607
|
detached: true,
|
|
1660
608
|
stdio: "ignore"
|
|
1661
609
|
});
|
|
1662
610
|
child.unref();
|
|
1663
611
|
writeFileSync2(PID_FILE, child.pid.toString());
|
|
1664
|
-
spinner.succeed(
|
|
1665
|
-
console.log(
|
|
612
|
+
spinner.succeed(chalk2.green("OpenQA started in daemon mode"));
|
|
613
|
+
console.log(chalk2.cyan(`PID: ${child.pid}`));
|
|
1666
614
|
} else {
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
const cfg = config.getConfigSync();
|
|
1671
|
-
console.log(chalk.cyan("\n\u{1F4CA} OpenQA Status:"));
|
|
1672
|
-
console.log(chalk.white(` Agent: Running`));
|
|
1673
|
-
console.log(chalk.white(` Target: ${cfg.saas.url || "Not configured"}`));
|
|
1674
|
-
console.log(chalk.white(` Web UI: http://localhost:${cfg.web.port}`));
|
|
1675
|
-
console.log(chalk.white(` DevTools: http://localhost:${cfg.web.port}`));
|
|
1676
|
-
console.log(chalk.white(` Kanban: http://localhost:${cfg.web.port}/kanban`));
|
|
1677
|
-
console.log(chalk.white(` Config: http://localhost:${cfg.web.port}/config`));
|
|
1678
|
-
console.log(chalk.gray("\nPress Ctrl+C to stop\n"));
|
|
1679
|
-
if (cfg.agent.autoStart) {
|
|
1680
|
-
await agent.startAutonomous();
|
|
1681
|
-
} else {
|
|
1682
|
-
console.log(chalk.yellow("Auto-start disabled. Agent is idle."));
|
|
1683
|
-
console.log(chalk.cyan("Set AGENT_AUTO_START=true to enable autonomous mode"));
|
|
1684
|
-
}
|
|
615
|
+
const { startWebServer: startWebServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
616
|
+
await startWebServer2();
|
|
617
|
+
spinner.succeed(chalk2.green("OpenQA started"));
|
|
1685
618
|
}
|
|
1686
619
|
} catch (error) {
|
|
1687
|
-
spinner.fail(
|
|
1688
|
-
console.error(
|
|
620
|
+
spinner.fail(chalk2.red("Failed to start OpenQA"));
|
|
621
|
+
console.error(chalk2.red(error.message));
|
|
1689
622
|
process.exit(1);
|
|
1690
623
|
}
|
|
1691
624
|
});
|
|
@@ -1693,78 +626,78 @@ program.command("stop").description("Stop the OpenQA agent").action(() => {
|
|
|
1693
626
|
const spinner = ora("Stopping OpenQA...").start();
|
|
1694
627
|
try {
|
|
1695
628
|
if (!existsSync(PID_FILE)) {
|
|
1696
|
-
spinner.fail(
|
|
629
|
+
spinner.fail(chalk2.red("OpenQA is not running"));
|
|
1697
630
|
process.exit(1);
|
|
1698
631
|
}
|
|
1699
632
|
const pid = readFileSync2(PID_FILE, "utf-8").trim();
|
|
1700
633
|
try {
|
|
1701
634
|
process.kill(parseInt(pid), "SIGTERM");
|
|
1702
635
|
unlinkSync(PID_FILE);
|
|
1703
|
-
spinner.succeed(
|
|
636
|
+
spinner.succeed(chalk2.green("OpenQA stopped"));
|
|
1704
637
|
} catch (error) {
|
|
1705
|
-
spinner.fail(
|
|
1706
|
-
console.error(
|
|
638
|
+
spinner.fail(chalk2.red("Failed to stop OpenQA"));
|
|
639
|
+
console.error(chalk2.red("Process not found. Cleaning up PID file..."));
|
|
1707
640
|
unlinkSync(PID_FILE);
|
|
1708
641
|
}
|
|
1709
642
|
} catch (error) {
|
|
1710
|
-
spinner.fail(
|
|
1711
|
-
console.error(
|
|
643
|
+
spinner.fail(chalk2.red("Failed to stop OpenQA"));
|
|
644
|
+
console.error(chalk2.red(error.message));
|
|
1712
645
|
process.exit(1);
|
|
1713
646
|
}
|
|
1714
647
|
});
|
|
1715
648
|
program.command("status").description("Show OpenQA status").action(() => {
|
|
1716
|
-
console.log(
|
|
649
|
+
console.log(chalk2.cyan.bold("\n\u{1F4CA} OpenQA Status\n"));
|
|
1717
650
|
if (existsSync(PID_FILE)) {
|
|
1718
651
|
const pid = readFileSync2(PID_FILE, "utf-8").trim();
|
|
1719
652
|
try {
|
|
1720
653
|
process.kill(parseInt(pid), 0);
|
|
1721
|
-
console.log(
|
|
1722
|
-
console.log(
|
|
1723
|
-
console.log(
|
|
654
|
+
console.log(chalk2.green("\u2713 Agent: Running"));
|
|
655
|
+
console.log(chalk2.white(` PID: ${pid}`));
|
|
656
|
+
console.log(chalk2.yellow("\n\u{1F4CB} Detailed status temporarily disabled"));
|
|
1724
657
|
} catch {
|
|
1725
|
-
console.log(
|
|
658
|
+
console.log(chalk2.red("\u2717 Agent: Not running (stale PID file)"));
|
|
1726
659
|
unlinkSync(PID_FILE);
|
|
1727
660
|
}
|
|
1728
661
|
} else {
|
|
1729
|
-
console.log(
|
|
662
|
+
console.log(chalk2.yellow("\u2717 Agent: Not running"));
|
|
1730
663
|
}
|
|
1731
664
|
});
|
|
1732
665
|
program.command("config").description("Manage configuration").argument("[action]", "Action: get, set, list").argument("[key]", "Configuration key (e.g., llm.provider)").argument("[value]", "Configuration value").action((action, key, value) => {
|
|
1733
666
|
const config = new ConfigManager();
|
|
1734
667
|
if (!action || action === "list") {
|
|
1735
668
|
const cfg = config.getConfigSync();
|
|
1736
|
-
console.log(
|
|
669
|
+
console.log(chalk2.cyan.bold("\n\u2699\uFE0F OpenQA Configuration\n"));
|
|
1737
670
|
console.log(JSON.stringify(cfg, null, 2));
|
|
1738
671
|
console.log("");
|
|
1739
672
|
return;
|
|
1740
673
|
}
|
|
1741
674
|
if (action === "get") {
|
|
1742
675
|
if (!key) {
|
|
1743
|
-
console.error(
|
|
676
|
+
console.error(chalk2.red('Error: key is required for "get" action'));
|
|
1744
677
|
process.exit(1);
|
|
1745
678
|
}
|
|
1746
679
|
const val = config.get(key);
|
|
1747
|
-
console.log(
|
|
680
|
+
console.log(chalk2.cyan(`${key}:`), chalk2.white(val || "Not set"));
|
|
1748
681
|
return;
|
|
1749
682
|
}
|
|
1750
683
|
if (action === "set") {
|
|
1751
684
|
if (!key || !value) {
|
|
1752
|
-
console.error(
|
|
685
|
+
console.error(chalk2.red('Error: key and value are required for "set" action'));
|
|
1753
686
|
process.exit(1);
|
|
1754
687
|
}
|
|
1755
688
|
config.set(key, value);
|
|
1756
|
-
console.log(
|
|
689
|
+
console.log(chalk2.green(`\u2713 Set ${key} = ${value}`));
|
|
1757
690
|
return;
|
|
1758
691
|
}
|
|
1759
|
-
console.error(
|
|
1760
|
-
console.log(
|
|
692
|
+
console.error(chalk2.red(`Unknown action: ${action}`));
|
|
693
|
+
console.log(chalk2.cyan("Available actions: get, set, list"));
|
|
1761
694
|
process.exit(1);
|
|
1762
695
|
});
|
|
1763
696
|
program.command("logs").description("Show agent logs").option("-f, --follow", "Follow log output").option("-n, --lines <number>", "Number of lines to show", "50").action((options) => {
|
|
1764
697
|
const config = new ConfigManager();
|
|
1765
698
|
const cfg = config.getConfigSync();
|
|
1766
699
|
const db = new OpenQADatabase(cfg.database.path);
|
|
1767
|
-
console.log(
|
|
700
|
+
console.log(chalk2.yellow("Logs feature temporarily disabled"));
|
|
1768
701
|
return;
|
|
1769
702
|
});
|
|
1770
703
|
program.parse();
|