feat: M8.2 Gateway Queue Adapter for SQS via api.riotpiao.com

- Unified QueueAdapter trait for concurrent dual-write operations
- GatewayQueueAdapter routes messages via api.riotpiao.com with X-Service: sqs header
- TokenProvider abstraction: StaticTokenProvider + AuthentikTokenProvider
- JWT bearer token support (from Authentik OAuth2)
- InMemoryQueueAdapter for testing
- Base64 encoding/decoding for SQS message bodies
- HTTP/REST integration (no direct gRPC complexity)
- 8 unit tests + comprehensive documentation
- Supports long-polling (ReceiveMessage), visibility timeout, DLQ

Uses standard SQS API patterns:
- SendMessage: Queue chunk for dual-write processing
- ReceiveMessage: Long-poll up to 10 messages, 20s wait
- DeleteMessage: Acknowledge on success
- ChangeMessageVisibility: Retry on failure
- SendToDLQ: After max retries

Files:
- crates/mem-cli/src/queue_adapter.rs (310 LOC)
- crates/mem-cli/src/gateway_queue_adapter.rs (530 LOC)
- tests/it_gateway_queue_adapter.rs (110 LOC)
- docs/M8.2-GATEWAY_QUEUE_ADAPTER.md (400 LOC)
This commit is contained in:
2026-08-28 13:11:56 -07:00
parent d99cf23e6c
commit cd3d00048a
12 changed files with 2450 additions and 1 deletions
+123
View File
@@ -0,0 +1,123 @@
//! Integration tests for M8.2 Gateway Queue Adapter
//!
//! Tests the unified queue adapter that routes SQS messages through api.riotpiao.com gateway.
use mem_cli::queue_adapter::{QueueAdapter, QueueMessage};
use mem_cli::gateway_queue_adapter::{GatewayQueueAdapter, StaticTokenProvider};
use std::sync::Arc;
use uuid::Uuid;
#[tokio::test]
async fn test_static_queue_adapter_creation() {
let adapter = GatewayQueueAdapter::with_static_token(
"https://api.riotpiao.com".to_string(),
"test-token-xyz".to_string(),
);
// Verify adapter is created
assert!(adapter.health_check().await.is_err()); // Will fail (no real gateway), but tests flow
}
#[tokio::test]
async fn test_queue_message_serialization() {
let msg = QueueMessage {
message_id: "msg-123".to_string(),
chunk_id: Uuid::new_v4(),
body: "test body".to_string(),
receive_count: 1,
receipt_handle: "handle-xyz".to_string(),
project: "myproject".to_string(),
attributes: std::collections::HashMap::new(),
};
assert_eq!(msg.message_id, "msg-123");
assert_eq!(msg.project, "myproject");
assert_eq!(msg.receive_count, 1);
}
#[test]
fn test_gateway_queue_naming() {
let adapter = GatewayQueueAdapter::with_static_token(
"https://api.riotpiao.com".to_string(),
"test-token".to_string(),
);
assert_eq!(adapter.queue_name("myproject"), "poimen-chunks-myproject");
assert_eq!(adapter.queue_name("prod"), "poimen-chunks-prod");
assert_eq!(adapter.queue_name("test-env"), "poimen-chunks-test-env");
}
#[test]
fn test_queue_adapter_base64_encoding() {
let original = "hello world";
let encoded = base64::encode(original.as_bytes());
let decoded_bytes = base64::decode(encoded.as_bytes()).unwrap();
let decoded = String::from_utf8(decoded_bytes).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_json_chunk_message() {
let chunk_data = serde_json::json!({
"chunk_id": Uuid::new_v4(),
"content": "test content",
"source": "obsidian",
"level": "L0",
"breadcrumb": ["root", "section1"],
});
assert_eq!(chunk_data["level"], "L0");
assert_eq!(chunk_data["source"], "obsidian");
}
#[tokio::test]
async fn test_authentik_token_provider_construction() {
use mem_cli::gateway_queue_adapter::AuthentikTokenProvider;
let provider = AuthentikTokenProvider::new(
"https://authentik.riotpiao.com/application/o/poimen-memory/".to_string(),
"client-id".to_string(),
"client-secret".to_string(),
);
// Just test that it constructs (won't actually authenticate without real Authentik)
let _ = provider;
}
#[test]
fn test_queue_stats_structure() {
use mem_cli::queue_adapter::QueueStats;
let stats = QueueStats {
available_messages: 10,
in_flight_messages: 3,
dead_letter_messages: 1,
total_processed: 100,
average_delay_secs: 45,
};
assert_eq!(stats.available_messages, 10);
assert_eq!(stats.in_flight_messages, 3);
assert_eq!(stats.total_processed, 100);
assert!(stats.average_delay_secs > 0);
}
#[test]
fn test_message_attributes_serialization() {
let mut attrs = std::collections::HashMap::new();
attrs.insert("source".to_string(), "obsidian".to_string());
attrs.insert("level".to_string(), "L0".to_string());
let json = serde_json::to_string(&attrs).unwrap();
assert!(json.contains("source"));
assert!(json.contains("obsidian"));
}
#[test]
fn test_breadcrumb_path_encoding() {
let breadcrumb = vec!["root".to_string(), "folder1".to_string(), "section".to_string()];
let json = serde_json::to_string(&breadcrumb).unwrap();
assert_eq!(json, r#"["root","folder1","section"]"#);
}