mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
Merge pull request #1459 from PR0M3TH3AN/ai/interop-tests-20260121-16965720185426536661
feat: implement protocol & interop tests with ephemeral keys
This commit is contained in:
1 file changed
+81
-89
@@ -1,11 +1,11 @@
|
||||
// Import environment setup FIRST to ensure polyfills are ready before other imports
|
||||
import './setup-test-env.js';
|
||||
|
||||
import { generateSecretKey, getPublicKey, nip19, nip04, nip44 } from 'nostr-tools';
|
||||
import * as NostrTools from 'nostr-tools';
|
||||
// Destructure what we need, handling potential version differences if necessary
|
||||
const { generateSecretKey, getPublicKey, nip19, nip04, nip44 } = NostrTools;
|
||||
|
||||
// --- Imports from Codebase ---
|
||||
// Relative path from scripts/agent/interop-test.mjs to js/nostr/client.js is ../../js/nostr/client.js
|
||||
import { NostrClient } from '../../js/nostr/client.js';
|
||||
import { decryptDM } from '../../js/dmDecryptor.js';
|
||||
import {
|
||||
@@ -15,53 +15,38 @@ import {
|
||||
} from '../../js/nostrEventSchemas.js';
|
||||
|
||||
// --- Configuration ---
|
||||
const RELAYS = [
|
||||
// Using a small, respectful list of public relays for interop testing
|
||||
const TEST_RELAYS = [
|
||||
'wss://relay.damus.io',
|
||||
'wss://nos.lol'
|
||||
];
|
||||
// Optional: Local relay if running
|
||||
const LOCAL_RELAY = 'ws://localhost:8008';
|
||||
|
||||
// --- Helpers ---
|
||||
function bytesToHex(bytes) {
|
||||
return Buffer.from(bytes).toString('hex');
|
||||
}
|
||||
|
||||
async function checkRelay(url) {
|
||||
return new Promise(resolve => {
|
||||
try {
|
||||
const ws = new WebSocket(url);
|
||||
ws.on('open', () => { ws.close(); resolve(true); });
|
||||
ws.on('error', () => { resolve(false); });
|
||||
} catch { resolve(false); }
|
||||
});
|
||||
}
|
||||
|
||||
// --- Main Test ---
|
||||
async function runTests() {
|
||||
console.log("=== Starting Protocol & Interop Tests ===");
|
||||
console.log(`[Config] Relays: ${TEST_RELAYS.join(', ')}`);
|
||||
|
||||
// 1. Setup Relays
|
||||
const activeRelays = [];
|
||||
if (await checkRelay(LOCAL_RELAY)) {
|
||||
console.log(`[Setup] Local relay found: ${LOCAL_RELAY}`);
|
||||
activeRelays.push(LOCAL_RELAY);
|
||||
} else {
|
||||
console.log(`[Setup] Local relay not found. Using public test relays.`);
|
||||
activeRelays.push(...RELAYS);
|
||||
}
|
||||
console.log(`[Setup] Active Relays:`, activeRelays);
|
||||
|
||||
// 2. Initialize Client
|
||||
// 1. Initialize Client
|
||||
// Programmatic instantiation as requested
|
||||
const client = new NostrClient();
|
||||
client.relays = activeRelays;
|
||||
client.readRelays = activeRelays;
|
||||
client.writeRelays = activeRelays;
|
||||
client.relays = [...TEST_RELAYS];
|
||||
client.readRelays = [...TEST_RELAYS];
|
||||
client.writeRelays = [...TEST_RELAYS];
|
||||
|
||||
// Initialize connection
|
||||
await client.init();
|
||||
console.log("[Setup] NostrClient initialized.");
|
||||
console.log("[Setup] NostrClient initialized and connecting...");
|
||||
|
||||
// 3. Generate Ephemeral Keys
|
||||
// Wait a bit for connections to establish
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
|
||||
// 2. Generate Ephemeral Keys
|
||||
// Use ephemeral keys (generate locally) as requested
|
||||
const aliceSkBytes = generateSecretKey();
|
||||
const aliceSk = bytesToHex(aliceSkBytes);
|
||||
const alicePk = getPublicKey(aliceSkBytes);
|
||||
@@ -72,33 +57,32 @@ async function runTests() {
|
||||
const bobPk = getPublicKey(bobSkBytes);
|
||||
console.log(`[Keys] Bob (Receiver): ${bobPk}`);
|
||||
|
||||
// Register Alice as signer
|
||||
// Register Alice as signer for the client
|
||||
await client.registerPrivateKeySigner({ privateKey: aliceSk });
|
||||
console.log("[Setup] Alice registered as active signer.");
|
||||
|
||||
// --- Test A: Video Post ---
|
||||
console.log("\n>>> Test A: Publish VIDEO_POST (Kind 30078)");
|
||||
// --- Test A: Publish VIDEO_POST ---
|
||||
console.log("\n>>> Test A: Publish VIDEO_POST");
|
||||
const videoRootId = `test-${Date.now()}-${Math.random().toString(36).substring(2)}`;
|
||||
const dTagValue = videoRootId;
|
||||
const videoContent = {
|
||||
version: 3,
|
||||
title: `Interop Test ${Date.now()}`,
|
||||
description: "Automated interop test with ephemeral keys.",
|
||||
title: `Interop Test Video ${Date.now()}`,
|
||||
description: "Automated interop test video event.",
|
||||
url: "https://example.com/video.mp4",
|
||||
thumbnail: "https://example.com/thumb.jpg",
|
||||
mimeType: "video/mp4",
|
||||
isForKids: false,
|
||||
isNsfw: false,
|
||||
enableComments: false, // Guardrail: no session actor comments
|
||||
enableComments: false,
|
||||
videoRootId,
|
||||
mode: "live"
|
||||
};
|
||||
|
||||
// Explicitly using schema builder as per instructions
|
||||
// Use schema builder
|
||||
const videoEventTemplate = buildVideoPostEvent({
|
||||
pubkey: alicePk,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
dTagValue,
|
||||
dTagValue: videoRootId,
|
||||
content: videoContent
|
||||
});
|
||||
|
||||
@@ -107,12 +91,14 @@ async function runTests() {
|
||||
const result = await client.signAndPublishEvent(videoEventTemplate);
|
||||
videoEvent = result.signedEvent;
|
||||
|
||||
// Check acceptance. The structure of summary is { accepted: [...], failed: [...] }
|
||||
// Check summary for acceptance
|
||||
const summary = result.summary;
|
||||
if (!summary || !summary.accepted || summary.accepted.length === 0) {
|
||||
throw new Error("Relays rejected the video event.");
|
||||
const acceptedCount = summary?.accepted?.length || 0;
|
||||
|
||||
if (acceptedCount === 0) {
|
||||
throw new Error(`Relays rejected the video event. Details: ${JSON.stringify(summary)}`);
|
||||
}
|
||||
console.log(`[Pass] Published Video Event ID: ${videoEvent.id}`);
|
||||
console.log(`[Pass] Published Video Event ID: ${videoEvent.id} (Accepted by ${acceptedCount} relays)`);
|
||||
} catch (e) {
|
||||
console.error("[Fail] Video publish failed:", e);
|
||||
process.exit(1);
|
||||
@@ -120,47 +106,42 @@ async function runTests() {
|
||||
|
||||
// --- Test B: Fetch & Verify ---
|
||||
console.log("\n>>> Test B: Fetch & Verify Roundtrip");
|
||||
// Give relays a moment to index
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
// Allow propagation
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
|
||||
// Request raw event to verify protocol details
|
||||
const result = await client.getEventById(videoEvent.id, { includeRaw: true });
|
||||
if (!result || !result.rawEvent) {
|
||||
console.error("[Fail] Could not fetch raw event back from relays.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { video, rawEvent } = result;
|
||||
|
||||
if (rawEvent.id !== videoEvent.id) {
|
||||
console.error(`[Fail] ID mismatch. Expected ${videoEvent.id}, got ${rawEvent.id}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Verify shape by parsing raw content
|
||||
let content;
|
||||
try {
|
||||
content = JSON.parse(rawEvent.content);
|
||||
} catch {
|
||||
content = {};
|
||||
}
|
||||
const result = await client.getEventById(videoEvent.id, { includeRaw: true });
|
||||
|
||||
if (content.title !== videoContent.title) {
|
||||
console.error(`[Fail] Content mismatch in raw event. Title: ${content.title} vs ${videoContent.title}`);
|
||||
if (!result || !result.rawEvent) {
|
||||
throw new Error("Could not fetch raw event back from relays.");
|
||||
}
|
||||
|
||||
const { video, rawEvent } = result;
|
||||
|
||||
if (rawEvent.id !== videoEvent.id) {
|
||||
throw new Error(`ID mismatch. Expected ${videoEvent.id}, got ${rawEvent.id}`);
|
||||
}
|
||||
|
||||
// Verify content shape
|
||||
const content = JSON.parse(rawEvent.content);
|
||||
if (content.title !== videoContent.title) {
|
||||
throw new Error(`Content mismatch. Title: ${content.title} vs ${videoContent.title}`);
|
||||
}
|
||||
|
||||
// Verify signature (basic check)
|
||||
const isSigValid = NostrTools.verifyEvent(rawEvent);
|
||||
if (!isSigValid) {
|
||||
throw new Error("Event signature verification failed.");
|
||||
}
|
||||
|
||||
console.log("[Pass] Event fetched, signature verified, and content matches.");
|
||||
} catch (e) {
|
||||
console.error(`[Fail] Fetch & Verify failed: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (video.title !== videoContent.title) {
|
||||
console.error(`[Fail] Content mismatch in normalized video object. Title: ${video.title}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("[Pass] Event fetched and verified successfully (Raw + Normalized).");
|
||||
|
||||
// --- Test C: View Event ---
|
||||
// --- Test C: Publish VIEW_EVENT ---
|
||||
console.log("\n>>> Test C: Publish VIEW_EVENT");
|
||||
// Explicitly using schema builder as per instructions
|
||||
// View event points to the video
|
||||
const viewEventTemplate = buildViewEvent({
|
||||
pubkey: alicePk,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
@@ -172,8 +153,8 @@ async function runTests() {
|
||||
|
||||
try {
|
||||
const viewResult = await client.signAndPublishEvent(viewEventTemplate);
|
||||
const viewSummary = viewResult.summary;
|
||||
if (viewSummary && viewSummary.accepted && viewSummary.accepted.length > 0) {
|
||||
const acceptedCount = viewResult.summary?.accepted?.length || 0;
|
||||
if (acceptedCount > 0) {
|
||||
console.log(`[Pass] View Event published. ID: ${viewResult.signedEvent.id}`);
|
||||
} else {
|
||||
console.warn(`[Warn] View Event publish result not OK:`, viewResult);
|
||||
@@ -186,12 +167,14 @@ async function runTests() {
|
||||
console.log("\n>>> Test D: Direct Message (Encryption/Decryption)");
|
||||
const dmMessage = `Secret message ${Date.now()}`;
|
||||
|
||||
// We are Alice. Send to Bob.
|
||||
// Note: client.sendDirectMessage uses the registered signer (Alice)
|
||||
console.log(`[DM] Sending from Alice to Bob...`);
|
||||
try {
|
||||
const bobNpub = nip19.npubEncode(bobPk);
|
||||
const realSendRes = await client.sendDirectMessage(bobNpub, dmMessage);
|
||||
if (!realSendRes.ok) {
|
||||
throw new Error(realSendRes.error || "Unknown error");
|
||||
const sendRes = await client.sendDirectMessage(bobNpub, dmMessage);
|
||||
if (!sendRes.ok) {
|
||||
throw new Error(sendRes.error || "Unknown error sending DM");
|
||||
}
|
||||
console.log("[DM] Message sent.");
|
||||
} catch (e) {
|
||||
@@ -199,17 +182,20 @@ async function runTests() {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Verify by fetching as Bob
|
||||
// Wait for propagation
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
|
||||
console.log(`[DM] Fetching for Bob...`);
|
||||
// Verify by fetching as Bob
|
||||
// We need to use the pool directly to fetch, as 'client' is configured as Alice
|
||||
console.log(`[DM] Fetching events for Bob (p=${bobPk})...`);
|
||||
const filter = {
|
||||
kinds: [4, 1059], // Legacy and GiftWrap
|
||||
'#p': [bobPk],
|
||||
authors: [alicePk] // Sent by Alice
|
||||
authors: [alicePk], // Sent by Alice
|
||||
limit: 1
|
||||
};
|
||||
|
||||
const dmEvents = await client.pool.list(activeRelays, [filter]);
|
||||
const dmEvents = await client.pool.list(TEST_RELAYS, [filter]);
|
||||
console.log(`[DM] Found ${dmEvents.length} events for Bob.`);
|
||||
|
||||
if (dmEvents.length === 0) {
|
||||
@@ -220,8 +206,10 @@ async function runTests() {
|
||||
const targetEvent = dmEvents[0];
|
||||
console.log(`[DM] Decrypting event kind ${targetEvent.kind}...`);
|
||||
|
||||
// Setup Bob's decryptors
|
||||
const bobDecryptors = [];
|
||||
|
||||
// NIP-44
|
||||
if (nip44) {
|
||||
bobDecryptors.push({
|
||||
scheme: 'nip44',
|
||||
@@ -230,6 +218,7 @@ async function runTests() {
|
||||
supportsGiftWrap: true
|
||||
});
|
||||
}
|
||||
// NIP-04
|
||||
if (nip04) {
|
||||
bobDecryptors.push({
|
||||
scheme: 'nip04',
|
||||
@@ -255,7 +244,10 @@ async function runTests() {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`[Pass] Decryption verified: "${decryptRes.plaintext}"`);
|
||||
console.log(`[Pass] Decryption verified. Scheme: ${decryptRes.scheme}, Message: "${decryptRes.plaintext}"`);
|
||||
|
||||
// Cleanup
|
||||
// In a real scenario we might delete the events, but these are ephemeral and test relays usually clean up or we accept the dust for this test.
|
||||
|
||||
console.log("\n=== All Tests Passed ===");
|
||||
process.exit(0);
|
||||
|
||||
Reference in new issue
Block a user