dominus-sdk-python 5.0.0__tar.gz → 5.1.0__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 (63) hide show
  1. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/PKG-INFO +8 -2
  2. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/README.md +7 -1
  3. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/__init__.py +1 -7
  4. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/namespaces/artifacts.py +10 -27
  5. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/namespaces/workflow.py +40 -8
  6. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus_sdk_python.egg-info/PKG-INFO +8 -2
  7. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/pyproject.toml +1 -1
  8. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/tests/test_workflow_lifecycle.py +17 -0
  9. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/tests/test_workflow_refs.py +42 -1
  10. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/config/__init__.py +0 -0
  11. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/config/endpoints.py +0 -0
  12. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/errors.py +0 -0
  13. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/helpers/__init__.py +0 -0
  14. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/helpers/auth.py +0 -0
  15. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/helpers/cache.py +0 -0
  16. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/helpers/console_capture.py +0 -0
  17. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/helpers/core.py +0 -0
  18. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/helpers/crypto.py +0 -0
  19. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/helpers/sse.py +0 -0
  20. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/helpers/trace.py +0 -0
  21. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/namespaces/__init__.py +0 -0
  22. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/namespaces/admin.py +0 -0
  23. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/namespaces/ai.py +0 -0
  24. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/namespaces/auth.py +0 -0
  25. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/namespaces/authority.py +0 -0
  26. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/namespaces/browser.py +0 -0
  27. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/namespaces/courier.py +0 -0
  28. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/namespaces/db.py +0 -0
  29. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/namespaces/ddl.py +0 -0
  30. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/namespaces/deployer.py +0 -0
  31. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/namespaces/fastapi.py +0 -0
  32. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/namespaces/files.py +0 -0
  33. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/namespaces/health.py +0 -0
  34. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/namespaces/jobs.py +0 -0
  35. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/namespaces/logs.py +0 -0
  36. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/namespaces/portal.py +0 -0
  37. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/namespaces/processor.py +0 -0
  38. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/namespaces/recipes.py +0 -0
  39. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/namespaces/redis.py +0 -0
  40. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/namespaces/secrets.py +0 -0
  41. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/namespaces/secure.py +0 -0
  42. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/namespaces/stash.py +0 -0
  43. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/namespaces/sync.py +0 -0
  44. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/namespaces/warden.py +0 -0
  45. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/services/__init__.py +0 -0
  46. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus/start.py +0 -0
  47. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus_sdk_python.egg-info/SOURCES.txt +0 -0
  48. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus_sdk_python.egg-info/dependency_links.txt +0 -0
  49. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus_sdk_python.egg-info/requires.txt +0 -0
  50. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/dominus_sdk_python.egg-info/top_level.txt +0 -0
  51. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/setup.cfg +0 -0
  52. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/tests/test_auth.py +0 -0
  53. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/tests/test_authority_public_vocabulary.py +0 -0
  54. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/tests/test_browser_namespace.py +0 -0
  55. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/tests/test_control_plane_namespaces.py +0 -0
  56. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/tests/test_errors.py +0 -0
  57. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/tests/test_flat_commands.py +0 -0
  58. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/tests/test_health.py +0 -0
  59. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/tests/test_logs.py +0 -0
  60. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/tests/test_provisioning_parity.py +0 -0
  61. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/tests/test_public_exports.py +0 -0
  62. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/tests/test_recipes_namespace.py +0 -0
  63. {dominus_sdk_python-5.0.0 → dominus_sdk_python-5.1.0}/tests/test_transport_compat.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominus-sdk-python
3
- Version: 5.0.0
3
+ Version: 5.1.0
4
4
  Summary: Python SDK for the Dominus gateway-first platform
5
5
  Author-email: CareBridge Systems <dev@carebridge.io>
6
6
  License: Proprietary
@@ -43,7 +43,7 @@ Async Python SDK for the Dominus gateway-first service plane.
43
43
  - Gateway-scoped client mode for MCP and other user-JWT sessions
44
44
  - Transport compatibility for wrapped `{success,data}` responses and unwrapped Warden/control-plane success objects
45
45
  - Local helpers for JWT verification, trace propagation, retries, and console capture
46
- - Current package version: `4.0.8`
46
+ - Current package version: `5.1.0`
47
47
 
48
48
  ## Install
49
49
 
@@ -71,6 +71,12 @@ run = await dominus.workflow.ensure(
71
71
  company="summit-radiology",
72
72
  )
73
73
 
