Skip to content

fix(cli): single model-command owner; fail loud on defaults and prompt input - #202

Merged
marlon-costa-dc merged 4 commits into
0.12.0-devfrom
fix/cli-single-command-owner-fail-loud
Sep 26, 2026
Merged

marlon-costa-dc merged 4 commits into
0.12.0-devfrom
fix/cli-single-command-owner-fail-loud

Conversation

@marlon-costa-dc

@marlon-costa-dc marlon-costa-dc commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #201. It resolves three rule violations (DRY/SSOT, zero residue, fail loud).

1. One model-command owner

  • Deleted u.Cli.build_model_command together with its Builder and SignatureCarrier. This builder duplicated cli.model_command and passed raw field annotations to Typer, so mapping and nested-model fields failed. cli.model_command stays the single owner and routes structured fields through u.Cli.is_json_option.
  • Deleted u.Cli.derive_model, which copied cli.derive_model. u.Cli.model_source_data stays because it is the primitive cli.derive_model uses.
  • Consumers: none outside flext-cli. I grepped every member of the workspace for build_model_command, u.Cli.derive_model and FlextCliUtilitiesModelCommands and found only the projected docs note that says FlextCliCli.build_model_command(...) does not exist. That note is still true.

2. u.Cli.field_default no longer drops defaults silently

  • Removed the except c.EXC_VALIDATION_TYPE_VALUE: None catch and the case _: None fall-through. None still means "no default".
  • Any other default that the CLI cannot represent now fails at cli.model_command build time. The default-source ValidationError escapes unchanged. A default that validates but that no Typer option can carry raises TypeError with the field name (c.Cli.ERR_FIELD_DEFAULT_NOT_CLI_VALUE_FMT). Structured defaults still go through the JSON-option path.

3. Prompt failures propagate

  • Removed the CLI_SAFE_EXCEPTIONS catch from confirm and the _guarded/_fatal helpers that prompt, prompt_choice and prompt_password used. An exception from the input or password reader now escapes with its cause.
  • KeyboardInterrupt and EOFError are still declared r.fail outcomes of confirm, and each result now carries the exception.
  • prompt_choice never read input. Its message parameter was used only by the removed failure log, so the parameter is gone. No member calls prompt_choice.
  • Removed the prompt failure-format constants, which nothing uses now. CLI_SAFE_EXCEPTIONS itself stays because cmd, tables, validation and cli_params still use it.

Tests

All tests go through the public service. Prompt tests use scripted input ports and check that reader failures propagate and that cancellation keeps its cause. Default tests build the command through cli.model_command and check that it fails with the cause. Tests that only asserted the old normalization were removed.

Local gates (worktree fix/cli-single-command-owner-fail-loud)

  • make gen twice: exit 0 both times, no diff after the second run.
  • make fmt: exit 0.
  • make check: exit 0 (Total: 1 Success: 1).
  • make test-full:
    • First run: exit 2. test_completion_before_wake_clear_returns_promptly failed (3.09s, limit < 2s) and test_normal_root_exit_leaves_no_descendant hit its 10s timeout. Both are timing-sensitive runtime-process tests and ran at load average ~9.8.
    • Rerun: exit 0 (incremental 2 passed; full 1200 passed).
    • These two tests are timing-flaky under load, independent of this change.

🤖 Generated with Claude Code


Summary by cubic

cli.model_command is now the single owner for model-backed commands, unrepresentable field defaults fail at command build time, and prompt input failures propagate with their cause.

  • Removes the duplicate u.Cli.build_model_command builder and u.Cli.derive_model; cli.model_command and cli.derive_model are the canonical entry points.
  • u.Cli.field_default no longer swallows invalid defaults; a validated default no Typer option can carry raises TypeError naming the field, and invalid default sources escape unchanged.
  • Prompt readers no longer catch-and-normalize exceptions; KeyboardInterrupt and EOFError remain declared outcomes and each result now carries the exception.
  • prompt_choice never read input, so its message parameter is removed.

