loki-mode 5.42.2 → 5.46.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.
@@ -0,0 +1,350 @@
1
+ #!/bin/bash
2
+ #===============================================================================
3
+ # Playwright Smoke Test Module (v5.46.0)
4
+ #
5
+ # Runs lightweight smoke tests against a running user application to verify
6
+ # it loads correctly. Advisory only - failures do NOT block iterations or
7
+ # council approval.
8
+ #
9
+ # Functions:
10
+ # playwright_verify_init() - Check/install Playwright
11
+ # playwright_verify_app(url) - Run smoke test against URL
12
+ # playwright_verify_should_run() - Check if verification should run
13
+ # playwright_verify_summary() - One-line summary for prompt injection
14
+ # playwright_verify_as_evidence() - Formatted output for council evidence
15
+ #
16
+ # Environment Variables:
17
+ # LOKI_PLAYWRIGHT_ENABLED - Enable/disable (default: true)
18
+ # LOKI_PLAYWRIGHT_INTERVAL - Run every N iterations (default: 5)
19
+ # LOKI_PLAYWRIGHT_TIMEOUT - Page load timeout in ms (default: 15000)
20
+ #
21
+ # Data:
22
+ # .loki/verification/playwright-results.json - Last results
23
+ # .loki/verification/screenshots/ - Captured screenshots
24
+ #
25
+ #===============================================================================
26
+
27
+ # Configuration
28
+ PLAYWRIGHT_ENABLED=${LOKI_PLAYWRIGHT_ENABLED:-true}
29
+ PLAYWRIGHT_INTERVAL=${LOKI_PLAYWRIGHT_INTERVAL:-5}
30
+ # Guard against zero/negative interval (division by zero in modulo)
31
+ if [ "$PLAYWRIGHT_INTERVAL" -le 0 ] 2>/dev/null; then
32
+ PLAYWRIGHT_INTERVAL=5
33
+ fi
34
+ PLAYWRIGHT_TIMEOUT=${LOKI_PLAYWRIGHT_TIMEOUT:-15000}
35
+ # Derive seconds for the outer timeout wrapper (add buffer for browser startup)
36
+ PLAYWRIGHT_TIMEOUT_SEC=$(( (PLAYWRIGHT_TIMEOUT / 1000) + 15 ))
37
+
38
+ # Internal state
39
+ PLAYWRIGHT_VERIFY_DIR=""
40
+ PLAYWRIGHT_RESULTS_FILE=""
41
+ PLAYWRIGHT_LAST_VERIFY_ITERATION=0
42
+ PLAYWRIGHT_AVAILABLE=false
43
+ PLAYWRIGHT_MAX_SCREENSHOTS=10
44
+
45
+ #===============================================================================
46
+ # Initialization
47
+ #===============================================================================
48
+
49
+ playwright_verify_init() {
50
+ # Check if npx playwright is available; attempt chromium install if needed.
51
+ # Returns 0 if playwright available, 1 if not.
52
+
53
+ if [ "$PLAYWRIGHT_ENABLED" != "true" ]; then
54
+ return 1
55
+ fi
56
+
57
+ PLAYWRIGHT_VERIFY_DIR=".loki/verification"
58
+ PLAYWRIGHT_RESULTS_FILE="${PLAYWRIGHT_VERIFY_DIR}/playwright-results.json"
59
+ mkdir -p "${PLAYWRIGHT_VERIFY_DIR}/screenshots"
60
+
61
+ # Check if playwright is accessible via npx
62
+ if ! npx playwright --version &>/dev/null; then
63
+ log_warn "Playwright not found via npx - smoke tests disabled"
64
+ PLAYWRIGHT_AVAILABLE=false
65
+ return 1
66
+ fi
67
+
68
+ # Check if chromium browser is installed; attempt install if not
69
+ if ! npx playwright install --dry-run chromium &>/dev/null; then
70
+ log_info "Installing Playwright chromium browser..."
71
+ if ! timeout 60 npx playwright install chromium &>/dev/null; then
72
+ log_warn "Failed to install Playwright chromium - smoke tests disabled"
73
+ PLAYWRIGHT_AVAILABLE=false
74
+ return 1
75
+ fi
76
+ fi
77
+
78
+ PLAYWRIGHT_AVAILABLE=true
79
+ log_info "Playwright smoke tests initialized (every ${PLAYWRIGHT_INTERVAL} iterations, ${PLAYWRIGHT_TIMEOUT}ms timeout)"
80
+ return 0
81
+ }
82
+
83
+ #===============================================================================
84
+ # Interval Control
85
+ #===============================================================================
86
+
87
+ playwright_verify_should_run() {
88
+ # Returns 0 (true) if verification should run this iteration
89
+ if [ "$PLAYWRIGHT_ENABLED" != "true" ]; then
90
+ return 1
91
+ fi
92
+
93
+ if [ "$PLAYWRIGHT_AVAILABLE" != "true" ]; then
94
+ return 1
95
+ fi
96
+
97
+ local current_iteration="${ITERATION_COUNT:-0}"
98
+ if [ "$current_iteration" -eq 0 ]; then
99
+ return 1
100
+ fi
101
+
102
+ if [ $((current_iteration % PLAYWRIGHT_INTERVAL)) -ne 0 ]; then
103
+ return 1
104
+ fi
105
+
106
+ # Don't verify same iteration twice
107
+ if [ "$current_iteration" -eq "$PLAYWRIGHT_LAST_VERIFY_ITERATION" ]; then
108
+ return 1
109
+ fi
110
+
111
+ return 0
112
+ }
113
+
114
+ #===============================================================================
115
+ # Smoke Test
116
+ #===============================================================================
117
+
118
+ playwright_verify_app() {
119
+ local url="$1"
120
+
121
+ if [ -z "$url" ]; then
122
+ log_warn "playwright_verify_app: no URL provided"
123
+ return 0
124
+ fi
125
+
126
+ local verify_dir="${PLAYWRIGHT_VERIFY_DIR:-.loki/verification}"
127
+ local screenshots_dir="${verify_dir}/screenshots"
128
+ mkdir -p "$screenshots_dir"
129
+
130
+ local timestamp
131
+ timestamp=$(date -u +%Y-%m-%dT%H%M%SZ)
132
+ local screenshot_path="${screenshots_dir}/verify-${timestamp}.png"
133
+ local results_file="${verify_dir}/playwright-results.json"
134
+ local script_file="${verify_dir}/.smoke-test.js"
135
+
136
+ log_step "Running Playwright smoke test against ${url}..."
137
+
138
+ # Generate inline smoke test script
139
+ cat > "$script_file" << 'SMOKE_SCRIPT'
140
+ const { chromium } = require('playwright');
141
+
142
+ (async () => {
143
+ const url = process.argv[2];
144
+ const screenshotPath = process.argv[3];
145
+ const resultsPath = process.argv[4];
146
+ const pageTimeout = parseInt(process.argv[5] || '15000', 10);
147
+ const startTime = Date.now();
148
+
149
+ const results = {
150
+ verified_at: new Date().toISOString(),
151
+ url: url,
152
+ passed: false,
153
+ checks: {
154
+ page_loads: false,
155
+ no_5xx: false,
156
+ no_console_errors: true,
157
+ has_title: false,
158
+ has_content: false
159
+ },
160
+ screenshot: screenshotPath,
161
+ errors: [],
162
+ duration_ms: 0
163
+ };
164
+
165
+ let browser;
166
+ try {
167
+ browser = await chromium.launch({ headless: true });
168
+ const page = await browser.newPage();
169
+
170
+ // Collect console errors
171
+ const consoleErrors = [];
172
+ page.on('console', msg => {
173
+ if (msg.type() === 'error') consoleErrors.push(msg.text());
174
+ });
175
+
176
+ // Navigate to URL
177
+ const response = await page.goto(url, {
178
+ waitUntil: 'domcontentloaded',
179
+ timeout: pageTimeout
180
+ });
181
+ results.checks.page_loads = true;
182
+
183
+ // Check HTTP status (no 5xx)
184
+ const status = response ? response.status() : 0;
185
+ results.checks.no_5xx = status < 500;
186
+ if (status >= 500) results.errors.push('HTTP ' + status);
187
+
188
+ // Check console errors
189
+ if (consoleErrors.length > 0) {
190
+ results.checks.no_console_errors = false;
191
+ results.errors.push(...consoleErrors.slice(0, 5));
192
+ }
193
+
194
+ // Check page title
195
+ const title = await page.title();
196
+ results.checks.has_title = title.length > 0;
197
+
198
+ // Check visible content
199
+ const bodyText = await page.evaluate(() => document.body?.innerText?.trim() || '');
200
+ results.checks.has_content = bodyText.length > 0;
201
+
202
+ // Capture screenshot
203
+ await page.screenshot({ path: screenshotPath, fullPage: false });
204
+
205
+ // Determine overall pass
206
+ results.passed = Object.values(results.checks).every(v => v === true);
207
+
208
+ } catch (err) {
209
+ results.errors.push(err.message);
210
+ } finally {
211
+ if (browser) await browser.close();
212
+ results.duration_ms = Date.now() - startTime;
213
+
214
+ // Atomic write: temp file then rename
215
+ const fs = require('fs');
216
+ const tmp = resultsPath + '.tmp';
217
+ fs.writeFileSync(tmp, JSON.stringify(results, null, 2));
218
+ fs.renameSync(tmp, resultsPath);
219
+ }
220
+
221
+ process.exit(results.passed ? 0 : 1);
222
+ })();
223
+ SMOKE_SCRIPT
224
+
225
+ # Run with outer timeout (never block iteration)
226
+ timeout "${PLAYWRIGHT_TIMEOUT_SEC}" node "$script_file" \
227
+ "$url" "$screenshot_path" "$results_file" "$PLAYWRIGHT_TIMEOUT" 2>/dev/null
228
+ local exit_code=$?
229
+
230
+ # Clean up generated script
231
+ rm -f "$script_file"
232
+
233
+ # Update tracking state
234
+ PLAYWRIGHT_LAST_VERIFY_ITERATION="${ITERATION_COUNT:-0}"
235
+
236
+ # Rotate screenshots: keep only the most recent N
237
+ _playwright_rotate_screenshots "$screenshots_dir"
238
+
239
+ # Log result
240
+ if [ -f "$results_file" ]; then
241
+ local summary
242
+ summary=$(playwright_verify_summary 2>/dev/null || true)
243
+ if [ -n "$summary" ]; then
244
+ log_info "Playwright: $summary"
245
+ fi
246
+ elif [ "$exit_code" -eq 124 ]; then
247
+ log_warn "Playwright smoke test timed out after ${PLAYWRIGHT_TIMEOUT_SEC}s"
248
+ else
249
+ log_warn "Playwright smoke test failed (exit $exit_code)"
250
+ fi
251
+
252
+ # Never fail the iteration
253
+ return 0
254
+ }
255
+
256
+ #===============================================================================
257
+ # Screenshot Rotation
258
+ #===============================================================================
259
+
260
+ _playwright_rotate_screenshots() {
261
+ local dir="$1"
262
+ local max=${PLAYWRIGHT_MAX_SCREENSHOTS}
263
+
264
+ # Count existing screenshots
265
+ local count
266
+ count=$(find "$dir" -maxdepth 1 -name 'verify-*.png' 2>/dev/null | wc -l | tr -d ' ')
267
+
268
+ if [ "$count" -gt "$max" ]; then
269
+ local to_remove=$((count - max))
270
+ # Remove oldest files (sorted by name, which is timestamp-based)
271
+ find "$dir" -maxdepth 1 -name 'verify-*.png' 2>/dev/null \
272
+ | sort | head -n "$to_remove" \
273
+ | xargs rm -f 2>/dev/null || true
274
+ fi
275
+ }
276
+
277
+ #===============================================================================
278
+ # Summary (for prompt injection)
279
+ #===============================================================================
280
+
281
+ playwright_verify_summary() {
282
+ # Returns one-line summary string
283
+ if [ ! -f "$PLAYWRIGHT_RESULTS_FILE" ]; then
284
+ echo ""
285
+ return 0
286
+ fi
287
+
288
+ _PW_RESULTS="$PLAYWRIGHT_RESULTS_FILE" python3 -c "
289
+ import json, os
290
+ try:
291
+ data = json.load(open(os.environ['_PW_RESULTS']))
292
+ checks = data.get('checks', {})
293
+ passed = sum(1 for v in checks.values() if v)
294
+ total = len(checks)
295
+ status = 'PASS' if data.get('passed') else 'FAIL'
296
+ duration = data.get('duration_ms', 0)
297
+ url = data.get('url', '?')
298
+ errors = data.get('errors', [])
299
+ err_detail = ''
300
+ if errors:
301
+ err_detail = ' Errors: ' + '; '.join(errors[:3])
302
+ print(f'{status} {passed}/{total} checks ({duration}ms) {url}{err_detail}')
303
+ except Exception:
304
+ print('')
305
+ " 2>/dev/null || echo ""
306
+ }
307
+
308
+ #===============================================================================
309
+ # Council Evidence (for completion-council.sh)
310
+ #===============================================================================
311
+
312
+ playwright_verify_as_evidence() {
313
+ # Writes formatted smoke test evidence to stdout or appends to file
314
+ local evidence_file="${1:-}"
315
+
316
+ if [ ! -f "$PLAYWRIGHT_RESULTS_FILE" ]; then
317
+ return 0
318
+ fi
319
+
320
+ {
321
+ echo ""
322
+ echo "## Playwright Smoke Test"
323
+ echo ""
324
+
325
+ _PW_RESULTS="$PLAYWRIGHT_RESULTS_FILE" python3 -c "
326
+ import json, os
327
+ try:
328
+ data = json.load(open(os.environ['_PW_RESULTS']))
329
+ checks = data.get('checks', {})
330
+ status = 'PASS' if data.get('passed') else 'FAIL'
331
+ print(f'Overall: {status} | URL: {data.get(\"url\", \"?\")} | Duration: {data.get(\"duration_ms\", 0)}ms')
332
+ print()
333
+ for name, result in checks.items():
334
+ label = '[PASS]' if result else '[FAIL]'
335
+ print(f' {label} {name}')
336
+ errors = data.get('errors', [])
337
+ if errors:
338
+ print()
339
+ print('Errors:')
340
+ for e in errors[:5]:
341
+ print(f' - {e}')
342
+ screenshot = data.get('screenshot', '')
343
+ if screenshot:
344
+ print()
345
+ print(f'Screenshot: {screenshot}')
346
+ except Exception:
347
+ print('Playwright data unavailable')
348
+ " 2>/dev/null || echo "Playwright data unavailable"
349
+ } >> "${evidence_file:-/dev/stdout}"
350
+ }