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

@ -67,7 +67,7 @@
- _Requirements: 5.1, 5.3_
- _Boundary: JSON List Formatter_
- [ ] 2.3 (P) Build command-name tab completion scoped to the active context
- [x] 2.3 (P) Build command-name tab completion scoped to the active context
- Implement completion that, when the session is interactive, offers only command names valid in the currently active context, completes an unambiguous partial match in full, and lists candidates without altering the input line when more than one match exists
- Observable completion: a black-box test (using the harness from 1.7) drives a partial command name plus a tab keypress in two different contexts and confirms the completion offered differs by context and matches the single-match/multiple-match behavior
- _Requirements: 7.1, 7.2, 7.3, 7.4_
@ -178,6 +178,7 @@
## Implementation Notes
- `src/tools/xce-build.sh controller asan xce.build --incremental` writes its build dir at `src/xce.build/` (relative to the script's own location), not a repo-root `xce.build/` — meson logs are at `src/xce.build/meson-logs/testlog.txt`. (task 1.1)
- New Unity test files need two wiring points, not just meson.build's source list: also register an `extern void test_<module>(void);` + `TEST(test_<module>);` in `src/controller/testsuite/controller_test.c`, mirroring the existing `test_rcfile` registration, or the suite silently never runs. (task 1.1)
- libedit (this project's pinned NetBSD libedit 3.1) provides GNU-readline-compatible `rl_attempted_completion_function`/`rl_completion_matches` verbatim — confirmed against the actual linked header, not assumed. An ambiguous match needs a SECOND consecutive Tab to list candidates (standard readline/bash double-Tab convention) — later black-box tests using Tab must account for this. Also: `test_test_cli.c`'s `clitest_spawn()` disabling kernel ECHO also silently suppressed libedit's own completion feedback in this libedit build (bug-1792) — use the new `clitest_spawn_ex(sess, path, argv, disable_echo)` with `disable_echo=false` for any future test that needs to observe libedit UI behavior (completion, etc.), not just raw command output. (task 2.3)
- `listfmt_json.c`'s tests only exercise power-of-two double values (Unity's `TEST_ASSERT_EQUAL_DOUBLE` is disabled project-wide, matching `test_host_runusage.c`/`test_e2e_capnp.c`); cJSON's real print path uses shortest-round-trippable-double formatting so fractional values are fine, but task 3.4 should add a real fractional-value (e.g. a resource value like 12.34) round-trip test once actual command fields are wired in, since nothing here proves that end-to-end yet. (task 2.2)
- `confirm_destructive()`'s EINTR-as-decline (9.9) works because it's a single, non-looping `fgets()` call — a naive "robust I/O" retry-on-EINTR wrapper would silently defeat this requirement. `g_interactive` is exposed via a new `xcetest_is_interactive()` accessor (declared in `confirm.h`, mirroring `xcetest_interrupted()`'s pattern) for task 3.3 to reuse. Task 3.3 must short-circuit around `confirm_destructive()` entirely (not pass a parameter into it) when a command's own `--yes` flag was given — the function has no bypass-flag parameter by design. (task 2.1)
- The `test_test_cli.c` black-box harness (`clitest_*` functions) is file-static and single-file by design (matching design.md's File Structure Plan and this directory's own no-split norm) — tasks 2.3, 3.5, 4.1-4.4 should add their black-box test functions directly into this same file rather than a new one. `clitest_wait_for_text()` is generic (arbitrary needle/stream) and safe to call repeatedly mid-session; `clitest_send_signal()` targets the child's real pid via `kill()`. (task 1.7)

View file

@ -21490,6 +21490,29 @@
"related_bugs": [],
"occurrences": 1,
"last_seen": "2026-09-09T08:50:58.000Z"
},
{
"id": "bug-1792",
"timestamp": "2026-09-09T12:40:00.000Z",
"error_message": "test_test_cli.c tab-completion test (xce-test-cli-ux task 2.3): Tab keystroke produced zero effect (no bell, no redraw, no completion) when driven through the existing clitest_spawn() pty harness -- 'wo' + Tab + Enter submitted literally \"wo\" (\"wo: No such command for xce test.\"), never \"worker\".",
"file": "src/controller/testsuite/test_test_cli.c",
"root_cause": "clitest_spawn() (task 1.7) deliberately clears kernel ECHO on the pty slave BEFORE forking, documented at the time as affecting ONLY the line discipline's own re-echo of injected bytes, 'not readline()/libedit's own prompt/redraw output, which stays intact'. An A/B probe (standalone C program linked against this project's exact pinned libedit, driven both ways via a raw openpty()+fork() harness, controlling for controlling-terminal presence too) showed that assumption does not hold for THIS libedit build: when the slave's ECHO bit already reads off at the moment readline() is invoked, libedit treats the session as a silent/non-interactive (password-style) input mode and suppresses ALL of its own live editing feedback, not just kernel echo -- Tab is accepted as a no-op, the line is only ever consumed whole on Enter, and no bell/redraw/candidate-list ever appears. With kernel ECHO left at its default-on state (libedit reconfigures the tty itself once its own readline() call takes over, taking on echo/redraw duties without any raw kernel double-echo reaching the captured stream), the identical Tab sequence produced the expected bell + in-place completion + ambiguous-match candidate listing. Controlling-terminal setup (setsid()/TIOCSCTTY) was also probed and made no observable difference -- ECHO state was the sole deciding factor.",
"fix": "Refactored clitest_spawn() into clitest_spawn_ex(sess, path, argv, disable_echo) with the ECHO-clearing block made conditional on the new parameter; clitest_spawn() itself is preserved as a one-line wrapper calling clitest_spawn_ex(..., true), so every existing (output-only) caller is unaffected. The new xce-test-cli-ux task 2.3 tab-completion test calls clitest_spawn_ex(..., false) directly to keep kernel ECHO on, which restored full interactive completion behavior end-to-end (single-match auto-complete, ambiguous-match double-Tab listing, all verified via the real xce_test binary). Documented the finding in both clitest_spawn_ex()'s own comment and the file's top-of-file API summary so future tasks in this spec (3.5, 4.1-4.4) that add more interactive-feedback-dependent tests to this same file know to use clitest_spawn_ex(..., false) rather than rediscovering this the hard way.",
"tags": [
"libedit",
"readline",
"tab-completion",
"pty",
"ECHO",
"test-harness",
"xce-test-cli-ux",
"task-2.3",
"test_test_cli.c",
"silent-input-mode"
],
"related_bugs": [],
"occurrences": 1,
"last_seen": "2026-09-09T12:40:00.000Z"
}
]
}

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.

