From e3ef3253dc8b1ee0c7f8db5ad663f129d030c6b4 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 21:34:07 +0000 Subject: [PATCH] chore(ai): add smoke test script (agent) --- scripts/agent/smoke-test.mjs | 363 ++++++++++++++--------------------- 1 file changed, 144 insertions(+), 219 deletions(-) diff --git a/scripts/agent/smoke-test.mjs b/scripts/agent/smoke-test.mjs index 40849e60..b110ae9a 100644 --- a/scripts/agent/smoke-test.mjs +++ b/scripts/agent/smoke-test.mjs @@ -1,292 +1,217 @@ -/** - * Agent Smoke Test - * Verified flows: Login, Publish (Video), DM Decrypt (NIP-04/44) - */ import { chromium } from 'playwright'; import { spawn } from 'child_process'; +import { startRelay } from './simple-relay.mjs'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const ROOT_DIR = path.resolve(__dirname, '../..'); +const ROOT_DIR = path.resolve(__dirname, '../../'); const ARTIFACTS_DIR = path.join(ROOT_DIR, 'artifacts'); if (!fs.existsSync(ARTIFACTS_DIR)) { - fs.mkdirSync(ARTIFACTS_DIR); + fs.mkdirSync(ARTIFACTS_DIR, { recursive: true }); } -const DATE_STR = new Date().toISOString().slice(0, 10).replace(/-/g, ''); -const LOG_FILE = path.join(ARTIFACTS_DIR, `smoke-${DATE_STR}.log`); +const TIMESTAMP = new Date().toISOString().slice(0, 10).replace(/-/g, ''); +const LOG_FILE = path.join(ARTIFACTS_DIR, `smoke-${TIMESTAMP}.log`); +const SUMMARY_FILE = path.join(ARTIFACTS_DIR, `smoke-summary.json`); function log(message) { - const line = `[${new Date().toISOString()}] ${message}`; + const timestamp = new Date().toISOString(); + const line = `[${timestamp}] ${message}`; console.log(line); fs.appendFileSync(LOG_FILE, line + '\n'); } -async function startProcess(command, args, name) { - log(`Starting ${name}...`); - const proc = spawn(command, args, { +async function runSmokeTest() { + const startTime = new Date().toISOString(); + log('Starting smoke test...'); + + // 1. Start Relay + log('Starting local relay on port 8888...'); + const relay = startRelay(8888); + + // 2. Start Web Server + log('Starting web server on port 8000...'); + const serverProcess = spawn('npx', ['serve', '-p', '8000'], { cwd: ROOT_DIR, - stdio: 'pipe', // Capture output - env: process.env + stdio: 'ignore', + shell: true }); - proc.stdout.on('data', (data) => { - log(`[${name}] ${data.toString().trim()}`); - }); - - proc.stderr.on('data', (data) => { - log(`[${name} ERR] ${data.toString().trim()}`); - }); - - proc.on('error', (err) => { - log(`[${name}] Failed to start: ${err.message}`); - }); - - // Give it a moment to start + // Give server time to boot await new Promise(resolve => setTimeout(resolve, 2000)); - return proc; -} -async function run() { - let serverProcess; - let relayProcess; let browser; - let exitCode = 0; + let testResult = { success: false, error: null }; try { - // 1. Start Relay - relayProcess = await startProcess('node', ['scripts/agent/simple-relay.mjs'], 'LocalRelay'); - - // 2. Start Web Server - serverProcess = await startProcess('python3', ['-m', 'http.server', '8000'], 'WebServer'); - - log('Launching browser...'); - browser = await chromium.launch({ headless: true }); + // 3. Launch Playwright + log('Launching headless browser...'); + browser = await chromium.launch(); const page = await browser.newPage(); - page.on('console', msg => { - const type = msg.type(); - const text = msg.text(); - if (text.startsWith('[SmokeTest]')) { - log(text); - } else if (type === 'error') { - log(`BROWSER ERROR: ${text}`); - } - }); + page.on('console', msg => log(`[BROWSER] ${msg.text()}`)); + page.on('pageerror', err => log(`[BROWSER ERROR] ${err.message}`)); - page.on('pageerror', err => { - log(`PAGE ERROR: ${err.message}`); - }); + log('Navigating to http://localhost:8000...'); + await page.goto('http://localhost:8000'); - log('Navigating to http://127.0.0.1:8000 ...'); - await page.goto('http://127.0.0.1:8000'); - - // Wait for App and NostrTools - log('Waiting for app and NostrTools...'); - await page.waitForFunction(() => window.NostrTools && document.querySelector('#app'), { timeout: 15000 }); - log('App loaded and NostrTools available.'); - - // Inject test logic - log('Running in-page smoke test logic...'); - - const result = await page.evaluate(async () => { - const logs = []; - function pageLog(m) { - const msg = `[SmokeTest] ${m}`; - console.log(msg); - logs.push(msg); - } + log('Waiting for NostrTools...'); + await page.waitForFunction(() => window.NostrTools); + // 4. Inject Test Script + log('Injecting test script...'); + testResult = await page.evaluate(async () => { try { - const RELAY_URL = 'ws://localhost:8008'; - pageLog(`Test Relay: ${RELAY_URL}`); + console.log('Test script started inside browser.'); - // 1. Import Modules const { nostrClient } = await import('./js/nostrClientFacade.js'); const { decryptDM } = await import('./js/dmDecryptor.js'); - const NostrTools = window.NostrTools; + const { bytesToHex } = window.NostrTools.utils || window.NostrTools; // Handle v2 or fallback + const { generateSecretKey, getPublicKey } = window.NostrTools; - if (!nostrClient) throw new Error('nostrClient import failed'); - if (!decryptDM) throw new Error('decryptDM import failed'); + // --- Setup --- + console.log('Generating keys...'); + const sk = generateSecretKey(); + const pk = getPublicKey(sk); + const privateKey = typeof sk === 'string' ? sk : bytesToHex(sk); // Handle if generateSecretKey returns hex (older versions) + const pubkey = pk; - // 2. Configure Client - pageLog('Configuring nostrClient...'); - nostrClient.relays = [RELAY_URL]; - nostrClient.writeRelays = [RELAY_URL]; - nostrClient.readRelays = [RELAY_URL]; + console.log(`Test User Pubkey: ${pubkey}`); - // Force connection logic if needed, but adding to relays list usually triggers connection attempt in client - // Check pool if available - if (nostrClient.pool) { - await nostrClient.pool.ensureRelay(RELAY_URL); - } + // Configure Client + console.log('Configuring NostrClient...'); + nostrClient.relays = ['ws://localhost:8888']; + nostrClient.readRelays = ['ws://localhost:8888']; + nostrClient.writeRelays = ['ws://localhost:8888']; - // Wait for connection - pageLog('Waiting for relay connection...'); - await new Promise(r => setTimeout(r, 1000)); + await nostrClient.init(); - // 3. Generate Identity & Login - pageLog('Generating ephemeral keys...'); + // Login + console.log('Logging in with private key...'); + await nostrClient.registerPrivateKeySigner({ privateKey, pubkey }); - // Helper since NostrTools.bytesToHex might be missing depending on version/bundle - function toHex(bytes) { - return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join(''); - } - - const skBytes = NostrTools.generateSecretKey(); - const sk = toHex(skBytes); - const pk = NostrTools.getPublicKey(skBytes); - pageLog(`Generated Pubkey: ${pk}`); - - await nostrClient.registerPrivateKeySigner({ privateKey: sk, pubkey: pk }); - pageLog('Logged in.'); - - // 4. Publish Video - pageLog('Publishing VIDEO_POST...'); - const videoRootId = `smoke-${Date.now()}`; + // --- Video Flow --- + console.log('Starting Video Flow...'); + const videoTitle = `Smoke Test Video ${Date.now()}`; const videoPayload = { legacyFormData: { - title: `Smoke Test Video ${Date.now()}`, - description: 'Smoke test artifact', - url: 'https://example.com/smoke.mp4', - magnet: 'magnet:?xt=urn:btih:c9e15763f722f23e98ce29a8c0c4000000000000&dn=smoke', + title: videoTitle, + description: 'Smoke test description', + url: 'https://example.com/video.mp4', + magnet: 'magnet:?xt=urn:btih:1234567890abcdef1234567890abcdef12345678&dn=video.mp4', mimeType: 'video/mp4', isForKids: false, - isNsfw: false, - isPrivate: true - }, - fileSha256: '0000000000000000000000000000000000000000000000000000000000000000' + isNsfw: false + } }; - const pubEvent = await nostrClient.publishVideo(videoPayload, pk); - if (!pubEvent || !pubEvent.id) throw new Error('Publish failed, no event returned'); - pageLog(`Video published: ${pubEvent.id}`); + console.log('Publishing video...'); + const publishedEvent = await nostrClient.publishVideo(videoPayload, pubkey); + console.log(`Published Video Event ID: ${publishedEvent.id}`); - // 5. Verify Video Readback - pageLog('Verifying video readback...'); - await new Promise(r => setTimeout(r, 500)); // Propagate - const fetched = await nostrClient.getEventById(pubEvent.id); - if (!fetched || fetched.id !== pubEvent.id) throw new Error('Failed to fetch video event'); - pageLog('Video verified.'); + // Verify fetch + console.log('Fetching videos to verify...'); + await new Promise(r => setTimeout(r, 1000)); - // 6. DM Flow (Self-DM) - pageLog('Testing DM flow (Self-Send)...'); - const message = `Smoke DM ${Date.now()}`; - const npub = NostrTools.nip19.npubEncode(pk); + const videos = await nostrClient.fetchVideos({ limit: 10 }); + const found = videos.find(v => v.id === publishedEvent.id); - const dmRes = await nostrClient.sendDirectMessage(npub, message); - if (!dmRes.ok) throw new Error(`DM Send failed: ${dmRes.error}`); - pageLog('DM sent.'); - - // 7. Fetch & Decrypt - pageLog('Fetching DM...'); - await new Promise(r => setTimeout(r, 500)); - - // We use listDirectMessages which handles fetching and decrypting usually, - // BUT the prompt asks to "verify decryption via js/dmDecryptor.js". - // `nostrClient.listDirectMessages` might use `dmDecryptor` internally or have its own logic. - // Let's manually fetch the raw event and decrypt it to be sure we are testing `dmDecryptor.js`. - - const filter = { - kinds: [4, 1059], - '#p': [pk], - limit: 10 - }; - const events = await nostrClient.pool.list([RELAY_URL], [filter]); - const targetEvent = events.find(e => e.pubkey === pk); // Sent by self - - if (!targetEvent) throw new Error('DM event not found on relay'); - pageLog(`Found DM event kind ${targetEvent.kind}`); - - // Setup Decrypt Context - const decryptors = []; - - // NIP-44 - if (NostrTools.nip44) { - decryptors.push({ - scheme: 'nip44', - decrypt: (pk, ct) => NostrTools.nip44.decrypt(sk, pk, ct), - priority: 1, - supportsGiftWrap: true - }); + if (!found) { + throw new Error('Published video not found in fetch results.'); } - // NIP-04 - if (NostrTools.nip04) { - decryptors.push({ - scheme: 'nip04', - decrypt: async (pk, ct) => NostrTools.nip04.decrypt(sk, pk, ct), - priority: 0 - }); + console.log('Video Flow Success: Video found.'); + + // --- DM Flow --- + console.log('Starting DM Flow (Self-DM)...'); + const dmMessage = `Smoke Test DM ${Date.now()}`; + + console.log('Sending DM...'); + const sendResult = await nostrClient.sendDirectMessage(window.NostrTools.nip19.npubEncode(pubkey), dmMessage); + + if (!sendResult.ok) { + throw new Error(`DM Send failed: ${sendResult.error}`); + } + console.log('DM Sent.'); + + // Verify Decryption explicitly using decryptDM + console.log('Fetching raw DM event to verify decryption...'); + await new Promise(r => setTimeout(r, 1000)); + + // We need to fetch the raw event. nostrClient.pool.list + const events = await nostrClient.pool.list(['ws://localhost:8888'], [ + { kinds: [4, 1059], authors: [pubkey], limit: 10 } // Simplified filter for self-DM + ]); + + // Find the event that corresponds to our message (we can't check content yet, so we try decrypting candidates) + let foundAndDecrypted = false; + const decryptContext = await nostrClient.buildDmDecryptContext(pubkey); + + for (const event of events) { + try { + const result = await decryptDM(event, decryptContext); + if (result.ok && result.plaintext === dmMessage) { + foundAndDecrypted = true; + break; + } + } catch (e) { + // Ignore decrypt errors for other events + } } - const context = { - actorPubkey: pk, - decryptors - }; - - pageLog('Decrypting...'); - const decrypted = await decryptDM(targetEvent, context); - - if (!decrypted.ok) { - console.error('Decryption errors:', decrypted.errors); - throw new Error('Decryption failed'); + if (!foundAndDecrypted) { + throw new Error('DM verification failed: Could not find or decrypt the sent message using decryptDM.'); } - if (decrypted.plaintext !== message) { - throw new Error(`Message mismatch: Got "${decrypted.plaintext}", expected "${message}"`); - } + console.log('DM Flow Success: Message decrypted and verified via decryptDM.'); - pageLog('Decryption verified!'); + return { success: true }; - return { success: true, logs }; - - } catch (e) { - pageLog(`FATAL: ${e.message}`); - return { success: false, error: e.message, stack: e.stack, logs }; + } catch (err) { + console.error('In-browser test error:', err); + return { success: false, error: err.toString(), stack: err.stack }; } }); - if (result.success) { - log('SMOKE TEST PASSED'); - const summary = { - timestamp: new Date().toISOString(), - status: 'success', - logs: result.logs - }; - fs.writeFileSync(path.join(ARTIFACTS_DIR, `smoke-summary-${DATE_STR}.json`), JSON.stringify(summary, null, 2)); + if (testResult.success) { + log('Test execution passed successfully.'); } else { - log('SMOKE TEST FAILED'); - log(`Error: ${result.error}`); + log(`Test execution failed: ${testResult.error}`); + if (testResult.stack) log(testResult.stack); - const screenshotPath = path.join(ARTIFACTS_DIR, `smoke-fail-${DATE_STR}.png`); + const screenshotPath = path.join(ARTIFACTS_DIR, 'smoke-failure.png'); await page.screenshot({ path: screenshotPath }); - log(`Screenshot saved: ${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; + log(`Screenshot saved to ${screenshotPath}`); + process.exitCode = 1; } } catch (err) { - log(`SCRIPT ERROR: ${err.message}`); + log(`FATAL ERROR: ${err.message}`); console.error(err); - exitCode = 1; + testResult.error = err.message; + process.exitCode = 1; } finally { + log('Cleaning up...'); if (browser) await browser.close(); - if (serverProcess) serverProcess.kill(); - if (relayProcess) relayProcess.kill(); - } + serverProcess.kill(); + relay.close(); - process.exit(exitCode); + // Write JSON summary + const summary = { + timestamp: new Date().toISOString(), + success: testResult.success === true, + error: testResult.error || null, + startTime, + endTime: new Date().toISOString() + }; + fs.writeFileSync(SUMMARY_FILE, JSON.stringify(summary, null, 2)); + log(`Summary written to ${SUMMARY_FILE}`); + + log('Done.'); + } } -run(); +runSmokeTest();