@openqa/cli 1.3.4 → 2.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/README.md +203 -6
- package/dist/agent/brain/diff-analyzer.js +140 -0
- package/dist/agent/brain/diff-analyzer.js.map +1 -0
- package/dist/agent/brain/llm-cache.js +47 -0
- package/dist/agent/brain/llm-cache.js.map +1 -0
- package/dist/agent/brain/llm-resilience.js +252 -0
- package/dist/agent/brain/llm-resilience.js.map +1 -0
- package/dist/agent/config/index.js +588 -0
- package/dist/agent/config/index.js.map +1 -0
- package/dist/agent/coverage/index.js +74 -0
- package/dist/agent/coverage/index.js.map +1 -0
- package/dist/agent/export/index.js +158 -0
- package/dist/agent/export/index.js.map +1 -0
- package/dist/agent/index-v2.js +2795 -0
- package/dist/agent/index-v2.js.map +1 -0
- package/dist/agent/index.js +369 -105
- package/dist/agent/index.js.map +1 -1
- package/dist/agent/logger.js +41 -0
- package/dist/agent/logger.js.map +1 -0
- package/dist/agent/metrics.js +39 -0
- package/dist/agent/metrics.js.map +1 -0
- package/dist/agent/notifications/index.js +106 -0
- package/dist/agent/notifications/index.js.map +1 -0
- package/dist/agent/openapi/spec.js +338 -0
- package/dist/agent/openapi/spec.js.map +1 -0
- package/dist/agent/tools/project-runner.js +481 -0
- package/dist/agent/tools/project-runner.js.map +1 -0
- package/dist/cli/config.html.js +454 -0
- package/dist/cli/daemon.js +8810 -0
- package/dist/cli/dashboard.html.js +1622 -0
- package/dist/cli/env-config.js +391 -0
- package/dist/cli/env-routes.js +820 -0
- package/dist/cli/env.html.js +679 -0
- package/dist/cli/index.js +5980 -1896
- package/dist/cli/kanban.html.js +577 -0
- package/dist/cli/routes.js +895 -0
- package/dist/cli/routes.js.map +1 -0
- package/dist/cli/server.js +5855 -1860
- package/dist/database/index.js +485 -60
- package/dist/database/index.js.map +1 -1
- package/dist/database/sqlite.js +281 -0
- package/dist/database/sqlite.js.map +1 -0
- package/install.sh +19 -10
- package/package.json +19 -5
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<h1 align="center">OpenQA</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<strong>Autonomous QA Testing Agent - Thinks, codes, and executes tests like a senior QA engineer. Powered by Orka
|
|
8
|
+
<strong>Autonomous QA Testing Agent - Thinks, codes, and executes tests like a senior QA engineer. Powered by Orka Team</strong>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
@@ -50,12 +50,30 @@ OpenQA is a **truly autonomous** QA testing agent that thinks, codes, and execut
|
|
|
50
50
|
|
|
51
51
|
## 🚀 Quick Start
|
|
52
52
|
|
|
53
|
-
###
|
|
53
|
+
### Development (Local)
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
|
+
# One-line installation
|
|
56
57
|
curl -fsSL https://openqa.orkajs.com/install.sh | bash
|
|
58
|
+
|
|
59
|
+
# Or via npm
|
|
60
|
+
npx @openqa/cli start
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Production Deployment
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Interactive production installer
|
|
67
|
+
curl -fsSL https://openqa.orkajs.com/install-production.sh | bash
|
|
57
68
|
```
|
|
58
69
|
|
|
70
|
+
**Supports:**
|
|
71
|
+
- 🐳 **Docker** (recommended)
|
|
72
|
+
- 🖥️ **VPS/Bare Metal** (Ubuntu/Debian with systemd)
|
|
73
|
+
- ☁️ **Cloud Platforms** (Railway, Render, Fly.io)
|
|
74
|
+
|
|
75
|
+
📖 **[Full Deployment Guide](./DEPLOYMENT.md)** - Complete production setup instructions
|
|
76
|
+
|
|
59
77
|
### Configure Your SaaS (3 lines!)
|
|
60
78
|
|
|
61
79
|
```bash
|
|
@@ -105,9 +123,68 @@ openqa start --daemon
|
|
|
105
123
|
|
|
106
124
|
Once started, open your browser:
|
|
107
125
|
|
|
108
|
-
- **
|
|
109
|
-
- **Kanban**: http://localhost:
|
|
110
|
-
- **Config**: http://localhost:
|
|
126
|
+
- **Dashboard**: http://localhost:4242 - Main dashboard with real-time monitoring
|
|
127
|
+
- **Kanban**: http://localhost:4242/kanban - View and manage QA tickets
|
|
128
|
+
- **Config**: http://localhost:4242/config - Configure OpenQA settings
|
|
129
|
+
|
|
130
|
+
### 🔐 Dashboard Authentication
|
|
131
|
+
|
|
132
|
+
OpenQA includes a secure authentication system to protect your dashboard:
|
|
133
|
+
|
|
134
|
+
#### First-Time Setup
|
|
135
|
+
|
|
136
|
+
On first launch, you'll be redirected to `/setup` to create an admin account:
|
|
137
|
+
|
|
138
|
+
1. Visit http://localhost:4242
|
|
139
|
+
2. Create your admin username and password (min 8 characters)
|
|
140
|
+
3. You'll be automatically logged in
|
|
141
|
+
|
|
142
|
+
#### Login
|
|
143
|
+
|
|
144
|
+
After setup, access the dashboard at http://localhost:4242/login with your credentials.
|
|
145
|
+
|
|
146
|
+
#### User Management (Admin Only)
|
|
147
|
+
|
|
148
|
+
Admins can manage users via the API:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# List all users
|
|
152
|
+
curl http://localhost:4242/api/accounts \
|
|
153
|
+
-H "Authorization: Bearer YOUR_TOKEN"
|
|
154
|
+
|
|
155
|
+
# Create a viewer account
|
|
156
|
+
curl -X POST http://localhost:4242/api/accounts \
|
|
157
|
+
-H "Authorization: Bearer YOUR_TOKEN" \
|
|
158
|
+
-H "Content-Type: application/json" \
|
|
159
|
+
-d '{"username": "viewer1", "password": "securepass123", "role": "viewer"}'
|
|
160
|
+
|
|
161
|
+
# Change password
|
|
162
|
+
curl -X POST http://localhost:4242/api/auth/change-password \
|
|
163
|
+
-H "Authorization: Bearer YOUR_TOKEN" \
|
|
164
|
+
-H "Content-Type: application/json" \
|
|
165
|
+
-d '{"currentPassword": "old", "newPassword": "newsecure123"}'
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Roles:**
|
|
169
|
+
- **admin** - Full access (manage users, configure, run tests)
|
|
170
|
+
- **viewer** - Read-only access (view tests, bugs, sessions)
|
|
171
|
+
|
|
172
|
+
**Security Features:**
|
|
173
|
+
- JWT-based authentication with httpOnly cookies
|
|
174
|
+
- Scrypt password hashing
|
|
175
|
+
- Rate limiting on auth endpoints
|
|
176
|
+
- CSRF protection via SameSite cookies
|
|
177
|
+
|
|
178
|
+
#### Disable Authentication (Development Only)
|
|
179
|
+
|
|
180
|
+
For local development, you can disable authentication:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
export OPENQA_AUTH_DISABLED=true
|
|
184
|
+
openqa start
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
⚠️ **Never disable authentication in production!**
|
|
111
188
|
|
|
112
189
|
### CLI Commands
|
|
113
190
|
|
|
@@ -330,7 +407,127 @@ curl -X POST http://localhost:3000/api/brain/analyze
|
|
|
330
407
|
# }
|
|
331
408
|
```
|
|
332
409
|
|
|
333
|
-
|
|
410
|
+
## 🚀 Production Deployment
|
|
411
|
+
|
|
412
|
+
### Quick Deploy (5 minutes)
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
# Interactive installer - Choose Docker, VPS, or Cloud
|
|
416
|
+
curl -fsSL https://openqa.orkajs.com/install-production.sh | bash
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Deployment Options
|
|
420
|
+
|
|
421
|
+
| Method | Time | Difficulty | Best For |
|
|
422
|
+
|--------|------|------------|----------|
|
|
423
|
+
| 🐳 **Docker** | 5 min | Easy | VPS, Local servers |
|
|
424
|
+
| 🖥️ **VPS/Systemd** | 15 min | Medium | Full control |
|
|
425
|
+
| ☁️ **Railway** | 3 min | Easiest | Quick deploy |
|
|
426
|
+
| 🎨 **Render** | 2 min | Easiest | Free tier |
|
|
427
|
+
| 🪰 **Fly.io** | 5 min | Easy | Global edge |
|
|
428
|
+
|
|
429
|
+
### Docker (Recommended)
|
|
430
|
+
|
|
431
|
+
```bash
|
|
432
|
+
# 1. Clone and configure
|
|
433
|
+
git clone https://github.com/Orka-Community/OpenQA.git
|
|
434
|
+
cd OpenQA
|
|
435
|
+
cp .env.production .env
|
|
436
|
+
|
|
437
|
+
# 2. Edit .env - Add your API keys
|
|
438
|
+
nano .env
|
|
439
|
+
# Required: OPENAI_API_KEY, OPENQA_JWT_SECRET, SAAS_URL
|
|
440
|
+
|
|
441
|
+
# 3. Start with Docker Compose
|
|
442
|
+
docker-compose -f docker-compose.production.yml up -d
|
|
443
|
+
|
|
444
|
+
# 4. Access at http://localhost:4242
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
**With HTTPS (Nginx):**
|
|
448
|
+
```bash
|
|
449
|
+
# Update nginx.conf with your domain
|
|
450
|
+
nano nginx.conf
|
|
451
|
+
|
|
452
|
+
# Get SSL certificate
|
|
453
|
+
sudo certbot certonly --standalone -d your-domain.com
|
|
454
|
+
|
|
455
|
+
# Start with Nginx
|
|
456
|
+
docker-compose -f docker-compose.production.yml --profile with-nginx up -d
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### Cloud Platforms
|
|
460
|
+
|
|
461
|
+
**Railway:**
|
|
462
|
+
```bash
|
|
463
|
+
railway init && railway up
|
|
464
|
+
# Set env vars in dashboard: OPENAI_API_KEY, OPENQA_JWT_SECRET, SAAS_URL
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
**Render:**
|
|
468
|
+
- Fork repo → Connect to Render → Auto-deploys with `render.yaml`
|
|
469
|
+
|
|
470
|
+
**Fly.io:**
|
|
471
|
+
```bash
|
|
472
|
+
flyctl launch
|
|
473
|
+
flyctl secrets set OPENAI_API_KEY=sk-xxx OPENQA_JWT_SECRET=$(openssl rand -hex 32)
|
|
474
|
+
flyctl deploy
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### VPS/Bare Metal
|
|
478
|
+
|
|
479
|
+
```bash
|
|
480
|
+
# Automated installer
|
|
481
|
+
curl -fsSL https://openqa.orkajs.com/install-production.sh | bash
|
|
482
|
+
# Choose option 2 (VPS/Bare Metal)
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
**Manual installation:**
|
|
486
|
+
```bash
|
|
487
|
+
# Install Node.js 20
|
|
488
|
+
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
|
489
|
+
sudo apt install -y nodejs build-essential git
|
|
490
|
+
|
|
491
|
+
# Install OpenQA
|
|
492
|
+
sudo useradd -r -m openqa
|
|
493
|
+
sudo -u openqa git clone https://github.com/Orka-Community/OpenQA.git /opt/openqa
|
|
494
|
+
cd /opt/openqa
|
|
495
|
+
sudo -u openqa npm ci --only=production
|
|
496
|
+
sudo -u openqa npm run build
|
|
497
|
+
|
|
498
|
+
# Configure
|
|
499
|
+
sudo -u openqa cp .env.production .env
|
|
500
|
+
sudo nano /opt/openqa/.env
|
|
501
|
+
|
|
502
|
+
# Install systemd service
|
|
503
|
+
sudo cp openqa.service /etc/systemd/system/
|
|
504
|
+
sudo systemctl enable openqa
|
|
505
|
+
sudo systemctl start openqa
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### 🔒 Security Checklist
|
|
509
|
+
|
|
510
|
+
Before going live:
|
|
511
|
+
|
|
512
|
+
- [ ] Set strong `OPENQA_JWT_SECRET` (generate: `openssl rand -hex 32`)
|
|
513
|
+
- [ ] Use strong admin password (min 12 chars)
|
|
514
|
+
- [ ] Enable HTTPS (SSL certificate)
|
|
515
|
+
- [ ] Never set `OPENQA_AUTH_DISABLED=true` in production
|
|
516
|
+
- [ ] Set `NODE_ENV=production`
|
|
517
|
+
- [ ] Restrict CORS origins
|
|
518
|
+
- [ ] Enable firewall (ports 80, 443 only)
|
|
519
|
+
- [ ] Setup automated backups
|
|
520
|
+
|
|
521
|
+
### 📚 Deployment Documentation
|
|
522
|
+
|
|
523
|
+
- **[DEPLOYMENT.md](./DEPLOYMENT.md)** - Complete deployment guide
|
|
524
|
+
- **[QUICKSTART-PRODUCTION.md](./QUICKSTART-PRODUCTION.md)** - 5-minute quick start
|
|
525
|
+
- **[Docker Compose](./docker-compose.production.yml)** - Production configuration
|
|
526
|
+
- **[Systemd Service](./openqa.service)** - Service configuration
|
|
527
|
+
|
|
528
|
+
### Development Deployment
|
|
529
|
+
|
|
530
|
+
For local development only:
|
|
334
531
|
|
|
335
532
|
```bash
|
|
336
533
|
docker-compose up -d
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// agent/brain/diff-analyzer.ts
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import { join, basename, dirname } from "path";
|
|
5
|
+
var DiffAnalyzer = class {
|
|
6
|
+
/**
|
|
7
|
+
* Get files changed between current branch and base branch
|
|
8
|
+
*/
|
|
9
|
+
getChangedFiles(repoPath, baseBranch = "main") {
|
|
10
|
+
try {
|
|
11
|
+
const output = execSync(`git diff --name-only ${baseBranch}...HEAD`, {
|
|
12
|
+
cwd: repoPath,
|
|
13
|
+
stdio: "pipe"
|
|
14
|
+
}).toString().trim();
|
|
15
|
+
if (!output) return [];
|
|
16
|
+
return output.split("\n").filter(Boolean);
|
|
17
|
+
} catch {
|
|
18
|
+
try {
|
|
19
|
+
const output = execSync("git diff --name-only HEAD~1", {
|
|
20
|
+
cwd: repoPath,
|
|
21
|
+
stdio: "pipe"
|
|
22
|
+
}).toString().trim();
|
|
23
|
+
if (!output) return [];
|
|
24
|
+
return output.split("\n").filter(Boolean);
|
|
25
|
+
} catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Map changed source files to their likely test files
|
|
32
|
+
*/
|
|
33
|
+
mapFilesToTests(changedFiles, repoPath) {
|
|
34
|
+
const testFiles = /* @__PURE__ */ new Set();
|
|
35
|
+
for (const file of changedFiles) {
|
|
36
|
+
if (!this.isSourceFile(file)) continue;
|
|
37
|
+
const candidates = this.getTestCandidates(file);
|
|
38
|
+
for (const candidate of candidates) {
|
|
39
|
+
if (existsSync(join(repoPath, candidate))) {
|
|
40
|
+
testFiles.add(candidate);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const file of changedFiles) {
|
|
45
|
+
if (this.isTestFile(file)) {
|
|
46
|
+
testFiles.add(file);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return Array.from(testFiles);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Analyze a diff and return risk assessment + affected tests
|
|
53
|
+
*/
|
|
54
|
+
analyze(repoPath, baseBranch = "main") {
|
|
55
|
+
const changedFiles = this.getChangedFiles(repoPath, baseBranch);
|
|
56
|
+
const affectedTests = this.mapFilesToTests(changedFiles, repoPath);
|
|
57
|
+
const riskLevel = this.assessRisk(changedFiles);
|
|
58
|
+
const summary = this.buildSummary(changedFiles, affectedTests, riskLevel);
|
|
59
|
+
return { changedFiles, affectedTests, riskLevel, summary };
|
|
60
|
+
}
|
|
61
|
+
getTestCandidates(filePath) {
|
|
62
|
+
const dir = dirname(filePath);
|
|
63
|
+
const base = basename(filePath);
|
|
64
|
+
const candidates = [];
|
|
65
|
+
const nameMatch = base.match(/^(.+)\.(tsx?|jsx?|vue|svelte|py|go|rs)$/);
|
|
66
|
+
if (!nameMatch) return candidates;
|
|
67
|
+
const name = nameMatch[1];
|
|
68
|
+
const ext = nameMatch[2];
|
|
69
|
+
const testExts = ext.startsWith("ts") ? ["test.ts", "test.tsx", "spec.ts", "spec.tsx"] : ext.startsWith("js") ? ["test.js", "test.jsx", "spec.js", "spec.jsx"] : ext === "py" ? ["test.py"] : ext === "go" ? ["_test.go"] : [];
|
|
70
|
+
for (const testExt of testExts) {
|
|
71
|
+
candidates.push(join(dir, `${name}.${testExt}`));
|
|
72
|
+
candidates.push(join(dir, "__tests__", `${name}.${testExt}`));
|
|
73
|
+
candidates.push(join(dir, "test", `${name}.${testExt}`));
|
|
74
|
+
candidates.push(join(dir, "tests", `${name}.${testExt}`));
|
|
75
|
+
candidates.push(join("__tests__", dir, `${name}.${testExt}`));
|
|
76
|
+
}
|
|
77
|
+
if (ext === "go") {
|
|
78
|
+
candidates.push(join(dir, `${name}_test.go`));
|
|
79
|
+
}
|
|
80
|
+
return candidates;
|
|
81
|
+
}
|
|
82
|
+
isSourceFile(file) {
|
|
83
|
+
return /\.(tsx?|jsx?|vue|svelte|py|go|rs)$/.test(file) && !this.isTestFile(file);
|
|
84
|
+
}
|
|
85
|
+
isTestFile(file) {
|
|
86
|
+
return /\.(test|spec)\.(tsx?|jsx?|py)$/.test(file) || /_test\.go$/.test(file) || file.includes("__tests__/");
|
|
87
|
+
}
|
|
88
|
+
assessRisk(changedFiles) {
|
|
89
|
+
const highRiskPatterns = [
|
|
90
|
+
/auth/i,
|
|
91
|
+
/security/i,
|
|
92
|
+
/middleware/i,
|
|
93
|
+
/database/i,
|
|
94
|
+
/migration/i,
|
|
95
|
+
/config/i,
|
|
96
|
+
/\.env/,
|
|
97
|
+
/package\.json$/,
|
|
98
|
+
/docker/i,
|
|
99
|
+
/ci\//i,
|
|
100
|
+
/payment/i,
|
|
101
|
+
/billing/i,
|
|
102
|
+
/permission/i
|
|
103
|
+
];
|
|
104
|
+
const mediumRiskPatterns = [
|
|
105
|
+
/api/i,
|
|
106
|
+
/route/i,
|
|
107
|
+
/controller/i,
|
|
108
|
+
/service/i,
|
|
109
|
+
/model/i,
|
|
110
|
+
/hook/i,
|
|
111
|
+
/context/i,
|
|
112
|
+
/store/i,
|
|
113
|
+
/util/i
|
|
114
|
+
];
|
|
115
|
+
let highCount = 0;
|
|
116
|
+
let mediumCount = 0;
|
|
117
|
+
for (const file of changedFiles) {
|
|
118
|
+
if (highRiskPatterns.some((p) => p.test(file))) highCount++;
|
|
119
|
+
else if (mediumRiskPatterns.some((p) => p.test(file))) mediumCount++;
|
|
120
|
+
}
|
|
121
|
+
if (highCount >= 2 || highCount >= 1 && changedFiles.length > 5) return "high";
|
|
122
|
+
if (highCount >= 1 || mediumCount >= 3) return "medium";
|
|
123
|
+
return "low";
|
|
124
|
+
}
|
|
125
|
+
buildSummary(changedFiles, affectedTests, riskLevel) {
|
|
126
|
+
const lines = [];
|
|
127
|
+
lines.push(`${changedFiles.length} file(s) changed, ${affectedTests.length} test(s) affected.`);
|
|
128
|
+
lines.push(`Risk level: ${riskLevel}.`);
|
|
129
|
+
if (affectedTests.length > 0) {
|
|
130
|
+
lines.push(`Run: ${affectedTests.slice(0, 5).join(", ")}${affectedTests.length > 5 ? ` (+${affectedTests.length - 5} more)` : ""}`);
|
|
131
|
+
} else if (changedFiles.length > 0) {
|
|
132
|
+
lines.push("No matching test files found \u2014 consider running full suite.");
|
|
133
|
+
}
|
|
134
|
+
return lines.join(" ");
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
export {
|
|
138
|
+
DiffAnalyzer
|
|
139
|
+
};
|
|
140
|
+
//# sourceMappingURL=diff-analyzer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../agent/brain/diff-analyzer.ts"],"sourcesContent":["import { execSync } from 'child_process';\nimport { existsSync } from 'fs';\nimport { join, basename, dirname } from 'path';\n\nexport interface DiffResult {\n changedFiles: string[];\n affectedTests: string[];\n riskLevel: 'low' | 'medium' | 'high';\n summary: string;\n}\n\nexport class DiffAnalyzer {\n /**\n * Get files changed between current branch and base branch\n */\n getChangedFiles(repoPath: string, baseBranch: string = 'main'): string[] {\n try {\n const output = execSync(`git diff --name-only ${baseBranch}...HEAD`, {\n cwd: repoPath,\n stdio: 'pipe',\n }).toString().trim();\n\n if (!output) return [];\n return output.split('\\n').filter(Boolean);\n } catch {\n // Fallback: diff against HEAD~1 if base branch doesn't exist\n try {\n const output = execSync('git diff --name-only HEAD~1', {\n cwd: repoPath,\n stdio: 'pipe',\n }).toString().trim();\n\n if (!output) return [];\n return output.split('\\n').filter(Boolean);\n } catch {\n return [];\n }\n }\n }\n\n /**\n * Map changed source files to their likely test files\n */\n mapFilesToTests(changedFiles: string[], repoPath: string): string[] {\n const testFiles = new Set<string>();\n\n for (const file of changedFiles) {\n // Skip non-source files\n if (!this.isSourceFile(file)) continue;\n\n const candidates = this.getTestCandidates(file);\n for (const candidate of candidates) {\n if (existsSync(join(repoPath, candidate))) {\n testFiles.add(candidate);\n }\n }\n }\n\n // Also include any changed test files directly\n for (const file of changedFiles) {\n if (this.isTestFile(file)) {\n testFiles.add(file);\n }\n }\n\n return Array.from(testFiles);\n }\n\n /**\n * Analyze a diff and return risk assessment + affected tests\n */\n analyze(repoPath: string, baseBranch: string = 'main'): DiffResult {\n const changedFiles = this.getChangedFiles(repoPath, baseBranch);\n const affectedTests = this.mapFilesToTests(changedFiles, repoPath);\n\n const riskLevel = this.assessRisk(changedFiles);\n\n const summary = this.buildSummary(changedFiles, affectedTests, riskLevel);\n\n return { changedFiles, affectedTests, riskLevel, summary };\n }\n\n private getTestCandidates(filePath: string): string[] {\n const dir = dirname(filePath);\n const base = basename(filePath);\n const candidates: string[] = [];\n\n // Remove extension\n const nameMatch = base.match(/^(.+)\\.(tsx?|jsx?|vue|svelte|py|go|rs)$/);\n if (!nameMatch) return candidates;\n const name = nameMatch[1];\n const ext = nameMatch[2];\n\n // Common test file patterns\n const testExts = ext.startsWith('ts') ? ['test.ts', 'test.tsx', 'spec.ts', 'spec.tsx']\n : ext.startsWith('js') ? ['test.js', 'test.jsx', 'spec.js', 'spec.jsx']\n : ext === 'py' ? ['test.py']\n : ext === 'go' ? ['_test.go']\n : [];\n\n for (const testExt of testExts) {\n // Same directory: Foo.test.ts\n candidates.push(join(dir, `${name}.${testExt}`));\n // __tests__ directory: __tests__/Foo.test.ts\n candidates.push(join(dir, '__tests__', `${name}.${testExt}`));\n // test/ directory: test/Foo.test.ts\n candidates.push(join(dir, 'test', `${name}.${testExt}`));\n // tests/ directory: tests/Foo.test.ts\n candidates.push(join(dir, 'tests', `${name}.${testExt}`));\n // Root __tests__: __tests__/dir/Foo.test.ts\n candidates.push(join('__tests__', dir, `${name}.${testExt}`));\n }\n\n // Go convention: same file with _test suffix\n if (ext === 'go') {\n candidates.push(join(dir, `${name}_test.go`));\n }\n\n return candidates;\n }\n\n private isSourceFile(file: string): boolean {\n return /\\.(tsx?|jsx?|vue|svelte|py|go|rs)$/.test(file) && !this.isTestFile(file);\n }\n\n private isTestFile(file: string): boolean {\n return /\\.(test|spec)\\.(tsx?|jsx?|py)$/.test(file) ||\n /_test\\.go$/.test(file) ||\n file.includes('__tests__/');\n }\n\n private assessRisk(changedFiles: string[]): 'low' | 'medium' | 'high' {\n const highRiskPatterns = [\n /auth/i, /security/i, /middleware/i, /database/i, /migration/i,\n /config/i, /\\.env/, /package\\.json$/, /docker/i, /ci\\//i,\n /payment/i, /billing/i, /permission/i,\n ];\n\n const mediumRiskPatterns = [\n /api/i, /route/i, /controller/i, /service/i, /model/i,\n /hook/i, /context/i, /store/i, /util/i,\n ];\n\n let highCount = 0;\n let mediumCount = 0;\n\n for (const file of changedFiles) {\n if (highRiskPatterns.some(p => p.test(file))) highCount++;\n else if (mediumRiskPatterns.some(p => p.test(file))) mediumCount++;\n }\n\n if (highCount >= 2 || (highCount >= 1 && changedFiles.length > 5)) return 'high';\n if (highCount >= 1 || mediumCount >= 3) return 'medium';\n return 'low';\n }\n\n private buildSummary(changedFiles: string[], affectedTests: string[], riskLevel: string): string {\n const lines: string[] = [];\n lines.push(`${changedFiles.length} file(s) changed, ${affectedTests.length} test(s) affected.`);\n lines.push(`Risk level: ${riskLevel}.`);\n\n if (affectedTests.length > 0) {\n lines.push(`Run: ${affectedTests.slice(0, 5).join(', ')}${affectedTests.length > 5 ? ` (+${affectedTests.length - 5} more)` : ''}`);\n } else if (changedFiles.length > 0) {\n lines.push('No matching test files found — consider running full suite.');\n }\n\n return lines.join(' ');\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// agent/brain/llm-cache.ts
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
var LLMCache = class {
|
|
4
|
+
store = /* @__PURE__ */ new Map();
|
|
5
|
+
ttlMs;
|
|
6
|
+
maxSize;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.ttlMs = options?.ttlMs ?? 36e5;
|
|
9
|
+
this.maxSize = options?.maxSize ?? 500;
|
|
10
|
+
}
|
|
11
|
+
key(prompt) {
|
|
12
|
+
return createHash("sha256").update(prompt).digest("hex");
|
|
13
|
+
}
|
|
14
|
+
get(prompt) {
|
|
15
|
+
const k = this.key(prompt);
|
|
16
|
+
const entry = this.store.get(k);
|
|
17
|
+
if (!entry) return null;
|
|
18
|
+
if (Date.now() > entry.expiresAt) {
|
|
19
|
+
this.store.delete(k);
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return entry.response;
|
|
23
|
+
}
|
|
24
|
+
set(prompt, response) {
|
|
25
|
+
if (this.store.size >= this.maxSize) {
|
|
26
|
+
const oldest = this.store.keys().next().value;
|
|
27
|
+
if (oldest) this.store.delete(oldest);
|
|
28
|
+
}
|
|
29
|
+
this.store.set(this.key(prompt), {
|
|
30
|
+
response,
|
|
31
|
+
expiresAt: Date.now() + this.ttlMs
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
clear() {
|
|
35
|
+
this.store.clear();
|
|
36
|
+
}
|
|
37
|
+
get size() {
|
|
38
|
+
return this.store.size;
|
|
39
|
+
}
|
|
40
|
+
stats() {
|
|
41
|
+
return { size: this.store.size, ttlMs: this.ttlMs, maxSize: this.maxSize };
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
export {
|
|
45
|
+
LLMCache
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=llm-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../agent/brain/llm-cache.ts"],"sourcesContent":["import { createHash } from 'crypto';\n\ninterface CacheEntry {\n response: string;\n expiresAt: number;\n}\n\nexport class LLMCache {\n private store = new Map<string, CacheEntry>();\n private ttlMs: number;\n private maxSize: number;\n\n constructor(options?: { ttlMs?: number; maxSize?: number }) {\n this.ttlMs = options?.ttlMs ?? 3600000; // 1 hour\n this.maxSize = options?.maxSize ?? 500;\n }\n\n private key(prompt: string): string {\n return createHash('sha256').update(prompt).digest('hex');\n }\n\n get(prompt: string): string | null {\n const k = this.key(prompt);\n const entry = this.store.get(k);\n if (!entry) return null;\n if (Date.now() > entry.expiresAt) {\n this.store.delete(k);\n return null;\n }\n return entry.response;\n }\n\n set(prompt: string, response: string): void {\n if (this.store.size >= this.maxSize) {\n // Evict oldest entry\n const oldest = this.store.keys().next().value;\n if (oldest) this.store.delete(oldest);\n }\n this.store.set(this.key(prompt), {\n response,\n expiresAt: Date.now() + this.ttlMs,\n });\n }\n\n clear(): void {\n this.store.clear();\n }\n\n get size(): number {\n return this.store.size;\n }\n\n stats(): { size: number; ttlMs: number; maxSize: number } {\n return { size: this.store.size, ttlMs: this.ttlMs, maxSize: this.maxSize };\n }\n}\n"],"mappings":";AAAA,SAAS,kBAAkB;AAOpB,IAAM,WAAN,MAAe;AAAA,EACZ,QAAQ,oBAAI,IAAwB;AAAA,EACpC;AAAA,EACA;AAAA,EAER,YAAY,SAAgD;AAC1D,SAAK,QAAQ,SAAS,SAAS;AAC/B,SAAK,UAAU,SAAS,WAAW;AAAA,EACrC;AAAA,EAEQ,IAAI,QAAwB;AAClC,WAAO,WAAW,QAAQ,EAAE,OAAO,MAAM,EAAE,OAAO,KAAK;AAAA,EACzD;AAAA,EAEA,IAAI,QAA+B;AACjC,UAAM,IAAI,KAAK,IAAI,MAAM;AACzB,UAAM,QAAQ,KAAK,MAAM,IAAI,CAAC;AAC9B,QAAI,CAAC,MAAO,QAAO;AACnB,QAAI,KAAK,IAAI,IAAI,MAAM,WAAW;AAChC,WAAK,MAAM,OAAO,CAAC;AACnB,aAAO;AAAA,IACT;AACA,WAAO,MAAM;AAAA,EACf;AAAA,EAEA,IAAI,QAAgB,UAAwB;AAC1C,QAAI,KAAK,MAAM,QAAQ,KAAK,SAAS;AAEnC,YAAM,SAAS,KAAK,MAAM,KAAK,EAAE,KAAK,EAAE;AACxC,UAAI,OAAQ,MAAK,MAAM,OAAO,MAAM;AAAA,IACtC;AACA,SAAK,MAAM,IAAI,KAAK,IAAI,MAAM,GAAG;AAAA,MAC/B;AAAA,MACA,WAAW,KAAK,IAAI,IAAI,KAAK;AAAA,IAC/B,CAAC;AAAA,EACH;AAAA,EAEA,QAAc;AACZ,SAAK,MAAM,MAAM;AAAA,EACnB;AAAA,EAEA,IAAI,OAAe;AACjB,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA,EAEA,QAA0D;AACxD,WAAO,EAAE,MAAM,KAAK,MAAM,MAAM,OAAO,KAAK,OAAO,SAAS,KAAK,QAAQ;AAAA,EAC3E;AACF;","names":[]}
|