Files
LegacyHUB/scripts/locustfile_search.py
Vadim Malanov d27dd0ffbb feat: align LegacyHUB with TeamHUB platform contract (D2/D4, assets, security)
Close 12 audit-driven platform-compliance gaps on a single branch.

- D4 dispatch: app/integrations/dispatch_client.py participant `legacyhub`,
  emits LegacyhubDocumentIndexed + AssetDerivativeReady after the indexing
  commit (idempotent uuid5), http_inbox route (reindex/tombstone) with
  audit-based dedupe; docs/dispatch-contract.md. Celery+Redis stays intra-module.
- D2 SSO: app/integrations/identity.py validates X-TeamHub-* + role/scope
  mapper; security.py adds trusted-header enforcement (AUTH_REQUIRE_IDENTITY)
  and a scope check on /search; docker-compose.teamhub.yml (external teamhub_net
  + internal db net, api not host-published); RUNBOOK network/firewall section.
- Asset standard: SearchHit/Citation carry asset_id/owner_module; buckets
  renamed teamhub-legacyhub-* (+quarantine/tmp/exports); purge-by-asset_id with
  legal-hold guard (app/indexing/projection.py); OCR-markdown derivative event.
- audit_log model + Alembic 0003 + record_audit on writes (same transaction).
- Secret masking: app/common/json_logger.py recursive mask wired into structlog
  (+ensure_ascii=False); event payloads redacted before persistence.
- Service X-API-Key mandatory on ingest endpoints (defence-in-depth).
- Port: host API 8000->8050 (collision with SalesHUB/MailHUB resolved),
  container still listens on 8000.
- Config: no plaintext secret defaults; fail-loud in non-dev (no value leak).
- Docs drift: README PG 5440, layered-auth note, 5173 removed from CORS;
  ingest/folder gated by ENABLE_FOLDER_INGEST (410 by default).
- ADRs: layers mapping, shared-core extraction, UI locale (RU-first).

Tests: 78 passing (ruff, compileall, pytest, tsc, vite build, compose config).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 11:44:15 +03:00

72 lines
2.1 KiB
Python

"""Locust load profile for the LegacyHUB hybrid search API.
Run:
pip install locust
locust -f scripts/locustfile_search.py \
--host http://localhost:8050 \
--users 50 --spawn-rate 5 --run-time 5m
Or headless with HTML report:
locust -f scripts/locustfile_search.py --host http://localhost:8050 \
--headless --users 100 --spawn-rate 10 --run-time 10m \
--html load_search.html
"""
from __future__ import annotations
import random
from locust import HttpUser, between, task
QUERIES = [
"ГОСТ 21.501-93 рабочие чертежи",
"класс бетона B25",
"регламент технического обслуживания",
"контроль качества сварных соединений",
"схема электропитания корпус 3",
"журнал ремонтов узлов",
"правила производства земляных работ",
"акты приемки скрытых работ",
"fundament concrete grade",
"maintenance schedule appendix",
]
MODES = ["hybrid", "hybrid", "hybrid", "lexical", "semantic"]
class SearchUser(HttpUser):
wait_time = between(0.5, 2.5)
api_prefix = "/api/v1"
@task(8)
def hybrid_search(self):
body = {
"query": random.choice(QUERIES),
"limit": random.choice([5, 10, 20]),
"filters": {
"document_id": None,
"source_path": None,
"block_type": None,
"min_ocr_confidence": None,
},
"search_mode": random.choice(MODES),
}
with self.client.post(
f"{self.api_prefix}/search",
json=body,
name="POST /search",
catch_response=True,
) as res:
if res.status_code != 200:
res.failure(f"HTTP {res.status_code}: {res.text[:120]}")
return
data = res.json()
if not data.get("results"):
res.failure("empty results")
@task(1)
def health(self):
self.client.get(f"{self.api_prefix}/health", name="GET /health")