@litocodes/persona-test 1.0.0 β 1.2.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 +93 -129
- package/index.js +195 -72
- package/package.json +2 -2
- package/personas.js +23 -0
- package/reporter.js +92 -0
- package/reports/REPORT_Agent_Lagos_2026-01-25T22-39-06.md +8 -0
- package/reports/REPORT_Zoomer_Zoe_2026-01-25T22-25-42.md +8 -0
- package/.env +0 -11
package/README.md
CHANGED
|
@@ -1,159 +1,123 @@
|
|
|
1
|
-
#
|
|
1
|
+
# π Persona: The Lagos Test
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Your app works on a MacBook Pro on Fiber. It breaks for the other 5 billion people.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Persona is **Chaos Engineering for UX**. It spawns adversarial AI agents with distinct psychological profiles to attack your app under real-world conditions.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
|
|
11
|
-
β β
|
|
12
|
-
β ββββββββββββ ββββββββββββ ββββββββββββ β
|
|
13
|
-
β β ποΈ EYE βββββΆβ π§ BRAIN βββββΆβ ποΈ HAND β β
|
|
14
|
-
β βPlaywrightβ β OpenRouterβ β Locator β β
|
|
15
|
-
β βScreenshotβ β Vision β β Execute β β
|
|
16
|
-
β ββββββββββββ ββββββββββββ ββββββββββββ β
|
|
17
|
-
β β² β β
|
|
18
|
-
β ββββββββββββββββββββββββββββββββββββ β
|
|
19
|
-
β REPEAT LOOP β
|
|
20
|
-
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
21
|
-
```
|
|
7
|
+
[](https://www.npmjs.com/package/@litocodes/persona-test)
|
|
8
|
+
|
|
9
|
+
---
|
|
22
10
|
|
|
23
|
-
|
|
24
|
-
2. **The Brain**: Screenshot + Persona System Prompt sent to Vision LLM
|
|
25
|
-
3. **The Hand**: LLM returns JSON action with visual element description
|
|
26
|
-
4. **The Locator**: Playwright semantic locators find and interact with elements
|
|
11
|
+
## π₯ The Lagos Test
|
|
27
12
|
|
|
28
|
-
|
|
13
|
+
Silicon Valley builds software for perfect internet. Persona tests for the **real world**:
|
|
29
14
|
|
|
30
15
|
```bash
|
|
31
|
-
#
|
|
32
|
-
|
|
16
|
+
# Test your site as an emerging market user on 3G
|
|
17
|
+
npx @litocodes/persona-test --url="https://your-site.com" --agent="lagos" --network="lagos-3g"
|
|
18
|
+
```
|
|
33
19
|
|
|
34
|
-
|
|
35
|
-
|
|
20
|
+
**What happens:**
|
|
21
|
+
- π 400ms latency, 400 Kbps bandwidth (real Lagos conditions)
|
|
22
|
+
- π± Simulated $50 Android phone behavior
|
|
23
|
+
- π±οΈ Double-clicks (because first click "didn't work")
|
|
24
|
+
- π Aggressive refresh when spinners hang
|
|
25
|
+
- π€ Immediate distrust of popups and data collection
|
|
36
26
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## π The Agents
|
|
40
30
|
|
|
41
|
-
|
|
31
|
+
| Agent | Personality | The Test |
|
|
32
|
+
|-------|-------------|----------|
|
|
33
|
+
| π **Agent Lagos** | Emerging market user on $50 phone, 3G | Survives your bloated JS bundle |
|
|
34
|
+
| π΄ **Grandpa Joe** | 70yo, low tech literacy | Finds the phone number |
|
|
35
|
+
| ποΈ **Zoomer Zoe** | 20yo, rage-clicks, impatient | Signs up in <5 clicks |
|
|
36
|
+
| π΅οΈ **Skeptical Sam** | Privacy paranoid, reads fine print | Finds and rejects cookies |
|
|
37
|
+
| π **Hacker Harry** | Security researcher | SQL injection testing |
|
|
38
|
+
| π§ **Explorer Emma** | Methodical, maps everything | Full site exploration |
|
|
42
39
|
|
|
43
|
-
|
|
40
|
+
---
|
|
44
41
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
## π Network Chaos Modes
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
--network="wifi" # No throttling
|
|
46
|
+
--network="4g" # 4 Mbps, 20ms
|
|
47
|
+
--network="3g" # 1.5 Mbps, 100ms
|
|
48
|
+
--network="lagos-3g" # 400 Kbps, 400ms (Nigerian 3G)
|
|
49
|
+
--network="lagos-tunnel" # 200 Kbps, 800ms (tunnel effect)
|
|
50
|
+
--network="chaos" # 100 Kbps, 1200ms (worst case)
|
|
51
|
+
--network="edge" # 50 Kbps, 800ms (2G)
|
|
48
52
|
```
|
|
49
53
|
|
|
50
|
-
|
|
54
|
+
---
|
|
51
55
|
|
|
52
|
-
## π
|
|
56
|
+
## π Quick Start
|
|
53
57
|
|
|
54
58
|
```bash
|
|
55
|
-
#
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
# With options
|
|
59
|
-
node index.js --url="https://google.com" --agent="boomer" --headless
|
|
59
|
+
# Run instantly
|
|
60
|
+
npx @litocodes/persona-test --url="https://your-site.com" --agent="lagos" --network="lagos-3g"
|
|
60
61
|
|
|
61
|
-
#
|
|
62
|
-
|
|
62
|
+
# Run ALL agents in parallel (Mission Control)
|
|
63
|
+
npx @litocodes/persona-test --url="https://your-site.com" --all
|
|
63
64
|
|
|
64
|
-
#
|
|
65
|
-
|
|
65
|
+
# Set your Cerebras API key (free)
|
|
66
|
+
export CEREBRAS_API_KEY=csk-xxxxx
|
|
66
67
|
```
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
## π₯ Available Personas
|
|
79
|
-
|
|
80
|
-
### π§ Boomer (70yo)
|
|
81
|
-
- Low tech literacy
|
|
82
|
-
- Needs high contrast and large text
|
|
83
|
-
- Quits if frustrated
|
|
84
|
-
- **Goal**: Find the contact phone number
|
|
85
|
-
|
|
86
|
-
### β‘ Zoomer (20yo)
|
|
87
|
-
- Extremely impatient
|
|
88
|
-
- Scrolls fast, ignores instructions
|
|
89
|
-
- Prefers social login
|
|
90
|
-
- **Goal**: Sign up as fast as possible
|
|
91
|
-
|
|
92
|
-
### π Skeptic (40yo)
|
|
93
|
-
- Paranoid about privacy
|
|
94
|
-
- Refuses all cookies
|
|
95
|
-
- Checks Privacy Policy
|
|
96
|
-
- **Goal**: Evaluate site trustworthiness
|
|
97
|
-
|
|
98
|
-
### π» Hacker (30yo)
|
|
99
|
-
- Security tester mindset
|
|
100
|
-
- Probes input fields
|
|
101
|
-
- Tests for SQL injection, XSS
|
|
102
|
-
- **Goal**: Test inputs for vulnerabilities
|
|
103
|
-
|
|
104
|
-
## π How It Works
|
|
105
|
-
|
|
106
|
-
1. Launch Chromium browser and navigate to target URL
|
|
107
|
-
2. Take a compressed JPEG screenshot
|
|
108
|
-
3. Send screenshot + persona prompt to Vision LLM
|
|
109
|
-
4. LLM analyzes the page and returns a JSON action:
|
|
110
|
-
```json
|
|
111
|
-
{
|
|
112
|
-
"action": "click",
|
|
113
|
-
"selector": "text=Sign Up",
|
|
114
|
-
"reason": "Found signup button, clicking to proceed"
|
|
115
|
-
}
|
|
116
|
-
```
|
|
117
|
-
5. Execute the action using Playwright semantic locators
|
|
118
|
-
6. Repeat until goal achieved or max actions reached
|
|
119
|
-
|
|
120
|
-
## π― Action Types
|
|
121
|
-
|
|
122
|
-
| Action | Description | Selector Format |
|
|
123
|
-
|--------|-------------|-----------------|
|
|
124
|
-
| `click` | Click an element | `text=Button Text` |
|
|
125
|
-
| `type` | Type into input | `placeholder=Email` |
|
|
126
|
-
| `scroll` | Scroll the page | `down`, `up`, `to=text` |
|
|
127
|
-
| `done` | Goal achieved | N/A |
|
|
128
|
-
| `quit` | Give up | N/A |
|
|
129
|
-
|
|
130
|
-
## π§ Project Structure
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## π¦ Output
|
|
72
|
+
|
|
73
|
+
Every session produces:
|
|
74
|
+
|
|
75
|
+
1. **π₯ Video Recording** - Watch the AI break your site
|
|
76
|
+
2. **π QA Report** - AI-generated friction analysis
|
|
77
|
+
3. **π Action Log** - Step-by-step decision trail
|
|
131
78
|
|
|
132
79
|
```
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
βββ brain.js # OpenRouter vision client
|
|
136
|
-
βββ personas.js # Persona definitions
|
|
137
|
-
βββ package.json # Dependencies
|
|
138
|
-
βββ .env # API configuration
|
|
139
|
-
βββ README.md # This file
|
|
80
|
+
videos/Agent_Lagos_lagos-3g_2026-01-25_ABANDONED.webm
|
|
81
|
+
reports/REPORT_Agent_Lagos_2026-01-25.md
|
|
140
82
|
```
|
|
141
83
|
|
|
142
|
-
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## π― Why This Exists
|
|
87
|
+
|
|
88
|
+
> "If you pass the Lagos Test, you're ready for the world."
|
|
89
|
+
> "If you fail, you're just a US-only toy."
|
|
90
|
+
|
|
91
|
+
Most testing tools check if buttons **work**.
|
|
92
|
+
Persona checks if buttons are **frustrating**.
|
|
93
|
+
|
|
94
|
+
We test for:
|
|
95
|
+
- **Confusion**, not correctness
|
|
96
|
+
- **Trust**, not just functionality
|
|
97
|
+
- **Global readiness**, not just US/EU
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## π οΈ CLI Options
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
persona --url="<URL>" --agent="<AGENT>" [options]
|
|
105
|
+
|
|
106
|
+
Options:
|
|
107
|
+
--url, -u Target URL (required)
|
|
108
|
+
--agent, -a lagos, boomer, zoomer, skeptic, hacker, explorer
|
|
109
|
+
--all Run ALL agents in parallel
|
|
110
|
+
--network, -n wifi, 4g, 3g, lagos-3g, lagos-tunnel, chaos, edge
|
|
111
|
+
--record Record video (default: true)
|
|
112
|
+
--headless Run without visible browser
|
|
113
|
+
```
|
|
143
114
|
|
|
144
|
-
|
|
145
|
-
- Create a `.env` file with your OpenRouter API key
|
|
115
|
+
---
|
|
146
116
|
|
|
147
|
-
|
|
148
|
-
- The AI's selector may not match exactly
|
|
149
|
-
- The element might not be visible
|
|
150
|
-
- Try running with `--headless=false` to see what's happening
|
|
117
|
+
## π License
|
|
151
118
|
|
|
152
|
-
|
|
153
|
-
- Check your API key
|
|
154
|
-
- Run `node index.js --test` to verify connection
|
|
155
|
-
- Check OpenRouter status
|
|
119
|
+
MIT Β© 2026
|
|
156
120
|
|
|
157
|
-
|
|
121
|
+
---
|
|
158
122
|
|
|
159
|
-
|
|
123
|
+
**Built for the next billion users.** π
|
package/index.js
CHANGED
|
@@ -1,130 +1,253 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// index.js - With
|
|
3
|
+
// index.js - With Parallel Execution & Network Throttling π
|
|
4
4
|
|
|
5
5
|
import { chromium } from 'playwright';
|
|
6
6
|
import minimist from 'minimist';
|
|
7
7
|
import dotenv from 'dotenv';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
8
10
|
import { parsePage, findElementById } from './parser.js';
|
|
9
11
|
import { getAgentAction, testConnection } from './brain.js';
|
|
10
|
-
import { getPersona, listPersonas } from './personas.js';
|
|
12
|
+
import { getPersona, listPersonas, PERSONAS } from './personas.js';
|
|
13
|
+
import { generateReport } from './reporter.js';
|
|
11
14
|
|
|
12
15
|
dotenv.config();
|
|
13
16
|
|
|
17
|
+
// Network throttling presets (simulating real-world conditions)
|
|
18
|
+
const NETWORK_PRESETS = {
|
|
19
|
+
'wifi': null, // No throttling
|
|
20
|
+
'4g': {
|
|
21
|
+
offline: false,
|
|
22
|
+
downloadThroughput: 4 * 1024 * 1024 / 8,
|
|
23
|
+
uploadThroughput: 3 * 1024 * 1024 / 8,
|
|
24
|
+
latency: 20
|
|
25
|
+
},
|
|
26
|
+
'3g': {
|
|
27
|
+
offline: false,
|
|
28
|
+
downloadThroughput: 1.5 * 1024 * 1024 / 8,
|
|
29
|
+
uploadThroughput: 750 * 1024 / 8,
|
|
30
|
+
latency: 100
|
|
31
|
+
},
|
|
32
|
+
'lagos-3g': { // π The Lagos Test - Real emerging market conditions
|
|
33
|
+
offline: false,
|
|
34
|
+
downloadThroughput: 400 * 1024 / 8, // 400 Kbps
|
|
35
|
+
uploadThroughput: 150 * 1024 / 8, // 150 Kbps
|
|
36
|
+
latency: 400 // High latency (the killer)
|
|
37
|
+
},
|
|
38
|
+
'lagos-tunnel': { // π The Tunnel Effect - Connection drops, reconnects
|
|
39
|
+
offline: false,
|
|
40
|
+
downloadThroughput: 200 * 1024 / 8, // 200 Kbps (barely usable)
|
|
41
|
+
uploadThroughput: 50 * 1024 / 8, // 50 Kbps
|
|
42
|
+
latency: 800 // Extreme latency
|
|
43
|
+
},
|
|
44
|
+
'chaos': { // π Chaos Mode - The worst case scenario
|
|
45
|
+
offline: false,
|
|
46
|
+
downloadThroughput: 100 * 1024 / 8, // 100 Kbps
|
|
47
|
+
uploadThroughput: 30 * 1024 / 8, // 30 Kbps
|
|
48
|
+
latency: 1200 // Pain
|
|
49
|
+
},
|
|
50
|
+
'edge': { // 2G/Edge
|
|
51
|
+
offline: false,
|
|
52
|
+
downloadThroughput: 50 * 1024 / 8,
|
|
53
|
+
uploadThroughput: 20 * 1024 / 8,
|
|
54
|
+
latency: 800
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Device simulation presets
|
|
59
|
+
const DEVICE_PRESETS = {
|
|
60
|
+
'macbook': { slowMo: 0 },
|
|
61
|
+
'iphone': { slowMo: 50 },
|
|
62
|
+
'android-mid': { slowMo: 100 },
|
|
63
|
+
'android-budget': { slowMo: 200 }, // $50 phone - noticeable input lag
|
|
64
|
+
'android-chaos': { slowMo: 400 } // $30 phone - painful
|
|
65
|
+
};
|
|
66
|
+
|
|
14
67
|
const argv = minimist(process.argv.slice(2), {
|
|
15
|
-
string: ['url', 'agent'],
|
|
16
|
-
boolean: ['headless', 'test', 'help'],
|
|
17
|
-
default: { headless: false },
|
|
18
|
-
alias: { u: 'url', a: 'agent', h: 'help' }
|
|
68
|
+
string: ['url', 'agent', 'network'],
|
|
69
|
+
boolean: ['headless', 'test', 'help', 'record', 'all'],
|
|
70
|
+
default: { headless: false, record: true, network: 'wifi' },
|
|
71
|
+
alias: { u: 'url', a: 'agent', h: 'help', r: 'record', n: 'network' }
|
|
19
72
|
});
|
|
20
73
|
|
|
21
74
|
if (argv.help) {
|
|
22
75
|
console.log(`
|
|
23
|
-
π PERSONA - AI User Testing (
|
|
24
|
-
|
|
25
|
-
Usage:
|
|
26
|
-
|
|
76
|
+
π PERSONA - AI User Testing (Parallel + Network)
|
|
77
|
+
|
|
78
|
+
Usage:
|
|
79
|
+
persona --url="<site>" --agent="<persona>"
|
|
80
|
+
persona --url="<site>" --all # Run ALL agents in parallel!
|
|
81
|
+
|
|
82
|
+
Options:
|
|
83
|
+
--url, -u Target URL (required)
|
|
84
|
+
--agent, -a Single persona: ${listPersonas().join(', ')}
|
|
85
|
+
--all Run ALL personas in parallel (Mission Control mode)
|
|
86
|
+
--network, -n Network: wifi, 4g, 3g, lagos-3g, edge
|
|
87
|
+
--record, -r Record video (default: true)
|
|
88
|
+
--headless Run headless
|
|
89
|
+
--test Test API connection
|
|
90
|
+
|
|
91
|
+
Examples:
|
|
92
|
+
persona --url="https://example.com" --agent="zoomer" --network="lagos-3g"
|
|
93
|
+
persona --url="https://example.com" --all # 5 browsers at once!
|
|
27
94
|
`);
|
|
28
95
|
process.exit(0);
|
|
29
96
|
}
|
|
30
97
|
|
|
31
98
|
if (argv.test) { await testConnection(); process.exit(0); }
|
|
32
|
-
if (!argv.url
|
|
99
|
+
if (!argv.url) { console.error('β --url required'); process.exit(1); }
|
|
100
|
+
if (!argv.agent && !argv.all) { console.error('β --agent or --all required'); process.exit(1); }
|
|
33
101
|
|
|
34
|
-
|
|
35
|
-
if (!
|
|
102
|
+
// Ensure folders exist
|
|
103
|
+
if (!fs.existsSync('videos')) fs.mkdirSync('videos');
|
|
36
104
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
console.log(`π― ${argv.url}`);
|
|
44
|
-
console.log(`π€ ${persona.name} | Goal: ${persona.goal}\n`);
|
|
105
|
+
/**
|
|
106
|
+
* Run a single agent session
|
|
107
|
+
*/
|
|
108
|
+
async function runAgent(url, persona, options = {}) {
|
|
109
|
+
const { network = 'wifi', record = true, headless = false } = options;
|
|
110
|
+
const networkConfig = NETWORK_PRESETS[network];
|
|
45
111
|
|
|
46
|
-
|
|
47
|
-
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
|
|
112
|
+
console.log(`\nπ [${persona.name}] Starting... (Network: ${network})`);
|
|
48
113
|
|
|
49
|
-
await
|
|
50
|
-
await page.waitForTimeout(2000);
|
|
51
|
-
console.log('β
Page loaded\n');
|
|
114
|
+
const browser = await chromium.launch({ headless });
|
|
52
115
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
116
|
+
const contextOptions = {
|
|
117
|
+
viewport: { width: 1280, height: 800 },
|
|
118
|
+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0'
|
|
119
|
+
};
|
|
56
120
|
|
|
57
|
-
|
|
58
|
-
|
|
121
|
+
if (record) {
|
|
122
|
+
contextOptions.recordVideo = { dir: 'videos/', size: { width: 1280, height: 800 } };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const context = await browser.newContext(contextOptions);
|
|
126
|
+
const page = await context.newPage();
|
|
59
127
|
|
|
128
|
+
// π Apply network throttling via CDP
|
|
129
|
+
if (networkConfig) {
|
|
130
|
+
const client = await context.newCDPSession(page);
|
|
131
|
+
await client.send('Network.emulateNetworkConditions', networkConfig);
|
|
132
|
+
console.log(` πΆ [${persona.name}] Network throttled to ${network}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
137
|
+
await page.waitForTimeout(2000);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
console.log(` β [${persona.name}] Failed to load: ${e.message.substring(0, 50)}`);
|
|
140
|
+
await browser.close();
|
|
141
|
+
return { agent: persona.name, outcome: 'LOAD_FAILED' };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const actionHistory = [];
|
|
145
|
+
const failedIds = [];
|
|
146
|
+
let outcome = 'MAX_ACTIONS';
|
|
147
|
+
|
|
148
|
+
for (let step = 1; step <= persona.maxActions; step++) {
|
|
60
149
|
const html = await page.content();
|
|
61
150
|
const { domMap, elements, elementCount } = parsePage(html, failedIds);
|
|
62
|
-
console.log(` π ${elementCount} elements (${failedIds.length} blacklisted)`);
|
|
63
151
|
|
|
64
|
-
if (elementCount === 0) {
|
|
65
|
-
console.log(' β οΈ No elements');
|
|
66
|
-
await page.mouse.wheel(0, 300);
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
152
|
+
if (elementCount === 0) { await page.mouse.wheel(0, 300); continue; }
|
|
69
153
|
|
|
70
|
-
// Pass full history to brain
|
|
71
154
|
const decision = await getAgentAction(domMap, persona, actionHistory);
|
|
72
155
|
|
|
73
|
-
console.log(` π€ ${decision.action
|
|
74
|
-
console.log(` π ${decision.reason}`);
|
|
75
|
-
console.log(` π€ Frustration: ${decision.frustration}/10`);
|
|
156
|
+
console.log(` π€ [${persona.name}] Step ${step}: ${decision.action} [${decision.elementId}] - ${decision.reason?.substring(0, 40)}`);
|
|
76
157
|
|
|
77
|
-
if (decision.action === 'done'
|
|
78
|
-
|
|
79
|
-
console.log(decision.action === 'done' ? ' π GOAL ACHIEVED!' : ' π€ GAVE UP');
|
|
80
|
-
console.log(` ${decision.reason}`);
|
|
81
|
-
console.log(`${'β'.repeat(60)}`);
|
|
82
|
-
break;
|
|
83
|
-
}
|
|
158
|
+
if (decision.action === 'done') { outcome = 'SUCCESS'; break; }
|
|
159
|
+
if (decision.action === 'quit') { outcome = 'ABANDONED'; break; }
|
|
84
160
|
|
|
85
161
|
const target = decision.elementId ? findElementById(elements, decision.elementId) : null;
|
|
86
|
-
|
|
87
|
-
if (!target) {
|
|
88
|
-
console.log(' β Invalid ID');
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
162
|
+
if (!target) continue;
|
|
91
163
|
|
|
92
164
|
try {
|
|
93
165
|
const locator = page.locator(target.selector).first();
|
|
166
|
+
await locator.highlight();
|
|
167
|
+
await page.waitForTimeout(200);
|
|
94
168
|
|
|
95
169
|
if (decision.action === 'click') {
|
|
96
170
|
await locator.click({ timeout: 4000 });
|
|
97
|
-
console.log(` π±οΈ Clicked: "${target.text}"`);
|
|
98
171
|
} else if (decision.action === 'type') {
|
|
99
|
-
|
|
100
|
-
await locator.fill(text, { timeout: 4000 });
|
|
101
|
-
console.log(` β¨οΈ Typed: "${text}"`);
|
|
102
|
-
} else if (decision.action === 'scroll') {
|
|
103
|
-
await page.mouse.wheel(0, 400);
|
|
104
|
-
console.log(' π Scrolled');
|
|
172
|
+
await locator.fill(decision.inputText || persona.testStrings?.sql || 'test', { timeout: 4000 });
|
|
105
173
|
}
|
|
106
174
|
|
|
107
|
-
// β
Add to history (keep last 3)
|
|
108
175
|
actionHistory.push({ action: decision.action, id: decision.elementId, text: target.text });
|
|
109
176
|
if (actionHistory.length > 3) actionHistory.shift();
|
|
110
177
|
|
|
111
178
|
} catch (e) {
|
|
112
|
-
const msg = e.message.split('\n')[0].substring(0, 50);
|
|
113
|
-
console.log(` β οΈ FAILED: ${msg}`);
|
|
114
|
-
|
|
115
|
-
// π¨ Add to blacklist
|
|
116
179
|
failedIds.push(decision.elementId);
|
|
117
|
-
console.log(` π« ID [${decision.elementId}] blacklisted`);
|
|
118
180
|
}
|
|
119
181
|
|
|
120
|
-
await page.waitForTimeout(
|
|
121
|
-
console.log(`β${'β'.repeat(58)}β`);
|
|
182
|
+
await page.waitForTimeout(1500);
|
|
122
183
|
}
|
|
123
184
|
|
|
124
|
-
|
|
125
|
-
|
|
185
|
+
// Save video
|
|
186
|
+
const videoPath = record ? await page.video()?.path() : null;
|
|
187
|
+
await context.close();
|
|
126
188
|
await browser.close();
|
|
127
|
-
|
|
189
|
+
|
|
190
|
+
if (record && videoPath && fs.existsSync(videoPath)) {
|
|
191
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
|
|
192
|
+
const filename = `${persona.name.replace(/\s+/g, '_')}_${network}_${timestamp}_${outcome}.webm`;
|
|
193
|
+
fs.renameSync(videoPath, path.join('videos', filename));
|
|
194
|
+
console.log(` π₯ [${persona.name}] Saved: videos/${filename}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// π Generate QA Report
|
|
198
|
+
await generateReport(persona, url, actionHistory, failedIds, outcome, network);
|
|
199
|
+
|
|
200
|
+
console.log(` β
[${persona.name}] Finished: ${outcome}`);
|
|
201
|
+
return { agent: persona.name, outcome, network };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Main entry point
|
|
206
|
+
*/
|
|
207
|
+
async function main() {
|
|
208
|
+
console.log(`
|
|
209
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
210
|
+
β π PERSONA - AI User Testing (Parallel + Network) β
|
|
211
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
212
|
+
`);
|
|
213
|
+
console.log(`π― Target: ${argv.url}`);
|
|
214
|
+
console.log(`πΆ Network: ${argv.network}`);
|
|
215
|
+
|
|
216
|
+
const options = {
|
|
217
|
+
network: argv.network,
|
|
218
|
+
record: argv.record,
|
|
219
|
+
headless: argv.headless
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
if (argv.all) {
|
|
223
|
+
// π PARALLEL MODE: Run ALL agents at once!
|
|
224
|
+
console.log(`\nπ₯ MISSION CONTROL: Launching ALL ${listPersonas().length} agents in parallel!\n`);
|
|
225
|
+
|
|
226
|
+
const agents = listPersonas().map(name => getPersona(name));
|
|
227
|
+
|
|
228
|
+
const results = await Promise.all(
|
|
229
|
+
agents.map(persona => runAgent(argv.url, persona, options))
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
// Summary
|
|
233
|
+
console.log(`\n${'β'.repeat(60)}`);
|
|
234
|
+
console.log('π MISSION REPORT');
|
|
235
|
+
console.log('β'.repeat(60));
|
|
236
|
+
results.forEach(r => {
|
|
237
|
+
const icon = r.outcome === 'SUCCESS' ? 'β
' : r.outcome === 'ABANDONED' ? 'π€' : 'β±οΈ';
|
|
238
|
+
console.log(` ${icon} ${r.agent}: ${r.outcome} (${r.network})`);
|
|
239
|
+
});
|
|
240
|
+
console.log('β'.repeat(60));
|
|
241
|
+
|
|
242
|
+
} else {
|
|
243
|
+
// Single agent mode
|
|
244
|
+
const persona = getPersona(argv.agent);
|
|
245
|
+
if (!persona) { console.error(`β Unknown: ${argv.agent}`); process.exit(1); }
|
|
246
|
+
|
|
247
|
+
await runAgent(argv.url, persona, options);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
console.log('\nβ
All done!\n');
|
|
128
251
|
}
|
|
129
252
|
|
|
130
253
|
main().catch(e => console.error('π', e));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@litocodes/persona-test",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "AI User Testing with Personality - Simulate real users breaking your website",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -39,4 +39,4 @@
|
|
|
39
39
|
"openai": "^4.77.0",
|
|
40
40
|
"playwright": "^1.49.1"
|
|
41
41
|
}
|
|
42
|
-
}
|
|
42
|
+
}
|
package/personas.js
CHANGED
|
@@ -83,6 +83,29 @@ Creates a mental map of the site.`,
|
|
|
83
83
|
goal: "Explore the entire site and understand its structure.",
|
|
84
84
|
maxActions: 20,
|
|
85
85
|
frustrationThreshold: 15
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
lagos: {
|
|
89
|
+
name: "Agent Lagos",
|
|
90
|
+
age: 28,
|
|
91
|
+
behavior: `Emerging market user on a $50 Android phone with unstable 3G.
|
|
92
|
+
DOUBLE-CLICKS everything because the first click "didn't work."
|
|
93
|
+
Refreshes aggressively if any spinner shows for >2 seconds.
|
|
94
|
+
Deeply suspicious of sites asking for personal info - "Is this a scam?"
|
|
95
|
+
Expects things to be SLOW but gets frustrated when they freeze completely.
|
|
96
|
+
Closes modals immediately - "I don't trust popups."
|
|
97
|
+
Looks for WhatsApp contact links instead of email forms.
|
|
98
|
+
Types slowly, one finger, makes typos.
|
|
99
|
+
If the page goes white/blank, assumes internet is down and quits.
|
|
100
|
+
Prefers simple text over fancy animations (they don't load).`,
|
|
101
|
+
goal: "Complete a basic task (signup/purchase) despite terrible network and device.",
|
|
102
|
+
maxActions: 12,
|
|
103
|
+
frustrationThreshold: 4,
|
|
104
|
+
testStrings: {
|
|
105
|
+
email: "user1234@gmail.com",
|
|
106
|
+
phone: "+234801234567",
|
|
107
|
+
name: "Chidi Okonkwo"
|
|
108
|
+
}
|
|
86
109
|
}
|
|
87
110
|
};
|
|
88
111
|
|
package/reporter.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// reporter.js - AI-Generated QA Reports
|
|
2
|
+
// Uses Cerebras to write professional test reports from session data
|
|
3
|
+
|
|
4
|
+
import OpenAI from 'openai';
|
|
5
|
+
import dotenv from 'dotenv';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
|
|
9
|
+
dotenv.config();
|
|
10
|
+
|
|
11
|
+
const openai = new OpenAI({
|
|
12
|
+
baseURL: 'https://api.cerebras.ai/v1',
|
|
13
|
+
apiKey: process.env.CEREBRAS_API_KEY
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const MODEL = process.env.CEREBRAS_MODEL || 'llama-3.3-70b';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate a markdown report from test session data
|
|
20
|
+
*/
|
|
21
|
+
export async function generateReport(agent, url, actionHistory, failedIds, outcome, network) {
|
|
22
|
+
console.log('\nπ Generating QA Report via Cerebras...');
|
|
23
|
+
|
|
24
|
+
const historyStr = actionHistory.length > 0
|
|
25
|
+
? actionHistory.map((h, i) => `${i + 1}. ${h.action.toUpperCase()} [${h.id}] "${h.text}"`).join('\n')
|
|
26
|
+
: 'No actions recorded';
|
|
27
|
+
|
|
28
|
+
const prompt = `You are a Senior QA Engineer & UX Specialist.
|
|
29
|
+
You just watched an AI Agent test a website. Write a professional but personality-driven report.
|
|
30
|
+
|
|
31
|
+
--- AGENT PROFILE ---
|
|
32
|
+
Name: ${agent.name}
|
|
33
|
+
Age: ${agent.age}
|
|
34
|
+
Traits: ${agent.behavior}
|
|
35
|
+
Goal: ${agent.goal}
|
|
36
|
+
|
|
37
|
+
--- TEST SESSION ---
|
|
38
|
+
URL: ${url}
|
|
39
|
+
Network: ${network}
|
|
40
|
+
Outcome: ${outcome}
|
|
41
|
+
Failed Elements: ${failedIds.length > 0 ? failedIds.join(', ') : 'None'}
|
|
42
|
+
|
|
43
|
+
Action Log:
|
|
44
|
+
${historyStr}
|
|
45
|
+
|
|
46
|
+
--- WRITE A MARKDOWN REPORT ---
|
|
47
|
+
Include:
|
|
48
|
+
1. **Result**: Pass/Fail with emoji
|
|
49
|
+
2. **Summary**: 2-3 sentences describing what happened
|
|
50
|
+
3. **Friction Points**: Where did the agent struggle? (loops, failed clicks, confusion)
|
|
51
|
+
4. **Recommendations**: 1-2 specific technical fixes (e.g., "Add loading state", "Fix aria-labels")
|
|
52
|
+
5. **Persona Quote**: A funny one-liner in character (e.g., Zoomer: "ugh why is this so slow")
|
|
53
|
+
|
|
54
|
+
Keep it concise. Sound like the persona where appropriate.`;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const response = await openai.chat.completions.create({
|
|
58
|
+
model: MODEL,
|
|
59
|
+
messages: [
|
|
60
|
+
{ role: 'system', content: 'You are an expert QA reporter who writes concise, actionable test reports.' },
|
|
61
|
+
{ role: 'user', content: prompt }
|
|
62
|
+
],
|
|
63
|
+
max_tokens: 500,
|
|
64
|
+
temperature: 0.7
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const reportText = response.choices[0]?.message?.content || 'Report generation failed';
|
|
68
|
+
|
|
69
|
+
// Save to file
|
|
70
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
|
|
71
|
+
const agentName = agent.name.replace(/\s+/g, '_');
|
|
72
|
+
const filename = `REPORT_${agentName}_${timestamp}.md`;
|
|
73
|
+
|
|
74
|
+
if (!fs.existsSync('reports')) fs.mkdirSync('reports');
|
|
75
|
+
|
|
76
|
+
const filepath = path.join('reports', filename);
|
|
77
|
+
fs.writeFileSync(filepath, `# QA Report: ${agent.name}\n\n${reportText}`);
|
|
78
|
+
|
|
79
|
+
console.log(`β
Report saved: ${filepath}`);
|
|
80
|
+
console.log(`\n${'β'.repeat(50)}`);
|
|
81
|
+
console.log(reportText.substring(0, 400) + (reportText.length > 400 ? '...' : ''));
|
|
82
|
+
console.log('β'.repeat(50));
|
|
83
|
+
|
|
84
|
+
return filepath;
|
|
85
|
+
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error('β Report generation failed:', error.message);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export default { generateReport };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# QA Report: Agent Lagos
|
|
2
|
+
|
|
3
|
+
### Test Report: Agent Lagos on Stripe.com
|
|
4
|
+
#### **Result**: π« Fail
|
|
5
|
+
#### **Summary**: Agent Lagos attempted to complete a basic task on Stripe.com but encountered significant friction due to the site's behavior on a slow network and low-end device. The agent's interactions were marred by failed clicks, aggressive refreshing, and a general distrust of the site's requests for personal information. Despite persistence, the agent ultimately failed to complete the task.
|
|
6
|
+
#### **Friction Points**: The agent struggled with double-clicking on elements that didn't respond immediately, refreshing the page when spinners appeared for more than 2 seconds, and being suspicious of popups and personal info requests. Specifically, elements 2, 1, 182, and 15 caused issues.
|
|
7
|
+
#### **Recommendations**: To improve the user experience for agents like Lagos, I recommend adding a clear loading state to indicate when the site is processing requests, and fixing aria-labels to improve accessibility and reduce confusion.
|
|
8
|
+
#### **Persona Quote**: "Is this a scam? Why it no work?!"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# QA Report: Zoomer Zoe
|
|
2
|
+
|
|
3
|
+
### Test Report: Zoomer Zoe on Hacker News
|
|
4
|
+
#### **Result**: π« Fail
|
|
5
|
+
#### **Summary**: Zoomer Zoe attempted to login to Hacker News, but ended up in a password reset loop. The agent clicked "login", then "Forgot your password?", and finally "Send reset email" without completing the intended sign-up process. This behavior indicates frustration with the login process.
|
|
6
|
+
#### **Friction Points**: The agent struggled with the login form, ignoring instructions and getting stuck in a loop. The presence of a "Forgot your password?" link before even attempting to login suggests the agent was already looking for an escape route.
|
|
7
|
+
#### **Recommendations**: Add a clear "Sign up" or "Create account" button on the initial page to streamline the onboarding process. Consider simplifying the login form to reduce friction.
|
|
8
|
+
#### **Persona Quote**: Zoomer Zoe: "Ugh, can't I just use Google to login already?!"
|
package/.env
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
# Cerebras API Configuration
|
|
2
|
-
# Get your API key from: https://cloud.cerebras.ai/
|
|
3
|
-
|
|
4
|
-
# Your Cerebras API Key (required)
|
|
5
|
-
CEREBRAS_API_KEY=csk-fptmf5m4hxn84c3jx95yw9tce2v6yxkhpjkwjpkxd88cmrjp
|
|
6
|
-
|
|
7
|
-
# Model to use (Cerebras models - very fast!)
|
|
8
|
-
# Options:
|
|
9
|
-
# - llama-3.3-70b (recommended - smart and fast)
|
|
10
|
-
# - llama-3.1-8b (lightweight, fastest)
|
|
11
|
-
CEREBRAS_MODEL=llama-3.3-70b
|