Merge branch 'unstable' into fix-webtorrent-webseed-regression-10274894627332192935

This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-01 08:00:23 -05:00
commit 438f895dd6
28 files changed
+874 -719

No files matched your search

+12 -3
View File
@@ -45,6 +45,9 @@ test.beforeEach(async () => {
restoreNostrClient();
restoreRelayManager();
window.nostr = originalWindowNostr;
if (nostrClient.extensionPermissionCache) {
nostrClient.extensionPermissionCache.clear();
}
// Setup default mock signer to prevent auto-save errors
setActiveSigner({
@@ -256,7 +259,7 @@ test(
let fetchCalls = 0;
nostrClient.fetchListIncrementally = async () => {
fetchCalls += 1;
return fetchCalls <= 4 ? [event] : [];
return [event]; // ALWAYS return the event
};
nostrClient.relays = relayUrls;
nostrClient.writeRelays = relayUrls;
@@ -266,9 +269,9 @@ test(
// Create a mock window.nostr that properly simulates extension behavior.
// Specifically, enable() needs to exist so nostrClient detects it as an extension.
window.nostr = {
enable: async () => {},
getPublicKey: async () => pubkey,
enable: async () => Promise.resolve(true),
nip04: {
encrypt: async () => "cipher",
decrypt: async () => {
if (!permissionState.enabled) throw new Error("permission denied");
decryptCalls.push("nip04");
@@ -279,6 +282,10 @@ test(
});
},
},
nip44: {
encrypt: async () => "cipher",
decrypt: async () => "plaintext",
}
};
clearActiveSigner();
@@ -573,6 +580,7 @@ test(
// Mock window.nostr
window.nostr = {
enable: async () => Promise.resolve(true),
nip04: {
decrypt: async (pk, ciphertext) => {
if (pk !== pubkey) throw new Error("Wrong pubkey");
@@ -621,6 +629,7 @@ test(
// Mock window.nostr
window.nostr = {
enable: async () => Promise.resolve(true),
nip04: {
decrypt: async (pk, ciphertext) => {
if (pk !== pubkey) throw new Error("Wrong pubkey");
+28 -53
View File
@@ -165,31 +165,25 @@ describe("NIP-07 Login Permissions", () => {
const pubkey = result.pubkey;
assert.equal(pubkey, HEX_PUBKEY);
assert.ok(env.enableCalls.length >= 1, "extension.enable should be invoked");
assert.ok(
nostrClient.extensionPermissionCache.has("nip04.encrypt"),
"nip04.encrypt permission should be tracked as granted",
);
assert.ok(
nostrClient.extensionPermissionCache.has("nip04.decrypt"),
"nip04.decrypt permission should be tracked as granted",
);
assert.ok(
nostrClient.extensionPermissionCache.has("nip44.encrypt"),
"nip44.encrypt permission should be tracked as granted",
);
assert.ok(
nostrClient.extensionPermissionCache.has("nip44.decrypt"),
"nip44.decrypt permission should be tracked as granted",
);
for (const method of EXPECTED_ENCRYPTION_PERMISSIONS) {
const permissionsToCheck = [
"nip04.encrypt",
"nip04.decrypt",
"nip44.encrypt",
"nip44.decrypt",
...EXPECTED_ENCRYPTION_PERMISSIONS
];
for (const method of permissionsToCheck) {
assert.ok(
nostrClient.extensionPermissionCache.has(method),
`${method} permission should be tracked as granted`,
`${method} permission should be tracked as granted`
);
}
assert.ok(
nostrClient.extensionPermissionCache.has("sign_event"),
"sign_event permission should be tracked as granted",
"sign_event permission should be tracked as granted"
);
} finally {
env.restore();
@@ -220,18 +214,18 @@ describe("NIP-07 Login Permissions", () => {
},
};
const result = await nip07Provider.login({ nostrClient });
const pubkey = result.pubkey;
assert.equal(pubkey, HEX_PUBKEY);
await nip07Provider.login({ nostrClient });
for (const method of EXPECTED_ENCRYPTION_PERMISSIONS) {
assert.ok(
nostrClient.extensionPermissionCache.has(method),
`nostrClient should track ${method} after login`,
`nostrClient should track ${method} after login`
);
}
const storedRaw = localStorage.getItem(PERMISSIONS_STORAGE_KEY);
assert.ok(storedRaw, "extension permissions should persist to localStorage");
let storedMethods = [];
try {
const parsed = JSON.parse(storedRaw);
@@ -252,9 +246,10 @@ describe("NIP-07 Login Permissions", () => {
for (const method of EXPECTED_ENCRYPTION_PERMISSIONS) {
assert.ok(
storedSet.has(method),
`stored permissions should include ${method}`,
`stored permissions should include ${method}`
);
}
const baselineEnableCalls = env.enableCalls.length;
const freshClient = new NostrClient();
freshClient.pubkey = HEX_PUBKEY;
@@ -262,7 +257,7 @@ describe("NIP-07 Login Permissions", () => {
for (const method of EXPECTED_ENCRYPTION_PERMISSIONS) {
assert.ok(
freshClient.extensionPermissionCache.has(method),
`fresh client should hydrate ${method} from storage`,
`fresh client should hydrate ${method} from storage`
);
}
@@ -274,7 +269,7 @@ describe("NIP-07 Login Permissions", () => {
assert.equal(
env.enableCalls.length,
baselineEnableCalls,
`cached permissions should prevent extra enable() calls for ${method}`,
`cached permissions should prevent extra enable() calls for ${method}`
);
}
@@ -288,7 +283,7 @@ describe("NIP-07 Login Permissions", () => {
assert.equal(
env.enableCalls.length,
baselineEnableCalls,
"decrypt calls should not trigger additional enable() prompts",
"decrypt calls should not trigger additional enable() prompts"
);
assert.equal(decryptCalls.nip04, 1);
assert.equal(decryptCalls.nip44, 1);
@@ -322,37 +317,27 @@ describe("NIP-07 Login Permissions", () => {
const result = await nip07Provider.login({ nostrClient });
const pubkey = result.pubkey;
assert.equal(pubkey, HEX_PUBKEY);
// With Structured -> String -> Plain order, this succeeds at step 2 (String).
// The login flow may perform multiple permission requests (e.g. core -> full).
// We expect at least one successful sequence (2 calls).
assert.ok(env.enableCalls.length >= 2, "should retry with alternate payloads");
// Check the first sequence (core permissions)
const [objectCall, stringCall] = env.enableCalls;
assert.ok(Array.isArray(objectCall?.permissions));
assert.equal(
typeof objectCall.permissions[0],
"object",
"structured permission request should be attempted first",
"structured permission request should be attempted first"
);
assert.ok(Array.isArray(stringCall?.permissions));
assert.equal(
typeof stringCall.permissions[0],
"string",
"string-based permissions should be attempted after structured payloads",
"string-based permissions should be attempted after structured payloads"
);
// If we have more calls (full permissions upgrade), verify they follow the same pattern
if (env.enableCalls.length > 2) {
// The 3rd call should be a new structured attempt for full permissions
const upgradeObjectCall = env.enableCalls[2];
// The 4th call (if 3rd failed) should be string
const upgradeStringCall = env.enableCalls[3];
if (upgradeObjectCall) {
assert.ok(Array.isArray(upgradeObjectCall.permissions), "upgrade attempt should use permissions");
}
// We don't strictly assert `plainCall` is undefined because a 2nd pass might happen
}
} finally {
env.restore();
@@ -376,21 +361,14 @@ describe("NIP-07 Login Permissions", () => {
const result = await nip07Provider.login({ nostrClient });
const pubkey = result.pubkey;
assert.equal(pubkey, HEX_PUBKEY);
// With Structured -> String -> Plain order, we expect at least 3 calls for the first success:
// 1. Structured (rejected)
// 2. String (rejected)
// 3. Plain (accepted)
// If login does a second pass (upgrade), it might repeat this sequence.
assert.ok(
env.enableCalls.length >= 3,
"should eventually succeed with plain enable()",
"should eventually succeed with plain enable()"
);
// Verify the first sequence structure
const [objectCall, stringCall, plainCall] = env.enableCalls;
assert.ok(objectCall !== undefined);
assert.ok(stringCall !== undefined);
// plainCall is the one that succeeded (called with undefined/no args)
assert.equal(plainCall, undefined, "successful attempt should use plain enable()");
} finally {
env.restore();
@@ -429,11 +407,8 @@ describe("NIP-07 Login Permissions", () => {
assert.equal(pubkey, HEX_PUBKEY);
assert.ok(
duration < 1000,
`login should fall back quickly from stalled enable payloads (duration: ${duration}ms)`,
`login should fall back quickly from stalled enable payloads (duration: ${duration}ms)`
);
// Structured (stalls) -> String (resolves)
// Plain is skipped because String resolved.
// Login might do this twice (Core then Full), so 2 or 4 calls.
assert.ok(env.enableCalls.length >= 2, "should attempt structured (stalls) and then string payload");
const [objectCall, stringCall] = env.enableCalls;
@@ -473,13 +448,13 @@ describe("NIP-07 Login Permissions", () => {
assert.equal(result.pubkey, HEX_PUBKEY);
assert.ok(
duration < 200,
`login should resolve before deferred permission grants (duration: ${duration}ms)`,
`login should resolve before deferred permission grants (duration: ${duration}ms)`
);
assert.ok(completionPromise, "completionPromise should be captured");
await assert.rejects(
completionPromise,
/permission denied/,
"deferred permission grants should still reject",
"deferred permission grants should still reject"
);
} finally {
nostrClient.ensureExtensionPermissions = originalEnsurePermissions;
+2 -2
View File
@@ -45,14 +45,14 @@ describe("Nostr Private Key Signer", () => {
const outboundCipher = await signer.nip04Encrypt(recipientPubkey, message);
assert.equal(typeof outboundCipher, "string");
const decryptedByRecipient = await nostrTools.nip04.decrypt(
recipientPrivateHex,
recipientKeyBytes,
signer.pubkey,
outboundCipher,
);
assert.equal(decryptedByRecipient, message);
const inboundCipher = await nostrTools.nip04.encrypt(
recipientPrivateHex,
recipientKeyBytes,
signer.pubkey,
message,
);
+5 -6
View File
@@ -92,10 +92,9 @@ test("sendDirectMessage succeeds with private key signer and no extension", asyn
nostrClient.readRelays = [RELAY_URL];
nostrClient.writeRelays = [RELAY_URL];
const senderPrivateKey = canonicalTools.utils.bytesToHex(
canonicalTools.generateSecretKey(),
);
const senderPubkey = canonicalTools.getPublicKey(senderPrivateKey);
const senderSecret = canonicalTools.generateSecretKey();
const senderPrivateKey = canonicalTools.utils.bytesToHex(senderSecret);
const senderPubkey = canonicalTools.getPublicKey(senderSecret);
const recipientSecret = canonicalTools.generateSecretKey();
const recipientPrivateKey = canonicalTools.utils.bytesToHex(recipientSecret);
const recipientPubkey = canonicalTools.getPublicKey(recipientSecret);
@@ -112,7 +111,7 @@ test("sendDirectMessage succeeds with private key signer and no extension", asyn
const originalNip44 = signer?.nip44Encrypt;
if (signer) {
signer.signEvent = (event) => {
const finalized = canonicalTools.finalizeEvent(event, senderPrivateKey);
const finalized = canonicalTools.finalizeEvent(event, senderSecret);
return { ...event, id: finalized.id, sig: finalized.sig };
};
delete signer.nip04Encrypt;
@@ -133,7 +132,7 @@ test("sendDirectMessage succeeds with private key signer and no extension", asyn
assert.notEqual(event.content, "", "ciphertext should not be empty");
const decrypted = await canonicalTools.nip04.decrypt(
recipientPrivateKey,
recipientSecret,
senderPubkey,
event.content,
);
+8 -8
View File
@@ -151,24 +151,24 @@ async function setupDmScenario() {
const senderPrivateKey = nostrTools.utils.bytesToHex(senderSecret);
const receiverPrivateKey = nostrTools.utils.bytesToHex(receiverSecret);
const senderPubkey = nostrTools.getPublicKey(senderPrivateKey);
const receiverPubkey = nostrTools.getPublicKey(receiverPrivateKey);
const senderPubkey = nostrTools.getPublicKey(senderSecret);
const receiverPubkey = nostrTools.getPublicKey(receiverSecret);
const createSigner = (privateKey, pubkey) => ({
const createSigner = (privateKeyBytes, pubkey) => ({
type: "test",
pubkey,
signEvent: (event) => {
const finalized = nostrTools.finalizeEvent(event, privateKey);
const finalized = nostrTools.finalizeEvent(event, privateKeyBytes);
return { ...event, id: finalized.id, sig: finalized.sig };
},
nip04Encrypt: (target, plaintext) =>
nostrTools.nip04.encrypt(privateKey, target, plaintext),
nostrTools.nip04.encrypt(privateKeyBytes, target, plaintext),
nip04Decrypt: (target, ciphertext) =>
nostrTools.nip04.decrypt(privateKey, target, ciphertext),
nostrTools.nip04.decrypt(privateKeyBytes, target, ciphertext),
});
const senderSigner = createSigner(senderPrivateKey, senderPubkey);
const receiverSigner = createSigner(receiverPrivateKey, receiverPubkey);
const senderSigner = createSigner(senderSecret, senderPubkey);
const receiverSigner = createSigner(receiverSecret, receiverPubkey);
const relayPool = new FakeRelayPool();
+24 -24
View File
@@ -25,8 +25,8 @@ test("Nip46RpcClient encrypts payloads with nip44 conversation keys", async () =
const clientSecret = utils.bytesToHex(generateSecretKey());
const remoteSecret = utils.bytesToHex(generateSecretKey());
const clientPublicKey = getPublicKey(clientSecret);
const remotePubkey = getPublicKey(remoteSecret);
const clientPublicKey = getPublicKey(utils.hexToBytes(clientSecret));
const remotePubkey = getPublicKey(utils.hexToBytes(remoteSecret));
const rpcClient = new Nip46RpcClient({
nostrClient: { relays: [] },
@@ -43,8 +43,8 @@ test("Nip46RpcClient encrypts payloads with nip44 conversation keys", async () =
const conversationKey =
typeof nip44.v2.getConversationKey === "function"
? nip44.v2.getConversationKey(clientSecret, remotePubkey)
: nip44.v2.utils.getConversationKey(clientSecret, remotePubkey);
? nip44.v2.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey)
: nip44.v2.utils.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey);
const decryptedWithTools = nip44.v2.decrypt(ciphertext, conversationKey);
assert.equal(
decryptedWithTools,
@@ -71,13 +71,13 @@ test("decryptNip46PayloadWithKeys handles nip44.v2 ciphertext", async () => {
const clientSecret = utils.bytesToHex(generateSecretKey());
const remoteSecret = utils.bytesToHex(generateSecretKey());
const remotePubkey = getPublicKey(remoteSecret);
const remotePubkey = getPublicKey(utils.hexToBytes(remoteSecret));
const serialized = JSON.stringify({ message: "hello", count: 42 });
const conversationKey =
typeof nip44.v2.getConversationKey === "function"
? nip44.v2.getConversationKey(clientSecret, remotePubkey)
: nip44.v2.utils.getConversationKey(clientSecret, remotePubkey);
? nip44.v2.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey)
: nip44.v2.utils.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey);
const ciphertext = nip44.v2.encrypt(serialized, conversationKey);
const { plaintext, algorithm } = await decryptNip46PayloadWithKeys(
@@ -104,13 +104,13 @@ test("decryptNip46PayloadWithKeys coerces structured handshake payloads", async
const clientSecret = utils.bytesToHex(generateSecretKey());
const remoteSecret = utils.bytesToHex(generateSecretKey());
const remotePubkey = getPublicKey(remoteSecret);
const remotePubkey = getPublicKey(utils.hexToBytes(remoteSecret));
const serialized = JSON.stringify({ message: "hello", count: 42 });
const conversationKey =
typeof nip44.v2.getConversationKey === "function"
? nip44.v2.getConversationKey(clientSecret, remotePubkey)
: nip44.v2.utils.getConversationKey(clientSecret, remotePubkey);
? nip44.v2.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey)
: nip44.v2.utils.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey);
const ciphertext = nip44.v2.encrypt(serialized, conversationKey);
const [ciphertextPart, nonce] = ciphertext.split("\n");
@@ -154,13 +154,13 @@ test("decryptNip46PayloadWithKeys decodes buffer-based handshake payloads", asyn
const clientSecret = utils.bytesToHex(generateSecretKey());
const remoteSecret = utils.bytesToHex(generateSecretKey());
const remotePubkey = getPublicKey(remoteSecret);
const remotePubkey = getPublicKey(utils.hexToBytes(remoteSecret));
const serialized = JSON.stringify({ message: "hello", count: 42 });
const conversationKey =
typeof nip44.v2.getConversationKey === "function"
? nip44.v2.getConversationKey(clientSecret, remotePubkey)
: nip44.v2.utils.getConversationKey(clientSecret, remotePubkey);
? nip44.v2.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey)
: nip44.v2.utils.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey);
const ciphertext = nip44.v2.encrypt(serialized, conversationKey);
const [ciphertextPart, nonce] = ciphertext.split("\n");
const ciphertextBuffer = Buffer.from(ciphertext, "utf8");
@@ -197,10 +197,10 @@ test("decryptNip46PayloadWithKeys supports nip04-style ciphertext wrappers", asy
const clientSecret = utils.bytesToHex(generateSecretKey());
const remoteSecret = utils.bytesToHex(generateSecretKey());
const remotePubkey = getPublicKey(remoteSecret);
const remotePubkey = getPublicKey(utils.hexToBytes(remoteSecret));
const serialized = JSON.stringify({ message: "hello", count: 42 });
const ciphertext = await nip04.encrypt(clientSecret, remotePubkey, serialized);
const ciphertext = await nip04.encrypt(utils.hexToBytes(clientSecret), remotePubkey, serialized);
const [ciphertextPart, iv] = ciphertext.split("?iv=");
const structuredPayload = {
@@ -254,8 +254,8 @@ test("parseNip46ConnectionString handles remote signer key hints", async () => {
const signerSecret = utils.bytesToHex(generateSecretKey());
const userSecret = utils.bytesToHex(generateSecretKey());
const signerPubkey = getPublicKey(signerSecret).toLowerCase();
const userPubkey = getPublicKey(userSecret).toLowerCase();
const signerPubkey = getPublicKey(utils.hexToBytes(signerSecret)).toLowerCase();
const userPubkey = getPublicKey(utils.hexToBytes(userSecret)).toLowerCase();
const signerNpub = nip19.npubEncode(signerPubkey);
const userNpub = nip19.npubEncode(userPubkey);
@@ -295,13 +295,13 @@ test(
const remoteSignerSecret = utils.bytesToHex(generateSecretKey());
const userSecret = utils.bytesToHex(generateSecretKey());
const remoteSignerPubkey = getPublicKey(remoteSignerSecret).toLowerCase();
const userPubkey = getPublicKey(userSecret).toLowerCase();
const remoteSignerPubkey = getPublicKey(utils.hexToBytes(remoteSignerSecret)).toLowerCase();
const userPubkey = getPublicKey(utils.hexToBytes(userSecret)).toLowerCase();
const conversationKey =
typeof nip44.v2.getConversationKey === "function"
? nip44.v2.getConversationKey(clientSecret, remoteSignerPubkey)
: nip44.v2.utils.getConversationKey(clientSecret, remoteSignerPubkey);
? nip44.v2.getConversationKey(utils.hexToBytes(clientSecret), remoteSignerPubkey)
: nip44.v2.utils.getConversationKey(utils.hexToBytes(clientSecret), remoteSignerPubkey);
const payload = JSON.stringify({ id: "ack", result: "ok" });
const ciphertext = nip44.v2.encrypt(payload, conversationKey);
@@ -333,12 +333,12 @@ test(
const { attemptDecryptNip46HandshakePayload } = await loadNip46Module();
const clientSecret = utils.bytesToHex(generateSecretKey());
const clientPubkey = getPublicKey(clientSecret).toLowerCase();
const clientPubkey = getPublicKey(utils.hexToBytes(clientSecret)).toLowerCase();
const remoteSecret = utils.bytesToHex(generateSecretKey());
const remotePubkey = getPublicKey(remoteSecret).toLowerCase();
const remotePubkey = getPublicKey(utils.hexToBytes(remoteSecret)).toLowerCase();
const payload = JSON.stringify({ id: "ack", result: "ack" });
const ciphertext = nip04.encrypt(remoteSecret, clientPubkey, payload);
const ciphertext = nip04.encrypt(utils.hexToBytes(remoteSecret), clientPubkey, payload);
const [ciphertextPart, ivPart] = ciphertext.split("?iv=");
assert.ok(ivPart, "nip04 encryption should emit an iv segment");
+196
View File
@@ -0,0 +1,196 @@
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 PORT = 8000;
const URL = `http://localhost:${PORT}`;
const ARTIFACTS_DIR = path.resolve('artifacts');
const LOG_FILE = path.join(ARTIFACTS_DIR, `smoke-${new Date().toISOString().split('T')[0].replace(/-/g, '')}.log`);
const SUMMARY_FILE = path.join(ARTIFACTS_DIR, 'smoke-summary.json');
if (!fs.existsSync(ARTIFACTS_DIR)) {
fs.mkdirSync(ARTIFACTS_DIR);
}
function log(msg) {
const timestamp = new Date().toISOString();
const line = `[${timestamp}] ${msg}`;
console.log(line);
try {
fs.appendFileSync(LOG_FILE, line + '\n');
} catch (e) {
// Ignore log write errors
}
}
async function main() {
log('Starting smoke test...');
let serverProcess;
let browser;
try {
// Start Python Server
log('Starting local server (python3 -m http.server)...');
serverProcess = spawn('python3', ['-m', 'http.server', String(PORT)], {
stdio: 'ignore'
});
// Give it time to start
await new Promise(r => setTimeout(r, 2000));
// Launch Browser
log('Launching browser...');
browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
const page = await context.newPage();
// Capture console logs from page
page.on('console', msg => {
if (msg.type() === 'error') {
log(`[BROWSER ERROR] ${msg.text()}`);
} else {
// log(`[BROWSER] ${msg.text()}`); // verbose
}
});
page.on('pageerror', err => log(`[BROWSER EXCEPTION] ${err}`));
log(`Navigating to ${URL}...`);
await page.goto(URL);
await page.waitForLoadState('networkidle');
log('Page loaded.');
// Inject Test Logic
log('Running in-browser smoke test...');
const result = await page.evaluate(async () => {
const logs = [];
function browserLog(msg) {
console.log(msg);
logs.push(msg);
}
try {
browserLog('Step 1: Importing Client...');
// Note: In browser context, paths are relative to the served root (http://localhost:8000/)
const { nostrClient } = await import('./js/nostrClientFacade.js');
const { encodeHexToNpub } = await import('./js/nostr/nip46Client.js');
if (!nostrClient) throw new Error('nostrClient not found');
browserLog('nostrClient imported.');
browserLog('Step 2: Generating Key & Login...');
const randomBytes = new Uint8Array(32);
window.crypto.getRandomValues(randomBytes);
const privateKey = Array.from(randomBytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
const { pubkey } = await nostrClient.registerPrivateKeySigner({ privateKey, persist: false });
nostrClient.pubkey = pubkey; // Explicitly set it on the client instance
if (!nostrClient.pubkey) throw new Error('Login failed: pubkey is null');
browserLog(`Logged in as: ${nostrClient.pubkey}`);
browserLog('Step 3: Connecting to Relays...');
const connectResults = await nostrClient.connectToRelays();
const successful = connectResults.filter(r => r.success);
browserLog(`Connected to ${successful.length} relays.`);
if (successful.length === 0) {
browserLog('Warning: Failed to connect to any relay. Tests depending on network will likely fail or use local cache.');
}
browserLog('Step 4: Publishing Video...');
const seriesId = `smoke-test-${Date.now()}-${Math.floor(Math.random()*1000)}`;
const videoPayload = {
title: "Smoke Test Video " + Date.now(),
description: "Automated smoke test artifact.",
url: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
videoRootId: seriesId,
mimeType: "video/mp4"
};
const videoEvent = await nostrClient.publishVideo(videoPayload, nostrClient.pubkey);
browserLog(`Video published. Event ID: ${videoEvent.id}`);
browserLog('Step 5: Verifying Video Presence...');
const fetched = await nostrClient.getEventById(videoEvent.id);
if (!fetched) throw new Error('Failed to retrieve video event from client cache/network.');
browserLog('Video event verified.');
browserLog('Step 6: DM Flow (Send to Self)...');
const message = "Smoke test DM " + Date.now();
const npub = encodeHexToNpub(nostrClient.pubkey);
browserLog(`Sending DM to self (${npub})...`);
const dmResult = await nostrClient.sendDirectMessage(npub, message);
if (!dmResult.ok) throw new Error(`DM send failed: ${dmResult.error} ${JSON.stringify(dmResult.details || {})}`);
browserLog('DM sent.');
browserLog('Step 7: Verifying DM Decryption...');
let found = null;
for (let i = 0; i < 5; i++) {
await new Promise(r => setTimeout(r, 1000));
const dms = await nostrClient.listDirectMessages(nostrClient.pubkey, { limit: 10 });
found = dms.find(dm => dm.plaintext === message);
if (found) break;
}
if (!found) {
throw new Error('DM verification failed: Message not found in decrypted list after retries.');
}
browserLog('DM verified and decrypted successfully.');
return { success: true, logs };
} catch (err) {
browserLog(`Error: ${err.message}`);
return { success: false, error: err.message, stack: err.stack, logs };
}
});
if (result.success) {
log('SUCCESS: All smoke tests passed.');
fs.writeFileSync(SUMMARY_FILE, JSON.stringify({
status: 'success',
timestamp: new Date().toISOString(),
logs: result.logs
}, null, 2));
} else {
log('FAILURE: Smoke tests failed.');
log(`Error: ${result.error}`);
fs.writeFileSync(SUMMARY_FILE, JSON.stringify({
status: 'failure',
timestamp: new Date().toISOString(),
error: result.error,
stack: result.stack,
logs: result.logs
}, null, 2));
const screenshotPath = path.join(ARTIFACTS_DIR, 'smoke-failure.png');
await page.screenshot({ path: screenshotPath });
log(`Screenshot saved to ${screenshotPath}`);
process.exitCode = 1;
}
} catch (error) {
log(`CRITICAL ERROR: ${error.message}`);
console.error(error);
process.exitCode = 1;
} finally {
if (browser) await browser.close();
if (serverProcess) {
log('Stopping server...');
serverProcess.kill();
}
}
}
main();