test(e2e): сквозной прогон загрузки ОСВ через compose с реальным tabular-parser
Тест tests/e2e (маркеры integration, e2e, fixtures): полное приложение src.app.app с lifespan на временной БД, реальные NATS и MinIO, tabular-parser из профиля services; ОСВ 66 за 2024 -> PROCESSED, документ 66/2024/Q4, 9 строк, counterparty_check = MATCH, status = RECOGNIZED; без профиля — skip. Makefile: цель e2e, check-full включает e2e, test-integration исключает e2e. README: раздел «Сквозной прогон (e2e)». Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
parent
ea2423dec9
commit
f361fae79b
15
Makefile
15
Makefile
@ -5,7 +5,7 @@ COMPOSE_FILE ?= ../docs/dev/docker-compose.yml
|
||||
# DBML относительно api/: и в основном workspace, и в worktree .wt/<срез>/ это соседний docs/
|
||||
DBML ?= ../../docs/model/data-model-v5.15.dbml
|
||||
|
||||
.PHONY: venv lint test test-integration dbml-check migration-cycle check check-infra docker-build web-check check-full up migrate seed seed-dev
|
||||
.PHONY: venv lint test test-integration e2e dbml-check migration-cycle check check-infra docker-build web-check check-full up migrate seed seed-dev
|
||||
|
||||
# Python 3.11 (версия платформы); внутренние пакеты raisa_* вне банка недоступны — пропускаем их
|
||||
venv:
|
||||
@ -21,8 +21,14 @@ lint:
|
||||
test:
|
||||
cd api && $(BIN)/pytest -m "not integration"
|
||||
|
||||
# e2e (профиль services, реальный tabular-parser) идёт отдельной целью `e2e`, поэтому здесь исключён
|
||||
test-integration:
|
||||
cd api && $(BIN)/pytest -m integration
|
||||
cd api && $(BIN)/pytest -m "integration and not e2e"
|
||||
|
||||
# Сквозной прогон загрузки ОСВ (Срез 2c): нужны compose-инфраструктура и сервис tabular-parser из профиля `services`
|
||||
# (README, «Сквозной прогон (e2e)»); без профиля тест пропускается с причиной «профиль services не поднят», код 0
|
||||
e2e:
|
||||
cd api && $(BIN)/pytest -m e2e -rs
|
||||
|
||||
# Сверка ORM с DBML (spec data-model): пустой вывод и код 0 — совпадение
|
||||
dbml-check:
|
||||
@ -52,8 +58,9 @@ web-check:
|
||||
|
||||
# Полная проверка перед PR. Порядок: быстрые без БД (check, dbml-check), затем БД/NATS из compose (test-integration),
|
||||
# затем docker, затем web. Цикл миграций upgrade -> downgrade -> upgrade на чистой БД входит в test-integration
|
||||
# (tests/test_migrations.py), поэтому отдельным шагом не гоняется: второй запуск только удлинил бы прогон
|
||||
check-full: check dbml-check check-infra test-integration docker-build web-check
|
||||
# (tests/test_migrations.py), поэтому отдельным шагом не гоняется: второй запуск только удлинил бы прогон. e2e — после
|
||||
# test-integration; без профиля `services` пропускается (не падает)
|
||||
check-full: check dbml-check check-infra test-integration e2e docker-build web-check
|
||||
|
||||
up:
|
||||
docker compose -f $(COMPOSE_FILE) up -d
|
||||
|
||||
27
README.md
27
README.md
@ -499,14 +499,15 @@ email без учёта регистра проверяет только сер
|
||||
| `make seed-dev` | то же и dev-пользователи/филиалы (`--dev`) — только для разработки, требует `DEBUG=true` |
|
||||
| `make lint` | `ruff check`, `ruff format --check`, `mypy src scripts` |
|
||||
| `make test` | юнит-тесты: `pytest -m "not integration"`, Docker не нужен |
|
||||
| `make test-integration` | интеграционные тесты: `pytest -m integration`, нужны PostgreSQL (и NATS) |
|
||||
| `make test-integration` | интеграционные тесты: `pytest -m "integration and not e2e"`, нужны PostgreSQL (и NATS) |
|
||||
| `make e2e` | сквозной прогон загрузки ОСВ: `pytest -m e2e`, нужны compose и tabular-parser из профиля `services` (без него — skip) |
|
||||
| `make dbml-check` | сверка ORM с `../docs/model/data-model-v5.15.dbml` |
|
||||
| `make migration-cycle` | миграции на временной БД: `upgrade head` → `downgrade base` → `upgrade head`, пустой autogenerate |
|
||||
| `make check` | быстрая проверка без Docker: `lint` + `test` |
|
||||
| `make docker-build` | сборка образа: `docker build -t arm-core .` |
|
||||
| `make web-check` | фронт: `npm install`, `biome check src`, `tsc --noEmit`, `npm run build` |
|
||||
| `make check-infra` | быстрая проверка, что PostgreSQL и NATS из compose доступны (иначе: `make up`) |
|
||||
| `make check-full` | полная проверка перед PR: `check` + `dbml-check` + `check-infra` + `test-integration` (в нём цикл миграций) + `docker-build` + `web-check`; нужна поднятая инфраструктура (`make up`) и Docker |
|
||||
| `make check-full` | полная проверка перед PR: `check` + `dbml-check` + `check-infra` + `test-integration` (в нём цикл миграций) + `e2e` (без профиля `services` пропускается) + `docker-build` + `web-check`; нужна поднятая инфраструктура (`make up`) и Docker |
|
||||
|
||||
|
||||
### Тесты
|
||||
@ -521,6 +522,28 @@ email без учёта регистра проверяет только сер
|
||||
- **Реальные данные** — репозиторий `../fixtures/` (путь можно задать переменной `ARM_KA_FIXTURES`). Тесты с маркером
|
||||
`fixtures` без каталога пропускаются с понятной причиной.
|
||||
|
||||
### Сквозной прогон (e2e)
|
||||
|
||||
Тест `api/tests/e2e/test_osv_ingest_e2e.py` (маркеры `integration`, `e2e`, `fixtures`) проверяет весь путь Среза 2c с
|
||||
РЕАЛЬНЫМ tabular-parser: `POST /legal-entities/{id}/files` (ОСВ 66 за 2024 из `../fixtures`) → outbox → NATS →
|
||||
tabular-parser (MinIO) → inbox → маппинг → `GET /files/{id}` = `PROCESSED`, документ 66/2024/Q4 (нарастающий,
|
||||
`RECOGNIZED`, `counterparty_check = MATCH`) и 9 строк ОСВ. Приложение — настоящий `src.app.app` с lifespan на
|
||||
отдельной временной БД с seed, NATS и MinIO — из `api/.env.test`; в конце файл удаляется через `DELETE /files/{id}`
|
||||
(объекту S3 ставится `expires_at`), БД снимается.
|
||||
|
||||
Предусловие — сервис tabular-parser из профиля `services` на `localhost:8001`. Без него тест пропускается
|
||||
(`профиль services не поднят`), `make check-full` при этом зелёный. Инфраструктура уже поднята (`make up`); поднимайте
|
||||
только парсер, без зависимостей (`--no-deps`), чтобы не пересоздавать PostgreSQL/NATS/MinIO:
|
||||
|
||||
```bash
|
||||
docker compose -f ../docs/dev/docker-compose.yml -p arm-ka --profile services up -d --build --no-deps tabular-parser
|
||||
curl -s localhost:8001/healthcheck # {"uptime": "...", "version": "...", "nats": "ok"}
|
||||
make e2e # PASSED
|
||||
docker compose -f ../docs/dev/docker-compose.yml -p arm-ka --profile services stop tabular-parser
|
||||
```
|
||||
|
||||
Образ собирается из `../tabular-parser` (main соседнего репозитория workspace).
|
||||
|
||||
### Проверка ORM и DBML
|
||||
|
||||
Модель данных — `../docs/model/data-model-v5.15.dbml`. ORM обязана совпадать с ней один в один:
|
||||
|
||||
0
api/tests/e2e/__init__.py
Normal file
0
api/tests/e2e/__init__.py
Normal file
25
api/tests/e2e/conftest.py
Normal file
25
api/tests/e2e/conftest.py
Normal file
@ -0,0 +1,25 @@
|
||||
"""Предусловия сквозных тестов (Срез 2c, tasks 5.1): сервис tabular-parser из профиля `services` compose.
|
||||
|
||||
Без него e2e не имеет смысла (результат распознавания даёт реальный парсер): тест пропускается с причиной, а
|
||||
`make check-full` без профиля остаётся зелёным. NATS, MinIO и PostgreSQL берутся из `.env.test` (инфраструктура
|
||||
compose `make up`); профиль поднимается командой из README («Сквозной прогон (e2e)»).
|
||||
"""
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
TABULAR_PARSER_HEALTHCHECK = "http://localhost:8001/healthcheck"
|
||||
HEALTHCHECK_TIMEOUT = 2.0
|
||||
SKIP_REASON = "профиль services не поднят"
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def tabular_parser_ready() -> None:
|
||||
"""`GET localhost:8001/healthcheck` отвечает 200 и `nats=ok`, иначе — skip «профиль services не поднят»."""
|
||||
try:
|
||||
response = httpx.get(TABULAR_PARSER_HEALTHCHECK, timeout=HEALTHCHECK_TIMEOUT)
|
||||
ready = response.status_code == httpx.codes.OK and response.json().get("nats") == "ok"
|
||||
except (httpx.HTTPError, ValueError):
|
||||
ready = False
|
||||
if not ready:
|
||||
pytest.skip(f"{SKIP_REASON}: {TABULAR_PARSER_HEALTHCHECK} недоступен или без подключения к NATS")
|
||||
158
api/tests/e2e/test_osv_ingest_e2e.py
Normal file
158
api/tests/e2e/test_osv_ingest_e2e.py
Normal file
@ -0,0 +1,158 @@
|
||||
"""Сквозной прогон загрузки ОСВ (Срез 2c, tasks 5.1; design D-13) через compose с РЕАЛЬНЫМ tabular-parser.
|
||||
|
||||
Полное приложение `src.app.app` с настоящим lifespan на отдельной временной БД (миграции + seed), реальные NATS и MinIO
|
||||
из `.env.test`, `NATS_ENABLED=true`. Путь: `POST /legal-entities/{id}/files` → outbox-публикатор → NATS →
|
||||
tabular-parser (читает файл из MinIO, пишет результат в MinIO) → inbox (ack, результат) → цикл маппинга → файл
|
||||
`PROCESSED`, документ и строки ОСВ. Предусловие — профиль `services` (`tests/e2e/conftest.py`); иначе тест пропускается.
|
||||
Очистка: `DELETE /files/{id}` через API и удаление временной БД (фикстура `committed_sessionmaker`).
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import importlib
|
||||
import time
|
||||
import uuid
|
||||
from collections.abc import AsyncIterator
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
|
||||
from src.core.nats import NatsConnection
|
||||
from src.models.directories import RegionalOffice
|
||||
from tests.factories import make_company_group, make_legal_entity
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.e2e, pytest.mark.fixtures]
|
||||
|
||||
# src/__init__.py реэкспортирует объект app под именем модуля, поэтому модуль берём через importlib
|
||||
app_module = importlib.import_module("src.app")
|
||||
db_module = importlib.import_module("src.db.db")
|
||||
|
||||
KA_RF = "ka_rf@armka.local" # Ростовский филиал, права DOCUMENT_EDIT/DOCUMENT_VIEW
|
||||
ROSTOV = "Ростовский региональный филиал"
|
||||
OWNER_INN = "3528052718"
|
||||
# Как в шапке ОСВ (fixtures/golden/expected_osv_parse/rb/2024-FY/ОСВ_66 за 2024.json): ИНН в шапке нет, поэтому
|
||||
# counterparty_check = MATCH только по имени владельца файла (D-8)
|
||||
OWNER_NAME = 'АО "Русский бисквит"'
|
||||
REAL_OSV_PATH = ("osv", "rb", "2024-FY", "ОСВ_66 за 2024.xls")
|
||||
|
||||
POLL_TIMEOUT = 60.0
|
||||
POLL_INTERVAL = 0.5
|
||||
LOOP_POLL_SECONDS = 1 # публикатор и цикл маппинга не ждут штатные 5 с между проходами
|
||||
NATS_READY_TIMEOUT = 10.0
|
||||
ENTRY_ROWS = 9 # 10 строк снимка минус TOTAL (D-3: итоговая строка не хранится)
|
||||
HTTP_CREATED = 201
|
||||
HTTP_NO_CONTENT = 204
|
||||
HTTP_NOT_FOUND = 404
|
||||
|
||||
|
||||
class _EngineStub:
|
||||
"""Подмена движка БД приложения: lifespan вызывает `dispose()` последним шагом остановки."""
|
||||
|
||||
async def dispose(self) -> None:
|
||||
return None
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def running_app(
|
||||
committed_sessionmaker: async_sessionmaker[AsyncSession], monkeypatch: pytest.MonkeyPatch
|
||||
) -> AsyncIterator[Any]:
|
||||
"""Настоящий `src.app.app` внутри lifespan: реальные NATS и MinIO, БД — временная (с seed)."""
|
||||
monkeypatch.setattr(app_module.settings, "NATS_ENABLED", True)
|
||||
monkeypatch.setattr(app_module.settings, "OUTBOX_POLL_SECONDS", LOOP_POLL_SECONDS)
|
||||
monkeypatch.setattr(app_module.settings, "MAPPING_POLL_SECONDS", LOOP_POLL_SECONDS)
|
||||
monkeypatch.setattr(app_module.settings.OPENBAO.SMB, "SMB_HOST", "")
|
||||
# фабрика сессий приложения: lifespan/inbox/циклы берут её из app_module, маршруты и аудит входа — из db_module
|
||||
monkeypatch.setattr(app_module, "async_session_maker", committed_sessionmaker)
|
||||
monkeypatch.setattr(db_module, "async_session_maker", committed_sessionmaker)
|
||||
monkeypatch.setattr(app_module, "engine", _EngineStub())
|
||||
|
||||
async with app_module.lifespan(app_module.app):
|
||||
application = app_module.app
|
||||
assert application.state.storage is not None, "MinIO недоступен (.env.test): S3 не подключён"
|
||||
nats: NatsConnection = application.state.nats
|
||||
if not await nats.wait_ready(timeout=NATS_READY_TIMEOUT):
|
||||
pytest.fail("NATS недоступен (.env.test)", pytrace=False)
|
||||
yield application
|
||||
|
||||
|
||||
async def _client(application: Any, email: str) -> httpx.AsyncClient:
|
||||
transport = httpx.ASGITransport(app=application)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as anonymous:
|
||||
response = await anonymous.post("/api/v1/dev/token", json={"email": email})
|
||||
assert response.status_code == httpx.codes.OK, response.text
|
||||
return httpx.AsyncClient(
|
||||
transport=transport,
|
||||
base_url="http://test/api/v1",
|
||||
headers={"Authorization": f"Bearer {response.json()['access_token']}"},
|
||||
)
|
||||
|
||||
|
||||
async def _wait_processed(client: httpx.AsyncClient, file_id: uuid.UUID) -> dict[str, Any]:
|
||||
"""Опрос карточки файла до `PROCESSED` (до 60 с); `ERROR` — немедленный отказ с причиной из карточки."""
|
||||
deadline = time.monotonic() + POLL_TIMEOUT
|
||||
card: dict[str, Any] = {}
|
||||
while time.monotonic() < deadline:
|
||||
response = await client.get(f"/files/{file_id}")
|
||||
assert response.status_code == httpx.codes.OK, response.text
|
||||
card = response.json()
|
||||
if card["status"] == "PROCESSED":
|
||||
return card
|
||||
assert card["status"] != "ERROR", f"файл в ERROR: {card}"
|
||||
await asyncio.sleep(POLL_INTERVAL)
|
||||
pytest.fail(f"за {POLL_TIMEOUT} с файл не стал PROCESSED; последняя карточка: {card}", pytrace=False)
|
||||
|
||||
|
||||
async def test_real_osv_is_parsed_end_to_end(
|
||||
tabular_parser_ready: None, # noqa: ARG001 - предусловие: профиль services (skip, если не поднят)
|
||||
fixtures_dir: Any,
|
||||
running_app: Any,
|
||||
committed_sessionmaker: async_sessionmaker[AsyncSession],
|
||||
) -> None:
|
||||
content = fixtures_dir.joinpath(*REAL_OSV_PATH).read_bytes()
|
||||
async with committed_sessionmaker() as session, session.begin():
|
||||
office = (await session.execute(select(RegionalOffice).where(RegionalOffice.name == ROSTOV))).scalar_one()
|
||||
group = await make_company_group(session, name="ГК Русский бисквит", office=office)
|
||||
entity = await make_legal_entity(session, name=OWNER_NAME, inn=OWNER_INN, group=group)
|
||||
|
||||
async with await _client(running_app, KA_RF) as client:
|
||||
response = await client.post(
|
||||
f"/legal-entities/{entity.id}/files",
|
||||
files=[("files", (REAL_OSV_PATH[-1], content, "application/octet-stream"))],
|
||||
)
|
||||
assert response.status_code == HTTP_CREATED, response.text
|
||||
body = response.json()
|
||||
assert body["rejected"] == []
|
||||
(accepted,) = body["accepted"]
|
||||
file_id = uuid.UUID(accepted["file_id"])
|
||||
try:
|
||||
card = await _wait_processed(client, file_id)
|
||||
|
||||
assert card["job"]["status"] == "done"
|
||||
assert card["job"]["mapping_error"] is None
|
||||
(brief,) = card["documents"]
|
||||
assert (brief["document_type"], brief["osv_account_code"]) == ("OSV", "66")
|
||||
assert (brief["period_year"], brief["period_quarter"], brief["is_cumulative"]) == (2024, "Q4", True)
|
||||
assert brief["counterparty_check"] == "MATCH"
|
||||
assert brief["status"] == "RECOGNIZED"
|
||||
|
||||
document = (await client.get(f"/documents/{brief['id']}")).json()
|
||||
assert document["file_id"] == str(file_id)
|
||||
assert document["legal_entity_id"] == str(entity.id)
|
||||
assert (document["period_year"], document["period_quarter"]) == (2024, "Q4")
|
||||
assert (document["counterparty_check"], document["status"]) == ("MATCH", "RECOGNIZED")
|
||||
|
||||
entries = (await client.get(f"/documents/{brief['id']}/entries")).json()
|
||||
assert entries["total"] == ENTRY_ROWS
|
||||
assert [row["row_number"] for row in entries["items"]] == sorted(
|
||||
row["row_number"] for row in entries["items"]
|
||||
)
|
||||
root = entries["items"][0]
|
||||
assert (root["account_code"], root["level"]) == ("66", 0)
|
||||
finally:
|
||||
# очистка: PROCESSED без подтверждения удаляется (204); временная БД снимается фикстурой
|
||||
deleted = await client.delete(f"/files/{file_id}")
|
||||
assert deleted.status_code == HTTP_NO_CONTENT, deleted.text
|
||||
assert (await client.get(f"/files/{file_id}")).status_code == HTTP_NOT_FOUND
|
||||
@ -305,8 +305,8 @@ async def delete(self, file_id) -> None # get_file(scope) → 404; есть do
|
||||
|
||||
**Interfaces:** тест (маркеры `integration`, `e2e`, `fixtures`): предусловия — `curl localhost:8001/healthcheck` tabular-parser из профиля `services` (`docker compose -f ../docs/dev/docker-compose.yml --profile services up -d --build tabular-parser`) и `NATS_ENABLED=true` у тестового приложения; иначе `pytest.skip("профиль services не поднят")`. Шаги: полное приложение `src.app.app` (lifespan с реальными NATS/MinIO из `.env.test`, `NATS_ENABLED=true` через monkeypatch settings) на отдельной БД (`committed_sessionmaker` + seed) → `POST /legal-entities/{id}/files` для ЮЛ, созданного фабрикой с ИНН `3528052718` (АО «Русский бисквит», владелец ОСВ 66) с `rb/2024-FY/ОСВ_66 за 2024.xls` → ждать до 60 с `GET /files/{id}` → `PROCESSED` → документ 66/2024/Q4, 9 строк (10 строк снимка минус TOTAL), `counterparty_check = MATCH`, `status = RECOGNIZED`; очистка: удалить файл через API.
|
||||
|
||||
- [ ] **Step 1:** тест; **Step 2:** поднять профиль (`make up` + `--profile services` для `tabular-parser`), прогнать `make e2e` → PASS; без профиля → skip; `make check-full` зелёный.
|
||||
- [ ] **Step 3: Commit** `test(e2e): сквозной прогон загрузки ОСВ через compose с реальным tabular-parser`.
|
||||
- [x] **Step 1:** тест; **Step 2:** поднять профиль (`make up` + `--profile services` для `tabular-parser`), прогнать `make e2e` → PASS; без профиля → skip; `make check-full` зелёный.
|
||||
- [x] **Step 3: Commit** `test(e2e): сквозной прогон загрузки ОСВ через compose с реальным tabular-parser`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -194,3 +194,6 @@ rows без TOTAL, у каждого не-корня есть родитель,
|
||||
| 34 | 2026-10-05 | R-21 (Task 9): `confirm` берёт блокировки «файл → документ» и перечитывает оба под `FOR UPDATE`; чтение файлов — методы `PrimaryFilesService`, документов — `DocumentService`; задание для `DocumentOut` — текущее задание файла; список файлов `ORDER BY uploaded_at DESC NULLS LAST, id`; у моделей нет `relationship()`, поэтому job/документы грузятся пачками (число запросов не зависит от числа файлов, проверено тестом) | согласовано с порядком блокировок автозамены; Review Focus 5 | переупорядочить блокировки |
|
||||
| 35 | 2026-10-05 | R-22 (Task 9): `test_openapi_routes` отвергает лишние маршруты под `/legal-entities`, `/files`, `/documents` (Task 10 обязан добавить DELETE в `EXPECTED_ROUTES`) | новый маршрут сначала должен попасть в спеку | ослабить тест |
|
||||
| 36 | 2026-10-05 | R-23 (Task 10): удаление файла блокирует все его задания (по id) → файл → документы, 409 проверяются после блокировок; файл исчез за время ожидания → 404; `expires_at = now` безусловно у `s3_object` файла и объектов результатов (объекты не разделяются между файлами); FK на `document` из `financial_statement_meta`/`consolidation_document`/`ex_snapshot_item` не чистятся (в Срезе 2c недостижимы; `CONFIRMED`-запрет закрывает консолидации; ББ/ОФР — очищать `financial_statement_meta` в Срезе 3); `FILE_HAS_CONFIRMED_DOCUMENTS` проверяется раньше `FILE_SUPERSEDES_OTHERS` | порядок блокировок задание → файл общий для обмена/маппинга | IntegrityError 500 в Срезе 3 без очистки |
|
||||
| 37 | 2026-10-05 | Task 11: `make test-integration` = `pytest -m "integration and not e2e"`, e2e идёт целью `make e2e` и отдельным шагом `check-full` после `test-integration` | e2e несёт маркер `integration` (требование брифа); без исключения он запускался бы в `check-full` дважды | двойной прогон e2e при поднятом профиле (секунды); без профиля — два skip |
|
||||
| 38 | 2026-10-05 | Task 11: профиль `services` для парсера поднимается как `-p arm-ka … up -d --build --no-deps tabular-parser` (README); предусловие теста — `GET localhost:8001/healthcheck` = 200 и `nats=ok`, иначе skip «профиль services не поднят» | проект `arm-ka` поднят из другого файла compose (worktree среза 1): без `--no-deps` compose мог пересоздать postgres/nats/minio при расхождении конфигурации | пересоздание инфраструктуры и потеря dev-данных |
|
||||
| 39 | 2026-10-05 | Task 11: e2e подменяет `async_session_maker` в `src.app` и `src.db.db` и `engine` в `src.app` (как `test_exchange_lifespan_nats`), `MAPPING_POLL_SECONDS`/`OUTBOX_POLL_SECONDS` = 1; ЮЛ создаётся с именем `АО "Русский бисквит"`, ИНН `3528052718` — в шапке ОСВ ИНН нет, MATCH честный по имени (D-8) | нужна временная БД вместо рабочей и быстрый прогон; шапка снимка: `organization_name = АО "Русский бисквит"`, `organization_inn = null` | e2e читает/пишет не ту БД (рабочая `armka`) либо ложный MISMATCH |
|
||||
|
||||
@ -58,7 +58,7 @@
|
||||
|
||||
## 5. Сквозной прогон и документы
|
||||
|
||||
- [ ] 5.1 **E2E через compose.** `tests/e2e/test_osv_ingest_e2e.py` (маркеры `integration`, `e2e`): при поднятом профиле
|
||||
- [x] 5.1 **E2E через compose.** `tests/e2e/test_osv_ingest_e2e.py` (маркеры `integration`, `e2e`): при поднятом профиле
|
||||
`services` (реальный `tabular-parser` против общего NATS/MinIO) — загрузка `rb/2024-FY/ОСВ_66 за 2024.xls` через API ядра,
|
||||
ожидание `PROCESSED` (≤ 60 с), проверка документа и 9 строк (10 строк снимка минус TOTAL); без профиля — skip с причиной. `make e2e` цель; `check-full`
|
||||
его включает (skip допустим).
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user