feat(xce-test-cli-ux): add flag typo-suggestion and help-block renderer

flagspec_reject() now names a likely-intended flag when an unrecognized
flag closely matches exactly one entry in the descriptor table (genuine
Levenshtein DP, silent on zero or ambiguous matches). flagspec_render_help()
generates the short/long/required-optional/default flag-listing block from
a descriptor table. 1278/1278 controller suite passing (+5).

Task 1.2 of .kiro/specs/xce-test-cli-ux/tasks.md.
Requirements: 6.1, 6.2, 6.3, 6.4, 8.2, 10.1, 10.2, 10.3

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 07:40:46 +00:00
parent 0748f10cc6
commit 89af46af3e
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
4 changed files with 335 additions and 17 deletions

View file

@ -375,6 +375,116 @@ static void test_reject_no_argument_flag_given_a_value_reports_distinctly(void)
cleanup_dir();
}
/* ---- flagspec_reject() typo-suggestion tests (Requirement 10) ---------
* A second fixture, g_ambiguous_spec, is purpose-built for the
* ambiguous-match case below so its edit distances are deliberate rather
* than incidental to g_testcmd_spec's two very dissimilar flag names. */
static const flagspec_t g_ambiguous_spec[2] = {
{ 0, "targeta", "target-a", no_argument, 0, NULL },
{ 0, "targetb", "target-b", no_argument, 0, NULL },
};
static void test_reject_suggests_single_close_match_flag_typo(void)
{
/* "--hst" is "--host" with the 'o' deleted -- one edit, well inside
* the closeness threshold for a 6-character long-form spelling -- and
* is nowhere near "--command". Requirement 10.1: name the
* likely-intended flag in the rejection message. */
fresh_dir();
capture_t cap;
capture_begin(&cap);
int rc = flagspec_reject("testcmd", g_testcmd_spec, 2, "--hst",
g_testcmd_usage);
char out_buf[512], err_buf[512];
capture_end(&cap, out_buf, sizeof(out_buf), err_buf, sizeof(err_buf));
TEST_ASSERT_TRUE(rc < 0);
TEST_ASSERT_EQUAL_STRING("", out_buf);
TEST_ASSERT_NOT_NULL(strstr(err_buf, "unrecognized flag '--hst'"));
TEST_ASSERT_NOT_NULL(strstr(err_buf, "did you mean"));
TEST_ASSERT_NOT_NULL(strstr(err_buf, "--host"));
cleanup_dir();
}
static void test_reject_gives_no_suggestion_for_a_distant_typo(void)
{
/* "--wombat" does not closely resemble "--host" or "--command" by any
* reasonable edit-distance threshold. Requirement 10.2: report
* unrecognized without guessing. */
fresh_dir();
capture_t cap;
capture_begin(&cap);
int rc = flagspec_reject("testcmd", g_testcmd_spec, 2, "--wombat",
g_testcmd_usage);
char out_buf[512], err_buf[512];
capture_end(&cap, out_buf, sizeof(out_buf), err_buf, sizeof(err_buf));
TEST_ASSERT_TRUE(rc < 0);
TEST_ASSERT_EQUAL_STRING("", out_buf);
TEST_ASSERT_NOT_NULL(strstr(err_buf, "unrecognized flag '--wombat'"));
TEST_ASSERT_NULL(strstr(err_buf, "did you mean"));
cleanup_dir();
}
static void test_reject_gives_no_suggestion_when_typo_is_ambiguous(void)
{
/* "--targetc" is exactly one substitution away from BOTH "--targeta"
* and "--targetb". Requirement 10.1 only promises a guess when
* exactly one flag qualifies -- two equally-plausible candidates must
* produce no suggestion at all, the same as a typo that matches
* nothing. */
fresh_dir();
capture_t cap;
capture_begin(&cap);
int rc = flagspec_reject("testcmd", g_ambiguous_spec, 2, "--targetc",
"testcmd --targeta|--targetb");
char out_buf[512], err_buf[512];
capture_end(&cap, out_buf, sizeof(out_buf), err_buf, sizeof(err_buf));
TEST_ASSERT_TRUE(rc < 0);
TEST_ASSERT_EQUAL_STRING("", out_buf);
TEST_ASSERT_NOT_NULL(strstr(err_buf, "unrecognized flag '--targetc'"));
TEST_ASSERT_NULL(strstr(err_buf, "did you mean"));
cleanup_dir();
}
/* ---- flagspec_render_help() tests (Requirement 8.2) -------------------- */
static void test_render_help_formats_multi_flag_table(void)
{
flagspec_t spec[3];
memcpy(spec, g_testcmd_spec, sizeof(g_testcmd_spec));
spec[2] = (flagspec_t){ 0, "json", "output-format", no_argument, 0,
"table" };
char* help = flagspec_render_help(spec, 3);
TEST_ASSERT_NOT_NULL(help);
/* required flag: short + long form, marked required */
TEST_ASSERT_NOT_NULL(strstr(help, "-h"));
TEST_ASSERT_NOT_NULL(strstr(help, "--host"));
TEST_ASSERT_NOT_NULL(strstr(help, "required"));
/* optional flag: short + long form, marked optional, default shown */
TEST_ASSERT_NOT_NULL(strstr(help, "-c"));
TEST_ASSERT_NOT_NULL(strstr(help, "--command"));
TEST_ASSERT_NOT_NULL(strstr(help, "optional"));
TEST_ASSERT_NOT_NULL(strstr(help, "off"));
/* long-only flag: no short form, still shows long form + default */
TEST_ASSERT_NOT_NULL(strstr(help, "--json"));
TEST_ASSERT_NOT_NULL(strstr(help, "table"));
free(help);
}
static void test_render_help_rejects_null_and_empty_inputs(void)
{
TEST_ASSERT_NULL(flagspec_render_help(NULL, 2));
TEST_ASSERT_NULL(flagspec_render_help(g_testcmd_spec, 0));
}
void test_flagspec(void)
{
UnitySetTestFile(__FILE__);
@ -390,4 +500,9 @@ void test_flagspec(void)
RUN_TEST(test_reject_rejects_missing_value_short_flag);
RUN_TEST(test_reject_handles_null_bad_flag_and_null_usage_gracefully);
RUN_TEST(test_reject_no_argument_flag_given_a_value_reports_distinctly);
RUN_TEST(test_reject_suggests_single_close_match_flag_typo);
RUN_TEST(test_reject_gives_no_suggestion_for_a_distant_typo);
RUN_TEST(test_reject_gives_no_suggestion_when_typo_is_ambiguous);
RUN_TEST(test_render_help_formats_multi_flag_table);
RUN_TEST(test_render_help_rejects_null_and_empty_inputs);
}