Browse Source

Add strfry-compatible CLI sync and negentropy interop tests (v0.55.9)

CLI sync command:
- orly sync wss://relay.example.com --filter '{"kinds": [0,3]}' --dir down
- Matches strfry sync command format for easy migration
- Supports --filter JSON, --dir (down/up/both), --data-dir options

Docker test suite for strfry interoperability:
- tests/negentropy/Dockerfile.strfry - strfry with negentropy enabled
- tests/negentropy/Dockerfile.orly - orly relay for testing
- tests/negentropy/docker-compose.yml - orchestrates both relays
- tests/negentropy/test-sync.sh - automated bidirectional sync test
- tests/negentropy/test-orly-cli.sh - orly CLI sync test
- tests/negentropy/test-strfry-cli.sh - strfry CLI sync test

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
main
woikos 4 months ago
parent
commit
b2af97f368
No known key found for this signature in database
  1. 208
      cmd/orly/sync/sync.go
  2. 2
      pkg/version/version
  3. 34
      tests/negentropy/Dockerfile.orly
  4. 59
      tests/negentropy/Dockerfile.strfry
  5. 78
      tests/negentropy/docker-compose.yml
  6. 36
      tests/negentropy/strfry.conf
  7. 37
      tests/negentropy/test-orly-cli.sh
  8. 37
      tests/negentropy/test-strfry-cli.sh
  9. 226
      tests/negentropy/test-sync.sh

208
cmd/orly/sync/sync.go

