dooers-cli 0.7.0__tar.gz → 0.7.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/PKG-INFO +2 -2
  2. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/pyproject.toml +2 -2
  3. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/src/dooers/cli/agent_store.py +26 -3
  4. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/src/dooers/cli/agents.py +32 -0
  5. dooers_cli-0.7.2/src/dooers/cli/env_prod.py +31 -0
  6. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/src/dooers/cli/ignore.py +2 -0
  7. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/src/dooers/cli/manifest_sync.py +8 -3
  8. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/src/dooers/cli/push.py +9 -3
  9. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/src/dooers/cli/push_client.py +14 -1
  10. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/src/dooers/cli/validate.py +32 -0
  11. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/tests/test_agent_store.py +21 -0
  12. dooers_cli-0.7.2/tests/test_env_prod.py +19 -0
  13. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/tests/test_manifest_sync.py +9 -3
  14. dooers_cli-0.7.2/tests/test_push_failure_display.py +44 -0
  15. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/tests/test_validate.py +45 -0
  16. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/uv.lock +2 -2
  17. dooers_cli-0.7.0/tests/test_push_failure_display.py +0 -22
  18. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/.gitignore +0 -0
  19. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/README.md +0 -0
  20. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/src/dooers/cli/__init__.py +0 -0
  21. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/src/dooers/cli/auth.py +0 -0
  22. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/src/dooers/cli/cli.py +0 -0
  23. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/src/dooers/cli/config.py +0 -0
  24. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/src/dooers/cli/core_client.py +0 -0
  25. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/src/dooers/cli/org.py +0 -0
  26. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/src/dooers/cli/settings.py +0 -0
  27. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/src/dooers/cli/token_store.py +0 -0
  28. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/src/dooers/cli/user_config.py +0 -0
  29. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/src/dooers/py.typed +0 -0
  30. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/tests/__init__.py +0 -0
  31. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/tests/test_agents_delete.py +0 -0
  32. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/tests/test_config.py +0 -0
  33. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/tests/test_core_client.py +0 -0
  34. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/tests/test_ignore.py +0 -0
  35. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/tests/test_org.py +0 -0
  36. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/tests/test_push_client_async.py +0 -0
  37. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/tests/test_push_hosting_guard.py +0 -0
  38. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/tests/test_push_poll.py +0 -0
  39. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/tests/test_push_teardown.py +0 -0
  40. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/tests/test_smoke.py +0 -0
  41. {dooers_cli-0.7.0 → dooers_cli-0.7.2}/tests/test_token_store.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dooers-cli
3
- Version: 0.7.0
3
+ Version: 0.7.2
4
4
  Summary: Dooers CLI — push agents to Dooers, manage agent records, authenticate from the terminal
5
5
  Project-URL: Repository, https://github.com/Dooers-ai/dooers-cli
6
6
  Author-email: Dooers <dev@dooers.ai>
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3.11
15
15
  Classifier: Programming Language :: Python :: 3.12
16
16
  Classifier: Typing :: Typed
17
17
  Requires-Python: >=3.10
18
- Requires-Dist: dooers-protocol>=0.6.0
18
+ Requires-Dist: dooers-protocol>=0.6.1
19
19
  Requires-Dist: httpx>=0.28.1
20
20
  Requires-Dist: pydantic>=2.13.2
21
21
  Requires-Dist: pyyaml>=6.0.2
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dooers-cli"
3
- version = "0.7.0"
3
+ version = "0.7.2"
4
4
  description = "Dooers CLI — push agents to Dooers, manage agent records, authenticate from the terminal"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -26,7 +26,7 @@ dependencies = [
26
26
  "pydantic>=2.13.2",
27
27
  "pyyaml>=6.0.2",
28
28
  "tqdm>=4.66.4",
29
- "dooers-protocol>=0.6.0",
29
+ "dooers-protocol>=0.6.1",
30
30
  ]
31
31
 
32
32
  [tool.uv.sources]
@@ -11,12 +11,26 @@ class AgentStoreError(RuntimeError):
11
11
 
12
12
 
13
13
  def _data(resp: httpx.Response):
