feat(xce-test-cli-ux): build destructive-command confirmation gate
confirm_destructive() prompts on stderr and blocking-reads a y/n answer via a single, non-looping fgets() call (not libedit) -- the mechanism that makes a real Ctrl-C during the prompt return EINTR and be treated as a decline (Requirement 9.9), same as an explicit "n" or EOF. confirm_set_skip()/confirm_is_skip_set() add a session-scoped toggle mirroring cmd_set's existing -e/+e style. Auto-skips when input is not interactive, via a new xcetest_is_interactive() accessor over the existing g_interactive global. Destructive-command enumeration and per-command wiring (Requirement 9.5) is task 3.3, not this task. Proven with a real fork+SIGINT test. 1297/1297 controller suite passing (+7). Task 2.1 of .kiro/specs/xce-test-cli-ux/tasks.md. Requirements: 9.1, 9.2, 9.3, 9.4, 9.6, 9.7, 9.8, 9.9 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019jaMf9QEwpugH2W9KAuc7f
This commit is contained in:
parent
7d0ac6f6eb
commit
a2686516f7
7 changed files with 580 additions and 2 deletions
101
src/controller/testsuite/confirm.h
Normal file
101
src/controller/testsuite/confirm.h
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
#pragma once
|
||||
/*
|
||||
* confirm.h -- destructive-command confirmation gate: prompts the operator
|
||||
* on stderr before a destructive command runs, offers a session-scoped
|
||||
* toggle to skip that prompt, and treats a Ctrl-C during the prompt the
|
||||
* same as declining it (xce-test-cli-ux spec, task 2.1; Requirements
|
||||
* 9.1-9.4, 9.6-9.9. Requirement 9.5 -- the five-command enumeration and
|
||||
* each cmd_*'s own `--yes` bypass flag -- is task 3.3, NOT this module;
|
||||
* this module is the generic, command-agnostic gate mechanism only).
|
||||
*
|
||||
* THE MECHANISM (see design.md's "Confirmation Gate" component and its
|
||||
* "Destructive Command Confirmation Gate" flowchart):
|
||||
* - confirm_destructive() is the gate itself. It internally checks, in
|
||||
* order: the session-scoped skip toggle (confirm_is_skip_set()), then
|
||||
* whether the top-level session is even interactive
|
||||
* (xcetest_is_interactive(), see below). If either says "don't
|
||||
* prompt", it returns 1 (proceed) immediately WITHOUT touching stdin
|
||||
* or printing anything. Otherwise it prints a yes/no prompt to
|
||||
* stderr and performs one blocking read of the answer from stdin.
|
||||
* - confirm_destructive() deliberately has NO parameter for a
|
||||
* per-command `--yes` bypass flag -- that flag is per-INVOCATION, not
|
||||
* session state, so it cannot be folded into confirm_is_skip_set()'s
|
||||
* persistent toggle without leaking into the next command. Task 3.3's
|
||||
* five call sites (cmd_jobkill, cmd_shutdown, cmd_qctrl, cmd_hostctrl,
|
||||
* cmd_upgradeconfig) are responsible for short-circuiting around
|
||||
* confirm_destructive() entirely when their OWN --yes flag was given,
|
||||
* e.g.:
|
||||
* if (!opts.yes && !confirm_destructive("kill job 1234"))
|
||||
* return NOT_RUN;
|
||||
* (design.md's Implementation Notes describes this same three-way
|
||||
* bypass -- toggle, TTY, --yes -- as one boolean expression each call
|
||||
* site evaluates; confirm_destructive() itself owns the first two,
|
||||
* the call site owns the third because only it knows its own flag.)
|
||||
*
|
||||
* RETURN CONTRACT (confirm_destructive): exactly two outcomes, matching
|
||||
* design.md's Service Interface postcondition -- no third "error" case is
|
||||
* defined for this function.
|
||||
* 1 -- proceed: skip-toggle set, OR non-interactive session, OR the
|
||||
* operator answered affirmatively ("y"/"yes", case-insensitive,
|
||||
* surrounding whitespace ignored).
|
||||
* 0 -- not run: the operator declined (anything else, including a bare
|
||||
* Enter or unrecognized text), OR stdin hit EOF, OR the prompt's
|
||||
* blocking read was interrupted by a real Ctrl-C/SIGINT (EINTR;
|
||||
* Requirement 9.9 -- treated identically to a decline, never
|
||||
* retried). The caller must report this as "not run", distinct
|
||||
* from "failed" (Requirement 9.2/9.9).
|
||||
*
|
||||
* WHY THE PROMPT READ IS A PLAIN fgets(), NOT libedit's readline(): a
|
||||
* y/n answer routed through readline() would pollute the session's
|
||||
* command history and behave inconsistently when stdin isn't a TTY
|
||||
* (design.md's Implementation Notes / research.md). It also would not
|
||||
* reliably surface EINTR the way a bare blocking libc read does.
|
||||
*
|
||||
* WHY EINTR IS RELIABLE HERE: task 1.3 (xcetest_interrupt.h,
|
||||
* xcetest_install_sigint()) replaced test.c's old `signal(SIGINT,
|
||||
* sigbreak)` installation with an explicit sigaction() call with
|
||||
* SA_RESTART cleared, once, at process startup. That is a PRECONDITION
|
||||
* of this module, not something confirm_destructive() re-checks on every
|
||||
* call -- see design.md's Service Interface preconditions.
|
||||
*/
|
||||
|
||||
/* Prompts the operator (on stderr) to confirm a destructive action
|
||||
* described by `action_description` (a short phrase completing a
|
||||
* sentence like "About to <action_description>. Continue? [y/N] " --
|
||||
* e.g. "kill job 1234"), unless the skip toggle is set or the session
|
||||
* isn't interactive, in which case it returns 1 without printing
|
||||
* anything or reading stdin. See the return contract above. */
|
||||
int confirm_destructive(const char* action_description);
|
||||
|
||||
/* Sets/gets the session-scoped confirmation-skip toggle (Requirement
|
||||
* 9.6/9.7), in the exact same style as test.c's existing g_continue_on_error
|
||||
* `-e`/`+e` toggle: a plain `static int`, defaulting to 0 (confirmation
|
||||
* required -- NOT skipped) at process start, flipped by `cmd_set` (task
|
||||
* 3.3 wires the `set` command's new branch; this module only owns the
|
||||
* flag itself). While set, confirm_destructive() proceeds without
|
||||
* prompting regardless of TTY state, so a `source`d script (Requirement
|
||||
* 13) can opt in to unattended destructive commands explicitly. */
|
||||
void confirm_set_skip(int skip);
|
||||
int confirm_is_skip_set(void);
|
||||
|
||||
/* Read-only accessor over test.c's private `g_interactive` flag (set once
|
||||
* in main() from isatty(STDIN_FILENO), never changed after that) --
|
||||
* mirrors xcetest_interrupted()'s accessor pattern over test.c's private
|
||||
* `break_out` flag (xcetest_interrupt.h, task 1.3). Declared here, not in
|
||||
* xcetest_interrupt.h, because confirm.c is its only consumer; g_interactive
|
||||
* itself is unrelated to that header's SIGINT/EINTR concern.
|
||||
*
|
||||
* Per design.md's Confirmation Gate invariant, this is deliberately the
|
||||
* TOP-LEVEL session's own stdin, not "is this line currently being read
|
||||
* from a sourced file" -- Requirement 13.7 (a later task) depends on that
|
||||
* distinction: a destructive command inside a `source`d file in an
|
||||
* otherwise-interactive session must still prompt unless the skip toggle
|
||||
* is set.
|
||||
*
|
||||
* Implemented in test.c. test.c itself is never linked into
|
||||
* controller_unit_test (it pulls in libedit/cJSON/libxce/Cap'n Proto/
|
||||
* OpenSSL and defines its own main() -- see xcetest_interrupt.h's header
|
||||
* comment for the same reasoning applied to xcetest_interrupted()), so
|
||||
* test_confirm.c supplies its own local fake definition, exactly as
|
||||
* test_interruptcall.c already does for xcetest_interrupted(). */
|
||||
int xcetest_is_interactive(void);
|
||||
Loading…
Add table
Add a link
Reference in a new issue