From b900f9c7e3fcce1678b807fa28f7a7e7ce3f60cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?FabioLeit=C3=A3o?= Date: Mon, 28 Sep 2026 14:10:47 -0300 Subject: [PATCH 1/3] fix(workflow): keep RC connector extras and fail sentinel on missing extras The baremetal host smoke ran `uv sync --extra compressed`. uv sync is exact, so it pruned pymongo (extra nosql). The Mongo connector then stored reason=unreachable, and the RC sentinel skipped the optional Mongo rule on the closed probe port and ended SENTINEL_OK. - Smoke prepare syncs compressed plus the extras the --bench-config targets need (scripts/rc_bench_extras.py list, from optional_extra_for_target), then `verify` fails the prepare when a distribution of those extras is missing. - MongoDB and Redis connectors record missing_optional_dependency when the nosql extra is absent, like SMB and WebDAV already do. - The sentinel fails on any missing_optional_dependency in the latest session before optional probes run. Real runs, RC v2 config copy with sqlite under /tmp, port 27018 closed: pymongo blocked -> scan_failures missing_optional_dependency -> SENTINEL_FAIL (exit 1); pymongo present -> SKIP -> SENTINEL_OK. Refs DataBoar/maestro#91 Refs DataBoar/maestro#92 --- CHANGELOG.md | 6 + connectors/mongodb_connector.py | 11 ++ connectors/redis_connector.py | 11 ++ scripts/benchmark_rc_sentinel_check.py | 37 +++++- scripts/lab-completao-host-smoke.sh | 44 ++++++- scripts/rc_bench_extras.py | 153 ++++++++++++++++++++++ tests/test_benchmark_rc_sentinel_check.py | 121 +++++++++++++++++ tests/test_rc_bench_extras.py | 116 ++++++++++++++++ 8 files changed, 495 insertions(+), 4 deletions(-) create mode 100644 scripts/rc_bench_extras.py create mode 100644 tests/test_rc_bench_extras.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f6be2f692..c8d671851 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,12 @@ Empty while the working tree carries **`1.8.0-rc`** (see section below). - **`filesystem_credit_card`** in `benchmark-rc-v2.sentinel.yaml` and `benchmark-rc-v3.sentinel.yaml` fails the post-smoke checker when `CREDIT_CARD` is absent from `filesystem_findings`. Corpus file: `tests/data/homelab_synthetic/synthetic_pan_luhn.txt` (Visa test PAN). +### RC smoke extras and missing-extra failures (maestro#91) + +- **Host smoke prepare** syncs `--extra compressed` plus the extras the `--bench-config` targets need (`scripts/rc_bench_extras.py list`, from `optional_extra_for_target`; RC v2 → `nosql`, v3 → `mysql nosql postgres shares`). `uv sync` no longer prunes `pymongo` before the scan. `rc_bench_extras.py verify` fails the prepare when a distribution of those extras is missing. +- **MongoDB / Redis connectors** record `scan_failures.reason=missing_optional_dependency` (not `unreachable`) when the `nosql` extra is absent. +- **RC sentinel** fails on any `missing_optional_dependency` in the latest session before optional probes run, so a closed lab port cannot turn a missing extra into `SKIP` + `SENTINEL_OK`. + ### Trust anchor (#1992) - **Key rotation** is accepted only when both embedded anchors (Ed25519 and ML-DSA-65) sign `data-boar/license-key-rotation/v1` plus the epoch. A raw `DATA_BOAR_LICENSE_PUBLIC_KEY_*`, `DATA_BOAR_LICENSE_MLDSA_PUBLIC_KEY_*`, or YAML public-key path fails closed (`untrusted_key_override`). Hybrid `dbmldsa_sig` still verifies, against the packaged or rotated ML-DSA key. diff --git a/connectors/mongodb_connector.py b/connectors/mongodb_connector.py index 129aa50b5..f75ae3f4a 100644 --- a/connectors/mongodb_connector.py +++ b/connectors/mongodb_connector.py @@ -135,6 +135,17 @@ def run(self) -> None: target_name = self.config.get("name", "mongodb") audit_name = audit_log_target_label(self.config, default="mongodb") + if not _MONGO_AVAILABLE: + from core.extras_runtime import missing_optional_message + + # Not "unreachable": the RC sentinel must not read a missing extra as + # a lab target that is down (maestro#91). + self.db_manager.save_failure( + target_name, + "missing_optional_dependency", + missing_optional_message(subject="MongoDB connector", extra="nosql"), + ) + return try: self.connect() except Exception as e: diff --git a/connectors/redis_connector.py b/connectors/redis_connector.py index 69baee67b..27062c0b7 100644 --- a/connectors/redis_connector.py +++ b/connectors/redis_connector.py @@ -150,6 +150,17 @@ def run(self) -> None: target_name = self.config.get("name", "redis") audit_name = audit_log_target_label(self.config, default="redis") + if not _REDIS_AVAILABLE: + from core.extras_runtime import missing_optional_message + + # Not "unreachable": the RC sentinel must not read a missing extra as + # a lab target that is down (maestro#91). + self.db_manager.save_failure( + target_name, + "missing_optional_dependency", + missing_optional_message(subject="Redis connector", extra="nosql"), + ) + return try: self.connect() except Exception as e: diff --git a/scripts/benchmark_rc_sentinel_check.py b/scripts/benchmark_rc_sentinel_check.py index 5a4713a8f..3ae254218 100644 --- a/scripts/benchmark_rc_sentinel_check.py +++ b/scripts/benchmark_rc_sentinel_check.py @@ -3,9 +3,11 @@ Validates SQLite findings for required patterns (min_count / max_count), optional connector probes, forbidden pattern substrings, and static negative SSRF/auth cases. +Any ``scan_failures.reason=missing_optional_dependency`` in the session fails before +optional probes run, so a closed lab port cannot mask a missing extra (maestro#91). Exit 0 = pass, 1 = sentinel fail, 2 = usage/config error. -Refs: maestro#82, maestro#86, data-boar#1980 (gap report §E), data-boar#1985. +Refs: maestro#82, maestro#86, maestro#91, data-boar#1980 (gap report §E), data-boar#1985. """ from __future__ import annotations @@ -259,6 +261,33 @@ def _count_scan_failures( return int(row[0]) if row else 0 +# A connector whose optional extra is absent is a tool/prepare defect, never a lab +# outage: it fails the sentinel even when the optional probe port is closed. +_MISSING_EXTRA_REASON = "missing_optional_dependency" + + +def _missing_extra_failures( + conn: sqlite3.Connection, session_id: str +) -> list[tuple[str, str]]: + # Minimal fixture sqlite files may lack scan_failures or its details column; + # the app schema (core.database.ScanFailure) always has both. + cols = {str(r[1]) for r in conn.execute("PRAGMA table_info(scan_failures)")} + if "reason" not in cols: + return [] + if "details" in cols: + query = ( + "SELECT target_name, details FROM scan_failures " + "WHERE session_id = ? AND reason = ? ORDER BY target_name" + ) + else: + query = ( + "SELECT target_name, '' FROM scan_failures " + "WHERE session_id = ? AND reason = ? ORDER BY target_name" + ) + rows = conn.execute(query, (session_id, _MISSING_EXTRA_REASON)).fetchall() + return [(str(r[0] or ""), str(r[1] or "")) for r in rows] + + def _latest_session_id(conn: sqlite3.Connection) -> tuple[str | None, str | None]: """Latest scan by started_at; only status=completed is acceptable evidence.""" row = conn.execute( @@ -292,6 +321,12 @@ def _check_findings_sentinel( if session_err: return [session_err] + for target_name, details in _missing_extra_failures(conn, session_id): + errors.append( + f"scan_failures reason={_MISSING_EXTRA_REASON} target={target_name!r}: " + f"optional extra absent in the scan venv, not a lab outage ({details[:200]})" + ) + total = sum( _count_rows(conn, t, session_id) for t in ( diff --git a/scripts/lab-completao-host-smoke.sh b/scripts/lab-completao-host-smoke.sh index d35de6331..8d1d28669 100644 --- a/scripts/lab-completao-host-smoke.sh +++ b/scripts/lab-completao-host-smoke.sh @@ -189,17 +189,55 @@ _lc_install_prebuilt_rust_wheel() { return 1 } +# Print the optional extras the bench config's targets need (one per line; empty when +# there is no bench config or helper). Needs an existing venv (PyYAML); non-zero exit +# means the caller must bootstrap first. +_lc_rc_bench_extras() { + local cfg="${LC_BENCH_CONFIG:-tests/config/benchmark-rc-v3.yaml}" + if [[ ! -f "$LC_REPO_ROOT/$cfg" || ! -f "$LC_REPO_ROOT/scripts/rc_bench_extras.py" ]]; then + return 0 + fi + (cd "$LC_REPO_ROOT" && uv run --no-sync python scripts/rc_bench_extras.py list --config "$cfg") +} + _lc_prepare_baremetal_runtime() { if [[ ! -f "$LC_REPO_ROOT/pyproject.toml" ]] || ! _lc_cmd uv; then return 1 fi + # `uv sync` is exact: every extra not named here is pruned from the venv. # --extra compressed pulls py7zr so .7z archives are scannable in the completao - # flow; without it `uv sync` prunes py7zr and .7z stays archive_unsupported (#931). - echo "Preparing baremetal venv (uv sync --extra compressed)..." - if ! (cd "$LC_REPO_ROOT" && uv sync --extra compressed); then + # flow (#931). The bench config's targets add theirs (mongodb -> nosql, smb -> shares, + # ...) so a connector extra is never pruned before the scan (maestro#91). This runs + # for both the engine import probe and the RC scan with the same config default as + # CONFIG_RC, so the second sync does not prune what the first installed. + local cfg="${LC_BENCH_CONFIG:-tests/config/benchmark-rc-v3.yaml}" + local extras="" extra="" + local -a extra_args=(--extra compressed) + if ! extras="$(_lc_rc_bench_extras 2>/dev/null)"; then + echo "Bootstrapping baremetal venv (uv sync --extra compressed)..." + if ! (cd "$LC_REPO_ROOT" && uv sync --extra compressed); then + echo "uv sync: FAILED" + return 1 + fi + if ! extras="$(_lc_rc_bench_extras)"; then + echo "rc_bench_extras list: FAILED ($cfg)" + return 1 + fi + fi + while IFS= read -r extra; do + [[ -n "$extra" ]] && extra_args+=(--extra "$extra") + done <<<"$extras" + echo "Preparing baremetal venv (uv sync ${extra_args[*]})..." + if ! (cd "$LC_REPO_ROOT" && uv sync "${extra_args[@]}"); then echo "uv sync: FAILED" return 1 fi + if [[ -f "$LC_REPO_ROOT/$cfg" && -f "$LC_REPO_ROOT/scripts/rc_bench_extras.py" ]]; then + if ! (cd "$LC_REPO_ROOT" && uv run --no-sync python scripts/rc_bench_extras.py verify --config "$cfg" --extra compressed); then + echo "rc_bench_extras verify: FAILED (connector extras missing after uv sync; $cfg)" + return 1 + fi + fi # Prefer the prebuilt Build-Once wheel (no Rust toolchain per host, #937); only # build from source via maturin when no usable wheel is available on this host. if _lc_install_prebuilt_rust_wheel; then diff --git a/scripts/rc_bench_extras.py b/scripts/rc_bench_extras.py new file mode 100644 index 000000000..7fbc6981e --- /dev/null +++ b/scripts/rc_bench_extras.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +"""Optional extras a Maestro RC bench config needs in the baremetal scan venv. + +``uv sync`` is exact: any extra not named on the command line is pruned. The host +smoke used a fixed ``--extra compressed`` and lost ``pymongo`` (extra ``nosql``), +so the Mongo target failed while the sentinel read it as a lab outage (maestro#91). + +``list`` — print the extras the config's targets need, one per line, from + ``core.extras_runtime.optional_extra_for_target`` (type/driver map). +``verify`` — after the sync, confirm every distribution declared by those extras + (plus any ``--extra``) in ``pyproject.toml`` is installed. Exit 1 with + ``EXTRA_MISSING`` lines otherwise, so the prepare fails loud before + the scan instead of recording connector failures. + +Exit 0 = ok, 1 = missing distribution, 2 = usage/config error. +""" + +from __future__ import annotations + +import argparse +import importlib.metadata +import re +import sys +import tomllib +from pathlib import Path +from typing import Any + +import yaml + +_REPO_ROOT = Path(__file__).resolve().parents[1] +if str(_REPO_ROOT) not in sys.path: + sys.path.insert(0, str(_REPO_ROOT)) + +from core.extras_runtime import optional_extra_for_target # noqa: E402 + +_PROJECT_NAME = "data-boar" +_REQ_NAME = re.compile(r"^\s*([A-Za-z0-9][A-Za-z0-9._-]*)(?:\[([^\]]*)\])?") + + +def _normalize(name: str) -> str: + return re.sub(r"[-_.]+", "-", name).lower() + + +def extras_for_config(config: dict[str, Any]) -> list[str]: + found: set[str] = set() + for target in config.get("targets") or []: + if isinstance(target, dict): + extra = optional_extra_for_target(target) + if extra: + found.add(extra) + return sorted(found) + + +def _optional_dependencies(pyproject: Path) -> dict[str, list[str]]: + data = tomllib.loads(pyproject.read_text(encoding="utf-8")) + return dict((data.get("project") or {}).get("optional-dependencies") or {}) + + +def distributions_for_extras( + extras: list[str], optional_deps: dict[str, list[str]] +) -> dict[str, list[str]]: + """Map extra -> distribution names; self-references (``data-boar[x]``) expand.""" + out: dict[str, list[str]] = {} + for extra in extras: + if extra not in optional_deps: + raise KeyError(f"extra {extra!r} not declared in pyproject.toml") + dists: list[str] = [] + pending = [extra] + seen: set[str] = set() + while pending: + current = pending.pop() + if current in seen: + continue + seen.add(current) + for req in optional_deps.get(current) or []: + m = _REQ_NAME.match(req) + if not m: + continue + name, sub = m.group(1), m.group(2) + if _normalize(name) == _PROJECT_NAME: + pending.extend( + s.strip() for s in (sub or "").split(",") if s.strip() + ) + elif name not in dists: + dists.append(name) + out[extra] = dists + return out + + +def missing_distributions(dist_map: dict[str, list[str]]) -> list[tuple[str, str]]: + missing: list[tuple[str, str]] = [] + for extra, dists in dist_map.items(): + for dist in dists: + try: + importlib.metadata.distribution(dist) + except importlib.metadata.PackageNotFoundError: + missing.append((extra, dist)) + return missing + + +def _load_config(path: Path) -> dict[str, Any]: + data = yaml.safe_load(path.read_text(encoding="utf-8")) + if not isinstance(data, dict): + raise ValueError(f"expected mapping in {path}") + return data + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0]) + parser.add_argument("command", choices=("list", "verify")) + parser.add_argument("--config", required=True, type=Path) + parser.add_argument( + "--extra", + action="append", + default=[], + help="Extra synced regardless of the config (repeatable; verify only).", + ) + parser.add_argument("--pyproject", type=Path, default=_REPO_ROOT / "pyproject.toml") + args = parser.parse_args(argv) + + config_path = args.config if args.config.is_absolute() else _REPO_ROOT / args.config + try: + extras = extras_for_config(_load_config(config_path)) + except (OSError, ValueError, yaml.YAMLError) as exc: + print( + f"rc_bench_extras: cannot read config {config_path}: {exc}", file=sys.stderr + ) + return 2 + + if args.command == "list": + for extra in extras: + print(extra) + return 0 + + wanted = sorted(set(extras) | set(args.extra)) + try: + dist_map = distributions_for_extras( + wanted, _optional_dependencies(args.pyproject) + ) + except (OSError, KeyError, tomllib.TOMLDecodeError) as exc: + print(f"rc_bench_extras: {exc}", file=sys.stderr) + return 2 + missing = missing_distributions(dist_map) + for extra, dist in missing: + print(f"EXTRA_MISSING extra={extra} dist={dist}", file=sys.stderr) + if missing: + return 1 + print(f"rc_bench_extras: OK ({', '.join(wanted) or 'none'})") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_benchmark_rc_sentinel_check.py b/tests/test_benchmark_rc_sentinel_check.py index bd7b53255..a2f7e4e4c 100644 --- a/tests/test_benchmark_rc_sentinel_check.py +++ b/tests/test_benchmark_rc_sentinel_check.py @@ -811,3 +811,124 @@ def test_rc_sentinel_golden_luhn_pan_tab_and_nbsp_still_credit_card() -> None: assert _credit_card_in_pattern(result.get("pattern_detected")), ( f"RC profile must detect Luhn-valid PAN with {label} separator (#1978)" ) + + +def _write_mongo_failure_db(path: Path, *, session_id: str, reason: str) -> None: + """RC v2 session with filesystem evidence and one Lab_Mongo scan_failures row.""" + conn = sqlite3.connect(str(path)) + try: + conn.executescript( + """ + CREATE TABLE scan_sessions ( + session_id TEXT PRIMARY KEY, + started_at TEXT, + status TEXT + ); + CREATE TABLE filesystem_findings ( + session_id TEXT, + target_name TEXT, + pattern_detected TEXT + ); + CREATE TABLE database_findings ( + session_id TEXT, + target_name TEXT, + pattern_detected TEXT + ); + CREATE TABLE application_findings ( + session_id TEXT, + target_name TEXT, + pattern_detected TEXT + ); + CREATE TABLE scan_failures ( + session_id TEXT, + target_name TEXT, + reason TEXT, + details TEXT + ); + """ + ) + ts = datetime.now(timezone.utc).isoformat() + conn.execute( + "INSERT INTO scan_sessions VALUES (?, ?, ?)", + (session_id, ts, "completed"), + ) + for pattern in ("LGPD_CPF", "CREDIT_CARD"): + conn.execute( + "INSERT INTO filesystem_findings VALUES (?, ?, ?)", + (session_id, "Data_Soup_Synthetic", pattern), + ) + conn.execute( + "INSERT INTO scan_failures VALUES (?, ?, ?, ?)", + ( + session_id, + "Lab_Mongo_Synthetic_RC", + reason, + "MongoDB connector requires optional dependencies. " + "Install with: pip install 'data-boar[nosql]'", + ), + ) + conn.commit() + finally: + conn.close() + + +def _shipped_v2_paths(tmp_path: Path) -> tuple[Path, Path]: + cfg = tmp_path / "bench.yaml" + cfg.write_text("sqlite_path: sentinel.db\n", encoding="utf-8") + spec = Path(__file__).resolve().parent / "config" / "benchmark-rc-v2.sentinel.yaml" + return cfg, spec + + +def _mongo_port_closed(spec: str) -> bool: + return "27018" not in spec + + +def test_missing_extra_fails_even_when_mongo_probe_is_closed( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """maestro#91: a missing ``nosql`` extra must not pass as SKIP (lab down).""" + monkeypatch.setattr(sentinel_mod, "_probe_reachable", _mongo_port_closed) + cfg, spec = _shipped_v2_paths(tmp_path) + db = tmp_path / "sentinel.db" + _write_mongo_failure_db( + db, session_id="sess-91-extra", reason="missing_optional_dependency" + ) + errs = sentinel_mod._check_findings_sentinel(cfg, spec, db) + assert len(errs) == 1 + assert "reason=missing_optional_dependency" in errs[0] + assert "Lab_Mongo_Synthetic_RC" in errs[0] + assert "data-boar[nosql]" in errs[0] + + +def test_unreachable_mongo_with_closed_probe_still_skips( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A real lab outage (port closed, reason=unreachable) stays a legitimate SKIP.""" + monkeypatch.setattr(sentinel_mod, "_probe_reachable", _mongo_port_closed) + cfg, spec = _shipped_v2_paths(tmp_path) + db = tmp_path / "sentinel.db" + _write_mongo_failure_db(db, session_id="sess-91-down", reason="unreachable") + assert sentinel_mod._check_findings_sentinel(cfg, spec, db) == [] + + +def test_missing_extra_in_older_session_does_not_fail_latest( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setattr(sentinel_mod, "_probe_reachable", _mongo_port_closed) + cfg, spec = _shipped_v2_paths(tmp_path) + db = tmp_path / "sentinel.db" + _write_mongo_failure_db(db, session_id="sess-new", reason="unreachable") + conn = sqlite3.connect(str(db)) + try: + conn.execute( + "INSERT INTO scan_sessions VALUES (?, ?, ?)", + ("sess-old", "2000-01-01T00:00:00+00:00", "completed"), + ) + conn.execute( + "INSERT INTO scan_failures VALUES (?, ?, ?, ?)", + ("sess-old", "Lab_Mongo_Synthetic_RC", "missing_optional_dependency", ""), + ) + conn.commit() + finally: + conn.close() + assert sentinel_mod._check_findings_sentinel(cfg, spec, db) == [] diff --git a/tests/test_rc_bench_extras.py b/tests/test_rc_bench_extras.py new file mode 100644 index 000000000..7183a0dae --- /dev/null +++ b/tests/test_rc_bench_extras.py @@ -0,0 +1,116 @@ +"""maestro#91: RC bench extras derivation, connector failure reason, smoke wiring.""" + +from __future__ import annotations + +import importlib.metadata +from pathlib import Path +from unittest.mock import MagicMock + +import pytest +import yaml + +from connectors import mongodb_connector, redis_connector +from scripts import rc_bench_extras + +_ROOT = Path(__file__).resolve().parents[1] + + +def _config(name: str) -> dict: + return yaml.safe_load((_ROOT / "tests" / "config" / name).read_text("utf-8")) + + +def test_rc_v2_needs_nosql_for_mongo_target() -> None: + assert rc_bench_extras.extras_for_config(_config("benchmark-rc-v2.yaml")) == [ + "nosql" + ] + + +def test_rc_v3_extras_follow_target_drivers() -> None: + assert rc_bench_extras.extras_for_config(_config("benchmark-rc-v3.yaml")) == [ + "mysql", + "nosql", + "postgres", + "shares", + ] + + +def test_distributions_for_extras_reads_pyproject_and_expands_self_refs() -> None: + deps = { + "nosql": ["pymongo>=4.0", "redis>=8.1.0"], + "postgres": ["psycopg2-binary>=2.9.11"], + "sql-community": ["data-boar[postgres]"], + } + got = rc_bench_extras.distributions_for_extras(["nosql", "sql-community"], deps) + assert got == {"nosql": ["pymongo", "redis"], "sql-community": ["psycopg2-binary"]} + + +def test_distributions_for_extras_rejects_undeclared_extra() -> None: + with pytest.raises(KeyError, match="not declared"): + rc_bench_extras.distributions_for_extras(["nope"], {"nosql": []}) + + +def test_verify_reports_pruned_distribution( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + real = importlib.metadata.distribution + + def _fake(name: str): + if name == "pymongo": + raise importlib.metadata.PackageNotFoundError(name) + return real(name) + + monkeypatch.setattr(importlib.metadata, "distribution", _fake) + rc = rc_bench_extras.main( + ["verify", "--config", "tests/config/benchmark-rc-v2.yaml"] + ) + assert rc == 1 + assert "EXTRA_MISSING extra=nosql dist=pymongo" in capsys.readouterr().err + + +def test_missing_distributions_detects_absent_package() -> None: + missing = rc_bench_extras.missing_distributions( + {"x": ["definitely-not-installed-databoar-91"]} + ) + assert missing == [("x", "definitely-not-installed-databoar-91")] + + +def test_list_command_prints_one_extra_per_line( + capsys: pytest.CaptureFixture[str], +) -> None: + assert ( + rc_bench_extras.main(["list", "--config", "tests/config/benchmark-rc-v3.yaml"]) + == 0 + ) + assert capsys.readouterr().out.split() == ["mysql", "nosql", "postgres", "shares"] + + +@pytest.mark.parametrize( + ("module", "cls", "flag", "target"), + [ + (mongodb_connector, "MongoDBConnector", "_MONGO_AVAILABLE", "Lab_Mongo_RC"), + (redis_connector, "RedisConnector", "_REDIS_AVAILABLE", "Lab_Redis_RC"), + ], +) +def test_connector_without_extra_records_missing_optional_dependency( + monkeypatch: pytest.MonkeyPatch, module, cls: str, flag: str, target: str +) -> None: + """Missing ``nosql`` must not be stored as ``unreachable`` (maestro#91).""" + monkeypatch.setattr(module, flag, False) + dbm = MagicMock() + connector = getattr(module, cls)( + {"name": target, "host": "127.0.0.1", "port": 1}, + scanner=MagicMock(), + db_manager=dbm, + ) + connector.run() + dbm.save_failure.assert_called_once() + name, reason, details = dbm.save_failure.call_args.args + assert (name, reason) == (target, "missing_optional_dependency") + assert "data-boar[nosql]" in details + + +def test_host_smoke_prepare_syncs_config_extras_and_verifies() -> None: + text = (_ROOT / "scripts" / "lab-completao-host-smoke.sh").read_text("utf-8") + assert "scripts/rc_bench_extras.py list --config" in text + assert "scripts/rc_bench_extras.py verify --config" in text + assert 'uv sync "${extra_args[@]}"' in text From 5fa2bf8da0f62cdb2efe9c49b9a51437d9ba650f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?FabioLeit=C3=A3o?= Date: Mon, 28 Sep 2026 14:33:56 -0300 Subject: [PATCH 2/3] test(maestro): align synthetic DB contract with docker exec paths Confirm-TargetDbSyntheticData in maestro now uses docker exec against named lab containers (lab-postgres, lab-mariadb, lab-mongodb), not docker compose exec -T. Update the three contract assertions so the sibling-maestro gate matches Lab-MaestroCommon.ps1 again. Refs #2010 --- tests/test_maestro_scripts.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_maestro_scripts.py b/tests/test_maestro_scripts.py index 45fb72929..ecd688100 100644 --- a/tests/test_maestro_scripts.py +++ b/tests/test_maestro_scripts.py @@ -256,9 +256,9 @@ def test_confirm_target_db_synthetic_data_contract() -> None: assert "lab_customers" in common assert "lab_people" in common assert "lab_smoke_mongo" in common - assert "docker compose exec -T lab-postgres" in common - assert "docker compose exec -T lab-mariadb" in common - assert "docker-compose.mongo.yml exec -T lab-mongodb" in common + assert "docker exec lab-postgres psql" in common + assert "docker exec lab-mariadb mariadb" in common + assert "docker exec lab-mongodb mongosh" in common assert '-replace "`r", ""' in common From c0003bdf96ea5207c08832c5369a71633decca9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?FabioLeit=C3=A3o?= Date: Mon, 28 Sep 2026 15:08:54 -0300 Subject: [PATCH 3/3] test(sampling): patch _MONGO_AVAILABLE in Mongo dedup unit test CI on Python 3.12/3.14 runs without the nosql extra, so pymongo is absent and MongoDBConnector.run() returns early on the missing-extra guard (maestro#91). The dedup test stubs connect() but must assert sampling behavior; mirror test_security and test_crypto_controls_audit by patching _MONGO_AVAILABLE True for the run() call. Refs #2011 --- tests/test_sampling_dedup_before_cap.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_sampling_dedup_before_cap.py b/tests/test_sampling_dedup_before_cap.py index 8a1fb8fd0..5be832d3d 100644 --- a/tests/test_sampling_dedup_before_cap.py +++ b/tests/test_sampling_dedup_before_cap.py @@ -148,9 +148,10 @@ def _noop_connect() -> None: connector._db = mock_db connector.connect = _noop_connect - with patch("utils.logger.log_connection"): - with patch.object(connector, "_save_inventory_snapshot"): - connector.run() + with patch("connectors.mongodb_connector._MONGO_AVAILABLE", True): + with patch("utils.logger.log_connection"): + with patch.object(connector, "_save_inventory_snapshot"): + connector.run() mock_coll.find.return_value.limit.assert_called_once_with(50) save_calls = db_manager.save_finding.call_args_list