14
- body = resp.json()
14
+ # The body may not be JSON: framework-level errors (e.g. a 403 "Forbidden")
15
+ # and empty success bodies (204) are common. Never let json() crash the CLI.
16
+ try:
17
+ body = resp.json()
18
+ except (json.JSONDecodeError, ValueError):
19
+ body = None
20
+
15
21
  if isinstance(body, dict) and body.get("success") is False:
16
22
  raise AgentStoreError(body.get("error", {}).get("message", f"HTTP {resp.status_code}"))
17
23
  if resp.status_code >= 400:
18
- raise AgentStoreError(f"HTTP {resp.status_code}")
19
- return body.get("data", body)
24
+ detail = ""
25
+ if isinstance(body, dict):
26
+ err = body.get("error")
27
+ if isinstance(err, dict):
28
+ detail = err.get("message", "")
29
+ detail = detail or body.get("message", "")
30
+ detail = detail or (resp.text or "").strip()
31
+ msg = f"HTTP {resp.status_code}"
32
+ raise AgentStoreError(f"{msg}: {detail}" if detail else msg)
33
+ return body.get("data", body) if isinstance(body, dict) else body
20
34
 
21
35
 
22
36
  def _record(d: dict) -> AgentRecord:
@@ -27,6 +41,7 @@ def _record(d: dict) -> AgentRecord:
27
41
  organization_id=d.get("organizationId"),
28
42
  host_url=d.get("hostUrl"),
29
43
  status=d.get("status"),
44
+ runtime_api_key=d.get("runtimeApiKey"),
30
45
  )
31
46
 
32
47
 
@@ -77,3 +92,11 @@ class HTTPCoreAgentStore:
77
92
  f"{self.api}/agents/{agent_id}", headers=self._h(), timeout=self._timeout
78
93
  )
79
94
  _data(r) # success body is {success, message} with no data key — do NOT call _record()
95
+
96
+ def regenerate_runtime_api_key(self, agent_id: str) -> AgentRecord:
97
+ r = httpx.post(
98
+ f"{self.api}/agents/{agent_id}/runtime-api-key/regenerate",
99
+ headers=self._h(),
100
+ timeout=self._timeout,
101
+ )
102
+ return _record(_data(r))
@@ -15,6 +15,7 @@ from dooers.protocol.teardown import format_teardown_result
15
15
 
16
16
  from dooers.cli import config
17
17
  from dooers.cli.agent_store import AgentStoreError, HTTPCoreAgentStore
18
+ from dooers.cli.env_prod import upsert_agent_seed_secret
18
19
  from dooers.cli.org import resolve_org_for_cli
19
20
  from dooers.cli.push_client import PushClient, PushClientError
20
21
  from dooers.cli.settings import Settings
@@ -65,9 +66,40 @@ def create(
65
66
  directory=Path.cwd(),
66
67
  )
67
68
  typer.echo(f"Created {rec.agent_id}. {config.MANIFEST_FILENAME} written.")
69
+ if rec.runtime_api_key:
70
+ env_path = upsert_agent_seed_secret(Path.cwd(), rec.runtime_api_key)
71
+ typer.echo(f"Wrote AGENT_SEED_SECRET to {env_path.name} (shown once — store it safely).")
72
+ else:
73
+ typer.echo(
74
+ "Warning: core did not return a runtime API key. "
75
+ "Set Agent access key in Studio and AGENT_SEED_SECRET in env.prod before push.",
76
+ err=True,
77
+ )
68
78
  typer.echo("Edit dooers.yaml (message_path, whatsapp, profile) then run dooers push.")
69
79
 
70
80
 
81
+ @app.command("rotate-key")
82
+ def rotate_key(
83
+ ctx: typer.Context,
84
+ agent_id: str = typer.Argument(..., help="Agent id to rotate the runtime API key for."),
85
+ ) -> None:
86
+ store, _, _ = _store(ctx)
87
+ try:
88
+ rec = store.regenerate_runtime_api_key(agent_id)
89
+ except AgentStoreError as e:
90
+ typer.echo(f"rotate-key failed: {e}", err=True)
91
+ raise typer.Exit(code=1) from e
92
+ if rec.runtime_api_key:
93
+ env_path = upsert_agent_seed_secret(Path.cwd(), rec.runtime_api_key)
94
+ typer.echo(f"Wrote AGENT_SEED_SECRET to {env_path.name} (shown once — store it safely).")
95
+ else:
96
+ typer.echo(
97
+ "Warning: core did not return a runtime API key. Check Studio for the new key.",
98
+ err=True,
99
+ )
100
+ typer.echo("Run dooers push to deploy the updated secret to your agent host.")
101
+
102
+
71
103
  @app.command(name="list")
