code review
This commit is contained in:
@@ -397,7 +397,7 @@ _local_path_payload
|
|||||||
_state_path_payload
|
_state_path_payload
|
||||||
```
|
```
|
||||||
|
|
||||||
Keep `_escape_json_pointer` if `set_draft_route` delegation still needs no local use. Remove it only if no remaining references exist.
|
Remove `_escape_json_pointer` only if no remaining references exist in `handlers.py`.
|
||||||
|
|
||||||
Do not remove from `handlers.py` yet unless `rg` proves there are no remaining
|
Do not remove from `handlers.py` yet unless `rg` proves there are no remaining
|
||||||
callers:
|
callers:
|
||||||
|
|||||||
@@ -409,6 +409,7 @@ src/wf_cli/
|
|||||||
- `WfMcpService.__post_init__` creates default `FileWorkflowArtifactStore`, `FileDraftWorkspaceStore`, `FileRunStore` if not provided. These are protocol-neutral stores.
|
- `WfMcpService.__post_init__` creates default `FileWorkflowArtifactStore`, `FileDraftWorkspaceStore`, `FileRunStore` if not provided. These are protocol-neutral stores.
|
||||||
- The stores are created from `_store_root(self.store)` which uses the MCP `Store` root.
|
- The stores are created from `_store_root(self.store)` which uses the MCP `Store` root.
|
||||||
- After extraction, store creation should be the caller's responsibility (config-driven), not `WfMcpService`'s.
|
- After extraction, store creation should be the caller's responsibility (config-driven), not `WfMcpService`'s.
|
||||||
|
- Next extraction slice must define the store initialization boundary: which caller constructs `FileWorkflowArtifactStore`, `FileDraftWorkspaceStore`, and `FileRunStore`; how config/API callers inject protocol-neutral store implementations; and which tests must explicitly construct stores instead of relying on `WfMcpService.__post_init__`.
|
||||||
|
|
||||||
### Naming Confusion
|
### Naming Confusion
|
||||||
|
|
||||||
@@ -446,6 +447,7 @@ src/wf_cli/
|
|||||||
8. **Run full test suite** — all existing tests must pass with import-only changes.
|
8. **Run full test suite** — all existing tests must pass with import-only changes.
|
||||||
9. **Update `CliContext`** to use `WorkflowApi` instead of `WorkflowSurfaceHandlers`.
|
9. **Update `CliContext`** to use `WorkflowApi` instead of `WorkflowSurfaceHandlers`.
|
||||||
10. **Add integration test** for `WorkflowApi` with a mock backend (not `WfMcpService`).
|
10. **Add integration test** for `WorkflowApi` with a mock backend (not `WfMcpService`).
|
||||||
|
11. **Define store ownership** before extracting service construction: remove implicit workflow-store creation from `WfMcpService.__post_init__`, add config/API hooks for injected stores, and update tests to pass stores explicitly.
|
||||||
|
|
||||||
### Dependency Graph After Extraction
|
### Dependency Graph After Extraction
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ The current suspicion:
|
|||||||
Write your findings to:
|
Write your findings to:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
random shit/wf_api_extraction_map.md
|
docs/superpowers/research/2026-06-01-wf-api-extraction-map.md
|
||||||
```
|
```
|
||||||
|
|
||||||
Keep it structured and link-heavy. Do not paste huge code blocks. Use file paths, symbol names, and short notes.
|
Keep it structured and link-heavy. Do not paste huge code blocks. Use file paths, symbol names, and short notes.
|
||||||
|
|||||||
@@ -50,3 +50,12 @@ that decides what content types are acceptable.
|
|||||||
|
|
||||||
If a result exposes a convenience `text` field, inspect the capability schema
|
If a result exposes a convenience `text` field, inspect the capability schema
|
||||||
and wrapper hints before using it. Do not assume every content block is text.
|
and wrapper hints before using it. Do not assume every content block is text.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"source": {"root": "local", "parts": ["content"]}, "target": {"root": "state", "parts": ["summary"]}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct: filter `content` to text blocks, extract each `text`, then combine or
|
||||||
|
select the value before writing it to a string state field.
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ class WorkflowArtifactApi:
|
|||||||
query=query,
|
query=query,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
entries.sort(key=lambda entry: str(entry["name"]))
|
entries.sort(key=lambda entry: str(entry.get("name", "")))
|
||||||
return paged_list_payload("nodes", entries, cursor=cursor, limit=limit)
|
return paged_list_payload("nodes", entries, cursor=cursor, limit=limit)
|
||||||
|
|
||||||
async def save_artifact(self, artifact: dict[str, Any]) -> dict[str, Any]:
|
async def save_artifact(self, artifact: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
|||||||
@@ -306,6 +306,7 @@ def _bounded_trace_example(
|
|||||||
trace_count: int,
|
trace_count: int,
|
||||||
) -> NextActionPatchExample:
|
) -> NextActionPatchExample:
|
||||||
"""Return a safe read_run_trace request; never suggest full trace reads."""
|
"""Return a safe read_run_trace request; never suggest full trace reads."""
|
||||||
|
limit = max(1, min(25, trace_count))
|
||||||
return NextActionPatchExample(
|
return NextActionPatchExample(
|
||||||
description=(
|
description=(
|
||||||
"Read a bounded debug trace slice. Increase start/limit only when needed."
|
"Read a bounded debug trace slice. Increase start/limit only when needed."
|
||||||
@@ -315,7 +316,7 @@ def _bounded_trace_example(
|
|||||||
"run_id": run_id,
|
"run_id": run_id,
|
||||||
"trace_range": {
|
"trace_range": {
|
||||||
"start": 0,
|
"start": 0,
|
||||||
"limit": 25,
|
"limit": limit,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
+8
-3
@@ -2,11 +2,16 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any, TypeAlias
|
from typing import Any, TypeAlias
|
||||||
|
|
||||||
|
from pydantic import TypeAdapter, ValidationError
|
||||||
|
|
||||||
from wf_artifacts import WorkflowCapabilityRef
|
from wf_artifacts import WorkflowCapabilityRef
|
||||||
from wf_platform import CapabilityRef
|
from wf_platform import CapabilityRef
|
||||||
|
|
||||||
WorkflowSurfaceCapabilityId: TypeAlias = CapabilityRef | WorkflowCapabilityRef
|
WorkflowSurfaceCapabilityId: TypeAlias = CapabilityRef | WorkflowCapabilityRef
|
||||||
|
|
||||||
|
_CAPABILITY_REF_ADAPTER = TypeAdapter(CapabilityRef)
|
||||||
|
_WORKFLOW_CAPABILITY_REF_ADAPTER = TypeAdapter(WorkflowCapabilityRef)
|
||||||
|
|
||||||
|
|
||||||
def parse_workflow_surface_capability_id(
|
def parse_workflow_surface_capability_id(
|
||||||
value: str | dict[str, Any],
|
value: str | dict[str, Any],
|
||||||
@@ -19,10 +24,10 @@ def parse_workflow_surface_capability_id(
|
|||||||
"""
|
"""
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
if "artifact_id" in value and "version" in value:
|
if "artifact_id" in value and "version" in value:
|
||||||
return WorkflowCapabilityRef._validate(value)
|
return _WORKFLOW_CAPABILITY_REF_ADAPTER.validate_python(value)
|
||||||
return CapabilityRef._validate(value)
|
return _CAPABILITY_REF_ADAPTER.validate_python(value)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return WorkflowCapabilityRef.parse(value)
|
return WorkflowCapabilityRef.parse(value)
|
||||||
except ValueError:
|
except TypeError, ValueError, ValidationError:
|
||||||
return CapabilityRef.parse(value)
|
return CapabilityRef.parse(value)
|
||||||
|
|||||||
+38
-37
@@ -57,6 +57,7 @@ class WorkflowRunApi:
|
|||||||
workflow_input: dict[str, Any],
|
workflow_input: dict[str, Any],
|
||||||
trace_range: TraceRangeLike | None = None,
|
trace_range: TraceRangeLike | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
|
trace_values = _trace_range_values(trace_range)
|
||||||
deployment, artifact, diagnostics, tree = (
|
deployment, artifact, diagnostics, tree = (
|
||||||
self.deployments.deployment_validation(deployment_id)
|
self.deployments.deployment_validation(deployment_id)
|
||||||
)
|
)
|
||||||
@@ -96,22 +97,7 @@ class WorkflowRunApi:
|
|||||||
error=run.error,
|
error=run.error,
|
||||||
output=run.output,
|
output=run.output,
|
||||||
trace_count=len(run.trace),
|
trace_count=len(run.trace),
|
||||||
trace=(
|
**_trace_slice_fields(run, trace_values),
|
||||||
[
|
|
||||||
asdict(entry)
|
|
||||||
for entry in run.trace[
|
|
||||||
trace_range.start : trace_range.start + trace_range.limit
|
|
||||||
]
|
|
||||||
]
|
|
||||||
if trace_range is not None
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
trace_start=trace_range.start if trace_range is not None else None,
|
|
||||||
trace_limit=trace_range.limit if trace_range is not None else None,
|
|
||||||
trace_truncated=(
|
|
||||||
trace_range is not None
|
|
||||||
and len(run.trace) > trace_range.start + trace_range.limit
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def resume_run(
|
async def resume_run(
|
||||||
@@ -123,6 +109,7 @@ class WorkflowRunApi:
|
|||||||
trace_range: TraceRangeLike | None = None,
|
trace_range: TraceRangeLike | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Resume one durable interrupted deployment run."""
|
"""Resume one durable interrupted deployment run."""
|
||||||
|
trace_values = _trace_range_values(trace_range)
|
||||||
record, stopped_run = restore_interrupted_run(self._run_store(), run_id)
|
record, stopped_run = restore_interrupted_run(self._run_store(), run_id)
|
||||||
environment = record.environment
|
environment = record.environment
|
||||||
diagnostics = validate_pinned_resume_environment(
|
diagnostics = validate_pinned_resume_environment(
|
||||||
@@ -176,22 +163,7 @@ class WorkflowRunApi:
|
|||||||
error=run.error,
|
error=run.error,
|
||||||
output=run.output,
|
output=run.output,
|
||||||
trace_count=len(run.trace),
|
trace_count=len(run.trace),
|
||||||
trace=(
|
**_trace_slice_fields(run, trace_values),
|
||||||
[
|
|
||||||
asdict(entry)
|
|
||||||
for entry in run.trace[
|
|
||||||
trace_range.start : trace_range.start + trace_range.limit
|
|
||||||
]
|
|
||||||
]
|
|
||||||
if trace_range is not None
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
trace_start=trace_range.start if trace_range is not None else None,
|
|
||||||
trace_limit=trace_range.limit if trace_range is not None else None,
|
|
||||||
trace_truncated=(
|
|
||||||
trace_range is not None
|
|
||||||
and len(run.trace) > trace_range.start + trace_range.limit
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def inspect_run(self, *, run_id: str) -> dict[str, Any]:
|
async def inspect_run(self, *, run_id: str) -> dict[str, Any]:
|
||||||
@@ -219,9 +191,9 @@ class WorkflowRunApi:
|
|||||||
trace_range: TraceRangeLike,
|
trace_range: TraceRangeLike,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Return only a caller-bounded debug trace slice from a stopped run."""
|
"""Return only a caller-bounded debug trace slice from a stopped run."""
|
||||||
|
trace_values = _trace_range_values(trace_range)
|
||||||
record, run = load_stored_run(self._run_store(), run_id)
|
record, run = load_stored_run(self._run_store(), run_id)
|
||||||
environment = record.environment
|
environment = record.environment
|
||||||
end = trace_range.start + trace_range.limit
|
|
||||||
return _run_payload(
|
return _run_payload(
|
||||||
deployment=environment.deployment,
|
deployment=environment.deployment,
|
||||||
artifact=environment.root_artifact,
|
artifact=environment.root_artifact,
|
||||||
@@ -230,13 +202,42 @@ class WorkflowRunApi:
|
|||||||
resume_readiness=record.resume_readiness.value,
|
resume_readiness=record.resume_readiness.value,
|
||||||
diagnostics=record.diagnostics,
|
diagnostics=record.diagnostics,
|
||||||
trace_count=len(run.trace),
|
trace_count=len(run.trace),
|
||||||
trace=[asdict(entry) for entry in run.trace[trace_range.start : end]],
|
**_trace_slice_fields(run, trace_values),
|
||||||
trace_start=trace_range.start,
|
|
||||||
trace_limit=trace_range.limit,
|
|
||||||
trace_truncated=len(run.trace) > end,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _trace_range_values(
|
||||||
|
trace_range: TraceRangeLike | None,
|
||||||
|
) -> tuple[int, int] | None:
|
||||||
|
"""Validate protocol-level trace ranges before they reach Python slicing."""
|
||||||
|
if trace_range is None:
|
||||||
|
return None
|
||||||
|
start = trace_range.start
|
||||||
|
limit = trace_range.limit
|
||||||
|
if start < 0:
|
||||||
|
raise ValueError("trace_range.start must be >= 0")
|
||||||
|
if limit <= 0:
|
||||||
|
raise ValueError("trace_range.limit must be > 0")
|
||||||
|
return start, limit
|
||||||
|
|
||||||
|
|
||||||
|
def _trace_slice_fields(
|
||||||
|
run: RunState,
|
||||||
|
trace_range: tuple[int, int] | None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Return bounded trace payload fields, or no trace fields when omitted."""
|
||||||
|
if trace_range is None:
|
||||||
|
return {}
|
||||||
|
start, limit = trace_range
|
||||||
|
end = start + limit
|
||||||
|
return {
|
||||||
|
"trace": [asdict(entry) for entry in run.trace[start:end]],
|
||||||
|
"trace_start": start,
|
||||||
|
"trace_limit": limit,
|
||||||
|
"trace_truncated": len(run.trace) > end,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _run_payload(
|
def _run_payload(
|
||||||
*,
|
*,
|
||||||
deployment: WorkflowDeployment,
|
deployment: WorkflowDeployment,
|
||||||
|
|||||||
@@ -110,18 +110,16 @@ def wrapper_hints_for_capability(
|
|||||||
output_schema, output_properties
|
output_schema, output_properties
|
||||||
)
|
)
|
||||||
output_map = {name: f"state.{name}" for name in sorted(output_map_properties)}
|
output_map = {name: f"state.{name}" for name in sorted(output_map_properties)}
|
||||||
state_schema = {
|
mapped_output_schema = {
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
name: schema for name, schema in sorted(output_map_properties.items())
|
|
||||||
},
|
|
||||||
}
|
|
||||||
wrapper_output_schema = {
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
name: schema for name, schema in sorted(output_map_properties.items())
|
name: schema for name, schema in sorted(output_map_properties.items())
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
# The minimal wrapper stores mapped outputs in state and returns the same
|
||||||
|
# fields. Split this later only when wrappers support separate return shape.
|
||||||
|
state_schema = mapped_output_schema
|
||||||
|
wrapper_output_schema = mapped_output_schema
|
||||||
missing_decisions = _missing_decisions_for_output(hint_output_schema)
|
missing_decisions = _missing_decisions_for_output(hint_output_schema)
|
||||||
outcome_candidates = _boolean_outcome_candidates(output_properties)
|
outcome_candidates = _boolean_outcome_candidates(output_properties)
|
||||||
if outcome_candidates:
|
if outcome_candidates:
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ from .core import WfMcpService
|
|||||||
|
|
||||||
LIVE_SOURCE_CHECK_TIMEOUT_SECONDS = 8.0
|
LIVE_SOURCE_CHECK_TIMEOUT_SECONDS = 8.0
|
||||||
_LIVE_SOURCE_CHECK_FAILURES = (
|
_LIVE_SOURCE_CHECK_FAILURES = (
|
||||||
KeyError,
|
|
||||||
TimeoutError,
|
TimeoutError,
|
||||||
OSError,
|
OSError,
|
||||||
anyio.ClosedResourceError,
|
anyio.ClosedResourceError,
|
||||||
@@ -80,6 +79,16 @@ async def live_source_diagnostics(
|
|||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
connection = service.connections.get(source_id)
|
connection = service.connections.get(source_id)
|
||||||
|
except KeyError as exc:
|
||||||
|
diagnostics.append(
|
||||||
|
_source_unreachable_diagnostic(
|
||||||
|
logical_ref=logical_ref,
|
||||||
|
source_id=source_id,
|
||||||
|
exc=exc,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
adapter = require_adapter(connection, service.adapters)
|
adapter = require_adapter(connection, service.adapters)
|
||||||
auth = service.load_auth(source_id)
|
auth = service.load_auth(source_id)
|
||||||
await asyncio.wait_for(
|
await asyncio.wait_for(
|
||||||
@@ -88,7 +97,23 @@ async def live_source_diagnostics(
|
|||||||
)
|
)
|
||||||
except _LIVE_SOURCE_CHECK_FAILURES as exc:
|
except _LIVE_SOURCE_CHECK_FAILURES as exc:
|
||||||
diagnostics.append(
|
diagnostics.append(
|
||||||
DependencyDiagnostic(
|
_source_unreachable_diagnostic(
|
||||||
|
logical_ref=logical_ref,
|
||||||
|
source_id=source_id,
|
||||||
|
exc=exc,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return diagnostics
|
||||||
|
|
||||||
|
|
||||||
|
def _source_unreachable_diagnostic(
|
||||||
|
*,
|
||||||
|
logical_ref: str,
|
||||||
|
source_id: str,
|
||||||
|
exc: BaseException,
|
||||||
|
) -> DependencyDiagnostic:
|
||||||
|
"""Build a liveness diagnostic without catching unrelated probe bugs."""
|
||||||
|
return DependencyDiagnostic(
|
||||||
severity=DiagnosticSeverity.ERROR,
|
severity=DiagnosticSeverity.ERROR,
|
||||||
code="source_unreachable",
|
code="source_unreachable",
|
||||||
logical_ref=logical_ref,
|
logical_ref=logical_ref,
|
||||||
@@ -102,8 +127,6 @@ async def live_source_diagnostics(
|
|||||||
"configuration, or bind this deployment to another source."
|
"configuration, or bind this deployment to another source."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
|
||||||
return diagnostics
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ from wf_api.artifacts import WorkflowArtifactApi
|
|||||||
from wf_api.capabilities import WorkflowCapabilityApi
|
from wf_api.capabilities import WorkflowCapabilityApi
|
||||||
from wf_api.deployments import WorkflowDeploymentApi
|
from wf_api.deployments import WorkflowDeploymentApi
|
||||||
from wf_api.drafts import WorkflowDraftApi
|
from wf_api.drafts import WorkflowDraftApi
|
||||||
|
from wf_api.listing import paged_list_payload
|
||||||
from wf_api.models import RawWorkflowPlan
|
from wf_api.models import RawWorkflowPlan
|
||||||
from wf_api.runs import WorkflowRunApi
|
from wf_api.runs import WorkflowRunApi
|
||||||
|
|
||||||
from ..broker.service.workflow_operation_context import context_from_service
|
from ..broker.service.workflow_operation_context import context_from_service
|
||||||
from wf_api.listing import paged_list_payload
|
|
||||||
from .models import TraceRange
|
from .models import TraceRange
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from wf_artifacts import FileWorkflowArtifactStore
|
from wf_artifacts import FileWorkflowArtifactStore
|
||||||
from wf_api.capabilities import WorkflowCapabilityApi
|
from wf_api.capabilities import WorkflowCapabilityApi
|
||||||
from wf_mcp.broker import WfMcpService
|
from wf_mcp.broker import WfMcpService
|
||||||
@@ -91,11 +93,8 @@ def test_inspect_capability_raises_on_unknown() -> None:
|
|||||||
)
|
)
|
||||||
api, _service = _capability_api(artifact_store, register_echo=True)
|
api, _service = _capability_api(artifact_store, register_echo=True)
|
||||||
|
|
||||||
try:
|
with pytest.raises(KeyError, match="no.such.capability"):
|
||||||
asyncio.run(api.inspect_capability(qualified_name="no.such.capability"))
|
asyncio.run(api.inspect_capability(qualified_name="no.such.capability"))
|
||||||
assert False, "expected KeyError"
|
|
||||||
except KeyError as exc:
|
|
||||||
assert "no.such.capability" in str(exc)
|
|
||||||
|
|
||||||
|
|
||||||
def test_call_capability_node_spec_success() -> None:
|
def test_call_capability_node_spec_success() -> None:
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ def test_validate_draft_workspace_refreshes_status() -> None:
|
|||||||
assert fetched["status"] == "invalid"
|
assert fetched["status"] == "invalid"
|
||||||
|
|
||||||
|
|
||||||
def test_create_minimal_draft_workspace_with_error_route() -> None:
|
def test_create_minimal_draft_workspace_minimal_success_path() -> None:
|
||||||
artifact_store = FileWorkflowArtifactStore(
|
artifact_store = FileWorkflowArtifactStore(
|
||||||
local_temp_root() / "drafts_minimal_workspace"
|
local_temp_root() / "drafts_minimal_workspace"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,15 +15,13 @@ def test_wf_api_has_no_wf_mcp_imports() -> None:
|
|||||||
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
|
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
|
||||||
for node in ast.walk(tree):
|
for node in ast.walk(tree):
|
||||||
if isinstance(node, ast.ImportFrom) and node.module is not None:
|
if isinstance(node, ast.ImportFrom) and node.module is not None:
|
||||||
if node.module.startswith("wf_mcp") or node.module.startswith(
|
if node.module == "wf_mcp" or node.module.startswith("wf_mcp."):
|
||||||
"wf_mcp."
|
|
||||||
):
|
|
||||||
violations.append(
|
violations.append(
|
||||||
f"{module}:{node.lineno}: from {node.module} import ..."
|
f"{module}:{node.lineno}: from {node.module} import ..."
|
||||||
)
|
)
|
||||||
elif isinstance(node, ast.Import):
|
elif isinstance(node, ast.Import):
|
||||||
for alias in node.names:
|
for alias in node.names:
|
||||||
if alias.name.startswith("wf_mcp"):
|
if alias.name == "wf_mcp" or alias.name.startswith("wf_mcp."):
|
||||||
violations.append(
|
violations.append(
|
||||||
f"{module}:{node.lineno}: import {alias.name}"
|
f"{module}:{node.lineno}: import {alias.name}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from wf_artifacts import FileWorkflowArtifactStore, WorkflowDeployment
|
from wf_artifacts import FileWorkflowArtifactStore, WorkflowDeployment
|
||||||
from wf_api.runs import WorkflowRunApi
|
from wf_api.runs import WorkflowRunApi
|
||||||
from wf_mcp.broker import WfMcpService
|
from wf_mcp.broker import WfMcpService
|
||||||
@@ -159,6 +161,29 @@ def test_run_api_inspect_and_bounded_trace() -> None:
|
|||||||
assert trace["trace_count"] == summary["trace_count"]
|
assert trace["trace_count"] == summary["trace_count"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_api_rejects_invalid_trace_range_before_store_lookup() -> None:
|
||||||
|
root = local_temp_root() / "run_api_invalid_trace_range"
|
||||||
|
service, _ = _service_with_echo(root)
|
||||||
|
context = context_from_service(service)
|
||||||
|
api = WorkflowRunApi(context)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="trace_range.start"):
|
||||||
|
asyncio.run(
|
||||||
|
api.read_run_trace(
|
||||||
|
run_id="missing",
|
||||||
|
trace_range=SimpleTraceRange(start=-1, limit=1),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="trace_range.limit"):
|
||||||
|
asyncio.run(
|
||||||
|
api.read_run_trace(
|
||||||
|
run_id="missing",
|
||||||
|
trace_range=SimpleTraceRange(start=0, limit=0),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_run_api_handler_delegation_matches() -> None:
|
def test_run_api_handler_delegation_matches() -> None:
|
||||||
root = local_temp_root() / "run_api_delegation"
|
root = local_temp_root() / "run_api_delegation"
|
||||||
service, _ = _service_with_echo(root)
|
service, _ = _service_with_echo(root)
|
||||||
|
|||||||
@@ -130,6 +130,18 @@ def test_next_actions_from_failed_run_recommends_bounded_trace() -> None:
|
|||||||
assert dumped["patch_examples"][0]["tool"] == NextActionTool.READ_RUN_TRACE.value
|
assert dumped["patch_examples"][0]["tool"] == NextActionTool.READ_RUN_TRACE.value
|
||||||
assert dumped["patch_examples"][0]["request"]["run_id"] == "run_123"
|
assert dumped["patch_examples"][0]["request"]["run_id"] == "run_123"
|
||||||
assert dumped["patch_examples"][0]["request"]["trace_range"]["start"] == 0
|
assert dumped["patch_examples"][0]["request"]["trace_range"]["start"] == 0
|
||||||
|
assert dumped["patch_examples"][0]["request"]["trace_range"]["limit"] == 12
|
||||||
|
|
||||||
|
|
||||||
|
def test_next_actions_from_failed_run_caps_large_trace_example() -> None:
|
||||||
|
actions = NextActions.from_run_result(
|
||||||
|
run_id="run_123",
|
||||||
|
status="failed",
|
||||||
|
trace_count=100,
|
||||||
|
diagnostics=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
dumped = actions.model_dump(mode="json")
|
||||||
assert dumped["patch_examples"][0]["request"]["trace_range"]["limit"] == 25
|
assert dumped["patch_examples"][0]["request"]["trace_range"]["limit"] == 25
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user