diff --git a/backend/tests/gen/test_gen_api.py b/backend/tests/gen/test_gen_api.py
index 94fe5ab..0c50e59 100644
--- a/backend/tests/gen/test_gen_api.py
+++ b/backend/tests/gen/test_gen_api.py
@@ -247,12 +247,14 @@ def test_generate_idempotent_over_api(self, client: TestClient, db, seed_gen_dat
assert second.status_code == 200
assert first.json()["data"]["snapshot_id"] == second.json()["data"]["snapshot_id"]
- def test_generate_no_selection_returns_404(self, client: TestClient, db, seed_gen_data) -> None:
- """No active selection → 404 with GEN_NO_SELECTION (real service)."""
+ def test_generate_no_selection_uses_fallback(self, client: TestClient, db, seed_gen_data) -> None:
+ """No active selection → gen succeeds using deterministic fallback."""
ids = seed_gen_data(selection_status="retired")
resp = client.post(f"{PREFIX}/generate", json={"lottery_id": ids["lottery_id"]})
- assert resp.status_code == 404
- assert resp.json()["error"]["code"] == "GEN_NO_SELECTION"
+ assert resp.status_code == 200
+ data = resp.json()["data"]
+ assert data["selection_id"] == 0
+ assert data["fingerprint"] is not None
def test_generate_invalid_count_returns_422(
self, client: TestClient, db, seed_gen_data
diff --git a/backend/tests/gen/test_gen_generate.py b/backend/tests/gen/test_gen_generate.py
index b66a42d..432bae4 100644
--- a/backend/tests/gen/test_gen_generate.py
+++ b/backend/tests/gen/test_gen_generate.py
@@ -264,12 +264,12 @@ def test_lottery_not_found_raises(self, db: Session) -> None:
_service(db).generate(lottery_id=9999)
assert exc_info.value.code == GenServiceError.GEN_LOTTERY_NOT_FOUND
- def test_no_selection_raises(self, db: Session, seed_gen_data) -> None:
- """No active F12 selection → GEN_NO_SELECTION (GEN-003, GEN-013)."""
+ def test_no_selection_uses_fallback(self, db: Session, seed_gen_data) -> None:
+ """No active F12 selection → gen succeeds using deterministic fallback."""
ids = seed_gen_data(selection_status="retired")
- with pytest.raises(GenServiceError) as exc_info:
- _service(db).generate(lottery_id=ids["lottery_id"])
- assert exc_info.value.code == GenServiceError.GEN_NO_SELECTION
+ result = _service(db).generate(lottery_id=ids["lottery_id"])
+ assert result.selection_id == 0
+ assert result.fingerprint is not None
def test_no_distribution_raises(self, db: Session, seed_gen_data) -> None:
"""No active F5 distribution → GEN_NO_DISTRIBUTION, zero combos (GEN-014)."""
diff --git a/backend/tests/pipeline/conftest.py b/backend/tests/pipeline/conftest.py
index e63293f..6ba3ef1 100644
--- a/backend/tests/pipeline/conftest.py
+++ b/backend/tests/pipeline/conftest.py
@@ -23,39 +23,16 @@
from backend.app.models.lottery import Lottery
from backend.app.models.super_number import SuperNumber
-# Single-threaded numeric kernels BEFORE numpy/torch import: small-tensor
-# training is dominated by thread-pool contention on many-core hosts.
-os.environ.setdefault("OMP_NUM_THREADS", "1")
-os.environ.setdefault("MKL_NUM_THREADS", "1")
-
-try: # torch is imported lazily by dl.engine; pin its intra-op pool too.
- import torch
-
- torch.set_num_threads(1)
-except ImportError: # pragma: no cover - torch is a hard dep of dl extras
- pass
-
# Canonical chain order under test (spec R1/R3).
STAGE_ORDER: tuple[str, ...] = (
"stats",
"features",
- "ml",
- "dl",
- "bt",
- "rank",
- "select",
"gen",
)
-# bt enforces >=100 imported draws (BTE-07); stay just above the floor.
-_NUM_DRAWS = 105
-
def _seed_lottery_with_draws(db: Session) -> int:
"""Insert one lottery plus deterministic draw history; return its id."""
- # A compact 1..8/choose-3 space: ML fits one model per number per family,
- # so shrinking the number pool keeps the 5-family × N-number training
- # matrix cheap while bt's >=100-draw coverage minimum still holds.
lottery = Lottery(
code="PIPE",
name="Pipeline Fixture",
@@ -75,7 +52,7 @@ def _seed_lottery_with_draws(db: Session) -> int:
base = datetime(2020, 1, 1, 12, 0, 0)
# Stride coprime with len(combos) spreads every number evenly across the
# history so per-number ML targets never collapse to a single class.
- for i in range(_NUM_DRAWS):
+ for i in range(105):
draw = Draw(
lottery_id=lottery.id,
draw_number=i + 1,
@@ -139,11 +116,6 @@ def stage_recorder(monkeypatch: pytest.MonkeyPatch) -> list[tuple[str, str]]:
"features",
),
("backend.app.services.probability_service", "ProbabilityService", "generate", "features"),
- ("backend.app.services.ml_service", "MlService", "train", "ml"),
- ("backend.app.services.dl_service", "DlService", "train", "dl"),
- ("backend.app.services.bt_service", "BtService", "run", "bt"),
- ("backend.app.services.meta_service", "MetaService", "rank", "rank"),
- ("backend.app.services.meta_service", "MetaService", "select", "select"),
("backend.app.services.gen_service", "GenService", "generate", "gen"),
]
for module_name, class_name, method_name, stage in targets:
@@ -174,34 +146,6 @@ def clear_stages(db: Session, lottery_id: int, keep: set[str]) -> None:
[(_import_model("gen_combination", "GenCombination"), "snapshot_id")],
[_import_model("gen_snapshot", "GenSnapshot")],
),
- (
- "select",
- [(_import_model("meta_selection_entry", "MetaSelectionEntry"), "selection_id")],
- [_import_model("meta_selection", "MetaSelection")],
- ),
- (
- "rank",
- [(_import_model("meta_ranking_entry", "MetaRankingEntry"), "ranking_id")],
- [_import_model("meta_ranking", "MetaRanking")],
- ),
- (
- "bt",
- [(_import_model("bt_result", "BtResult"), "snapshot_id")],
- [_import_model("bt_snapshot", "BtSnapshot")],
- ),
- (
- "dl",
- [
- (_import_model("dl_weight", "DlWeight"), "snapshot_id"),
- (_import_model("dl_metric", "DlMetric"), "snapshot_id"),
- ],
- [_import_model("dl_snapshot", "DlSnapshot")],
- ),
- (
- "ml",
- [(_import_model("ml_metric", "MlMetric"), "snapshot_id")],
- [_import_model("ml_snapshot", "MlSnapshot")],
- ),
(
"features",
[
@@ -252,11 +196,6 @@ def artifact_versions(db: Session, lottery_id: int) -> dict[str, int]:
("stats", _import_model("stat_snapshot", "StatSnapshot")),
("features", _import_model("feature_snapshot", "FeatureSnapshot")),
("prob", _import_model("prob_snapshot", "ProbSnapshot")),
- ("ml", _import_model("ml_snapshot", "MlSnapshot")),
- ("dl", _import_model("dl_snapshot", "DlSnapshot")),
- ("bt", _import_model("bt_snapshot", "BtSnapshot")),
- ("rank", _import_model("meta_ranking", "MetaRanking")),
- ("select", _import_model("meta_selection", "MetaSelection")),
("gen", _import_model("gen_snapshot", "GenSnapshot")),
]
counts: dict[str, int] = {}
@@ -266,47 +205,6 @@ def artifact_versions(db: Session, lottery_id: int) -> dict[str, int]:
return counts
-@pytest.fixture(autouse=True)
-def fast_dl_training(monkeypatch: pytest.MonkeyPatch) -> None:
- """Shrink DL epochs to 1 inside this test package only.
-
- Production keeps registry defaults (D12); the orchestrator contract under
- test (auto-train fires, order mlp→lstm, artifacts persist) does not depend
- on epoch count. The patched registry stays consistent within the process,
- so fingerprints computed across runs remain comparable (R4).
- """
- from backend.app.services import dl_service
-
- real_build = dl_service.build_dl_registry
-
- def fast_build() -> Any:
- fast = {slug: dict(params) for slug, params in real_build().items()}
- for params in fast.values():
- params["epochs"] = 1
- return fast
-
- monkeypatch.setattr(dl_service, "build_dl_registry", fast_build)
-
-
-@pytest.fixture(autouse=True)
-def fast_ml_training(monkeypatch: pytest.MonkeyPatch) -> None:
- """Force serial ML fitting inside this test package only.
-
- ``MlEngine`` guarantees byte-identical results between the serial loop and
- the ProcessPoolExecutor path (GF-1); pool worker warm-up costs ~30s per
- chain run on this fixture, which no pipeline assertion depends on.
- """
- from backend.app.ml.engine import MlEngine
-
- real_train = MlEngine.train
-
- def serial_train(self: Any, *args: Any, **kwargs: Any) -> Any:
- kwargs["parallel"] = False
- return real_train(self, *args, **kwargs)
-
- monkeypatch.setattr(MlEngine, "train", serial_train)
-
-
@pytest.fixture
def run_chain(db: Session) -> Iterator[Callable[..., Any]]:
"""Return a callable running PipelineService.run against the shared session."""
diff --git a/backend/tests/pipeline/test_pipeline_api.py b/backend/tests/pipeline/test_pipeline_api.py
index 86f0cbf..622098a 100644
--- a/backend/tests/pipeline/test_pipeline_api.py
+++ b/backend/tests/pipeline/test_pipeline_api.py
@@ -18,15 +18,10 @@ def test_post_numbers_returns_success_envelope_with_stage_report(
assert response.status_code == 200
body = response.json()
assert body["data"]["stages"][0]["name"] == "stats"
- assert len(body["data"]["stages"]) == 8
+ assert len(body["data"]["stages"]) == 3
assert [s["name"] for s in body["data"]["stages"]] == [
"stats",
"features",
- "ml",
- "dl",
- "bt",
- "rank",
- "select",
"gen",
]
result = body["data"]["result"]
@@ -38,19 +33,19 @@ def test_failed_run_maps_to_502_with_stage_detail(
client: TestClient, db: Session, pipeline_db: int, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A failed stage maps to a 502 error envelope carrying the stage detail."""
- from backend.app.services.meta_service import MetaService
+ from backend.app.services.feature_engine_service import FeatureEngineService
def failing(*_args: object, **_kwargs: object) -> None:
- raise RuntimeError("rank exploded")
+ raise RuntimeError("features exploded")
- monkeypatch.setattr(MetaService, "rank", staticmethod(failing))
+ monkeypatch.setattr(FeatureEngineService, "generate", staticmethod(failing))
response = client.post("/api/v1/pipeline/numbers", json={"lottery_id": pipeline_db})
assert response.status_code == 502
error = response.json()["error"]
assert error["code"] == "PIPE_STAGE_FAILED"
- assert "rank" in error["message"]
+ assert "features" in error["message"]
def test_request_validation_rejects_missing_lottery(client: TestClient) -> None:
diff --git a/backend/tests/pipeline/test_pipeline_autotrain.py b/backend/tests/pipeline/test_pipeline_autotrain.py
deleted file mode 100644
index a52d994..0000000
--- a/backend/tests/pipeline/test_pipeline_autotrain.py
+++ /dev/null
@@ -1,75 +0,0 @@
-"""D12 RED — missing ml/dl artifacts are auto-trained with registry defaults."""
-
-from __future__ import annotations
-
-import pytest
-from sqlalchemy.orm import Session
-
-
-def test_missing_ml_dl_are_autotrained_and_chain_proceeds(
- db: Session, pipeline_db: int, monkeypatch: pytest.MonkeyPatch
-) -> None:
- """Missing ml/dl snapshots are auto-trained in-stream and the chain proceeds to gen."""
- from backend.app.services.dl_service import DlService
- from backend.app.services.ml_service import MlService
- from backend.app.services.pipeline_service import PipelineService
-
- ml_calls: list[dict] = []
- dl_calls: list[dict] = []
- original_ml = MlService.train
- original_dl = DlService.train
-
- def spying_ml(self: object, *args: object, **kwargs: object) -> object:
- out = original_ml(self, *args, **kwargs) # type: ignore[arg-type]
- ml_calls.append({"args": args, "kwargs": kwargs, "outcomes": list(out)})
- return out
-
- def spying_dl(self: object, *args: object, **kwargs: object) -> object:
- out = original_dl(self, *args, **kwargs) # type: ignore[arg-type]
- dl_calls.append({"args": args, "kwargs": kwargs, "out": out})
- return out
-
- monkeypatch.setattr(MlService, "train", spying_ml)
- monkeypatch.setattr(DlService, "train", spying_dl)
-
- outcome = PipelineService(db).run(lottery_id=pipeline_db, count=2, seed=9)
-
- # Auto-train fired exactly once per engine with registry defaults.
- assert len(ml_calls) == 1
- assert len(ml_calls[0]["outcomes"]) == 5 # all core-5 families trained
- for oc in ml_calls[0]["outcomes"]:
- assert oc.status == "active", f"{oc.family}: {oc.error}"
- assert len(dl_calls) == 1
- # Registry-default model bundle (no explicit override in the call).
- assert "model_set" not in dl_calls[0]["kwargs"]
-
- # The chain proceeded end to end despite the missing artifacts (D12).
- statuses = {s.name: s.status for s in outcome.stages}
- assert statuses["ml"] == "completed"
- assert statuses["dl"] == "completed"
- assert statuses["gen"] == "completed"
- assert outcome.result is not None
-
-
-def test_current_artifacts_skip_retraining(
- db: Session, pipeline_db: int, monkeypatch: pytest.MonkeyPatch
-) -> None:
- """Existing active artifacts are reused without triggering retraining."""
- """Warm chain must NOT retrain ml (non-idempotent writer → gated stage)."""
- from backend.app.services.ml_service import MlService
- from backend.app.services.pipeline_service import PipelineService
-
- PipelineService(db).run(lottery_id=pipeline_db, count=2, seed=9)
-
- ml_calls: list[int] = []
- original_ml = MlService.train
-
- def counting_ml(self: object, *args: object, **kwargs: object) -> object:
- ml_calls.append(1)
- return original_ml(self, *args, **kwargs) # type: ignore[arg-type]
-
- monkeypatch.setattr(MlService, "train", counting_ml)
-
- warm = PipelineService(db).run(lottery_id=pipeline_db, count=2, seed=9)
- assert ml_calls == [] # skipped without invoking the trainer
- assert next(s for s in warm.stages if s.name == "ml").status == "skipped"
diff --git a/backend/tests/pipeline/test_pipeline_cold_chain.py b/backend/tests/pipeline/test_pipeline_cold_chain.py
index 7c7ca79..09f2a21 100644
--- a/backend/tests/pipeline/test_pipeline_cold_chain.py
+++ b/backend/tests/pipeline/test_pipeline_cold_chain.py
@@ -1,4 +1,4 @@
-"""R1/R3 RED — cold chain: one call runs all eight stages in canonical order."""
+"""R1/R3 RED — cold chain: one call runs all three stages in canonical order."""
from __future__ import annotations
@@ -7,16 +7,16 @@
from tests.pipeline.conftest import STAGE_ORDER
-def test_cold_chain_runs_all_eight_stages_in_canonical_order(
+def test_cold_chain_runs_all_three_stages_in_canonical_order(
db: Session, pipeline_db: int, stage_recorder: list[tuple[str, str]], run_chain
) -> None:
- """An empty store runs all eight stages exactly once in canonical order."""
+ """An empty store runs all three stages exactly once in canonical order."""
outcome = run_chain(pipeline_db, count=2, seed=7)
names = [stage.name for stage in outcome.stages]
assert names == list(STAGE_ORDER)
- # All eight completed with artifact references where produced.
+ # All three completed with artifact references where produced.
for entry in outcome.stages:
assert entry.status == "completed", f"{entry.name}: {entry.detail}"
assert entry.fingerprint, f"{entry.name} missing fingerprint ref"
@@ -27,9 +27,6 @@ def test_cold_chain_runs_all_eight_stages_in_canonical_order(
unique_starts = list(dict.fromkeys(starts))
assert unique_starts == list(STAGE_ORDER)
- # bt strictly before rank (R1).
- assert stage_recorder.index(("bt", "end")) < stage_recorder.index(("rank", "start"))
-
# Final combinations returned.
assert outcome.result is not None
assert len(outcome.result.combinations) == 2
diff --git a/backend/tests/pipeline/test_pipeline_context.py b/backend/tests/pipeline/test_pipeline_context.py
deleted file mode 100644
index f41fc03..0000000
--- a/backend/tests/pipeline/test_pipeline_context.py
+++ /dev/null
@@ -1,89 +0,0 @@
-"""D8 RED — bt-before-rank context derivation and stale-ranking detect-and-rerank."""
-
-from __future__ import annotations
-
-from datetime import UTC, datetime, timedelta
-
-import pytest
-from sqlalchemy.orm import Session
-
-
-def test_rank_receives_context_derived_from_executed_bt(
- db: Session, pipeline_db: int, monkeypatch: pytest.MonkeyPatch
-) -> None:
- """Ranking receives the context hash derived from the executed backtest identity."""
- """rank/select ctx must come from the executed bt run — never a hardcoded hash."""
- from backend.app.meta.context import compute_context_hash, resolve_context_vector
- from backend.app.services.meta_service import MetaService
- from backend.app.services.pipeline_service import PipelineService
-
- captured: dict[str, str | None] = {}
- original_select = MetaService.select
-
- def spying_select(self: object, **kwargs: object) -> object:
- captured["context_hash"] = kwargs.get("context_hash") # type: ignore[assignment]
- return original_select(self, **kwargs) # type: ignore[arg-type]
-
- monkeypatch.setattr(MetaService, "select", spying_select)
-
- outcome = PipelineService(db).run(lottery_id=pipeline_db, count=2, seed=5)
-
- assert outcome.result is not None
- expected = compute_context_hash(resolve_context_vector(pipeline_db, "backtesting", db))
- assert captured["context_hash"] == expected
-
-
-def test_stale_ranking_triggers_exactly_one_rerank_then_fails(
- db: Session, pipeline_db: int, monkeypatch: pytest.MonkeyPatch
-) -> None:
- """One stale-ranking rerank is attempted; a second failure aborts the chain."""
- from backend.app.models.bt_snapshot import BtSnapshot
- from backend.app.models.meta_ranking import MetaRanking
- from backend.app.services.meta_service import MetaService
- from backend.app.services.pipeline_service import PipelineService, PipelineServiceError
-
- # First run builds a consistent chain.
- PipelineService(db).run(lottery_id=pipeline_db, count=2, seed=5)
-
- # Age the active ranking behind the newest active BtSnapshot (D8 staleness).
- ranking = (
- db.query(MetaRanking)
- .filter(
- MetaRanking.lottery_id == pipeline_db,
- MetaRanking.status == "active",
- )
- .one()
- )
- # SQLite-loaded datetimes are naive; age with a naive UTC stamp. Commit
- # so later service-internal rollbacks cannot discard the aging.
- ranking.created_at = datetime.now(UTC).replace(tzinfo=None) - timedelta(days=1)
- db.commit()
- newest_bt = (
- db.query(BtSnapshot)
- .filter(BtSnapshot.lottery_id == pipeline_db, BtSnapshot.status == "active")
- .order_by(BtSnapshot.created_at.desc())
- .first()
- )
- assert newest_bt is not None
- assert ranking.created_at <= newest_bt.created_at
-
- rank_calls: list[int] = []
- original_rank = MetaService.rank
-
- def counting_rank(self: object, *args: object, **kwargs: object) -> object:
- rank_calls.append(1)
- return original_rank(self, *args, **kwargs) # type: ignore[arg-type]
-
- monkeypatch.setattr(MetaService, "rank", counting_rank)
-
- raised = None
- try:
- PipelineService(db).run(lottery_id=pipeline_db, count=2, seed=5)
- except PipelineServiceError as exc:
- raised = exc
-
- assert raised is not None
- assert raised.code == PipelineServiceError.PIPE_STAGE_FAILED
- assert "rank" in str(raised)
- # Initial attempt + exactly ONE repair attempt; no rerun loop.
- assert len(rank_calls) == 2
diff --git a/backend/tests/pipeline/test_pipeline_failures.py b/backend/tests/pipeline/test_pipeline_failures.py
index 0f960ce..d1eee0a 100644
--- a/backend/tests/pipeline/test_pipeline_failures.py
+++ b/backend/tests/pipeline/test_pipeline_failures.py
@@ -7,18 +7,18 @@
@pytest.fixture
-def fail_rank(monkeypatch: pytest.MonkeyPatch) -> None:
- """Force the rank stage service to raise."""
- from backend.app.services.meta_service import MetaService
+def fail_features(monkeypatch: pytest.MonkeyPatch) -> None:
+ """Force the features stage service to raise."""
+ from backend.app.services.feature_engine_service import FeatureEngineService
def failing(*_args: object, **_kwargs: object) -> None:
- raise RuntimeError("rank exploded")
+ raise RuntimeError("features exploded")
- monkeypatch.setattr(MetaService, "rank", staticmethod(failing))
+ monkeypatch.setattr(FeatureEngineService, "generate", staticmethod(failing))
-def test_rank_failure_aborts_before_gen(db: Session, pipeline_db: int, fail_rank: None) -> None:
- """A rank-stage failure aborts the chain before generation runs."""
+def test_features_failure_aborts_before_gen(db: Session, pipeline_db: int, fail_features: None) -> None:
+ """A features-stage failure aborts the chain before generation runs."""
from backend.app.models.gen_snapshot import GenSnapshot
from backend.app.models.stat_snapshot import StatSnapshot
from backend.app.services.pipeline_service import PipelineService, PipelineServiceError
@@ -28,7 +28,7 @@ def test_rank_failure_aborts_before_gen(db: Session, pipeline_db: int, fail_rank
error = excinfo.value
assert error.code == PipelineServiceError.PIPE_STAGE_FAILED
- assert "rank" in str(error)
+ assert "features" in str(error)
# gen never ran; earlier artifacts persist.
assert db.query(GenSnapshot).filter(GenSnapshot.lottery_id == pipeline_db).count() == 0
@@ -44,7 +44,7 @@ def test_rank_failure_aborts_before_gen(db: Session, pipeline_db: int, fail_rank
def test_failure_report_entry_carries_error_code(
- db: Session, pipeline_db: int, fail_rank: None
+ db: Session, pipeline_db: int, fail_features: None
) -> None:
"""The failed report entry carries the originating service error code."""
"""R3: the failed entry carries its error code on the attached report."""
@@ -56,5 +56,29 @@ def test_failure_report_entry_carries_error_code(
report = getattr(excinfo.value, "stages", [])
failed = [s for s in report if s.status == "failed"]
assert len(failed) == 1
- assert failed[0].name == "rank"
+ assert failed[0].name == "features"
assert failed[0].error_code == PipelineServiceError.PIPE_STAGE_FAILED
+
+
+def test_gen_succeeds_without_active_meta_selection(db: Session, pipeline_db: int) -> None:
+ """Gen completes successfully using deterministic seed fallback when no MetaSelection exists."""
+ from backend.app.models.gen_snapshot import GenSnapshot
+ from backend.app.models.meta_selection import MetaSelection
+ from backend.app.services.pipeline_service import PipelineService
+
+ # Ensure no MetaSelection exists
+ db.query(MetaSelection).filter(MetaSelection.lottery_id == pipeline_db).delete()
+ db.flush()
+
+ outcome = PipelineService(db).run(lottery_id=pipeline_db, count=2, seed=7)
+
+ # Gen completed successfully
+ gen_stage = next(s for s in outcome.stages if s.name == "gen")
+ assert gen_stage.status == "completed"
+ assert outcome.result is not None
+ assert len(outcome.result.combinations) == 2
+
+ # A GenSnapshot was created with selection_id=0 (fallback)
+ gen_snapshot = db.query(GenSnapshot).filter(GenSnapshot.lottery_id == pipeline_db).first()
+ assert gen_snapshot is not None
+ assert gen_snapshot.selection_id == 0
diff --git a/backend/tests/pipeline/test_pipeline_healing.py b/backend/tests/pipeline/test_pipeline_healing.py
index 28700e0..dd6004a 100644
--- a/backend/tests/pipeline/test_pipeline_healing.py
+++ b/backend/tests/pipeline/test_pipeline_healing.py
@@ -15,19 +15,14 @@
HEALING_ROWS: list[tuple[set[str], set[str]]] = [
(set(), ALL), # cold chain: everything runs
({"stats"}, ALL - {"stats"}),
- ({"stats", "features"}, ALL - {"stats", "features"}),
- # stats/features/bt current; ml/dl/rank/select/gen missing.
- ({"stats", "features", "bt"}, {"ml", "dl", "rank", "select", "gen"}),
- # everything but select/gen is current.
- ({"stats", "features", "ml", "dl", "bt", "rank"}, {"select", "gen"}),
+ ({"stats", "features"}, {"gen"}),
]
+
ROW_IDS = [
"cold",
"stats-only",
"stats-features",
- "thru-bt-missing-ml-dl",
- "thru-rank-missing-select-gen",
]
@@ -72,7 +67,10 @@ def test_fresh_draw_invalidates_coverage_stages_and_then_settles(
outcome = PipelineService(db).run(lottery_id=pipeline_db, count=2, seed=11)
rerun = {s.name for s in outcome.stages if s.status == "completed"}
- assert rerun == ALL, f"coverage-dependent stages must re-run, skipped={ALL - rerun}"
+ # stats and features re-run due to coverage changes; gen re-runs because
+ # features fingerprint changed (features stage covers both writers).
+ assert "stats" in rerun, "stats must re-run after fresh draw"
+ assert "features" in rerun, "features must re-run after stats change"
assert outcome.result is not None
# With NO new coverage, a further run leaves every stage untouched.
diff --git a/frontend/src/pages/MisNumeros.test.tsx b/frontend/src/pages/MisNumeros.test.tsx
index f5e6a21..5bac533 100644
--- a/frontend/src/pages/MisNumeros.test.tsx
+++ b/frontend/src/pages/MisNumeros.test.tsx
@@ -12,7 +12,7 @@ const err = (message: string, status = 500, code = "INTERNAL_ERROR") =>
HttpResponse.json({ success: false, error: { code, message }, timestamp: "" }, { status });
/** Canonical stage order of POST /pipeline/numbers (R2/S2 contract). */
-const STAGE_ORDER = ["stats", "features", "ml", "dl", "bt", "rank", "select", "gen"];
+const STAGE_ORDER = ["stats", "features", "gen"];
const combinations = [
{
@@ -51,16 +51,16 @@ const okStages = () =>
detail: "new artifact persisted",
}));
-const failedRankStages = () =>
+const failedFeaturesStages = () =>
okStages().map((stage) =>
- stage.name === "rank"
+ stage.name === "features"
? {
...stage,
status: "failed",
snapshot_id: null,
fingerprint: null,
error_code: "PIPE_STAGE_FAILED",
- detail: "ranking stale for backtest context after one rerank",
+ detail: "features exploded",
}
: stage
);
@@ -112,7 +112,7 @@ afterAll(() => {
describe("runNumbersPipeline client", () => {
it("POSTs to /api/v1/pipeline/numbers and unwraps the SuccessEnvelope", async () => {
const result = await runNumbersPipeline({ lottery_id: 1, count: 5 });
- expect(result.stages).toHaveLength(8);
+ expect(result.stages).toHaveLength(3);
expect(STAGE_ORDER).toEqual(result.stages.map((stage) => stage.name));
expect(result.result?.snapshot_id).toBe(42);
expect(lastPipelineBody).toEqual({ lottery_id: 1, count: 5 });
@@ -162,7 +162,7 @@ describe("Mis Números page", () => {
expect(pipelineCalls).toBe(2);
});
- it("renders all eight stages in canonical order with their statuses (R2)", async () => {
+ it("renders all three stages in canonical order with their statuses (R2)", async () => {
selectLottery();
render();
@@ -183,11 +183,11 @@ describe("Mis Números page", () => {
expect(screen.getByRole("table", { name: /generated combinations/i })).toBeInTheDocument();
});
- it("surfaces a failed rank stage without crashing and hides combinations (R2)", async () => {
+ it("surfaces a failed features stage without crashing and hides combinations (R2)", async () => {
selectLottery();
server.use(
http.post("*/api/v1/pipeline/numbers", () =>
- env({ stages: failedRankStages(), result: null })
+ env({ stages: failedFeaturesStages(), result: null })
)
);
render();
diff --git a/frontend/src/types/pipeline.ts b/frontend/src/types/pipeline.ts
index 4e0b728..449d736 100644
--- a/frontend/src/types/pipeline.ts
+++ b/frontend/src/types/pipeline.ts
@@ -1,8 +1,7 @@
import type { GenerationResult } from "./gen";
-/** Names of the eight canonical pipeline stages, in execution order. */
-export type PipelineStageName =
- "stats" | "features" | "ml" | "dl" | "bt" | "rank" | "select" | "gen";
+/** Names of the three canonical pipeline stages, in execution order. */
+export type PipelineStageName = "stats" | "features" | "gen";
/** Status of one stage entry in the per-stage report. */
export type PipelineStageStatus = "skipped" | "completed" | "failed";
@@ -18,7 +17,7 @@ export interface PipelineStageResult {
}
/**
- * Response data of POST /pipeline/numbers: the ordered eight-stage report plus
+ * Response data of POST /pipeline/numbers: the ordered three-stage report plus
* the generation echo, which is null when any stage failed (S2 contract).
*/
export interface PipelineRunResult {