feat(xce-test-cli-ux): add command-name tab completion scoped to context

Registers libedit's GNU-readline-compatible rl_attempted_completion_function
under g_interactive only. The candidate generator reads active_ctx->commands[]
live on every call, so completion correctly tracks context switches rather
than freezing at registration time. Single/multi-match UI (auto-complete vs.
double-Tab candidate listing) is entirely libedit's stock behavior once given
a correct context-scoped generator. Proven via a black-box pty test verified
sound against a false-positive risk: xce_test has no fuzzy command-name
matching, so an uncompleted partial token cannot coincidentally trigger the
same effect a real completion would. Also fixes a harness bug (bug-1792):
disabling kernel ECHO for output capture was silently suppressing libedit's
own completion feedback too; added clitest_spawn_ex() with disable_echo as
an explicit parameter, clitest_spawn() unchanged for existing callers.
1307/1307 controller suite passing (+1), re-verified 8/8 for flakiness.

Task 2.3 of .kiro/specs/xce-test-cli-ux/tasks.md.
Requirements: 7.1, 7.2, 7.3, 7.4

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019jaMf9QEwpugH2W9KAuc7f
This commit is contained in:
Rongsong Shen 2026-09-09 13:38:56 +00:00
parent 07d238eada
commit 0ef232a013
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
4 changed files with 356 additions and 14 deletions

View file

@ -860,6 +860,101 @@ command_t* find_command(char* name)
return NULL;
}
/* xce-test-cli-ux task 2.3, Requirement 7: command-name tab completion.
*
* libedit API verification (design.md's flagged P0 risk, research.md's
* "libedit completion API may not perfectly mirror GNU readline" risk):
* confirmed by reading the ACTUAL linked headers in this build environment
* (this project's `src/flake.nix`-pinned libedit, pkg-config name
* "libedit", providing `editline/readline.h` -- the same header already
* included at the top of this file) plus a standalone pty-driven probe
* program built and run against this exact library. Findings:
*
* - The GNU-readline-style completion API IS present verbatim:
* `rl_completion_func_t* rl_attempted_completion_function` (signature
* `char** (*)(const char*, int, int)`, exactly matching this task's
* `xcetest_complete` shape from design.md) and
* `char** rl_completion_matches(const char*, rl_compentry_func_t*)`
* (matches on a `char* (*)(const char*, int)` generator, exactly
* `xcetest_complete_generator`'s shape). No lower-level `<histedit.h>`
* fallback was needed.
* - `rl_attempted_completion_over` exists and suppresses libedit's
* default filename-completion fallback -- used below so an empty
* match list in the active context yields NO completion rather than
* local filenames.
* - Single/multiple-match UI behavior is entirely the library's: a sole
* match is completed in full (plus a trailing space) with ZERO extra
* code (7.2); multiple matches with no further common prefix ring the
* bell on the first Tab and print the candidate list (input line left
* untouched -- confirmed via a raw pty capture) on a SECOND,
* consecutive Tap of Tab -- standard GNU-readline/bash double-Tab
* convention (7.3), not something this code implements. This
* implementation's own responsibility is narrowly the context-scoped
* candidate lookup below.
* - `rl_variable_bind("show-all-if-ambiguous", "on")` was probed and
* returns failure (1) against this libedit build -- that variable is
* not implemented here, so it is not used; the stock double-Tab
* behavior above is what ships.
*/
/* xcetest_complete_generator - the readline "compentry" generator: called
* repeatedly with state==0 on the first call (resets the scan) and
* state!=0 on each subsequent call, returning one more `strdup()`ed match
* per call (readline/libedit frees each returned string) and NULL once
* exhausted. Iterates active_ctx->commands[] -- the CURRENTLY active
* context at the moment Tab is pressed, read fresh on every call rather
* than captured at registration time -- so completion always tracks
* `worker`/`scheduler`/`client`/`..` context switches for the life of the
* session (Requirement 7.1/7.4: only the active context's commands are
* ever offered). Plain prefix match against `text`, same convention as
* find_command()'s exact-match lookup just above. */
static char* xcetest_complete_generator(const char* text, int state)
{
static int idx;
static size_t len;
if (state == 0) {
idx = 0;
len = strlen(text);
}
while (active_ctx->commands[idx].name != NULL) {
const char* name = active_ctx->commands[idx].name;
idx++;
if (strncmp(name, text, len) == 0) {
return strdup(name);
}
}
return NULL;
}
/* xcetest_complete - the readline "attempted completion" entry point,
* registered as rl_attempted_completion_function only when g_interactive
* (main(), below). `start`/`end` are byte offsets of the word being
* completed within rl_line_buffer; `start == 0` means the FIRST word on
* the line, i.e. the command name itself. Per this task's explicit
* boundary (command-NAME completion only -- no flag-value completion of
* jobids/hostnames/resource names), every other word position (start != 0)
* returns NULL and leaves rl_attempted_completion_over at its default so
* libedit's normal fallback applies there, unchanged from today's
* zero-customization behavior. For the command-name position, this always
* sets rl_attempted_completion_over so a context with zero matching
* commands offers NOTHING (never falls back to filename completion). */
static char** xcetest_complete(const char* text, int start, int end)
{
(void)end;
if (start != 0) {
return NULL;
}
rl_attempted_completion_over = 1;
return rl_completion_matches(text, xcetest_complete_generator);
}
/* Execute a command line. */
int execute_line(char* line)
{
@ -4544,6 +4639,17 @@ int main(int argc, char* argv[])
g_interactive = isatty(STDIN_FILENO);
g_continue_on_error = g_interactive ? 1 : 0;
/* xce-test-cli-ux task 2.3, Requirement 7: register command-name tab
* completion, but ONLY for a genuinely interactive session -- design.md's
* explicit precondition on xcetest_complete(). A piped/redirected/
* scripted stdin never calls readline()'s completion machinery anyway
* (no terminal to press Tab on), but gating the registration itself
* (rather than relying on that) keeps the precondition explicit and
* matches this file's existing g_interactive-gated conventions. */
if (g_interactive) {
rl_attempted_completion_function = xcetest_complete;
}
/* xce-test-rc-file task 2.2: auto-load $XDG_CONFIG_HOME/xce_test/rc (or
* $HOME/.config/xce_test/rc) before the read loop starts, for both
* interactive and scripted sessions alike -- unless --norc was given.