test(xce-test-cli-ux): verify help/completion/context-switching hold together (task 4.4)

Add one black-box regression test proving process-level help, in-session
help content, and context-scoped tab completion work correctly when
exercised together in one continuous session (context switches +
completion + help lookups), not just individually as each was already
covered by tasks 1.6/2.3/3.5. This closes the final task of the
xce-test-cli-ux implementation plan (23/23).

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-10 04:36:42 +00:00
parent 31d6606067
commit 38595099b9
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
2 changed files with 366 additions and 1 deletions

View file

@ -2852,6 +2852,368 @@ test_help_context_scoped_not_found_no_cross_context_hint(void)
clitest_buf_free(&err);
}
/*
* test_help_completion_and_context_switching_hold_together_in_one_session --
* xce-test-cli-ux task 4.4, Requirements 4.1-4.3, 7.1-7.4, 8.1-8.7, 11.2.
*
* Every mechanism this test touches (process-level help/version, in-session
* `help <cmd>`, tab completion) already has dedicated per-mechanism coverage
* elsewhere in this file (and, for process-level flags, in
* test_xcetest_process_entry.c from task 1.6). What none of those tests do
* is run several of these mechanisms back-to-back in ONE live session,
* interleaved with real context switches -- exactly task 4.4's own
* "hold together ... not just in isolation" framing. This test is that
* missing combined/integration run:
*
* 1. Top-level, ambiguous "w" (ambiguous across worker/waldump/
* walcompare) is double-Tab listed; the candidate set is confirmed to
* include "worker" (a top-level-only command, not present at all in
* the client context) and to EXCLUDE "workerlist"/"wait-primary"
* (client-only).
* 2. Top-level, unambiguous "cl" -> Tab -> Enter completes to "client" in
* full and switches context (Requirement 7.1/7.2).
* 3. Client context, ambiguous "w" is double-Tab listed; the candidate
* set now includes "workerlist"/"waldump"/"walcompare"/"wait-primary"
* -- a DIFFERENT set than step 1's, for the IDENTICAL typed prefix, in
* the SAME running process (Requirement 7.1/7.4).
* 4. Client context, unambiguous "jobk" -> Tab -> Enter completes to
* "jobkill" and actually executes it bare (no live controller needed:
* cmd_jobkill's own missing-jobid validation returns before ever
* reaching the RPC/confirmation-gate code, exactly like task 2.3's
* "jstart" proof step).
* 5. Still in the client context -- and only NOW, after two context
* switches and two tab-completion demonstrations -- `help jobkill` is
* looked up. Its rendered text is confirmed to include the
* hand-authored purpose description (8.1), the flagspec_render_help()
* -generated "-y, --yes" flag line (8.2), the hand-authored example
* invocation (8.3), and the confirmation-gate/`set -y` scripting note
* (8.4) -- proving prior session state (context switches, completion)
* did not corrupt this content.
* 6. `..` then `worker` switches to a THIRD context (a context this
* session has not yet visited).
* 7. Worker context, ambiguous "j" (jstart/jfinish/jfail) is double-Tab
* listed; the candidate set is confirmed to include jstart/jfinish/
* jfail and to EXCLUDE every client-only "job*" command (jobkill,
* jobrun, jobsub, jobinfo, jobckpt) -- a second, independent
* different-contexts-different-candidates proof (worker vs. client).
* 8. `help jobkill` in the worker context -- a name valid in the OLD
* (client) context but not this one -- is confirmed reported as not
* found, with the corrected grammar (11.2: "match", not "matchs"),
* never leaking client context's jobkill text into the not-found
* response.
*
* Buffers are freed and reinitialized between stages (the same idiom
* test_help_context_scoped_not_found_no_cross_context_hint above already
* uses across its own context switch) so each stage's assertions search
* only that stage's own freshly captured output, never a stale match left
* over from an earlier stage's text.
*
* Requirement 4.1-4.3 (process-level -h/--help/--version describing the
* in-session `help` command) is deliberately NOT re-exercised here: the
* task's own guidance treats a process-level flag invocation as inherently
* a separate process from an interactive session, and
* test_xcetest_process_entry.c (task 1.6) already covers it end-to-end
* against the real binary. This task's implementer report quotes that
* output directly instead of duplicating the check here.
*/
static void
test_help_completion_and_context_switching_hold_together_in_one_session(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 for the WHOLE session: it needs libedit's live
* completion feedback (steps 1, 3, 7) as well as ordinary command
* output (steps 2, 4, 5, 6, 8) -- task 2.3's own header comment
* confirms full-line sends behave identically either way, and its own
* combined tab-completion test already mixes both under
* disable_echo=false throughout. */
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");
/* --- Step 1: top-level, ambiguous "w" -> {worker, waldump,
* walcompare}. "worker" exists ONLY at top level (client has
* "workerlist" instead), so its presence here plus its absence in step
* 3's client-context listing is the clearest possible proof that
* candidates are context-scoped for the SAME typed prefix. */
clitest_buf_free(&out);
clitest_buf_free(&err);
clitest_buf_init(&out);
clitest_buf_init(&err);
TEST_ASSERT_EQUAL_INT(0, clitest_send_partial(&sess, "w"));
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, "walcompare", false, 3000),
"top-level ambiguous 'w' must list 'walcompare' among the "
"candidates");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(out.data, "worker"),
"top-level ambiguous 'w' must list 'worker' among the candidates "
"(Requirement 7.1/7.3)");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(out.data, "waldump"),
"top-level ambiguous 'w' must list 'waldump' among the "
"candidates");
TEST_ASSERT_NULL_MESSAGE(
strstr(out.data, "workerlist"),
"top-level ambiguous 'w' must NOT offer 'workerlist' -- that "
"command exists only in the client context (Requirement 7.4)");
TEST_ASSERT_NULL_MESSAGE(
strstr(out.data, "wait-primary"),
"top-level ambiguous 'w' must NOT offer 'wait-primary' -- that "
"command exists only in the client context (Requirement 7.4)");
/* Clear the unaltered "w" input line (Requirement 7.3) and confirm the
* session is still usable by switching context next. */
TEST_ASSERT_EQUAL_INT(0, clitest_send_raw(&sess, &ctrl_u, 1));
/* --- Step 2: top-level, unambiguous "cl" -> Tab -> Enter completes in
* full to "client" and switches context (Requirement 7.1/7.2). */
clitest_buf_free(&out);
clitest_buf_free(&err);
clitest_buf_init(&out);
clitest_buf_init(&err);
TEST_ASSERT_EQUAL_INT(0, clitest_send_partial(&sess, "cl"));
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/client>", false, 5000),
"single unambiguous match 'cl' must complete in full to 'client' "
"and switch context (Requirement 7.1/7.2)");
/* --- Step 3: client context, ambiguous "w" -> {workerlist, waldump,
* walcompare, wait-primary} -- a DIFFERENT candidate set than step 1's,
* for the IDENTICAL typed prefix, in the SAME running process. */
clitest_buf_free(&out);
clitest_buf_free(&err);
clitest_buf_init(&out);
clitest_buf_init(&err);
TEST_ASSERT_EQUAL_INT(0, clitest_send_partial(&sess, "w"));
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, "wait-primary", false,
3000),
"client-context ambiguous 'w' must list 'wait-primary' among the "
"candidates -- proving the SAME prefix offers a DIFFERENT "
"candidate in this context than at top level (Requirement 7.1/7.4)");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(out.data, "workerlist"),
"client-context ambiguous 'w' must list 'workerlist' (not bare "
"'worker', which is not a client command) among the candidates");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(out.data, "waldump"),
"client-context ambiguous 'w' must also list 'waldump'");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(out.data, "walcompare"),
"client-context ambiguous 'w' must also list 'walcompare'");
TEST_ASSERT_EQUAL_INT(0, clitest_send_raw(&sess, &ctrl_u, 1));
/* --- Step 4: client context, unambiguous "jobk" -> Tab -> Enter
* completes in full to "jobkill" and actually runs it bare. No live
* controller is needed: cmd_jobkill's own missing-'-j' validation
* returns (to stderr) before ever reaching the RPC/confirmation-gate
* code, exactly like task 2.3's "jstart" proof step above. */
clitest_buf_free(&out);
clitest_buf_free(&err);
clitest_buf_init(&out);
clitest_buf_init(&err);
TEST_ASSERT_EQUAL_INT(0, clitest_send_partial(&sess, "jobk"));
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,
"you need to specify jobid with -j", true,
3000),
"single unambiguous match 'jobk' must complete in full to "
"'jobkill' in the client context (a command absent from both the "
"top-level and worker contexts), proven by hitting cmd_jobkill's "
"own bare-invocation validation message (Requirement 7.1/7.2/7.4)");
/* --- Step 5: NOW, mid-session (after two context switches and two
* tab-completion demonstrations), look up jobkill's full help text.
* Requirements 8.1-8.4: purpose description, generated flag block,
* hand-authored example, and confirmation-gate scripting note. */
clitest_buf_free(&out);
clitest_buf_free(&err);
clitest_buf_init(&out);
clitest_buf_init(&err);
TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, "help jobkill"));
/* Each phrase gets its OWN clitest_wait_for_text() call (not a single
* wait followed by plain strstr) -- cmd_help composes this output from
* several separate prints (description -> generated flagspec_render_
* help() block -> hand-authored tail), which this harness's poll loop
* can observe arriving across more than one read() chunk. This matches
* test_help_jobkill_shows_generated_flag_and_confirmation_note's own
* established pattern above, not the single-wait-then-strstr shortcut
* used for the double-Tab candidate listings elsewhere in this test
* (those really are one burst -- this multi-part composition is not,
* confirmed by this exact test failing on a bare strstr on first
* write). */
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err,
"Sends a signal to a running job", false,
5000),
"Requirement 8.1: mid-session 'help jobkill' must still include "
"its purpose description");
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err, "-y, --yes", false, 5000),
"Requirement 8.2: mid-session 'help jobkill' must still show the "
"generated -y/--yes flag line");
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err, "example: jobkill", false,
5000),
"Requirement 8.3: mid-session 'help jobkill' must still retain "
"its example invocation");
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err,
"destructive action and prompts for "
"confirmation",
false, 5000),
"Requirement 8.4: mid-session 'help jobkill' must still mention "
"the confirmation gate as a related scripting note");
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err, "set -y", false, 5000),
"the confirmation note must still point at `set -y` as the "
"session-wide bypass, even mid-session");
/* --- Step 6: switch to a THIRD context this session has not yet
* visited (worker), via '..' then 'worker' as plainly-typed lines. */
clitest_buf_free(&out);
clitest_buf_free(&err);
clitest_buf_init(&out);
clitest_buf_init(&err);
TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, ".."));
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err, "xce>", false, 5000),
"expected to return to the top-level context after '..'");
TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, "worker"));
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err, "xce/worker>", false, 5000),
"expected to enter the worker context");
/* --- Step 7: worker context, ambiguous "j" -> {jstart, jfinish,
* jfail}, and NONE of the client-only "job*" commands -- a second,
* independent different-contexts-different-candidates proof. */
clitest_buf_free(&out);
clitest_buf_free(&err);
clitest_buf_init(&out);
clitest_buf_init(&err);
TEST_ASSERT_EQUAL_INT(0, clitest_send_partial(&sess, "j"));
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, "jfinish", false, 3000),
"worker-context ambiguous 'j' must list 'jfinish' among the "
"candidates");
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(out.data, "jstart"),
"worker-context ambiguous 'j' must also "
"list 'jstart'");
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(out.data, "jfail"),
"worker-context ambiguous 'j' must also "
"list 'jfail'");
TEST_ASSERT_NULL_MESSAGE(
strstr(out.data, "jobkill"),
"worker-context ambiguous 'j' must NOT offer 'jobkill' -- that "
"command exists only in the client context (Requirement 7.4)");
TEST_ASSERT_NULL_MESSAGE(
strstr(out.data, "jobrun"),
"worker-context ambiguous 'j' must NOT offer 'jobrun' either "
"(client-only)");
TEST_ASSERT_NULL_MESSAGE(
strstr(out.data, "jobsub"),
"worker-context ambiguous 'j' must NOT offer 'jobsub' either "
"(client-only)");
TEST_ASSERT_NULL_MESSAGE(
strstr(out.data, "jobinfo"),
"worker-context ambiguous 'j' must NOT offer 'jobinfo' either "
"(client-only)");
TEST_ASSERT_NULL_MESSAGE(
strstr(out.data, "jobckpt"),
"worker-context ambiguous 'j' must NOT offer 'jobckpt' either "
"(client-only)");
TEST_ASSERT_EQUAL_INT(0, clitest_send_raw(&sess, &ctrl_u, 1));
/* --- Step 8: 'help jobkill' in the worker context -- a name valid in
* the OLD (client) context but not this one -- must be reported not
* found, with the corrected grammar (Requirement 11.2), never leaking
* client context's jobkill text into the not-found response. This is
* the same not-found behavior test_help_context_scoped_not_found_no_
* cross_context_hint proves from the top-level context; here it is
* proven from the worker context, mid-session, after everything above
* has already run. */
clitest_buf_free(&out);
clitest_buf_free(&err);
clitest_buf_init(&out);
clitest_buf_init(&err);
TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, "help jobkill"));
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err,
"No commands match `jobkill'", true, 5000),
"Requirement 8.7/11.2: from the worker context (jobkill's OLD "
"context, client, is not the active one), 'help jobkill' must be "
"reported as not found there, with corrected grammar");
TEST_ASSERT_NULL_MESSAGE(
strstr(err.data, "matchs"),
"the corrected not-found message must not contain the old "
"'matchs' typo anywhere in the captured output");
TEST_ASSERT_NULL_MESSAGE(
strstr(out.data, "No commands match"),
"the not-found diagnostic must not also appear on stdout");
TEST_ASSERT_NULL_MESSAGE(
strstr(out.data, "Sends a signal to a running job"),
"the worker-context not-found response must never leak client "
"context's jobkill help content");
TEST_ASSERT_NULL_MESSAGE(
strstr(err.data, "Sends a signal to a running job"),
"the worker-context not-found response must never leak client "
"context's jobkill help content, on stderr either");
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'");
/* Step 4's deliberate bare "jobkill" latches g_session_failed for the
* rest of the session (same mechanism, and same deliberate choice of
* proof command, as test_tab_completion_context_scoped_single_and_
* multi_match above) even though the REPL itself kept running to
* completion through every later step. */
TEST_ASSERT_EQUAL_INT_MESSAGE(
1, exit_code,
"xce_test should exit 1 -- the deliberate bare-'jobkill' proof "
"step in step 4 latches g_session_failed even though the REPL "
"itself kept running to completion through every later step");
clitest_kill_and_reap(&sess);
clitest_close(&sess);
clitest_buf_free(&out);
clitest_buf_free(&err);
}
/*
* test_expect_jobs_malformed_count_diagnostic_on_stderr_not_stdout --
* xce-test-cli-ux task 3.6, Requirement 3.1/3.2/3.3/3.4: `expect-jobs` given
@ -3074,6 +3436,8 @@ void test_test_cli(void)
RUN_TEST(test_help_reslist_shows_json_example);
RUN_TEST(test_help_unknown_command_shows_corrected_grammar);
RUN_TEST(test_help_context_scoped_not_found_no_cross_context_hint);
RUN_TEST(
test_help_completion_and_context_switching_hold_together_in_one_session);
RUN_TEST(test_expect_jobs_malformed_count_diagnostic_on_stderr_not_stdout);
RUN_TEST(test_wait_primary_invalid_timeout_diagnostic_on_stderr_not_stdout);
RUN_TEST(test_set_list_success_result_on_stdout_not_stderr);