Skip to content

Commit e80dc90

Browse files
committed
Address eleventh review: command step fails without CLI, ID mismatch warning, state persistence
- Command step returns FAILED when CLI not installed (was silent COMPLETED) - Catalog install warns on workflow ID vs catalog key mismatch - Engine persists state.save() before returning on unknown step type - Update tests to expect FAILED for command steps without CLI - Integration tests use shell steps for CLI-independent execution
1 parent 38b7b17 commit e80dc90

4 files changed

Lines changed: 35 additions & 23 deletions

File tree

src/specify_cli/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4488,6 +4488,13 @@ def _validate_and_install_local(yaml_path: Path, source_label: str) -> None:
44884488
console.print(f" \u2022 {err}")
44894489
raise typer.Exit(1)
44904490

4491+
# Warn if the workflow's internal ID doesn't match the catalog key
4492+
if definition.id and definition.id != source:
4493+
console.print(
4494+
f"[yellow]Warning:[/yellow] Workflow ID in YAML ({definition.id!r}) "
4495+
f"differs from catalog key ({source!r}). Using catalog key."
4496+
)
4497+
44914498
registry.add(source, {
44924499
"name": definition.name or info.get("name", source),
44934500
"version": definition.version or info.get("version", "0.0.0"),

src/specify_cli/workflows/engine.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,7 @@ def _execute_steps(
524524
"error": f"Unknown step type: {step_type!r}",
525525
}
526526
)
527+
state.save()
527528
return
528529

529530
result: StepResult = step_impl.execute(step_config, context)

src/specify_cli/workflows/steps/command/__init__.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,22 @@ def execute(self, config: dict[str, Any], context: StepContext) -> StepResult:
7171
output=output,
7272
error=dispatch_result["stderr"] or f"Command exited with code {dispatch_result['exit_code']}",
7373
)
74+
return StepResult(
75+
status=StepStatus.COMPLETED,
76+
output=output,
77+
)
7478
else:
75-
output["exit_code"] = 0
79+
output["exit_code"] = 1
7680
output["dispatched"] = False
77-
78-
return StepResult(
79-
status=StepStatus.COMPLETED,
80-
output=output,
81-
)
81+
return StepResult(
82+
status=StepStatus.FAILED,
83+
output=output,
84+
error=(
85+
f"Cannot dispatch command {command!r}: "
86+
f"integration {integration!r} CLI not found or not installed. "
87+
f"Install the CLI tool or check 'specify integration list'."
88+
),
89+
)
8290

8391
@staticmethod
8492
def _try_dispatch(

tests/test_workflows.py

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ def test_execute_basic(self):
414414
"input": {"args": "{{ inputs.name }}"},
415415
}
416416
result = step.execute(config, ctx)
417-
assert result.status == StepStatus.COMPLETED
417+
assert result.status == StepStatus.FAILED
418418
assert result.output["command"] == "speckit.specify"
419419
assert result.output["integration"] == "claude"
420420
assert result.output["input"]["args"] == "login"
@@ -473,7 +473,7 @@ def test_options_merge(self):
473473
assert result.output["options"]["thinking-budget"] == 32768
474474

475475
def test_dispatch_not_attempted_without_cli(self):
476-
"""When the CLI tool is not installed, dispatched should be False."""
476+
"""When the CLI tool is not installed, step should fail."""
477477
from specify_cli.workflows.steps.command import CommandStep
478478
from specify_cli.workflows.base import StepContext, StepStatus
479479

@@ -489,9 +489,9 @@ def test_dispatch_not_attempted_without_cli(self):
489489
"input": {"args": "{{ inputs.name }}"},
490490
}
491491
result = step.execute(config, ctx)
492-
assert result.status == StepStatus.COMPLETED
492+
assert result.status == StepStatus.FAILED
493493
assert result.output["dispatched"] is False
494-
assert result.output["exit_code"] == 0
494+
assert result.error is not None
495495

496496
def test_dispatch_with_mock_cli(self, tmp_path, monkeypatch):
497497
"""When the CLI is installed, dispatch invokes the command by name."""
@@ -1328,7 +1328,7 @@ def test_execute_simple_workflow(self, project_dir):
13281328
engine = WorkflowEngine(project_dir)
13291329
state = engine.execute(definition, {"name": "login"})
13301330

1331-
assert state.status == RunStatus.COMPLETED
1331+
assert state.status == RunStatus.FAILED
13321332
assert "step-one" in state.step_results
13331333
assert state.step_results["step-one"]["output"]["command"] == "speckit.specify"
13341334
assert state.step_results["step-one"]["output"]["input"]["args"] == "login"
@@ -1345,18 +1345,16 @@ def test_execute_with_gate_pauses(self, project_dir):
13451345
version: "1.0.0"
13461346
steps:
13471347
- id: step-one
1348-
command: speckit.specify
1349-
input:
1350-
args: "test"
1348+
type: shell
1349+
run: "echo test"
13511350
- id: gate
13521351
type: gate
13531352
message: "Review?"
13541353
options: [approve, reject]
13551354
on_reject: abort
13561355
- id: step-two
1357-
command: speckit.plan
1358-
input:
1359-
args: "test"
1356+
type: shell
1357+
run: "echo done"
13601358
"""
13611359
definition = WorkflowDefinition.from_string(yaml_str)
13621360
engine = WorkflowEngine(project_dir)
@@ -1726,9 +1724,8 @@ def test_full_sequential_workflow(self, project_dir):
17261724
default: "login"
17271725
steps:
17281726
- id: specify
1729-
command: speckit.specify
1730-
input:
1731-
args: "{{ inputs.feature }}"
1727+
type: shell
1728+
run: "echo speckit.specify {{ inputs.feature }}"
17321729
17331730
- id: check-scope
17341731
type: if
@@ -1743,9 +1740,8 @@ def test_full_sequential_workflow(self, project_dir):
17431740
run: "echo partial scope"
17441741
17451742
- id: plan
1746-
command: speckit.plan
1747-
input:
1748-
args: "{{ steps.specify.output.command }}"
1743+
type: shell
1744+
run: "echo speckit.plan"
17491745
"""
17501746
definition = WorkflowDefinition.from_string(yaml_str)
17511747
engine = WorkflowEngine(project_dir)

0 commit comments

Comments
 (0)