Complete BZZZ functionality port to CHORUS
🎭 CHORUS now contains full BZZZ functionality adapted for containers Core systems ported: - P2P networking (libp2p with DHT and PubSub) - Task coordination (COOEE protocol) - HMMM collaborative reasoning - SHHH encryption and security - SLURP admin election system - UCXL content addressing - UCXI server integration - Hypercore logging system - Health monitoring and graceful shutdown - License validation with KACHING Container adaptations: - Environment variable configuration (no YAML files) - Container-optimized logging to stdout/stderr - Auto-generated agent IDs for container deployments - Docker-first architecture All proven BZZZ P2P protocols, AI integration, and collaboration features are now available in containerized form. Next: Build and test container deployment. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
235
pkg/hmmm_adapter/adapter_stub.go
Normal file
235
pkg/hmmm_adapter/adapter_stub.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package hmmm_adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Joiner joins a pub/sub topic (ensure availability before publish).
|
||||
type Joiner func(topic string) error
|
||||
|
||||
// Publisher publishes a raw JSON payload to a topic.
|
||||
type Publisher func(topic string, payload []byte) error
|
||||
|
||||
// Adapter bridges BZZZ pub/sub to a RawPublisher-compatible interface.
|
||||
// It does not impose any message envelope so HMMM can publish raw JSON frames.
|
||||
// The adapter provides additional features like topic caching, metrics, and validation.
|
||||
type Adapter struct {
|
||||
join Joiner
|
||||
publish Publisher
|
||||
|
||||
// Topic join cache to avoid redundant joins
|
||||
joinedTopics map[string]bool
|
||||
joinedTopicsMu sync.RWMutex
|
||||
|
||||
// Metrics tracking
|
||||
publishCount int64
|
||||
joinCount int64
|
||||
errorCount int64
|
||||
metricsLock sync.RWMutex
|
||||
|
||||
// Configuration
|
||||
maxPayloadSize int
|
||||
joinTimeout time.Duration
|
||||
publishTimeout time.Duration
|
||||
}
|
||||
|
||||
// AdapterConfig holds configuration options for the Adapter
|
||||
type AdapterConfig struct {
|
||||
MaxPayloadSize int `yaml:"max_payload_size"`
|
||||
JoinTimeout time.Duration `yaml:"join_timeout"`
|
||||
PublishTimeout time.Duration `yaml:"publish_timeout"`
|
||||
}
|
||||
|
||||
// DefaultAdapterConfig returns sensible defaults for the adapter
|
||||
func DefaultAdapterConfig() AdapterConfig {
|
||||
return AdapterConfig{
|
||||
MaxPayloadSize: 1024 * 1024, // 1MB max payload
|
||||
JoinTimeout: 30 * time.Second,
|
||||
PublishTimeout: 10 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// NewAdapter constructs a new adapter with explicit join/publish hooks.
|
||||
// Wire these to BZZZ pubsub methods, e.g., JoinDynamicTopic and a thin PublishRaw helper.
|
||||
func NewAdapter(join Joiner, publish Publisher) *Adapter {
|
||||
return NewAdapterWithConfig(join, publish, DefaultAdapterConfig())
|
||||
}
|
||||
|
||||
// NewAdapterWithConfig constructs a new adapter with custom configuration.
|
||||
func NewAdapterWithConfig(join Joiner, publish Publisher, config AdapterConfig) *Adapter {
|
||||
return &Adapter{
|
||||
join: join,
|
||||
publish: publish,
|
||||
joinedTopics: make(map[string]bool),
|
||||
maxPayloadSize: config.MaxPayloadSize,
|
||||
joinTimeout: config.JoinTimeout,
|
||||
publishTimeout: config.PublishTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
// Publish ensures the topic is joined before sending a raw payload.
|
||||
// Includes validation, caching, metrics, and timeout handling.
|
||||
func (a *Adapter) Publish(ctx context.Context, topic string, payload []byte) error {
|
||||
// Input validation
|
||||
if topic == "" {
|
||||
a.incrementErrorCount()
|
||||
return fmt.Errorf("topic cannot be empty")
|
||||
}
|
||||
if len(payload) == 0 {
|
||||
a.incrementErrorCount()
|
||||
return fmt.Errorf("payload cannot be empty")
|
||||
}
|
||||
if len(payload) > a.maxPayloadSize {
|
||||
a.incrementErrorCount()
|
||||
return fmt.Errorf("payload size %d exceeds maximum %d bytes", len(payload), a.maxPayloadSize)
|
||||
}
|
||||
|
||||
// Check if we need to join the topic (with caching)
|
||||
if !a.isTopicJoined(topic) {
|
||||
joinCtx, cancel := context.WithTimeout(ctx, a.joinTimeout)
|
||||
defer cancel()
|
||||
|
||||
if err := a.joinTopic(joinCtx, topic); err != nil {
|
||||
a.incrementErrorCount()
|
||||
return fmt.Errorf("failed to join topic %s: %w", topic, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Publish with timeout
|
||||
publishCtx, cancel := context.WithTimeout(ctx, a.publishTimeout)
|
||||
defer cancel()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- a.publish(topic, payload)
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
a.incrementErrorCount()
|
||||
return fmt.Errorf("failed to publish to topic %s: %w", topic, err)
|
||||
}
|
||||
a.incrementPublishCount()
|
||||
return nil
|
||||
case <-publishCtx.Done():
|
||||
a.incrementErrorCount()
|
||||
return fmt.Errorf("publish to topic %s timed out after %v", topic, a.publishTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
// isTopicJoined checks if a topic has already been joined (with caching)
|
||||
func (a *Adapter) isTopicJoined(topic string) bool {
|
||||
a.joinedTopicsMu.RLock()
|
||||
defer a.joinedTopicsMu.RUnlock()
|
||||
return a.joinedTopics[topic]
|
||||
}
|
||||
|
||||
// joinTopic joins a topic and updates the cache
|
||||
func (a *Adapter) joinTopic(ctx context.Context, topic string) error {
|
||||
// Double-check locking pattern to avoid redundant joins
|
||||
if a.isTopicJoined(topic) {
|
||||
return nil
|
||||
}
|
||||
|
||||
a.joinedTopicsMu.Lock()
|
||||
defer a.joinedTopicsMu.Unlock()
|
||||
|
||||
// Check again after acquiring write lock
|
||||
if a.joinedTopics[topic] {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Execute join with context
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- a.join(topic)
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
if err == nil {
|
||||
a.joinedTopics[topic] = true
|
||||
a.incrementJoinCount()
|
||||
}
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// GetMetrics returns current adapter metrics
|
||||
func (a *Adapter) GetMetrics() AdapterMetrics {
|
||||
a.metricsLock.RLock()
|
||||
defer a.metricsLock.RUnlock()
|
||||
|
||||
return AdapterMetrics{
|
||||
PublishCount: a.publishCount,
|
||||
JoinCount: a.joinCount,
|
||||
ErrorCount: a.errorCount,
|
||||
JoinedTopics: len(a.joinedTopics),
|
||||
}
|
||||
}
|
||||
|
||||
// AdapterMetrics holds metrics data for the adapter
|
||||
type AdapterMetrics struct {
|
||||
PublishCount int64 `json:"publish_count"`
|
||||
JoinCount int64 `json:"join_count"`
|
||||
ErrorCount int64 `json:"error_count"`
|
||||
JoinedTopics int `json:"joined_topics"`
|
||||
}
|
||||
|
||||
// ResetMetrics resets all metrics counters (useful for testing)
|
||||
func (a *Adapter) ResetMetrics() {
|
||||
a.metricsLock.Lock()
|
||||
defer a.metricsLock.Unlock()
|
||||
|
||||
a.publishCount = 0
|
||||
a.joinCount = 0
|
||||
a.errorCount = 0
|
||||
}
|
||||
|
||||
// ClearTopicCache clears the joined topics cache (useful for testing or reconnections)
|
||||
func (a *Adapter) ClearTopicCache() {
|
||||
a.joinedTopicsMu.Lock()
|
||||
defer a.joinedTopicsMu.Unlock()
|
||||
|
||||
a.joinedTopics = make(map[string]bool)
|
||||
}
|
||||
|
||||
// GetJoinedTopics returns a list of currently joined topics
|
||||
func (a *Adapter) GetJoinedTopics() []string {
|
||||
a.joinedTopicsMu.RLock()
|
||||
defer a.joinedTopicsMu.RUnlock()
|
||||
|
||||
topics := make([]string, 0, len(a.joinedTopics))
|
||||
for topic := range a.joinedTopics {
|
||||
topics = append(topics, topic)
|
||||
}
|
||||
return topics
|
||||
}
|
||||
|
||||
// incrementPublishCount safely increments the publish counter
|
||||
func (a *Adapter) incrementPublishCount() {
|
||||
a.metricsLock.Lock()
|
||||
a.publishCount++
|
||||
a.metricsLock.Unlock()
|
||||
}
|
||||
|
||||
// incrementJoinCount safely increments the join counter
|
||||
func (a *Adapter) incrementJoinCount() {
|
||||
a.metricsLock.Lock()
|
||||
a.joinCount++
|
||||
a.metricsLock.Unlock()
|
||||
}
|
||||
|
||||
// incrementErrorCount safely increments the error counter
|
||||
func (a *Adapter) incrementErrorCount() {
|
||||
a.metricsLock.Lock()
|
||||
a.errorCount++
|
||||
a.metricsLock.Unlock()
|
||||
}
|
||||
|
||||
358
pkg/hmmm_adapter/adapter_stub_test.go
Normal file
358
pkg/hmmm_adapter/adapter_stub_test.go
Normal file
@@ -0,0 +1,358 @@
|
||||
package hmmm_adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestAdapter_Publish_OK(t *testing.T) {
|
||||
var joined, published bool
|
||||
a := NewAdapter(
|
||||
func(topic string) error { joined = (topic == "bzzz/meta/issue/42"); return nil },
|
||||
func(topic string, payload []byte) error { published = (topic == "bzzz/meta/issue/42" && len(payload) > 0); return nil },
|
||||
)
|
||||
if err := a.Publish(context.Background(), "bzzz/meta/issue/42", []byte(`{"ok":true}`)); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !joined || !published {
|
||||
t.Fatalf("expected join and publish to be called")
|
||||
}
|
||||
|
||||
// Verify metrics
|
||||
metrics := a.GetMetrics()
|
||||
if metrics.PublishCount != 1 {
|
||||
t.Fatalf("expected publish count 1, got %d", metrics.PublishCount)
|
||||
}
|
||||
if metrics.JoinCount != 1 {
|
||||
t.Fatalf("expected join count 1, got %d", metrics.JoinCount)
|
||||
}
|
||||
if metrics.ErrorCount != 0 {
|
||||
t.Fatalf("expected error count 0, got %d", metrics.ErrorCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapter_Publish_JoinError(t *testing.T) {
|
||||
a := NewAdapter(
|
||||
func(topic string) error { return errors.New("join failed") },
|
||||
func(topic string, payload []byte) error { return nil },
|
||||
)
|
||||
if err := a.Publish(context.Background(), "t", []byte("{}")); err == nil {
|
||||
t.Fatalf("expected join error")
|
||||
}
|
||||
|
||||
// Verify error was tracked
|
||||
metrics := a.GetMetrics()
|
||||
if metrics.ErrorCount != 1 {
|
||||
t.Fatalf("expected error count 1, got %d", metrics.ErrorCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapter_Publish_PublishError(t *testing.T) {
|
||||
a := NewAdapter(
|
||||
func(topic string) error { return nil },
|
||||
func(topic string, payload []byte) error { return errors.New("publish failed") },
|
||||
)
|
||||
if err := a.Publish(context.Background(), "test-topic", []byte(`{"test":true}`)); err == nil {
|
||||
t.Fatalf("expected publish error")
|
||||
}
|
||||
|
||||
// Verify error was tracked
|
||||
metrics := a.GetMetrics()
|
||||
if metrics.ErrorCount != 1 {
|
||||
t.Fatalf("expected error count 1, got %d", metrics.ErrorCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapter_Publish_EmptyTopic(t *testing.T) {
|
||||
a := NewAdapter(
|
||||
func(topic string) error { return nil },
|
||||
func(topic string, payload []byte) error { return nil },
|
||||
)
|
||||
|
||||
err := a.Publish(context.Background(), "", []byte(`{"test":true}`))
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for empty topic")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "topic cannot be empty") {
|
||||
t.Fatalf("expected empty topic error, got: %v", err)
|
||||
}
|
||||
|
||||
metrics := a.GetMetrics()
|
||||
if metrics.ErrorCount != 1 {
|
||||
t.Fatalf("expected error count 1, got %d", metrics.ErrorCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapter_Publish_EmptyPayload(t *testing.T) {
|
||||
a := NewAdapter(
|
||||
func(topic string) error { return nil },
|
||||
func(topic string, payload []byte) error { return nil },
|
||||
)
|
||||
|
||||
err := a.Publish(context.Background(), "test-topic", []byte{})
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for empty payload")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "payload cannot be empty") {
|
||||
t.Fatalf("expected empty payload error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapter_Publish_PayloadTooLarge(t *testing.T) {
|
||||
config := DefaultAdapterConfig()
|
||||
config.MaxPayloadSize = 10 // Very small limit for testing
|
||||
|
||||
a := NewAdapterWithConfig(
|
||||
func(topic string) error { return nil },
|
||||
func(topic string, payload []byte) error { return nil },
|
||||
config,
|
||||
)
|
||||
|
||||
largePayload := make([]byte, 20) // Larger than limit
|
||||
err := a.Publish(context.Background(), "test-topic", largePayload)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for payload too large")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "exceeds maximum") {
|
||||
t.Fatalf("expected payload size error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapter_Publish_TopicCaching(t *testing.T) {
|
||||
joinCallCount := 0
|
||||
a := NewAdapter(
|
||||
func(topic string) error { joinCallCount++; return nil },
|
||||
func(topic string, payload []byte) error { return nil },
|
||||
)
|
||||
|
||||
topic := "bzzz/meta/issue/123"
|
||||
|
||||
// First publish should join
|
||||
err := a.Publish(context.Background(), topic, []byte(`{"msg1":true}`))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if joinCallCount != 1 {
|
||||
t.Fatalf("expected 1 join call, got %d", joinCallCount)
|
||||
}
|
||||
|
||||
// Second publish to same topic should not join again
|
||||
err = a.Publish(context.Background(), topic, []byte(`{"msg2":true}`))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if joinCallCount != 1 {
|
||||
t.Fatalf("expected 1 join call total, got %d", joinCallCount)
|
||||
}
|
||||
|
||||
// Verify metrics
|
||||
metrics := a.GetMetrics()
|
||||
if metrics.JoinCount != 1 {
|
||||
t.Fatalf("expected join count 1, got %d", metrics.JoinCount)
|
||||
}
|
||||
if metrics.PublishCount != 2 {
|
||||
t.Fatalf("expected publish count 2, got %d", metrics.PublishCount)
|
||||
}
|
||||
|
||||
// Verify topic is cached
|
||||
joinedTopics := a.GetJoinedTopics()
|
||||
if len(joinedTopics) != 1 || joinedTopics[0] != topic {
|
||||
t.Fatalf("expected topic to be cached: %v", joinedTopics)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapter_Publish_Timeout(t *testing.T) {
|
||||
config := DefaultAdapterConfig()
|
||||
config.PublishTimeout = 10 * time.Millisecond // Very short timeout
|
||||
|
||||
a := NewAdapterWithConfig(
|
||||
func(topic string) error { return nil },
|
||||
func(topic string, payload []byte) error {
|
||||
time.Sleep(50 * time.Millisecond) // Longer than timeout
|
||||
return nil
|
||||
},
|
||||
config,
|
||||
)
|
||||
|
||||
err := a.Publish(context.Background(), "test-topic", []byte(`{"test":true}`))
|
||||
if err == nil {
|
||||
t.Fatalf("expected timeout error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "timed out") {
|
||||
t.Fatalf("expected timeout error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapter_Publish_JoinTimeout(t *testing.T) {
|
||||
config := DefaultAdapterConfig()
|
||||
config.JoinTimeout = 10 * time.Millisecond // Very short timeout
|
||||
|
||||
a := NewAdapterWithConfig(
|
||||
func(topic string) error {
|
||||
time.Sleep(50 * time.Millisecond) // Longer than timeout
|
||||
return nil
|
||||
},
|
||||
func(topic string, payload []byte) error { return nil },
|
||||
config,
|
||||
)
|
||||
|
||||
err := a.Publish(context.Background(), "test-topic", []byte(`{"test":true}`))
|
||||
if err == nil {
|
||||
t.Fatalf("expected join timeout error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "failed to join topic") {
|
||||
t.Fatalf("expected join timeout error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapter_ConcurrentPublish(t *testing.T) {
|
||||
joinCalls := make(map[string]int)
|
||||
var joinMutex sync.Mutex
|
||||
|
||||
a := NewAdapter(
|
||||
func(topic string) error {
|
||||
joinMutex.Lock()
|
||||
joinCalls[topic]++
|
||||
joinMutex.Unlock()
|
||||
return nil
|
||||
},
|
||||
func(topic string, payload []byte) error { return nil },
|
||||
)
|
||||
|
||||
const numGoroutines = 10
|
||||
const numTopics = 3
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
topic := fmt.Sprintf("bzzz/meta/issue/%d", id%numTopics)
|
||||
payload := fmt.Sprintf(`{"id":%d}`, id)
|
||||
|
||||
err := a.Publish(context.Background(), topic, []byte(payload))
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error from goroutine %d: %v", id, err)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Verify each topic was joined exactly once
|
||||
joinMutex.Lock()
|
||||
for topic, count := range joinCalls {
|
||||
if count != 1 {
|
||||
t.Errorf("topic %s was joined %d times, expected 1", topic, count)
|
||||
}
|
||||
}
|
||||
joinMutex.Unlock()
|
||||
|
||||
// Verify metrics
|
||||
metrics := a.GetMetrics()
|
||||
if metrics.JoinCount != numTopics {
|
||||
t.Fatalf("expected join count %d, got %d", numTopics, metrics.JoinCount)
|
||||
}
|
||||
if metrics.PublishCount != numGoroutines {
|
||||
t.Fatalf("expected publish count %d, got %d", numGoroutines, metrics.PublishCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapter_ResetMetrics(t *testing.T) {
|
||||
a := NewAdapter(
|
||||
func(topic string) error { return nil },
|
||||
func(topic string, payload []byte) error { return nil },
|
||||
)
|
||||
|
||||
// Generate some metrics
|
||||
a.Publish(context.Background(), "topic1", []byte(`{"test":true}`))
|
||||
a.Publish(context.Background(), "topic2", []byte(`{"test":true}`))
|
||||
|
||||
metrics := a.GetMetrics()
|
||||
if metrics.PublishCount == 0 {
|
||||
t.Fatalf("expected non-zero publish count")
|
||||
}
|
||||
|
||||
// Reset metrics
|
||||
a.ResetMetrics()
|
||||
|
||||
metrics = a.GetMetrics()
|
||||
if metrics.PublishCount != 0 {
|
||||
t.Fatalf("expected publish count to be reset to 0, got %d", metrics.PublishCount)
|
||||
}
|
||||
if metrics.JoinCount != 0 {
|
||||
t.Fatalf("expected join count to be reset to 0, got %d", metrics.JoinCount)
|
||||
}
|
||||
if metrics.ErrorCount != 0 {
|
||||
t.Fatalf("expected error count to be reset to 0, got %d", metrics.ErrorCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapter_ClearTopicCache(t *testing.T) {
|
||||
a := NewAdapter(
|
||||
func(topic string) error { return nil },
|
||||
func(topic string, payload []byte) error { return nil },
|
||||
)
|
||||
|
||||
// Publish to create cached topics
|
||||
a.Publish(context.Background(), "topic1", []byte(`{"test":true}`))
|
||||
a.Publish(context.Background(), "topic2", []byte(`{"test":true}`))
|
||||
|
||||
joinedTopics := a.GetJoinedTopics()
|
||||
if len(joinedTopics) != 2 {
|
||||
t.Fatalf("expected 2 joined topics, got %d", len(joinedTopics))
|
||||
}
|
||||
|
||||
// Clear cache
|
||||
a.ClearTopicCache()
|
||||
|
||||
joinedTopics = a.GetJoinedTopics()
|
||||
if len(joinedTopics) != 0 {
|
||||
t.Fatalf("expected 0 joined topics after cache clear, got %d", len(joinedTopics))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapter_DefaultConfig(t *testing.T) {
|
||||
config := DefaultAdapterConfig()
|
||||
|
||||
if config.MaxPayloadSize <= 0 {
|
||||
t.Fatalf("expected positive max payload size, got %d", config.MaxPayloadSize)
|
||||
}
|
||||
if config.JoinTimeout <= 0 {
|
||||
t.Fatalf("expected positive join timeout, got %v", config.JoinTimeout)
|
||||
}
|
||||
if config.PublishTimeout <= 0 {
|
||||
t.Fatalf("expected positive publish timeout, got %v", config.PublishTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapter_CustomConfig(t *testing.T) {
|
||||
config := AdapterConfig{
|
||||
MaxPayloadSize: 1000,
|
||||
JoinTimeout: 5 * time.Second,
|
||||
PublishTimeout: 2 * time.Second,
|
||||
}
|
||||
|
||||
a := NewAdapterWithConfig(
|
||||
func(topic string) error { return nil },
|
||||
func(topic string, payload []byte) error { return nil },
|
||||
config,
|
||||
)
|
||||
|
||||
if a.maxPayloadSize != 1000 {
|
||||
t.Fatalf("expected max payload size 1000, got %d", a.maxPayloadSize)
|
||||
}
|
||||
if a.joinTimeout != 5*time.Second {
|
||||
t.Fatalf("expected join timeout 5s, got %v", a.joinTimeout)
|
||||
}
|
||||
if a.publishTimeout != 2*time.Second {
|
||||
t.Fatalf("expected publish timeout 2s, got %v", a.publishTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
3
pkg/hmmm_adapter/go.mod
Normal file
3
pkg/hmmm_adapter/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module temp_test
|
||||
|
||||
go 1.24.5
|
||||
367
pkg/hmmm_adapter/integration_test.go
Normal file
367
pkg/hmmm_adapter/integration_test.go
Normal file
@@ -0,0 +1,367 @@
|
||||
package hmmm_adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"chorus.services/bzzz/p2p"
|
||||
"chorus.services/bzzz/pubsub"
|
||||
"chorus.services/hmmm/pkg/hmmm"
|
||||
)
|
||||
|
||||
// TestAdapterPubSubIntegration tests the complete integration between the adapter and BZZZ pubsub
|
||||
func TestAdapterPubSubIntegration(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Create P2P node
|
||||
node, err := p2p.NewNode(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create P2P node: %v", err)
|
||||
}
|
||||
defer node.Close()
|
||||
|
||||
// Create PubSub system
|
||||
ps, err := pubsub.NewPubSub(ctx, node.Host(), "bzzz/test/coordination", "hmmm/test/meta-discussion")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create PubSub: %v", err)
|
||||
}
|
||||
defer ps.Close()
|
||||
|
||||
// Create adapter using actual BZZZ pubsub methods
|
||||
adapter := NewAdapter(
|
||||
ps.JoinDynamicTopic,
|
||||
ps.PublishRaw,
|
||||
)
|
||||
|
||||
// Test publishing to a per-issue topic
|
||||
topic := "bzzz/meta/issue/integration-test-42"
|
||||
testPayload := []byte(`{"version": 1, "type": "meta_msg", "issue_id": 42, "message": "Integration test message"}`)
|
||||
|
||||
err = adapter.Publish(ctx, topic, testPayload)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to publish message: %v", err)
|
||||
}
|
||||
|
||||
// Verify metrics
|
||||
metrics := adapter.GetMetrics()
|
||||
if metrics.PublishCount != 1 {
|
||||
t.Errorf("Expected publish count 1, got %d", metrics.PublishCount)
|
||||
}
|
||||
if metrics.JoinCount != 1 {
|
||||
t.Errorf("Expected join count 1, got %d", metrics.JoinCount)
|
||||
}
|
||||
if metrics.ErrorCount != 0 {
|
||||
t.Errorf("Expected error count 0, got %d", metrics.ErrorCount)
|
||||
}
|
||||
|
||||
// Verify topic is cached
|
||||
joinedTopics := adapter.GetJoinedTopics()
|
||||
if len(joinedTopics) != 1 || joinedTopics[0] != topic {
|
||||
t.Errorf("Expected topic to be cached: got %v", joinedTopics)
|
||||
}
|
||||
|
||||
// Test repeated publishing to same topic (should use cache)
|
||||
err = adapter.Publish(ctx, topic, []byte(`{"version": 1, "type": "meta_msg", "issue_id": 42, "message": "Second message"}`))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to publish second message: %v", err)
|
||||
}
|
||||
|
||||
// Verify join count didn't increase (cached)
|
||||
metrics = adapter.GetMetrics()
|
||||
if metrics.JoinCount != 1 {
|
||||
t.Errorf("Expected join count to remain 1 (cached), got %d", metrics.JoinCount)
|
||||
}
|
||||
if metrics.PublishCount != 2 {
|
||||
t.Errorf("Expected publish count 2, got %d", metrics.PublishCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHMMMRouterIntegration tests the adapter working with the HMMM Router
|
||||
func TestHMMMRouterIntegration(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Create P2P node
|
||||
node, err := p2p.NewNode(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create P2P node: %v", err)
|
||||
}
|
||||
defer node.Close()
|
||||
|
||||
// Create PubSub system
|
||||
ps, err := pubsub.NewPubSub(ctx, node.Host(), "bzzz/test/coordination", "hmmm/test/meta-discussion")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create PubSub: %v", err)
|
||||
}
|
||||
defer ps.Close()
|
||||
|
||||
// Create adapter
|
||||
adapter := NewAdapter(
|
||||
ps.JoinDynamicTopic,
|
||||
ps.PublishRaw,
|
||||
)
|
||||
|
||||
// Create HMMM Router using our adapter
|
||||
hmmmRouter := hmmm.NewRouter(adapter, hmmm.DefaultConfig())
|
||||
|
||||
// Create a valid HMMM message
|
||||
msg := hmmm.Message{
|
||||
Version: 1,
|
||||
Type: "meta_msg",
|
||||
IssueID: 42,
|
||||
ThreadID: "test-thread-1",
|
||||
MsgID: "test-msg-1",
|
||||
NodeID: node.ID().String(),
|
||||
Author: "test-author",
|
||||
HopCount: 0,
|
||||
Timestamp: time.Now(),
|
||||
Message: "Test message from HMMM Router integration test",
|
||||
}
|
||||
|
||||
// Publish through HMMM Router
|
||||
err = hmmmRouter.Publish(ctx, msg)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to publish via HMMM Router: %v", err)
|
||||
}
|
||||
|
||||
// Verify adapter metrics were updated
|
||||
metrics := adapter.GetMetrics()
|
||||
if metrics.PublishCount != 1 {
|
||||
t.Errorf("Expected publish count 1, got %d", metrics.PublishCount)
|
||||
}
|
||||
if metrics.JoinCount != 1 {
|
||||
t.Errorf("Expected join count 1, got %d", metrics.JoinCount)
|
||||
}
|
||||
|
||||
// Verify the expected topic was joined
|
||||
expectedTopic := hmmm.TopicForIssue(42)
|
||||
joinedTopics := adapter.GetJoinedTopics()
|
||||
if len(joinedTopics) != 1 || joinedTopics[0] != expectedTopic {
|
||||
t.Errorf("Expected topic %s to be joined, got %v", expectedTopic, joinedTopics)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPerIssueTopicPublishing tests publishing to multiple per-issue topics
|
||||
func TestPerIssueTopicPublishing(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Create P2P node
|
||||
node, err := p2p.NewNode(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create P2P node: %v", err)
|
||||
}
|
||||
defer node.Close()
|
||||
|
||||
// Create PubSub system
|
||||
ps, err := pubsub.NewPubSub(ctx, node.Host(), "bzzz/test/coordination", "hmmm/test/meta-discussion")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create PubSub: %v", err)
|
||||
}
|
||||
defer ps.Close()
|
||||
|
||||
// Create adapter
|
||||
adapter := NewAdapter(
|
||||
ps.JoinDynamicTopic,
|
||||
ps.PublishRaw,
|
||||
)
|
||||
|
||||
// Test publishing to multiple per-issue topics
|
||||
issueIDs := []int64{100, 101, 102, 103, 104}
|
||||
|
||||
for _, issueID := range issueIDs {
|
||||
topic := hmmm.TopicForIssue(issueID)
|
||||
testMessage := map[string]interface{}{
|
||||
"version": 1,
|
||||
"type": "meta_msg",
|
||||
"issue_id": issueID,
|
||||
"thread_id": "test-thread",
|
||||
"msg_id": "test-msg-" + string(rune(issueID)),
|
||||
"node_id": node.ID().String(),
|
||||
"hop_count": 0,
|
||||
"timestamp": time.Now().UTC(),
|
||||
"message": "Test message for issue " + string(rune(issueID)),
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(testMessage)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal test message: %v", err)
|
||||
}
|
||||
|
||||
err = adapter.Publish(ctx, topic, payload)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to publish to topic %s: %v", topic, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify all topics were joined
|
||||
metrics := adapter.GetMetrics()
|
||||
if metrics.JoinCount != int64(len(issueIDs)) {
|
||||
t.Errorf("Expected join count %d, got %d", len(issueIDs), metrics.JoinCount)
|
||||
}
|
||||
if metrics.PublishCount != int64(len(issueIDs)) {
|
||||
t.Errorf("Expected publish count %d, got %d", len(issueIDs), metrics.PublishCount)
|
||||
}
|
||||
|
||||
joinedTopics := adapter.GetJoinedTopics()
|
||||
if len(joinedTopics) != len(issueIDs) {
|
||||
t.Errorf("Expected %d joined topics, got %d", len(issueIDs), len(joinedTopics))
|
||||
}
|
||||
|
||||
// Verify all expected topics are present
|
||||
expectedTopics := make(map[string]bool)
|
||||
for _, issueID := range issueIDs {
|
||||
expectedTopics[hmmm.TopicForIssue(issueID)] = true
|
||||
}
|
||||
|
||||
for _, topic := range joinedTopics {
|
||||
if !expectedTopics[topic] {
|
||||
t.Errorf("Unexpected topic joined: %s", topic)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestConcurrentPerIssuePublishing tests concurrent publishing to multiple per-issue topics
|
||||
func TestConcurrentPerIssuePublishing(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Create P2P node
|
||||
node, err := p2p.NewNode(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create P2P node: %v", err)
|
||||
}
|
||||
defer node.Close()
|
||||
|
||||
// Create PubSub system
|
||||
ps, err := pubsub.NewPubSub(ctx, node.Host(), "bzzz/test/coordination", "hmmm/test/meta-discussion")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create PubSub: %v", err)
|
||||
}
|
||||
defer ps.Close()
|
||||
|
||||
// Create adapter
|
||||
adapter := NewAdapter(
|
||||
ps.JoinDynamicTopic,
|
||||
ps.PublishRaw,
|
||||
)
|
||||
|
||||
// Test concurrent publishing
|
||||
const numGoroutines = 20
|
||||
const numIssues = 5
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
|
||||
issueID := int64(200 + (id % numIssues)) // Distribute across 5 issues
|
||||
topic := hmmm.TopicForIssue(issueID)
|
||||
|
||||
testMessage := map[string]interface{}{
|
||||
"version": 1,
|
||||
"type": "meta_msg",
|
||||
"issue_id": issueID,
|
||||
"thread_id": "concurrent-test",
|
||||
"msg_id": string(rune(id)),
|
||||
"node_id": node.ID().String(),
|
||||
"hop_count": 0,
|
||||
"timestamp": time.Now().UTC(),
|
||||
"message": "Concurrent test message",
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(testMessage)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to marshal message in goroutine %d: %v", id, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = adapter.Publish(ctx, topic, payload)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to publish in goroutine %d: %v", id, err)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Verify results
|
||||
metrics := adapter.GetMetrics()
|
||||
if metrics.PublishCount != numGoroutines {
|
||||
t.Errorf("Expected publish count %d, got %d", numGoroutines, metrics.PublishCount)
|
||||
}
|
||||
if metrics.JoinCount != numIssues {
|
||||
t.Errorf("Expected join count %d, got %d", numIssues, metrics.JoinCount)
|
||||
}
|
||||
if metrics.ErrorCount != 0 {
|
||||
t.Errorf("Expected error count 0, got %d", metrics.ErrorCount)
|
||||
}
|
||||
|
||||
joinedTopics := adapter.GetJoinedTopics()
|
||||
if len(joinedTopics) != numIssues {
|
||||
t.Errorf("Expected %d unique topics joined, got %d", numIssues, len(joinedTopics))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdapterValidation tests input validation in integration scenario
|
||||
func TestAdapterValidation(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create P2P node
|
||||
node, err := p2p.NewNode(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create P2P node: %v", err)
|
||||
}
|
||||
defer node.Close()
|
||||
|
||||
// Create PubSub system
|
||||
ps, err := pubsub.NewPubSub(ctx, node.Host(), "bzzz/test/coordination", "hmmm/test/meta-discussion")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create PubSub: %v", err)
|
||||
}
|
||||
defer ps.Close()
|
||||
|
||||
// Create adapter with small payload limit for testing
|
||||
config := DefaultAdapterConfig()
|
||||
config.MaxPayloadSize = 100 // Small limit
|
||||
|
||||
adapter := NewAdapterWithConfig(
|
||||
ps.JoinDynamicTopic,
|
||||
ps.PublishRaw,
|
||||
config,
|
||||
)
|
||||
|
||||
// Test empty topic
|
||||
err = adapter.Publish(ctx, "", []byte(`{"test": true}`))
|
||||
if err == nil {
|
||||
t.Error("Expected error for empty topic")
|
||||
}
|
||||
|
||||
// Test empty payload
|
||||
err = adapter.Publish(ctx, "test-topic", []byte{})
|
||||
if err == nil {
|
||||
t.Error("Expected error for empty payload")
|
||||
}
|
||||
|
||||
// Test payload too large
|
||||
largePayload := make([]byte, 200) // Larger than limit
|
||||
err = adapter.Publish(ctx, "test-topic", largePayload)
|
||||
if err == nil {
|
||||
t.Error("Expected error for payload too large")
|
||||
}
|
||||
|
||||
// Verify all errors were tracked
|
||||
metrics := adapter.GetMetrics()
|
||||
if metrics.ErrorCount != 3 {
|
||||
t.Errorf("Expected error count 3, got %d", metrics.ErrorCount)
|
||||
}
|
||||
if metrics.PublishCount != 0 {
|
||||
t.Errorf("Expected publish count 0, got %d", metrics.PublishCount)
|
||||
}
|
||||
}
|
||||
301
pkg/hmmm_adapter/smoke_test.go
Normal file
301
pkg/hmmm_adapter/smoke_test.go
Normal file
@@ -0,0 +1,301 @@
|
||||
package hmmm_adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestPerIssueTopicSmokeTest tests the per-issue topic functionality without full BZZZ integration
|
||||
func TestPerIssueTopicSmokeTest(t *testing.T) {
|
||||
// Mock pubsub functions that track calls
|
||||
joinedTopics := make(map[string]int)
|
||||
publishedMessages := make(map[string][]byte)
|
||||
var mu sync.Mutex
|
||||
|
||||
joiner := func(topic string) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
joinedTopics[topic]++
|
||||
return nil
|
||||
}
|
||||
|
||||
publisher := func(topic string, payload []byte) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
publishedMessages[topic] = payload
|
||||
return nil
|
||||
}
|
||||
|
||||
adapter := NewAdapter(joiner, publisher)
|
||||
|
||||
// Test per-issue topic publishing
|
||||
issueID := int64(42)
|
||||
topic := fmt.Sprintf("bzzz/meta/issue/%d", issueID)
|
||||
|
||||
testMessage := map[string]interface{}{
|
||||
"version": 1,
|
||||
"type": "meta_msg",
|
||||
"issue_id": issueID,
|
||||
"thread_id": "test-thread-42",
|
||||
"msg_id": "smoke-test-msg-1",
|
||||
"node_id": "test-node-id",
|
||||
"hop_count": 0,
|
||||
"timestamp": time.Now().UTC(),
|
||||
"message": "Smoke test: HMMM per-issue room initialized.",
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(testMessage)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal test message: %v", err)
|
||||
}
|
||||
|
||||
// Publish the message
|
||||
err = adapter.Publish(context.Background(), topic, payload)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to publish message: %v", err)
|
||||
}
|
||||
|
||||
// Verify join was called once
|
||||
mu.Lock()
|
||||
if joinedTopics[topic] != 1 {
|
||||
t.Errorf("Expected topic %s to be joined once, got %d times", topic, joinedTopics[topic])
|
||||
}
|
||||
|
||||
// Verify message was published
|
||||
if _, exists := publishedMessages[topic]; !exists {
|
||||
t.Errorf("Expected message to be published to topic %s", topic)
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
// Verify metrics
|
||||
metrics := adapter.GetMetrics()
|
||||
if metrics.PublishCount != 1 {
|
||||
t.Errorf("Expected publish count 1, got %d", metrics.PublishCount)
|
||||
}
|
||||
if metrics.JoinCount != 1 {
|
||||
t.Errorf("Expected join count 1, got %d", metrics.JoinCount)
|
||||
}
|
||||
if metrics.ErrorCount != 0 {
|
||||
t.Errorf("Expected error count 0, got %d", metrics.ErrorCount)
|
||||
}
|
||||
|
||||
// Test publishing another message to the same topic (should not join again)
|
||||
testMessage2 := map[string]interface{}{
|
||||
"version": 1,
|
||||
"type": "meta_msg",
|
||||
"issue_id": issueID,
|
||||
"thread_id": "test-thread-42",
|
||||
"msg_id": "smoke-test-msg-2",
|
||||
"node_id": "test-node-id",
|
||||
"hop_count": 0,
|
||||
"timestamp": time.Now().UTC(),
|
||||
"message": "Second message in same issue room.",
|
||||
}
|
||||
|
||||
payload2, err := json.Marshal(testMessage2)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal second test message: %v", err)
|
||||
}
|
||||
|
||||
err = adapter.Publish(context.Background(), topic, payload2)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to publish second message: %v", err)
|
||||
}
|
||||
|
||||
// Verify join was still called only once (topic cached)
|
||||
mu.Lock()
|
||||
if joinedTopics[topic] != 1 {
|
||||
t.Errorf("Expected topic %s to still be joined only once (cached), got %d times", topic, joinedTopics[topic])
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
// Verify updated metrics
|
||||
metrics = adapter.GetMetrics()
|
||||
if metrics.PublishCount != 2 {
|
||||
t.Errorf("Expected publish count 2, got %d", metrics.PublishCount)
|
||||
}
|
||||
if metrics.JoinCount != 1 {
|
||||
t.Errorf("Expected join count to remain 1 (cached), got %d", metrics.JoinCount)
|
||||
}
|
||||
|
||||
t.Logf("✅ Per-issue topic smoke test passed: topic=%s, publishes=%d, joins=%d",
|
||||
topic, metrics.PublishCount, metrics.JoinCount)
|
||||
}
|
||||
|
||||
// TestMultiplePerIssueTopics tests publishing to multiple different per-issue topics
|
||||
func TestMultiplePerIssueTopics(t *testing.T) {
|
||||
joinedTopics := make(map[string]int)
|
||||
publishedMessages := make(map[string][]byte)
|
||||
var mu sync.Mutex
|
||||
|
||||
joiner := func(topic string) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
joinedTopics[topic]++
|
||||
return nil
|
||||
}
|
||||
|
||||
publisher := func(topic string, payload []byte) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
publishedMessages[topic] = payload
|
||||
return nil
|
||||
}
|
||||
|
||||
adapter := NewAdapter(joiner, publisher)
|
||||
|
||||
// Test multiple issues
|
||||
issueIDs := []int64{100, 200, 300}
|
||||
|
||||
for _, issueID := range issueIDs {
|
||||
topic := fmt.Sprintf("bzzz/meta/issue/%d", issueID)
|
||||
|
||||
testMessage := map[string]interface{}{
|
||||
"version": 1,
|
||||
"type": "meta_msg",
|
||||
"issue_id": issueID,
|
||||
"thread_id": fmt.Sprintf("issue-%d", issueID),
|
||||
"msg_id": fmt.Sprintf("msg-%d-1", issueID),
|
||||
"node_id": "test-node-id",
|
||||
"hop_count": 0,
|
||||
"timestamp": time.Now().UTC(),
|
||||
"message": fmt.Sprintf("Message for issue %d", issueID),
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(testMessage)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal message for issue %d: %v", issueID, err)
|
||||
}
|
||||
|
||||
err = adapter.Publish(context.Background(), topic, payload)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to publish message for issue %d: %v", issueID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify all topics were joined once
|
||||
mu.Lock()
|
||||
for _, issueID := range issueIDs {
|
||||
topic := fmt.Sprintf("bzzz/meta/issue/%d", issueID)
|
||||
if joinedTopics[topic] != 1 {
|
||||
t.Errorf("Expected topic %s to be joined once, got %d times", topic, joinedTopics[topic])
|
||||
}
|
||||
if _, exists := publishedMessages[topic]; !exists {
|
||||
t.Errorf("Expected message to be published to topic %s", topic)
|
||||
}
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
// Verify metrics
|
||||
metrics := adapter.GetMetrics()
|
||||
expectedJoinCount := int64(len(issueIDs))
|
||||
expectedPublishCount := int64(len(issueIDs))
|
||||
|
||||
if metrics.PublishCount != expectedPublishCount {
|
||||
t.Errorf("Expected publish count %d, got %d", expectedPublishCount, metrics.PublishCount)
|
||||
}
|
||||
if metrics.JoinCount != expectedJoinCount {
|
||||
t.Errorf("Expected join count %d, got %d", expectedJoinCount, metrics.JoinCount)
|
||||
}
|
||||
if metrics.ErrorCount != 0 {
|
||||
t.Errorf("Expected error count 0, got %d", metrics.ErrorCount)
|
||||
}
|
||||
|
||||
// Verify all topics are cached
|
||||
cachedTopics := adapter.GetJoinedTopics()
|
||||
if len(cachedTopics) != len(issueIDs) {
|
||||
t.Errorf("Expected %d cached topics, got %d", len(issueIDs), len(cachedTopics))
|
||||
}
|
||||
|
||||
t.Logf("✅ Multiple per-issue topics test passed: issues=%v, publishes=%d, joins=%d",
|
||||
issueIDs, metrics.PublishCount, metrics.JoinCount)
|
||||
}
|
||||
|
||||
// TestHMMMMessageFormat tests that the adapter can handle HMMM-formatted messages
|
||||
func TestHMMMMessageFormat(t *testing.T) {
|
||||
joinedTopics := make(map[string]bool)
|
||||
var publishedPayload []byte
|
||||
var mu sync.Mutex
|
||||
|
||||
joiner := func(topic string) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
joinedTopics[topic] = true
|
||||
return nil
|
||||
}
|
||||
|
||||
publisher := func(topic string, payload []byte) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
publishedPayload = make([]byte, len(payload))
|
||||
copy(publishedPayload, payload)
|
||||
return nil
|
||||
}
|
||||
|
||||
adapter := NewAdapter(joiner, publisher)
|
||||
|
||||
// Create HMMM-compliant message (following HMMM message schema)
|
||||
hmmmMessage := map[string]interface{}{
|
||||
"version": 1,
|
||||
"type": "meta_msg",
|
||||
"issue_id": 42,
|
||||
"thread_id": "issue-42",
|
||||
"msg_id": "seed-" + fmt.Sprintf("%d", time.Now().UnixNano()),
|
||||
"parent_id": nil,
|
||||
"node_id": "test-node-12D3KooW",
|
||||
"author": "test-author",
|
||||
"hop_count": 0,
|
||||
"timestamp": time.Now().UTC(),
|
||||
"message": "Seed: HMMM per-issue room initialized.",
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(hmmmMessage)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal HMMM message: %v", err)
|
||||
}
|
||||
|
||||
topic := "bzzz/meta/issue/42"
|
||||
err = adapter.Publish(context.Background(), topic, payload)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to publish HMMM message: %v", err)
|
||||
}
|
||||
|
||||
// Verify the message was published correctly
|
||||
mu.Lock()
|
||||
if !joinedTopics[topic] {
|
||||
t.Errorf("Expected topic %s to be joined", topic)
|
||||
}
|
||||
|
||||
if len(publishedPayload) == 0 {
|
||||
t.Fatalf("Expected payload to be published")
|
||||
}
|
||||
|
||||
// Unmarshal and verify the published payload matches the original
|
||||
var publishedMessage map[string]interface{}
|
||||
err = json.Unmarshal(publishedPayload, &publishedMessage)
|
||||
mu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to unmarshal published payload: %v", err)
|
||||
}
|
||||
|
||||
// Verify key fields
|
||||
if publishedMessage["version"].(float64) != 1 {
|
||||
t.Errorf("Expected version 1, got %v", publishedMessage["version"])
|
||||
}
|
||||
if publishedMessage["type"].(string) != "meta_msg" {
|
||||
t.Errorf("Expected type 'meta_msg', got %v", publishedMessage["type"])
|
||||
}
|
||||
if publishedMessage["issue_id"].(float64) != 42 {
|
||||
t.Errorf("Expected issue_id 42, got %v", publishedMessage["issue_id"])
|
||||
}
|
||||
if publishedMessage["message"].(string) != "Seed: HMMM per-issue room initialized." {
|
||||
t.Errorf("Expected specific message, got %v", publishedMessage["message"])
|
||||
}
|
||||
|
||||
t.Logf("✅ HMMM message format test passed: successfully published and parsed HMMM-compliant message")
|
||||
}
|
||||
Reference in New Issue
Block a user