@@ -367,6 +367,83 @@ static void restore_test_env(const char *name, char *saved) {
367367 }
368368}
369369
370+ /* An unreadable CBM_HOOK_DEADLINE_MS must fall back to the DEFAULT budget, not
371+ * to the shortest one the setting allows.
372+ *
373+ * atoi answers 0 for text it cannot read, and 0 is below HA_DEADLINE_MIN_MS, so
374+ * the clamp used to hand back 50 ms -- the worst possible answer for a setting
375+ * whose whole purpose is to give the hook more room. The comment above
376+ * ha_deadline_ms records a hunt for hook runs that never finished (0 of 24 real
377+ * sessions), which is exactly the symptom a silently-shortened deadline makes.
378+ *
379+ * POSIX only: the Windows path arms a fixed timer and reads no environment. */
380+ #ifndef _WIN32
381+ TEST(cli_hook_deadline_ignores_an_unreadable_value) {
382+ enum { HOOK_DEADLINE_DEFAULT = 2000, HOOK_DEADLINE_MIN = 50, HOOK_DEADLINE_MAX = 10000 };
383+ char *saved = save_test_env("CBM_HOOK_DEADLINE_MS");
384+
385+ /* Positive control: a good value is still used, so a failure below is about
386+ * the unreadable case and not about the reader being broken outright. */
387+ cbm_setenv("CBM_HOOK_DEADLINE_MS", "1234", 1);
388+ ASSERT_EQ(cbm_hook_augment_deadline_ms_for_testing(), 1234);
389+
390+ /* Unset falls back to the default. */
391+ cbm_unsetenv("CBM_HOOK_DEADLINE_MS");
392+ ASSERT_EQ(cbm_hook_augment_deadline_ms_for_testing(), HOOK_DEADLINE_DEFAULT);
393+
394+ /* The claim: text the reader cannot read gets the default, never the floor. */
395+ const char *unreadable[] = {"abc", "2000ms", " 2000", "2000 ", "", "1e3"};
396+ for (size_t i = 0; i < sizeof(unreadable) / sizeof(unreadable[0]); i++) {
397+ cbm_setenv("CBM_HOOK_DEADLINE_MS", unreadable[i], 1);
398+ int ms = cbm_hook_augment_deadline_ms_for_testing();
399+ if (ms != HOOK_DEADLINE_DEFAULT) {
400+ printf(" unreadable value \"%s\" gave %d ms\n", unreadable[i], ms);
401+ }
402+ ASSERT_EQ(ms, HOOK_DEADLINE_DEFAULT);
403+ }
404+
405+ /* Both clamps still hold for values that DO read. */
406+ cbm_setenv("CBM_HOOK_DEADLINE_MS", "1", 1);
407+ ASSERT_EQ(cbm_hook_augment_deadline_ms_for_testing(), HOOK_DEADLINE_MIN);
408+ cbm_setenv("CBM_HOOK_DEADLINE_MS", "999999", 1);
409+ ASSERT_EQ(cbm_hook_augment_deadline_ms_for_testing(), HOOK_DEADLINE_MAX);
410+
411+ restore_test_env("CBM_HOOK_DEADLINE_MS", saved);
412+ PASS();
413+ }
414+ #endif
415+
416+ /* CBM_INDEX_MAX_RESTARTS=0 means no restarts. It used to mean 100 of them.
417+ *
418+ * The old reader kept the default unless atoi answered greater than zero, so
419+ * the one value a person sets when they want the worker left alone did the
420+ * opposite. A typo did the same thing, with nothing on screen either way. */
421+ TEST(cli_index_restart_cap_honours_zero_and_refuses_junk) {
422+ enum { INDEX_RESTART_CAP_DEFAULT = 100 };
423+ char *saved = save_test_env("CBM_INDEX_MAX_RESTARTS");
424+
425+ /* Positive control: a good value is still used. */
426+ cbm_setenv("CBM_INDEX_MAX_RESTARTS", "7", 1);
427+ ASSERT_EQ(cbm_index_restart_cap_for_testing(), 7);
428+
429+ cbm_unsetenv("CBM_INDEX_MAX_RESTARTS");
430+ ASSERT_EQ(cbm_index_restart_cap_for_testing(), INDEX_RESTART_CAP_DEFAULT);
431+
432+ /* The claim: zero is a real answer meaning no restarts. */
433+ cbm_setenv("CBM_INDEX_MAX_RESTARTS", "0", 1);
434+ ASSERT_EQ(cbm_index_restart_cap_for_testing(), 0);
435+
436+ /* Text the reader cannot read keeps the default. */
437+ const char *unreadable[] = {"abc", "5x", " 5", "5 ", ""};
438+ for (size_t i = 0; i < sizeof(unreadable) / sizeof(unreadable[0]); i++) {
439+ cbm_setenv("CBM_INDEX_MAX_RESTARTS", unreadable[i], 1);
440+ ASSERT_EQ(cbm_index_restart_cap_for_testing(), INDEX_RESTART_CAP_DEFAULT);
441+ }
442+
443+ restore_test_env("CBM_INDEX_MAX_RESTARTS", saved);
444+ PASS();
445+ }
446+
370447/* Helper: mkdirp */
371448static int test_mkdirp(const char *path) {
372449 char tmp[1024];
@@ -13697,6 +13774,10 @@ TEST(cli_update_only_names_an_installer_that_exists_issue1632) {
1369713774
1369813775SUITE(cli) {
1369913776 RUN_TEST(cli_update_only_names_an_installer_that_exists_issue1632);
13777+ #ifndef _WIN32
13778+ RUN_TEST(cli_hook_deadline_ignores_an_unreadable_value);
13779+ #endif
13780+ RUN_TEST(cli_index_restart_cap_honours_zero_and_refuses_junk);
1370013781 RUN_TEST(cli_progress_visibility_policy);
1370113782 RUN_TEST(cli_raw_mcp_result_preserves_tool_error_status);
1370213783 RUN_TEST(cli_maintenance_cancellation_forces_failure_status);
0 commit comments