View file

@ -79,10 +79,19 @@
* child's stdin+stdout), and a plain pipe read fd carrying ONLY the
* child's stderr.
*
* clitest_spawn(sess, path, argv)
* clitest_spawn(sess, path, argv) / clitest_spawn_ex(sess, path, argv, disable_echo)
* Capability 1: launches `path` with argv (NULL-terminated, argv[0]
* conventionally the program name) with a genuine pty on stdin+stdout
* and a separate pipe on stderr.
* and a separate pipe on stderr. clitest_spawn() is the original entry
* point (kernel ECHO disabled on the pty slave before forking, the
* right choice for any test that only cares about a command's OUTPUT).
* clitest_spawn_ex() (added by xce-test-cli-ux task 2.3, Requirement 7)
* exposes the same launch with `disable_echo` explicit -- pass false
* for any test that needs to OBSERVE libedit's own live interactive
* editing/redraw output (e.g. tab completion), which this project's
* linked libedit build suppresses entirely when the slave's ECHO bit
* already reads off at readline()-call time (see clitest_spawn_ex()'s
* own comment for the full empirical finding).
*
* clitest_send_line(sess, line) / clitest_send_partial(sess, partial) /
* clitest_send_tab(sess) / clitest_send_raw(sess, buf, n)
@ -216,8 +225,8 @@ typedef struct {
bool reaped;
} clitest_session_t;
/* clitest_spawn - capability 1: forks and execs `path` (argv[0]
* conventionally the program name, NULL-terminated) with:
/* clitest_spawn_ex - capability 1 (parameterized): forks and execs `path`
* (argv[0] conventionally the program name, NULL-terminated) with:
* - a genuine pty pair on the child's stdin+stdout (openpty(), <pty.h>),
* so isatty()/g_interactive/libedit's readline() all behave exactly as
* they do for a real interactive terminal session -- the same proven
@ -228,20 +237,45 @@ typedef struct {
* entirely off the pty -- this is capability 5 (stdout/stderr as
* genuinely distinguishable streams), and exactly what Requirement 3's
* stdout/stderr-discipline testing needs.
* Kernel echo is disabled on the pty slave BEFORE forking (matching
* test_repl_prompt_context.c's verified-safe technique): this only
* suppresses the line discipline's own re-echo of bytes we write into the
* master, not readline()/libedit's own prompt/redraw output, which stays
* intact.
* `disable_echo` controls whether kernel ECHO is cleared on the pty slave
* BEFORE forking:
* - true (clitest_spawn()'s behavior, matching
* test_repl_prompt_context.c's technique) suppresses the line
* discipline's own re-echo of bytes written into the master. This was
* originally documented as affecting ONLY that kernel-level re-echo,
* "not readline()/libedit's own prompt/redraw output, which stays
* intact" -- xce-test-cli-ux task 2.3 (tab completion, Requirement 7)
* found that assumption does NOT hold for THIS project's linked
* libedit build: an A/B probe (disable_echo true vs. false, otherwise
* identical) showed that starting the slave with ECHO already off
* makes libedit treat the session as a silent/non-interactive input
* mode -- Tab is accepted as a no-op (no bell, no completion, no
* redraw at all) and the line is only ever consumed whole on Enter.
* This is the same convention GNU-readline-family libraries use for
* password-style hidden input (ECHO-off-at-call-time as the signal to
* suppress ALL of the library's own visual feedback, not just
* kernel echo) -- consistent with, not a contradiction of, the
* original comment's intent, just a sharper edge than it accounted
* for. Every test that only cares about a command's OUTPUT (the vast
* majority, e.g. this file's own smoke test) is unaffected either way.
* - false (added by task 2.3 specifically for Requirement 7's tab-
* completion tests, which NEED libedit's live interactive redraw to
* observe anything at all) leaves kernel ECHO at its default-on
* state; libedit takes over echo/redraw duties itself once its own
* readline() call reconfigures the tty, so captured stdout still
* reflects the program's editing state (bell + redraw + candidate
* listings), not a raw kernel double-echo of every byte.
* No controlling-terminal setup (no setsid()/TIOCSCTTY) is performed --
* unnecessary here because clitest_send_signal() below delivers signals
* directly via kill(pid, signo) rather than relying on the pty line
* discipline to turn a literal Ctrl-C byte into SIGINT.
* discipline to turn a literal Ctrl-C byte into SIGINT (confirmed via the
* same probe: ctty presence/absence made no observable difference to
* completion behavior -- ECHO was the sole deciding factor).
* Returns 0 on success (sess->pid/master_fd/err_fd populated) or -1 on
* failure (openpty/pipe/fork failure -- errno is left set by whichever
* call failed). */
static int clitest_spawn(clitest_session_t* sess, const char* path,
char* const argv[])
static int clitest_spawn_ex(clitest_session_t* sess, const char* path,
char* const argv[], bool disable_echo)
{
int master_fd;
int slave_fd;
@ -262,7 +296,7 @@ static int clitest_spawn(clitest_session_t* sess, const char* path,
return -1;
}
if (tcgetattr(slave_fd, &tio) == 0) {
if (disable_echo && tcgetattr(slave_fd, &tio) == 0) {
tio.c_lflag &= (tcflag_t)~ECHO;
(void)tcsetattr(slave_fd, TCSANOW, &tio);
}
@ -305,6 +339,18 @@ static int clitest_spawn(clitest_session_t* sess, const char* path,
return 0;
}
/* clitest_spawn - the pre-task-2.3 entry point, preserved with its exact
* original behavior (disable_echo=true) so every existing caller (this
* file's own smoke test included) is unaffected. See clitest_spawn_ex()'s
* comment above for why a session that needs to OBSERVE libedit's live
* interactive editing/completion feedback (task 2.3, Requirement 7) must
* call clitest_spawn_ex() directly with disable_echo=false instead. */
static int clitest_spawn(clitest_session_t* sess, const char* path,
char* const argv[])
{
return clitest_spawn_ex(sess, path, argv, true);
}
/* ------------------------------------------------------------------ */
/* Input injection */
/* ------------------------------------------------------------------ */
@ -574,8 +620,174 @@ static void test_smoke_launch_send_echo_capture_stdout_and_exit_code(void)
clitest_buf_free(&err);
}
/* ------------------------------------------------------------------ */
/* Tab completion (task 2.3, Requirement 7) */
/* ------------------------------------------------------------------ */
/* test_tab_completion_context_scoped_single_and_multi_match -- drives a
* single xce_test session through all four Requirement 7 acceptance
* criteria:
* 7.2 (single unambiguous match completes in full) in the TOP-LEVEL
* context: "wo" matches only "worker" among test.c's top_commands[].
* 7.1/7.4 (candidates scoped to the ACTIVE context, never a command
* invalid there) after switching into the worker context: "js"
* matches "jstart", a command that exists ONLY in
* test.c's worker_commands[] -- not in top_commands[] -- so getting
* it back proves completion is reading the worker context's own
* table, not a stale/global one.
* 7.3 (multiple matches list candidates without altering the input
* line) still inside the worker context: "jf" matches both "jfail"
* and "jfinish" (neither a prefix of the other, so there is no
* further common prefix to auto-lengthen). This task's own
* libedit-probe investigation (see xcetest_complete's file-header
* comment in test.c) confirmed this linked libedit's completion
* engine needs a SECOND, consecutive Tab to print the candidate
* list in that case -- the first Tap only rings the bell, standard
* GNU-readline/bash double-Tab-on-ambiguous convention, not
* something this task's code implements.
*
* Verification strategy for the two SINGLE-match cases (7.2 and 7.1/7.2/
* 7.4): the same libedit probe found that an unambiguous completion's
* on-screen redraw only repaints the SUFFIX past what is already
* displayed (e.g. typing "wo" then Tab emits "wo" + a cursor-reposition
* escape + "rker " -- never the contiguous literal substring "worker" in
* the raw byte stream), so this test does NOT strstr for the completed
* word directly. Instead it presses Tab then Enter and checks the
* resulting SEMANTIC effect, which is redraw-implementation-independent:
* an incomplete/wrong completion would submit the bare, unrecognized
* prefix ("wo"/"js"), which fails distinctly from a correct completion
* ("wo" -> "xce/worker>" context prompt; "js" -> cmd_jobstart's own
* argument-validation message). The ambiguous-match case (7.3) is
* unaffected by this redraw-suffix behavior -- the candidate list is
* printed on a fresh line, not an edit of the existing prompt line -- so
* it is verified directly via strstr for both full candidate names,
* confirmed against the raw pty transcript captured during this task's
* investigation. */
static void test_tab_completion_context_scoped_single_and_multi_match(void)
{
char xce_test_path[PATH_MAX];
char* const argv[] = { "xce_test", "--norc", NULL };
clitest_session_t sess;
clitest_buf_t out;
clitest_buf_t err;
int exit_code = -999;
const char ctrl_u = '\x15'; /* unix-line-discard */
const char enter = '\n';
TEST_ASSERT_EQUAL_INT_MESSAGE(
0, resolve_xce_test_path(xce_test_path, sizeof(xce_test_path)),
"must be able to resolve the sibling xce_test binary's path");
clitest_buf_init(&out);
clitest_buf_init(&err);
/* disable_echo=false (clitest_spawn_ex, not plain clitest_spawn): this
* test needs to OBSERVE libedit's own live interactive editing/redraw
* output (bell, candidate listings, in-place completion) -- this
* project's linked libedit build suppresses ALL of that (silent/
* password-style input mode) when the pty slave's ECHO bit already
* reads off at readline()-call time, which is exactly what plain
* clitest_spawn() does for every OTHER (output-only) test. See
* clitest_spawn_ex()'s own comment above for the full empirical A/B
* finding behind this. */
TEST_ASSERT_EQUAL_INT_MESSAGE(
0, clitest_spawn_ex(&sess, xce_test_path, argv, false),
"clitest_spawn_ex must launch xce_test behind a real pty with "
"kernel ECHO left on, so libedit's own tab-completion feedback is "
"observable");
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err, "xce>", false, 5000),
"expected the initial 'xce>' interactive prompt on stdout");
/* --- Requirement 7.2, top-level context: unambiguous "wo" -> "worker".
* Press Tab then Enter and confirm the worker-context prompt appears --
* only reachable if the submitted line was genuinely the full,
* correctly-completed "worker", not the bare "wo" prefix. */
TEST_ASSERT_EQUAL_INT(0, clitest_send_partial(&sess, "wo"));
TEST_ASSERT_EQUAL_INT(0, clitest_send_tab(&sess));
TEST_ASSERT_EQUAL_INT(0, clitest_send_raw(&sess, &enter, 1));
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err, "xce/worker>", false, 5000),
"single unambiguous match 'wo' must complete in full to 'worker' "
"in the top-level context, proven by successfully switching into "
"the worker context (Requirement 7.2)");
/* --- Requirement 7.1/7.2/7.4, worker context: unambiguous "js" ->
* "jstart", a command that does not exist at the top level at all.
* Press Tab then Enter and confirm cmd_jobstart's own bare-invocation
* message appears -- only reachable if "js" was genuinely completed to
* "jstart" (an unrecognized "js" would instead print "js: No such
* command for xce test."), proving candidates are drawn from the
* ACTIVE context's own command table, not a stale/global one. */
TEST_ASSERT_EQUAL_INT(0, clitest_send_partial(&sess, "js"));
TEST_ASSERT_EQUAL_INT(0, clitest_send_tab(&sess));
TEST_ASSERT_EQUAL_INT(0, clitest_send_raw(&sess, &enter, 1));
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err, "positive jobid", false,
3000),
"single unambiguous match 'js' must complete in full to 'jstart' "
"in the worker context, proving candidates are drawn from the "
"ACTIVE context's own command table (Requirement 7.1/7.2/7.4)");
/* --- Requirement 7.3, worker context: ambiguous "jf" matches both
* "jfail" and "jfinish" -- first Tab only bells (no further common
* prefix to lengthen), second Tab lists both candidates. */
TEST_ASSERT_EQUAL_INT(0, clitest_send_partial(&sess, "jf"));
TEST_ASSERT_EQUAL_INT(0, clitest_send_tab(&sess));
TEST_ASSERT_EQUAL_INT(0, clitest_send_tab(&sess));
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err, "jfail", false, 3000),
"ambiguous 'jf' must list 'jfail' among the candidates "
"(Requirement 7.3)");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(out.data, "jfinish"),
"ambiguous 'jf' must also list 'jfinish' among the candidates "
"(Requirement 7.3)");
/* The input line ("jf") must have been left unaltered by the listing:
* clear it and confirm the session is still perfectly usable, i.e.
* nothing about the listing corrupted readline's line-editing state. */
TEST_ASSERT_EQUAL_INT(0, clitest_send_raw(&sess, &ctrl_u, 1));
TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, ".."));
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err, "xce>", false, 5000),
"expected the top-level prompt again after '..', proving the "
"session's line-editing state survived the ambiguous-match "
"listing intact");
TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, "quit"));
TEST_ASSERT_EQUAL_INT_MESSAGE(
0, clitest_wait_for_exit(&sess, &out, &err, 5000, &exit_code),
"xce_test did not exit within the harness's safety timeout after "
"'quit'");
/* exit_code is 1, not 0: the bare "jstart" invocation above (Requirement
* 7.1/7.2/7.4's proof step) deliberately hits cmd_jobstart's own
* "you need specify a positive jobid" validation-failure return path
* on purpose (a fast, deterministic, network-free way to prove "js"
* completed to "jstart" and was recognized/executed) -- test.c's own
* g_session_failed latch (main(), near "return g_session_failed ? 1 :
* 0") is sticky for the rest of the session once ANY command returns
* nonzero, independent of the interactive continue-on-error toggle
* that let the REPL keep going afterward. This is expected, correct
* xce_test behavior given this test's own choice of proof command, not
* a defect in tab completion itself. */
TEST_ASSERT_EQUAL_INT_MESSAGE(
1, exit_code,
"xce_test should exit 1 -- the deliberate bare-'jstart' proof step "
"above latches g_session_failed even though the REPL itself kept "
"running to completion");
clitest_kill_and_reap(&sess); /* idempotent safety net */
clitest_close(&sess);
clitest_buf_free(&out);
clitest_buf_free(&err);
}
void test_test_cli(void)
{
UnitySetTestFile(__FILE__);
RUN_TEST(test_smoke_launch_send_echo_capture_stdout_and_exit_code);
RUN_TEST(test_tab_completion_context_scoped_single_and_multi_match);
}