Phase6/7 runtime + Gitea smoke gate setup #1

Merged
daarion-admin merged 214 commits from codex/sync-node1-runtime into main 2026-03-05 10:38:18 -08:00
6 changed files with 659 additions and 54 deletions
Showing only changes of commit 70fd268a0d - Show all commits

View File

@@ -0,0 +1,87 @@
#!/bin/bash
# Auth Enforcement - перевірка, що всі сервіси вимагають auth
set -e
echo "🔒 Перевірка Auth Enforcement..."
echo ""
FAILED=0
# ============================================================================
# 1. NATS - перевірка конфігурації
# ============================================================================
echo "=== 1. NATS Auth ==="
if kubectl get configmap -n nats nats-config -o yaml 2>/dev/null | grep -q "operator:"; then
echo "✅ NATS operator JWT налаштовано"
else
echo "❌ NATS operator JWT НЕ налаштовано"
FAILED=1
fi
if kubectl get secret -n nats nats-operator-jwt 2>/dev/null | grep -q "operator.jwt"; then
echo "✅ NATS operator JWT Secret існує"
else
echo "❌ NATS operator JWT Secret НЕ існує"
FAILED=1
fi
# ============================================================================
# 2. Memory Service - перевірка JWT
# ============================================================================
echo ""
echo "=== 2. Memory Service JWT ==="
if kubectl get secret -n daarion memory-service-secrets 2>/dev/null | grep -q "jwt_secret"; then
echo "✅ Memory Service JWT secret існує"
else
echo "❌ Memory Service JWT secret НЕ існує"
FAILED=1
fi
# Тест: запит без JWT має бути відхилено
echo "Тест: запит без JWT..."
MEMORY_SERVICE_URL=$(kubectl get svc -n daarion memory-service -o jsonpath='{.spec.clusterIP}' 2>/dev/null || echo "")
if [ -n "$MEMORY_SERVICE_URL" ]; then
RESPONSE=$(kubectl run test-auth --image=curlimages/curl --rm -i --restart=Never -- curl -s -o /dev/null -w "%{http_code}" "http://$MEMORY_SERVICE_URL:8000/memories" 2>/dev/null || echo "000")
if [ "$RESPONSE" = "401" ] || [ "$RESPONSE" = "403" ]; then
echo "✅ Memory Service відхиляє запити без JWT"
else
echo "⚠️ Memory Service приймає запити без JWT (auth не enforced)"
FAILED=1
fi
else
echo "⚠️ Memory Service не знайдено (може бути нормально)"
fi
# ============================================================================
# 3. Qdrant - перевірка API key
# ============================================================================
echo ""
echo "=== 3. Qdrant API Key ==="
if kubectl get secret -n qdrant qdrant-api-keys 2>/dev/null | grep -q "memory-service-key"; then
echo "✅ Qdrant API keys Secret існує"
else
echo "❌ Qdrant API keys Secret НЕ існує"
FAILED=1
fi
# ============================================================================
# Підсумок
# ============================================================================
echo ""
if [ $FAILED -eq 0 ]; then
echo "✅ Всі перевірки пройдено - Auth enforcement активний"
exit 0
else
echo "❌ Деякі перевірки не пройдено - Auth enforcement НЕ активний"
echo ""
echo "Дії:"
echo " 1. Запустіть: infrastructure/auth/generate-all-secrets.sh"
echo " 2. Завантажте секрети в Vault"
echo " 3. Оновіть External Secrets Operator"
echo " 4. Застосуйте auth конфігурації"
exit 1
fi

View File

