fix(cli): filter unknown --inputs keys even on flow persistence restore

Review follow-up: the `id` (persistence-restore) branch of
_resolve_flow_inputs returned the raw payload, so typo keys passed alongside
`id` skipped the unknown-key warning/drop and reached kickoff — which can
fail strict (extra="forbid") flow state models. The restore path now still
warns on and drops unknown keys (keeping `id` and known state fields); it
only skips the required-field prompt and pre-kickoff validation, which
persistence hydrates. Regression test: test_id_restore_still_drops_unknown_keys.

Also drop the duplicate module import in test_input_prompt.py (both `import`
and `from ... import` of crewai_cli.input_prompt) flagged by the code-quality
bot; monkeypatching now uses the string target form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RBYGqJHC2TMC6fonFziuuh
This commit is contained in:
Joao Moura
2026-07-07 14:03:04 -07:00
parent 592d27f808
commit ae05160f48
3 changed files with 36 additions and 11 deletions

View File

@@ -89,13 +89,6 @@ def _resolve_flow_inputs(flow: Any, provided: dict[str, Any]) -> dict[str, Any]:
# dict / unschematized state — nothing to derive; pass inputs through.
return dict(provided)
# ``id`` signals a persistence restore: kickoff hydrates the full state from
# storage, so required fields may come from the restored state rather than
# --inputs. Forward the inputs unchanged instead of prompting/erroring for
# fields the resume will supply.
if "id" in provided:
return dict(provided)
properties = {
name: spec
for name, spec in (schema.get("properties") or {}).items()
@@ -104,10 +97,21 @@ def _resolve_flow_inputs(flow: Any, provided: dict[str, Any]) -> dict[str, Any]:
state_model = type(flow.state)
defaults = _flow_state_defaults(flow)
# ``id`` signals a persistence restore: kickoff hydrates the full state from
# storage, so required fields may come from the restored state rather than
# --inputs. We still filter the rest of the payload below, but skip the
# required-field prompt and pre-kickoff validation, which would otherwise
# fail on fields the resume will supply.
restoring = "id" in provided
# Unknown keys are almost always typos — warn and drop them (they'd fail
# structured-state validation at kickoff anyway).
# structured-state validation at kickoff anyway). ``id`` is a reserved
# kickoff key rather than a state field, so forward it untouched.
collected: dict[str, Any] = {}
for key, value in provided.items():
if key == "id":
collected["id"] = value
continue
if key in properties:
collected[key] = value
continue
@@ -119,6 +123,9 @@ def _resolve_flow_inputs(flow: Any, provided: dict[str, Any]) -> dict[str, Any]:
err=True,
)
if restoring:
return collected
missing = _missing_required(state_model, {**defaults, **collected})
if missing and _is_interactive():
collected.update(

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
import pytest
import crewai_cli.input_prompt as input_prompt_module
from crewai_cli.input_prompt import (
closest_name,
parse_inputs_json,
@@ -51,7 +50,7 @@ def test_prompt_for_inputs_uses_describe_and_coerce(monkeypatch, capsys):
seen.append(text)
return "42"
monkeypatch.setattr(input_prompt_module.click, "prompt", fake_prompt)
monkeypatch.setattr("crewai_cli.input_prompt.click.prompt", fake_prompt)
result = prompt_for_inputs(
["count"],
@@ -71,7 +70,7 @@ def test_prompt_for_inputs_uses_describe_and_coerce(monkeypatch, capsys):
def test_prompt_for_inputs_keeps_raw_string_without_coerce(monkeypatch):
monkeypatch.setattr(
input_prompt_module.click, "prompt", lambda text, **kwargs: "AI"
"crewai_cli.input_prompt.click.prompt", lambda text, **kwargs: "AI"
)
result = prompt_for_inputs(

View File

@@ -381,3 +381,22 @@ def test_id_only_input_skips_required_validation(tmp_path: Path) -> None:
resolved = run_declarative_flow_module._resolve_flow_inputs(flow, {"id": "run-123"})
assert resolved == {"id": "run-123"}
def test_id_restore_still_drops_unknown_keys(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
# A persistence restore (`id` present) still filters typo keys so they don't
# reach kickoff and trip strict (extra="forbid") state models — it only
# skips the required-field prompt/validation, not the unknown-key warning.
path = _write(tmp_path, REQUIRED_FLOW_YAML)
flow = run_declarative_flow_module.load_declarative_flow(str(path))
resolved = run_declarative_flow_module._resolve_flow_inputs(
flow, {"id": "run-123", "prospect_emai": "typo"}
)
captured = capsys.readouterr()
assert resolved == {"id": "run-123"} # id kept, typo dropped
assert "Ignoring unknown input 'prospect_emai'" in captured.err
assert "Ignoring unknown input 'id'" not in captured.err