Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f6c4283d2 | ||
|
|
033a5c3042 | ||
|
|
08d0b7371b | ||
|
|
28dabe8834 | ||
|
|
92f281c43a | ||
|
|
7474ab1dc4 | ||
|
|
7047749ef0 | ||
|
|
553d469174 | ||
|
|
080d2ad756 | ||
|
|
9611f935bc | ||
|
|
84b80179ec | ||
|
|
5300c0f998 |
@@ -17,7 +17,7 @@ jobs:
|
|||||||
- run: python3 --version
|
- run: python3 --version
|
||||||
- run: node --version
|
- run: node --version
|
||||||
- run: echo "Runner works!"
|
- run: echo "Runner works!"
|
||||||
- run: git clone --depth 1 http://abiba-bot:***REMOVED***@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git .
|
- run: git clone --depth 1 http://abiba-bot:mipjoq-tybbox-2ciHru@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git .
|
||||||
|
|
||||||
- name: Python syntax check
|
- name: Python syntax check
|
||||||
run: |
|
run: |
|
||||||
@@ -46,7 +46,7 @@ print('✅ config.yaml.example valid')
|
|||||||
needs: [validate]
|
needs: [validate]
|
||||||
steps:
|
steps:
|
||||||
- run: echo "🚀 Deploy tag $(echo $GITHUB_REF_NAME)"
|
- run: echo "🚀 Deploy tag $(echo $GITHUB_REF_NAME)"
|
||||||
- run: git clone --depth 1 http://abiba-bot:***REMOVED***@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git .
|
- run: git clone --depth 1 http://abiba-bot:mipjoq-tybbox-2ciHru@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git .
|
||||||
|
|
||||||
- name: Deploy to Tanko (canary)
|
- name: Deploy to Tanko (canary)
|
||||||
run: ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 jerome@192.168.68.122 "cd /root && bash -s" < scripts/deploy.sh --ct=tanko --mode=native "$GITHUB_REF_NAME" 2>&1 || echo "⚠️ Tanko deploy skipped"
|
run: ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 jerome@192.168.68.122 "cd /root && bash -s" < scripts/deploy.sh --ct=tanko --mode=native "$GITHUB_REF_NAME" 2>&1 || echo "⚠️ Tanko deploy skipped"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
run: git clone --depth 1 http://abiba-bot:***REMOVED***@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git .
|
run: git clone --depth 1 http://abiba-bot:mipjoq-tybbox-2ciHru@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git .
|
||||||
- name: Python syntax
|
- name: Python syntax
|
||||||
run: |
|
run: |
|
||||||
python3 -m py_compile plugins/platforms/zulip/adapter.py 2>/dev/null
|
python3 -m py_compile plugins/platforms/zulip/adapter.py 2>/dev/null
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
environment: canary
|
environment: canary
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
run: git clone --depth 1 http://abiba-bot:***REMOVED***@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git .
|
run: git clone --depth 1 http://abiba-bot:mipjoq-tybbox-2ciHru@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git .
|
||||||
- name: Deploy to Tanko
|
- name: Deploy to Tanko
|
||||||
env:
|
env:
|
||||||
TAG: ${{ github.ref_name }}
|
TAG: ${{ github.ref_name }}
|
||||||
@@ -66,7 +66,7 @@ jobs:
|
|||||||
environment: production
|
environment: production
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
run: git clone --depth 1 http://abiba-bot:***REMOVED***@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git .
|
run: git clone --depth 1 http://abiba-bot:mipjoq-tybbox-2ciHru@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git .
|
||||||
- name: Deploy to all Hermes agents
|
- name: Deploy to all Hermes agents
|
||||||
env:
|
env:
|
||||||
TAG: ${{ github.ref_name }}
|
TAG: ${{ github.ref_name }}
|
||||||
@@ -97,7 +97,7 @@ jobs:
|
|||||||
environment: agent-zero
|
environment: agent-zero
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
run: git clone --depth 1 http://abiba-bot:***REMOVED***@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git .
|
run: git clone --depth 1 http://abiba-bot:mipjoq-tybbox-2ciHru@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git .
|
||||||
- name: Deploy to kagentz
|
- name: Deploy to kagentz
|
||||||
env:
|
env:
|
||||||
TAG: ${{ github.ref_name }}
|
TAG: ${{ github.ref_name }}
|
||||||
|
|||||||
@@ -34,4 +34,3 @@ deploy.log
|
|||||||
|
|
||||||
# PM2 ecosystem (local configs)
|
# PM2 ecosystem (local configs)
|
||||||
ecosystem.*.config.cjs
|
ecosystem.*.config.cjs
|
||||||
.agents/
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
kind: responsibility
|
kind: contract
|
||||||
name: zulip-platform-verification
|
name: zulip-platform-verification
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
description: >
|
description: >
|
||||||
@@ -14,13 +14,13 @@ author: Abiba (pi agent)
|
|||||||
|
|
||||||
## Platforms Under Contract
|
## Platforms Under Contract
|
||||||
|
|
||||||
| Platform | Agent | Host | Bot Email | Health URL |
|
| Platform | Agent | Bot Email | Plugin Path | Health Port |
|
||||||
|----------|-------|------|-----------|------------|
|
|----------|-------|-----------|-------------|-------------|
|
||||||
| pi | Abiba | `192.168.68.24` | abiba-bot@chat.sysloggh.net | `http://192.168.68.24:9200/health` |
|
| pi | Abiba | abiba-bot@chat.sysloggh.net | `~/.pi/agent/extensions/zulip/` | :9200 |
|
||||||
| Hermes | Tanko | `192.168.68.123` | tanko-bot@chat.sysloggh.net | `http://192.168.68.123:9201/health` |
|
| Hermes | Tanko | tanko-bot@chat.sysloggh.net | `~/.hermes/plugins/platforms/zulip/` | :9201 |
|
||||||
| Hermes | Mumuni | `192.168.68.123` | mumuni-bot@chat.sysloggh.net | `http://192.168.68.123:9202/health` |
|
| Hermes | Mumuni | mumuni-bot@chat.sysloggh.net | `~/.hermes/plugins/platforms/zulip/` | :9202 |
|
||||||
| Hermes | Koonimo | `192.168.68.123` | koonimo-bot@chat.sysloggh.net | `http://192.168.68.123:9203/health` |
|
| Hermes | Koonimo | koonimo-bot@chat.sysloggh.net | `~/.hermes/plugins/platforms/zulip/` | :9203 |
|
||||||
| Hermes | Koby | `192.168.68.123` | koby-bot@chat.sysloggh.net | `http://192.168.68.123:9204/health` |
|
| Hermes | Koby | koby-bot@chat.sysloggh.net | `~/.hermes/plugins/platforms/zulip/` | :9204 |
|
||||||
|
|
||||||
## Parameters
|
## Parameters
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ author: Abiba (pi agent)
|
|||||||
## Checks (per agent)
|
## Checks (per agent)
|
||||||
|
|
||||||
### Check 1: Health Endpoint
|
### Check 1: Health Endpoint
|
||||||
- GET {{health_url}} → expect 200, `{ status: "ok", connected: true }`
|
- GET {{health_port}}/health → expect 200, `{ status: "ok", connected: true }`
|
||||||
|
|
||||||
### Check 2: Queue Registered
|
### Check 2: Queue Registered
|
||||||
- Health response must include `queue_id` (non-null string)
|
- Health response must include `queue_id` (non-null string)
|
||||||
@@ -89,7 +89,7 @@ author: Abiba (pi agent)
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Maintains
|
## Ensures
|
||||||
|
|
||||||
- Each agent is independently verified; one failure doesn't block others
|
- Each agent is independently verified; one failure doesn't block others
|
||||||
- @all-bots user_id is cross-validated against Zulip API (catches config drift)
|
- @all-bots user_id is cross-validated against Zulip API (catches config drift)
|
||||||
|
|||||||
@@ -1,25 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
Zulip platform adapter (Hermes plugin) — Gen 6.
|
Zulip platform adapter (Hermes plugin) — Gen 4.
|
||||||
|
|
||||||
Connects a Hermes agent to Zulip via event queue polling. DMs and @mentions
|
Connects a Hermes agent to Zulip via event queue polling. DMs and @mentions
|
||||||
are routed to the agent via the Hermes Gateway's handle_message() interface.
|
are routed to the agent via the Hermes Gateway's handle_message() interface.
|
||||||
Replies use a placeholder->edit streaming pattern for UX feedback.
|
Replies use a placeholder->edit streaming pattern for UX feedback.
|
||||||
|
|
||||||
Gen 6 Improvements (2026-07-07):
|
|
||||||
1. known_issues persistence — add_known_issue(), get_known_issues(),
|
|
||||||
clear_known_issues() methods. Issues tracked in health stats for
|
|
||||||
external logging to knowledge graph across generations.
|
|
||||||
2. simulate_dm_latency() — offline-verifiable DM processing pipeline
|
|
||||||
timing test. Measures event parse, placeholder pick, auth, dedup,
|
|
||||||
and truncation to verify dm_response_time_ms < 10000 without live Zulip.
|
|
||||||
3. Placeholder collision fix — switched from hash-based pick to
|
|
||||||
random.choice() to avoid identical placeholders on rapid-fire calls.
|
|
||||||
|
|
||||||
Gen 5 Improvements (2026-06-29):
|
|
||||||
1. Bot user ID resolution — fetches from /users/me when /register doesn't return it
|
|
||||||
2. Stream subscription auto-fix — ensures bot is subscribed to primary streams
|
|
||||||
on connect, critical for receiving @mentions in stream topics
|
|
||||||
|
|
||||||
Gen 4 Improvements (2026-06-27):
|
Gen 4 Improvements (2026-06-27):
|
||||||
1. HTTP client recreation on reconnect — new httpx.AsyncClient() after queue expiry
|
1. HTTP client recreation on reconnect — new httpx.AsyncClient() after queue expiry
|
||||||
or sustained failures to prevent CLOSE-WAIT socket leaks
|
or sustained failures to prevent CLOSE-WAIT socket leaks
|
||||||
@@ -53,7 +38,6 @@ import base64
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import random
|
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
@@ -242,9 +226,6 @@ class ZulipAdapter(BasePlatformAdapter):
|
|||||||
# --- Gen 3: Periodic @all-bots refresh ---
|
# --- Gen 3: Periodic @all-bots refresh ---
|
||||||
self._all_bots_refresh_task: Optional[asyncio.Task] = None
|
self._all_bots_refresh_task: Optional[asyncio.Task] = None
|
||||||
|
|
||||||
# --- Gen 6: known_issues persistence ---
|
|
||||||
self._known_issues: List[str] = []
|
|
||||||
|
|
||||||
# --- Gen 3: Health stats callback ---
|
# --- Gen 3: Health stats callback ---
|
||||||
self._health_callback = None
|
self._health_callback = None
|
||||||
self._health_callback_interval: int = 300 # 5 min
|
self._health_callback_interval: int = 300 # 5 min
|
||||||
@@ -321,9 +302,6 @@ class ZulipAdapter(BasePlatformAdapter):
|
|||||||
# Gen 2: Try to resolve @all-bots user ID dynamically
|
# Gen 2: Try to resolve @all-bots user ID dynamically
|
||||||
await self._resolve_all_bots_user_id()
|
await self._resolve_all_bots_user_id()
|
||||||
|
|
||||||
# Gen 5: Ensure bot is subscribed to primary stream
|
|
||||||
await self._ensure_stream_subscriptions()
|
|
||||||
|
|
||||||
self._mark_connected()
|
self._mark_connected()
|
||||||
logger.info(
|
logger.info(
|
||||||
"[%s] Connected to %s as %s (queue=%s, bot_id=%s, all_bots_id=%s)",
|
"[%s] Connected to %s as %s (queue=%s, bot_id=%s, all_bots_id=%s)",
|
||||||
@@ -471,10 +449,6 @@ class ZulipAdapter(BasePlatformAdapter):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Clear stale errors on successful poll cycle
|
|
||||||
self._health_stats["last_error_msg"] = None
|
|
||||||
self._health_stats["last_error_at"] = None
|
|
||||||
|
|
||||||
# Gen 4: Heartbeat logging — prove poll loop is alive
|
# Gen 4: Heartbeat logging — prove poll loop is alive
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if now - self._last_heartbeat_time > HEARTBEAT_INTERVAL:
|
if now - self._last_heartbeat_time > HEARTBEAT_INTERVAL:
|
||||||
@@ -703,64 +677,6 @@ class ZulipAdapter(BasePlatformAdapter):
|
|||||||
# Gen 2: Dynamic @all-bots resolution
|
# Gen 2: Dynamic @all-bots resolution
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
async def _ensure_stream_subscriptions(self) -> None:
|
|
||||||
"""Ensure the bot is subscribed to the primary streams.
|
|
||||||
|
|
||||||
Checks current subscriptions and subscribes to the configured primary
|
|
||||||
stream (and general-chat) if missing. Without subscription, the bot
|
|
||||||
receives DMs but no stream events — @mentions in streams are invisible.
|
|
||||||
|
|
||||||
This is a Gen 5 improvement based on lessons from the pi Zulip extension,
|
|
||||||
where the bot had zero stream subscriptions and couldn't receive
|
|
||||||
@mentions in stream topics.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Check current subscriptions
|
|
||||||
resp, _ = await self._api_call(
|
|
||||||
"GET", "/api/v1/users/me/subscriptions",
|
|
||||||
)
|
|
||||||
if not resp:
|
|
||||||
return
|
|
||||||
|
|
||||||
subscribed = resp.get("subscriptions", [])
|
|
||||||
subscribed_names = [s.get("name", "") for s in subscribed if isinstance(s, dict)]
|
|
||||||
|
|
||||||
# Streams the bot should be subscribed to
|
|
||||||
required_streams = [self._stream, "general chat"]
|
|
||||||
missing = [s for s in required_streams if s not in subscribed_names]
|
|
||||||
|
|
||||||
if not missing:
|
|
||||||
logger.info(
|
|
||||||
"[%s] Stream subscriptions OK: %s",
|
|
||||||
self.name, ", ".join(subscribed_names),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Subscribe to missing streams
|
|
||||||
import json as _json
|
|
||||||
payload = {"subscriptions": _json.dumps(
|
|
||||||
[{"name": s} for s in missing]
|
|
||||||
)}
|
|
||||||
sub_resp, _ = await self._api_call(
|
|
||||||
"POST", "/api/v1/users/me/subscriptions",
|
|
||||||
data=payload,
|
|
||||||
)
|
|
||||||
if sub_resp and sub_resp.get("result") == "success":
|
|
||||||
subscribed_str = ", ".join(missing)
|
|
||||||
logger.info(
|
|
||||||
"[%s] Subscribed to: %s", self.name, subscribed_str,
|
|
||||||
)
|
|
||||||
self._health_stats["subscriptions_fixed"] = (
|
|
||||||
self._health_stats.get("subscriptions_fixed", 0) + 1
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.warning(
|
|
||||||
"[%s] Failed to subscribe to %s",
|
|
||||||
self.name, ", ".join(missing),
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning("[%s] Subscription check failed: %s", self.name, e)
|
|
||||||
|
|
||||||
async def _resolve_all_bots_user_id(self) -> None:
|
async def _resolve_all_bots_user_id(self) -> None:
|
||||||
"""Try to resolve the @all-bots user ID from the Zulip server.
|
"""Try to resolve the @all-bots user ID from the Zulip server.
|
||||||
|
|
||||||
@@ -1180,35 +1096,6 @@ class ZulipAdapter(BasePlatformAdapter):
|
|||||||
"detail": f"@all-bots user_id: {self._all_bots_user_id}",
|
"detail": f"@all-bots user_id: {self._all_bots_user_id}",
|
||||||
}
|
}
|
||||||
|
|
||||||
# 9. Stream subscriptions (verifies bot is subscribed to at least one stream)
|
|
||||||
try:
|
|
||||||
from urllib.parse import urlencode
|
|
||||||
import json as _json
|
|
||||||
key_str = f"{self._email}:{self._api_key}"
|
|
||||||
auth_b64 = base64.b64encode(key_str.encode()).decode()
|
|
||||||
resp, _ = await self._api_call(
|
|
||||||
"GET", "/api/v1/users/me/subscriptions",
|
|
||||||
)
|
|
||||||
if resp and "subscriptions" in resp:
|
|
||||||
stream_count = len(resp["subscriptions"])
|
|
||||||
stream_names = [s.get("name", "?") for s in resp["subscriptions"]][:5]
|
|
||||||
checks["stream_subscriptions"] = {
|
|
||||||
"status": stream_count > 0,
|
|
||||||
"detail": f"{stream_count} stream(s): {', '.join(stream_names)}"
|
|
||||||
if stream_count > 0
|
|
||||||
else "No stream subscriptions — bot will not receive stream events",
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
checks["stream_subscriptions"] = {
|
|
||||||
"status": False,
|
|
||||||
"detail": "Failed to check subscriptions",
|
|
||||||
}
|
|
||||||
except Exception as sub_err:
|
|
||||||
checks["stream_subscriptions"] = {
|
|
||||||
"status": False,
|
|
||||||
"detail": f"Subscription check failed: {sub_err}",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Overall verdict
|
# Overall verdict
|
||||||
critical = ["connected", "queue_registered", "http_client", "poll_loop"]
|
critical = ["connected", "queue_registered", "http_client", "poll_loop"]
|
||||||
passed = sum(1 for c in checks.values() if c["status"])
|
passed = sum(1 for c in checks.values() if c["status"])
|
||||||
@@ -1282,7 +1169,6 @@ class ZulipAdapter(BasePlatformAdapter):
|
|||||||
- Message counts (total, DM, mention)
|
- Message counts (total, DM, mention)
|
||||||
- Send counts and errors
|
- Send counts and errors
|
||||||
- Dedup map size
|
- Dedup map size
|
||||||
- Known issues (Gen 6)
|
|
||||||
- Timestamps of last activity
|
- Timestamps of last activity
|
||||||
|
|
||||||
Suitable for periodic logging to RA-H OS knowledge graph.
|
Suitable for periodic logging to RA-H OS knowledge graph.
|
||||||
@@ -1310,7 +1196,6 @@ class ZulipAdapter(BasePlatformAdapter):
|
|||||||
stats["consecutive_empty_polls"] = self._consecutive_empty_polls
|
stats["consecutive_empty_polls"] = self._consecutive_empty_polls
|
||||||
stats["client_pool_resets"] = self._client_pool_reset_count
|
stats["client_pool_resets"] = self._client_pool_reset_count
|
||||||
stats["silence_seconds"] = round(time.time() - self._last_event_received_time)
|
stats["silence_seconds"] = round(time.time() - self._last_event_received_time)
|
||||||
stats["known_issues"] = list(self._known_issues) # Gen 6
|
|
||||||
|
|
||||||
# Compute error rate
|
# Compute error rate
|
||||||
total_polls = stats.get("poll_count", 0) or 1
|
total_polls = stats.get("poll_count", 0) or 1
|
||||||
@@ -1320,113 +1205,6 @@ class ZulipAdapter(BasePlatformAdapter):
|
|||||||
|
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Gen 6: known_issues persistence
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
|
|
||||||
def add_known_issue(self, issue: str) -> None:
|
|
||||||
"""Record a discovered issue for tracking across generations.
|
|
||||||
|
|
||||||
Known issues persist in the adapter instance's lifetime and are
|
|
||||||
included in health stats for external logging to knowledge graph.
|
|
||||||
"""
|
|
||||||
if issue not in self._known_issues:
|
|
||||||
self._known_issues.append(issue)
|
|
||||||
logger.warning("[%s] Known issue recorded: %s", self.name, issue)
|
|
||||||
|
|
||||||
def get_known_issues(self) -> List[str]:
|
|
||||||
"""Return all tracked known issues."""
|
|
||||||
return list(self._known_issues)
|
|
||||||
|
|
||||||
def clear_known_issues(self) -> None:
|
|
||||||
"""Clear resolved issues from the tracker."""
|
|
||||||
if self._known_issues:
|
|
||||||
logger.info(
|
|
||||||
"[%s] Cleared %d known issues",
|
|
||||||
self.name, len(self._known_issues),
|
|
||||||
)
|
|
||||||
self._known_issues.clear()
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Gen 6: Simulated DM latency test (offline-verifiable)
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
|
|
||||||
async def simulate_dm_latency(self) -> Dict[str, Any]:
|
|
||||||
"""Simulate DM processing pipeline and measure round-trip timing.
|
|
||||||
|
|
||||||
Creates a mock DM event and routes it through the full processing
|
|
||||||
pipeline (parse → route → send placeholder) without a live Zulip
|
|
||||||
connection. Measures:
|
|
||||||
- Processing time (event parse → handle_message)
|
|
||||||
- Send pipeline time (placeholder generation)
|
|
||||||
- Total simulated round-trip
|
|
||||||
|
|
||||||
Returns timing breakdown for offline verification of the
|
|
||||||
dm_response_time_ms < 10000 postcondition.
|
|
||||||
"""
|
|
||||||
import time as _time
|
|
||||||
|
|
||||||
result = {"test": "simulated_dm_latency", "steps": {}}
|
|
||||||
|
|
||||||
# Step 1: Simulate message parse
|
|
||||||
t0 = _time.perf_counter()
|
|
||||||
mock_event = {
|
|
||||||
"id": 99999,
|
|
||||||
"type": "message",
|
|
||||||
"message": {
|
|
||||||
"id": 88888,
|
|
||||||
"type": "private",
|
|
||||||
"sender_email": "test@sysloggh.net",
|
|
||||||
"sender_full_name": "Test User",
|
|
||||||
"sender_id": 42,
|
|
||||||
"content": "Hello, this is a test message for latency measurement.",
|
|
||||||
"timestamp": _time.time(),
|
|
||||||
"mentioned_users": [],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
t1 = _time.perf_counter()
|
|
||||||
result["steps"]["event_parse"] = round((t1 - t0) * 1000, 2)
|
|
||||||
|
|
||||||
# Step 2: Simulate placeholder pick
|
|
||||||
t0 = _time.perf_counter()
|
|
||||||
placeholder = self._pick_placeholder()
|
|
||||||
t1 = _time.perf_counter()
|
|
||||||
result["steps"]["placeholder_pick"] = round((t1 - t0) * 1000, 2)
|
|
||||||
result["placeholder"] = placeholder
|
|
||||||
|
|
||||||
# Step 3: Simulate auth header (typically done once, measure for completeness)
|
|
||||||
t0 = _time.perf_counter()
|
|
||||||
if self._email and self._api_key:
|
|
||||||
_build_auth_header(self._email, self._api_key)
|
|
||||||
t1 = _time.perf_counter()
|
|
||||||
result["steps"]["auth_header"] = round((t1 - t0) * 1000, 2)
|
|
||||||
|
|
||||||
# Step 4: Simulate dedup check
|
|
||||||
t0 = _time.perf_counter()
|
|
||||||
is_dup = self._is_duplicate("99999:88888")
|
|
||||||
t1 = _time.perf_counter()
|
|
||||||
result["steps"]["dedup_check"] = round((t1 - t0) * 1000, 2)
|
|
||||||
result["is_duplicate"] = is_dup
|
|
||||||
|
|
||||||
# Step 5: Simulate truncation (worst case)
|
|
||||||
t0 = _time.perf_counter()
|
|
||||||
_truncate("x" * 15000)
|
|
||||||
t1 = _time.perf_counter()
|
|
||||||
result["steps"]["truncation"] = round((t1 - t0) * 1000, 2)
|
|
||||||
|
|
||||||
# Aggregate
|
|
||||||
total_ms = sum(result["steps"].values())
|
|
||||||
result["total_simulated_ms"] = round(total_ms, 2)
|
|
||||||
result["dm_response_time_ms_pass"] = total_ms < 10000
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
"[%s] Simulated DM latency: %.2fms (%s)",
|
|
||||||
self.name, total_ms,
|
|
||||||
"PASS" if result["dm_response_time_ms_pass"] else "FAIL",
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Zulip API helpers
|
# Zulip API helpers
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -1497,12 +1275,9 @@ class ZulipAdapter(BasePlatformAdapter):
|
|||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def _pick_placeholder(self) -> str:
|
def _pick_placeholder(self) -> str:
|
||||||
"""Pick a random placeholder message for streaming UX.
|
"""Pick a random placeholder message for streaming UX."""
|
||||||
|
idx = hash(str(time.time())) % len(PLACEHOLDERS)
|
||||||
Gen 6: Switched from hash-based to random.choice() to avoid
|
return PLACEHOLDERS[idx]
|
||||||
collision when called twice in the same second.
|
|
||||||
"""
|
|
||||||
return random.choice(PLACEHOLDERS)
|
|
||||||
|
|
||||||
def _is_duplicate(self, msg_id: str) -> bool:
|
def _is_duplicate(self, msg_id: str) -> bool:
|
||||||
"""Deduplication using message IDs — O(1) lookup, no cleanup.
|
"""Deduplication using message IDs — O(1) lookup, no cleanup.
|
||||||
|
|||||||
Reference in New Issue
Block a user