Files
bitvid/scripts/agent/interop-test.mjs
T
google-labs-jules[bot] 8dc1d0e791 feat(tests): implement interop-test.mjs with ephemeral keys and protocol checks
- Implements `scripts/agent/interop-test.mjs` to verify Nostr protocol compatibility.
- Uses `NostrClient` programmatically with ephemeral keys (no persistence).
- Defines a strict test relay list (`relay.damus.io`, `nos.lol`) to avoid spam.
- Validates Video Post publishing (Kind 30078) using `js/nostrEventSchemas.js` builders.
- Validates View Event publishing (Kind 30079).
- Validates DM encryption and decryption (Kind 4) using `js/dmDecryptor.js`.
- Adheres to AGENTS.md guardrails (no session actor comments).
- Outputs clear pass/fail logs for verification.
2026-01-21 07:14:32 +00:00

260 lines
7.9 KiB
JavaScript

// Import environment setup FIRST to ensure polyfills are ready before other imports
import './setup-test-env.js';
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 ---
import { NostrClient } from '../../js/nostr/client.js';
import { decryptDM } from '../../js/dmDecryptor.js';
import {
buildVideoPostEvent,
buildViewEvent,
NOTE_TYPES
} from '../../js/nostrEventSchemas.js';
// --- Configuration ---
// Using a small, respectful list of public relays for interop testing
const TEST_RELAYS = [
'wss://relay.damus.io',
'wss://nos.lol'
];
// --- Helpers ---
function bytesToHex(bytes) {
return Buffer.from(bytes).toString('hex');
}
// --- Main Test ---
async function runTests() {
console.log("=== Starting Protocol & Interop Tests ===");
console.log(`[Config] Relays: ${TEST_RELAYS.join(', ')}`);
// 1. Initialize Client
// Programmatic instantiation as requested
const client = new NostrClient();
client.relays = [...TEST_RELAYS];
client.readRelays = [...TEST_RELAYS];
client.writeRelays = [...TEST_RELAYS];
// Initialize connection
await client.init();
console.log("[Setup] NostrClient initialized and connecting...");
// 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);
console.log(`[Keys] Alice (Sender): ${alicePk}`);
const bobSkBytes = generateSecretKey();
const bobSk = bytesToHex(bobSkBytes);
const bobPk = getPublicKey(bobSkBytes);
console.log(`[Keys] Bob (Receiver): ${bobPk}`);
// Register Alice as signer for the client
await client.registerPrivateKeySigner({ privateKey: aliceSk });
console.log("[Setup] Alice registered as active signer.");
// --- 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 videoContent = {
version: 3,
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,
videoRootId,
mode: "live"
};
// Use schema builder
const videoEventTemplate = buildVideoPostEvent({
pubkey: alicePk,
created_at: Math.floor(Date.now() / 1000),
dTagValue: videoRootId,
content: videoContent
});
let videoEvent;
try {
const result = await client.signAndPublishEvent(videoEventTemplate);
videoEvent = result.signedEvent;
// Check summary for acceptance
const summary = result.summary;
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} (Accepted by ${acceptedCount} relays)`);
} catch (e) {
console.error("[Fail] Video publish failed:", e);
process.exit(1);
}
// --- Test B: Fetch & Verify ---
console.log("\n>>> Test B: Fetch & Verify Roundtrip");
// Allow propagation
await new Promise(r => setTimeout(r, 2000));
try {
const result = await client.getEventById(videoEvent.id, { includeRaw: true });
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);
}
// --- Test C: Publish VIEW_EVENT ---
console.log("\n>>> Test C: Publish VIEW_EVENT");
const viewEventTemplate = buildViewEvent({
pubkey: alicePk,
created_at: Math.floor(Date.now() / 1000),
pointerValue: videoEvent.id,
pointerTag: ['e', videoEvent.id],
dedupeTag: `view:${videoEvent.id}:${Date.now()}`,
includeSessionTag: false // Not a session actor
});
try {
const viewResult = await client.signAndPublishEvent(viewEventTemplate);
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);
}
} catch (e) {
console.error("[Fail] View Event publish threw:", e);
}
// --- Test D: Encrypted DM ---
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 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) {
console.error("[Fail] DM Send failed:", e);
process.exit(1);
}
// Wait for propagation
await new Promise(r => setTimeout(r, 2000));
// 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
limit: 1
};
const dmEvents = await client.pool.list(TEST_RELAYS, [filter]);
console.log(`[DM] Found ${dmEvents.length} events for Bob.`);
if (dmEvents.length === 0) {
console.error("[Fail] No DM events found on relays.");
process.exit(1);
}
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',
decrypt: (pk, ct) => nip44.decrypt(bobSk, pk, ct),
priority: 1,
supportsGiftWrap: true
});
}
// NIP-04
if (nip04) {
bobDecryptors.push({
scheme: 'nip04',
decrypt: async (pk, ct) => nip04.decrypt(bobSk, pk, ct),
priority: 0
});
}
const decryptContext = {
actorPubkey: bobPk,
decryptors: bobDecryptors
};
const decryptRes = await decryptDM(targetEvent, decryptContext);
if (!decryptRes.ok) {
console.error("[Fail] Decryption failed:", decryptRes.errors);
process.exit(1);
}
if (decryptRes.plaintext !== dmMessage) {
console.error(`[Fail] Message mismatch. Got: "${decryptRes.plaintext}", Expected: "${dmMessage}"`);
process.exit(1);
}
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);
}
runTests().catch(e => {
console.error("Unhandled Error:", e);
process.exit(1);
});