Task 0.5 of clean-analyst-bootstrap. Greenfield rewrite — no fallback, no aliases. Existing dev environments lose their cached PAT and must re-authenticate. Env var renames (hard cutover): - DA_CONFIG_DIR -> AGNES_CONFIG_DIR - DA_SERVER -> AGNES_SERVER - DA_SERVER_URL -> AGNES_SERVER_URL (test-only stale ref, not in spec) - DA_NO_UPDATE_CHECK -> AGNES_NO_UPDATE_CHECK - DA_LOCAL_DIR -> AGNES_LOCAL_DIR - DA_TOKEN -> AGNES_TOKEN - DA_STREAM_RETRIES -> AGNES_STREAM_RETRIES Config dir rename: ~/.config/da/ -> ~/.config/agnes/ (across code, comments, docstrings, error messages, install templates, dev scripts). Stale `da X` references in CLI source (and adjacent app/, tests/): swept docstrings, comments, help text, and error messages where the verb survives the rewrite (init, pull, push, catalog, status, diagnose, auth, admin, skills, query, schema, describe, explore, disk-info, snapshot, login, logout, whoami, server, setup) and replaced `da X` with `agnes X`. Intentionally kept `da sync`, `da fetch`, `da analyst`, `da metrics` — those verbs are removed in later tasks; the legacy strings will be detected by `_LEGACY_STRINGS` (added in Task 2). Test fixes: - TestCLIVersion now asserts output starts with `agnes ` (was `da `). Test results: 2675 passed, 25 skipped (full pytest run, excluding 9 pre-existing test_db.py / test_user_management.py / test_e2e_extract.py / test_cli_binary_rename.py failures unrelated to this rename).
138 lines
5.4 KiB
Python
138 lines
5.4 KiB
Python
"""`da sync --quiet` truly suppresses stdout chatter, including the download
|
|
loop and final summary.
|
|
|
|
Without --quiet, the same fixture prints "Downloading", "Downloaded:", etc.;
|
|
with --quiet, stdout stays empty and the terse one-liner lands on stderr.
|
|
The first test forces the download loop to run so the contrast between
|
|
noisy/quiet stdout is observable (mutation-tests the flag — see PR #145
|
|
for the original empty-manifest test that passed even without --quiet).
|
|
"""
|
|
import json
|
|
from typer.testing import CliRunner
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
from cli.main import app
|
|
|
|
|
|
def _fake_manifest_one_table():
|
|
resp = MagicMock()
|
|
resp.json.return_value = {
|
|
"tables": {
|
|
"orders": {
|
|
"hash": "abc123",
|
|
"rows": 5,
|
|
"size_bytes": 100,
|
|
"query_mode": "local",
|
|
"source_type": "keboola",
|
|
}
|
|
},
|
|
"assets": {},
|
|
"server_time": "2026-04-30T00:00:00Z",
|
|
}
|
|
resp.raise_for_status = MagicMock()
|
|
return resp
|
|
|
|
|
|
def _stub_download(_url, target_path):
|
|
from pathlib import Path
|
|
Path(target_path).write_bytes(b"PAR1" + b"\x00" * 16 + b"PAR1")
|
|
|
|
|
|
def test_quiet_suppresses_stdout_when_downloading(tmp_path, monkeypatch):
|
|
"""Manifest has tables that actually trigger downloads. Without --quiet
|
|
stdout would contain 'Downloading' / 'Downloaded:'. With --quiet stdout
|
|
stays empty and the terse summary lands on stderr."""
|
|
monkeypatch.setenv("AGNES_LOCAL_DIR", str(tmp_path))
|
|
monkeypatch.setenv("AGNES_CONFIG_DIR", str(tmp_path / "_cfg"))
|
|
runner = CliRunner()
|
|
|
|
with patch("cli.commands.sync.api_get", return_value=_fake_manifest_one_table()), \
|
|
patch("cli.commands.sync.stream_download", side_effect=_stub_download), \
|
|
patch("cli.commands.sync._md5_file", return_value="abc123"), \
|
|
patch("cli.commands.sync._rebuild_duckdb_views"), \
|
|
patch("cli.commands.sync._fetch_and_write_rules"):
|
|
result = runner.invoke(app, ["sync", "--quiet"])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert result.stdout == "", f"expected empty stdout, got: {result.stdout!r}"
|
|
assert "sync: 1 tables" in result.stderr
|
|
|
|
|
|
def test_noisy_mode_prints_to_stdout(tmp_path, monkeypatch):
|
|
"""Anchor: the noisy path DOES print download chatter to stdout, so the
|
|
contrast in the quiet test above is meaningful."""
|
|
monkeypatch.setenv("AGNES_LOCAL_DIR", str(tmp_path))
|
|
monkeypatch.setenv("AGNES_CONFIG_DIR", str(tmp_path / "_cfg"))
|
|
runner = CliRunner()
|
|
|
|
with patch("cli.commands.sync.api_get", return_value=_fake_manifest_one_table()), \
|
|
patch("cli.commands.sync.stream_download", side_effect=_stub_download), \
|
|
patch("cli.commands.sync._md5_file", return_value="abc123"), \
|
|
patch("cli.commands.sync._rebuild_duckdb_views"), \
|
|
patch("cli.commands.sync._fetch_and_write_rules"):
|
|
result = runner.invoke(app, ["sync"])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
assert "Downloaded:" in result.stdout
|
|
|
|
|
|
def test_quiet_manifest_failure_exits_nonzero(tmp_path, monkeypatch):
|
|
"""SessionStart hook contract: server unreachable → non-zero exit (so
|
|
`|| true` swallows it cleanly), error message on stderr."""
|
|
monkeypatch.setenv("AGNES_LOCAL_DIR", str(tmp_path))
|
|
monkeypatch.setenv("AGNES_CONFIG_DIR", str(tmp_path / "_cfg"))
|
|
runner = CliRunner()
|
|
|
|
fake_resp = MagicMock()
|
|
fake_resp.raise_for_status.side_effect = RuntimeError("boom")
|
|
|
|
with patch("cli.commands.sync.api_get", return_value=fake_resp):
|
|
result = runner.invoke(app, ["sync", "--quiet"])
|
|
|
|
assert result.exit_code == 1
|
|
assert "manifest fetch failed" in result.stderr
|
|
|
|
|
|
def test_quiet_skips_remote_mode_tables(tmp_path, monkeypatch):
|
|
"""Materialized rows go through the download path; remote rows do not.
|
|
Locks in the contract that --quiet honors the same skipped_remote
|
|
filter as the noisy path."""
|
|
monkeypatch.setenv("AGNES_LOCAL_DIR", str(tmp_path))
|
|
monkeypatch.setenv("AGNES_CONFIG_DIR", str(tmp_path / "_cfg"))
|
|
|
|
resp = MagicMock()
|
|
resp.json.return_value = {
|
|
"tables": {
|
|
"live_orders": {
|
|
"hash": "x", "rows": 0, "size_bytes": 0,
|
|
"query_mode": "remote", "source_type": "bigquery",
|
|
},
|
|
"agg_90d": {
|
|
"hash": "abc", "rows": 5, "size_bytes": 100,
|
|
"query_mode": "materialized", "source_type": "bigquery",
|
|
},
|
|
},
|
|
"assets": {},
|
|
"server_time": "2026-04-30T00:00:00Z",
|
|
}
|
|
resp.raise_for_status = MagicMock()
|
|
|
|
runner = CliRunner()
|
|
download_calls = []
|
|
|
|
def _spy_download(url, target):
|
|
download_calls.append(url)
|
|
from pathlib import Path
|
|
Path(target).write_bytes(b"PAR1" + b"\x00" * 16 + b"PAR1")
|
|
|
|
with patch("cli.commands.sync.api_get", return_value=resp), \
|
|
patch("cli.commands.sync.stream_download", side_effect=_spy_download), \
|
|
patch("cli.commands.sync._md5_file", return_value="abc"), \
|
|
patch("cli.commands.sync._rebuild_duckdb_views"), \
|
|
patch("cli.commands.sync._fetch_and_write_rules"):
|
|
result = runner.invoke(app, ["sync", "--quiet"])
|
|
|
|
assert result.exit_code == 0, result.stdout
|
|
# Remote table never downloaded; materialized table downloaded.
|
|
assert any("agg_90d" in u for u in download_calls)
|
|
assert not any("live_orders" in u for u in download_calls)
|