fix(cli): I1+I2 review — surface manifest_unauthorized + add 3 typed-error tests

This commit is contained in:
ZdenekSrotyr 2026-05-04 18:19:35 +02:00
parent 9b70ca3069
commit b799aa534a
2 changed files with 104 additions and 0 deletions

View file

@ -186,6 +186,22 @@ def init(
}}), err=True)
raise typer.Exit(1)
# `run_pull` records per-stage failures into `result.errors` and only
# raises for programming errors. A manifest-stage failure here means
# the analyst has a saved token + saved server URL but no parquets,
# no DuckDB views — surface a typed error so the operator knows the
# workspace is not actually queryable. Common cause: PAT validates
# against /api/catalog/tables but lacks resource_grants for any tables.
manifest_err = next((e for e in result.errors if e.get("stage") == "manifest"), None)
if manifest_err:
typer.echo(render_error(0, {"detail": {
"kind": "manifest_unauthorized",
"hint": "Manifest fetch failed — workspace partially set up. "
"Check that the PAT has resource_grants for at least one table.",
"message": manifest_err.get("error", ""),
}}), err=True)
raise typer.Exit(1)
# ------------------------------------------------------------------
# Step 8: render AGNES_WORKSPACE.md from the static client-side
# template. Three placeholders: created_at, server_url, workspace_path.

View file

@ -131,3 +131,91 @@ def test_init_partial_state_friendly_exit(tmp_path, monkeypatch):
])
assert result.exit_code == 1
assert "Traceback" not in (result.output + (result.stderr or ""))
def test_init_auth_failed_on_401(tmp_path, monkeypatch):
"""PAT verify endpoint returns 401 -> auth_failed typed error, exit 1."""
from unittest.mock import MagicMock
monkeypatch.setenv("AGNES_CONFIG_DIR", str(tmp_path / "_cfg"))
def _api_get(path, *args, **kwargs):
resp = MagicMock()
resp.status_code = 401
resp.json.return_value = {"detail": "Invalid token"}
return resp
monkeypatch.setattr("cli.commands.init.api_get", _api_get, raising=False)
result = runner.invoke(init_app, [
"--server-url", "http://x",
"--token", "bad-pat",
"--workspace", str(tmp_path),
])
assert result.exit_code == 1
output = result.output + (result.stderr or "")
assert "Traceback" not in output
# Typed-error envelope should mention the kind or the actionable hint.
assert ("auth_failed" in output) or ("Token expired" in output) or ("Token format invalid" in output)
def test_init_server_unreachable_on_connect_error(tmp_path, monkeypatch):
"""Network failure during verify -> server_unreachable typed error, exit 1."""
monkeypatch.setenv("AGNES_CONFIG_DIR", str(tmp_path / "_cfg"))
def _api_get(path, *args, **kwargs):
raise ConnectionError("simulated network failure")
monkeypatch.setattr("cli.commands.init.api_get", _api_get, raising=False)
result = runner.invoke(init_app, [
"--server-url", "http://unreachable.example.com",
"--token", "test-pat",
"--workspace", str(tmp_path),
])
assert result.exit_code == 1
output = result.output + (result.stderr or "")
assert "Traceback" not in output
assert ("server_unreachable" in output) or ("Cannot reach" in output)
def test_init_manifest_unauthorized_when_pull_records_manifest_error(tmp_path, monkeypatch):
"""Manifest stage fails -> manifest_unauthorized typed error, exit 1.
Reproduces the I1 review finding: `run_pull` records per-stage failures
into `result.errors` rather than raising. Without the post-pull error
inspection, init would silently exit 0 with a partially-set-up workspace.
"""
from unittest.mock import MagicMock
from cli.lib.pull import PullResult
monkeypatch.setenv("AGNES_CONFIG_DIR", str(tmp_path / "_cfg"))
# init's verify-PAT call succeeds; welcome-fetch succeeds.
def _init_api_get(path, *args, **kwargs):
resp = MagicMock()
resp.status_code = 200
if path == "/api/welcome":
resp.json.return_value = {"content": "# Test\n"}
else:
resp.json.return_value = []
return resp
monkeypatch.setattr("cli.commands.init.api_get", _init_api_get, raising=False)
# run_pull returns a PullResult carrying a manifest-stage error.
def _fake_run_pull(server_url, token, workspace, *, dry_run=False):
result = PullResult()
result.errors.append({"stage": "manifest", "error": "401 Unauthorized"})
return result
monkeypatch.setattr("cli.commands.init.run_pull", _fake_run_pull, raising=False)
result = runner.invoke(init_app, [
"--server-url", "http://x",
"--token", "t",
"--workspace", str(tmp_path),
])
assert result.exit_code == 1
output = result.output + (result.stderr or "")
assert "Traceback" not in output
assert ("manifest_unauthorized" in output) or ("Manifest fetch failed" in output)