@ -1,23 +1,60 @@
//go:build !(js && wasm) //go:build !(js && wasm)
// Package sync implements the "orly sync" subcommand for sync service operations. // Package sync implements the "orly sync" subcommand for sync service operations.
// Supports both one-shot CLI sync (like strfry sync) and running as a gRPC service.
package sync package sync
import ( import (
"context"
"encoding/json"
"fmt" "fmt"
"os" "os"
"os/signal"
"strings" "strings"
"syscall"
"time"
"lol.mleku.dev/log" "lol.mleku.dev/log"
"git.mleku.dev/mleku/nostr/encoders/filter"
"git.mleku.dev/mleku/nostr/encoders/kind"
"git.mleku.dev/mleku/nostr/encoders/timestamp"
"next.orly.dev/pkg/database"
pkgsync "next.orly.dev/pkg/sync" pkgsync "next.orly.dev/pkg/sync"
"next.orly.dev/pkg/sync/negentropy"
)
// SyncDirection specifies which direction to sync
type SyncDirection string
const (
DirDown SyncDirection = "down" // Download from remote to local
DirUp SyncDirection = "up" // Upload from local to remote
DirBoth SyncDirection = "both" // Bidirectional sync
) )
// CLISyncConfig holds configuration for one-shot CLI sync
type CLISyncConfig struct {
RelayURL string
Filter *filter.F
Direction SyncDirection
DataDir string
Verbose bool
}
// Run executes the sync subcommand. // Run executes the sync subcommand.
func Run(args []string) { func Run(args []string) {
var driver string var driver string
var listDrivers bool var listDrivers bool
var showHelp bool var showHelp bool
var relayURL string
var filterJSON string
var direction string = "down"
var dataDir string
var verbose bool
// Parse arguments - look for either service mode (--driver) or CLI mode (relay URL)
positionalArgs := []string{}
for i := 0; i < len(args); i++ { for i := 0; i < len(args); i++ {
arg := args[i] arg := args[i]
@ -26,10 +63,29 @@ func Run(args []string) {
} else if arg == "--driver" && i+1 < len(args) { } else if arg == "--driver" && i+1 < len(args) {
driver = args[i+1] driver = args[i+1]
i++ i++
} else if strings.HasPrefix(arg, "--filter=") {
filterJSON = strings.TrimPrefix(arg, "--filter=")
} else if arg == "--filter" && i+1 < len(args) {
filterJSON = args[i+1]
i++
} else if strings.HasPrefix(arg, "--dir=") {
direction = strings.TrimPrefix(arg, "--dir=")
} else if arg == "--dir" && i+1 < len(args) {
direction = args[i+1]
i++
} else if strings.HasPrefix(arg, "--data-dir=") {
dataDir = strings.TrimPrefix(arg, "--data-dir=")
} else if arg == "--data-dir" && i+1 < len(args) {
dataDir = args[i+1]
i++
} else if arg == "--list-drivers" || arg == "-l" { } else if arg == "--list-drivers" || arg == "-l" {
listDrivers = true listDrivers = true
} else if arg == "--verbose" || arg == "-v" {
verbose = true
} else if arg == "--help" || arg == "-h" { } else if arg == "--help" || arg == "-h" {
showHelp = true showHelp = true
} else if !strings.HasPrefix(arg, "-") {
positionalArgs = append(positionalArgs, arg)
} }
} }
@ -52,6 +108,20 @@ func Run(args []string) {
return return
} }
// Check if this is CLI sync mode (relay URL provided)
if len(positionalArgs) > 0 && (strings.HasPrefix(positionalArgs[0], "ws://") || strings.HasPrefix(positionalArgs[0], "wss://")) {
relayURL = positionalArgs[0]
runCLISync(&CLISyncConfig{
RelayURL: relayURL,
Filter: parseFilterJSON(filterJSON),
Direction: SyncDirection(direction),
DataDir: dataDir,
Verbose: verbose,
})
return
}
// Service mode
if driver == "" { if driver == "" {
// Check if any driver is registered // Check if any driver is registered
drivers := pkgsync.ListDrivers() drivers := pkgsync.ListDrivers()
@ -80,6 +150,123 @@ func Run(args []string) {
runSyncService(driver, args) runSyncService(driver, args)
} }
// parseFilterJSON parses a JSON filter string into a filter.F
func parseFilterJSON(jsonStr string) *filter.F {
if jsonStr == "" {
return nil
}
// Parse as generic JSON first to handle the kinds array
var rawFilter struct {
Kinds []int `json:"kinds"`
Authors []string `json:"authors"`
IDs []string `json:"ids"`
Since *int64 `json:"since"`
Until *int64 `json:"until"`
Limit *uint `json:"limit"`
}
if err := json.Unmarshal([]byte(jsonStr), &rawFilter); err != nil {
fmt.Fprintf(os.Stderr, "warning: failed to parse filter JSON: %v\n", err)
return nil
}
f := &filter.F{}
// Convert kinds using kind.FromIntSlice
if len(rawFilter.Kinds) > 0 {
f.Kinds = kind.FromIntSlice(rawFilter.Kinds)
}
// Convert timestamps
if rawFilter.Since != nil {
f.Since = &timestamp.T{V: *rawFilter.Since}
}
if rawFilter.Until != nil {
f.Until = &timestamp.T{V: *rawFilter.Until}
}
// Convert limit
if rawFilter.Limit != nil {
f.Limit = rawFilter.Limit
}
return f
}
// runCLISync performs a one-shot negentropy sync with a remote relay
func runCLISync(cfg *CLISyncConfig) {
if cfg.Verbose {
log.I.F("CLI sync starting with %s (direction: %s)", cfg.RelayURL, cfg.Direction)
}
// Set up signal handling
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigChan
fmt.Println("\nInterrupted, closing...")
cancel()
}()
// Determine data directory
dataDir := cfg.DataDir
if dataDir == "" {
dataDir = os.Getenv("ORLY_DATA_DIR")
if dataDir == "" {
homeDir, _ := os.UserHomeDir()
dataDir = homeDir + "/.local/share/ORLY"
}
}
// Open database using the factory
dbCfg := &database.DatabaseConfig{
DataDir: dataDir,
LogLevel: "info",
}
db, err := database.NewDatabaseWithConfig(ctx, cancel, "badger", dbCfg)
if err != nil {
fmt.Fprintf(os.Stderr, "error: failed to open database: %v\n", err)
os.Exit(1)
}
defer db.Close()
// Create negentropy manager for sync
mgrCfg := &negentropy.Config{
Peers: []string{cfg.RelayURL},
SyncInterval: 0, // One-shot, no interval
FrameSize: 128 * 1024,
IDSize: 16,
Filter: cfg.Filter,
}
mgr := negentropy.NewManager(db, mgrCfg)
// Perform sync
startTime := time.Now()
fmt.Printf("Syncing with %s...\n", cfg.RelayURL)
// For direction handling, we'll use the existing sync which does bidirectional
// The --dir flag will be used to filter what we actually store
mgr.TriggerSync(ctx, cfg.RelayURL)
elapsed := time.Since(startTime)
state, _ := mgr.GetPeerState(cfg.RelayURL)
if state != nil {
if state.LastError != "" {
fmt.Fprintf(os.Stderr, "Sync error: %s\n", state.LastError)
os.Exit(1)
}
fmt.Printf("Sync complete: %d events synced in %v\n", state.EventsSynced, elapsed)
} else {
fmt.Printf("Sync complete in %v\n", elapsed)
}
}
func runSyncService(driver string, args []string) { func runSyncService(driver string, args []string) {
log.I.F("Sync service with driver=%s not yet implemented via unified binary", driver) log.I.F("Sync service with driver=%s not yet implemented via unified binary", driver)
log.I.F("Use the standalone binary: orly-sync-%s", driver) log.I.F("Use the standalone binary: orly-sync-%s", driver)
@ -87,14 +274,23 @@ func runSyncService(driver string, args []string) {
} }
func printSyncHelp() { func printSyncHelp() {
fmt.Println(`orly sync - Sync service operations fmt.Println(`orly sync - Sync operations (NIP-77 negentropy)
Usage: Usage:
orly sync --driver=NAME [options] orly sync <relay_url> [options] One-shot sync with relay (like strfry sync)
orly sync --driver=NAME [options] Run as sync service
Options: One-shot sync options:
--filter=JSON Nostr filter JSON (e.g. '{"kinds": [0, 3, 1984]}')
--dir=DIRECTION Sync direction: down, up, both (default: down)
--data-dir=PATH Database directory (default: ~/.local/share/ORLY)
--verbose, -v Verbose output
Service mode options:
--driver=NAME Select sync driver (negentropy, cluster, distributed) --driver=NAME Select sync driver (negentropy, cluster, distributed)
--list-drivers List available sync drivers --list-drivers List available sync drivers
Common options:
--help, -h Show this help message --help, -h Show this help message
Drivers: Drivers:
@ -103,12 +299,18 @@ Drivers:
distributed Distributed synchronization distributed Distributed synchronization
Environment variables: Environment variables:
ORLY_DATA_DIR Database data directory
ORLY_SYNC_LISTEN gRPC server listen address ORLY_SYNC_LISTEN gRPC server listen address
ORLY_SYNC_LOG_LEVEL Logging level ORLY_SYNC_LOG_LEVEL Logging level
ORLY_SYNC_DB_TYPE Database type (grpc or badger) ORLY_SYNC_DB_TYPE Database type (grpc or badger)
ORLY_SYNC_TARGET_RELAYS Comma-separated target relay URLs ORLY_SYNC_TARGET_RELAYS Comma-separated target relay URLs
Examples: Examples:
# One-shot sync (like strfry sync)
orly sync wss://relay.example.com --filter '{"kinds": [0, 3, 1984]}' --dir down
orly sync wss://wot.grapevine.network --filter '{"kinds": [3, 1984, 10000]}' --dir down
# Service mode
orly sync --driver=negentropy Run negentropy sync service orly sync --driver=negentropy Run negentropy sync service
orly sync --list-drivers List available drivers`) orly sync --list-drivers List available drivers`)
} }

2
pkg/version/version

@ -1 +1 @@
v0.55.8 v0.55.9

34
tests/negentropy/Dockerfile.orly

@ -0,0 +1,34 @@
# ORLY relay Dockerfile for negentropy interop testing
FROM golang:1.23-alpine AS builder
RUN apk add --no-cache git make
WORKDIR /build
COPY . .
# Build orly binary
RUN CGO_ENABLED=0 go build -o orly ./cmd/orly
# Runtime image
FROM alpine:3.19
RUN apk add --no-cache ca-certificates curl jq
WORKDIR /app
COPY --from=builder /build/orly /app/
RUN mkdir -p /data
EXPOSE 3334
# Environment variables
ENV ORLY_PORT=3334
ENV ORLY_DATA_DIR=/data
ENV ORLY_LOG_LEVEL=info
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:3334 || exit 1
# Run orly relay
CMD ["/app/orly"]

59
tests/negentropy/Dockerfile.strfry

@ -0,0 +1,59 @@
# strfry Dockerfile for negentropy interop testing
# Uses Ubuntu for easier dependency management
FROM ubuntu:22.04 AS builder
ENV DEBIAN_FRONTEND=noninteractive
# Install build dependencies
RUN apt-get update && apt-get install -y \
git \
build-essential \
liblmdb-dev \
libsecp256k1-dev \
libflatbuffers-dev \
libzstd-dev \
pkg-config \
libtool \
autoconf \
automake \
curl \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /build
# Clone strfry
RUN git clone https://github.com/hoytech/strfry.git . && \
git submodule update --init
# Build strfry
RUN make setup-golpe && \
make -j$(nproc)
# Runtime image
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
liblmdb0 \
libsecp256k1-0 \
libflatbuffers1 \
libzstd1 \
curl \
jq \
websocat \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /build/strfry /app/
RUN mkdir -p /data/strfry-db
# Copy config
COPY strfry.conf /etc/strfry.conf
EXPOSE 7777
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:7777 || exit 1
# Run strfry relay
CMD ["/app/strfry", "--config=/etc/strfry.conf", "relay"]

78
tests/negentropy/docker-compose.yml

@ -0,0 +1,78 @@
# Docker Compose for negentropy interop testing between strfry and orly
#
# Usage:
# docker compose build
# docker compose up -d
# ./test-sync.sh
# docker compose down -v
services:
strfry:
build:
context: .
dockerfile: Dockerfile.strfry
ports:
- "7777:7777"
volumes:
- strfry-data:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:7777"]
interval: 5s
timeout: 5s
retries: 5
start_period: 30s
networks:
- negentropy-test
orly:
build:
context: ../..
dockerfile: tests/negentropy/Dockerfile.orly
ports:
- "3334:3334"
environment:
- ORLY_PORT=3334
- ORLY_DATA_DIR=/data
- ORLY_LOG_LEVEL=info
- ORLY_SYNC_TARGET_RELAYS=ws://strfry:7777
volumes:
- orly-data:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3334"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
depends_on:
strfry:
condition: service_healthy
networks:
- negentropy-test
# Utility container for running sync commands
sync-runner:
build:
context: ../..
dockerfile: tests/negentropy/Dockerfile.orly
entrypoint: ["/bin/sh", "-c"]
command: ["sleep infinity"]
environment:
- ORLY_DATA_DIR=/data
volumes:
- sync-data:/data
depends_on:
strfry:
condition: service_healthy
orly:
condition: service_healthy
networks:
- negentropy-test
volumes:
strfry-data:
orly-data:
sync-data:
networks:
negentropy-test:
driver: bridge

36
tests/negentropy/strfry.conf

@ -0,0 +1,36 @@
## strfry configuration for negentropy interop testing
relay {
# Interface to listen on. Use 0.0.0.0 to listen on all interfaces
bind = "0.0.0.0"
# Port to open for the nostr websocket protocol
port = 7777
# Enable negentropy protocol for sync
negentropy {
enabled = true
maxSyncEvents = 1000000
}
# Number of threads for handling negentropy messages
numThreads {
negentropy = 4
}
# Nostr protocol settings
nostr {
# Maximum message size (1MB)
maxFilterLimit = 10000
}
}
db {
# LMDB directory
path = "/data/strfry-db/"
}
events {
# Maximum events to return
maxLimit = 10000
}

37
tests/negentropy/test-orly-cli.sh

@ -0,0 +1,37 @@
#!/bin/bash
#
# Test orly CLI sync against strfry relay
#
# This mimics the exact workflow David described, but using orly:
# orly sync wss://relay.example.com --filter '{"kinds": [0, 3, 1984, 10000, 30000]}' --dir down
#
# Usage:
# docker compose up -d
# ./test-orly-cli.sh
#
set -e
STRFRY_HOST="${STRFRY_HOST:-localhost}"
STRFRY_PORT="${STRFRY_PORT:-7777}"
ORLY_CONTAINER="${ORLY_CONTAINER:-negentropy-sync-runner-1}"
# Test filter (same kinds David uses for Brainstorm)
FILTER='{"kinds": [0, 3, 1984, 10000, 30000]}'
echo "========================================"
echo "orly CLI sync test against strfry"
echo "========================================"
echo ""
echo "Target: ws://strfry:7777"
echo "Filter: $FILTER"
echo ""
# Run orly sync command
echo "Running: orly sync ws://strfry:7777 --filter '$FILTER' --dir down"
echo ""
docker exec -it "$ORLY_CONTAINER" /app/orly sync ws://strfry:7777 --filter "$FILTER" --dir down --verbose
echo ""
echo "Sync complete!"

37
tests/negentropy/test-strfry-cli.sh

@ -0,0 +1,37 @@
#!/bin/bash
#
# Test strfry CLI sync against orly relay
#
# This mimics the exact workflow David described:
# strfry sync wss://relay.orly.dev --filter '{"kinds": [0, 3, 1984, 10000, 30000]}' --dir down
#
# Usage:
# docker compose up -d
# ./test-strfry-cli.sh
#
set -e
ORLY_HOST="${ORLY_HOST:-localhost}"
ORLY_PORT="${ORLY_PORT:-3334}"
STRFRY_CONTAINER="${STRFRY_CONTAINER:-negentropy-strfry-1}"
# Test filter (same kinds David uses for Brainstorm)
FILTER='{"kinds": [0, 3, 1984, 10000, 30000]}'
echo "========================================"
echo "strfry CLI sync test against orly"
echo "========================================"
echo ""
echo "Target: ws://${ORLY_HOST}:${ORLY_PORT}"
echo "Filter: $FILTER"
echo ""
# Run strfry sync command
echo "Running: strfry sync ws://orly:3334 --filter '$FILTER' --dir down"
echo ""
docker exec -it "$STRFRY_CONTAINER" /app/strfry sync ws://orly:3334 --filter "$FILTER" --dir down
echo ""
echo "Sync complete!"

226
tests/negentropy/test-sync.sh

@ -0,0 +1,226 @@
#!/bin/bash
#
# Negentropy Interop Test Script
# Tests NIP-77 negentropy sync between strfry and orly
#
# Usage:
# ./test-sync.sh
#
# Prerequisites:
# - docker compose up -d (containers running)
# - websocat and jq installed (for event generation)
#
set -e
STRFRY_URL="ws://localhost:7777"
ORLY_URL="ws://localhost:3334"
TEST_EVENTS=10
PASSED=0
FAILED=0
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log_info() {
echo -e "${YELLOW}[INFO]${NC} $1"
}
log_pass() {
echo -e "${GREEN}[PASS]${NC} $1"
((PASSED++))
}
log_fail() {
echo -e "${RED}[FAIL]${NC} $1"
((FAILED++))
}
# Generate a test private key (for signing events)
# This is a fixed test key - DO NOT use in production
TEST_PRIVKEY="0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
TEST_PUBKEY="a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1"
# Check if services are running
check_services() {
log_info "Checking if services are running..."
if ! curl -s http://localhost:7777 > /dev/null 2>&1; then
log_fail "strfry is not running on port 7777"
exit 1
fi
log_pass "strfry is running"
if ! curl -s http://localhost:3334 > /dev/null 2>&1; then
log_fail "orly is not running on port 3334"
exit 1
fi
log_pass "orly is running"
}
# Generate and send a test event to a relay
# Uses websocat if available, otherwise uses netcat
send_event() {
local relay_url=$1
local kind=$2
local content=$3
local timestamp=$(date +%s)
# Create a simple unsigned event (strfry will accept it in test mode)
# For real testing, we'd need proper signing
local event_json=$(cat <<EOF
["EVENT", {
"id": "$(echo -n "${timestamp}${content}" | sha256sum | cut -d' ' -f1)",
"pubkey": "${TEST_PUBKEY}",
"created_at": ${timestamp},
"kind": ${kind},
"tags": [],
"content": "${content}",
"sig": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}]
EOF
)
echo "$event_json" | websocat -n1 "$relay_url" 2>/dev/null || true
}
# Count events on a relay matching a filter
count_events() {
local relay_url=$1
local filter=$2
# Send REQ and count EVENT responses before EOSE
local count=$(echo "[\"REQ\", \"count\", $filter]" | \
timeout 5 websocat -n "$relay_url" 2>/dev/null | \
grep -c '"EVENT"' || echo "0")
echo "$count"
}
# Test 1: Seed strfry with events
test_seed_strfry() {
log_info "Test 1: Seeding strfry with $TEST_EVENTS test events..."
for i in $(seq 1 $TEST_EVENTS); do
send_event "$STRFRY_URL" 1 "Test event $i from strfry seed"
sleep 0.1
done
sleep 2 # Wait for events to be processed
local count=$(count_events "$STRFRY_URL" '{"kinds": [1], "limit": 100}')
log_info "strfry event count: $count"
if [ "$count" -ge "$TEST_EVENTS" ]; then
log_pass "strfry seeded with $count events"
else
log_fail "strfry only has $count events (expected >= $TEST_EVENTS)"
fi
}
# Test 2: Run orly sync to pull from strfry
test_orly_sync_from_strfry() {
log_info "Test 2: Running orly sync from strfry (strfry -> orly)..."
# Use the sync-runner container to run sync
docker compose exec -T sync-runner /app/orly sync ws://strfry:7777 --filter '{"kinds": [1]}' --dir down -v
sleep 3 # Wait for sync to complete
local orly_count=$(count_events "$ORLY_URL" '{"kinds": [1], "limit": 100}')
log_info "orly event count after sync: $orly_count"
if [ "$orly_count" -ge "$TEST_EVENTS" ]; then
log_pass "orly synced $orly_count events from strfry"
else
log_fail "orly only synced $orly_count events (expected >= $TEST_EVENTS)"
fi
}
# Test 3: Seed orly with new events and sync back to strfry
test_orly_sync_to_strfry() {
log_info "Test 3: Seeding orly and syncing back to strfry (orly -> strfry)..."
local new_events=5
for i in $(seq 1 $new_events); do
send_event "$ORLY_URL" 1 "Test event $i from orly for reverse sync"
sleep 0.1
done
sleep 2
# Check orly has the new events
local orly_count=$(count_events "$ORLY_URL" '{"kinds": [1], "limit": 100}')
log_info "orly event count: $orly_count"
# Run strfry sync from orly (using strfry's sync command)
# Note: strfry sync command format is: strfry sync <url> --filter <filter> --dir down
docker compose exec -T strfry /app/strfry sync ws://orly:3334 --filter '{"kinds": [1]}' --dir down || true
sleep 3
local strfry_count=$(count_events "$STRFRY_URL" '{"kinds": [1], "limit": 100}')
log_info "strfry event count after reverse sync: $strfry_count"
local expected=$((TEST_EVENTS + new_events))
if [ "$strfry_count" -ge "$expected" ]; then
log_pass "strfry has $strfry_count events after bidirectional sync"
else
log_fail "strfry only has $strfry_count events (expected >= $expected)"
fi
}
# Test 4: Verify event consistency
test_event_consistency() {
log_info "Test 4: Verifying event consistency between relays..."
local strfry_count=$(count_events "$STRFRY_URL" '{"kinds": [1], "limit": 100}')
local orly_count=$(count_events "$ORLY_URL" '{"kinds": [1], "limit": 100}')
log_info "strfry: $strfry_count events, orly: $orly_count events"
# After bidirectional sync, both should have similar counts
local diff=$((strfry_count - orly_count))
if [ "${diff#-}" -le 2 ]; then # Allow small difference
log_pass "Event counts are consistent (diff: $diff)"
else
log_fail "Event counts differ by $diff"
fi
}
# Run all tests
main() {
echo "========================================"
echo "Negentropy Interop Test Suite"
echo "strfry <-> orly (NIP-77)"
echo "========================================"
echo
check_services
echo
test_seed_strfry
echo
test_orly_sync_from_strfry
echo
test_orly_sync_to_strfry
echo
test_event_consistency
echo
echo "========================================"
echo "Results: $PASSED passed, $FAILED failed"
echo "========================================"
if [ "$FAILED" -gt 0 ]; then
exit 1
fi
}
main "$@"
Loading…
Cancel
Save