#!/usr/bin/env python3 """Tests for Nostr agent messaging layer.""" import json import os import sys import tempfile import unittest from pathlib import Path sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) from tools.nostr_agent import ( NostrKeypair, NostrEvent, FleetDirectory, AgentMessenger, bech32_encode, bech32_decode, encrypt_dm, decrypt_dm, ) class TestBech32(unittest.TestCase): """Test NIP-19 bech32 encoding.""" def test_encode_decode_roundtrip(self): data = os.urandom(32) encoded = bech32_encode("npub", data) self.assertTrue(encoded.startswith("npub1")) hrp, decoded = bech32_decode(encoded) self.assertEqual(hrp, "npub") self.assertEqual(decoded, data) def test_nsec_encode_decode(self): data = os.urandom(32) encoded = bech32_encode("nsec", data) self.assertTrue(encoded.startswith("nsec1")) hrp, decoded = bech32_decode(encoded) self.assertEqual(hrp, "nsec") self.assertEqual(decoded, data) class TestNostrKeypair(unittest.TestCase): """Test key generation and management.""" def test_generate(self): kp = NostrKeypair.generate() self.assertEqual(len(kp.public_key), 32) self.assertEqual(len(kp.secret_key), 32) self.assertTrue(kp.npub.startswith("npub1")) self.assertTrue(kp.nsec.startswith("nsec1")) def test_pubkey_hex(self): kp = NostrKeypair.generate() self.assertEqual(len(kp.pubkey_hex), 64) self.assertEqual(len(kp.seckey_hex), 64) def test_deterministic_from_nsec(self): kp1 = NostrKeypair.generate() kp2 = NostrKeypair.from_nsec(kp1.nsec) self.assertEqual(kp1.pubkey_hex, kp2.pubkey_hex) self.assertEqual(kp1.seckey_hex, kp2.seckey_hex) def test_from_hex(self): kp1 = NostrKeypair.generate() kp2 = NostrKeypair.from_hex(kp1.seckey_hex) self.assertEqual(kp1.pubkey_hex, kp2.pubkey_hex) def test_save_load(self): with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as f: path = f.name try: kp1 = NostrKeypair.generate() kp1.save(path) kp2 = NostrKeypair.load(path) self.assertEqual(kp1.pubkey_hex, kp2.pubkey_hex) self.assertEqual(kp1.seckey_hex, kp2.seckey_hex) # Verify file permissions mode = oct(os.stat(path).st_mode)[-3:] self.assertEqual(mode, "600") finally: os.unlink(path) def test_load_or_create_new(self): with tempfile.TemporaryDirectory() as tmp: path = os.path.join(tmp, "keys.json") kp = NostrKeypair.load_or_create(path) self.assertTrue(os.path.exists(path)) self.assertTrue(kp.npub.startswith("npub1")) def test_load_or_create_existing(self): with tempfile.TemporaryDirectory() as tmp: path = os.path.join(tmp, "keys.json") kp1 = NostrKeypair.load_or_create(path) kp2 = NostrKeypair.load_or_create(path) self.assertEqual(kp1.pubkey_hex, kp2.pubkey_hex) def test_two_keys_differ(self): kp1 = NostrKeypair.generate() kp2 = NostrKeypair.generate() self.assertNotEqual(kp1.pubkey_hex, kp2.pubkey_hex) class TestNostrEvent(unittest.TestCase): """Test event creation and signing.""" def test_create_event(self): event = NostrEvent(kind=1, content="Hello Nostr!") self.assertEqual(event.kind, 1) self.assertEqual(event.content, "Hello Nostr!") def test_compute_id(self): kp = NostrKeypair.generate() event = NostrEvent(kind=1, content="test", created_at=1234567890) event.pubkey = kp.pubkey_hex event_id = event.compute_id() self.assertEqual(len(event_id), 64) # sha256 hex # Deterministic self.assertEqual(event.compute_id(), event_id) def test_sign_event(self): kp = NostrKeypair.generate() event = NostrEvent(kind=1, content="signed message") event.sign(kp) self.assertEqual(event.pubkey, kp.pubkey_hex) self.assertEqual(len(event.id), 64) self.assertEqual(len(event.sig), 128) # 64 bytes hex def test_to_dict(self): kp = NostrKeypair.generate() event = NostrEvent(kind=1, content="test") event.sign(kp) d = event.to_dict() self.assertIn("id", d) self.assertIn("pubkey", d) self.assertIn("sig", d) self.assertIn("content", d) self.assertEqual(d["kind"], 1) def test_to_relay_message(self): kp = NostrKeypair.generate() event = NostrEvent(kind=1, content="test") event.sign(kp) msg = event.to_relay_message() parsed = json.loads(msg) self.assertEqual(parsed[0], "EVENT") self.assertEqual(parsed[1]["content"], "test") def test_from_dict(self): kp = NostrKeypair.generate() event = NostrEvent(kind=1, content="roundtrip") event.sign(kp) d = event.to_dict() event2 = NostrEvent.from_dict(d) self.assertEqual(event2.id, event.id) self.assertEqual(event2.content, event.content) def test_tags(self): event = NostrEvent(kind=1, content="tagged", tags=[["p", "abc123"], ["t", "fleet"]]) self.assertEqual(len(event.tags), 2) self.assertEqual(event.tags[0][1], "abc123") class TestEncryptedDM(unittest.TestCase): """Test encrypted direct messaging.""" def test_encrypt_decrypt_roundtrip(self): alice = NostrKeypair.generate() bob = NostrKeypair.generate() message = "Hello Bob, this is Alice!" encrypted = encrypt_dm(alice, bob.pubkey_hex, message) self.assertNotEqual(encrypted, message) self.assertIn("ciphertext", encrypted) decrypted = decrypt_dm(bob, alice.pubkey_hex, encrypted) self.assertEqual(decrypted, message) def test_wrong_key_fails(self): alice = NostrKeypair.generate() bob = NostrKeypair.generate() eve = NostrKeypair.generate() encrypted = encrypt_dm(alice, bob.pubkey_hex, "secret") with self.assertRaises(Exception): decrypt_dm(eve, alice.pubkey_hex, encrypted) def test_unicode_message(self): alice = NostrKeypair.generate() bob = NostrKeypair.generate() message = "Hello 🧙‍♂️ こんにちは العربية" encrypted = encrypt_dm(alice, bob.pubkey_hex, message) decrypted = decrypt_dm(bob, alice.pubkey_hex, encrypted) self.assertEqual(decrypted, message) def test_empty_message(self): alice = NostrKeypair.generate() bob = NostrKeypair.generate() encrypted = encrypt_dm(alice, bob.pubkey_hex, "") decrypted = decrypt_dm(bob, alice.pubkey_hex, encrypted) self.assertEqual(decrypted, "") def test_long_message(self): alice = NostrKeypair.generate() bob = NostrKeypair.generate() message = "x" * 10000 encrypted = encrypt_dm(alice, bob.pubkey_hex, message) decrypted = decrypt_dm(bob, alice.pubkey_hex, encrypted) self.assertEqual(decrypted, message) class TestFleetDirectory(unittest.TestCase): """Test fleet directory management.""" def setUp(self): self.tmp_dir = tempfile.mkdtemp() self.dir_path = os.path.join(self.tmp_dir, "directory.json") self.directory = FleetDirectory(self.dir_path) def tearDown(self): import shutil shutil.rmtree(self.tmp_dir, ignore_errors=True) def test_register_and_get(self): self.directory.register("ezra", "npub1test...", "abcdef", "scribe") entry = self.directory.get("ezra") self.assertIsNotNone(entry) self.assertEqual(entry["npub"], "npub1test...") self.assertEqual(entry["role"], "scribe") def test_get_unknown(self): self.assertIsNone(self.directory.get("unknown")) def test_get_pubkey(self): self.directory.register("bilbo", "npub1...", "deadbeef", "hobbit") self.assertEqual(self.directory.get_pubkey("bilbo"), "deadbeef") self.assertIsNone(self.directory.get_pubkey("gandalf")) def test_persistence(self): self.directory.register("ezra", "npub1...", "abc", "scribe") dir2 = FleetDirectory(self.dir_path) self.assertIsNotNone(dir2.get("ezra")) def test_list_all(self): self.directory.register("ezra", "npub1...", "abc", "scribe") self.directory.register("bilbo", "npub2...", "def", "hobbit") all_entries = self.directory.list_all() self.assertEqual(len(all_entries), 2) class TestAgentMessenger(unittest.TestCase): """Test high-level agent messaging.""" def setUp(self): self.tmp_dir = tempfile.mkdtemp() def tearDown(self): import shutil shutil.rmtree(self.tmp_dir, ignore_errors=True) def test_create_messenger(self): msg = AgentMessenger("test-wizard", keys_dir=self.tmp_dir) card = msg.identity_card() self.assertEqual(card["wizard"], "test-wizard") self.assertTrue(card["npub"].startswith("npub1")) def test_dm_roundtrip(self): ezra = AgentMessenger("ezra-test", keys_dir=os.path.join(self.tmp_dir, "ezra")) bilbo = AgentMessenger("bilbo-test", keys_dir=os.path.join(self.tmp_dir, "bilbo")) # Register each other ezra.directory = bilbo.directory # Share directory for test event = ezra.create_dm("bilbo-test", "Good morning from Ezra!") self.assertEqual(event.kind, 4) self.assertIn(bilbo.keypair.pubkey_hex, [t[1] for t in event.tags if t[0] == "p"]) decrypted = bilbo.read_dm(event) self.assertEqual(decrypted, "Good morning from Ezra!") def test_broadcast(self): ezra = AgentMessenger("ezra-test", keys_dir=os.path.join(self.tmp_dir, "ezra")) event = ezra.create_broadcast("Fleet status: all systems nominal") self.assertEqual(event.kind, 30078) self.assertEqual(event.content, "Fleet status: all systems nominal") self.assertTrue(any(t[0] == "t" and t[1] == "timmy-fleet" for t in event.tags)) def test_identity_card(self): msg = AgentMessenger("test", keys_dir=self.tmp_dir) card = msg.identity_card() self.assertIn("wizard", card) self.assertIn("npub", card) self.assertIn("pubkey_hex", card) if __name__ == "__main__": unittest.main()