feat: discover existing workflow objects
This commit is contained in:
@@ -189,6 +189,11 @@ server state is needed.
|
|||||||
|
|
||||||
Draft workspaces are intentionally not part of `wf_client`. They are a separate
|
Draft workspaces are intentionally not part of `wf_client`. They are a separate
|
||||||
server/admin surface and must be explicitly enabled when composing a server.
|
server/admin surface and must be explicitly enabled when composing a server.
|
||||||
|
|
||||||
|
Use `app.artifacts(...)`, `app.deployments()`, and `app.runs(...)` to discover
|
||||||
|
existing remote objects as lightweight immutable summaries. Exact loaders
|
||||||
|
(`app.workflow(...)`, `app.deployment(...)`, and `app.run(...)`) reconstruct the
|
||||||
|
selected rich object without making collection listing eager or trace-heavy.
|
||||||
- `examples/agent_challenges/` contains reusable opencode challenge harnesses
|
- `examples/agent_challenges/` contains reusable opencode challenge harnesses
|
||||||
for evaluating whether agents can use the public workflow CLI/server path.
|
for evaluating whether agents can use the public workflow CLI/server path.
|
||||||
|
|
||||||
|
|||||||
@@ -175,6 +175,12 @@ to server/admin and console callers, but normal server composition keeps draft
|
|||||||
JSON-RPC registration opt-in so artifact, deployment, and run durability do not
|
JSON-RPC registration opt-in so artifact, deployment, and run durability do not
|
||||||
depend on a draft store.
|
depend on a draft store.
|
||||||
|
|
||||||
|
Existing remote objects can be discovered without eagerly loading their full
|
||||||
|
plans, bindings, or traces. `app.artifacts(...)` and `app.runs(...)` return
|
||||||
|
paged immutable summary rows; `app.deployments()` returns an immutable tuple.
|
||||||
|
Call `app.workflow(id, version=...)`, `app.deployment(id)`, or `app.run(id)` to
|
||||||
|
reconstruct the selected rich object.
|
||||||
|
|
||||||
## WorkflowApiSurface And Domain Services
|
## WorkflowApiSurface And Domain Services
|
||||||
|
|
||||||
`WorkflowApiSurface` is the public application contract shared by local and
|
`WorkflowApiSurface` is the public application contract shared by local and
|
||||||
|
|||||||
@@ -25,6 +25,25 @@ Choose the object that matches the operation:
|
|||||||
- `Deployment`: bind one artifact version to concrete sources and validate it.
|
- `Deployment`: bind one artifact version to concrete sources and validate it.
|
||||||
- `Run`: inspect, refresh, resume, or trace one durable execution.
|
- `Run`: inspect, refresh, resume, or trace one durable execution.
|
||||||
|
|
||||||
|
Discover existing saved objects as lightweight summaries, then load the exact
|
||||||
|
object selected by the application:
|
||||||
|
|
||||||
|
```python
|
||||||
|
artifacts = await app.artifacts(query="report", kind="workflow")
|
||||||
|
deployments = await app.deployments()
|
||||||
|
runs = await app.runs(status="interrupted", limit=25)
|
||||||
|
|
||||||
|
artifact = await app.workflow(
|
||||||
|
artifacts.items[0].artifact_id,
|
||||||
|
version=artifacts.items[0].version,
|
||||||
|
)
|
||||||
|
run = await app.run(runs.items[0].run_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
Artifact and run discovery are paged. Deployment discovery returns an immutable
|
||||||
|
tuple because the server operation is not paged. Listing never reconstructs
|
||||||
|
full objects or loads run traces.
|
||||||
|
|
||||||
Do not collapse artifact saving, deployment configuration, and execution into
|
Do not collapse artifact saving, deployment configuration, and execution into
|
||||||
one invented "publish" operation.
|
one invented "publish" operation.
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from .app import App
|
|||||||
from .authoring import EditableWorkflow
|
from .authoring import EditableWorkflow
|
||||||
from .capabilities import CapabilityResult, CapabilitySummary, RemoteCapability
|
from .capabilities import CapabilityResult, CapabilitySummary, RemoteCapability
|
||||||
from .deployments import Deployment, DeploymentValidation
|
from .deployments import Deployment, DeploymentValidation
|
||||||
|
from .discovery import ArtifactSummary, DeploymentSummary, RunSummary
|
||||||
from .errors import (
|
from .errors import (
|
||||||
ArtifactNotFound,
|
ArtifactNotFound,
|
||||||
ArtifactVersionConflict,
|
ArtifactVersionConflict,
|
||||||
@@ -33,6 +34,7 @@ __all__ = [
|
|||||||
"ArtifactVersionConflict",
|
"ArtifactVersionConflict",
|
||||||
"App",
|
"App",
|
||||||
"ArtifactRef",
|
"ArtifactRef",
|
||||||
|
"ArtifactSummary",
|
||||||
"CapabilityNotFound",
|
"CapabilityNotFound",
|
||||||
"CapabilityRef",
|
"CapabilityRef",
|
||||||
"CapabilityResult",
|
"CapabilityResult",
|
||||||
@@ -42,11 +44,13 @@ __all__ = [
|
|||||||
"DeploymentRequired",
|
"DeploymentRequired",
|
||||||
"Deployment",
|
"Deployment",
|
||||||
"DeploymentValidation",
|
"DeploymentValidation",
|
||||||
|
"DeploymentSummary",
|
||||||
"InvalidResponse",
|
"InvalidResponse",
|
||||||
"Page",
|
"Page",
|
||||||
"ProtocolError",
|
"ProtocolError",
|
||||||
"RemoteCapability",
|
"RemoteCapability",
|
||||||
"Run",
|
"Run",
|
||||||
|
"RunSummary",
|
||||||
"RevisionConflict",
|
"RevisionConflict",
|
||||||
"TransportError",
|
"TransportError",
|
||||||
"ValidationFailed",
|
"ValidationFailed",
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ import httpx
|
|||||||
from wf_api.models import (
|
from wf_api.models import (
|
||||||
CapabilityCallResult,
|
CapabilityCallResult,
|
||||||
InspectCapabilityResult,
|
InspectCapabilityResult,
|
||||||
|
ListArtifactsResult,
|
||||||
ListCapabilitiesResult,
|
ListCapabilitiesResult,
|
||||||
ListDeploymentsResult,
|
ListDeploymentsResult,
|
||||||
|
ListRunsResult,
|
||||||
RunResult,
|
RunResult,
|
||||||
RunTraceResult,
|
RunTraceResult,
|
||||||
SaveArtifactResult,
|
SaveArtifactResult,
|
||||||
@@ -156,6 +158,23 @@ class PublicErrorWorkflowClientPort:
|
|||||||
version=version,
|
version=version,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def list_artifacts(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
query: str | None = None,
|
||||||
|
kind: Literal["workflow", "wrapper"] | None = None,
|
||||||
|
cursor: str | None = None,
|
||||||
|
limit: int = 50,
|
||||||
|
) -> ListArtifactsResult:
|
||||||
|
return await self._invoke(
|
||||||
|
"workflow.artifacts.list",
|
||||||
|
self._rpc.list_artifacts,
|
||||||
|
query=query,
|
||||||
|
kind=kind,
|
||||||
|
cursor=cursor,
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
|
|
||||||
async def create_artifact_from_plan(
|
async def create_artifact_from_plan(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -256,6 +275,21 @@ class PublicErrorWorkflowClientPort:
|
|||||||
run_id=run_id,
|
run_id=run_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def list_runs(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
status: str | None = None,
|
||||||
|
cursor: str | None = None,
|
||||||
|
limit: int = 50,
|
||||||
|
) -> ListRunsResult:
|
||||||
|
return await self._invoke(
|
||||||
|
"workflow.runs.list",
|
||||||
|
self._rpc.list_runs,
|
||||||
|
status=status,
|
||||||
|
cursor=cursor,
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
|
|
||||||
async def resume_run(
|
async def resume_run(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
|||||||
+80
-1
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any, Literal
|
||||||
|
|
||||||
from wf_platform import CapabilityRef, Page, SourceRef
|
from wf_platform import CapabilityRef, Page, SourceRef
|
||||||
from wf_transport_rpc_http import RpcWorkflowApiClient
|
from wf_transport_rpc_http import RpcWorkflowApiClient
|
||||||
@@ -14,10 +14,14 @@ from ._identity import require_response_identity
|
|||||||
from .authoring import EditableWorkflow
|
from .authoring import EditableWorkflow
|
||||||
from .capabilities import CapabilitySummary, RemoteCapability
|
from .capabilities import CapabilitySummary, RemoteCapability
|
||||||
from .codec import (
|
from .codec import (
|
||||||
|
decode_artifacts_page,
|
||||||
decode_capabilities_page,
|
decode_capabilities_page,
|
||||||
decode_capability_inspect,
|
decode_capability_inspect,
|
||||||
|
decode_deployments,
|
||||||
|
decode_runs_page,
|
||||||
decode_workflow_artifact,
|
decode_workflow_artifact,
|
||||||
)
|
)
|
||||||
|
from .discovery import ArtifactSummary, DeploymentSummary, RunSummary
|
||||||
from .errors import InvalidResponse
|
from .errors import InvalidResponse
|
||||||
from .protocols import WorkflowClientPort
|
from .protocols import WorkflowClientPort
|
||||||
from .workflows import WorkflowArtifact
|
from .workflows import WorkflowArtifact
|
||||||
@@ -148,6 +152,81 @@ class App:
|
|||||||
total=wire["total"],
|
total=wire["total"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def artifacts(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
query: str | None = None,
|
||||||
|
kind: Literal["workflow", "wrapper"] | None = None,
|
||||||
|
cursor: str | None = None,
|
||||||
|
limit: int = 50,
|
||||||
|
) -> Page[ArtifactSummary]:
|
||||||
|
"""List lightweight saved artifact versions without reconstructing them."""
|
||||||
|
wire = decode_artifacts_page(
|
||||||
|
await self._port.list_artifacts(
|
||||||
|
query=query, kind=kind, cursor=cursor, limit=limit
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return Page(
|
||||||
|
items=tuple(
|
||||||
|
ArtifactSummary(
|
||||||
|
artifact_id=row["artifact_id"],
|
||||||
|
version=row["version"],
|
||||||
|
kind=row["kind"],
|
||||||
|
title=row["display_name"],
|
||||||
|
description=row["description"],
|
||||||
|
outcomes=tuple(row["outcomes"]),
|
||||||
|
required_sources=tuple(row["required_sources"]),
|
||||||
|
)
|
||||||
|
for row in wire["nodes"]
|
||||||
|
),
|
||||||
|
next_cursor=wire["next_cursor"],
|
||||||
|
total=wire["total"],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def deployments(self) -> tuple[DeploymentSummary, ...]:
|
||||||
|
"""List lightweight saved deployment rows."""
|
||||||
|
wire = decode_deployments(await self._port.list_deployments())
|
||||||
|
return tuple(
|
||||||
|
DeploymentSummary(
|
||||||
|
deployment_id=row["id"],
|
||||||
|
artifact_id=row["artifact_id"],
|
||||||
|
artifact_version=row["artifact_version"],
|
||||||
|
binding_count=row["binding_count"],
|
||||||
|
drift_policy=row["drift_policy"],
|
||||||
|
)
|
||||||
|
for row in wire["deployments"]
|
||||||
|
)
|
||||||
|
|
||||||
|
async def runs(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
status: str | None = None,
|
||||||
|
cursor: str | None = None,
|
||||||
|
limit: int = 50,
|
||||||
|
) -> Page[RunSummary]:
|
||||||
|
"""List lightweight durable-run rows without loading traces."""
|
||||||
|
wire = decode_runs_page(
|
||||||
|
await self._port.list_runs(status=status, cursor=cursor, limit=limit)
|
||||||
|
)
|
||||||
|
return Page(
|
||||||
|
items=tuple(
|
||||||
|
RunSummary(
|
||||||
|
run_id=row["run_id"],
|
||||||
|
deployment_id=row["deployment_id"],
|
||||||
|
artifact_id=row["artifact_id"],
|
||||||
|
artifact_version=row["artifact_version"],
|
||||||
|
status=row["status"],
|
||||||
|
resume_readiness=row["resume_readiness"],
|
||||||
|
diagnostic_count=row["diagnostic_count"],
|
||||||
|
created_at=row["created_at"],
|
||||||
|
updated_at=row["updated_at"],
|
||||||
|
)
|
||||||
|
for row in wire["runs"]
|
||||||
|
),
|
||||||
|
next_cursor=wire["next_cursor"],
|
||||||
|
total=wire["total"],
|
||||||
|
)
|
||||||
|
|
||||||
def new_workflow(
|
def new_workflow(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ from wf_api.models import (
|
|||||||
CapabilityCallResult,
|
CapabilityCallResult,
|
||||||
DependencyDiagnosticPayload,
|
DependencyDiagnosticPayload,
|
||||||
InspectCapabilityResult,
|
InspectCapabilityResult,
|
||||||
|
ListArtifactsResult,
|
||||||
ListCapabilitiesResult,
|
ListCapabilitiesResult,
|
||||||
ListDeploymentsResult,
|
ListDeploymentsResult,
|
||||||
|
ListRunsResult,
|
||||||
RawWorkflowPlan,
|
RawWorkflowPlan,
|
||||||
RunResult,
|
RunResult,
|
||||||
RunTraceResult,
|
RunTraceResult,
|
||||||
@@ -114,6 +116,16 @@ def decode_capabilities_page(payload: object) -> ListCapabilitiesResult:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_artifacts_page(payload: object) -> ListArtifactsResult:
|
||||||
|
"""Validate one cursor-paged artifact catalog response."""
|
||||||
|
return _validate(payload, ListArtifactsResult, "workflow.artifacts.list")
|
||||||
|
|
||||||
|
|
||||||
|
def decode_runs_page(payload: object) -> ListRunsResult:
|
||||||
|
"""Validate one cursor-paged durable-run response."""
|
||||||
|
return _validate(payload, ListRunsResult, "workflow.runs.list")
|
||||||
|
|
||||||
|
|
||||||
def decode_validate_artifact_plan(payload: object) -> ValidateArtifactPlanResult:
|
def decode_validate_artifact_plan(payload: object) -> ValidateArtifactPlanResult:
|
||||||
"""Validate a non-persisting artifact-plan response at the client boundary."""
|
"""Validate a non-persisting artifact-plan response at the client boundary."""
|
||||||
return _validate(
|
return _validate(
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"""Lightweight immutable rows for discovering existing workflow objects."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ArtifactSummary:
|
||||||
|
"""Catalog identity and display metadata for one artifact version."""
|
||||||
|
|
||||||
|
artifact_id: str
|
||||||
|
version: int
|
||||||
|
kind: str
|
||||||
|
title: str
|
||||||
|
description: str | None
|
||||||
|
outcomes: tuple[str, ...]
|
||||||
|
required_sources: tuple[str, ...]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class DeploymentSummary:
|
||||||
|
"""Identity and binding metadata for one saved deployment."""
|
||||||
|
|
||||||
|
deployment_id: str
|
||||||
|
artifact_id: str
|
||||||
|
artifact_version: int
|
||||||
|
binding_count: int
|
||||||
|
drift_policy: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class RunSummary:
|
||||||
|
"""Identity and lifecycle metadata for one durable run."""
|
||||||
|
|
||||||
|
run_id: str
|
||||||
|
deployment_id: str
|
||||||
|
artifact_id: str
|
||||||
|
artifact_version: int
|
||||||
|
status: str
|
||||||
|
resume_readiness: str
|
||||||
|
diagnostic_count: int
|
||||||
|
created_at: str
|
||||||
|
updated_at: str
|
||||||
@@ -8,8 +8,10 @@ from typing import Any, Literal, Protocol
|
|||||||
from wf_api.models import (
|
from wf_api.models import (
|
||||||
CapabilityCallResult,
|
CapabilityCallResult,
|
||||||
InspectCapabilityResult,
|
InspectCapabilityResult,
|
||||||
|
ListArtifactsResult,
|
||||||
ListCapabilitiesResult,
|
ListCapabilitiesResult,
|
||||||
ListDeploymentsResult,
|
ListDeploymentsResult,
|
||||||
|
ListRunsResult,
|
||||||
RunResult,
|
RunResult,
|
||||||
RunTraceResult,
|
RunTraceResult,
|
||||||
SaveArtifactResult,
|
SaveArtifactResult,
|
||||||
@@ -60,6 +62,15 @@ class WorkflowClientPort(Protocol):
|
|||||||
version: int,
|
version: int,
|
||||||
) -> WorkflowArtifactPayload: ...
|
) -> WorkflowArtifactPayload: ...
|
||||||
|
|
||||||
|
async def list_artifacts(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
query: str | None = None,
|
||||||
|
kind: Literal["workflow", "wrapper"] | None = None,
|
||||||
|
cursor: str | None = None,
|
||||||
|
limit: int = 50,
|
||||||
|
) -> ListArtifactsResult: ...
|
||||||
|
|
||||||
async def create_artifact_from_plan(
|
async def create_artifact_from_plan(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -112,6 +123,14 @@ class WorkflowClientPort(Protocol):
|
|||||||
trace_range: TraceRangeLike | None = None,
|
trace_range: TraceRangeLike | None = None,
|
||||||
) -> RunResult: ...
|
) -> RunResult: ...
|
||||||
|
|
||||||
|
async def list_runs(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
status: str | None = None,
|
||||||
|
cursor: str | None = None,
|
||||||
|
limit: int = 50,
|
||||||
|
) -> ListRunsResult: ...
|
||||||
|
|
||||||
async def inspect_run(self, *, run_id: str) -> RunResult: ...
|
async def inspect_run(self, *, run_id: str) -> RunResult: ...
|
||||||
|
|
||||||
async def resume_run(
|
async def resume_run(
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ class FakeWorkflowClient:
|
|||||||
async def inspect_artifact(self, **params: Any) -> object:
|
async def inspect_artifact(self, **params: Any) -> object:
|
||||||
return self._response("workflow.artifacts.inspect", params)
|
return self._response("workflow.artifacts.inspect", params)
|
||||||
|
|
||||||
|
async def list_artifacts(self, **params: Any) -> object:
|
||||||
|
return self._response("workflow.artifacts.list", params)
|
||||||
|
|
||||||
async def save_artifact(self, artifact: dict[str, Any]) -> object:
|
async def save_artifact(self, artifact: dict[str, Any]) -> object:
|
||||||
return self._response("workflow.artifacts.save", {"artifact": artifact})
|
return self._response("workflow.artifacts.save", {"artifact": artifact})
|
||||||
|
|
||||||
@@ -55,6 +58,9 @@ class FakeWorkflowClient:
|
|||||||
async def run_deployment(self, **params: Any) -> object:
|
async def run_deployment(self, **params: Any) -> object:
|
||||||
return self._response("workflow.runs.start", params)
|
return self._response("workflow.runs.start", params)
|
||||||
|
|
||||||
|
async def list_runs(self, **params: Any) -> object:
|
||||||
|
return self._response("workflow.runs.list", params)
|
||||||
|
|
||||||
async def inspect_run(self, **params: Any) -> object:
|
async def inspect_run(self, **params: Any) -> object:
|
||||||
return self._response("workflow.runs.inspect", params)
|
return self._response("workflow.runs.inspect", params)
|
||||||
|
|
||||||
|
|||||||
@@ -273,6 +273,110 @@ async def test_capability_discovery_returns_rich_page() -> None:
|
|||||||
assert page.items[0].outcomes == ("ok",)
|
assert page.items[0].outcomes == ("ok",)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_app_discovers_existing_artifacts_deployments_and_runs() -> None:
|
||||||
|
"""Catch collection methods disappearing while exact loaders still work."""
|
||||||
|
|
||||||
|
class ExistingObjectsPort(_Port):
|
||||||
|
async def list_artifacts(self, **params: Any) -> object:
|
||||||
|
self.calls.append(("artifacts", params))
|
||||||
|
return {
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "report.v3",
|
||||||
|
"artifact_id": "report",
|
||||||
|
"version": 3,
|
||||||
|
"kind": "workflow",
|
||||||
|
"display_name": "Report",
|
||||||
|
"description": None,
|
||||||
|
"outcomes": ["ok"],
|
||||||
|
"input_schema": {"type": "object", "properties": {}},
|
||||||
|
"output_schema": {"type": "object", "properties": {}},
|
||||||
|
"required_sources": ["app.default"],
|
||||||
|
"diagnostics": [],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cursor": "0",
|
||||||
|
"next_cursor": None,
|
||||||
|
"limit": 10,
|
||||||
|
"total": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def list_deployments(self) -> object:
|
||||||
|
self.calls.append(("deployments", {}))
|
||||||
|
return {
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"id": "report.production",
|
||||||
|
"artifact_id": "report",
|
||||||
|
"artifact_version": 3,
|
||||||
|
"binding_count": 1,
|
||||||
|
"drift_policy": "block",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
async def list_runs(self, **params: Any) -> object:
|
||||||
|
self.calls.append(("runs", params))
|
||||||
|
return {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"run_id": "run-123",
|
||||||
|
"deployment_id": "report.production",
|
||||||
|
"artifact_id": "report",
|
||||||
|
"artifact_version": 3,
|
||||||
|
"status": "interrupted",
|
||||||
|
"resume_readiness": "ready",
|
||||||
|
"diagnostic_count": 0,
|
||||||
|
"created_at": "2026-09-02T00:00:00Z",
|
||||||
|
"updated_at": "2026-09-02T00:01:00Z",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cursor": None,
|
||||||
|
"next_cursor": "next",
|
||||||
|
"limit": 25,
|
||||||
|
"total": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
port = ExistingObjectsPort()
|
||||||
|
app = App._from_port(cast(WorkflowClientPort, port))
|
||||||
|
|
||||||
|
artifacts = await app.artifacts(query="report", kind="workflow", limit=10)
|
||||||
|
deployments = await app.deployments()
|
||||||
|
runs = await app.runs(status="interrupted", limit=25)
|
||||||
|
|
||||||
|
assert artifacts.items[0].artifact_id == "report"
|
||||||
|
assert artifacts.items[0].version == 3
|
||||||
|
assert artifacts.total == 1
|
||||||
|
assert deployments[0].deployment_id == "report.production"
|
||||||
|
assert deployments[0].artifact_version == 3
|
||||||
|
assert runs.items[0].run_id == "run-123"
|
||||||
|
assert runs.items[0].status == "interrupted"
|
||||||
|
assert runs.next_cursor == "next"
|
||||||
|
assert port.calls == [
|
||||||
|
(
|
||||||
|
"artifacts",
|
||||||
|
{"query": "report", "kind": "workflow", "cursor": None, "limit": 10},
|
||||||
|
),
|
||||||
|
("deployments", {}),
|
||||||
|
("runs", {"status": "interrupted", "cursor": None, "limit": 25}),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_app_rejects_malformed_existing_object_discovery() -> None:
|
||||||
|
"""Catch unvalidated wire dictionaries leaking through collection methods."""
|
||||||
|
|
||||||
|
class MalformedPort(_Port):
|
||||||
|
async def list_runs(self, **params: Any) -> object:
|
||||||
|
return {"runs": [{"run_id": "missing-the-rest"}]}
|
||||||
|
|
||||||
|
app = App._from_port(cast(WorkflowClientPort, MalformedPort()))
|
||||||
|
|
||||||
|
with pytest.raises(InvalidResponse, match="workflow.runs.list"):
|
||||||
|
await app.runs()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_capability_reconstructs_structural_reference() -> None:
|
async def test_capability_reconstructs_structural_reference() -> None:
|
||||||
capability = await _app().capability("app.default.search")
|
capability = await _app().capability("app.default.search")
|
||||||
|
|||||||
@@ -54,8 +54,16 @@ async def test_http_app_calls_authors_saves_deploys_and_runs(tmp_path) -> None:
|
|||||||
validation = await graph.validate()
|
validation = await graph.validate()
|
||||||
artifact = await graph.save(version=1, title="HTTP client proof")
|
artifact = await graph.save(version=1, title="HTTP client proof")
|
||||||
run = await artifact.run({})
|
run = await artifact.run({})
|
||||||
|
artifacts = await app.artifacts(query="http_client_proof")
|
||||||
|
deployments = await app.deployments()
|
||||||
|
runs = await app.runs(status="completed", limit=25)
|
||||||
|
|
||||||
assert validation.ok is True
|
assert validation.ok is True
|
||||||
assert artifact.ref == ArtifactRef("http_client_proof", 1)
|
assert artifact.ref == ArtifactRef("http_client_proof", 1)
|
||||||
assert run.status == "completed"
|
assert run.status == "completed"
|
||||||
assert run.output == {"value": "hello"}
|
assert run.output == {"value": "hello"}
|
||||||
|
assert [(item.artifact_id, item.version) for item in artifacts.items] == [
|
||||||
|
("http_client_proof", 1)
|
||||||
|
]
|
||||||
|
assert deployments[0].artifact_id == "http_client_proof"
|
||||||
|
assert runs.items[0].run_id == run.run_id
|
||||||
|
|||||||
@@ -61,8 +61,11 @@ async def _rpc(
|
|||||||
|
|
||||||
|
|
||||||
def test_rpc_app_can_omit_draft_methods(tmp_path) -> None:
|
def test_rpc_app_can_omit_draft_methods(tmp_path) -> None:
|
||||||
server = build_local_static_workflow_server(tmp_path / "store")
|
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||||
|
|
||||||
|
# Start enabled so this test proves the RPC composition override disables
|
||||||
|
# the surface instead of merely observing the server's default-off state.
|
||||||
|
assert server.api.drafts_enabled is True
|
||||||
app = create_rpc_app(server, drafts=False)
|
app = create_rpc_app(server, drafts=False)
|
||||||
methods = {method["name"] for method in app.get_openrpc()["methods"]}
|
methods = {method["name"] for method in app.get_openrpc()["methods"]}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user