Files
stonks-oracle/tests/test_pbt_report_chunking.py
T
Celes Renata bc077bfcc8
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/build-2 Pipeline was successful
ci/woodpecker/push/build-3 Pipeline was successful
ci/woodpecker/push/build-1 Pipeline was successful
ci/woodpecker/push/finalize Pipeline was successful
Build and Push / lint-and-test (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.adapters.broker_adapter name:broker-adapter]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.aggregation.worker name:aggregation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.extractor.worker name:extractor]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.ingestion.worker name:ingestion]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.lake_publisher.worker name:lake-publisher]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.parser.worker name:parser]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.recommendation.worker name:recommendation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.scheduler.app name:scheduler]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.api.app:app --host 0.0.0.0 --port 8000 name:query-api]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.risk.app:app --host 0.0.0.0 --port 8000 name:risk]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.symbol_registry.app:app --host 0.0.0.0 --port 8000 name:symbol-registry]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.trading.app:app --host 0.0.0.0 --port 8000 name:trading-engine]) (push) Has been cancelled
Build and Push / build-dashboard (push) Has been cancelled
Build and Push / build-superset (push) Has been cancelled
Build and Push / integration-test (push) Has been cancelled
Build and Push / beta-gate (push) Has been cancelled
feat: trading feedback engine — periodic performance reports with AI summarization
- Migration 038: trading_reports table + report-summarizer agent seed
- 6 reporting modules: models, collector, sections, validator, summarizer, generator
- API endpoints: GET /api/reports (paginated, filterable), GET /api/reports/{id}
- Frontend hooks: useReports, useReport with TanStack Query
- Scheduler: daily (after 16:30 ET) and weekly (Saturday) report triggers
- Redis queue consumer for async report generation with retry/dedup
- 5 property-based tests (chunking, serialization, validation, accuracy, deltas)
- 109 unit/integration tests across all modules
- 6 frontend hook tests with MSW mocks
2026-05-01 22:13:09 +00:00

111 lines
4.2 KiB
Python

# Feature: trading-feedback-engine, Property 1: Chunking round-trip and size constraint
"""Property-based tests for report data chunking.
Feature: trading-feedback-engine
Tests the chunking round-trip and size constraint property from the design
specification: for any input string, splitting it into chunks with a maximum
size limit produces chunks where (a) every chunk is ≤ the size limit in
characters (for chunks that don't contain a single oversized line), (b) no
chunk is empty (except when the input itself is empty, which produces exactly
one empty chunk), and (c) concatenating all chunks in order reconstructs the
original input string.
"""
from __future__ import annotations
from hypothesis import given, settings
from hypothesis import strategies as st
from services.reporting.summarizer import chunk_data
# ---------------------------------------------------------------------------
# Property 1: Chunking Round-Trip and Size Constraint
# Validates: Requirements 2.2
# ---------------------------------------------------------------------------
@given(
text=st.text(),
max_chars=st.integers(min_value=1, max_value=10000),
)
@settings(max_examples=100)
def test_chunk_data_round_trip(text: str, max_chars: int) -> None:
"""**Validates: Requirements 2.2**
For any input string and any max_chars ≥ 1, concatenating all chunks
produced by chunk_data SHALL reconstruct the original input string
exactly (round-trip property).
"""
chunks = chunk_data(text, max_chars)
reconstructed = "".join(chunks)
assert reconstructed == text, (
f"Round-trip failed: concatenation of {len(chunks)} chunks does not "
f"equal original input.\n"
f" original length: {len(text)}\n"
f" reconstructed length: {len(reconstructed)}\n"
f" max_chars: {max_chars}"
)
@given(
text=st.text(),
max_chars=st.integers(min_value=1, max_value=10000),
)
@settings(max_examples=100)
def test_chunk_data_no_empty_chunks(text: str, max_chars: int) -> None:
"""**Validates: Requirements 2.2**
For any input string and any max_chars ≥ 1, chunk_data SHALL produce
no empty chunks — except when the input itself is empty, in which case
it SHALL produce exactly one empty chunk.
"""
chunks = chunk_data(text, max_chars)
if text == "":
assert chunks == [""], (
f"Empty input should produce exactly [''], got {chunks!r}"
)
else:
for i, chunk in enumerate(chunks):
assert chunk != "", (
f"Chunk {i} is empty for non-empty input.\n"
f" input length: {len(text)}\n"
f" max_chars: {max_chars}\n"
f" total chunks: {len(chunks)}"
)
@given(
text=st.text(),
max_chars=st.integers(min_value=1, max_value=10000),
)
@settings(max_examples=100)
def test_chunk_data_size_constraint(text: str, max_chars: int) -> None:
"""**Validates: Requirements 2.2**
For any input string and any max_chars ≥ 1, every chunk produced by
chunk_data SHALL be ≤ max_chars in length — UNLESS the chunk contains
a single line that by itself exceeds max_chars (since chunk_data never
breaks mid-line, such a line is emitted as its own chunk).
A chunk is considered "oversized due to a single long line" when it
consists of exactly one segment (a line with its trailing newline, or
the final line without one) whose length exceeds max_chars.
"""
chunks = chunk_data(text, max_chars)
for i, chunk in enumerate(chunks):
if len(chunk) > max_chars:
# This chunk exceeds the limit. It must be because it contains
# a single line that is itself longer than max_chars.
# A single-segment chunk has at most one newline (at the end).
lines_in_chunk = chunk.split("\n")
# If the chunk ends with \n, split produces a trailing empty string
non_empty_lines = [ln for ln in lines_in_chunk if ln]
assert len(non_empty_lines) <= 1, (
f"Chunk {i} exceeds max_chars={max_chars} "
f"(len={len(chunk)}) but contains multiple non-empty lines, "
f"which should not happen.\n"
f" lines: {non_empty_lines!r}"
)