Written for commit 0999f31. Summary will update on new commits.

Review in cubic

Marlon Costa and others added 4 commits September 26, 2026 09:40
Delete the duplicate u.Cli.build_model_command Builder (and its
SignatureCarrier) that passed raw field annotations to Typer, plus the
u.Cli.derive_model copy of cli.derive_model. No workspace member consumes
either; u.Cli.model_source_data stays as the primitive cli.derive_model uses.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Remove the CLI_SAFE_EXCEPTIONS catch-and-normalize from confirm and the
_guarded/_fatal helpers behind prompt, prompt_choice and prompt_password.
A reader exception now propagates unchanged; cancellation (KeyboardInterrupt)
and end of input (EOFError) stay declared r.fail outcomes and carry the
exception. prompt_choice never read input, so its message parameter, used
only by the removed failure log, is dropped. Tests assert propagation
through the public service with scripted input ports.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
u.Cli.field_default no longer catches the default-source validation error
and returns None. None stays the typed absence of a default; any other
unrepresentable default now fails cli.model_command at build: the
validation error escapes unchanged, and a validated default no Typer option
carries raises TypeError naming the field. Structured defaults keep the
JSON-option path. Drop the prompt failure-format constants left unused by
the prompt fail-loud change. make fmt normalized two option test files.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… test

