mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-30 01:12:39 +00:00
Merge branch 'unstable' into ai/tests-improve-coverage-20250220-10675377090391221934
This commit is contained in:
29 files changed
+1377
-1034
No files matched your search
+152
-182
@@ -1,215 +1,185 @@
|
||||
import './setup-test-env.js';
|
||||
import { NostrClient } from '../../js/nostr/client.js';
|
||||
import { buildVideoPostEvent, buildViewEvent, NOTE_TYPES } from '../../js/nostrEventSchemas.js';
|
||||
import * as NostrTools from 'nostr-tools';
|
||||
|
||||
import { WebSocket } from 'ws';
|
||||
if (!global.WebSocket) {
|
||||
global.WebSocket = WebSocket;
|
||||
}
|
||||
const bytesToHex = (bytes) => {
|
||||
return Buffer.from(bytes).toString('hex');
|
||||
};
|
||||
|
||||
import { NostrClient, resolveActiveSigner } from '../../js/nostr/client.js';
|
||||
import { startRelay } from './load-test-relay.mjs';
|
||||
import {
|
||||
buildVideoPostEvent,
|
||||
buildViewEvent,
|
||||
buildLegacyDirectMessageEvent
|
||||
} from '../../js/nostrEventSchemas.js';
|
||||
import { decryptDM } from '../../js/dmDecryptor.js';
|
||||
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
||||
import { bytesToHex } from '../../vendor/crypto-helpers.bundle.min.js';
|
||||
const RELAYS = [
|
||||
"wss://relay.damus.io",
|
||||
"wss://nos.lol"
|
||||
];
|
||||
|
||||
// Configuration
|
||||
const RELAY_PORT = 8899;
|
||||
const RELAY_URL = `ws://localhost:${RELAY_PORT}`;
|
||||
const EPHEMERAL_SK_BYTES = generateSecretKey();
|
||||
const EPHEMERAL_SK = bytesToHex(EPHEMERAL_SK_BYTES);
|
||||
const EPHEMERAL_PK = getPublicKey(EPHEMERAL_SK_BYTES);
|
||||
async function runTests() {
|
||||
console.log("Starting Protocol & Interop Tests...");
|
||||
|
||||
console.log(`[Setup] Generated Ephemeral Public Key: ${EPHEMERAL_PK}`);
|
||||
// --- Setup ---
|
||||
console.log("[Setup] Generating ephemeral keys...");
|
||||
const aliceSkBytes = NostrTools.generateSecretKey();
|
||||
const aliceSk = bytesToHex(aliceSkBytes);
|
||||
const alicePk = NostrTools.getPublicKey(aliceSkBytes);
|
||||
|
||||
async function runInteropTests() {
|
||||
let relayServer;
|
||||
let client;
|
||||
const bobSkBytes = NostrTools.generateSecretKey();
|
||||
const bobSk = bytesToHex(bobSkBytes);
|
||||
const bobPk = NostrTools.getPublicKey(bobSkBytes);
|
||||
|
||||
try {
|
||||
// 1. Start Local Relay
|
||||
console.log('[Setup] Starting local test relay...');
|
||||
relayServer = startRelay(RELAY_PORT);
|
||||
console.log(`[Setup] Alice: ${alicePk}`);
|
||||
console.log(`[Setup] Bob: ${bobPk}`);
|
||||
|
||||
// 2. Initialize Client
|
||||
console.log('[Setup] Initializing NostrClient...');
|
||||
client = new NostrClient();
|
||||
client.relays = [RELAY_URL];
|
||||
client.writeRelays = [RELAY_URL];
|
||||
client.readRelays = [RELAY_URL];
|
||||
const aliceClient = new NostrClient();
|
||||
aliceClient.relays = [...RELAYS];
|
||||
aliceClient.readRelays = [...RELAYS];
|
||||
aliceClient.writeRelays = [...RELAYS];
|
||||
|
||||
// Register signer
|
||||
await client.registerPrivateKeySigner({ privateKey: EPHEMERAL_SK });
|
||||
console.log('[Setup] Registered private key signer.');
|
||||
const bobClient = new NostrClient();
|
||||
bobClient.relays = [...RELAYS];
|
||||
bobClient.readRelays = [...RELAYS];
|
||||
bobClient.writeRelays = [...RELAYS];
|
||||
|
||||
// Init client (connects to relay)
|
||||
await client.init();
|
||||
console.log('[Setup] Client initialized and connected.');
|
||||
console.log("[Setup] Connecting clients...");
|
||||
// Initialize to connect
|
||||
await aliceClient.init(); // This might try to load stored session, but shouldn't matter
|
||||
await bobClient.init();
|
||||
|
||||
// --- Test A: Video Post ---
|
||||
console.log('\n[Test A] Video Post Event Roundtrip');
|
||||
const videoContent = {
|
||||
version: 3,
|
||||
title: 'Interop Test Video',
|
||||
url: 'https://example.com/video.mp4',
|
||||
description: 'A test video for interoperability verification.',
|
||||
mode: 'live',
|
||||
videoRootId: 'interop-test-root-1'
|
||||
};
|
||||
// Register signers
|
||||
await aliceClient.registerPrivateKeySigner({ privateKey: aliceSk, pubkey: alicePk });
|
||||
aliceClient.pubkey = alicePk; // Explicitly set client pubkey to ensure correct signer resolution
|
||||
|
||||
const videoEventTemplate = buildVideoPostEvent({
|
||||
pubkey: EPHEMERAL_PK,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
dTagValue: 'interop-test-root-1', // Using rootId as d-tag for simplicity
|
||||
content: videoContent
|
||||
});
|
||||
await bobClient.registerPrivateKeySigner({ privateKey: bobSk, pubkey: bobPk });
|
||||
bobClient.pubkey = bobPk; // Explicitly set client pubkey to ensure correct signer resolution
|
||||
|
||||
console.log('[Test A] Publishing Video Post...');
|
||||
const { signedEvent: publishedVideo } = await client.signAndPublishEvent(videoEventTemplate, {
|
||||
context: 'test-video-post'
|
||||
});
|
||||
let testsPassed = 0;
|
||||
let testsTotal = 0;
|
||||
|
||||
if (!publishedVideo || !publishedVideo.id) {
|
||||
throw new Error('[Test A] Failed to publish video event (no ID returned).');
|
||||
}
|
||||
console.log(`[Test A] Published Video Event ID: ${publishedVideo.id}`);
|
||||
// --- Test A: Video Post ---
|
||||
testsTotal++;
|
||||
console.log("\n[Test A] Publishing VIDEO_POST...");
|
||||
try {
|
||||
const videoPayload = {
|
||||
version: 3,
|
||||
title: "Interop Test Video " + Date.now(),
|
||||
description: "This is a test video event.",
|
||||
videoRootId: "interop-test-" + Date.now(),
|
||||
mode: "dev",
|
||||
isPrivate: false,
|
||||
isNsfw: false,
|
||||
isForKids: false,
|
||||
enableComments: true
|
||||
};
|
||||
|
||||
// Fetch back
|
||||
console.log('[Test A] Fetching event back...');
|
||||
// Small delay to ensure relay processing
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
// We use signAndPublishEvent directly with a manually built event to test the schema helper
|
||||
const event = buildVideoPostEvent({
|
||||
pubkey: alicePk,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
dTagValue: videoPayload.videoRootId,
|
||||
content: videoPayload
|
||||
});
|
||||
|
||||
const fetchedVideo = await client.getEventById(publishedVideo.id, { includeRaw: true });
|
||||
const { signedEvent } = await aliceClient.signAndPublishEvent(event);
|
||||
console.log(`[Test A] Published event ${signedEvent.id}`);
|
||||
|
||||
if (!fetchedVideo) {
|
||||
throw new Error('[Test A] Failed to fetch video event back from relay.');
|
||||
}
|
||||
// Verify
|
||||
console.log("[Test A] Verifying roundtrip...");
|
||||
// Give relays a moment
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
|
||||
const rawVideoEvent = fetchedVideo.rawEvent || fetchedVideo; // handle if getEventById returns raw or object wrapper
|
||||
// getEventById returns the Video object (converted), but we can ask for raw too
|
||||
const result = await bobClient.getEventById(signedEvent.id, { includeRaw: true });
|
||||
if (!result || !result.rawEvent) throw new Error("Failed to fetch event back");
|
||||
|
||||
if (rawVideoEvent.id !== publishedVideo.id) {
|
||||
throw new Error(`[Test A] ID mismatch: expected ${publishedVideo.id}, got ${rawVideoEvent.id}`);
|
||||
}
|
||||
const fetched = result.rawEvent;
|
||||
if (fetched.id !== signedEvent.id) throw new Error("Fetched ID mismatch");
|
||||
|
||||
// Validate content shape (simplified check)
|
||||
const parsedContent = typeof rawVideoEvent.content === 'string' ? JSON.parse(rawVideoEvent.content) : rawVideoEvent.content;
|
||||
if (parsedContent.title !== videoContent.title) {
|
||||
throw new Error(`[Test A] Content mismatch: expected title "${videoContent.title}", got "${parsedContent.title}"`);
|
||||
}
|
||||
// Parse content
|
||||
const content = typeof fetched.content === 'string' ? JSON.parse(fetched.content) : fetched.content;
|
||||
if (content.title !== videoPayload.title) throw new Error("Content mismatch: title");
|
||||
|
||||
console.log('[Test A] SUCCESS: Video Post roundtrip verified.');
|
||||
console.log("[Test A] PASSED");
|
||||
testsPassed++;
|
||||
} catch (e) {
|
||||
console.error("[Test A] FAILED:", e);
|
||||
}
|
||||
|
||||
// --- Test B: View Event ---
|
||||
testsTotal++;
|
||||
console.log("\n[Test B] Publishing VIEW_EVENT...");
|
||||
try {
|
||||
const viewEvent = buildViewEvent({
|
||||
pubkey: alicePk,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
pointerValue: "test-video-pointer", // Arbitrary pointer for test
|
||||
content: "view-test"
|
||||
});
|
||||
|
||||
// --- Test B: View Event ---
|
||||
console.log('\n[Test B] View Event Visibility');
|
||||
const viewEventTemplate = buildViewEvent({
|
||||
pubkey: EPHEMERAL_PK,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
pointerValue: 'interop-test-root-1', // referencing the video we just created
|
||||
pointerTag: ['d', 'interop-test-root-1']
|
||||
});
|
||||
const { signedEvent: signedView } = await aliceClient.signAndPublishEvent(viewEvent);
|
||||
console.log(`[Test B] Published view event ${signedView.id}`);
|
||||
|
||||
console.log('[Test B] Publishing View Event...');
|
||||
const { signedEvent: publishedView } = await client.signAndPublishEvent(viewEventTemplate, {
|
||||
context: 'test-view-event'
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
|
||||
if (!publishedView || !publishedView.id) {
|
||||
throw new Error('[Test B] Failed to publish view event.');
|
||||
}
|
||||
console.log(`[Test B] Published View Event ID: ${publishedView.id}`);
|
||||
// Bob fetches raw event
|
||||
const fetchedView = await bobClient.fetchRawEventById(signedView.id);
|
||||
if (!fetchedView) throw new Error("Failed to fetch view event");
|
||||
if (fetchedView.id !== signedView.id) throw new Error("View event ID mismatch");
|
||||
|
||||
// Fetch back to verify
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
const fetchedView = await client.fetchRawEventById(publishedView.id);
|
||||
if (!fetchedView) {
|
||||
throw new Error('[Test B] Failed to fetch view event back.');
|
||||
}
|
||||
console.log('[Test B] SUCCESS: View Event verified.');
|
||||
console.log("[Test B] PASSED");
|
||||
testsPassed++;
|
||||
} catch (e) {
|
||||
console.error("[Test B] FAILED:", e);
|
||||
}
|
||||
|
||||
// --- Test C: Direct Message ---
|
||||
testsTotal++;
|
||||
console.log("\n[Test C] Sending Direct Message...");
|
||||
try {
|
||||
const message = "Hello Bob, this is Alice " + Date.now();
|
||||
const npubBob = NostrTools.nip19.npubEncode(bobPk);
|
||||
|
||||
// --- Test C: Encrypted DM ---
|
||||
console.log('\n[Test C] Encrypted DM Roundtrip (Self-DM)');
|
||||
const dmMessage = "This is a secret interop message.";
|
||||
const sendResult = await aliceClient.sendDirectMessage(npubBob, message);
|
||||
if (!sendResult.ok) throw new Error("Failed to send DM: " + (sendResult.error || "unknown"));
|
||||
console.log("[Test C] DM sent successfully");
|
||||
|
||||
// 1. Encrypt
|
||||
const signer = resolveActiveSigner(EPHEMERAL_PK);
|
||||
if (!signer || typeof signer.nip04Encrypt !== 'function') {
|
||||
throw new Error('[Test C] Signer does not support NIP-04 encryption.');
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
|
||||
console.log('[Test C] Encrypting message...');
|
||||
const ciphertext = await signer.nip04Encrypt(EPHEMERAL_PK, dmMessage);
|
||||
console.log("[Test C] Bob checking messages...");
|
||||
// Force bob to list messages (which triggers decryption using his registered signer)
|
||||
const messages = await bobClient.listDirectMessages(bobPk, { limit: 5 });
|
||||
|
||||
// 2. Build Event
|
||||
const dmTemplate = buildLegacyDirectMessageEvent({
|
||||
pubkey: EPHEMERAL_PK,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
recipientPubkey: EPHEMERAL_PK,
|
||||
ciphertext: ciphertext
|
||||
});
|
||||
|
||||
// 3. Publish
|
||||
console.log('[Test C] Publishing DM...');
|
||||
const { signedEvent: publishedDM } = await client.signAndPublishEvent(dmTemplate, {
|
||||
context: 'test-dm'
|
||||
});
|
||||
console.log(`[Test C] Published DM Event ID: ${publishedDM.id}`);
|
||||
|
||||
// 4. Fetch
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
const fetchedDM = await client.fetchRawEventById(publishedDM.id);
|
||||
if (!fetchedDM) {
|
||||
throw new Error('[Test C] Failed to fetch DM event.');
|
||||
}
|
||||
|
||||
// 5. Decrypt using dmDecryptor.js helper
|
||||
console.log('[Test C] Attempting decryption...');
|
||||
|
||||
// We need to construct the context expected by decryptDM
|
||||
// It expects { actorPubkey, decryptors }
|
||||
// We can reuse client.buildDmDecryptContext, but let's do it manually to test the decryptor isolation if possible,
|
||||
// or just use the client helper to ensure we are testing the full stack availability.
|
||||
// Let's use `client.decryptDirectMessageEvent` which wraps `decryptDM`.
|
||||
// But the prompt asked to "attempt decryption using the appropriate decryptor helper (see js/dmDecryptor.js / docs)".
|
||||
// So I'll call decryptDM directly to verify strict compliance with that instruction.
|
||||
|
||||
const decryptContext = await client.buildDmDecryptContext(EPHEMERAL_PK);
|
||||
const decryptionResult = await decryptDM(fetchedDM, decryptContext);
|
||||
|
||||
if (!decryptionResult.ok) {
|
||||
console.error('[Test C] Decryption errors:', decryptionResult.errors);
|
||||
throw new Error('[Test C] Decryption failed.');
|
||||
}
|
||||
|
||||
if (decryptionResult.plaintext !== dmMessage) {
|
||||
throw new Error(`[Test C] Decryption mismatch: expected "${dmMessage}", got "${decryptionResult.plaintext}"`);
|
||||
}
|
||||
|
||||
console.log(`[Test C] Decrypted text: "${decryptionResult.plaintext}"`);
|
||||
console.log('[Test C] SUCCESS: Encrypted DM roundtrip verified.');
|
||||
|
||||
|
||||
} catch (err) {
|
||||
console.error('\n[Error] Test failed:', err);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
console.log('\n[Teardown] Cleaning up...');
|
||||
if (client && client.pool) {
|
||||
// Close pool connections to allow exit
|
||||
if (typeof client.pool.close === 'function') {
|
||||
client.pool.close(client.relays);
|
||||
}
|
||||
}
|
||||
if (relayServer) {
|
||||
await relayServer.close();
|
||||
console.log('[Teardown] Local relay stopped.');
|
||||
}
|
||||
console.log('[Teardown] Done.');
|
||||
process.exit(0);
|
||||
const received = messages.find(m => m.plaintext === message);
|
||||
if (!received) {
|
||||
console.log("Received messages:", messages.map(m => m.plaintext));
|
||||
throw new Error("Bob did not find the specific message");
|
||||
}
|
||||
|
||||
if (received.sender.pubkey !== alicePk) {
|
||||
console.log(`[Test C] Sender mismatch: Expected ${alicePk}, got ${received.sender.pubkey}`);
|
||||
throw new Error("Sender mismatch");
|
||||
}
|
||||
|
||||
console.log("[Test C] PASSED");
|
||||
testsPassed++;
|
||||
} catch (e) {
|
||||
console.error("[Test C] FAILED:", e);
|
||||
}
|
||||
|
||||
// --- Summary ---
|
||||
console.log("\n---------------------------------------------------");
|
||||
console.log(`Tests Completed: ${testsPassed}/${testsTotal}`);
|
||||
|
||||
aliceClient.logout();
|
||||
bobClient.logout();
|
||||
|
||||
if (testsPassed === testsTotal) {
|
||||
console.log("ALL TESTS PASSED");
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.error("SOME TESTS FAILED");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
runInteropTests();
|
||||
runTests().catch(e => {
|
||||
console.error("Unhandled execution error:", e);
|
||||
process.exit(1);
|
||||
});
|
||||
+240
-269
@@ -1,286 +1,257 @@
|
||||
|
||||
import './setup-test-env.js';
|
||||
import { indexedDB, IDBKeyRange } from "fake-indexeddb";
|
||||
import { WebSocket } from 'ws';
|
||||
import * as NostrTools from 'nostr-tools';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
// --- Polyfills ---
|
||||
if (!global.indexedDB) {
|
||||
global.indexedDB = indexedDB;
|
||||
}
|
||||
if (!global.IDBKeyRange) {
|
||||
global.IDBKeyRange = IDBKeyRange;
|
||||
}
|
||||
if (!global.NostrTools) {
|
||||
global.NostrTools = NostrTools;
|
||||
}
|
||||
// setup-test-env.js handles WebSocket, crypto, localStorage, window, self, navigator
|
||||
|
||||
// --- Imports from Codebase ---
|
||||
// Note: We need to use relative paths from scripts/agent/
|
||||
import { startRelay } from './load-test-relay.mjs';
|
||||
import { NostrClient } from '../../js/nostr/client.js';
|
||||
import { decryptDM } from '../../js/dmDecryptor.js';
|
||||
import { startRelay } from './load-test-relay.mjs';
|
||||
import {
|
||||
buildVideoPostEvent,
|
||||
buildLegacyDirectMessageEvent
|
||||
} from '../../js/nostrEventSchemas.js';
|
||||
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// --- Configuration ---
|
||||
const ARTIFACTS_DIR = 'artifacts';
|
||||
if (!fs.existsSync(ARTIFACTS_DIR)) {
|
||||
fs.mkdirSync(ARTIFACTS_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
const RELAY_PORT = 8890;
|
||||
const RELAY_PORT = 8899;
|
||||
const RELAY_URL = `ws://localhost:${RELAY_PORT}`;
|
||||
const HTTP_PORT = 8000;
|
||||
const HTTP_URL = `http://localhost:${HTTP_PORT}`;
|
||||
|
||||
// --- Logging ---
|
||||
const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, ''); // YYYYMMDD
|
||||
const logFile = path.join(ARTIFACTS_DIR, `smoke-${dateStr}.log`);
|
||||
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
||||
// Artifacts
|
||||
const ARTIFACTS_DIR = 'artifacts';
|
||||
const LOG_FILE = path.join(ARTIFACTS_DIR, `smoke-${new Date().toISOString().split('T')[0].replace(/-/g, '')}.log`);
|
||||
const SCREENSHOT_FILE = path.join(ARTIFACTS_DIR, 'smoke-test-ui.png');
|
||||
|
||||
function log(msg, ...args) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const message = `[${timestamp}] ${msg} ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : a).join(' ')}`;
|
||||
console.log(message);
|
||||
logStream.write(message + '\n');
|
||||
// Logger
|
||||
function log(message) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const line = `[${timestamp}] ${message}`;
|
||||
console.log(line);
|
||||
fs.appendFileSync(LOG_FILE, line + '\n');
|
||||
}
|
||||
|
||||
function error(msg, ...args) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const message = `[${timestamp}] [ERROR] ${msg} ${args.map(a => (a instanceof Error ? a.stack : (typeof a === 'object' ? JSON.stringify(a) : a))).join(' ')}`;
|
||||
console.error(message);
|
||||
logStream.write(message + '\n');
|
||||
// Generate Keys
|
||||
const bytesToHex = (bytes) => Buffer.from(bytes).toString('hex');
|
||||
const EPHEMERAL_SK_BYTES = generateSecretKey();
|
||||
const EPHEMERAL_SK = bytesToHex(EPHEMERAL_SK_BYTES);
|
||||
const EPHEMERAL_PK = getPublicKey(EPHEMERAL_SK_BYTES);
|
||||
|
||||
async function startHttpServer() {
|
||||
log('Starting HTTP Server...');
|
||||
const serve = spawn('npx', ['serve', '.', '-p', String(HTTP_PORT)], {
|
||||
stdio: 'ignore',
|
||||
shell: true
|
||||
});
|
||||
|
||||
const maxRetries = 20;
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
await fetch(HTTP_URL);
|
||||
log('HTTP Server is ready.');
|
||||
return serve;
|
||||
} catch (e) {
|
||||
await new Promise(r => setTimeout(r, 250));
|
||||
}
|
||||
}
|
||||
serve.kill();
|
||||
throw new Error('HTTP Server failed to start within timeout.');
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
async function waitForServer(url, timeoutMs = 10000) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
async function runSmokeTest() {
|
||||
// Ensure artifacts dir
|
||||
if (!fs.existsSync(ARTIFACTS_DIR)) {
|
||||
fs.mkdirSync(ARTIFACTS_DIR);
|
||||
}
|
||||
fs.writeFileSync(LOG_FILE, ''); // Clear log
|
||||
|
||||
let relayServer;
|
||||
let httpServer;
|
||||
let browser;
|
||||
let nodeClient;
|
||||
let exitCode = 0;
|
||||
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (res.ok) return true;
|
||||
} catch (e) {
|
||||
// ignore
|
||||
log('--- Smoke Test Started ---');
|
||||
log(`Ephemeral Pubkey: ${EPHEMERAL_PK}`);
|
||||
|
||||
// 1. Start Infrastructure
|
||||
relayServer = startRelay(RELAY_PORT);
|
||||
httpServer = await startHttpServer();
|
||||
|
||||
// 2. Initialize Node Client
|
||||
log('Initializing Node Client...');
|
||||
nodeClient = new NostrClient();
|
||||
nodeClient.relays = [RELAY_URL];
|
||||
nodeClient.writeRelays = [RELAY_URL];
|
||||
nodeClient.readRelays = [RELAY_URL];
|
||||
|
||||
await nodeClient.registerPrivateKeySigner({ privateKey: EPHEMERAL_SK });
|
||||
await nodeClient.init();
|
||||
log('Node Client connected.');
|
||||
|
||||
// 3. Publish Video Event (Node)
|
||||
log('Node: Publishing Video Post...');
|
||||
const videoEventTemplate = buildVideoPostEvent({
|
||||
pubkey: EPHEMERAL_PK,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
dTagValue: `smoke-test-${Date.now()}`,
|
||||
content: {
|
||||
version: 3,
|
||||
title: 'Smoke Test Video',
|
||||
url: 'https://example.com/video.mp4',
|
||||
description: 'A video to verify feed rendering.',
|
||||
mode: 'live',
|
||||
videoRootId: `smoke-root-${Date.now()}`
|
||||
}
|
||||
});
|
||||
|
||||
const { signedEvent: publishedVideo } = await nodeClient.signAndPublishEvent(videoEventTemplate, {
|
||||
context: 'smoke-video'
|
||||
});
|
||||
log(`Node: Published Video ID: ${publishedVideo.id}`);
|
||||
|
||||
// 4. Publish DM (Node -> Self)
|
||||
log('Node: Publishing Encrypted DM...');
|
||||
const dmMessage = "Smoke Test Secret Message";
|
||||
const signer = await nodeClient.ensureActiveSignerForPubkey(EPHEMERAL_PK);
|
||||
const ciphertext = await signer.nip04Encrypt(EPHEMERAL_PK, dmMessage);
|
||||
|
||||
const dmTemplate = buildLegacyDirectMessageEvent({
|
||||
pubkey: EPHEMERAL_PK,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
recipientPubkey: EPHEMERAL_PK,
|
||||
ciphertext: ciphertext
|
||||
});
|
||||
|
||||
const { signedEvent: publishedDM } = await nodeClient.signAndPublishEvent(dmTemplate, {
|
||||
context: 'smoke-dm'
|
||||
});
|
||||
log(`Node: Published DM ID: ${publishedDM.id}`);
|
||||
|
||||
// 5. Browser Test (Playwright)
|
||||
log('Browser: Launching...');
|
||||
browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext();
|
||||
|
||||
// Inject LocalStorage for Auto-Login and Relay Config
|
||||
const savedProfiles = {
|
||||
entries: [{ pubkey: EPHEMERAL_PK, authType: "nip07" }],
|
||||
activePubkey: EPHEMERAL_PK,
|
||||
version: 1
|
||||
};
|
||||
const relayList = [{ url: RELAY_URL, mode: "both" }];
|
||||
|
||||
// Note: Key format derived from investigation: bitvid:profile:<pubkey>:relayList:v1
|
||||
const relayStorageKey = `bitvid:profile:${EPHEMERAL_PK}:relayList:v1`;
|
||||
|
||||
await context.addInitScript(({ profiles, relays, relayKey }) => {
|
||||
// Disable Whitelist Mode to allow fresh pubkey login
|
||||
window.localStorage.setItem('bitvid_admin_whitelist_mode', 'false');
|
||||
|
||||
window.localStorage.setItem('bitvid:savedProfiles:v1', JSON.stringify(profiles));
|
||||
window.localStorage.setItem(relayKey, JSON.stringify(relays));
|
||||
// Mock NIP-07 to avoid errors if app tries to use it (though we won't sign in browser)
|
||||
window.nostr = {
|
||||
getPublicKey: async () => profiles.activePubkey,
|
||||
signEvent: async () => { throw new Error("Mock NIP-07 cannot sign"); }
|
||||
};
|
||||
}, { profiles: savedProfiles, relays: relayList, relayKey: relayStorageKey });
|
||||
|
||||
const page = await context.newPage();
|
||||
|
||||
log(`Browser: Navigating to ${HTTP_URL}...`);
|
||||
|
||||
// Capture Console
|
||||
page.on('console', msg => log(`[Browser Console] ${msg.type()}: ${msg.text()}`));
|
||||
page.on('pageerror', err => log(`[Browser Error] ${err.message}`));
|
||||
|
||||
await page.goto(HTTP_URL);
|
||||
|
||||
// Verify Login (Profile Button presence)
|
||||
log('Browser: Verifying Login...');
|
||||
// #profileButton might be hidden initially, wait for it
|
||||
try {
|
||||
await page.waitForSelector('#profileButton', { state: 'visible', timeout: 15000 });
|
||||
log('Browser: Login verified (Profile button visible).');
|
||||
} catch (e) {
|
||||
log('Browser: Login verification failed (Profile button not found). Dumping localStorage...');
|
||||
const storage = await page.evaluate(() => JSON.stringify(localStorage));
|
||||
log(`[Browser Storage] ${storage}`);
|
||||
await page.screenshot({ path: path.join(ARTIFACTS_DIR, 'smoke-fail-login.png') });
|
||||
}
|
||||
|
||||
// Verify Feed (Video Card presence)
|
||||
log('Browser: Verifying Video in Feed...');
|
||||
// Look for the video title
|
||||
try {
|
||||
// Wait longer for feed to load (relays might be slow)
|
||||
await page.waitForSelector(`text=Smoke Test Video`, { timeout: 60000 });
|
||||
log('Browser: Video found in feed!');
|
||||
} catch (e) {
|
||||
log('Browser: Video NOT found in feed. Taking screenshot...');
|
||||
await page.screenshot({ path: path.join(ARTIFACTS_DIR, 'smoke-fail-feed.png') });
|
||||
|
||||
// Debug: Check relays
|
||||
const relayState = await page.evaluate(() => {
|
||||
// Try to access global nostrClient if exposed, or check localStorage again
|
||||
return {
|
||||
localStorage: JSON.stringify(localStorage),
|
||||
// If we can't access client directly, we rely on logs
|
||||
};
|
||||
});
|
||||
log(`[Browser Debug] Storage: ${relayState.localStorage}`);
|
||||
|
||||
throw new Error('Browser: Video NOT found in feed.');
|
||||
}
|
||||
|
||||
// Screenshot
|
||||
await page.screenshot({ path: SCREENSHOT_FILE });
|
||||
log(`Browser: Screenshot saved to ${SCREENSHOT_FILE}`);
|
||||
|
||||
// 6. Node Verification (DM Decrypt)
|
||||
log('Node: Verifying DM Decryption...');
|
||||
// Fetch back
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
const fetchedDM = await nodeClient.fetchRawEventById(publishedDM.id);
|
||||
if (!fetchedDM) {
|
||||
throw new Error('Node: Failed to fetch DM back.');
|
||||
}
|
||||
|
||||
const decryptContext = await nodeClient.buildDmDecryptContext(EPHEMERAL_PK);
|
||||
const decryptionResult = await decryptDM(fetchedDM, decryptContext);
|
||||
|
||||
if (!decryptionResult.ok) {
|
||||
log(`Node: Decryption failed: ${JSON.stringify(decryptionResult.errors)}`);
|
||||
throw new Error('Node: Decryption failed.');
|
||||
}
|
||||
|
||||
if (decryptionResult.plaintext !== dmMessage) {
|
||||
throw new Error(`Node: Decryption mismatch. Got "${decryptionResult.plaintext}"`);
|
||||
}
|
||||
log(`Node: DM Decrypted successfully: "${decryptionResult.plaintext}"`);
|
||||
|
||||
log('--- Smoke Test PASSED ---');
|
||||
|
||||
} catch (err) {
|
||||
log(`--- Smoke Test FAILED: ${err.message} ---`);
|
||||
if (err.stack) log(err.stack);
|
||||
exitCode = 1;
|
||||
} finally {
|
||||
// Cleanup
|
||||
log('Cleaning up...');
|
||||
if (nodeClient && nodeClient.pool) {
|
||||
// Close pool connections
|
||||
if (typeof nodeClient.pool.close === 'function') {
|
||||
nodeClient.pool.close(nodeClient.relays);
|
||||
}
|
||||
}
|
||||
if (browser) await browser.close();
|
||||
if (httpServer) httpServer.kill();
|
||||
if (relayServer) await relayServer.close();
|
||||
|
||||
process.exit(exitCode);
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Main Test Flow ---
|
||||
async function run() {
|
||||
let relayServer = null;
|
||||
let httpServer = null;
|
||||
let client = null;
|
||||
let success = true;
|
||||
|
||||
try {
|
||||
log('--- Starting Smoke Test ---');
|
||||
|
||||
// 1. Start Relay
|
||||
log(`Starting local relay on port ${RELAY_PORT}...`);
|
||||
relayServer = startRelay(RELAY_PORT);
|
||||
// Wait a bit for relay to be ready (startRelay is synchronous in setup but async in effect?)
|
||||
// It uses WebSocketServer which starts immediately.
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
log('Relay started.');
|
||||
|
||||
// 2. Start HTTP Server
|
||||
log(`Starting HTTP server (npx serve) on port ${HTTP_PORT}...`);
|
||||
httpServer = spawn('npx', ['serve', '-p', String(HTTP_PORT)], {
|
||||
stdio: 'ignore', // or 'inherit' to see output
|
||||
detached: false
|
||||
});
|
||||
|
||||
const serverReady = await waitForServer(HTTP_URL);
|
||||
if (!serverReady) {
|
||||
throw new Error(`HTTP server did not start on ${HTTP_URL} within timeout.`);
|
||||
}
|
||||
log('HTTP server is ready.');
|
||||
|
||||
// 3. Init NostrClient
|
||||
log('Initializing NostrClient...');
|
||||
client = new NostrClient();
|
||||
client.relays = [RELAY_URL];
|
||||
client.readRelays = [RELAY_URL];
|
||||
client.writeRelays = [RELAY_URL];
|
||||
|
||||
// Override dev logger to avoid pollution if needed, or let it log
|
||||
// We can rely on standard logging.
|
||||
|
||||
await client.init();
|
||||
log(`NostrClient initialized and connected to ${RELAY_URL}`);
|
||||
|
||||
// 4. Login (Ephemeral)
|
||||
log('Generating ephemeral keys for Sender...');
|
||||
const senderSk = NostrTools.generateSecretKey();
|
||||
const senderSkHex = Buffer.from(senderSk).toString('hex');
|
||||
const senderPk = NostrTools.getPublicKey(senderSk);
|
||||
|
||||
log(`Sender Pubkey: ${senderPk}`);
|
||||
|
||||
await client.registerPrivateKeySigner({ privateKey: senderSkHex, persist: false });
|
||||
client.pubkey = senderPk; // Manually set logged-in user
|
||||
|
||||
if (client.pubkey !== senderPk) {
|
||||
throw new Error(`Client pubkey mismatch. Expected ${senderPk}, got ${client.pubkey}`);
|
||||
}
|
||||
log('Sender logged in.');
|
||||
|
||||
// 5. Publish Video
|
||||
log('Publishing test video...');
|
||||
const videoPayload = {
|
||||
title: `Smoke Test Video ${Date.now()}`,
|
||||
description: 'Smoke test description',
|
||||
magnet: `magnet:?xt=urn:btih:${Math.random().toString(16).slice(2)}`,
|
||||
url: `${HTTP_URL}/test.mp4`, // Fake URL
|
||||
thumbnail: `${HTTP_URL}/thumb.jpg`,
|
||||
mode: 'live',
|
||||
videoRootId: `smoke-${Date.now()}`,
|
||||
isNsfw: false,
|
||||
isForKids: true
|
||||
};
|
||||
|
||||
const publishedEvent = await client.publishVideo(videoPayload, senderPk);
|
||||
if (!publishedEvent || !publishedEvent.id) {
|
||||
throw new Error('Failed to publish video event.');
|
||||
}
|
||||
log(`Video published. Event ID: ${publishedEvent.id}`);
|
||||
|
||||
// Verify Read Back
|
||||
log('Verifying video can be fetched...');
|
||||
// We use subscribeVideos for main feed, or getEventById.
|
||||
// Let's use getEventById for direct verification.
|
||||
const fetched = await client.getEventById(publishedEvent.id);
|
||||
if (!fetched) {
|
||||
throw new Error('Failed to fetch published video.');
|
||||
}
|
||||
if (fetched.title !== videoPayload.title) {
|
||||
throw new Error(`Fetched video title mismatch. Expected "${videoPayload.title}", got "${fetched.title}"`);
|
||||
}
|
||||
log('Video fetched successfully.');
|
||||
|
||||
// 6. DM Flow
|
||||
log('Testing DM Flow...');
|
||||
const recipientSk = NostrTools.generateSecretKey();
|
||||
const recipientSkHex = Buffer.from(recipientSk).toString('hex');
|
||||
const recipientPk = NostrTools.getPublicKey(recipientSk);
|
||||
const recipientNpub = NostrTools.nip19.npubEncode(recipientPk);
|
||||
|
||||
log(`Recipient Pubkey: ${recipientPk}`);
|
||||
|
||||
const dmMessage = `Smoke Test Message ${Date.now()}`;
|
||||
|
||||
// Send DM
|
||||
const sendResult = await client.sendDirectMessage(recipientNpub, dmMessage);
|
||||
if (!sendResult.ok) {
|
||||
throw new Error(`Failed to send DM: ${sendResult.error}`);
|
||||
}
|
||||
log('DM sent successfully.');
|
||||
|
||||
// Verify Decryption (Simulate Recipient)
|
||||
log('Verifying DM decryption (Recipient view)...');
|
||||
|
||||
// We need to find the DM event.
|
||||
// Since we don't have a second client syncing, we can manually query the pool/relay.
|
||||
const filter = {
|
||||
kinds: [4, 1059], // 4 for legacy, 1059 for NIP-17
|
||||
'#p': [recipientPk],
|
||||
limit: 1
|
||||
};
|
||||
|
||||
// wait a moment for propagation
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
const events = await client.pool.list([RELAY_URL], [filter]);
|
||||
if (events.length === 0) {
|
||||
throw new Error('Recipient could not find DM event on relay.');
|
||||
}
|
||||
|
||||
const dmEvent = events[0];
|
||||
log(`Found DM event kind ${dmEvent.kind} id ${dmEvent.id}`);
|
||||
|
||||
// Construct decryption context for decryptDM
|
||||
// We need a decryptor for the recipient.
|
||||
// Since we have the recipient's private key, we can use NostrTools.nip04 or nip44.
|
||||
|
||||
const decryptors = [];
|
||||
|
||||
// NIP-04 Decryptor
|
||||
decryptors.push({
|
||||
scheme: 'nip04',
|
||||
decrypt: async (remotePubkey, ciphertext) => {
|
||||
return await NostrTools.nip04.decrypt(recipientSkHex, remotePubkey, ciphertext);
|
||||
}
|
||||
});
|
||||
|
||||
// NIP-44 Decryptor
|
||||
decryptors.push({
|
||||
scheme: 'nip44',
|
||||
supportsGiftWrap: true,
|
||||
decrypt: (remotePubkey, ciphertext) => {
|
||||
const conversationKey = NostrTools.nip44.v2.utils.getConversationKey(recipientSk, remotePubkey);
|
||||
return NostrTools.nip44.v2.decrypt(ciphertext, conversationKey);
|
||||
}
|
||||
});
|
||||
|
||||
const context = {
|
||||
actorPubkey: recipientPk,
|
||||
decryptors
|
||||
};
|
||||
|
||||
const decryptResult = await decryptDM(dmEvent, context);
|
||||
|
||||
if (!decryptResult.ok) {
|
||||
throw new Error(`DM decryption failed: ${JSON.stringify(decryptResult.errors)}`);
|
||||
}
|
||||
|
||||
if (decryptResult.plaintext !== dmMessage) {
|
||||
throw new Error(`DM plaintext mismatch. Expected "${dmMessage}", got "${decryptResult.plaintext}"`);
|
||||
}
|
||||
|
||||
log('DM verified successfully.');
|
||||
|
||||
} catch (err) {
|
||||
success = false;
|
||||
error('Smoke Test Failed!', err);
|
||||
} finally {
|
||||
// Report
|
||||
const summary = {
|
||||
timestamp: new Date().toISOString(),
|
||||
success,
|
||||
logFile
|
||||
};
|
||||
const summaryFile = path.join(ARTIFACTS_DIR, `smoke-summary-${dateStr}.json`);
|
||||
fs.writeFileSync(summaryFile, JSON.stringify(summary, null, 2));
|
||||
log(`Summary saved to ${summaryFile}`);
|
||||
|
||||
// Teardown
|
||||
log('Teardown...');
|
||||
if (client) {
|
||||
// client.logout(); // Cleans up
|
||||
// Close pool connections?
|
||||
// client.pool.close(client.relays); // SimplePool doesn't have explicit close usually?
|
||||
// NostrTools SimplePool usually manages connections.
|
||||
// We can just rely on process exit.
|
||||
}
|
||||
|
||||
if (relayServer) {
|
||||
relayServer.close();
|
||||
log('Relay stopped.');
|
||||
}
|
||||
|
||||
if (httpServer) {
|
||||
httpServer.kill();
|
||||
log('HTTP server stopped.');
|
||||
}
|
||||
|
||||
log('Done.');
|
||||
process.exit(success ? 0 : 1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
runSmokeTest();
|
||||
@@ -147,19 +147,26 @@ function collectGenericLogs(filepath, sourceName) {
|
||||
for (const line of lines) {
|
||||
// Simple heuristic for generic logs: look for "Error:" or "Exception:"
|
||||
// and capture following lines that look like stack traces (start with 'at ' or are indented)
|
||||
// Also support bracketed log levels like [ERROR], [FAIL], [CRITICAL], [WARN]
|
||||
|
||||
const isErrorStart = /Error:|Exception:|FAIL|CRITICAL/.test(line);
|
||||
const errorMatch = line.match(/(?:\[\s*(?:ERROR|FAIL|CRITICAL|EXCEPTION|WARN|WARNING)\s*\])|(?:\b(?:Error|Exception|Warning):)|(?:\b(?:FAIL|CRITICAL)\b)/i);
|
||||
const isErrorStart = !!errorMatch;
|
||||
const isStackLine = /^\s+at /.test(line) || /^\s+/.test(line); // Indented lines often follow errors
|
||||
|
||||
if (isErrorStart) {
|
||||
if (currentError) {
|
||||
errors.push(currentError);
|
||||
}
|
||||
|
||||
let severity = 'High';
|
||||
if (line.match(/CRITICAL/i)) severity = 'Critical';
|
||||
else if (line.match(/WARN|WARNING/i)) severity = 'Medium';
|
||||
|
||||
currentError = {
|
||||
source: sourceName,
|
||||
title: sanitize(line.trim()),
|
||||
details: [sanitize(line.trim())],
|
||||
severity: 'High'
|
||||
severity: severity
|
||||
};
|
||||
} else if (currentError && isStackLine) {
|
||||
currentError.details.push(sanitize(line.trim()));
|
||||
|
||||
+246
-247
@@ -1,9 +1,48 @@
|
||||
#!/usr/bin/env node
|
||||
// scripts/agent/validate-events.mjs
|
||||
import WebSocket from 'ws';
|
||||
import { webcrypto } from 'node:crypto';
|
||||
|
||||
// Polyfill WebSocket
|
||||
if (typeof globalThis.WebSocket === 'undefined') {
|
||||
globalThis.WebSocket = WebSocket;
|
||||
}
|
||||
|
||||
// Polyfill window and self
|
||||
if (typeof globalThis.window === 'undefined') {
|
||||
globalThis.window = globalThis;
|
||||
}
|
||||
if (typeof globalThis.self === 'undefined') {
|
||||
globalThis.self = globalThis;
|
||||
}
|
||||
|
||||
// Polyfill crypto
|
||||
if (typeof globalThis.crypto === 'undefined') {
|
||||
globalThis.crypto = webcrypto;
|
||||
}
|
||||
|
||||
// Polyfill localStorage
|
||||
if (typeof globalThis.localStorage === 'undefined') {
|
||||
const storage = new Map();
|
||||
globalThis.localStorage = {
|
||||
getItem: (k) => storage.get(k) || null,
|
||||
setItem: (k, v) => storage.set(String(k), String(v)),
|
||||
removeItem: (k) => storage.delete(k),
|
||||
clear: () => storage.clear(),
|
||||
key: (i) => Array.from(storage.keys())[i] || null,
|
||||
get length() { return storage.size; }
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof globalThis.window.localStorage === 'undefined') {
|
||||
globalThis.window.localStorage = globalThis.localStorage;
|
||||
}
|
||||
|
||||
// Mock navigator
|
||||
if (typeof globalThis.navigator === 'undefined') {
|
||||
globalThis.navigator = { userAgent: 'node' };
|
||||
}
|
||||
|
||||
import { execSync } from "child_process";
|
||||
import {
|
||||
NOTE_TYPES,
|
||||
validateEventStructure,
|
||||
buildVideoPostEvent,
|
||||
buildHttpAuthEvent,
|
||||
buildReportEvent,
|
||||
@@ -28,352 +67,312 @@ import {
|
||||
buildBlockListEvent,
|
||||
buildHashtagPreferenceEvent,
|
||||
buildAdminListEvent,
|
||||
} from "../../js/nostrEventSchemas.js";
|
||||
validateEventStructure,
|
||||
NOTE_TYPES,
|
||||
ADMIN_LIST_IDENTIFIERS
|
||||
} from '../../js/nostrEventSchemas.js';
|
||||
import { buildNip71VideoEvent } from '../../js/nostr/nip71.js';
|
||||
|
||||
const PUBKEY = "0000000000000000000000000000000000000000000000000000000000000001";
|
||||
const CREATED_AT = 1700000000;
|
||||
// Note: Most builders are defined in js/nostrEventSchemas.js and are already instrumented
|
||||
// with validation checks (validateEventAgainstSchema) when running in Dev Mode.
|
||||
// buildNip71VideoEvent was instrumented in js/nostr/nip71.js as part of this work.
|
||||
// This script verifies that all builders produce valid events according to the schema.
|
||||
|
||||
const IGNORED_BUILDERS = new Set([
|
||||
"buildShareUrlFromEventId", // Method in js/app.js, not an event builder
|
||||
"buildShareUrlFromEvent", // Substring match or alias
|
||||
"buildListEvent", // Local helper in js/adminListStore.js wrapping buildAdminListEvent
|
||||
"buildProfileFromEvent", // Local helper in js/services/authService.js
|
||||
]);
|
||||
|
||||
const KNOWN_BUILDERS = {
|
||||
buildVideoPostEvent: {
|
||||
builder: buildVideoPostEvent,
|
||||
const testCases = [
|
||||
{
|
||||
name: 'Video Post',
|
||||
type: NOTE_TYPES.VIDEO_POST,
|
||||
builder: buildVideoPostEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
dTagValue: "test-video-id",
|
||||
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
created_at: 1234567890,
|
||||
dTagValue: 'test-video',
|
||||
content: {
|
||||
version: 3,
|
||||
title: "Test Video",
|
||||
videoRootId: "test-video-id",
|
||||
infoHash: "0123456789abcdef0123456789abcdef01234567",
|
||||
},
|
||||
title: 'Test Video',
|
||||
videoRootId: 'test-root',
|
||||
}
|
||||
}
|
||||
},
|
||||
buildHttpAuthEvent: {
|
||||
builder: buildHttpAuthEvent,
|
||||
{
|
||||
name: 'HTTP Auth',
|
||||
type: NOTE_TYPES.HTTP_AUTH,
|
||||
builder: buildHttpAuthEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
url: "https://example.com/auth",
|
||||
method: "GET",
|
||||
payload: "hash",
|
||||
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
created_at: 1234567890,
|
||||
url: 'https://example.com',
|
||||
method: 'GET'
|
||||
}
|
||||
},
|
||||
buildReportEvent: {
|
||||
builder: buildReportEvent,
|
||||
{
|
||||
name: 'Report',
|
||||
type: NOTE_TYPES.REPORT,
|
||||
builder: buildReportEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
eventId: "e".repeat(64),
|
||||
reportType: "nudity",
|
||||
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
created_at: 1234567890,
|
||||
eventId: '0000000000000000000000000000000000000000000000000000000000000002',
|
||||
reportType: 'nudity'
|
||||
}
|
||||
},
|
||||
buildVideoMirrorEvent: {
|
||||
builder: buildVideoMirrorEvent,
|
||||
{
|
||||
name: 'Video Mirror',
|
||||
type: NOTE_TYPES.VIDEO_MIRROR,
|
||||
builder: buildVideoMirrorEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
tags: [["url", "https://example.com/video.mp4"], ["m", "video/mp4"]],
|
||||
content: "Mirroring video",
|
||||
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
created_at: 1234567890,
|
||||
content: 'Alt text'
|
||||
}
|
||||
},
|
||||
buildRepostEvent: {
|
||||
builder: buildRepostEvent,
|
||||
{
|
||||
name: 'Repost',
|
||||
type: NOTE_TYPES.REPOST,
|
||||
builder: buildRepostEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
eventId: "e".repeat(64),
|
||||
eventRelay: "wss://relay.example.com",
|
||||
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
created_at: 1234567890,
|
||||
eventId: '0000000000000000000000000000000000000000000000000000000000000002',
|
||||
targetKind: 1
|
||||
}
|
||||
},
|
||||
buildShareEvent: {
|
||||
builder: buildShareEvent,
|
||||
{
|
||||
name: 'Share',
|
||||
type: NOTE_TYPES.SHARE,
|
||||
builder: buildShareEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
video: { id: "e".repeat(64), pubkey: PUBKEY },
|
||||
relays: ["wss://relay.example.com"],
|
||||
content: "Check this out",
|
||||
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
created_at: 1234567890,
|
||||
content: 'Check this out',
|
||||
video: { id: '0000000000000000000000000000000000000000000000000000000000000002' }
|
||||
}
|
||||
},
|
||||
buildRelayListEvent: {
|
||||
builder: buildRelayListEvent,
|
||||
{
|
||||
name: 'Relay List',
|
||||
type: NOTE_TYPES.RELAY_LIST,
|
||||
builder: buildRelayListEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
relays: [{ url: "wss://relay.example.com", read: true, write: true }],
|
||||
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
created_at: 1234567890,
|
||||
relays: ['wss://relay.example.com']
|
||||
}
|
||||
},
|
||||
buildDmRelayListEvent: {
|
||||
builder: buildDmRelayListEvent,
|
||||
{
|
||||
name: 'DM Relay List',
|
||||
type: NOTE_TYPES.DM_RELAY_LIST,
|
||||
builder: buildDmRelayListEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
relays: ["wss://relay.example.com"],
|
||||
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
created_at: 1234567890,
|
||||
relays: ['wss://relay.example.com']
|
||||
}
|
||||
},
|
||||
buildProfileMetadataEvent: {
|
||||
builder: buildProfileMetadataEvent,
|
||||
{
|
||||
name: 'Profile Metadata',
|
||||
type: NOTE_TYPES.PROFILE_METADATA,
|
||||
builder: buildProfileMetadataEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
metadata: { name: "Test User", about: "Testing" },
|
||||
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
created_at: 1234567890,
|
||||
metadata: { name: 'Alice' }
|
||||
}
|
||||
},
|
||||
buildMuteListEvent: {
|
||||
builder: buildMuteListEvent,
|
||||
{
|
||||
name: 'Mute List',
|
||||
type: NOTE_TYPES.MUTE_LIST,
|
||||
builder: buildMuteListEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
pTags: [PUBKEY],
|
||||
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
created_at: 1234567890,
|
||||
pTags: ['0000000000000000000000000000000000000000000000000000000000000002']
|
||||
}
|
||||
},
|
||||
buildDeletionEvent: {
|
||||
builder: buildDeletionEvent,
|
||||
{
|
||||
name: 'Deletion',
|
||||
type: NOTE_TYPES.DELETION,
|
||||
builder: buildDeletionEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
eventIds: ["e".repeat(64)],
|
||||
reason: "mistake",
|
||||
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
created_at: 1234567890,
|
||||
eventIds: ['0000000000000000000000000000000000000000000000000000000000000002']
|
||||
}
|
||||
},
|
||||
buildLegacyDirectMessageEvent: {
|
||||
builder: buildLegacyDirectMessageEvent,
|
||||
{
|
||||
name: 'Legacy DM',
|
||||
type: NOTE_TYPES.LEGACY_DM,
|
||||
builder: buildLegacyDirectMessageEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
recipientPubkey: PUBKEY,
|
||||
ciphertext: "encrypted",
|
||||
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
created_at: 1234567890,
|
||||
recipientPubkey: '0000000000000000000000000000000000000000000000000000000000000002',
|
||||
ciphertext: 'base64ciphertext'
|
||||
}
|
||||
},
|
||||
buildDmAttachmentEvent: {
|
||||
builder: buildDmAttachmentEvent,
|
||||
{
|
||||
name: 'DM Attachment',
|
||||
type: NOTE_TYPES.DM_ATTACHMENT,
|
||||
builder: buildDmAttachmentEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
recipientPubkey: PUBKEY,
|
||||
attachment: { url: "https://example.com/file", x: "hash", type: "image/jpeg" },
|
||||
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
created_at: 1234567890,
|
||||
recipientPubkey: '0000000000000000000000000000000000000000000000000000000000000002',
|
||||
attachment: { url: 'https://example.com/file.jpg', x: 'hash' }
|
||||
}
|
||||
},
|
||||
buildDmReadReceiptEvent: {
|
||||
builder: buildDmReadReceiptEvent,
|
||||
{
|
||||
name: 'DM Read Receipt',
|
||||
type: NOTE_TYPES.DM_READ_RECEIPT,
|
||||
builder: buildDmReadReceiptEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
recipientPubkey: PUBKEY,
|
||||
eventId: "e".repeat(64),
|
||||
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
created_at: 1234567890,
|
||||
recipientPubkey: '0000000000000000000000000000000000000000000000000000000000000002',
|
||||
eventId: '0000000000000000000000000000000000000000000000000000000000000002'
|
||||
}
|
||||
},
|
||||
buildDmTypingIndicatorEvent: {
|
||||
builder: buildDmTypingIndicatorEvent,
|
||||
{
|
||||
name: 'DM Typing Indicator',
|
||||
type: NOTE_TYPES.DM_TYPING,
|
||||
builder: buildDmTypingIndicatorEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
recipientPubkey: PUBKEY,
|
||||
expiresAt: CREATED_AT + 60,
|
||||
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
created_at: 1234567890,
|
||||
recipientPubkey: '0000000000000000000000000000000000000000000000000000000000000002',
|
||||
expiresAt: 1234567900
|
||||
}
|
||||
},
|
||||
buildViewEvent: {
|
||||
builder: buildViewEvent,
|
||||
{
|
||||
name: 'View Event',
|
||||
type: NOTE_TYPES.VIEW_EVENT,
|
||||
builder: buildViewEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
pointerValue: "e".repeat(64),
|
||||
pointerTag: ["e", "e".repeat(64)],
|
||||
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
created_at: 1234567890,
|
||||
pointerValue: '0000000000000000000000000000000000000000000000000000000000000002'
|
||||
}
|
||||
},
|
||||
buildZapRequestEvent: {
|
||||
builder: buildZapRequestEvent,
|
||||
{
|
||||
name: 'Zap Request',
|
||||
type: NOTE_TYPES.ZAP_REQUEST,
|
||||
builder: buildZapRequestEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
recipientPubkey: PUBKEY,
|
||||
amountSats: 100,
|
||||
relays: ["wss://relay.example.com"],
|
||||
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
created_at: 1234567890,
|
||||
recipientPubkey: '0000000000000000000000000000000000000000000000000000000000000002',
|
||||
amountSats: 100
|
||||
}
|
||||
},
|
||||
buildReactionEvent: {
|
||||
builder: buildReactionEvent,
|
||||
{
|
||||
name: 'Reaction',
|
||||
type: NOTE_TYPES.VIDEO_REACTION,
|
||||
builder: buildReactionEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
pointerValue: "e".repeat(64),
|
||||
targetPointer: { type: "e", value: "e".repeat(64) },
|
||||
content: "+",
|
||||
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
created_at: 1234567890,
|
||||
pointerValue: '0000000000000000000000000000000000000000000000000000000000000002',
|
||||
content: '+'
|
||||
}
|
||||
},
|
||||
buildCommentEvent: {
|
||||
builder: buildCommentEvent,
|
||||
{
|
||||
name: 'Comment',
|
||||
type: NOTE_TYPES.VIDEO_COMMENT,
|
||||
builder: buildCommentEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
videoEventId: "e".repeat(64),
|
||||
content: "Nice video",
|
||||
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
created_at: 1234567890,
|
||||
videoEventId: '0000000000000000000000000000000000000000000000000000000000000002',
|
||||
content: 'Nice video'
|
||||
}
|
||||
},
|
||||
buildWatchHistoryEvent: {
|
||||
builder: buildWatchHistoryEvent,
|
||||
{
|
||||
name: 'Watch History',
|
||||
type: NOTE_TYPES.WATCH_HISTORY,
|
||||
builder: buildWatchHistoryEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
monthIdentifier: "2023-01",
|
||||
content: { version: 2, month: "2023-01", items: [] },
|
||||
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
created_at: 1234567890,
|
||||
monthIdentifier: 'watch-history:2025-01',
|
||||
content: { version: 2, month: '2025-01', items: [] }
|
||||
}
|
||||
},
|
||||
buildSubscriptionListEvent: {
|
||||
builder: buildSubscriptionListEvent,
|
||||
{
|
||||
name: 'Subscription List',
|
||||
type: NOTE_TYPES.SUBSCRIPTION_LIST,
|
||||
builder: buildSubscriptionListEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
content: "encrypted-json",
|
||||
encryption: "nip44_v2",
|
||||
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
created_at: 1234567890,
|
||||
content: 'encrypted-payload'
|
||||
}
|
||||
},
|
||||
buildBlockListEvent: {
|
||||
builder: buildBlockListEvent,
|
||||
{
|
||||
name: 'Block List',
|
||||
type: NOTE_TYPES.USER_BLOCK_LIST,
|
||||
builder: buildBlockListEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
content: "encrypted-json",
|
||||
encryption: "nip44_v2",
|
||||
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
created_at: 1234567890,
|
||||
content: 'encrypted-payload'
|
||||
}
|
||||
},
|
||||
buildHashtagPreferenceEvent: {
|
||||
builder: buildHashtagPreferenceEvent,
|
||||
{
|
||||
name: 'Hashtag Preferences',
|
||||
type: NOTE_TYPES.HASHTAG_PREFERENCES,
|
||||
builder: buildHashtagPreferenceEvent,
|
||||
params: {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
content: "encrypted-json",
|
||||
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
created_at: 1234567890,
|
||||
content: JSON.stringify({ version: 1, interests: [], disinterests: [] })
|
||||
}
|
||||
},
|
||||
buildAdminListEvent: {
|
||||
builder: buildAdminListEvent,
|
||||
{
|
||||
name: 'Admin List (Moderation)',
|
||||
type: NOTE_TYPES.ADMIN_MODERATION_LIST,
|
||||
// Special handling for args
|
||||
args: ["moderation", {
|
||||
pubkey: PUBKEY,
|
||||
created_at: CREATED_AT,
|
||||
hexPubkeys: [PUBKEY],
|
||||
}]
|
||||
builder: (params) => buildAdminListEvent(ADMIN_LIST_IDENTIFIERS.moderation, params),
|
||||
params: {
|
||||
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
created_at: 1234567890,
|
||||
hexPubkeys: []
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function getUsedBuilders() {
|
||||
console.log("Scanning repo for event builder usage...");
|
||||
try {
|
||||
const output = execSync('grep -r "build[A-Za-z]\\+Event" js/', { encoding: 'utf-8' });
|
||||
const lines = output.split('\n');
|
||||
const usedBuilders = new Set();
|
||||
const regex = /(build[A-Za-z]+Event)/g;
|
||||
|
||||
lines.forEach(line => {
|
||||
// Skip the schemas file itself
|
||||
if (line.includes('js/nostrEventSchemas.js')) return;
|
||||
|
||||
const matches = line.match(regex);
|
||||
if (matches) {
|
||||
matches.forEach(match => usedBuilders.add(match));
|
||||
{
|
||||
name: 'NIP-71 Video',
|
||||
type: NOTE_TYPES.NIP71_VIDEO,
|
||||
builder: buildNip71VideoEvent,
|
||||
params: {
|
||||
pubkey: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
title: 'Test NIP-71',
|
||||
metadata: { kind: 21, title: 'Test NIP-71' },
|
||||
pointerIdentifiers: { videoRootId: 'test-root' }
|
||||
}
|
||||
});
|
||||
return usedBuilders;
|
||||
} catch (error) {
|
||||
// Grep returns status 1 if no matches found, which might throw an error in execSync
|
||||
// depending on the version/platform, or just empty output.
|
||||
console.error("Grep failed or found nothing:", error.message);
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
async function run() {
|
||||
const usedBuilders = getUsedBuilders();
|
||||
console.log(`Found ${usedBuilders.size} unique builder(s) used in the codebase.`);
|
||||
|
||||
// also check if we missed any exported builder in our KNOWN_BUILDERS list
|
||||
// by assuming KNOWN_BUILDERS contains all we want to support.
|
||||
|
||||
let failureCount = 0;
|
||||
const missingTests = [];
|
||||
|
||||
usedBuilders.forEach(builderName => {
|
||||
if (!KNOWN_BUILDERS[builderName] && !IGNORED_BUILDERS.has(builderName)) {
|
||||
missingTests.push(builderName);
|
||||
}
|
||||
});
|
||||
|
||||
if (missingTests.length > 0) {
|
||||
console.warn("\n[WARNING] The following builders are used in the repo but have no test case in this script:");
|
||||
missingTests.forEach(name => console.warn(` - ${name}`));
|
||||
// We don't fail the build for this yet, but we warn.
|
||||
// Or maybe we should fail? The prompt said "Check runtime event construction...".
|
||||
// Let's count it as a "gap" but allow passing if validation of known ones works.
|
||||
}
|
||||
|
||||
console.log("\nValidating builders...");
|
||||
|
||||
for (const [name, config] of Object.entries(KNOWN_BUILDERS)) {
|
||||
// If we want to be strict, we could check if it is used. But validation is good regardless.
|
||||
const isUsed = usedBuilders.has(name);
|
||||
const usageLabel = isUsed ? "(USED)" : "(UNUSED)";
|
||||
let failed = false;
|
||||
|
||||
for (const testCase of testCases) {
|
||||
try {
|
||||
let event;
|
||||
if (config.args) {
|
||||
event = config.builder(...config.args);
|
||||
} else {
|
||||
event = config.builder(config.params);
|
||||
}
|
||||
|
||||
const { valid, errors } = validateEventStructure(config.type, event);
|
||||
|
||||
if (valid) {
|
||||
console.log(`[PASS] ${name} ${usageLabel}`);
|
||||
} else {
|
||||
console.error(`[FAIL] ${name} ${usageLabel}`);
|
||||
errors.forEach((err) => console.error(` - ${err}`));
|
||||
failureCount++;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[ERROR] ${name} ${usageLabel} threw an exception:`, e);
|
||||
failureCount++;
|
||||
const event = testCase.builder(testCase.params);
|
||||
if (!event) {
|
||||
console.error(`[FAIL] ${testCase.name}: Builder returned null`);
|
||||
failed = true;
|
||||
continue;
|
||||
}
|
||||
const { valid, errors } = validateEventStructure(testCase.type, event);
|
||||
if (!valid) {
|
||||
console.error(`[FAIL] ${testCase.name}:`);
|
||||
errors.forEach(err => console.error(` - ${err}`));
|
||||
failed = true;
|
||||
} else {
|
||||
console.log(`[PASS] ${testCase.name}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[FAIL] ${testCase.name} threw error:`, err);
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (failureCount > 0) {
|
||||
console.error(`\nValidation failed with ${failureCount} errors.`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("\nAll known builders validated successfully.");
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
if (failed) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -26,9 +26,8 @@ function runCommand(command, args = [], options = {}) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function getChangedFiles() {
|
||||
function getChangedFiles(baseRef) {
|
||||
try {
|
||||
const baseRef = process.env.GITHUB_BASE_REF || 'main';
|
||||
const baseUrl = process.env.GITHUB_BASE_REPO_URL;
|
||||
let remote = 'origin';
|
||||
|
||||
@@ -64,7 +63,6 @@ function getChangedFiles() {
|
||||
console.error('Error getting changed files:', error.stderr || error.message);
|
||||
// Fallback: try 2-dot diff if 3-dot failed (e.g. shallow clone issues)
|
||||
try {
|
||||
const baseRef = process.env.GITHUB_BASE_REF || 'main';
|
||||
const remote = process.env.GITHUB_BASE_REPO_URL ? 'upstream' : 'origin';
|
||||
const cmd = `git diff --name-only ${remote}/${baseRef} HEAD`;
|
||||
console.log(`Retrying with 2-dot diff: ${cmd}`);
|
||||
@@ -77,8 +75,7 @@ function getChangedFiles() {
|
||||
}
|
||||
}
|
||||
|
||||
function checkReleaseChannel(changedFiles) {
|
||||
const baseRef = process.env.GITHUB_BASE_REF || 'main';
|
||||
function checkReleaseChannel(changedFiles, baseRef) {
|
||||
const warnings = [];
|
||||
|
||||
if (baseRef === 'main') {
|
||||
@@ -100,13 +97,11 @@ function checkReleaseChannel(changedFiles) {
|
||||
return warnings;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
console.log('Starting Automated PR Review...');
|
||||
let commentBody = '## 🤖 Automated PR Review\n\n';
|
||||
let hasFailures = false;
|
||||
async function reviewPR(prNumber, baseRef) {
|
||||
console.log(`Starting Automated PR Review for PR #${prNumber} (base: ${baseRef})...`);
|
||||
let commentBody = '## 🤖 Automated PR Review\n\n';
|
||||
let hasFailures = false;
|
||||
let hasFormatChanges = false;
|
||||
let microFixesStatus = '';
|
||||
|
||||
// 1. Install Dependencies
|
||||
if (process.env.CI || process.argv.includes('--force-ci')) {
|
||||
@@ -149,13 +144,13 @@ async function main() {
|
||||
}
|
||||
|
||||
// 5. Security/Protocol Review Flags
|
||||
const changedFiles = getChangedFiles();
|
||||
const changedFiles = getChangedFiles(baseRef);
|
||||
const sensitiveChanges = changedFiles.filter(file =>
|
||||
SENSITIVE_PATHS.some(pattern => pattern.test(file))
|
||||
);
|
||||
|
||||
// Release Channel Check
|
||||
const channelWarnings = checkReleaseChannel(changedFiles);
|
||||
const channelWarnings = checkReleaseChannel(changedFiles, baseRef);
|
||||
if (channelWarnings.length > 0) {
|
||||
commentBody += '### 🚀 Release Channel Checks\n\n';
|
||||
channelWarnings.forEach(w => commentBody += `${w}\n\n`);
|
||||
@@ -182,7 +177,7 @@ async function main() {
|
||||
commentBody += '\n';
|
||||
|
||||
// 6. Post Comment (if in CI)
|
||||
if (process.env.GITHUB_TOKEN && process.env.PR_NUMBER) {
|
||||
if (process.env.GITHUB_TOKEN && prNumber) {
|
||||
// Verify gh CLI is available
|
||||
console.log('Verifying GitHub CLI...');
|
||||
runCommand('gh', ['--version']);
|
||||
@@ -194,7 +189,7 @@ async function main() {
|
||||
execSync('git config user.name "bitvid-agent"');
|
||||
execSync('git config user.email "[email protected]"');
|
||||
execSync('git add .');
|
||||
execSync('git commit -m "fix(ai): formatting (agent)"');
|
||||
execSync('git commit -m "fix(ai): formatting (agent) suggested"');
|
||||
execSync('git push');
|
||||
|
||||
commentBody += '> **✅ Micro-fixes applied:** Formatting fixes have been committed to this branch.\n\n';
|
||||
@@ -204,13 +199,11 @@ async function main() {
|
||||
// Fallback: Create a new branch and open a PR
|
||||
try {
|
||||
console.log('Attempting to create a follow-up PR with fixes...');
|
||||
const prNumber = process.env.PR_NUMBER;
|
||||
const fixBranchName = `agent-fix/pr-${prNumber}-${Date.now()}`;
|
||||
|
||||
// Push the current HEAD (with fixes) to a new branch on origin
|
||||
execSync(`git push origin HEAD:refs/heads/${fixBranchName}`);
|
||||
|
||||
const baseRef = process.env.GITHUB_BASE_REF || 'main';
|
||||
const prTitle = `fix(ai): formatting fixes for PR #${prNumber}`;
|
||||
const prBody = `This PR applies automated formatting fixes for PR #${prNumber}.`;
|
||||
|
||||
@@ -231,9 +224,9 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Posting comment to PR #' + process.env.PR_NUMBER);
|
||||
console.log('Posting comment to PR #' + prNumber);
|
||||
try {
|
||||
const gh = spawnSync('gh', ['pr', 'comment', process.env.PR_NUMBER, '-F', '-'], {
|
||||
const gh = spawnSync('gh', ['pr', 'comment', prNumber, '-F', '-'], {
|
||||
input: commentBody,
|
||||
stdio: ['pipe', 'inherit', 'inherit']
|
||||
});
|
||||
@@ -247,17 +240,92 @@ async function main() {
|
||||
|
||||
}
|
||||
|
||||
// Always print the report to stdout so it's visible in logs (especially if posting fails or for debugging)
|
||||
// Always print the report to stdout
|
||||
console.log('---------------------------------------------------');
|
||||
console.log('PR Review Report:');
|
||||
console.log(`PR Review Report for PR #${prNumber}:`);
|
||||
console.log(commentBody);
|
||||
console.log('---------------------------------------------------');
|
||||
|
||||
if (hasFailures) {
|
||||
process.exit(1);
|
||||
if (hasFailures) {
|
||||
throw new Error('Lint or Test failures detected.');
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const isAll = process.argv.includes('--all');
|
||||
|
||||
if (isAll) {
|
||||
console.log('Fetching all open PRs...');
|
||||
// Get list of open PRs
|
||||
const listCmd = ['pr', 'list', '--state', 'open', '--json', 'number,baseRefName,headRefName'];
|
||||
const result = spawnSync('gh', listCmd, { encoding: 'utf-8' });
|
||||
|
||||
if (result.error || result.status !== 0) {
|
||||
throw new Error(`Failed to list PRs: ${result.stderr || result.error?.message}`);
|
||||
}
|
||||
|
||||
const prs = JSON.parse(result.stdout);
|
||||
console.log(`Found ${prs.length} open PRs.`);
|
||||
|
||||
let hasGlobalFailures = false;
|
||||
|
||||
// Save current branch to restore later
|
||||
let originalBranch;
|
||||
try {
|
||||
originalBranch = execSync('git branch --show-current', { encoding: 'utf-8' }).trim();
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
for (const pr of prs) {
|
||||
try {
|
||||
console.log(`\n=== Processing PR #${pr.number} ===`);
|
||||
|
||||
// Checkout PR
|
||||
runCommand('gh', ['pr', 'checkout', pr.number]);
|
||||
|
||||
// Run Review
|
||||
await reviewPR(pr.number, pr.baseRefName);
|
||||
|
||||
} catch (e) {
|
||||
console.error(`❌ Failed to process PR #${pr.number}:`, e.message);
|
||||
hasGlobalFailures = true;
|
||||
// Continue to next PR
|
||||
} finally {
|
||||
// Clean up or reset if needed?
|
||||
// Since we use gh pr checkout, we are on a different branch.
|
||||
// Next iteration will checkout another branch.
|
||||
}
|
||||
}
|
||||
|
||||
if (originalBranch) {
|
||||
console.log(`Restoring original branch: ${originalBranch}`);
|
||||
try {
|
||||
execSync(`git checkout ${originalBranch}`);
|
||||
} catch(e) {
|
||||
console.error('Failed to restore original branch');
|
||||
}
|
||||
}
|
||||
|
||||
if (hasGlobalFailures) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
} else {
|
||||
const prNumber = process.env.PR_NUMBER;
|
||||
const baseRef = process.env.GITHUB_BASE_REF || 'main';
|
||||
|
||||
if (prNumber) {
|
||||
await reviewPR(prNumber, baseRef);
|
||||
} else {
|
||||
// Local run or manual invocation without specific PR env vars
|
||||
console.log('No PR_NUMBER found and no --all flag. Running local review against ' + baseRef);
|
||||
await reviewPR('local', baseRef);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Automated PR Review failed:', error);
|
||||
console.error('Automated PR Review failed:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import fs from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import path from "node:path";
|
||||
|
||||
// Ensure NODE_ENV is set to "test" so that nostrToolsBootstrap.js uses the mock toolkit
|
||||
process.env.NODE_ENV = "test";
|
||||
|
||||
const rootDir = path.dirname(fileURLToPath(new URL("../package.json", import.meta.url)));
|
||||
const testsDir = path.join(rootDir, "tests");
|
||||
const testFiles = [];
|
||||
|
||||
Reference in new issue
Block a user