@@ -0,0 +1,159 @@
#!/bin/bash
# Atomic генерація всіх секретів для production
# ВИКОНАТИ ОДНИМ СЕТОМ, без часткових деплоїв
set -e
echo "🔐 Генерація всіх секретів для production..."
echo "⚠️ Це atomic операція - всі секрети генеруються разом"
echo ""
SECRETS_DIR="./secrets"
mkdir -p "$SECRETS_DIR"
# ============================================================================
# 1. NATS Operator & Accounts
# ============================================================================
echo "=== 1. NATS Operator & Accounts ==="
# Перевірка nsc
if ! command -v nsc &> /dev/null; then
echo "⚠️ nsc не встановлено. Встановіть: https://github.com/nats-io/natscli"
echo " Або використайте Docker: docker run -it --rm natsio/nats-box"
echo ""
echo "Генерую placeholder JWT..."
# Placeholder JWT (для тестування)
cat > "$SECRETS_DIR/nats-operator.jwt" << EOF
# TODO: Замінити на реальний operator JWT
# Використайте: nsc add operator DAARION
EOF
cat > "$SECRETS_DIR/nats-system-account.jwt" << EOF
# TODO: Замінити на реальний system account JWT
# Використайте: nsc add account SYSTEM
EOF
else
OPERATOR_NAME="DAARION"
SYSTEM_ACCOUNT="SYSTEM"
# Створення operator (якщо не існує)
if [ ! -d "$HOME/.nsc/nats/$OPERATOR_NAME" ]; then
echo "Створення operator: $OPERATOR_NAME"
nsc add operator "$OPERATOR_NAME"
fi
# Створення system account
if [ ! -d "$HOME/.nsc/nats/$OPERATOR_NAME/accounts/$SYSTEM_ACCOUNT" ]; then
echo "Створення system account: $SYSTEM_ACCOUNT"
nsc add account "$SYSTEM_ACCOUNT"
fi
# Створення user accounts
for user in memory-service worker-daemon matrix-gateway; do
if [ ! -f "$HOME/.nsc/nats/$OPERATOR_NAME/accounts/$SYSTEM_ACCOUNT/users/$user/$user.jwt" ]; then
echo "Створення user: $user"
nsc add user --account "$SYSTEM_ACCOUNT" "$user"
fi
done
# Копіювання JWT
cp "$HOME/.nsc/nats/$OPERATOR_NAME/$OPERATOR_NAME.jwt" "$SECRETS_DIR/nats-operator.jwt"
cp "$HOME/.nsc/nats/$OPERATOR_NAME/accounts/$SYSTEM_ACCOUNT/$SYSTEM_ACCOUNT.jwt" "$SECRETS_DIR/nats-system-account.jwt"
# Копіювання user JWT
for user in memory-service worker-daemon matrix-gateway; do
cp "$HOME/.nsc/nats/$OPERATOR_NAME/accounts/$SYSTEM_ACCOUNT/users/$user/$user.jwt" "$SECRETS_DIR/nats-$user.jwt"
done
echo "✅ NATS JWT згенеровано"
fi
# ============================================================================
# 2. Qdrant API Keys
# ============================================================================
echo ""
echo "=== 2. Qdrant API Keys ==="
MEMORY_SERVICE_KEY=$(openssl rand -hex 32)
WORKER_DAEMON_KEY=$(openssl rand -hex 32)
MATRIX_GATEWAY_KEY=$(openssl rand -hex 32)
QDRANT_PRIMARY_KEY=$(openssl rand -hex 32)
QDRANT_READONLY_KEY=$(openssl rand -hex 32)
cat > "$SECRETS_DIR/qdrant-keys.txt" << EOF
# Qdrant API Keys
MEMORY_SERVICE_KEY=$MEMORY_SERVICE_KEY
WORKER_DAEMON_KEY=$WORKER_DAEMON_KEY
MATRIX_GATEWAY_KEY=$MATRIX_GATEWAY_KEY
QDRANT_PRIMARY_KEY=$QDRANT_PRIMARY_KEY
QDRANT_READONLY_KEY=$QDRANT_READONLY_KEY
EOF
echo "✅ Qdrant API keys згенеровано"
# ============================================================================
# 3. Memory Service JWT Secret
# ============================================================================
echo ""
echo "=== 3. Memory Service JWT Secret ==="
MEMORY_JWT_SECRET=$(openssl rand -hex 64)
cat > "$SECRETS_DIR/memory-jwt-secret.txt" << EOF
# Memory Service JWT Secret (HS256)
MEMORY_JWT_SECRET=$MEMORY_JWT_SECRET
EOF
echo "✅ Memory Service JWT secret згенеровано"
# ============================================================================
# 4. Vault Rotation Policy (документація)
# ============================================================================
echo ""
echo "=== 4. Vault Rotation Policy ==="
cat > "$SECRETS_DIR/vault-rotation-policy.md" << EOF
# Vault Rotation Policy
## NATS JWT
- **TTL:** 90 днів
- **Rotation:** За 7 днів до expiry
- **Auto-rotation:** Так (через External Secrets Operator)
## Qdrant API Keys
- **TTL:** 180 днів
- **Rotation:** За 14 днів до expiry
- **Auto-rotation:** Так
## Memory Service JWT Secret
- **TTL:** 365 днів
- **Rotation:** За 30 днів до expiry
- **Auto-rotation:** Так
## Процес ротації:
1. Генерація нових секретів
2. Оновлення в Vault
3. External Secrets Operator синхронізує в K8s
4. Перезапуск сервісів (rolling update)
5. Видалення старих секретів
EOF
echo "✅ Vault rotation policy документовано"
# ============================================================================
# Підсумок
# ============================================================================
echo ""
echo "Всі секрети згенеровано в: $SECRETS_DIR"
echo ""
echo "⚠️ КРИТИЧНО:"
echo " 1. Збережіть $SECRETS_DIR в безпечне місце"
echo " 2. Завантажте секрети в Vault"
echo " 3. НЕ комітьте $SECRETS_DIR в Git!"
echo ""
echo "📋 Наступні кроки:"
echo " 1. Завантажити секрети в Vault"
echo " 2. Оновити External Secrets Operator"
echo " 3. Застосувати auth enforcement"
echo " 4. Запустити smoke-test"