72
104
  def list_agents(ctx: typer.Context, org: str | None = typer.Option(None, "--org")) -> None:
73
105
  store, settings, _ = _store(ctx)
@@ -0,0 +1,31 @@
1
+ """Helpers for maintaining env.prod alongside dooers push."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+
8
+ _ENV_PROD = "env.prod"
9
+ _SEED_LINE = re.compile(r"^AGENT_SEED_SECRET=.*$", re.MULTILINE)
10
+
11
+
12
+ def upsert_agent_seed_secret(directory: Path, secret: str) -> Path:
13
+ """Write or replace AGENT_SEED_SECRET in env.prod (creates file if missing)."""
14
+ path = directory / _ENV_PROD
15
+ line = f"AGENT_SEED_SECRET={secret.strip()}\n"
16
+ if path.is_file():
17
+ text = path.read_text(encoding="utf-8")
18
+ if _SEED_LINE.search(text):
19
+ text = _SEED_LINE.sub(line.rstrip("\n"), text)
20
+ else:
21
+ if text and not text.endswith("\n"):
22
+ text += "\n"
23
+ text += "\n# Runtime API key (core ↔ agent settings.seed)\n" + line
24
+ path.write_text(text, encoding="utf-8")
25
+ else:
26
+ path.write_text(
27
+ "# Production runtime env (uploaded by dooers push; never commit secrets to git)\n"
28
+ + line,
29
+ encoding="utf-8",
30
+ )
31
+ return path
@@ -13,6 +13,8 @@ from pathlib import Path
13
13
  DEFAULT_IGNORE_PATTERNS: list[str] = [
14
14
  ".git/",
15
15
  ".gitignore",
16
+ ".env",
17
+ ".env.*",
16
18
  ".venv/",
17
19
  "venv/",
18
20
  "node_modules/",
@@ -15,11 +15,16 @@ def _norm_path(p: str) -> str:
15
15
  def build_agent_patch(manifest: AgentManifest, deployed_url: str) -> dict:
16
16
  """Map the declarative manifest to a core v2 PATCH /agents/:id body.
17
17
 
