Files
bitvid/scripts/agent/smoke-test.mjs
T

243 lines
8.0 KiB
JavaScript

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 ROOT_DIR = path.resolve(__dirname, '../..');
const ARTIFACTS_DIR = path.join(ROOT_DIR, 'artifacts');
if (!fs.existsSync(ARTIFACTS_DIR)) {
fs.mkdirSync(ARTIFACTS_DIR);
}
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');
}
async function startServer() {
log('Starting local server (python3)...');
const serverProcess = spawn('python3', ['-m', 'http.server', '8000'], {
cwd: ROOT_DIR,
stdio: 'ignore'
});
// 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);
}
// Wait a bit for connection
pageLog('Waiting for relay connection...');
await new Promise(resolve => setTimeout(resolve, 3000));
// 2. Generate Identity
pageLog('Generating ephemeral identity...');
const bytesToHex = (bytes) => {
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
};
let privateKey;
if (NostrTools.generateSecretKey) {
const secret = NostrTools.generateSecretKey();
if (secret instanceof Uint8Array) {
privateKey = bytesToHex(secret);
} else {
privateKey = secret;
}
} 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 };
}
});
if (result.success) {
log('Smoke Test Passed!');
result.logs.forEach(l => log(`[PAGE] ${l}`));
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));
} else {
log('Smoke Test Failed!');
result.logs.forEach(l => log(`[PAGE] ${l}`));
log(`Error: ${result.error}`);
log(`Stack: ${result.stack}`);
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;
}
} 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);
}
run();