fix: smooth draft schema projection ux
This commit is contained in:
@@ -96,6 +96,10 @@ clear operator feedback before adding more architecture.
|
|||||||
for editing top-level workflow output bindings. Accepts repeatable `--map`
|
for editing top-level workflow output bindings. Accepts repeatable `--map`
|
||||||
and `--merge` flag. Implementation:
|
and `--merge` flag. Implementation:
|
||||||
[`set-workflow-output API/RPC/CLI`](historical/superpowers/plans/2026-06-29-set-workflow-output.md).
|
[`set-workflow-output API/RPC/CLI`](historical/superpowers/plans/2026-06-29-set-workflow-output.md).
|
||||||
|
- Completed: challenge-driven output UX polish makes `set-workflow-output`
|
||||||
|
project missing top-level output schema fields from declared `input.*` and
|
||||||
|
`state.*` sources, and challenge prompt templates now always include
|
||||||
|
`ux_issues_found: []` so debug-profile reports do not fail by omission.
|
||||||
- Keep status read-only; do not mutate registry, auth, config, or stores.
|
- Keep status read-only; do not mutate registry, auth, config, or stores.
|
||||||
|
|
||||||
## Priority 2: Durable Run/Resume Hardening
|
## Priority 2: Durable Run/Resume Hardening
|
||||||
|
|||||||
+9
-5
@@ -277,15 +277,15 @@ wf draft create concat_ws --capability wf.std.concat --name concat_ws
|
|||||||
```
|
```
|
||||||
|
|
||||||
Capability-backed draft creation auto-binds required capability inputs only.
|
Capability-backed draft creation auto-binds required capability inputs only.
|
||||||
Optional inputs remain available in the workflow input schema but are not wired
|
Optional inputs are not wired by default. Bind one when the workflow should
|
||||||
to the step until explicitly requested. Bind one when the workflow should
|
expose it; the focused helper projects the workflow input schema:
|
||||||
expose it:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wf draft bind report_ws --revision 2 --step call --from input.path --to local.path
|
wf draft bind report_ws --revision 2 --step call --from input.path --to local.path
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `wf draft set-input --merge` instead when adding several explicit mappings
|
Use `wf draft set-input --merge` instead when adding several explicit mappings
|
||||||
|
for fields already declared in the workflow input or state schema.
|
||||||
to an existing step input map.
|
to an existing step input map.
|
||||||
|
|
||||||
List and inspect drafts:
|
List and inspect drafts:
|
||||||
@@ -337,8 +337,9 @@ wf draft compile concat_ws
|
|||||||
`context.*`) to top-level output fields: `state.value=result` means
|
`context.*`) to top-level output fields: `state.value=result` means
|
||||||
`state.value -> output.result`.
|
`state.value -> output.result`.
|
||||||
|
|
||||||
The output field must already be declared in `output_schema`. The command edits
|
For single-field `input.*` and `state.*` sources, the command projects missing
|
||||||
the projection map; it does not infer or add output schema fields.
|
top-level `output_schema` fields from the source schema. More complex or
|
||||||
|
undeclared paths still rely on `wf draft validate` diagnostics.
|
||||||
|
|
||||||
By default, `set-input`, `set-output`, and `set-workflow-output` replace the
|
By default, `set-input`, `set-output`, and `set-workflow-output` replace the
|
||||||
whole map for that step or output scope. Use repeated `--map` flags in one
|
whole map for that step or output scope. Use repeated `--map` flags in one
|
||||||
@@ -386,6 +387,9 @@ Use `set-route` separately for outcome routing.
|
|||||||
|
|
||||||
Use `wf draft add-step` when adding a new capability-backed step
|
Use `wf draft add-step` when adding a new capability-backed step
|
||||||
to an existing draft. The command is explicit: it does not guess missing maps.
|
to an existing draft. The command is explicit: it does not guess missing maps.
|
||||||
|
Explicit top-level `--input input.x=x` and `--input state.x=x` mappings project
|
||||||
|
the corresponding workflow input/state schema fields from the capability input
|
||||||
|
schema.
|
||||||
When the capability declares multiple outcomes, provide exactly one
|
When the capability declares multiple outcomes, provide exactly one
|
||||||
`--route OUTCOME=TARGET` for each declared outcome. Missing or unknown outcomes
|
`--route OUTCOME=TARGET` for each declared outcome. Missing or unknown outcomes
|
||||||
are rejected before the draft is mutated.
|
are rejected before the draft is mutated.
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ challenge_report:
|
|||||||
failed: 0
|
failed: 0
|
||||||
missed_requirements:
|
missed_requirements:
|
||||||
- "none"
|
- "none"
|
||||||
|
ux_issues_found: []
|
||||||
notes: "short explanation"
|
notes: "short explanation"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -122,6 +123,8 @@ Reporting rules:
|
|||||||
abandoned workflow plans.
|
abandoned workflow plans.
|
||||||
- `attempts.failed` should count attempts that failed validation, failed to run,
|
- `attempts.failed` should count attempts that failed validation, failed to run,
|
||||||
produced wrong output, or were abandoned.
|
produced wrong output, or were abandoned.
|
||||||
|
- `ux_issues_found` should always be present. Use an empty list when you found
|
||||||
|
no UX issues; otherwise include concrete command/error/workaround notes.
|
||||||
|
|
||||||
Spawned subagents count as you. If a subagent reads product code, set
|
Spawned subagents count as you. If a subagent reads product code, set
|
||||||
`read.product_code: true`. If a subagent reads prior attempts, set
|
`read.product_code: true`. If a subagent reads prior attempts, set
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ challenge_report:
|
|||||||
failed: 0
|
failed: 0
|
||||||
missed_requirements:
|
missed_requirements:
|
||||||
- "none"
|
- "none"
|
||||||
|
ux_issues_found: []
|
||||||
notes: "short explanation"
|
notes: "short explanation"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -126,6 +127,8 @@ Reporting rules:
|
|||||||
abandoned workflow plans.
|
abandoned workflow plans.
|
||||||
- `attempts.failed` should count attempts that failed validation, failed to run,
|
- `attempts.failed` should count attempts that failed validation, failed to run,
|
||||||
produced wrong output, or were abandoned.
|
produced wrong output, or were abandoned.
|
||||||
|
- `ux_issues_found` should always be present. Use an empty list when you found
|
||||||
|
no UX issues; otherwise include concrete command/error/workaround notes.
|
||||||
|
|
||||||
Spawned subagents count as you. If a subagent reads product code, set
|
Spawned subagents count as you. If a subagent reads product code, set
|
||||||
`read.product_code: true`. If a subagent reads prior attempts, set
|
`read.product_code: true`. If a subagent reads prior attempts, set
|
||||||
|
|||||||
@@ -55,8 +55,15 @@ wf draft validate <workspace_id>
|
|||||||
wf draft save <workspace_id> --artifact <artifact_id> --version <n> --title <title>
|
wf draft save <workspace_id> --artifact <artifact_id> --version <n> --title <title>
|
||||||
|
|
||||||
Draft creation auto-binds required capability inputs only. Optional inputs are
|
Draft creation auto-binds required capability inputs only. Optional inputs are
|
||||||
reported in wrapper-hint notes; bind them explicitly with `wf draft bind` or
|
reported in wrapper-hint notes; bind them explicitly only when the workflow
|
||||||
`wf draft set-input --merge` only when the workflow should expose them.
|
should expose them. Use `wf draft bind --from input.x --to local.x` for an
|
||||||
|
existing step, or `wf draft add-step --input input.x=x` while adding a step;
|
||||||
|
both project the workflow input schema for top-level fields.
|
||||||
|
|
||||||
|
`wf draft set-workflow-output` projects missing public output schema fields for
|
||||||
|
single-field `input.*` and `state.*` sources. Prefer it for final workflow
|
||||||
|
outputs; use `wf draft bind --from local.x --to output.y` when the source is a
|
||||||
|
step-local capability output.
|
||||||
|
|
||||||
When `wf draft validate` returns a `repair_hint`, run that exact focused command
|
When `wf draft validate` returns a `repair_hint`, run that exact focused command
|
||||||
before writing JSON Patch manually. To make one capability output public, use
|
before writing JSON Patch manually. To make one capability output public, use
|
||||||
|
|||||||
@@ -115,7 +115,8 @@ wf draft add-step <workspace_id> --revision <n> --step <step_id> --capability <q
|
|||||||
`set-workflow-output` maps a graph source path (`input.*`, `state.*`, or
|
`set-workflow-output` maps a graph source path (`input.*`, `state.*`, or
|
||||||
`context.*`) to one public workflow output field. It edits top-level
|
`context.*`) to one public workflow output field. It edits top-level
|
||||||
`WorkflowDraft.output`; `set-output` edits one step's local-to-state bindings.
|
`WorkflowDraft.output`; `set-output` edits one step's local-to-state bindings.
|
||||||
The public output field must already exist in `output_schema`.
|
For single-field `input.*` and `state.*` sources, missing public output schema
|
||||||
|
fields are projected automatically from the source schema.
|
||||||
|
|
||||||
`set-input` direction: `input.text=text` means graph source `input.text` maps to
|
`set-input` direction: `input.text=text` means graph source `input.text` maps to
|
||||||
node-local target `local.text`.
|
node-local target `local.text`.
|
||||||
@@ -158,6 +159,9 @@ wf draft validate <workspace_id>
|
|||||||
capabilities require exact route coverage; missing or unknown outcomes are
|
capabilities require exact route coverage; missing or unknown outcomes are
|
||||||
rejected before mutation. It still requires explicit choices; if you do not
|
rejected before mutation. It still requires explicit choices; if you do not
|
||||||
know a map, inspect the capability or run validation rather than guessing.
|
know a map, inspect the capability or run validation rather than guessing.
|
||||||
|
Explicit top-level `--input input.x=x` and `--input state.x=x` mappings
|
||||||
|
project the corresponding workflow input/state schema fields from the
|
||||||
|
capability input schema.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wf draft add-step <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --route ok=__end__ --route error=fail --input input.text=text --input input.other=other --bind-output result=state.result --bind-output title=state.title
|
wf draft add-step <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --route ok=__end__ --route error=fail --input input.text=text --input input.other=other --bind-output result=state.result --bind-output title=state.title
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Sequence
|
from collections.abc import Mapping, Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -64,6 +64,18 @@ def _local_parts(path: str) -> tuple[str, ...]:
|
|||||||
return LocalPath.parse(path.removeprefix("local.")).parts
|
return LocalPath.parse(path.removeprefix("local.")).parts
|
||||||
|
|
||||||
|
|
||||||
|
def _schema_path_exists(schema: Mapping[str, Any], parts: Sequence[str]) -> bool:
|
||||||
|
current: Any = schema
|
||||||
|
for part in parts:
|
||||||
|
if not isinstance(current, Mapping):
|
||||||
|
return False
|
||||||
|
properties = current.get("properties")
|
||||||
|
if not isinstance(properties, Mapping) or part not in properties:
|
||||||
|
return False
|
||||||
|
current = properties[part]
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class WorkflowDraftAuthoringApi:
|
class WorkflowDraftAuthoringApi:
|
||||||
"""Capability-aware semantic edits over revisioned workflow drafts."""
|
"""Capability-aware semantic edits over revisioned workflow drafts."""
|
||||||
|
|
||||||
@@ -343,6 +355,7 @@ class WorkflowDraftAuthoringApi:
|
|||||||
source_schema=output_schema,
|
source_schema=output_schema,
|
||||||
source_field=local_field,
|
source_field=local_field,
|
||||||
target_parts=target_parts,
|
target_parts=target_parts,
|
||||||
|
allow_existing_equivalent=True,
|
||||||
)
|
)
|
||||||
output_map = {
|
output_map = {
|
||||||
**self.drafts._step_output_map(
|
**self.drafts._step_output_map(
|
||||||
@@ -434,7 +447,40 @@ class WorkflowDraftAuthoringApi:
|
|||||||
|
|
||||||
input_map = input_map or {}
|
input_map = input_map or {}
|
||||||
bind_outputs = bind_outputs or {}
|
bind_outputs = bind_outputs or {}
|
||||||
|
projected_input_schema = workspace.draft.get("input_schema", {})
|
||||||
|
if not isinstance(projected_input_schema, dict):
|
||||||
|
raise ValueError("draft input_schema must be an object")
|
||||||
projected_state_schema = state_schema
|
projected_state_schema = state_schema
|
||||||
|
input_schema = (
|
||||||
|
spec.input_schema_contract or spec.input_model.model_json_schema()
|
||||||
|
)
|
||||||
|
for graph_path, local_path in input_map.items():
|
||||||
|
try:
|
||||||
|
source_root, source_parts = _graph_parts(graph_path)
|
||||||
|
local_parts = LocalPath.parse(local_path).parts
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if source_root not in {"input", "state"} or len(local_parts) != 1:
|
||||||
|
continue
|
||||||
|
schema_key = "input_schema" if source_root == "input" else "state_schema"
|
||||||
|
target_schema = (
|
||||||
|
projected_input_schema
|
||||||
|
if source_root == "input"
|
||||||
|
else projected_state_schema
|
||||||
|
)
|
||||||
|
if _schema_path_exists(target_schema, source_parts):
|
||||||
|
continue
|
||||||
|
projected = project_property_to_schema_path(
|
||||||
|
target_schema=target_schema,
|
||||||
|
source_schema=input_schema,
|
||||||
|
source_field=local_parts[0],
|
||||||
|
target_parts=source_parts,
|
||||||
|
allow_existing_equivalent=True,
|
||||||
|
)
|
||||||
|
if schema_key == "input_schema":
|
||||||
|
projected_input_schema = projected
|
||||||
|
else:
|
||||||
|
projected_state_schema = projected
|
||||||
for output_field, path in bind_outputs.items():
|
for output_field, path in bind_outputs.items():
|
||||||
sf = state_root_field(path)
|
sf = state_root_field(path)
|
||||||
projected_state_schema = project_output_property_to_state_schema(
|
projected_state_schema = project_output_property_to_state_schema(
|
||||||
@@ -442,6 +488,7 @@ class WorkflowDraftAuthoringApi:
|
|||||||
output_schema=output_schema,
|
output_schema=output_schema,
|
||||||
output_field=output_field,
|
output_field=output_field,
|
||||||
state_field=sf,
|
state_field=sf,
|
||||||
|
allow_existing_equivalent=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
patch: list[dict[str, Any]] = [
|
patch: list[dict[str, Any]] = [
|
||||||
@@ -460,6 +507,15 @@ class WorkflowDraftAuthoringApi:
|
|||||||
"value": step_routes,
|
"value": step_routes,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
if projected_input_schema != workspace.draft.get("input_schema", {}):
|
||||||
|
patch.insert(
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
"op": "replace",
|
||||||
|
"path": "/input_schema",
|
||||||
|
"value": projected_input_schema,
|
||||||
|
},
|
||||||
|
)
|
||||||
if projected_state_schema != state_schema:
|
if projected_state_schema != state_schema:
|
||||||
patch.insert(
|
patch.insert(
|
||||||
0,
|
0,
|
||||||
|
|||||||
+106
-5
@@ -25,6 +25,7 @@ from wf_core.models.steps import (
|
|||||||
InputValueBinding,
|
InputValueBinding,
|
||||||
OutputBinding,
|
OutputBinding,
|
||||||
)
|
)
|
||||||
|
from wf_core.paths import GraphSourcePath, parse_toml_path_segments
|
||||||
|
|
||||||
from .capability_requirements import (
|
from .capability_requirements import (
|
||||||
required_capabilities_for_plan,
|
required_capabilities_for_plan,
|
||||||
@@ -43,6 +44,7 @@ from .draft_payloads import (
|
|||||||
output_bindings_payload as _draft_output_bindings_payload,
|
output_bindings_payload as _draft_output_bindings_payload,
|
||||||
)
|
)
|
||||||
from .operation_context import WorkflowOperationContext
|
from .operation_context import WorkflowOperationContext
|
||||||
|
from .schema_projection import project_property_to_schema_path
|
||||||
|
|
||||||
|
|
||||||
class WorkflowDraftApi:
|
class WorkflowDraftApi:
|
||||||
@@ -339,17 +341,87 @@ class WorkflowDraftApi:
|
|||||||
{"path": source, "target": target}
|
{"path": source, "target": target}
|
||||||
for source, target in output_map.items()
|
for source, target in output_map.items()
|
||||||
]
|
]
|
||||||
return await self.patch_draft_workspace(
|
workspace = self._draft_store().get_workspace(workspace_id)
|
||||||
workspace_id=workspace_id,
|
output_schema = self._workflow_output_schema_for_bindings(
|
||||||
revision=revision,
|
draft=workspace.draft,
|
||||||
patch=[
|
output_bindings=output_bindings,
|
||||||
|
)
|
||||||
|
patch = [
|
||||||
{
|
{
|
||||||
"op": "replace",
|
"op": "replace",
|
||||||
"path": "/output",
|
"path": "/output",
|
||||||
"value": output_bindings,
|
"value": output_bindings,
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
|
if output_schema is not workspace.draft.get("output_schema"):
|
||||||
|
patch.insert(
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
"op": "replace",
|
||||||
|
"path": "/output_schema",
|
||||||
|
"value": output_schema,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
return await self.patch_draft_workspace(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
revision=revision,
|
||||||
|
patch=patch,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _workflow_output_schema_for_bindings(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
draft: dict[str, Any],
|
||||||
|
output_bindings: Sequence[Mapping[str, Any]],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Project missing top-level output fields from input/state schemas.
|
||||||
|
|
||||||
|
This is intentionally conservative: only single-field ``input.x`` and
|
||||||
|
``state.x`` sources can be copied unambiguously. More complex sources
|
||||||
|
still fall through to existing validation diagnostics instead of
|
||||||
|
guessing a schema.
|
||||||
|
"""
|
||||||
|
output_schema = draft.get("output_schema", {})
|
||||||
|
if not isinstance(output_schema, dict):
|
||||||
|
raise ValueError("draft output_schema must be an object")
|
||||||
|
projected = output_schema
|
||||||
|
changed = False
|
||||||
|
for binding in output_bindings:
|
||||||
|
source = binding.get("path")
|
||||||
|
target = binding.get("target")
|
||||||
|
if not isinstance(source, str) or not isinstance(target, str):
|
||||||
|
continue
|
||||||
|
source_schema = _workflow_source_schema(draft, source)
|
||||||
|
if source_schema is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
target_parts = parse_toml_path_segments(target)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if _schema_path_exists(projected, target_parts):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
source_path = GraphSourcePath.parse(source)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if len(source_path.parts) != 1:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
updated = project_property_to_schema_path(
|
||||||
|
target_schema=projected,
|
||||||
|
source_schema=source_schema,
|
||||||
|
source_field=source_path.parts[0],
|
||||||
|
target_parts=target_parts,
|
||||||
|
allow_existing_equivalent=True,
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
if str(exc).startswith("source field "):
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
if updated != projected:
|
||||||
|
changed = True
|
||||||
|
projected = updated
|
||||||
|
return projected if changed else output_schema
|
||||||
|
|
||||||
def _step_input_maps(
|
def _step_input_maps(
|
||||||
self,
|
self,
|
||||||
@@ -367,6 +439,35 @@ class WorkflowDraftApi:
|
|||||||
return _output_map_from_payload(step.get("output", []))
|
return _output_map_from_payload(step.get("output", []))
|
||||||
|
|
||||||
|
|
||||||
|
def _workflow_source_schema(
|
||||||
|
draft: Mapping[str, Any],
|
||||||
|
source_path: str,
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
try:
|
||||||
|
parsed = GraphSourcePath.parse(source_path)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
if parsed.root == "input":
|
||||||
|
schema = draft.get("input_schema")
|
||||||
|
elif parsed.root == "state":
|
||||||
|
schema = draft.get("state_schema")
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
return schema if isinstance(schema, dict) else None
|
||||||
|
|
||||||
|
|
||||||
|
def _schema_path_exists(schema: Mapping[str, Any], parts: Sequence[str]) -> bool:
|
||||||
|
current: Any = schema
|
||||||
|
for part in parts:
|
||||||
|
if not isinstance(current, Mapping):
|
||||||
|
return False
|
||||||
|
properties = current.get("properties")
|
||||||
|
if not isinstance(properties, Mapping) or part not in properties:
|
||||||
|
return False
|
||||||
|
current = properties[part]
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _draft_input_maps(
|
def _draft_input_maps(
|
||||||
*,
|
*,
|
||||||
input: Sequence[InputBinding] | None,
|
input: Sequence[InputBinding] | None,
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ def project_output_property_to_state_schema(
|
|||||||
output_schema: JsonObject,
|
output_schema: JsonObject,
|
||||||
output_field: str,
|
output_field: str,
|
||||||
state_field: str,
|
state_field: str,
|
||||||
|
allow_existing_equivalent: bool = False,
|
||||||
) -> JsonObject:
|
) -> JsonObject:
|
||||||
"""Root state projection convenience wrapper.
|
"""Root state projection convenience wrapper.
|
||||||
|
|
||||||
@@ -86,6 +87,7 @@ def project_output_property_to_state_schema(
|
|||||||
source_schema=output_schema,
|
source_schema=output_schema,
|
||||||
source_field=output_field,
|
source_field=output_field,
|
||||||
target_parts=(state_field,),
|
target_parts=(state_field,),
|
||||||
|
allow_existing_equivalent=allow_existing_equivalent,
|
||||||
)
|
)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
msg = str(exc)
|
msg = str(exc)
|
||||||
|
|||||||
@@ -364,6 +364,9 @@ def set_workflow_output(
|
|||||||
This edits WorkflowDraft.output (top-level workflow output). Use
|
This edits WorkflowDraft.output (top-level workflow output). Use
|
||||||
wf draft set-output for step-level output bindings.
|
wf draft set-output for step-level output bindings.
|
||||||
|
|
||||||
|
For single-field input/state sources, missing output_schema fields are
|
||||||
|
projected automatically from the source schema.
|
||||||
|
|
||||||
Repeat --map for multiple mappings:
|
Repeat --map for multiple mappings:
|
||||||
--map state.markdown=markdown --map state.title=title
|
--map state.markdown=markdown --map state.title=title
|
||||||
|
|
||||||
|
|||||||
@@ -950,6 +950,9 @@ def test_browser_click_wrapper_produces_expected_paths_and_command_prefix() -> N
|
|||||||
)
|
)
|
||||||
assert BROWSER_CLICK_DEF.default_prompt.name == "challenge-prompt.md"
|
assert BROWSER_CLICK_DEF.default_prompt.name == "challenge-prompt.md"
|
||||||
assert BROWSER_CLICK_DEF.default_prompt.parent.name == "browser_click_challenge"
|
assert BROWSER_CLICK_DEF.default_prompt.parent.name == "browser_click_challenge"
|
||||||
|
assert "ux_issues_found: []" in BROWSER_CLICK_DEF.default_prompt.read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_generic_runner_can_be_configured_with_fake_challenge_and_fake_opencode(
|
def test_generic_runner_can_be_configured_with_fake_challenge_and_fake_opencode(
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ def test_report_challenge_prompt_requires_full_product_lifecycle() -> None:
|
|||||||
assert "render_markdown_report" in prompt
|
assert "render_markdown_report" in prompt
|
||||||
assert "deployment" in prompt.lower()
|
assert "deployment" in prompt.lower()
|
||||||
assert "run_id" in prompt
|
assert "run_id" in prompt
|
||||||
|
assert "ux_issues_found: []" in prompt
|
||||||
|
|
||||||
|
|
||||||
def test_report_challenge_workspace_template_contains_safe_input_files(
|
def test_report_challenge_workspace_template_contains_safe_input_files(
|
||||||
|
|||||||
@@ -1217,6 +1217,41 @@ async def test_set_workflow_output_map_merges_top_level_output(tmp_path: Path) -
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_set_workflow_output_map_projects_missing_output_schema(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_out_project")
|
||||||
|
api, _service, _authoring = _draft_api(artifact_store, register_echo=True)
|
||||||
|
draft = {
|
||||||
|
**_echo_draft(),
|
||||||
|
"state_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"echoed": {"type": "string"},
|
||||||
|
"after": {"type": "object"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"output_schema": {"type": "object", "properties": {}},
|
||||||
|
"output": [],
|
||||||
|
}
|
||||||
|
await api.create_draft_workspace(workspace_id="report", draft=draft)
|
||||||
|
|
||||||
|
result = await api.set_workflow_output_map(
|
||||||
|
workspace_id="report",
|
||||||
|
revision=1,
|
||||||
|
output_map={"state.after": "after"},
|
||||||
|
merge=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["status"] == "valid", result["diagnostics"]
|
||||||
|
fetched = await api.get_draft_workspace(workspace_id="report", include_draft=True)
|
||||||
|
assert fetched["draft"]["output"] == [{"path": "state.after", "target": "after"}]
|
||||||
|
assert fetched["draft"]["output_schema"]["properties"]["after"] == {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# -- Browser-click test helpers for forward-route tests --
|
# -- Browser-click test helpers for forward-route tests --
|
||||||
|
|
||||||
|
|
||||||
@@ -1312,6 +1347,44 @@ async def test_create_draft_from_capability_does_not_bind_optional_inputs(
|
|||||||
assert any("open_browser" in note for note in created["wrapper_hints"]["notes"])
|
assert any("open_browser" in note for note in created["wrapper_hints"]["notes"])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_step_projects_explicit_optional_workflow_inputs(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
api, _service = _browser_click_api(
|
||||||
|
FileWorkflowArtifactStore(tmp_path / "drafts_explicit_optional_input")
|
||||||
|
)
|
||||||
|
|
||||||
|
await api.create_draft_workspace_from_capability(
|
||||||
|
workspace_id="browser",
|
||||||
|
capability_name="local.browser_click.open_click_page",
|
||||||
|
name="browser",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await api.add_step_from_capability(
|
||||||
|
workspace_id="browser",
|
||||||
|
revision=1,
|
||||||
|
step_id="wait",
|
||||||
|
capability_name="local.browser_click.wait_for_click",
|
||||||
|
route_from_step="call",
|
||||||
|
routes={"ok": "__end__"},
|
||||||
|
input_map={
|
||||||
|
"state.session_id": "session_id",
|
||||||
|
"input.simulate": "simulate",
|
||||||
|
"input.timeout_seconds": "timeout_seconds",
|
||||||
|
},
|
||||||
|
bind_outputs={"after": "state.after"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["status"] == "valid", result["diagnostics"]
|
||||||
|
workspace = await api.get_draft_workspace(
|
||||||
|
workspace_id="browser", include_draft=True
|
||||||
|
)
|
||||||
|
properties = workspace["draft"]["input_schema"]["properties"]
|
||||||
|
assert properties["simulate"]["type"] == "object"
|
||||||
|
assert properties["timeout_seconds"]["type"] == "integer"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_add_step_persists_invalid_forward_route(tmp_path: Path) -> None:
|
async def test_add_step_persists_invalid_forward_route(tmp_path: Path) -> None:
|
||||||
api, _service = _browser_click_api(
|
api, _service = _browser_click_api(
|
||||||
@@ -1551,6 +1624,40 @@ async def test_bind_draft_local_output_to_workflow_output_reuses_compatible_stat
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_bind_draft_local_output_to_state_reuses_compatible_state(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_bind_existing_state")
|
||||||
|
api, _service, authoring = _draft_api(artifact_store, register_echo=True)
|
||||||
|
draft = _echo_draft()
|
||||||
|
draft["state_schema"] = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"echoed": {
|
||||||
|
"description": "Echoed text",
|
||||||
|
"title": "Echoed",
|
||||||
|
"type": "string",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await api.create_draft_workspace(workspace_id="report", draft=draft)
|
||||||
|
|
||||||
|
result = await authoring.bind_draft(
|
||||||
|
workspace_id="report",
|
||||||
|
revision=1,
|
||||||
|
step_id="echo",
|
||||||
|
source_path="local.echoed",
|
||||||
|
target_path="state.echoed",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["status"] == "valid", result["diagnostics"]
|
||||||
|
workspace = await api.get_draft_workspace(workspace_id="report", include_draft=True)
|
||||||
|
assert workspace["draft"]["steps"]["echo"]["output"] == [
|
||||||
|
{"source": "echoed", "target": "state.echoed"}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_bind_draft_local_output_to_quoted_workflow_output_field(
|
async def test_bind_draft_local_output_to_quoted_workflow_output_field(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
|
|||||||
@@ -99,6 +99,26 @@ def test_project_output_property_rejects_existing_state_field() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_project_output_property_allows_equivalent_existing_state_field() -> None:
|
||||||
|
state_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"after": {"type": "object"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
projected = project_output_property_to_state_schema(
|
||||||
|
state_schema=state_schema,
|
||||||
|
output_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"after": {"type": "object"}},
|
||||||
|
},
|
||||||
|
output_field="after",
|
||||||
|
state_field="after",
|
||||||
|
allow_existing_equivalent=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert projected == state_schema
|
||||||
|
|
||||||
|
|
||||||
def test_project_output_property_rejects_invalid_output_schema() -> None:
|
def test_project_output_property_rejects_invalid_output_schema() -> None:
|
||||||
with pytest.raises(ValueError, match="output_schema is not valid JSON Schema"):
|
with pytest.raises(ValueError, match="output_schema is not valid JSON Schema"):
|
||||||
project_output_property_to_state_schema(
|
project_output_property_to_state_schema(
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ def test_wf_draft_map_help_explains_replace_merge_and_validate() -> None:
|
|||||||
assert "replaces the full workflow output map" in workflow_output_help
|
assert "replaces the full workflow output map" in workflow_output_help
|
||||||
assert "Use --merge only" in workflow_output_help
|
assert "Use --merge only" in workflow_output_help
|
||||||
assert "GRAPH_SOURCE=OUTPUT_FIELD" in workflow_output_help
|
assert "GRAPH_SOURCE=OUTPUT_FIELD" in workflow_output_help
|
||||||
|
assert "output_schema fields are projected" in workflow_output_help
|
||||||
assert "draft validate" in workflow_output_help
|
assert "draft validate" in workflow_output_help
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user