Build the settings model before pytest.raises so the block holds exactly
one invocation that can raise (SonarCloud python:S5778 on PR #202).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@marlon-costa-dc
marlon-costa-dc merged commit 8d8b8eb into 0.12.0-dev Sep 26, 2026
7 of 9 checks passed
@marlon-costa-dc
marlon-costa-dc deleted the fix/cli-single-command-owner-fail-loud branch September 26, 2026 12:54
marlon-costa-dc added a commit that referenced this pull request Sep 26, 2026
…g-free process tests (#203)

Follow-up to #202. It resolves two rule violations: the remaining
catch-and-normalize sites (fail loud), and tests that measured host
timing instead of behaviour.

## 1. Remaining `CLI_SAFE_EXCEPTIONS` catches
This is the same violation class #202 removed from prompts.
`CLI_SAFE_EXCEPTIONS` has no consumers left, so it is deleted.

| Site | Before | After |
| --- | --- | --- |
| `u.Cli.cmd_validate_settings` (`_utilities/cmd.py`) |
`ValueError`/`TypeError`/`KeyError` became `r.fail("Settings validation
failed: …")` | No catch. Any exception escapes with its cause. |
| `u.Cli.tables_resolve_config` (`_utilities/tables.py`) | Every listed
type became `r.fail` with message only | Only the model's
`ValidationError` (the declared outcome) becomes `r.fail(…,
exception=exc)`. Everything else escapes. |
| `u.Cli.tables_render` (`_utilities/tables.py`) | Renderer errors
became `r.fail_op("Table formatting", …)` | No catch. Renderer
exceptions escape. |
| `u.Cli.process_mapping` (`_utilities/validation.py`) | `skip` mode
swallowed exceptions into a debug log and `collect` aggregated them |
Removed together with `t.Cli.MappingProcessor`. Nothing in the workspace
calls it. |
| `cli.apply_to_config` (`services/cli_params.py`) | Every listed type
became `r.fail` with message only | Only a `CliParamsConfig`
`ValidationError` becomes `r.fail(…, exception=exc)`. `params_apply`
failures remain owner `r.fail`s. Everything else escapes. |

Constants removed because nothing uses them now: `CLI_SAFE_EXCEPTIONS`,
`ERR_SETTINGS_VALIDATION_FAILED`, `OUTPUT_TABLE_FORMATTING_OPERATION`.

### Consumers (grep of every member in the workspace)
- `process_mapping`, `MappingProcessor` and `CLI_SAFE_EXCEPTIONS` have
no consumers outside flext-cli.
- `cmd_validate_settings`, `tables_resolve_config`, `tables_render` and
`apply_to_config` are called only inside flext-cli (`services/cmd.py`,
`services/tables.py`, `_utilities/formatters.py`,
`services/_cli_parts/flextclicli_part_02.py`).
- Other repos match only unrelated homonyms (`u.Web.validate_settings`,
flext-core `_validate_settings`) or generated architecture docs.

## 2. Timing-sensitive runtime-process tests
- `test_completion_before_wake_clear_returns_promptly` is now
`…_ends_monitoring`.
- The trace holds the monitor at `wake.clear()` until the waiter has set
`process_done` and `wake`. It waits without a timeout; the old 2s cap is
gone.
- A monitor that lost that wake-up would sleep until its deadline and
report `timed_out`. So `not outcome.timed_out` is the proof. The
`elapsed < 2` assertion is removed.
- `test_normal_root_exit_leaves_no_descendant`:
- The descendant writes its pid and process group to a pipe, then blocks
on an `Event`.
  - The root does a blocking `readline`, records it, and exits.
- The proof is that the pid and process group no longer exist when
`run_to_file` returns.
- Removed: file polling, the heartbeat, `sleep(0.15)`, and the `< 5s`
assertion.
  - Interpreters start with `-I -S`.
- No timeout was raised.

## Local gates (worktree `fix/cli-fail-loud-remaining`)
- `make gen` twice: exit 0 both times, and the diff hash is the same
after each run (fixed point).
- `make check`: exit 0 (`Total: 1  Success: 1`).
- `make test-full`:
- Run 1: exit 2, `1 failed, 1192 passed`, at load average ~25. The
failure was
`test_file_tools_yaml.py::test_single_source_returns_requested_model`,
blocked in `os.fsync` for 36s during `atomic_write_text_file` (host I/O
stall). It is not a test changed here. Both rewritten tests passed in
that run.
- Runs 2 and 3: exit 0 each, `1193 passed`, at load average ~20. Both
rewritten tests passed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Follow-up to #202: makes the remaining exception-handling sites fail
loud and rewrites two runtime-process tests so they assert behavior, not
the host clock. Exceptions these call paths used to normalize into
generic failures now escape with their cause; only the Pydantic
`ValidationError`s the callers declare as outcomes are still reported as
failures, and callers that relied on the old normalized messages now see
the underlying exception.

**Fail loud at remaining catch sites**

- `cmd_validate_settings` and `tables_render` no longer catch anything;
renderer errors now propagate instead of becoming `r.fail` messages.
- `tables_resolve_config` and `apply_to_config` only report the model's
`ValidationError` as a failure and now attach it via `exception=exc`;
`params_apply` failures remain the `r.fail` results their owners
produce.
- Removes `process_mapping` and `MappingProcessor`; their skip mode
swallowed exceptions into a debug log and collect mode aggregated them,
and nothing in the workspace calls them.
- Deletes the now-unused `CLI_SAFE_EXCEPTIONS`,
`ERR_SETTINGS_VALIDATION_FAILED`, and
`OUTPUT_TABLE_FORMATTING_OPERATION` constants.

**Timing-free runtime-process tests**

- The completion test holds the monitor at its first `wake.clear()`
until the root waiter has set `process_done` and `wake`, so a lost
notification would surface as `timed_out`; the `elapsed < 2` assertion,
the child `sleep`, and the 2s wait caps are gone.
- The descendant test has the child announce its pid and process group
over a pipe and then block forever; the root reads the line and exits,
and the test asserts the pid and process group no longer exist when
`run_to_file` returns. File polling, the heartbeat, `sleep(0.15)`, and
the `< 5s` assertion are removed.
- Both scenarios start interpreters with `-I -S`.

<sup>Written for commit 8280536.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/flext-sh/flext-cli/pull/203?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant