refactor(e2e): seed-once model + run the stack via the root compose (migrate → seed → assert)#1563
refactor(e2e): seed-once model + run the stack via the root compose (migrate → seed → assert)#1563ktursunov wants to merge 6 commits into
Conversation
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis PR reworks the e2e test suite to a session-scoped "seed once, assert many" model with per-fixture namespacing for isolation, adds a metric coverage gate (script, CI job, e2e.sh command), enhances expect_engine to enforce full stat-field assertions, captures analytics-api logs, and updates all metric test YAML fixtures to a department-of-5 scenario. Estimated code review effort: 4 (Complex) | ~75 minutes ChangesSeed-once E2E framework and metric coverage gate
Sequence Diagram(s)sequenceDiagram
participant Conftest as conftest.py
participant SeedOnce as seed_once.build_world
participant ClickHouse
participant AnalyticsApi
participant Tests as metric test cases
Conftest->>SeedOnce: build_world(all_fixtures)
SeedOnce->>SeedOnce: merge_namespaced_bronze
SeedOnce->>ClickHouse: seed bronze, build staging/silver/gold once
Conftest->>AnalyticsApi: start(build_world)
Tests->>AnalyticsApi: namespaced request per fixture
AnalyticsApi-->>Tests: response asserted via expect_engine
Conftest->>AnalyticsApi: teardown -> collect_metrics -> stop
sequenceDiagram
participant E2EJob as e2e job
participant Artifact as coverage-inputs artifact
participant GateJob as metric-coverage-gate job
participant Gate as metric_coverage.py
E2EJob->>Artifact: upload catalog_metrics.json
GateJob->>Artifact: download coverage-inputs
GateJob->>Gate: run against catalog_metrics.json
Gate-->>GateJob: markdown report + exit code
GateJob->>GateJob: append to step summary, fail if non-zero
Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
src/ingestion/tests/e2e/lib/namespace.py (1)
72-81: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winNamespacing is shallow — nested structures would silently bypass the rewrite (and the guard test).
namespace_recordonly inspects top-levelrec.items()values; a bronze field whose value is a nesteddict/list(rather than a flat string or a serialized JSON string) would not be scanned for@example.comor prefixed-field names. Sincemeta/test_seed_isolation.py's_iter_valuesuses the exact same shallow traversal, such a leak would not even be caught by the DB-free isolation guard — defeating its stated purpose of "asserting the invariant... fails fast... before the stack is ever built." Currently this is fine as long as every bronze fixture keeps identity data as strings/serialized JSON, but it's a silent failure mode for future fixtures.♻️ Suggested defensive recursion
-def namespace_record(rec: dict, token: str) -> dict: +def _rewrite_value(v, token: str): + if isinstance(v, str) and _EMAIL_DOMAIN in v: + return _domain_rewrite(v, token) + if isinstance(v, dict): + return {k: _rewrite_value(vv, token) for k, vv in v.items()} + if isinstance(v, list): + return [_rewrite_value(vv, token) for vv in v] + return v + + +def namespace_record(rec: dict, token: str) -> dict: """Return a copy of a resolved bronze record with identity rewritten for `token`.""" out: dict = {} for k, v in rec.items(): - if isinstance(v, str) and _EMAIL_DOMAIN in v: - v = _domain_rewrite(v, token) # emails anywhere, incl. JSON blobs + v = _rewrite_value(v, token) if k in _PREFIXED_FIELDS and isinstance(v, str) and v: v = f"{token}-{v}" out[k] = v return out🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/ingestion/tests/e2e/lib/namespace.py` around lines 72 - 81, The namespacing logic in namespace_record is only shallow, so nested dict/list values can bypass email and prefixed-field rewriting. Update namespace_record to recurse through nested structures (and keep the same behavior for strings/JSON blobs) so every embedded value is rewritten, and mirror that traversal in the _iter_values guard used by meta/test_seed_isolation.py so the isolation check catches nested leaks too.src/ingestion/tests/e2e/meta/test_seed_isolation.py (1)
34-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
_PREFIXEDduplicatesnamespace._PREFIXED_FIELDS— risk of silent drift.This guard test hardcodes its own copy of the prefixed-field list instead of importing
namespace._PREFIXED_FIELDS. If a future change adds/removes a field in one location but not the other, the guard could pass while namespacing is actually broken (or vice versa), undermining the test's purpose as the load-bearing isolation proof.♻️ Suggested fix
-_PREFIXED = ("unique_key", "source_id", "department", "id") +_PREFIXED = namespace._PREFIXED_FIELDSAlso applies to: 56-56
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/ingestion/tests/e2e/meta/test_seed_isolation.py` at line 34, The guard test is duplicating the prefixed-field list instead of reusing the source of truth, which can drift from namespace._PREFIXED_FIELDS. Update test_seed_isolation to import and reference namespace._PREFIXED_FIELDS (or the relevant namespace constant) in the _PREFIXED-based assertions and any related checks, so the isolation test always validates the actual production list rather than a hardcoded copy..github/workflows/e2e-bronze-to-api.yml (1)
113-142: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winConsider
persist-credentials: falseon checkout steps that only read code.Static analysis (zizmor
artipacked) flags both this job's checkout (Line 126) and thee2ejob's checkout (Line 51) for not settingpersist-credentials: false. By defaultactions/checkoutpersists theGITHUB_TOKENin local git config for the rest of the job; since neither job pushes commits, disabling persistence removes unnecessary residual credential exposure to any script/action running later in the same job. The concrete risk here is bounded (the uploaded artifact path is scoped to.artifacts/, not the whole workspace/.git), but it's a one-line, no-downside hardening.🔒 Proposed fix
- uses: actions/checkout@v4 + with: + persist-credentials: falseApply the same change to the checkout step in the
e2ejob (Line 51).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/e2e-bronze-to-api.yml around lines 113 - 142, The checkout steps in the `e2e` job and `metric-coverage-gate` job are persisting the `GITHUB_TOKEN` unnecessarily, which leaves credentials available to later steps in the same job. Update each `actions/checkout@v4` usage to disable credential persistence by setting `persist-credentials: false`, keeping the change scoped to the read-only checkouts in these jobs.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/ingestion/tests/e2e/conftest.py`:
- Around line 243-275: The _collect_metrics helper can still block or abort
teardown because it only checks subprocess return codes; update _collect_metrics
in conftest.py to be fully best-effort by adding a timeout to subprocess.run and
catching subprocess-related exceptions around the collect_metrics.py invocation.
Ensure any timeout or execution failure only logs a warning and returns so
analytics_api teardown can always continue to proc.stop() without being
interrupted.
In `@src/ingestion/tests/e2e/metrics/wiki_confluence_comments.test.yaml`:
- Around line 22-27: The wiki comment fixture is reusing the same page_id values
across fixtures, which can cause cross-matching in wiki_bullet_rows because it
joins on (tenant_id, page_id). Update the confluence wiki page entries in this
test fixture so each page_id is fixture-unique, either by prefixing the IDs here
or by applying the same namespacing in lib/namespace.py, and keep the related
unique_key values unchanged.
---
Nitpick comments:
In @.github/workflows/e2e-bronze-to-api.yml:
- Around line 113-142: The checkout steps in the `e2e` job and
`metric-coverage-gate` job are persisting the `GITHUB_TOKEN` unnecessarily,
which leaves credentials available to later steps in the same job. Update each
`actions/checkout@v4` usage to disable credential persistence by setting
`persist-credentials: false`, keeping the change scoped to the read-only
checkouts in these jobs.
In `@src/ingestion/tests/e2e/lib/namespace.py`:
- Around line 72-81: The namespacing logic in namespace_record is only shallow,
so nested dict/list values can bypass email and prefixed-field rewriting. Update
namespace_record to recurse through nested structures (and keep the same
behavior for strings/JSON blobs) so every embedded value is rewritten, and
mirror that traversal in the _iter_values guard used by
meta/test_seed_isolation.py so the isolation check catches nested leaks too.
In `@src/ingestion/tests/e2e/meta/test_seed_isolation.py`:
- Line 34: The guard test is duplicating the prefixed-field list instead of
reusing the source of truth, which can drift from namespace._PREFIXED_FIELDS.
Update test_seed_isolation to import and reference namespace._PREFIXED_FIELDS
(or the relevant namespace constant) in the _PREFIXED-based assertions and any
related checks, so the isolation test always validates the actual production
list rather than a hardcoded copy.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 87b78bd7-d0b7-4764-bd0b-26d5af8277bf
📒 Files selected for processing (59)
.github/workflows/e2e-bronze-to-api.ymlsrc/ingestion/tests/e2e/.gitignoresrc/ingestion/tests/e2e/README.mdsrc/ingestion/tests/e2e/api/test_session_smoke.pysrc/ingestion/tests/e2e/conftest.pysrc/ingestion/tests/e2e/e2e.shsrc/ingestion/tests/e2e/lib/analytics_api.pysrc/ingestion/tests/e2e/lib/collect_metrics.pysrc/ingestion/tests/e2e/lib/expect_engine.pysrc/ingestion/tests/e2e/lib/metric_coverage.pysrc/ingestion/tests/e2e/lib/namespace.pysrc/ingestion/tests/e2e/lib/seed_once.pysrc/ingestion/tests/e2e/meta/test_expect_engine.pysrc/ingestion/tests/e2e/meta/test_seed_isolation.pysrc/ingestion/tests/e2e/metrics/ai_ai_loc_share2.test.yamlsrc/ingestion/tests/e2e/metrics/ai_cc_active.test.yamlsrc/ingestion/tests/e2e/metrics/ai_cc_cost.test.yamlsrc/ingestion/tests/e2e/metrics/ai_cc_lines.test.yamlsrc/ingestion/tests/e2e/metrics/ai_cc_overage.test.yamlsrc/ingestion/tests/e2e/metrics/ai_cc_sessions.test.yamlsrc/ingestion/tests/e2e/metrics/ai_cc_tool_accept.test.yamlsrc/ingestion/tests/e2e/metrics/ai_cc_tool_acceptance.test.yamlsrc/ingestion/tests/e2e/metrics/ai_claude_web.test.yamlsrc/ingestion/tests/e2e/metrics/ai_cursor_acceptance.test.yamlsrc/ingestion/tests/e2e/metrics/ai_prs_total.test.yamlsrc/ingestion/tests/e2e/metrics/ai_prs_with_cc.test.yamlsrc/ingestion/tests/e2e/metrics/collab_active_days.test.yamlsrc/ingestion/tests/e2e/metrics/collab_emails_read.test.yamlsrc/ingestion/tests/e2e/metrics/collab_emails_received.test.yamlsrc/ingestion/tests/e2e/metrics/collab_emails_sent.test.yamlsrc/ingestion/tests/e2e/metrics/collab_files_engaged.test.yamlsrc/ingestion/tests/e2e/metrics/collab_files_shared_external.test.yamlsrc/ingestion/tests/e2e/metrics/collab_files_shared_internal.test.yamlsrc/ingestion/tests/e2e/metrics/collab_meeting_free.test.yamlsrc/ingestion/tests/e2e/metrics/collab_meeting_hours.test.yamlsrc/ingestion/tests/e2e/metrics/collab_meetings_count.test.yamlsrc/ingestion/tests/e2e/metrics/collab_teams_chats.test.yamlsrc/ingestion/tests/e2e/metrics/collab_teams_meeting_hours.test.yamlsrc/ingestion/tests/e2e/metrics/collab_teams_meetings.test.yamlsrc/ingestion/tests/e2e/metrics/collab_zulip_chat.test.yamlsrc/ingestion/tests/e2e/metrics/task_delivery_bugs_to_task_ratio_jira.test.yamlsrc/ingestion/tests/e2e/metrics/task_delivery_due_date_compliance_jira.test.yamlsrc/ingestion/tests/e2e/metrics/task_delivery_tasks_completed_jira.test.yamlsrc/ingestion/tests/e2e/metrics/team_bullet_collab_emails_sent.test.yamlsrc/ingestion/tests/e2e/metrics/team_bullet_task_delivery_tasks_completed.test.yamlsrc/ingestion/tests/e2e/metrics/templates/confluence_wiki_pages.yamlsrc/ingestion/tests/e2e/metrics/templates/jira_task.yamlsrc/ingestion/tests/e2e/metrics/templates/m365_email.yamlsrc/ingestion/tests/e2e/metrics/templates/outline_wiki_pages.yamlsrc/ingestion/tests/e2e/metrics/templates/people.yamlsrc/ingestion/tests/e2e/metrics/test_fixtures.pysrc/ingestion/tests/e2e/metrics/wiki_confluence_active_authors.test.yamlsrc/ingestion/tests/e2e/metrics/wiki_confluence_comments.test.yamlsrc/ingestion/tests/e2e/metrics/wiki_confluence_edits.test.yamlsrc/ingestion/tests/e2e/metrics/wiki_confluence_pages_created.test.yamlsrc/ingestion/tests/e2e/metrics/wiki_outline_active_authors.test.yamlsrc/ingestion/tests/e2e/metrics/wiki_outline_comments.test.yamlsrc/ingestion/tests/e2e/metrics/wiki_outline_edits.test.yamlsrc/ingestion/tests/e2e/metrics/wiki_outline_pages_created.test.yaml
| def _collect_metrics(proc: AnalyticsApiProcess) -> None: | ||
| """Run `lib/collect_metrics.py` (a script — NOT a test) against the | ||
| live API, primary worker only. Snapshots the metric catalog into `.artifacts/` | ||
| so the metric-coverage gate analyses a file with no second app boot. | ||
| Best-effort: a failure just means the gate finds no artifact and fails loudly — | ||
| never abort the session for it. Must run while the API is up (called from | ||
| analytics_api teardown, before proc.stop()).""" | ||
| if not _IS_PRIMARY: | ||
| return | ||
| import subprocess | ||
| import sys | ||
|
|
||
| script = Path(__file__).parent / "lib" / "collect_metrics.py" | ||
| out_dir = Path(__file__).parent / ".artifacts" | ||
| result = subprocess.run( | ||
| [ | ||
| sys.executable, | ||
| str(script), | ||
| "--url", | ||
| proc.base_url, | ||
| "--out-dir", | ||
| str(out_dir), | ||
| "--tenant", | ||
| str(TEST_TENANT_ID), | ||
| ], | ||
| check=False, | ||
| ) | ||
| if result.returncode != 0: | ||
| LOG.warning( | ||
| "coverage-artifact collection failed (rc=%d); gate jobs may lack inputs", | ||
| result.returncode, | ||
| ) | ||
|
|
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
_collect_metrics can block session teardown indefinitely; exceptions aren't caught.
The docstring states this is "Best-effort... never abort the session for it," but the implementation only handles a non-zero returncode:
subprocess.runhas notimeout=, so ifcollect_metrics.pyhangs (network stall, deadlock, etc.), the call blocks forever. Sinceanalytics_api's teardown istry: _collect_metrics(proc) finally: proc.stop(), thefinallynever gets a chance to run until the hang resolves — defeating the guarantee that the process always gets stopped.- Any exception raised by
subprocess.runitself (e.g. missing interpreter, permission error) propagates uncaught, again contradicting "never abort the session for it."
🛡️ Suggested fix
- result = subprocess.run(
- [
- sys.executable,
- str(script),
- "--url",
- proc.base_url,
- "--out-dir",
- str(out_dir),
- "--tenant",
- str(TEST_TENANT_ID),
- ],
- check=False,
- )
- if result.returncode != 0:
- LOG.warning(
- "coverage-artifact collection failed (rc=%d); gate jobs may lack inputs",
- result.returncode,
- )
+ try:
+ result = subprocess.run(
+ [
+ sys.executable,
+ str(script),
+ "--url",
+ proc.base_url,
+ "--out-dir",
+ str(out_dir),
+ "--tenant",
+ str(TEST_TENANT_ID),
+ ],
+ check=False,
+ timeout=60,
+ )
+ if result.returncode != 0:
+ LOG.warning(
+ "coverage-artifact collection failed (rc=%d); gate jobs may lack inputs",
+ result.returncode,
+ )
+ except Exception:
+ LOG.exception("coverage-artifact collection raised; gate jobs may lack inputs")Also applies to: 301-307
🧰 Tools
🪛 ast-grep (0.44.0)
[error] 256-268: Command coming from incoming request
Context: subprocess.run(
[
sys.executable,
str(script),
"--url",
proc.base_url,
"--out-dir",
str(out_dir),
"--tenant",
str(TEST_TENANT_ID),
],
check=False,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
🪛 Ruff (0.15.20)
[error] 257-257: subprocess call: check for execution of untrusted input
(S603)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/ingestion/tests/e2e/conftest.py` around lines 243 - 275, The
_collect_metrics helper can still block or abort teardown because it only checks
subprocess return codes; update _collect_metrics in conftest.py to be fully
best-effort by adding a timeout to subprocess.run and catching
subprocess-related exceptions around the collect_metrics.py invocation. Ensure
any timeout or execution failure only logs a warning and returns so
analytics_api teardown can always continue to proc.stop() without being
interrupted.
dee48b7 to
b4223ac
Compare
147bcaa to
6abe20a
Compare
…xture namespacing Rewrite the bronze-to-API e2e rig from per-fixture (truncate → seed → dbt → reapply gold views → refresh → query, ×~50 fixtures) to SEED-ONCE: seed every fixture's namespaced bronze once → dbt build once → reapply gold-view migrations once (prod order, after real silver) → refresh MVs once → tests are pure assertions. Kills the per-fixture view-rebuild churn. - Isolation via per-fixture identity namespacing (lib/namespace.py): email domain and unique_key/id/source_id/department are rewritten per fixture token, applied in lockstep to seeded bronze AND each request $filter. meta/test_seed_isolation.py proves cross-fixture RMT-key disjointness offline from the placeholder DDL. - Run the stack via the ROOT docker-compose (reuse its ClickHouse + MariaDB under an isolated `insight-e2e` project); a slim compose/docker-compose.e2e.yml overlay adds the build-only binary services + migrate + runner. Versions pinned to the gitops SSOT (CH 25.7.5 / MariaDB 12.2.2). 3-phase: up+health → e2e-migrate (the real apply-ch-migrations.sh) → seed-once + assert. - Cleanups: drop the redundant CH readiness wait (e2e-migrate already proves CH is up); rename metrics/test_fixtures.py → test_metrics.py and the `fixture` marker → `metric` (both clashed with pytest vocabulary); remove dead code (a broken spawn() calling an undefined build(); the write-only TouchedLedger per-test truncation path superseded by namespacing); refresh stale cargo-build docs. Three bugs fixed en route: 1. analytics-api spawn PIPE deadlock — unread subprocess.PIPE stdout filled the 64KB buffer during schema-validation logging → process blocked → never served /health. Redirect stdout to a temp file. 2. RMT `id` collapse — bronze_bamboohr.employees / bronze_jira.jira_issue are ORDER BY id with shared literal ids across fixtures → merge-collapse. Namespace `id`; rewrite the guard to derive the real ORDER BY key from the DDL. 3. Team bullets were skip-listed as "company-wide blends" — actually they filtered `org_unit_id eq`, which analytics-api never implemented (only `org_unit_id IN`), so the predicate was silently dropped and the value went company-wide. Scope by `person_id IN (roster)` (how the API scopes team aggregates) → isolable → the skip list is removed entirely. Local suite: 85 passed, 0 skipped. Signed-off-by: Konstantin Tursunov <Konstantin.Tursunov@constructor.tech>
…-blend class) Companion to meta/test_seed_isolation.py. That guard proves bronze-key disjointness; this one proves each fixture's REQUEST actually scopes to its own data at query time, instead of silently falling back to a company-wide (all fixtures) result. Both are DB-free static checks that fail in CI before the stack is built. Two checks (meta/test_request_scoping.py): - A. supported-filter: every $filter clause must be a (field, operator) the analytics API implements (person_id eq/in, org_unit_id in, metric_date ge/lt/le, drill_id/metric_key/section_id eq). The API substring-scrapes a fixed grammar and SILENTLY DROPS anything else, so the request runs unscoped. This is exactly the defect that blended the team bullets: they filtered `org_unit_id eq`, which the API never implemented (only `org_unit_id in`), so the predicate vanished and the value spanned every seeded fixture. Now caught at author time. - B. benchmark-scoping: a case asserting cohort stats (median/p25/p75/range/peer_n) must scope by identity (person_id eq/in or org_unit_id in) so the cohort resolves to this fixture, not the shared world. _COMPANY_WIDE_ACKNOWLEDGED (empty) forces a genuinely company-wide/sole-seeded benchmark to be a conscious, documented opt-out. Validated over all 50 resolved fixtures: both checks pass; negative control confirms `org_unit_id eq` is flagged. Signed-off-by: Konstantin Tursunov <Konstantin.Tursunov@constructor.tech>
…torfabric#1657) Adds metrics/test_known_bugs.py — granular xfail coverage for bug constructorfabric#1657 (analytics silently drops an `org_unit_id eq` filter and returns the whole-company value). Per team-bullet metric, two tests over the same `org_unit_id eq` request so only the defect-affected check is xfailed: - test_org_unit_eq_query_succeeds_silently — MUST PASS: HTTP 200 / result 'ok', proving the defect is SILENT (no error, just wrong numbers). - test_org_unit_eq_scopes_to_department — xfail(strict=True): the department stats a correct `org_unit_id eq` would return. Fails today (company-wide blend); becomes a hard failure (xpassed) when the API implements org_unit_id eq — the signal to drop the marker. Reuses each team_bullet_*.test.yaml case, swapping the person_id-IN roster workaround for the natural `org_unit_id eq` filter, so the suite exercises the real product path the fixtures otherwise dodge. Lives in Python (not a *.test.yaml) so the request-scoping guard, which forbids org_unit_id eq in fixtures, does not flag it. Local: 2 passed, 2 xfailed (0 failed, 0 xpassed). Signed-off-by: Konstantin Tursunov <Konstantin.Tursunov@constructor.tech>
…uments constructorfabric#1658) m20260618_000001 (cc_overage) redefined the AI IC bullet (...0013) and team sibling (...0006) query_refs last, rebuilding from the pre-distribution shape and dropping the p25/p75/n department distribution that m20260611_000001 (dept-reconcile) had added. The bullets now return only value/median/range_min/range_max. Adds a strict xfail to metrics/test_known_bugs.py: test_ai_ic_bullet_emits_department_distribution asserts the department p25:120 / p75:240 / n:5 that ai_cc_cost's {60,120,180,240,300}c distribution should produce (quantileExact over 5 sorted values). Fails today (those stats are absent from the returned row); xpasses — a hard failure — when the query_ref re-emits them. Unlike the constructorfabric#1657 tests, the bullet's value/median/range checks already PASS via the existing ai_cc_cost.test.yaml fixture, so only the missing distribution is xfailed. Local: 2 passed, 3 xfailed (0 failed, 0 xpassed). Signed-off-by: Konstantin Tursunov <Konstantin.Tursunov@constructor.tech>
Remove metrics/test_known_bugs.py. The strict-xfail tracking of constructorfabric#1657 (org_unit_id eq silently dropped) and constructorfabric#1658 (AI bullet lost p25/p75/n) inside the assertion suite was more confusing than useful: both are filed on GitHub, meta/test_request_scoping.py already forbids the org_unit_id-eq fixture shape, and ai_cc_cost.test.yaml documents the Signed-off-by: Konstantin Tursunov <Konstantin.Tursunov@constructor.tech> constructorfabric#1658 stat gap in place.
The meta/ guards (request-scoping, seed-isolation, expect-engine,
ref-resolver) are static/unit checks — they touch no ClickHouse,
MariaDB, or analytics, yet were gated behind the ~30-min runner-image
build in the e2e job, so a bad fixture failed slowly.
Mark them `offline` (pytest.ini) and split CI:
* new `offline-guards` job — setup-python 3.12 + `pip install -e .`,
`pytest -m offline`; no Docker, seconds to fail.
* the heavy `e2e` job now runs `-m "not offline"` so it still covers
everything that needs the live stack (metric tests, api/ smoke, the
dbt runner) without re-running the guards.
test_dbt_runner stays `smoke` (its dbt_runner fixture needs the stack).
Signed-off-by: Konstantin Tursunov <Konstantin.Tursunov@constructor.tech>
Rewrites the bronze→API E2E rig from a per-fixture loop (
truncate → seed → dbt → reapply ~40 gold views → refresh → query, ×~50 fixtures) to a seed-once model: materialise every fixture's data once per session, then run each test as a pure assertion. Faster, simpler, and it exercises the real migration + compose path. Rebased onmain(reconciles theanalytics-api→analyticsrename from #1579). 2 commits.Commit 1 — seed-once rig (
4a4234b9)dbt build(staging → enrich → silver) once → reapply gold-view migrations once → refresh MVs once → each test just namespaces its request, POSTs/v1/metrics/queries, and checks expects. No per-test seed/dbt/migrate.lib/namespace.py): a per-fixture token rewrites the email domain andunique_key/id/source_id/department, applied identically to the seeded bronze and the request$filter.meta/test_seed_isolation.pyproves no two fixtures share an RMTORDER BYkey — offline, before the stack is built.uphealth-gated ClickHouse + MariaDB; (2)e2e-migrateruns the realsrc/ingestion/scripts/apply-ch-migrations.sh(the same entrypoint the Helm post-install hook uses); (3)e2e-runnerseeds + asserts. Runs as an isolatedinsight-e2ecompose project (own network/volumes/ports) so a developer's./dev-compose.shstack is untouched. analytics is spawned in-process by pytest after the world is built, so the runner needs no docker socket.docker-compose.ymlDB services, pinned to the versions the gitops charts deploy: ClickHouse 25.7.5, MariaDB 12.2.2. No second DB stack.compose/docker-compose.{yml,runner,cache}.yml,lib/compose.py, the deadmigration_applierbootstrap, a dead+brokenspawn()(called an undefinedbuild()), and the write-only per-testTouchedLedger; renamedmetrics/test_fixtures.py→test_metrics.pyand the pytest markerfixture→metric(both clashed with pytest's own vocabulary).subprocess.PIPEstdout filled the 64 KB buffer during startup logging → process blocked → never served/health. Now sinks to a temp file.idcollapse —bronze_bamboohr.employees/bronze_jira.jira_issueareORDER BY idwith literal ids shared across fixtures → merge-collapse. Namespacedid; the guard now derives the realORDER BYkey from the placeholder DDL.org_unit_id eq, which the analytics API never implemented (onlyorg_unit_id in), so the predicate was silently dropped and the value blended company-wide. Scoping byperson_id IN (roster)— how the API actually scopes team aggregates — isolates them. The skip list is removed entirely.Commit 2 — request-scoping guard (
b7ac15d4)meta/test_request_scoping.py— a DB-free companion totest_seed_isolation.pythat hardens the failure class the un-skip exposed, at author time:$filterclause must be a(field, operator)the API implements (person_id eq/in,org_unit_id in,metric_date ge/lt/le,drill_id/metric_key/section_id eq). Anything else is silently dropped by the API → unscoped result; this catches the exactorg_unit_id eqtrap.median/p25/p75/range/peer_n) must scope by identity (person_id/org_unit_id), so the cohort resolves to this fixture and not the whole shared world.Validation
system.view_refreshes.last_refresh_resultdropped) — fixed by switchingrefresh_intermediatestoSYSTEM REFRESH VIEW+SYSTEM WAIT VIEW(race-free, CH 24.10+).Notes for reviewers
compose/docker-compose.runner.yml+docker-compose.cache.yml; this PR consolidates them into onedocker-compose.e2e.ymloverlay layered on the root compose. Deliberate (one overlay, single version SSOT, less duplication) — flagging for whoever owns the e2e compose setup.mariadb-datavolumes may need a reset; the e2e project uses ephemeral, project-scoped volumes so it's unaffected.$filterclauses likeorg_unit_id eq, returning company-wide-but-200results — filed as backend: analytics silently dropsorg_unit_id eqfilter → whole-company aggregates (HTTP 200) #1657. This PR's guard prevents it in tests; the API-side fix is tracked separately.Follow-ups (deferred)
metric_datewindow) + a small physical annex for any genuinely company-wide-cohort metric the current namespacing can't reach — only when a metric needs it (the guard flags such cases).reapply_migrationsstep is now redundant given fix(seed): single source of truth for ClickHouse placeholder DDL #1597 (placeholder-DDL SSOT).