Skip to content

Commit 57b6fc2

Browse files
test: make dbt and warehouse tests hermetic (#144)
* test: make dbt and warehouse tests hermetic * fix: keep SQL lint credential-free in CI
1 parent 189fa23 commit 57b6fc2

15 files changed

Lines changed: 448 additions & 345 deletions

.github/workflows/dagster-ci.yml

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,30 @@ jobs:
5252
cd macro_agents
5353
uv sync --extra dev
5454
55+
- name: Cache dbt packages
56+
id: cache-dbt-packages
57+
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
58+
with:
59+
path: dbt_project/dbt_packages
60+
key: ${{ runner.os }}-dbt-packages-${{ hashFiles('dbt_project/package-lock.yml') }}
61+
62+
- name: Install dbt packages
63+
if: steps.cache-dbt-packages.outputs.cache-hit != 'true'
64+
env:
65+
BIGQUERY_PROJECT: econ-data-project-478800
66+
DBT_SEND_ANONYMOUS_USAGE_STATS: "false"
67+
run: |
68+
cd macro_agents
69+
uv run dbt deps --project-dir ../dbt_project --profiles-dir ../dbt_project --target dev --no-send-anonymous-usage-stats
70+
71+
- name: Prepare dbt manifest offline
72+
env:
73+
BIGQUERY_PROJECT: econ-data-project-478800
74+
DBT_SEND_ANONYMOUS_USAGE_STATS: "false"
75+
run: |
76+
cd macro_agents
77+
uv run dbt parse --project-dir ../dbt_project --profiles-dir ../dbt_project --target dev --no-send-anonymous-usage-stats
78+
5579
- name: Lint with ruff
5680
run: |
5781
cd macro_agents
@@ -63,6 +87,12 @@ jobs:
6387
cd macro_agents
6488
uv run ty check --extra-search-path src src/macro_agents
6589
90+
- name: Lint dbt SQL with SQLFluff
91+
run: |
92+
cd macro_agents
93+
# The preceding dbt parse validates dbt templating; Jinja mode keeps style lint credential-free.
94+
uv run sqlfluff lint ../dbt_project/models --templater jinja --ignore templating,parsing --disable-progress-bar --processes 4
95+
6696
- name: Test Dagster definitions with dbt Platform
6797
if: ${{ env.DBT_PLATFORM_TOKEN != '' }}
6898
env:
@@ -85,4 +115,9 @@ jobs:
85115
- name: Test with pytest
86116
run: |
87117
cd macro_agents
88-
uv run pytest tests/ -v -m "not skip_ci"
118+
uv run pytest tests/ -v -m "not skip_ci and not network"
119+
120+
- name: Enforce critical-path coverage floor
121+
run: |
122+
cd macro_agents
123+
uv run pytest tests/test_bigquery_warehouse.py tests/test_asset_check_failure_sensor.py tests/test_sec_search.py --cov --cov-report=term-missing

AGENTS.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
- `make ruff`: lint/format Python with Ruff.
1313
- `make lint` / `make fix`: lint or fix dbt SQL with SQLFluff.
1414
- `make typecheck-dagster`: run `ty` type-checker over `macro_agents`.
15+
- `make dbt-deps`: explicitly install locked dbt packages (networked setup step).
1516
- `make dbt-manifest`: parse dbt project.
1617

1718
## Worktrunk Hooks (wt)
@@ -54,11 +55,13 @@
5455
## Testing Guidelines
5556
- Framework: `pytest` for Python (`macro_agents`).
5657
- Run Dagster tests: `cd macro_agents && uv run pytest tests/ -v`.
58+
- Install dbt packages once with `make dbt-deps`; pytest performs one offline dbt parse per session.
59+
- Network integration tests are opt-in: `RUN_NETWORK_TESTS=1 uv run pytest -m network`.
5760
- Test standards:
5861
- Prefer behavior-focused assertions (status, metadata, materialized values) over snapshots.
5962
- Cover loading, empty, error, and populated states.
6063
- Use deterministic fixtures and local resources (temp DuckDB, ephemeral Dagster instances); avoid network calls.
61-
- For Dagster assets, use `DagsterInstance.ephemeral()` and `build_op_context` with mocked resources.
64+
- Use `DagsterInstance.ephemeral()` and `build_op_context` as context managers so their database resources are disposed deterministically.
6265
- Keep tests isolated and fast; clean up temp files and avoid shared mutable state.
6366

6467
## Commit & Pull Request Guidelines

Readme.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,11 @@ Dagster UI: `http://localhost:3000`
8181
### Development (without Docker)
8282

8383
```bash
84+
# From the repository root, install Python and locked dbt dependencies
85+
make setup-dagster
86+
make dbt-manifest
87+
8488
cd macro_agents
85-
uv sync --extra dev
8689

8790
# Validate definitions
8891
uv run dg check defs
@@ -152,6 +155,9 @@ docker compose --env-file .env build # Rebuild images
152155
docker compose logs -f dagster_user_code # Tail user code logs
153156

154157
# Development
158+
make dbt-deps # Install locked dbt packages
159+
make dbt-manifest # Generate the local manifest
160+
cd macro_agents
155161
uv run pytest tests/ -v # Run tests
156162
uv run ruff check . && ruff format . # Lint + format
157163
uv run dg check defs # Validate Dagster definitions

dbt_project/models/signals/int_cross_asset_commodity_signals.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ gold_real_zscore AS (
7474
STDDEV_SAMP(CASE WHEN beta IS NOT NULL AND alpha IS NOT NULL THEN gold_price - (alpha + beta * real_yield_10y) END) OVER w AS residual_std
7575
FROM gold_real_residual
7676
WINDOW w AS (ORDER BY date ROWS BETWEEN 251 PRECEDING AND CURRENT ROW)
77-
)
77+
) AS rolling_residuals
7878
),
7979

8080
copper_gold_yield_corr AS (

macro_agents/pyproject.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ root_module = "macro_agents"
100100
[tool.pytest.ini_options]
101101
markers = [
102102
"essential: Essential tests that must pass in CI",
103+
"network: Tests that require external network access and are opt-in",
103104
"skip_ci: Tests to skip in CI (brittle or non-essential)",
104105
]
105106
filterwarnings = [
@@ -109,3 +110,16 @@ filterwarnings = [
109110
"ignore::DeprecationWarning:dagster._core.definitions.sensor_definition:",
110111
"ignore:Non-compliant tag keys like .*:DeprecationWarning:dagster._core.definitions.schedule_definition",
111112
]
113+
114+
[tool.coverage.run]
115+
source = ["macro_agents"]
116+
117+
[tool.coverage.report]
118+
fail_under = 50
119+
include = [
120+
"src/macro_agents/defs/asset_failure_sensor.py",
121+
"src/macro_agents/defs/domains/sec/search.py",
122+
"src/macro_agents/defs/resources/bigquery_query.py",
123+
"src/macro_agents/defs/resources/bigquery_warehouse.py",
124+
]
125+
show_missing = true

macro_agents/src/macro_agents/defs/transformation/dbt.py

Lines changed: 34 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"nasdaq_companies_summary "
3131
"source:staging.nasdaq_companies_prices_raw"
3232
)
33+
REQUIRED_DBT_PACKAGES = ("dbt_project_evaluator", "dbt_utils")
3334

3435

3536
class CustomizedDagsterDbtTranslator(DagsterDbtTranslator):
@@ -91,6 +92,21 @@ def _required_env_var(*names: str) -> dg.EnvVar:
9192
raise RuntimeError(f"Set {joined_names} to enable dbt Platform orchestration.")
9293

9394

95+
def _require_dbt_packages(project_dir: Path) -> None:
96+
"""Require explicitly installed dbt packages without network side effects."""
97+
packages_dir = project_dir / "dbt_packages"
98+
missing_packages = [
99+
package
100+
for package in REQUIRED_DBT_PACKAGES
101+
if not (packages_dir / package).exists()
102+
]
103+
if missing_packages:
104+
raise RuntimeError(
105+
"dbt packages are not installed: "
106+
f"{', '.join(missing_packages)}. Run `make dbt-deps`."
107+
)
108+
109+
94110
dbt_platform_mode = orchestration_mode in {
95111
"dbt_platform",
96112
"dbt_cloud",
@@ -156,11 +172,12 @@ def full_dbt_assets(context: dg.AssetExecutionContext, dbt: DbtCloudWorkspace):
156172
else:
157173
dbt_project_dir = os.getenv("DBT_PROJECT_DIR")
158174
if dbt_project_dir:
159-
dbt_project_dir = Path(dbt_project_dir).resolve()
160-
if not dbt_project_dir.exists():
175+
configured_project_dir = Path(dbt_project_dir)
176+
if not configured_project_dir.exists():
161177
raise FileNotFoundError(
162178
"DBT_PROJECT_DIR environment variable points to a non-existent path."
163179
)
180+
dbt_project_dir = configured_project_dir.resolve()
164181
else:
165182
current_file = Path(__file__).resolve()
166183
macro_agents_root = current_file.parent.parent.parent
@@ -184,16 +201,14 @@ def full_dbt_assets(context: dg.AssetExecutionContext, dbt: DbtCloudWorkspace):
184201

185202
dbt_project_dir = None
186203
for path in possible_dbt_project_paths:
187-
try:
188-
abs_path = path.resolve()
189-
if abs_path.exists() and abs_path.is_dir():
190-
if (abs_path / "dbt_project.yml").exists() or (
191-
abs_path / "dbt_project.yaml"
192-
).exists():
193-
dbt_project_dir = abs_path
194-
break
195-
except (OSError, RuntimeError):
204+
if not path.exists() or not path.is_dir():
196205
continue
206+
abs_path = path.resolve()
207+
if (abs_path / "dbt_project.yml").exists() or (
208+
abs_path / "dbt_project.yaml"
209+
).exists():
210+
dbt_project_dir = abs_path
211+
break
197212

198213
if dbt_project_dir is None:
199214
raise FileNotFoundError(
@@ -207,83 +222,28 @@ def full_dbt_assets(context: dg.AssetExecutionContext, dbt: DbtCloudWorkspace):
207222

208223
dbt_project_dir_path = dbt_project_dir
209224

210-
dbt_packages_dir = dbt_project_dir_path / "dbt_packages"
211-
dbt_utils_dir = (
212-
dbt_packages_dir / "dbt_utils" if dbt_packages_dir.exists() else None
213-
)
214-
215-
if not dbt_packages_dir.exists() or not dbt_utils_dir or not dbt_utils_dir.exists():
216-
import subprocess
217-
218-
logger.warning("dbt packages not found. Running 'dbt deps' (15s timeout)...")
219-
try:
220-
result = subprocess.run(
221-
["dbt", "deps", "--target", environment],
222-
cwd=dbt_project_dir_path,
223-
capture_output=True,
224-
text=True,
225-
timeout=15,
226-
)
227-
if result.returncode != 0:
228-
logger.warning(
229-
f"dbt deps failed (non-fatal): {result.stderr or result.stdout}"
230-
)
231-
else:
232-
logger.info("dbt packages installed successfully")
233-
except subprocess.TimeoutExpired:
234-
logger.warning(
235-
"dbt deps timed out (network may be unavailable), continuing without packages"
236-
)
237-
except Exception as e:
238-
logger.warning(f"Could not install dbt packages (non-fatal): {e}")
225+
_require_dbt_packages(dbt_project_dir_path)
239226

240227
dbt_project = DbtProject(
241228
project_dir=dbt_project_dir_path,
242229
target=environment,
243230
)
244231

245-
dbt_project.prepare_if_dev()
246-
247232
manifest_path = dbt_project.manifest_path
248-
if manifest_path and manifest_path.exists():
249-
logger.info("Using dbt manifest")
233+
if not manifest_path.exists():
234+
raise FileNotFoundError(
235+
f"dbt manifest not found at {manifest_path}. Run `make dbt-manifest`."
236+
)
237+
logger.info("Using dbt manifest at %s", manifest_path)
250238

251239
dbt_resource = DbtCliResource(project_dir=dbt_project_dir_path)
252240
dbt_cloud_polling_sensor = None
253241

254242
@dbt_assets(
255-
manifest=dbt_project.manifest_path,
243+
manifest=manifest_path,
256244
exclude=DBT_NASDAQ_EXCLUDE,
257245
dagster_dbt_translator=CustomizedDagsterDbtTranslator(),
258246
)
259247
def full_dbt_assets(context: dg.AssetExecutionContext, dbt: DbtCliResource):
260-
dbt_packages_dir = dbt_project_dir_path / "dbt_packages"
261-
dbt_utils_dir = (
262-
dbt_packages_dir / "dbt_utils" if dbt_packages_dir.exists() else None
263-
)
264-
265-
if (
266-
not dbt_packages_dir.exists()
267-
or not dbt_utils_dir
268-
or not dbt_utils_dir.exists()
269-
):
270-
context.log.info(
271-
"dbt packages not found. Running 'dbt deps' before build..."
272-
)
273-
try:
274-
deps_result = dbt.cli(["deps"], context=context).wait()
275-
return_code = getattr(deps_result, "return_code", None)
276-
if return_code not in (None, 0):
277-
context.log.error("Failed to install dbt packages.")
278-
raise RuntimeError(
279-
"dbt packages installation failed. Please run 'dbt deps' manually"
280-
)
281-
else:
282-
context.log.info("dbt packages installed successfully")
283-
except Exception as e:
284-
context.log.error(f"Could not install dbt packages: {e}")
285-
raise RuntimeError(
286-
"dbt packages are required but could not be installed. Please run 'dbt deps' manually"
287-
) from e
288-
248+
_require_dbt_packages(dbt_project_dir_path)
289249
yield from dbt.cli(["build"], context=context).stream()

0 commit comments

Comments
 (0)