74
+ recipe_run = await dominus.workflow.ensure(
75
+ workflow_recipe_ref="recipe://workflow-recipe-v1/report-cycle@v3",
76
+ subject="PCM47474562",
77
+ company="summit-radiology",
78
+ )
79
+
74
80
  timeline = await dominus.authority.get_run_timeline(
75
81
  run["run_id"],
76
82
  since="2026-04-11T08:33:00Z",
@@ -9,7 +9,7 @@ Async Python SDK for the Dominus gateway-first service plane.
9
9
  - Gateway-scoped client mode for MCP and other user-JWT sessions
10
10
  - Transport compatibility for wrapped `{success,data}` responses and unwrapped Warden/control-plane success objects
11
11
  - Local helpers for JWT verification, trace propagation, retries, and console capture
12
- - Current package version: `4.0.8`
12
+ - Current package version: `5.1.0`
13
13
 
14
14
  ## Install
15
15
 
@@ -37,6 +37,12 @@ run = await dominus.workflow.ensure(
37
37
  company="summit-radiology",
38
38
  )
39
39
 
40
+ recipe_run = await dominus.workflow.ensure(
41
+ workflow_recipe_ref="recipe://workflow-recipe-v1/report-cycle@v3",
42
+ subject="PCM47474562",
43
+ company="summit-radiology",
44
+ )
45
+
40
46
  timeline = await dominus.authority.get_run_timeline(
41
47
  run["run_id"],
42
48
  since="2026-04-11T08:33:00Z",
@@ -90,14 +90,12 @@ from .namespaces.health import HealthNamespace
90
90
  # Export new namespaces (Node.js SDK parity)
91
91
  from .namespaces.artifacts import (
92
92
  ARTIFACT_REF_PREFIX,
93
- DISPLAY_REF_PREFIX,
94
93
  ARTIFACT_ENVIRONMENTS,
95
94
  ArtifactsNamespace,
96
95
  build_artifact_ref,
97
96
  build_v2_artifact_ref,
98
97
  build_pinned_artifact_ref,
99
98
  build_legacy_artifact_ref,
100
- build_display_artifact_ref,
101
99
  parse_artifact_ref,
102
100
  try_parse_artifact_ref,
103
101
  validate_artifact_address,
@@ -105,7 +103,6 @@ from .namespaces.artifacts import (
105
103
  is_pinned_artifact_ref,
106
104
  is_head_artifact_ref,
107
105
  is_legacy_artifact_ref,
108
- is_display_artifact_ref,
109
106
  )
110
107
  from .namespaces.jobs import JobsNamespace
111
108
  from .namespaces.processor import ProcessorNamespace
@@ -165,7 +162,7 @@ from .errors import (
165
162
  TimeoutError as DominusTimeoutError,
166
163
  )
167
164
 
168
- __version__ = "4.6.2"
165
+ __version__ = "5.1.0"
169
166
  __all__ = [
170
167
  # Main SDK instance
171
168
  "dominus",
@@ -195,14 +192,12 @@ __all__ = [
195
192
  "HealthNamespace",
196
193
  # New namespaces (Node.js SDK parity)
197
194
  "ARTIFACT_REF_PREFIX",
198
- "DISPLAY_REF_PREFIX",
199
195
  "ARTIFACT_ENVIRONMENTS",
200
196
  "ArtifactsNamespace",
201
197
  "build_artifact_ref",
202
198
  "build_v2_artifact_ref",
203
199
  "build_pinned_artifact_ref",
204
200
  "build_legacy_artifact_ref",
205
- "build_display_artifact_ref",
206
201
  "parse_artifact_ref",
207
202
  "try_parse_artifact_ref",
208
203
  "validate_artifact_address",
@@ -210,7 +205,6 @@ __all__ = [
210
205
  "is_pinned_artifact_ref",
211
206
  "is_head_artifact_ref",
212
207
  "is_legacy_artifact_ref",
213
- "is_display_artifact_ref",
214
208
  "JobsNamespace",
215
209
  "ProcessorNamespace",
216
210
  "SyncNamespace",
@@ -35,10 +35,13 @@ def _safe_string(value: Any) -> str:
35
35
 
36
36
 
37
37
  ARTIFACT_REF_PREFIX = "ar://"
38
- DISPLAY_REF_PREFIX = "art:r:"
39
38
  ARTIFACT_ENVIRONMENTS = ("development", "staging", "production")
40
39
 
41
40
 
41
+ def _legacy_project_head_ref(project_id: str, environment: str, key: str) -> str:
42
+ return f"{ARTIFACT_REF_PREFIX}dominus/project:{project_id}/{environment}/legacy/{quote(key, safe='')}"
43
+
44
+
42
45
  def build_artifact_ref(
43
46
  *,
44
47
  project_slug: Optional[str] = None,
@@ -109,13 +112,6 @@ def build_pinned_artifact_ref(
109
112
  return f"{ref}@v{version}"
110
113
 
111
114
 
112
- def build_display_artifact_ref(artifact_key: str) -> Optional[str]:
113
- normalized_key = _safe_string(artifact_key)
114
- if not normalized_key:
115
- return None
116
- return f"{DISPLAY_REF_PREFIX}{normalized_key}"
117
-
118
-
119
115
  def build_legacy_artifact_ref(
120
116
  *,
121
117
  project_slug: str,
@@ -146,15 +142,6 @@ def parse_artifact_ref(artifact_ref: str) -> Optional[Dict[str, Any]]:
146
142
  normalized = _safe_string(artifact_ref)
147
143
  if not normalized:
148
144
  return None
149
- if normalized.startswith(DISPLAY_REF_PREFIX):
150
- artifact_key = normalized[len(DISPLAY_REF_PREFIX):]
151
- if not artifact_key:
152
- return None
153
- return {
154
- "format": "display",
155
- "raw": normalized,
156
- "artifact_key": artifact_key,
157
- }
158
145
  if not normalized.startswith(ARTIFACT_REF_PREFIX):
159
146
  return None
160
147
 
@@ -245,10 +232,6 @@ def is_legacy_artifact_ref(artifact_ref: Dict[str, Any]) -> bool:
245
232
  return artifact_ref.get("format") == "v1"
246
233
 
247
234
 
248
- def is_display_artifact_ref(artifact_ref: Dict[str, Any]) -> bool:
249
- return artifact_ref.get("format") == "display"
250
-
251
-
252
235
  def _split_version_suffix(value: str) -> tuple[str, Optional[str]]:
253
236
  if "@" not in value:
254
237
  return value, None
@@ -366,10 +349,10 @@ class ArtifactsNamespace:
366
349
  category=category,
367
350
  content_type=content_type,
368
351
  )
369
- disp = build_display_artifact_ref(use_key)
352
+ fallback_ref = _legacy_project_head_ref(project_id, environment, use_key)
370
353
  return {
371
354
  "key": use_key,
372
- "ref": v2.get("compatibility_ref") or v2.get("head_ref") or disp,
355
+ "ref": v2.get("head_ref") or fallback_ref,
373
356
  "storage_type": v2.get("storage_type") or "redis",
374
357
  "size_bytes": v2.get("size_bytes", 0),
375
358
  "expires_at": v2.get("expires_at") or "",
@@ -377,14 +360,14 @@ class ArtifactsNamespace:
377
360
 
378
361
  async def retrieve(self, key: str) -> Dict[str, Any]:
379
362
  """
380
- Retrieve by key via V2 display ref + target_project_id (compat with legacy data).
363
+ Retrieve by key via the addressed V2 compatibility head.
381
364
  """
382
- project_id, _environment = await self._legacy_project_context()
365
+ project_id, environment = await self._legacy_project_context()
366
+ ref = _legacy_project_head_ref(project_id, environment, key)
383
367
  return await self._api(
384
368
  "/api/artifact/v2/retrieve",
385
369
  body={
386
- "ref": f"{DISPLAY_REF_PREFIX}{key}",
387
- "target_project_id": project_id,
370
+ "ref": ref,
388
371
  },
389
372
  )
390
373
 
@@ -104,6 +104,14 @@ class WorkflowNamespace:
104
104
  return {"workflow_id": str(workflow_id).strip()}
105
105
  raise ValueError("workflow_id or workflow_ref is required")
106
106
 
107
+ @staticmethod
108
+ def _is_workflow_recipe_ref(value: Optional[str]) -> bool:
109
+ return str(value or "").strip().startswith("recipe://workflow-recipe-v1/")
110
+
111
+ @staticmethod
112
+ def _is_pipeline_recipe_ref(value: Optional[str]) -> bool:
113
+ return str(value or "").strip().startswith("recipe://pipeline-recipe-v1/")
114
+
107
115
  @staticmethod
108
116
  def _instance_base_endpoint(workflow_id: str) -> str:
109
117
  from urllib.parse import quote
@@ -210,9 +218,11 @@ class WorkflowNamespace:
210
218
 
211
219
  def _build_authority_ensure_body(
212
220
  self,
213
- workflow_id: str,
221
+ workflow_id: Optional[str] = None,
214
222
  *,
215
223
  workflow_ref: Optional[str] = None,
224
+ workflow_recipe_ref: Optional[str] = None,
225
+ pipeline_recipe_ref: Optional[str] = None,
216
226
  subject: Optional[str] = None,
217
227
  company: Optional[str] = None,
218
228
  inputs: Optional[Dict[str, Any]] = None,
@@ -227,12 +237,30 @@ class WorkflowNamespace:
227
237
  idempotency_key: Optional[str] = None,
228
238
  ) -> Dict[str, Any]:
229
239
  body: Dict[str, Any] = {"mode": mode}
230
- if workflow_ref:
231
- body["workflow_ref"] = workflow_ref
232
- elif self._is_workflow_ref(workflow_id):
233
- body["workflow_ref"] = workflow_id
234
- else:
235
- body["workflow_id"] = workflow_id
240
+ launch_sources: List[tuple[str, str]] = []
241
+ normalized_workflow_id = str(workflow_id or "").strip()
242
+ if workflow_ref and str(workflow_ref).strip():
243
+ launch_sources.append(("workflow_ref", str(workflow_ref).strip()))
244
+ if workflow_recipe_ref and str(workflow_recipe_ref).strip():
245
+ launch_sources.append(("workflow_recipe_ref", str(workflow_recipe_ref).strip()))
246
+ if pipeline_recipe_ref and str(pipeline_recipe_ref).strip():
247
+ launch_sources.append(("pipeline_recipe_ref", str(pipeline_recipe_ref).strip()))
248
+ if normalized_workflow_id:
249
+ if self._is_workflow_ref(normalized_workflow_id):
250
+ launch_sources.append(("workflow_ref", normalized_workflow_id))
251
+ elif self._is_workflow_recipe_ref(normalized_workflow_id):
252
+ launch_sources.append(("workflow_recipe_ref", normalized_workflow_id))
253
+ elif self._is_pipeline_recipe_ref(normalized_workflow_id):
254
+ launch_sources.append(("pipeline_recipe_ref", normalized_workflow_id))
255
+ else:
256
+ launch_sources.append(("workflow_id", normalized_workflow_id))
257
+ if len(launch_sources) != 1:
258
+ raise ValueError(
259
+ "ensure requires exactly one of workflow_id, workflow_ref, "
260
+ "workflow_recipe_ref, or pipeline_recipe_ref"
261
+ )
262
+ field, value = launch_sources[0]
263
+ body[field] = value
236
264
  if subject is not None:
237
265
  body["subject"] = subject
238
266
  if company is not None:
@@ -855,7 +883,7 @@ class WorkflowNamespace:
855
883
 
856
884
  async def ensure(
857
885
  self,
858
- workflow_id: str,
886
+ workflow_id: Optional[str] = None,
859
887
  *,
860
888
  instance_id: Optional[str] = None,
861
889
  group: Optional[str] = None,
@@ -866,6 +894,8 @@ class WorkflowNamespace:
866
894
  mode: str = "blocking",
867
895
  context: Optional[Dict[str, Any]] = None,
868
896
  workflow_ref: Optional[str] = None,
897
+ workflow_recipe_ref: Optional[str] = None,
898
+ pipeline_recipe_ref: Optional[str] = None,
869
899
  subject: Optional[str] = None,
870
900
  company: Optional[str] = None,
871
901
  inputs: Optional[Dict[str, Any]] = None,
@@ -886,6 +916,8 @@ class WorkflowNamespace:
886
916
  body=self._build_authority_ensure_body(
887
917
  workflow_id,
888
918
  workflow_ref=workflow_ref,
919
+ workflow_recipe_ref=workflow_recipe_ref,
920
+ pipeline_recipe_ref=pipeline_recipe_ref,
889
921
  subject=subject,
890
922
  company=company,
891
923
  inputs=inputs,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominus-sdk-python
3
- Version: 5.0.0
3
+ Version: 5.1.0
4
4
  Summary: Python SDK for the Dominus gateway-first platform
5
5
  Author-email: CareBridge Systems <dev@carebridge.io>
6
6
  License: Proprietary
@@ -43,7 +43,7 @@ Async Python SDK for the Dominus gateway-first service plane.
43
43
  - Gateway-scoped client mode for MCP and other user-JWT sessions
44
44
  - Transport compatibility for wrapped `{success,data}` responses and unwrapped Warden/control-plane success objects
45
45
  - Local helpers for JWT verification, trace propagation, retries, and console capture
46
- - Current package version: `4.0.8`
46
+ - Current package version: `5.1.0`
47
47
 
48
48
  ## Install
49
49
 
@@ -71,6 +71,12 @@ run = await dominus.workflow.ensure(
71
71
  company="summit-radiology",
72
72
  )
73
73
 
74
+ recipe_run = await dominus.workflow.ensure(
75
+ workflow_recipe_ref="recipe://workflow-recipe-v1/report-cycle@v3",
76
+ subject="PCM47474562",
77
+ company="summit-radiology",
78
+ )
79
+
74
80
  timeline = await dominus.authority.get_run_timeline(
75
81
  run["run_id"],
76
82
  since="2026-04-11T08:33:00Z",
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dominus-sdk-python"
7
- version = "5.0.0"
7
+ version = "5.1.0"
8
8
  description = "Python SDK for the Dominus gateway-first platform"
9
9
  readme = "README.md"
10
10
  license = {text = "Proprietary"}
@@ -274,6 +274,23 @@ async def test_workflow_ensure_supports_authority_one_call_lifecycle():
274
274
  assert isinstance(body["idempotency_key"], str) and body["idempotency_key"]
275
275
 
276
276
 
277
+ @pytest.mark.asyncio
278
+ async def test_workflow_ensure_can_infer_recipe_ref_from_first_argument():
279
+ client = FakeClient()
280
+ namespace = WorkflowNamespace(client)
281
+
282
+ await namespace.ensure(
283
+ "recipe://workflow-recipe-v1/report-cycle@v3",
284
+ company="summit-radiology",
285
+ mode="async",
286
+ )
287
+
288
+ body = client.calls[0]["body"]
289
+ assert body["workflow_recipe_ref"] == "recipe://workflow-recipe-v1/report-cycle@v3"
290
+ assert body["company"] == "summit-radiology"
291
+ assert "workflow_id" not in body
292
+
293
+
277
294
  @pytest.mark.asyncio
278
295
  async def test_workflow_get_detects_authority_run_ids():
279
296
  client = FakeClient()
@@ -76,6 +76,47 @@ async def test_workflow_ensure_accepts_workflow_ref():
76
76
  assert body["context"] == {"report_ref": {"report_id": "r-1"}}
77
77
 
78
78
 
79
+ @pytest.mark.asyncio
80
+ async def test_workflow_ensure_accepts_recipe_refs():
81
+ client = FakeClient()
82
+ namespace = WorkflowNamespace(client)
83
+
84
+ await namespace.ensure(
85
+ workflow_recipe_ref="recipe://workflow-recipe-v1/patient-intake@v4",
86
+ subject="sub-1",
87
+ company="co-1",
88
+ mode="async",
89
+ )
90
+ await namespace.ensure(
91
+ pipeline_recipe_ref="recipe://pipeline-recipe-v1/intake-pipeline@v2",
92
+ context={"report_ref": {"report_id": "r-1"}},
93
+ mode="async",
94
+ )
95
+
96
+ first = client.calls[0]["body"]
97
+ assert first["workflow_recipe_ref"] == "recipe://workflow-recipe-v1/patient-intake@v4"
98
+ assert first["subject"] == "sub-1"
99
+ assert "workflow_id" not in first
100
+ second = client.calls[1]["body"]
101
+ assert second["pipeline_recipe_ref"] == "recipe://pipeline-recipe-v1/intake-pipeline@v2"
102
+ assert second["context"] == {"report_ref": {"report_id": "r-1"}}
103
+ assert "workflow_ref" not in second
104
+
105
+
106
+ @pytest.mark.asyncio
107
+ async def test_workflow_ensure_rejects_mixed_legacy_and_recipe_refs():
108
+ client = FakeClient()
109
+ namespace = WorkflowNamespace(client)
110
+
111
+ with pytest.raises(ValueError, match="exactly one"):
112
+ await namespace.ensure(
113
+ "wf-123",
114
+ workflow_recipe_ref="recipe://workflow-recipe-v1/patient-intake@v4",
115
+ )
116
+
117
+ assert client.calls == []
118
+
119
+
79
120
  @pytest.mark.asyncio
80
121
  async def test_workflow_save_forwards_builder_metadata():
81
122
  client = FakeClient()
@@ -125,7 +166,7 @@ def test_artifact_ref_helpers_round_trip():
125
166
  }
126
167
 
127
168
 
128
- def test_artifact_ref_helpers_support_v2_and_display_refs():
169
+ def test_artifact_ref_helpers_support_v2_refs():
129
170
  v2_ref = build_artifact_ref(
130
171
  group="carebridge",
131
172
  owner="carebridge-summit",