docs: refresh roadmap and status plan
This commit is contained in:
@@ -0,0 +1,502 @@
|
||||
# wf Status And Product Smoke Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add a read-only `wf status` command that tells users what target they are connected to and summarizes server/source/admin state, then add a small product smoke test path for the remote server workflow.
|
||||
|
||||
**Architecture:** Do not add a new backend status endpoint in this slice. The CLI should compose status from existing surfaces already available on `CliContext`: workflow capability list, source inventory, admin connection/status/events/auth summaries, and source registry summaries when available. Local/static servers and remote RPC servers should both work; unavailable optional admin surfaces should report `available: false` instead of failing the whole command.
|
||||
|
||||
**Tech Stack:** Python 3.14, Typer CLI, existing `WorkflowApiSurface` / admin surface protocols, JSON output via `wf_cli.io.emit_json`, pytest, basedpyright, ruff.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Create: `src/wf_cli/commands/status.py`
|
||||
- Owns `wf status`.
|
||||
- Builds one compact JSON payload from existing CLI context surfaces.
|
||||
- Contains small private helpers for safe optional calls.
|
||||
- Modify: `src/wf_cli/app.py`
|
||||
- Register `status` as a top-level command.
|
||||
- Modify: `tests/wf_cli/test_remote_target.py`
|
||||
- Add remote status test using the existing ASGI-backed RPC client patch seam.
|
||||
- Create: `tests/wf_cli/test_status.py`
|
||||
- Add local/static and partial-unavailable behavior tests.
|
||||
- Modify: `docs/wf_cli.md`
|
||||
- Document `wf status`.
|
||||
- Modify: `docs/current_roadmap.md`
|
||||
- Mark this slice complete after implementation.
|
||||
|
||||
Do not modify `wf_server`, `wf_api`, or `wf_transport_rpc_http` unless tests prove an existing surface is missing.
|
||||
|
||||
---
|
||||
|
||||
## Payload Contract
|
||||
|
||||
`wf status` must emit JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"target": {
|
||||
"mode": "local|remote",
|
||||
"config_path": "wf.config.json",
|
||||
"url": "http://127.0.0.1:8765/rpc|null"
|
||||
},
|
||||
"workflow": {
|
||||
"capability_count": 88,
|
||||
"sample_capabilities": ["wf.std.constant"]
|
||||
},
|
||||
"sources": {
|
||||
"available": true,
|
||||
"source_count": 7,
|
||||
"sample_sources": ["wf.std"]
|
||||
},
|
||||
"admin": {
|
||||
"available": true,
|
||||
"connection_count": 4,
|
||||
"status_count": 4,
|
||||
"event_count": 12,
|
||||
"auth_count": 1
|
||||
},
|
||||
"registry": {
|
||||
"available": true,
|
||||
"entry_count": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Counts may be `0`.
|
||||
- Optional sections must keep `available: false` when unavailable.
|
||||
- Do not include auth payload values.
|
||||
- Do not include full capability/source/connection lists; this command is a quick orientation summary.
|
||||
- Do not mutate config, stores, registry, auth, or catalog.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add Local Status Command Tests
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/wf_cli/test_status.py`
|
||||
- Create later: `src/wf_cli/commands/status.py`
|
||||
- Modify later: `src/wf_cli/app.py`
|
||||
|
||||
- [ ] **Step 1: Write failing local status test**
|
||||
|
||||
Create `tests/wf_cli/test_status.py`:
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from wf_cli.app import app
|
||||
|
||||
|
||||
def test_wf_status_local_static_target(tmp_path) -> None:
|
||||
config_path = tmp_path / "wf.json"
|
||||
config_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"version": 1,
|
||||
"client": {"target": {"kind": "local"}},
|
||||
"server": {
|
||||
"store": {
|
||||
"kind": "filesystem",
|
||||
"root": str(tmp_path / "store"),
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = CliRunner().invoke(
|
||||
app,
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"--local",
|
||||
"status",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
payload = json.loads(result.output)
|
||||
assert payload["target"]["mode"] == "local"
|
||||
assert payload["target"]["config_path"] == str(config_path)
|
||||
assert payload["target"]["url"] is None
|
||||
assert payload["workflow"]["capability_count"] >= 1
|
||||
assert "wf.std.constant" in payload["workflow"]["sample_capabilities"]
|
||||
assert payload["sources"]["available"] is True
|
||||
assert payload["sources"]["source_count"] >= 1
|
||||
assert payload["admin"]["available"] is True
|
||||
assert payload["registry"]["available"] is False
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the failing test**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/wf_cli/test_status.py::test_wf_status_local_static_target -q
|
||||
```
|
||||
|
||||
Expected: FAIL because `wf status` is not registered.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Implement `wf status`
|
||||
|
||||
**Files:**
|
||||
- Create: `src/wf_cli/commands/status.py`
|
||||
- Modify: `src/wf_cli/app.py`
|
||||
|
||||
- [ ] **Step 1: Create status command module**
|
||||
|
||||
Create `src/wf_cli/commands/status.py`:
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import typer
|
||||
|
||||
from wf_cli.context import CliTyperState, load_cli_context_from_typer
|
||||
from wf_cli.io import emit_json
|
||||
from wf_cli.remote_errors import run_cli_operation
|
||||
|
||||
|
||||
def status_command(ctx: typer.Context) -> None:
|
||||
"""Print a compact read-only summary of the selected workflow target."""
|
||||
|
||||
state = CliTyperState.from_context(ctx)
|
||||
context = load_cli_context_from_typer(ctx)
|
||||
payload: dict[str, Any] = {
|
||||
"target": {
|
||||
"mode": "remote" if state.rpc_url is not None else "local",
|
||||
"config_path": str(context.config_path),
|
||||
"url": state.rpc_url,
|
||||
},
|
||||
"workflow": _workflow_status(context),
|
||||
"sources": _sources_status(context),
|
||||
"admin": _admin_status(context),
|
||||
"registry": _registry_status(context),
|
||||
}
|
||||
emit_json(payload)
|
||||
|
||||
|
||||
def _workflow_status(context) -> dict[str, Any]:
|
||||
capabilities = run_cli_operation(
|
||||
context,
|
||||
context.handlers.list_capabilities(limit=20),
|
||||
)
|
||||
items = capabilities.get("capabilities", [])
|
||||
names = [
|
||||
item.get("name")
|
||||
for item in items
|
||||
if isinstance(item, dict) and isinstance(item.get("name"), str)
|
||||
]
|
||||
return {
|
||||
"capability_count": len(items),
|
||||
"sample_capabilities": names[:5],
|
||||
}
|
||||
|
||||
|
||||
def _sources_status(context) -> dict[str, Any]:
|
||||
try:
|
||||
payload = run_cli_operation(context, context.source_admin.list_sources(limit=20))
|
||||
except Exception as exc:
|
||||
return _unavailable(exc)
|
||||
sources = payload.get("sources", [])
|
||||
source_ids = [
|
||||
item.get("id")
|
||||
for item in sources
|
||||
if isinstance(item, dict) and isinstance(item.get("id"), str)
|
||||
]
|
||||
return {
|
||||
"available": True,
|
||||
"source_count": len(sources),
|
||||
"sample_sources": source_ids[:5],
|
||||
}
|
||||
|
||||
|
||||
def _admin_status(context) -> dict[str, Any]:
|
||||
try:
|
||||
connections = run_cli_operation(context, context.admin.list_connections())
|
||||
statuses = run_cli_operation(context, context.admin.get_connection_statuses())
|
||||
events = run_cli_operation(context, context.admin.list_events())
|
||||
auth = run_cli_operation(context, context.admin.list_auth_records())
|
||||
except Exception as exc:
|
||||
return _unavailable(exc)
|
||||
return {
|
||||
"available": True,
|
||||
"connection_count": len(connections.get("connections", [])),
|
||||
"status_count": len(statuses.get("statuses", [])),
|
||||
"event_count": len(events.get("events", [])),
|
||||
"auth_count": len(auth.get("auth_records", [])),
|
||||
}
|
||||
|
||||
|
||||
def _registry_status(context) -> dict[str, Any]:
|
||||
admin = context.source_registry_admin
|
||||
if admin is None:
|
||||
return {"available": False, "reason": "source registry admin is not configured"}
|
||||
try:
|
||||
payload = run_cli_operation(context, admin.list_registry_entries(limit=20))
|
||||
except Exception as exc:
|
||||
return _unavailable(exc)
|
||||
return {
|
||||
"available": True,
|
||||
"entry_count": len(payload.get("entries", [])),
|
||||
}
|
||||
|
||||
|
||||
def _unavailable(exc: Exception) -> dict[str, Any]:
|
||||
return {
|
||||
"available": False,
|
||||
"reason": str(exc),
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Register top-level command**
|
||||
|
||||
Modify `src/wf_cli/app.py`:
|
||||
|
||||
```python
|
||||
from .commands import (
|
||||
admin,
|
||||
artifacts,
|
||||
caps,
|
||||
deployments,
|
||||
docs,
|
||||
drafts,
|
||||
explain,
|
||||
runs,
|
||||
schema,
|
||||
sources,
|
||||
status,
|
||||
)
|
||||
```
|
||||
|
||||
Add registration near the other top-level commands:
|
||||
|
||||
```python
|
||||
app.command("status")(status.status_command)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run local status test**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/wf_cli/test_status.py::test_wf_status_local_static_target -q
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 4: Typecheck the new command**
|
||||
|
||||
If basedpyright reports unknown types in helper functions, add local annotations by importing `CliContext`:
|
||||
|
||||
```python
|
||||
from wf_cli.context import CliContext, CliTyperState, load_cli_context_from_typer
|
||||
```
|
||||
|
||||
Then annotate helpers:
|
||||
|
||||
```python
|
||||
def _workflow_status(context: CliContext) -> dict[str, Any]:
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
uv run basedpyright --level error src\wf_cli\commands\status.py tests\wf_cli\test_status.py
|
||||
```
|
||||
|
||||
Expected: `0 errors`.
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Add Remote Status Test
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/wf_cli/test_remote_target.py`
|
||||
|
||||
- [ ] **Step 1: Add remote test using existing RPC patch helper**
|
||||
|
||||
Append this test near the existing remote target CLI tests:
|
||||
|
||||
```python
|
||||
def test_wf_status_uses_rpc_url_override(monkeypatch, tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
config_path = tmp_path / "wf.json"
|
||||
config_path.write_text('{"version": 1}', encoding="utf-8")
|
||||
|
||||
result = CliRunner().invoke(
|
||||
app,
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"--url",
|
||||
"http://test/rpc",
|
||||
"status",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
payload = json.loads(result.output)
|
||||
assert payload["target"]["mode"] == "remote"
|
||||
assert payload["target"]["url"] == "http://test/rpc"
|
||||
assert payload["workflow"]["capability_count"] >= 1
|
||||
assert payload["sources"]["available"] is True
|
||||
assert payload["admin"]["available"] is True
|
||||
assert payload["registry"]["available"] is False
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run remote test**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/wf_cli/test_remote_target.py::test_wf_status_uses_rpc_url_override -q
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Add Product Smoke Script Documentation
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/wf_cli.md`
|
||||
- Modify: `docs/current_roadmap.md`
|
||||
|
||||
- [ ] **Step 1: Document `wf status`**
|
||||
|
||||
In `docs/wf_cli.md`, under the remote server section or before capability
|
||||
discovery, add:
|
||||
|
||||
````markdown
|
||||
Check the selected target:
|
||||
|
||||
```bash
|
||||
wf status
|
||||
wf --url http://127.0.0.1:8765/rpc status
|
||||
```
|
||||
|
||||
`status` is read-only. It reports the selected target, capability/source
|
||||
availability, admin counts, auth record count, and desired registry count when
|
||||
the target exposes those admin surfaces. It does not return auth payload values.
|
||||
```
|
||||
````
|
||||
|
||||
- [ ] **Step 2: Mark roadmap item complete**
|
||||
|
||||
In `docs/current_roadmap.md`, under `Priority 1: Product Smoke And Status UX`,
|
||||
change:
|
||||
|
||||
```markdown
|
||||
- Add `wf status` as a compact target/server status command.
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```markdown
|
||||
- Completed: `wf status` is a compact read-only target/server status command.
|
||||
```
|
||||
|
||||
Leave the manual product smoke item open.
|
||||
|
||||
- [ ] **Step 3: Run docs lint**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
uv run ruff check docs\wf_cli.md docs\current_roadmap.md
|
||||
```
|
||||
|
||||
Expected: no lint errors. Ruff may report "No Python files found"; that is acceptable for markdown-only paths.
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Final Verification
|
||||
|
||||
**Files:** all touched files.
|
||||
|
||||
- [ ] **Step 1: Run focused tests**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/wf_cli/test_status.py tests/wf_cli/test_remote_target.py -q
|
||||
```
|
||||
|
||||
Expected: all tests pass.
|
||||
|
||||
- [ ] **Step 2: Run broader CLI/RPC smoke tests**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/wf_transport_rpc_http tests/wf_cli/test_remote_target.py tests/wf_cli/test_status.py -q
|
||||
```
|
||||
|
||||
Expected: all tests pass.
|
||||
|
||||
- [ ] **Step 3: Run lint**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
uv run ruff check src\wf_cli tests\wf_cli tests\wf_transport_rpc_http
|
||||
```
|
||||
|
||||
Expected: `All checks passed!`
|
||||
|
||||
- [ ] **Step 4: Run typecheck**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
uv run basedpyright --level error src\wf_cli tests\wf_cli\test_status.py tests\wf_cli\test_remote_target.py
|
||||
```
|
||||
|
||||
Expected: `0 errors, 0 warnings, 0 notes`.
|
||||
|
||||
- [ ] **Step 5: Manual product smoke**
|
||||
|
||||
If a server is running:
|
||||
|
||||
```powershell
|
||||
uv run wf --url http://127.0.0.1:8765/rpc status
|
||||
uv run wf --url http://127.0.0.1:8765/rpc cap call wf.std.constant --input '{"value":"status smoke"}'
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- `status` returns JSON with `target.mode == "remote"`.
|
||||
- `cap call` returns `outcome == "ok"`.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```powershell
|
||||
git add src\wf_cli\commands\status.py src\wf_cli\app.py tests\wf_cli\test_status.py tests\wf_cli\test_remote_target.py docs\wf_cli.md docs\current_roadmap.md
|
||||
git commit -m "feat: add workflow status command"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Checklist
|
||||
|
||||
- Spec coverage: this plan implements the roadmap's first Priority 1 item (`wf status`) and leaves manual product smoke as a visible follow-up.
|
||||
- Placeholder scan: no TODO/TBD placeholders remain.
|
||||
- Scope check: no new backend endpoint, no store mutation, no new server state.
|
||||
- Type consistency: `CliContext`, `CliTyperState`, and existing admin/source methods match current code.
|
||||
- Security: auth status returns counts only; no payload values.
|
||||
Reference in New Issue
Block a user