Skip to content

Commit 2960e28

Browse files
docs: adjust agents.md (2nd iteration)
1 parent 01e22ec commit 2960e28

File tree

1 file changed

+203
-14
lines changed

1 file changed

+203
-14
lines changed

AGENTS.md

Lines changed: 203 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,23 @@ This file provides context for AI coding tools (GitHub Copilot, Cursor, Aider, G
2525
-`client.current_signer` → track current account name for switching back
2626
-`NamedAccount.create(name)` → for local dev (not Account.create())
2727

28+
**Time conversion (methods, not imports):**
29+
-`client.arkiv.to_seconds(days=7)` → method on arkiv module
30+
-`client.arkiv.to_blocks(days=1)` → method on arkiv module
31+
-`from arkiv import to_seconds` → WRONG, not exported
32+
33+
**Account and entity access:**
34+
-`client.eth.default_account` → current account address (string)
35+
-`entity.attributes` → dict of custom attributes (or None)
36+
-`entity.payload` → bytes data (or None, always check!)
37+
-`account.address` → when you have NamedAccount object
38+
39+
**Testing:**
40+
- ✅ Use fixture name `arkiv_client` (not `client`)
41+
- ✅ Use fixture name `arkiv_node` for node access
42+
- ✅ Session-scoped fixtures = shared blockchain state
43+
- ✅ Use unique identifiers (timestamps/UUIDs) for test isolation
44+
2845
**Run examples:** `uv run python -m arkiv_starter.01_clients` (etc., 01-05)
2946

3047
---
@@ -441,7 +458,7 @@ account = Account.create() # LocalAccount won't work with node.fund_account()
441458
### Account Attributes
442459

443460
```python
444-
# ✅ CORRECT
461+
# ✅ CORRECT - When you have an account object
445462
print(account.address) # Ethereum address
446463
print(account.private_key) # Private key for signing
447464
print(account.name) # Account name (NamedAccount only)
@@ -450,6 +467,22 @@ print(account.name) # Account name (NamedAccount only)
450467
print(account.key) # Use account.private_key instead
451468
```
452469

470+
### Accessing the Current Account from Client
471+
472+
```python
473+
# ✅ CORRECT - Get current account address from client
474+
current_address = client.eth.default_account # Returns address string
475+
476+
# ✅ CORRECT - Check if entity belongs to current user
477+
entity = client.arkiv.get_entity(entity_key)
478+
if entity.owner != client.eth.default_account:
479+
print("You don't own this entity")
480+
481+
# ❌ WRONG - These don't exist
482+
client.account.address # AttributeError
483+
client.default_account.address # AttributeError
484+
```
485+
453486
### Managing Multiple Accounts
454487

455488
The Arkiv client can manage multiple accounts and switch between them:
@@ -521,21 +554,149 @@ cd src && python -m arkiv_starter.02_entity_crud # Wrong directory
521554

522555
## 🧪 Testing Patterns
523556

524-
### Use Fixtures from conftest.py
557+
### Available Test Fixtures
558+
559+
The project's `conftest.py` provides these fixtures:
525560

526561
```python
527-
# Tests have access to these fixtures:
528-
def test_something(client, account, arkiv_node):
529-
# client: Arkiv instance
530-
# account: NamedAccount instance
531-
# arkiv_node: ArkivNode instance (already started and funded)
562+
# ✅ CORRECT - Use these exact fixture names
563+
def test_something(arkiv_client, arkiv_node):
564+
# arkiv_client: Arkiv instance (session-scoped, shared across tests)
565+
# arkiv_node: ArkivNode instance (session-scoped, already started)
532566

533-
entity_key, receipt = client.arkiv.create_entity(
567+
entity_key, receipt = arkiv_client.arkiv.create_entity(
534568
payload=b"test data",
535569
expires_in=3600,
536570
content_type="text/plain"
537571
)
538572
assert entity_key is not None
573+
574+
# ❌ WRONG - These fixture names don't exist
575+
def test_something(client, account): # NameError: fixture 'client' not found
576+
```
577+
578+
### Test Isolation with Session-Scoped Fixtures
579+
580+
**⚠️ CRITICAL:** Session-scoped fixtures mean:
581+
- All tests share the SAME node and blockchain
582+
- Data created in one test is visible in other tests
583+
- Tests run in order but share state
584+
585+
**Solutions for test isolation:**
586+
587+
```python
588+
# ✅ Pattern 1: Unique identifiers per test
589+
import pytest
590+
import time
591+
592+
@pytest.fixture
593+
def unique_channel():
594+
"""Generate unique channel name for test isolation."""
595+
return f"test-{int(time.time() * 1000000)}"
596+
597+
def test_messages(arkiv_client, unique_channel):
598+
chat = ChatClient(arkiv_client, channel=unique_channel)
599+
# Now isolated from other tests
600+
601+
# ✅ Pattern 2: Function-scoped client for full isolation
602+
from typing import cast
603+
from web3.providers.base import BaseProvider
604+
from arkiv import Arkiv, NamedAccount
605+
from arkiv.provider import ProviderBuilder
606+
607+
@pytest.fixture
608+
def isolated_client(arkiv_node):
609+
"""Create a fresh client with unique account per test."""
610+
provider = cast(BaseProvider, ProviderBuilder().node(arkiv_node).build())
611+
account = NamedAccount.create(f"test-{int(time.time() * 1000000)}")
612+
arkiv_node.fund_account(account)
613+
client = Arkiv(provider, account=account)
614+
yield client
615+
client.arkiv.cleanup_filters()
616+
617+
def test_with_isolation(isolated_client):
618+
# Fresh client with unique account
619+
pass
620+
621+
# ✅ Pattern 3: Query with test-specific filters
622+
def test_user_messages(arkiv_client, unique_channel):
623+
alice = ChatClient(arkiv_client, username="Alice", channel=unique_channel)
624+
alice.send_message("Test message")
625+
626+
# Query may return messages from other tests too
627+
all_alice_msgs = alice.get_user_messages("Alice")
628+
629+
# Filter for messages from THIS test
630+
test_msgs = [m for m in all_alice_msgs if "Test message" in m["text"]]
631+
assert len(test_msgs) == 1
632+
```
633+
634+
### Testing Event Watchers
635+
636+
Event watchers require special timing considerations:
637+
638+
```python
639+
import time
640+
641+
def test_event_watching(arkiv_client):
642+
received_events = []
643+
644+
def on_event(event, tx_hash):
645+
received_events.append(event)
646+
647+
# ✅ CORRECT - Set up watcher BEFORE creating entities
648+
watcher = arkiv_client.arkiv.watch_entity_created(on_event)
649+
650+
# Give watcher time to initialize
651+
time.sleep(0.2)
652+
653+
# Create entity
654+
entity_key, receipt = arkiv_client.arkiv.create_entity(
655+
payload=b"test",
656+
expires_in=3600,
657+
content_type="text/plain"
658+
)
659+
660+
# Wait for event processing (local node needs time)
661+
time.sleep(1.0)
662+
663+
# Now check events
664+
assert len(received_events) >= 1
665+
666+
# Cleanup
667+
watcher.uninstall()
668+
669+
# ❌ WRONG - Common pitfalls
670+
def test_event_watching_wrong(arkiv_client):
671+
# Create entity FIRST → miss the event!
672+
entity_key, _ = arkiv_client.arkiv.create_entity(...)
673+
674+
# Set up watcher AFTER → too late!
675+
watcher = arkiv_client.arkiv.watch_entity_created(on_event)
676+
677+
# No sleep → event not processed yet
678+
assert len(received_events) >= 1 # Fails!
679+
```
680+
681+
### Type Hints for Arkiv Code
682+
683+
```python
684+
# ✅ CORRECT - Standard type hint imports
685+
from typing import Any, Callable, Optional, cast
686+
from web3.providers.base import BaseProvider
687+
from arkiv import Arkiv, NamedAccount
688+
from arkiv.types import CreateEvent, DeleteEvent, TxHash
689+
690+
# Callback type hints
691+
def setup_watcher(callback: Callable[[str, int], None]) -> None:
692+
pass
693+
694+
# Provider casting (avoids IDE errors)
695+
provider = cast(BaseProvider, ProviderBuilder().node(node).build())
696+
697+
# ❌ WRONG - Old-style or incorrect
698+
def setup_watcher(callback: callable) -> None: # Use Callable, not callable
699+
pass
539700
```
540701

541702
### Local Node Doesn't Support All Queries Yet
@@ -589,6 +750,27 @@ except ValueError:
589750
print("Entity not found")
590751
```
591752

753+
### Accessing Entity Attributes
754+
755+
```python
756+
# ✅ CORRECT - Attributes are directly on the entity object
757+
entity = client.arkiv.get_entity(entity_key)
758+
attr_dict = entity.attributes # Returns dict or None
759+
760+
# Example: Get channel from attributes
761+
if entity.attributes:
762+
channel = entity.attributes.get("channel", "default")
763+
else:
764+
channel = "default"
765+
766+
# Example: Access payload safely
767+
if entity.payload:
768+
data = entity.payload.decode('utf-8')
769+
770+
# ❌ WRONG - No separate method exists
771+
attributes = client.arkiv.get_entity_attributes(entity_key) # Doesn't exist!
772+
```
773+
592774
### Querying Entities
593775

594776
```python
@@ -663,14 +845,15 @@ for event in event_filter.get_new_entries():
663845
### Time Conversions
664846

665847
```python
666-
from arkiv import to_seconds, to_blocks
667-
668-
# Convert time to seconds
669-
expires_in = to_seconds(days=7)
670-
expires_in = to_seconds(hours=2, minutes=30)
848+
# ✅ CORRECT - to_seconds and to_blocks are METHODS on the client
849+
expires_in = client.arkiv.to_seconds(days=7)
850+
expires_in = client.arkiv.to_seconds(hours=2, minutes=30)
671851

672852
# Convert time to blocks (assuming 2s block time)
673-
expires_in_blocks = to_blocks(days=1, block_time=2)
853+
expires_in_blocks = client.arkiv.to_blocks(days=1, block_time=2)
854+
855+
# ❌ WRONG - Don't try to import them
856+
from arkiv import to_seconds, to_blocks # ImportError!
674857
```
675858

676859
---
@@ -961,6 +1144,12 @@ client.arkiv.watch_entity_created(on_entity_created)
9611144
11. **Prefer high-level event watchers** - Use `watch_entity_*()` methods over raw contract filters
9621145
12. **Note the watch_entity_deleted type bug** - Always add `# type: ignore[arg-type]` when using it
9631146
13. **Use `entity_exists()` to check existence** - Cleaner than try-except with `get_entity()`
1147+
14. **Test fixture names matter** - Use `arkiv_client` and `arkiv_node`, not `client` or `account`
1148+
15. **Session-scoped fixtures share state** - Use unique identifiers (timestamps, UUIDs) for test isolation
1149+
16. **Event watchers need timing** - Set up BEFORE entity creation, add sleeps for processing
1150+
17. **to_seconds is a method** - Use `client.arkiv.to_seconds()`, not `from arkiv import to_seconds`
1151+
18. **Current account access** - Use `client.eth.default_account`, not `client.account.address`
1152+
19. **Entity attributes are direct** - Use `entity.attributes`, not a separate method call
9641153

9651154
---
9661155

0 commit comments

Comments
 (0)