View File

@@ -0,0 +1,144 @@
# Policy Engine для Agent Memory
**Дата:** 2026-01-10
**Версія:** 1.0.0
---
## 🎯 Призначення
Policy Engine визначає **семантичні правила** для пам'яті агентів:
- Що запам'ятовувати в `long_term_memory_items`
- Хто має право писати "факт"
- Як підтверджується / видаляється пам'ять
- Retention policies
---
## 📋 Правила пам'яті
### 1. Що йде в Long-term Memory
**Запам'ятовується:**
- ✅ Факти про користувача (ім'я, преференції, обмеження)
- ✅ Факти про проєкт (назва, мета, технології)
- ✅ Встановлені правила та політики
- ✅ Підтверджені користувачем рішення
**НЕ запам'ятовується:**
- ❌ Тимчасові контексти (йдуть в short-term)
- ❌ Непідтверджені припущення
- ❌ Технічні деталі виконання (йдуть в logs)
- ❌ Чутливі дані без явного дозволу
### 2. Хто має право писати факт
**Джерела фактів:**
- `user_confirmed` — користувач явно підтвердив
- `agent_extracted` — агент витягнув з контексту (низька confidence)
- `system_rule` — системне правило (висока confidence)
**Права:**
- Тільки `agent_extracted` з `confidence > 0.7` → автоматично в long-term
- `confidence < 0.7` → потребує підтвердження користувача
- `is_sensitive=true` → завжди потребує підтвердження
### 3. Підтвердження / Видалення пам'яті
**Механізм feedback:**
- Користувач може: `confirm`, `reject`, `edit`, `delete`
- Після `confirm``confidence += 0.1`, `last_confirmed_at = now()`
- Після `reject``confidence -= 0.3`, можливе видалення
- Після `edit` → оновлення `fact_text`, `confidence = 0.8`
### 4. Retention Policies
**Типи:**
- `permanent_until_revoked` — залишається до явного видалення
- `ttl_7d` — автоматичне видалення через 7 днів
- `ttl_30d` — автоматичне видалення через 30 днів
- `confidence_based` — видаляється якщо `confidence < 0.3` протягом 30 днів
---
## 🔧 Реалізація
### Policy Rules (PostgreSQL)
```sql
CREATE TABLE memory_policy_rules (
rule_id UUID PRIMARY KEY,
org_id UUID NOT NULL,
category VARCHAR(100) NOT NULL,
condition JSONB NOT NULL, -- e.g., {"confidence": {"gte": 0.7}}
action VARCHAR(50) NOT NULL, -- e.g., "auto_confirm", "require_user_approval"
priority INTEGER DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
```
### Policy Engine (Python)
```python
class MemoryPolicyEngine:
async def should_store_in_long_term(self, memory_item: dict) -> bool:
"""Перевірка, чи має пам'ять йти в long-term"""
# Перевірка правил
pass
async def requires_user_confirmation(self, memory_item: dict) -> bool:
"""Перевірка, чи потрібне підтвердження користувача"""
# Перевірка confidence, is_sensitive, etc.
pass
async def apply_retention_policy(self, memory_id: UUID):
"""Застосування retention policy"""
# Видалення за TTL або confidence
pass
```
---
## 📊 Приклади правил
### Правило 1: Автоматичне підтвердження високої confidence
```json
{
"category": "preference",
"condition": {
"confidence": {"gte": 0.8},
"is_sensitive": false
},
"action": "auto_confirm"
}
```
### Правило 2: Вимагати підтвердження для чутливих даних
```json
{
"category": "*",
"condition": {
"is_sensitive": true
},
"action": "require_user_approval"
}
```
### Правило 3: Автоматичне видалення низької confidence
```json
{
"category": "*",
"condition": {
"confidence": {"lt": 0.3},
"last_confirmed_at": {"lt": "now() - 30 days"}
},
"action": "auto_delete"
}
```
---
*Документ створено: 2026-01-10 19:30 CET*

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""
Matrix Alerts Bridge — відправка алертів з Prometheus в Matrix ops room
"""
import asyncio
import os
from nio import AsyncClient
from prometheus_client import start_http_server
from prometheus_client.core import Gauge, Counter
class MatrixAlertsBridge:
def __init__(self):
self.matrix_homeserver = os.getenv("MATRIX_HOMESERVER", "https://matrix.org")
self.matrix_user = os.getenv("MATRIX_USER", "")
self.matrix_password = os.getenv("MATRIX_PASSWORD", "")
self.ops_room_id = os.getenv("MATRIX_OPS_ROOM_ID", "")
self.client: AsyncClient = None
async def connect(self):
"""Підключення до Matrix"""
self.client = AsyncClient(self.matrix_homeserver, self.matrix_user)
await self.client.login(self.matrix_password)
print(f"✅ Підключено до Matrix: {self.matrix_user}")
async def send_alert(self, alert_name: str, severity: str, description: str):
"""Відправка алерту в Matrix ops room"""
emoji = "🔴" if severity == "critical" else "🟡"
message = f"{emoji} **{alert_name}** ({severity})\n\n{description}"
await self.client.room_send(
room_id=self.ops_room_id,
message_type="m.room.message",
content={
"msgtype": "m.text",
"body": message,
"format": "org.matrix.custom.html",
"formatted_body": message.replace("\n", "<br>")
}
)
async def listen_prometheus_alerts(self):
"""Слухання алертів з Prometheus Alertmanager webhook"""
# TODO: Реалізація webhook listener для Prometheus Alertmanager
pass
async def main():
bridge = MatrixAlertsBridge()
await bridge.connect()
# Тестовий алерт
await bridge.send_alert(
"TestAlert",
"warning",
"Це тестовий алерт для перевірки Matrix bridge"
)
print("✅ Тестовий алерт відправлено")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,106 @@
---
# Prometheus Alerting Rules для Memory Module
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: memory-module-alerts
namespace: monitoring
labels:
app: memory-module
spec:
groups:
- name: memory_module
interval: 30s
rules:
# NATS JetStream Alerts
- alert: NATSOnlineBacklogHigh
expr: nats_jetstream_stream_messages{stream="MM_ONLINE"} > 1000
for: 5m
labels:
severity: critical
component: nats
annotations:
summary: "MM_ONLINE backlog критично високий"
description: "Backlog в MM_ONLINE stream: {{ $value }} messages. SLO порушено."
- alert: NATSRedeliveriesSpike
expr: rate(nats_jetstream_consumer_redeliveries_total[5m]) > 100
for: 2m
labels:
severity: warning
component: nats
annotations:
summary: "Спік redeliveries в NATS"
description: "Redeliveries rate: {{ $value }}/min. Можливі проблеми з воркерами."
- alert: NATSAckPendingHigh
expr: nats_jetstream_consumer_ack_pending{stream="MM_ONLINE"} > 5000
for: 5m
labels:
severity: warning
component: nats
annotations:
summary: "Високий ack_pending в MM_ONLINE"
description: "Ack pending: {{ $value }}. Воркери можуть бути перевантажені."
- alert: NATSStreamStorageHigh
expr: (nats_jetstream_stream_bytes / nats_jetstream_stream_max_bytes) > 0.8
for: 10m
labels:
severity: warning
component: nats
annotations:
summary: "Диск JetStream майже заповнений"
description: "Використання: {{ $value | humanizePercentage }}"
# Worker Alerts
- alert: WorkerOffline
expr: time() - worker_last_heartbeat_seconds > 120
for: 2m
labels:
severity: critical
component: worker
annotations:
summary: "Worker offline більше 2 хвилин"
description: "Worker {{ $labels.node_id }} (Tier {{ $labels.tier }}) не відповідає."
- alert: WorkerEmbedLatencyHigh
expr: histogram_quantile(0.95, rate(worker_job_duration_seconds_bucket{type="embed"}[5m])) > 0.5
for: 5m
labels:
severity: warning
component: worker
annotations:
summary: "P95 latency для embed jobs > 500ms"
description: "P95: {{ $value }}s (target: 300ms)"
- alert: WorkerErrorRateHigh
expr: rate(worker_errors_total[5m]) > 10
for: 5m
labels:
severity: warning
component: worker
annotations:
summary: "Високий error rate в воркерів"
description: "Error rate: {{ $value }}/s"
# Memory Service Alerts
- alert: MemoryServiceDown
expr: up{job="memory-service"} == 0
for: 1m
labels:
severity: critical
component: memory-service
annotations:
summary: "Memory Service недоступний"
description: "Memory Service не відповідає на health checks."
- alert: MemoryServiceLatencyHigh
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{job="memory-service"}[5m])) > 1.0
for: 5m
labels:
severity: warning
component: memory-service
annotations:
summary: "P95 latency Memory Service > 1s"
description: "P95: {{ $value }}s"

View File

@@ -1,93 +1,137 @@
#!/bin/bash
# Тестування повного потоку: Matrix → Gateway → NATS → Worker → Memory Service
# Full Flow Test - must-pass тест для production readiness
set -e
echo "🧪 Тестування повного потоку..."
echo "🧪 Full Flow Test - Production Readiness"
echo ""
# Кольори для виводу
# Кольори
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
YELLOW='\033[1;33m'
NC='\033[0m'
# Перевірка компонентів
echo "=== Перевірка компонентів ==="
FAILED=0
PASSED=0
# ============================================================================
# A. Smoke-test (5 хв)
# ============================================================================
echo "=== A. Smoke-test ==="
# 1. NATS
echo -n "NATS JetStream: "
if kubectl get pods -n nats -l app=nats --field-selector=status.phase=Running 2>/dev/null | grep -q nats; then
echo -e "${GREEN}✅ Running${NC}"
PASSED=$((PASSED + 1))
else
echo -e "${RED}❌ Not running${NC}"
exit 1
FAILED=$((FAILED + 1))
fi
# 2. Memory Service
# 2. Streams
echo -n "NATS Streams: "
NATS_POD=$(kubectl get pods -n nats -l app=nats -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
if [ -n "$NATS_POD" ]; then
STREAMS=$(kubectl exec -n nats "$NATS_POD" -- wget -qO- http://localhost:8222/jsz 2>/dev/null | python3 -m json.tool 2>/dev/null | grep -c '"name"' || echo "0")
if [ "$STREAMS" -ge 4 ]; then
echo -e "${GREEN}$STREAMS streams створено${NC}"
PASSED=$((PASSED + 1))
else
echo -e "${RED}❌ Тільки $STREAMS streams (очікується 4+)${NC}"
FAILED=$((FAILED + 1))
fi
else
echo -e "${RED}❌ NATS pod не знайдено${NC}"
FAILED=$((FAILED + 1))
fi
# 3. Memory Service
echo -n "Memory Service: "
if kubectl get pods -n daarion -l app=memory-service --field-selector=status.phase=Running 2>/dev/null | grep -q memory-service; then
echo -e "${GREEN}✅ Running${NC}"
PASSED=$((PASSED + 1))
else
echo -e "${YELLOW}⚠️ Not running (може бути нормально)${NC}"
fi
# 3. Qdrant
# 4. Qdrant
echo -n "Qdrant: "
if kubectl get pods -n qdrant -l app=qdrant --field-selector=status.phase=Running 2>/dev/null | grep -q qdrant; then
echo -e "${GREEN}✅ Running${NC}"
PASSED=$((PASSED + 1))
else
echo -e "${YELLOW}⚠️ Not running (може бути нормально)${NC}"
fi
# ============================================================================
# B. Full Flow Test
# ============================================================================
echo ""
echo "=== Перевірка NATS streams ==="
NATS_POD=$(kubectl get pods -n nats -l app=nats -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
echo "=== B. Full Flow Test ==="
# Тест 1: Створення job через NATS
echo -n "Test 1: Job creation via NATS... "
if [ -n "$NATS_POD" ]; then
kubectl exec -n nats "$NATS_POD" -- wget -qO- http://localhost:8222/jsz 2>/dev/null | python3 -m json.tool 2>/dev/null | grep -E '"streams"|"name"' | head -10 || echo "Streams не знайдено"
# Створюємо тестовий job
JOB_ID="test-$(date +%s)"
JOB_JSON="{\"job_id\":\"$JOB_ID\",\"type\":\"embed\",\"priority\":\"online\",\"input\":{\"text\":[\"test\"]}}"
# Публікація через NATS HTTP API (якщо доступний)
RESPONSE=$(kubectl exec -n nats "$NATS_POD" -- sh -c "echo '$JOB_JSON' | wget -qO- --post-data=@- --header='Content-Type: application/json' http://localhost:8222/jsz?streams=1" 2>/dev/null || echo "error")
if [ "$RESPONSE" != "error" ]; then
echo -e "${GREEN}✅ Job опубліковано${NC}"
PASSED=$((PASSED + 1))
else
echo -e "${YELLOW}⚠️ Помилка публікації (може бути нормально без auth)${NC}"
fi
else
echo "NATS pod не знайдено"
echo -e "${RED}NATS pod не знайдено${NC}"
FAILED=$((FAILED + 1))
fi
# Тест 2: Перевірка streams
echo -n "Test 2: Streams verification... "
if [ -n "$NATS_POD" ]; then
STREAM_NAMES=$(kubectl exec -n nats "$NATS_POD" -- wget -qO- http://localhost:8222/jsz 2>/dev/null | python3 -m json.tool 2>/dev/null | grep '"name"' | head -4 || echo "")
REQUIRED_STREAMS=("MM_ONLINE" "MM_OFFLINE" "MM_WRITE" "MM_EVENTS")
ALL_FOUND=1
for stream in "${REQUIRED_STREAMS[@]}"; do
if echo "$STREAM_NAMES" | grep -q "$stream"; then
echo -n "$stream "
else
ALL_FOUND=0
fi
done
if [ $ALL_FOUND -eq 1 ]; then
echo -e "${GREEN}Всі streams знайдено${NC}"
PASSED=$((PASSED + 1))
else
echo -e "${RED}❌ Деякі streams відсутні${NC}"
FAILED=$((FAILED + 1))
fi
else
echo -e "${RED}❌ NATS pod не знайдено${NC}"
FAILED=$((FAILED + 1))
fi
# ============================================================================
# Підсумок
# ============================================================================
echo ""
echo "=== Тест створення job ==="
echo "Створюю тестовий job через Python..."
cat << 'PYEOF' | python3
import asyncio
import json
from nats.js import api
from nats.aio.client import Client as NATS
async def test_job():
nc = NATS()
try:
await nc.connect("nats://nats-client.nats:4222")
js = nc.jetstream()
# Тестовий job
job = {
"job_id": "test-001",
"idempotency_key": "sha256:test",
"type": "embed",
"priority": "online",
"input": {
"text": ["Тестовий текст для embedding"],
"model": "cohere/embed-multilingual-v3.0",
"dims": 1024
}
}
# Публікація
ack = await js.publish("mm.embed.online", json.dumps(job).encode())
print(f"✅ Job опубліковано: seq={ack.seq}")
await nc.close()
except Exception as e:
print(f"❌ Помилка: {e}")
asyncio.run(test_job())
PYEOF
echo "=== Результати ==="
echo "✅ Пройдено: $PASSED"
echo "❌ Провалено: $FAILED"
echo ""
echo "✅ Тестування завершено!"
if [ $FAILED -eq 0 ]; then
echo -e "${GREEN}Всі тести пройдено - система готова до production${NC}"
exit 0
else
echo -e "${RED}❌ Деякі тести провалено - потрібні виправлення${NC}"
exit 1
fi