18
- Derives serverConfig.apiMessagesUrl (and whatsapp inbound) from the
19
- deployed host + the declared paths. Only includes keys the creator set.
18
+ Records the deployed URL as ``hostUrl`` and derives
19
+ serverConfig.apiMessagesUrl (and whatsapp inbound) from the deployed host +
20
+ the declared paths. Only includes keys the creator set.
21
+
22
+ The CLI writes ``hostUrl`` here (with the creator's token) because the async
23
+ push completion runs in a Pub/Sub webhook that has no core credentials, so
24
+ dooers-push can no longer write it back itself.
20
25
  """
21
26
  host = _host_and_seg(deployed_url)
22
- patch: dict = {}
27
+ patch: dict = {"hostUrl": deployed_url}
23
28
 
24
29
  # Only sync a non-empty description. An empty/absent one means "leave as-is"
25
30
  # — never send null, which would wipe a description set elsewhere (e.g. Studio).
@@ -17,7 +17,12 @@ from dooers.protocol.push import (
17
17
 
18
18
  from dooers.cli import config, ignore
19
19
  from dooers.cli.manifest_sync import build_agent_patch
20
- from dooers.cli.push_client import PushClient, PushClientError, PushTransientError
20
+ from dooers.cli.push_client import (
21
+ PushClient,
22
+ PushClientError,
23
+ PushTransientError,
24
+ friendly_push_error,
25
+ )
21
26
  from dooers.cli.settings import Settings
22
27
  from dooers.cli.token_store import TokenStore, is_token_expired
23
28
 
@@ -153,7 +158,8 @@ def push(
153
158
  agent_id=agent_id, archive_path=archive_path, tag=tag, env=target_env
154
159
  )
155
160
  except PushClientError as e:
156
- typer.echo(f"Push failed: {e}", err=True)
161
+ code = e.envelope.error_code if e.envelope else None
162
+ typer.echo(f"Push failed: {friendly_push_error(code, str(e))}", err=True)
157
163
  raise typer.Exit(code=1) from e
158
164
  finally:
159
165
  os.unlink(archive_path)
@@ -170,7 +176,7 @@ def push(
170
176
  typer.echo("\nAudit: 0 endpoints detected.")
171
177
 
172
178
  # Poll until terminal, relabeling the spinner with the current phase.
173
- spinner_state: dict[str, Callable[[], None] | str] = {}
179
+ spinner_state: dict[str, object] = {}
174
180
 
175
181
  def _start_spinner(label: str) -> None:
176
182
  cancel = spinner_state.get("cancel")
@@ -3,7 +3,7 @@
3
3
  from pathlib import Path
4
4
 
5
5
  import httpx
6
- from dooers.protocol.errors import ErrorEnvelope
6
+ from dooers.protocol.errors import ErrorCode, ErrorEnvelope
7
7
  from dooers.protocol.push import BuildStatusResponse, PushAcceptedResponse
8
8
  from dooers.protocol.teardown import TeardownResponse
9
9
 
@@ -11,6 +11,19 @@ from dooers.protocol.teardown import TeardownResponse
11
11
  _TRANSIENT_STATUS = frozenset({502, 503, 504})
12
12
 
13
13
 
14
+ def friendly_push_error(error_code: object, message: str) -> str:
15
+ """Map a server error code to a CLI-friendly message; fall back to `message`.
16
+
17
+ `error_code` may be an `ErrorCode` (a str enum) or a plain string.
18
+ """
19
+ if error_code == ErrorCode.org_not_provisioned.value:
20
+ return (
21
+ "Your organization isn't set up for agent hosting yet. "
22
+ "Contact Dooers to enable hosting for your organization."
23
+ )
24
+ return message
25
+
26
+
14
27
  class PushClientError(RuntimeError):
15
28
  def __init__(self, message: str, envelope: ErrorEnvelope | None = None) -> None:
16
29
  super().__init__(message)
@@ -93,6 +93,38 @@ def collect_issues(root: Path | None = None) -> list[ValidationIssue]:
93
93
  )
94
94
  )
95
95
 
96
+ for env_path in [base / ".env", *base.glob("env.*")]:
97
+ if not env_path.is_file():
98
+ continue
99
+ for raw in env_path.read_text().splitlines():
100
+ line = raw.strip()
101
+ if not line or line.startswith("#") or "=" not in line:
102
+ continue
103
+ key, _, value = line.partition("=")
104
+ if key.strip().upper() in {
105
+ "AGENT_DATABASE_HOST",
106
+ "DATABASE_HOST",
107
+ "POSTGRES_HOST",
108
+ } and value.strip().lower() in {"127.0.0.1", "localhost", "0.0.0.0"}:
109
+ issues.append(
110
+ ValidationIssue(
111
+ "warning",
112
+ f"{env_path.name} sets {key.strip()} to localhost — "
113
+ "hosted deploys need a cloud database configured in Dooers Studio",
114
+ )
115
+ )
116
+
117
+ manifest = read_manifest(directory=base) if manifest_path.exists() else None
118
+ if manifest is not None and manifest.hosting and not (base / "env.prod").is_file():
119
+ issues.append(
120
+ ValidationIssue(
121
+ "warning",
122
+ "hosting is enabled but env.prod is missing — "
123
+ "create env.prod with production runtime variables (OPENAI_API_KEY, "
124
+ "AGENT_DATABASE_*, etc.) for Cloud Run deploys",
125
+ )
126
+ )
127
+
96
128
  return issues
97
129
 
98
130
 
@@ -99,3 +99,24 @@ def test_get_populates_status():
99
99
  )
100
100
  rec = HTTPCoreAgentStore(BASE, "tok").get(A)
101
101
  assert rec.status == "active"
102
+
103
+
104
+ @respx.mock
105
+ def test_delete_nonjson_403_raises_clean_error_not_jsondecode():
106
+ # Core returns a framework-level 403 with a plain-text body (not JSON).
107
+ respx.delete(f"{BASE}/api/v2/agents/{A}").mock(
108
+ return_value=httpx.Response(403, text="Forbidden")
109
+ )
110
+ store = HTTPCoreAgentStore(BASE, "tok")
111
+ with pytest.raises(AgentStoreError) as ei:
112
+ store.delete(A)
113
+ msg = str(ei.value)
114
+ assert "403" in msg and "Forbidden" in msg # clean message, no JSONDecodeError
115
+
116
+
117
+ @respx.mock
118
+ def test_delete_empty_success_body_does_not_crash():
119
+ # A 204/empty success body must not blow up json() parsing.
120
+ respx.delete(f"{BASE}/api/v2/agents/{A}").mock(return_value=httpx.Response(204))
121
+ store = HTTPCoreAgentStore(BASE, "tok")
122
+ store.delete(A) # should simply return without raising
@@ -0,0 +1,19 @@
1
+ from pathlib import Path
2
+
3
+ from dooers.cli.env_prod import upsert_agent_seed_secret
4
+
5
+
6
+ def test_upsert_creates_env_prod(tmp_path: Path) -> None:
7
+ path = upsert_agent_seed_secret(tmp_path, "secret-one-time-key-abc")
8
+ assert path.name == "env.prod"
9
+ assert path.read_text(encoding="utf-8").strip().endswith("AGENT_SEED_SECRET=secret-one-time-key-abc")
10
+
11
+
12
+ def test_upsert_replaces_existing_line(tmp_path: Path) -> None:
13
+ env = tmp_path / "env.prod"
14
+ env.write_text("FOO=1\nAGENT_SEED_SECRET=old\nBAR=2\n", encoding="utf-8")
15
+ upsert_agent_seed_secret(tmp_path, "new-key-xxxxxxxxxxxx")
16
+ text = env.read_text(encoding="utf-8")
17
+ assert "AGENT_SEED_SECRET=new-key-xxxxxxxxxxxx" in text
18
+ assert "AGENT_SEED_SECRET=old" not in text
19
+ assert "FOO=1" in text
@@ -166,14 +166,20 @@ def test_description_empty_string_not_in_patch():
166
166
 
167
167
 
168
168
  # ---------------------------------------------------------------------------
169
- # minimal manifest → empty patch
169
+ # hostUrl — always recorded (the CLI writes it; the async webhook can't)
170
170
  # ---------------------------------------------------------------------------
171
171
 
172
172
 
173
- def test_minimal_manifest_empty_patch():
173
+ def test_minimal_manifest_records_host_url_only():
174
174
  m = _manifest()
175
175
  patch = build_agent_patch(m, DEPLOYED)
176
- assert patch == {}
176
+ assert patch == {"hostUrl": DEPLOYED}
177
+
178
+
179
+ def test_host_url_always_present_alongside_other_fields():
180
+ m = _manifest(message_path="/agent", description="A helpful agent")
181
+ patch = build_agent_patch(m, DEPLOYED)
182
+ assert patch["hostUrl"] == DEPLOYED
177
183
 
178
184
 
179
185
  # ---------------------------------------------------------------------------
@@ -0,0 +1,44 @@
1
+ """Push failure display tests."""
2
+
3
+ from dooers.protocol.errors import ErrorCode
4
+ from dooers.protocol.push import BuildStatus, PushResponse, format_push_failure
5
+
6
+ from dooers.cli.push_client import friendly_push_error
7
+
8
+
9
+ def test_friendly_push_error_org_not_provisioned() -> None:
10
+ msg = friendly_push_error(ErrorCode.org_not_provisioned, "raw server message")
11
+ assert "set up for agent hosting" in msg.lower()
12
+ assert "raw server message" not in msg
13
+
14
+
15
+ def test_friendly_push_error_accepts_plain_string_code() -> None:
16
+ assert "hosting" in friendly_push_error("org_not_provisioned", "x").lower()
17
+
18
+
19
+ def test_friendly_push_error_falls_back_to_message() -> None:
20
+ assert friendly_push_error(ErrorCode.forbidden, "you do not own this agent") == (
21
+ "you do not own this agent"
22
+ )
23
+
24
+
25
+ def test_format_push_failure_includes_step_and_logs() -> None:
26
+ response = PushResponse(
27
+ agent_id="agent-1",
28
+ build_id="build-123",
29
+ image="img",
30
+ status=BuildStatus.failed,
31
+ error="docker build failed: missing Dockerfile",
32
+ failed_step="build image",
33
+ build_log_url="https://console.cloud.google.com/cloud-build/builds/build-123?project=proj",
34
+ )
35
+
36
+ text = format_push_failure(response)
37
+
38
+ assert "Deployment failed" in text
39
+ assert "Failed during: build image" in text
40
+ assert "docker build failed" in text
41
+ # Console URL (embeds the project id) must NOT be surfaced; a sanitized
42
+ # build reference is shown instead. See de1b80c.
43
+ assert "Build logs:" not in text
44
+ assert "Reference: build build-123" in text
@@ -80,3 +80,48 @@ organization_id: "org_1"
80
80
 
81
81
  issues = collect_issues(tmp_path)
82
82
  assert any("legacy SDK imports" in i.message for i in issues)
83
+
84
+
85
+ def test_validate_warns_on_localhost_database_host(tmp_path: Path) -> None:
86
+ _write(
87
+ tmp_path / "dooers.yaml",
88
+ """
89
+ protocol_version: "2"
90
+ agent_id: "550e8400-e29b-41d4-a716-446655440000"
91
+ name: test
92
+ organization_id: "org_1"
93
+ hosting: true
94
+ """.strip()
95
+ + "\n",
96
+ )
97
+ _write(tmp_path / "Dockerfile", "FROM python:3.12-slim\n")
98
+ _write(
99
+ tmp_path / "pyproject.toml",
100
+ '[project]\ndependencies = ["dooers-agents-server>=0.12.0"]\n',
101
+ )
102
+ _write(tmp_path / ".env", "AGENT_DATABASE_HOST=127.0.0.1\n")
103
+
104
+ issues = collect_issues(tmp_path)
105
+ assert any("localhost" in i.message.lower() for i in issues)
106
+
107
+
108
+ def test_validate_warns_when_hosting_without_env_prod(tmp_path: Path) -> None:
109
+ _write(
110
+ tmp_path / "dooers.yaml",
111
+ """
112
+ protocol_version: "2"
113
+ agent_id: "550e8400-e29b-41d4-a716-446655440000"
114
+ name: test
115
+ organization_id: "org_1"
116
+ hosting: true
117
+ """.strip()
118
+ + "\n",
119
+ )
120
+ _write(tmp_path / "Dockerfile", "FROM python:3.12-slim\n")
121
+ _write(
122
+ tmp_path / "pyproject.toml",
123
+ '[project]\ndependencies = ["dooers-agents-server>=0.12.0"]\n',
124
+ )
125
+
126
+ issues = collect_issues(tmp_path)
127
+ assert any("env.prod is missing" in i.message for i in issues)
@@ -232,7 +232,7 @@ toml = [
232
232
 
233
233
  [[package]]
234
234
  name = "dooers-cli"
235
- version = "0.7.0"
235
+ version = "0.7.2"
236
236
  source = { editable = "." }
237
237
  dependencies = [
238
238
  { name = "dooers-protocol" },
@@ -278,7 +278,7 @@ provides-extras = ["dev"]
278
278
 
279
279
  [[package]]
280
280
  name = "dooers-protocol"
281
- version = "0.6.0"
281
+ version = "0.6.1"
282
282
  source = { editable = "../dooers-protocol" }
283
283
  dependencies = [
284
284
  { name = "pydantic" },
@@ -1,22 +0,0 @@
1
- """Push failure display tests."""
2
-
3
- from dooers.protocol.push import BuildStatus, PushResponse, format_push_failure
4
-
5
-
6
- def test_format_push_failure_includes_step_and_logs() -> None:
7
- response = PushResponse(
8
- agent_id="agent-1",
9
- build_id="build-123",
10
- image="img",
11
- status=BuildStatus.failed,
12
- error="docker build failed: missing Dockerfile",
13
- failed_step="build image",
14
- build_log_url="https://console.cloud.google.com/cloud-build/builds/build-123?project=proj",
15
- )
16
-
17
- text = format_push_failure(response)
18
-
19
- assert "Deployment failed" in text
20
- assert "Failed during: build image" in text
21
- assert "docker build failed" in text
22
- assert "Build logs:" in text
File without changes
File without changes
File without changes
File without changes