From 4daa41a91ba537e9cec951d9c3457781cc7e9c61 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 21:32:55 +0000 Subject: [PATCH] chore(ai): add smoke test script (agent) --- package-lock.json | 1 + package.json | 1 + scripts/agent/smoke-test.mjs | 491 +++++++++++++++-------------------- 3 files changed, 218 insertions(+), 275 deletions(-) diff --git a/package-lock.json b/package-lock.json index a29401bd..cdd0e7d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "jsdom": "^24.1.3", "nostr-tools": "^2.19.4", "pixelmatch": "^5.3.0", + "playwright": "^1.57.0", "pngjs": "^7.0.0", "postcss": "^8.4.41", "prettier": "^3.8.0", diff --git a/package.json b/package.json index 306969b4..049eb220 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "jsdom": "^24.1.3", "nostr-tools": "^2.19.4", "pixelmatch": "^5.3.0", + "playwright": "^1.57.0", "pngjs": "^7.0.0", "postcss": "^8.4.41", "prettier": "^3.8.0", diff --git a/scripts/agent/smoke-test.mjs b/scripts/agent/smoke-test.mjs index 7b314893..77638270 100644 --- a/scripts/agent/smoke-test.mjs +++ b/scripts/agent/smoke-test.mjs @@ -1,301 +1,242 @@ -import { chromium } from '@playwright/test'; -import { spawn } from 'node:child_process'; -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const REPO_ROOT = path.resolve(__dirname, '../../'); +const ROOT_DIR = path.resolve(__dirname, '../..'); +const ARTIFACTS_DIR = path.join(ROOT_DIR, 'artifacts'); -const ARTIFACTS_DIR = path.join(REPO_ROOT, 'artifacts'); -const TIMESTAMP = new Date().toISOString().replace(/[:.]/g, '-'); -const LOG_FILE = path.join(ARTIFACTS_DIR, `smoke-${TIMESTAMP}.log`); -const REPORT_FILE = path.join(ARTIFACTS_DIR, `smoke-report-${TIMESTAMP}.json`); -const RELAY_PORT = 8008; -const HTTP_PORT = 8000; - -// Ensure artifacts dir exists if (!fs.existsSync(ARTIFACTS_DIR)) { - fs.mkdirSync(ARTIFACTS_DIR, { recursive: true }); + fs.mkdirSync(ARTIFACTS_DIR); } -function log(message, type = 'INFO') { - const msg = `[${new Date().toISOString()}] [${type}] ${message}`; - console.log(msg); - fs.appendFileSync(LOG_FILE, msg + '\n'); +const DATE_STR = new Date().toISOString().slice(0, 10).replace(/-/g, ''); +const LOG_FILE = path.join(ARTIFACTS_DIR, `smoke-${DATE_STR}.log`); + +function log(message) { + const line = `[${new Date().toISOString()}] ${message}`; + console.log(line); + fs.appendFileSync(LOG_FILE, line + '\n'); } -const stats = { - steps: 0, - failures: 0, - details: [] -}; +async function startServer() { + log('Starting local server (python3)...'); + const serverProcess = spawn('python3', ['-m', 'http.server', '8000'], { + cwd: ROOT_DIR, + stdio: 'ignore' + }); -function recordStep(name, success, error = null) { - stats.steps++; - const detail = { name, success, timestamp: new Date().toISOString() }; - if (!success) { - stats.failures++; - detail.error = error?.message || String(error); - log(`Step Failed: ${name} - ${detail.error}`, 'ERROR'); - if (error?.stack) { - log(error.stack, 'ERROR'); + // Give it a moment to start + await new Promise(resolve => setTimeout(resolve, 2000)); + return serverProcess; +} + +async function run() { + let serverProcess; + let browser; + let exitCode = 0; + + try { + serverProcess = await startServer(); + + log('Launching browser...'); + browser = await chromium.launch({ headless: true }); + const page = await browser.newPage(); + + page.on('console', msg => { + const text = msg.text(); + // Filter out some noise if needed, but keeping it is safer for debugging + if (msg.type() === 'error') { + log(`BROWSER ERROR: ${text}`); + } else { + // log(`BROWSER: ${text}`); // Optional verbose logging + } + }); + + page.on('pageerror', err => { + log(`PAGE ERROR: ${err.message}`); + }); + + log('Navigating to app...'); + await page.goto('http://localhost:8000'); + + // Wait for app to load (checking for #app or similar) + await page.waitForSelector('#app', { timeout: 10000 }); + log('App loaded.'); + + // Inject test logic + log('Running in-page smoke test...'); + const result = await page.evaluate(async () => { + const logs = []; + function pageLog(m) { logs.push(m); console.log('[SmokeTest] ' + m); } + + try { + // 1. Import Client + pageLog('Importing nostrClient...'); + const { nostrClient } = await import('./js/nostrClientFacade.js'); + const { NostrTools } = window; // Assumed to be loaded via script tag in index.html + + if (!nostrClient) throw new Error('nostrClient not found'); + if (!NostrTools) throw new Error('NostrTools not found on window'); + + // Override relays to a specific subset to avoid spamming all defaults if local relay is not available + // Ideally this would be a local relay, but we use a public one for the smoke test as fallback. + const TEST_RELAY = 'wss://relay.damus.io'; + pageLog(`Configuring nostrClient to use ${TEST_RELAY}...`); + + // If nostrClient is already initialized, we might need to reconnect or just update properties + nostrClient.relays = [TEST_RELAY]; + nostrClient.writeRelays = [TEST_RELAY]; + nostrClient.readRelays = [TEST_RELAY]; + + if (nostrClient.pool) { + // Ensure connection to our test relay + await nostrClient.pool.ensureRelay(TEST_RELAY); } - } else { - log(`Step Passed: ${name}`, 'SUCCESS'); - } - stats.details.push(detail); -} -async function startProcess(command, args, cwd, readyCheck) { - return new Promise((resolve, reject) => { - const proc = spawn(command, args, { cwd, stdio: 'pipe' }); - let started = false; - const label = args.some(a => a.includes('relay')) ? 'RELAY' : command; + // Wait a bit for connection + pageLog('Waiting for relay connection...'); + await new Promise(resolve => setTimeout(resolve, 3000)); - const checkData = (data) => { - const output = data.toString(); - if (started) { - if (label === 'RELAY') log(`[RELAY] ${output.trim()}`, 'DEBUG'); - return; - } - if (readyCheck(output)) { - started = true; - resolve(proc); - } + // 2. Generate Identity + pageLog('Generating ephemeral identity...'); + + const bytesToHex = (bytes) => { + return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join(''); }; - proc.stdout.on('data', (data) => { - checkData(data); - }); - proc.stderr.on('data', (data) => { - checkData(data); - }); - - proc.on('error', (err) => reject(err)); - proc.on('exit', (code) => { - if (!started) reject(new Error(`${command} exited early with code ${code}`)); - }); - - // Timeout fallback - setTimeout(() => { - if (!started) { - // For http server, sometimes it doesn't output much - if (command.includes('python') || command.includes('http.server')) { - started = true; - resolve(proc); - } else { - reject(new Error(`${command} timed out waiting for ready signal`)); - } + let privateKey; + if (NostrTools.generateSecretKey) { + const secret = NostrTools.generateSecretKey(); + if (secret instanceof Uint8Array) { + privateKey = bytesToHex(secret); + } else { + privateKey = secret; } - }, 3000); + } else { + privateKey = NostrTools.generatePrivateKey(); + } + + const pubkey = NostrTools.getPublicKey(privateKey); + pageLog(`Generated pubkey: ${pubkey}`); + + // 3. Login + pageLog('Logging in...'); + await nostrClient.registerPrivateKeySigner({ privateKey, pubkey }); + + // 4. Publish Video + pageLog('Publishing video...'); + const videoPayload = { + legacyFormData: { // extractVideoPublishPayload expects this structure or direct fields + title: `Smoke Test Video ${Date.now()}`, + description: 'Automated smoke test artifact', + url: 'https://example.com/video.mp4', + magnet: 'magnet:?xt=urn:btih:c9e15763f722f23e98ce29a8c0c4000000000000&dn=test', // Valid-ish magnet + mimeType: 'video/mp4', + isForKids: false, + isNsfw: false, + isPrivate: true, + }, + fileSha256: '0000000000000000000000000000000000000000000000000000000000000000' + }; + + // Note: publishVideo takes (videoPayload, pubkey) + const event = await nostrClient.publishVideo(videoPayload, pubkey); + pageLog(`Video published! ID: ${event.id}`); + + // 5. Verify Read Back + pageLog('Verifying read back...'); + // We can use getEventById + const fetched = await nostrClient.getEventById(event.id); + if (!fetched) throw new Error('Failed to fetch published video'); + if (fetched.id !== event.id) throw new Error('Fetched ID mismatch'); + pageLog('Read back successful.'); + + // 6. DM Flow + pageLog('Testing DM flow (Self-DM)...'); + // We'll send a DM to ourselves + const npub = NostrTools.nip19.npubEncode(pubkey); + const message = `Smoke Test DM ${Date.now()}`; + + const dmResult = await nostrClient.sendDirectMessage(npub, message); + if (!dmResult.ok) throw new Error(`DM Send failed: ${dmResult.error}`); + pageLog('DM Sent.'); + + // Decrypt + // We need to fetch it. listDirectMessages + pageLog('Fetching and decrypting DMs...'); + // Allow some time for propagation to relay and back + await new Promise(resolve => setTimeout(resolve, 2000)); + + const dms = await nostrClient.listDirectMessages(pubkey, { limit: 10 }); + + const match = dms.find(r => r.content === message || r.plaintext === message); + + if (!match) { + pageLog('DM not found in list. Fetched count: ' + dms.length); + console.log('DMs:', JSON.stringify(dms)); + throw new Error('DM verification failed'); + } + + pageLog('DM Verified.'); + + return { success: true, logs }; + + } catch (e) { + pageLog(`ERROR: ${e.message}`); + return { success: false, error: e.message, stack: e.stack, logs }; + } }); -} -async function runSmokeTest() { - let relayProc, serverProc, browser; + if (result.success) { + log('Smoke Test Passed!'); + result.logs.forEach(l => log(`[PAGE] ${l}`)); - try { - log('Starting smoke test...'); + const summary = { + timestamp: new Date().toISOString(), + status: 'success', + steps: ['login', 'publish', 'dm_send', 'dm_decrypt'], + logs: result.logs + }; + fs.writeFileSync(path.join(ARTIFACTS_DIR, `smoke-summary-${DATE_STR}.json`), JSON.stringify(summary, null, 2)); - // 1. Start Relay - log('Starting local relay...'); - relayProc = await startProcess( - 'node', - ['scripts/agent/simple-relay.mjs'], - REPO_ROOT, - (output) => output.includes(`running on ws://localhost:${RELAY_PORT}`) || output.includes(`running on port ${RELAY_PORT}`) - ); + } else { + log('Smoke Test Failed!'); + result.logs.forEach(l => log(`[PAGE] ${l}`)); + log(`Error: ${result.error}`); + log(`Stack: ${result.stack}`); - } catch (e) { - log('Failed to start relay: ' + e.message, 'FATAL'); - // We attempt to proceed, as relay might be running + const screenshotPath = path.join(ARTIFACTS_DIR, `failure-${DATE_STR}.png`); + await page.screenshot({ path: screenshotPath }); + log(`Screenshot saved to ${screenshotPath}`); + + const summary = { + timestamp: new Date().toISOString(), + status: 'failure', + error: result.error, + logs: result.logs + }; + fs.writeFileSync(path.join(ARTIFACTS_DIR, `smoke-summary-${DATE_STR}.json`), JSON.stringify(summary, null, 2)); + + exitCode = 1; } - try { - // 2. Start HTTP Server - log('Starting HTTP server...'); - serverProc = await startProcess( - 'python3', - ['-m', 'http.server', String(HTTP_PORT)], - REPO_ROOT, - (output) => output.includes(`Serving HTTP on 0.0.0.0 port ${HTTP_PORT}`) - ); - - // 3. Launch Browser - log('Launching browser...'); - browser = await chromium.launch({ headless: true }); // headless: true for CI - const context = await browser.newContext(); - const page = await context.newPage(); - - // 4. Navigate to app - log(`Navigating to http://localhost:${HTTP_PORT}...`); - await page.goto(`http://localhost:${HTTP_PORT}`, { waitUntil: 'networkidle' }); - - // 5. Inject Test Logic - // We will do everything inside page.evaluate to access the window context and modules - log('Executing test logic in browser...'); - - const testResult = await page.evaluate(async ({ relayUrl }) => { - const logs = []; - const steps = []; - - function log(msg) { console.log(msg); logs.push(msg); } - - try { - // Dynamic import of the client - // Note: The path must be relative to the URL being served - const { nostrClient } = await import('./js/nostr/defaultClient.js'); - - // Wait for NostrTools - if (!window.NostrTools) { - await new Promise(r => setTimeout(r, 1000)); - if (!window.NostrTools) throw new Error("NostrTools not available on window"); - } - const NostrTools = window.NostrTools; - - // --- Step 1: Init --- - // Reset relays to our local relay only - nostrClient.relays = [relayUrl]; - nostrClient.readRelays = [relayUrl]; - nostrClient.writeRelays = [relayUrl]; - - // Re-init or connect - await nostrClient.init(); - // Force connection again just in case init was already called by the app with defaults - await nostrClient.connectToRelays(); - - steps.push({ name: 'Init & Connect', success: true }); - - // --- Step 2: Login (Alice) --- - const sk1 = NostrTools.generateSecretKey(); - const pk1 = NostrTools.getPublicKey(sk1); - const hexSk1 = NostrTools.utils.bytesToHex(sk1); - - await nostrClient.registerPrivateKeySigner({ privateKey: hexSk1, pubkey: pk1 }); - if (nostrClient.sessionActor?.pubkey !== pk1) throw new Error(`Login failed: sessionActor.pubkey (${nostrClient.sessionActor?.pubkey}) !== ${pk1}`); - - steps.push({ name: 'Login (Alice)', success: true }); - - // --- Step 3: Publish Video --- - const videoPayload = { - title: 'Smoke Test Video ' + Date.now(), - description: 'Test Description', - url: 'https://example.com/video.mp4', - thumbnail: 'https://example.com/thumb.jpg', - mode: 'live' - }; - - const event = await nostrClient.publishVideo(videoPayload, pk1); - if (!event || !event.id) throw new Error("Publish failed"); - - steps.push({ name: 'Publish Video', success: true }); - - // --- Step 4: Verify Video --- - // Wait a bit - await new Promise(r => setTimeout(r, 1500)); - - // Fetch directly - const fetched = await nostrClient.getEventById(event.id); - if (!fetched) { - // Try debugging - const raw = await nostrClient.fetchRawEventById(event.id); - throw new Error(`Could not fetch published video ${event.id}. Raw result: ${raw ? 'found' : 'null'}`); - } - if (fetched.title !== videoPayload.title) throw new Error("Video content mismatch"); - - steps.push({ name: 'Verify Video', success: true }); - - // --- Step 5: DM (Alice -> Bob) --- - const sk2 = NostrTools.generateSecretKey(); - const pk2 = NostrTools.getPublicKey(sk2); - const hexSk2 = NostrTools.utils.bytesToHex(sk2); - const npub2 = NostrTools.nip19.npubEncode(pk2); - - const msg = "Secret Message " + Date.now(); - const dmResult = await nostrClient.sendDirectMessage(npub2, msg); - if (!dmResult.ok) throw new Error("DM send failed: " + dmResult.error); - - steps.push({ name: 'Send DM', success: true }); - - // --- Step 6: Decrypt DM (Bob) --- - // Switch to Bob - nostrClient.logout(); - await nostrClient.registerPrivateKeySigner({ privateKey: hexSk2, pubkey: pk2 }); - - await new Promise(r => setTimeout(r, 500)); - - const dms = await nostrClient.listDirectMessages(pk2); - const found = dms.find(m => m.plaintext === msg); - - if (!found) { - // Log details - return { - success: false, - steps, - logs, - error: "DM not found. Found: " + dms.map(d => d.plaintext).join(', ') - }; - } - - steps.push({ name: 'Decrypt DM', success: true }); - - return { success: true, steps, logs }; - - } catch (e) { - return { success: false, steps, logs, error: e.message, stack: e.stack }; - } - }, { relayUrl: `ws://localhost:${RELAY_PORT}` }); - - // Process results - if (testResult.logs) { - testResult.logs.forEach(l => log(`[Browser] ${l}`)); - } - - if (testResult.steps) { - testResult.steps.forEach(s => recordStep(s.name, s.success)); - } - - if (!testResult.success) { - throw new Error(testResult.error || "Unknown browser test failure"); - } - - } catch (err) { - log(`Test failed: ${err.message}`, 'FATAL'); - recordStep('Smoke Test Suite', false, err); - // Take screenshot if browser is active - if (browser) { - try { - const page = browser.contexts()[0].pages()[0]; - if (page) { - await page.screenshot({ path: path.join(ARTIFACTS_DIR, `failure-${TIMESTAMP}.png`) }); - log(`Screenshot saved to failure-${TIMESTAMP}.png`); - } - } catch (e) { - log('Failed to capture screenshot: ' + e.message); - } - } - } finally { - // Cleanup - if (browser) await browser.close(); - if (serverProc) { - try { process.kill(serverProc.pid); } catch (e) { /* ignore */ } - } - if (relayProc) { - try { process.kill(relayProc.pid); } catch (e) { /* ignore */ } - } - - // Report - fs.writeFileSync(REPORT_FILE, JSON.stringify({ - timestamp: new Date().toISOString(), - stats, - logFile: LOG_FILE - }, null, 2)); - - console.log(`Report saved to ${REPORT_FILE}`); - if (stats.failures > 0) process.exit(1); + } catch (err) { + log(`CRITICAL ERROR: ${err.message}`); + log(err.stack); + exitCode = 1; + } finally { + if (browser) await browser.close(); + if (serverProcess) { + serverProcess.kill(); + log('Server stopped.'); } + } + + process.exit(exitCode); } -runSmokeTest(); +run();