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
- 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
111 lines
4.2 KiB
Python
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}"
|
|
)
|