feat(xce-harness-component): relocate xce_test into standalone harness component

Move xce_test's 15 production sources + headers from src/controller/testsuite/
into a new src/harness/ component with its own meson.build, and strip every
now-dangling controller-side reference (11 controller_unit_test_src entries,
4 standalone test-binary blocks, 11 controller_test.c extern/RUN_TEST lines)
so the controller build is never left broken.

Fixes a broken relative-symlink bridge (xce_test_controller_link) added to
keep two controller-embedded tests (test_test_cli.c, test_xcetest_process_entry.c)
finding the relocated xce_test binary until task 2.1 moves them too.

Discovered and documented in design.md: four controller-domain regression
tests call functions directly from four of xce_test's own files, requiring
controller_unit_test to permanently recompile them from src/harness/ — a
real dependency neither the original discovery nor design audit caught.

controller_unit_test: 1228 Tests 0 Failures 0 Ignored (verified via the
Unity binary's own printed summary line, not just meson's aggregate count).

Task 1.1 of xce-harness-component.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLSNJhNoULvVcUhXEFafQC
This commit is contained in:
Rongsong Shen 2026-09-14 05:08:15 +00:00
parent b8ef82c1b2
commit cf55d691de
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
36 changed files with 550 additions and 180 deletions

View 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);