myxce/.kiro/specs/xce-test-cli-ux/tasks.md
Rongsong Shen 4d1f247674
test(xce-test-cli-ux): build reusable pty-driven black-box CLI harness
clitest_spawn/send_line/send_partial/send_tab/send_signal/wait_for_text/
wait_for_exit/kill_and_reap launch the real compiled xce_test behind a
genuine pty (so isatty/g_interactive/libedit behave as a real terminal),
with stderr routed to a separate pipe for real stream-separation testing.
All waits are poll()-bounded with an explicit timeout so a future
regression can't hang the whole test suite. This is the shared
infrastructure tasks 2.3, 3.5, and all of Validation phase (4.1-4.4)
will build their black-box scenarios on top of. One smoke test proves
launch/send/capture/exit-code/stream-separation end to end.
1290/1290 controller suite passing (+1).

Task 1.7 of .kiro/specs/xce-test-cli-ux/tasks.md.
Foundation phase (1.1-1.7) now complete.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019jaMf9QEwpugH2W9KAuc7f
2026-09-09 11:06:31 +00:00

22 KiB

Implementation Plan

  • 1. Foundation: shared libraries, process-level fixes, and test harness

  • 1.1 Build the flag-descriptor table format and getopt_long-based parsing/rejection core

    • Define the flag-descriptor structure (short form, long form, concept name, required flag, default description) and a builder that produces a getopt_long-compatible option table from it
    • Implement the shared rejection path: an unrecognized or malformed flag prints the offending flag plus the command's usage summary to stderr and returns a nonzero result
    • Write the unit test first (red), then implement until it passes (green): a fake command's descriptor table correctly builds its option table, and both an unrecognized flag and a flag missing its required value are rejected with the right message
    • Observable completion: running the new unit test suite shows the descriptor-driven rejection producing the same messages for a made-up two-flag test command regardless of which flag is malformed, and confirms the rejection text lands on stderr, not stdout
    • Requirements: 2.1, 2.2, 2.3, 2.4, 3.1
    • Boundary: Flag Parsing Layer
  • 1.2 Build the flag typo-suggestion utility and the generated help-flag-block renderer

    • Add a small edit-distance comparison so an unrecognized flag that closely matches exactly one flag in the descriptor table is named as the likely intended one in the rejection message
    • Add a renderer that produces the flag-listing portion of help text (short form, long form, required/optional, default) directly from the descriptor table
    • Observable completion: the unit test suite shows a single close-match flag typo producing a "did you mean" suggestion, a non-close-match producing no suggestion, and the renderer producing a correctly formatted block for a multi-flag test table
    • Requirements: 6.1, 6.2, 6.3, 6.4, 8.2, 10.1, 10.2, 10.3
    • Boundary: Flag Parsing Layer
    • Depends: 1.1
  • 1.3 Correct the SIGINT installation and expose an interruption accessor

    • Replace the process's existing signal-based SIGINT installation with an explicit installation that clears the automatic-restart behavior, so a blocking read reliably returns an interrupted result instead of silently retrying
    • Expose a small accessor over the existing interrupt flag so the interruptible-call adapter (built next, in 1.4/1.5) can poll it without depending on the flag's storage details
    • Observable completion: a small standalone test that sends an interrupt signal to the process during a blocking read confirms the read returns immediately with an interrupted result rather than continuing to block
    • Requirements: 9.9
  • 1.4 Build the interruptible-call adapter's heap-block-ownership mechanism

    • Implement a wrapper that runs a given blocking call on a detached worker thread while the caller polls the interruption accessor from 1.3 on a short interval
    • Implement the atomic ownership handoff so that whichever side (the polling caller or the worker) is left holding the call's heap-allocated state is the one that frees it, with no path that frees it from both sides
    • Observable completion: the unit test suite shows a normal (uninterrupted) call returning its real result and freeing cleanly, and a forced-timeout call returning immediately with no double-free and no leak (verified under the project's leak/ASAN tooling)
    • Requirements: 1.1, 1.2, 1.3, 1.4
    • Boundary: Interruptible Call Adapter
    • Depends: 1.3
  • 1.5 Add the per-connection lock registry to the interruptible-call adapter

    • Add a small registry, keyed by connection identity, that serializes any two calls sharing a connection so an abandoned call's worker thread is never running concurrently with a newly issued call on the same connection
    • Make lock acquisition itself interruptible, so a new call waiting on a connection still held by a prior abandoned call can itself be cancelled
    • Observable completion: the unit test suite shows two overlapping calls against the same fake connection identity (one abandoned, one issued immediately after) never executing concurrently, while two calls against different connection identities run independently
    • Requirements: 1.1, 1.5
    • Boundary: Interruptible Call Adapter
    • Depends: 1.4
  • 1.6 Add process-level help and version discovery

    • Ensure invoking the process with a help or version flag prints the appropriate output and exits without starting the interactive/scripted session
    • Extend the process-level help output to describe the in-session help command as the way to list and learn available commands
    • Observable completion: invoking the process with the help flag prints usage text mentioning the in-session help command, and invoking with the version flag prints version information, both without entering the REPL
    • Requirements: 4.1, 4.2, 4.3
    • Boundary: Process Entry
  • 1.7 Build the black-box CLI test harness

    • Add a subprocess/pty-driven test harness capable of launching the built client, sending input lines (including partial input followed by a tab keypress and raw signals), and capturing its stdout/stderr/exit code separately
    • Observable completion: a smoke test using the new harness launches the client, sends a trivial command, and confirms the expected output arrives on the correct stream with the expected exit code
    • Boundary: Testing Infrastructure
  • 2. Core: independent new capabilities and standalone fixes

  • 2.1 (P) Build the destructive-command confirmation gate and its session-scoped toggle

    • Implement a confirmation check that prompts the operator on stderr before proceeding, returns a distinct "not run" outcome on decline, and treats an interrupt during the prompt the same as a decline
    • Add a bypass flag usable per invocation and a session-scoped toggle (mirroring the existing continue-on-error toggle's style) that both default to requiring confirmation and both reset at process start
    • Ensure the confirmation check is skipped automatically when input is not from an interactive session
    • Observable completion: the unit test suite shows a simulated non-interactive session skipping the prompt, a simulated decline and a simulated interrupt both producing the "not run" outcome, and the toggle switching prompting on and off
    • Requirements: 9.1, 9.2, 9.3, 9.4, 9.6, 9.7, 9.8, 9.9
    • Boundary: Confirmation Gate
    • Depends: 1.1, 1.3
  • 2.2 (P) Build the JSON list-output helper library

    • Implement shared helpers for building a JSON array of row objects and printing it, usable by any listing command's existing per-row data
    • Observable completion: the unit test suite shows a multi-row, multi-field test payload serializing to valid JSON with no field truncated regardless of value length
    • Requirements: 5.1, 5.3
    • Boundary: JSON List Formatter
  • 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
    • Boundary: Tab Completion
    • Depends: 1.7
  • 2.4 (P) Build the in-session source command with missing-file and cycle detection

    • Implement a command that reads and executes another file's lines in order, reusing the existing rc-file loading primitive, reporting an explicit error (rather than silently doing nothing) when the file does not exist or cannot be read
    • Add cycle detection using a canonicalized in-flight path stack so a file that sources itself, directly or transitively, is rejected with an error instead of hanging
    • Ensure a failing line inside the sourced file follows the session's current abort-on-failure/continue-on-error setting, and that nested sourcing (one file sourcing another, non-cyclically) composes correctly
    • Observable completion: the unit/integration test suite shows a two-file mutual-cycle case rejected with an error, a missing-file case rejected with an error, and a nested non-cyclic case executing every line from both files
    • Requirements: 13.1, 13.2, 13.3, 13.4, 13.5, 13.6
    • Boundary: Source Command
  • 2.5 (P) Fix bug-1711's display and text defects

    • Correct the misspelled unrecognized-worker-status text and the misspelled default launch-type label (the malformed help-not-found grammar is fixed in 3.5, alongside that same code path's broader rework)
    • Left-justify the resource-listing command's text columns to match the worker-listing command's convention, and label/prefix its flags column as hexadecimal
    • Format the job-listing command's submit/start/finish time columns using the existing human-readable time formatting instead of raw epoch seconds, split its combined task-count column into two correctly labeled columns, and add a raw-state fallback for its ambiguous run-plus-exit state
    • Replace the configuration-listing command's silently empty group/config sections with either their real contents or an explicit not-implemented marker
    • Observable completion: running each affected command against a small fixture shows the corrected text/formatting for every defect listed above
    • Requirements: 11.1, 11.3, 11.4, 11.5, 11.6, 11.7, 11.8
    • Boundary: Bug-1711 Fixes
  • 2.6 (P) Fix bug-1557's join maxmem default

    • Correct the simulated worker-join command's default memory value to the codebase's current bytes-canonical convention
    • Observable completion: joining a simulated worker without specifying memory reports the corrected byte-scale default instead of the stale value
    • Requirements: 12.1
    • Boundary: Bug-1557 Fix
  • 3. Integration: migrate existing commands onto the new foundations

  • 3.1 Migrate every flag-accepting command's parsing to the shared validation library

    • Convert every command currently parsing flags via the older mechanisms (including the one command using a fully hand-rolled long-flag parser) to the shared descriptor-table-driven parsing built in 1.1/1.2, so every one uniformly rejects an unrecognized or malformed flag and gains a long-form equivalent for each concept
    • While migrating the queue-control command, initialize its action selection to an explicit unset sentinel and reject the invocation if none of its mutually-exclusive action flags was given, matching its sibling host-control command's existing pattern
    • Observable completion: every migrated command rejects a deliberately invalid flag with a usage summary and, where applicable, a suggestion, and the queue-control command rejects an invocation given none of its action flags instead of transmitting an indeterminate value
    • Requirements: 2.1, 2.2, 2.3, 2.4, 6.1, 6.2, 6.3, 6.4, 10.1, 10.2, 10.3
    • Boundary: Flag Parsing Layer
    • Depends: 1.1, 1.2
  • 3.2 Wrap every blocking request with the interruptible-call adapter

    • Route every command's blocking request through the interruptible-call adapter built in 1.4/1.5, so an interrupt during any of them abandons the wait immediately, reports the command as failed, and leaves the active connection and context intact
    • Ensure the session never exits as a side effect of a single interrupt keypress, and that an interrupt at an empty prompt simply discards the current input line
    • Observable completion: interrupting a command that is blocked waiting on a request returns control to the prompt immediately (well before the request's own timeout) and the same connection accepts a subsequent command normally
    • Requirements: 1.1, 1.2, 1.3, 1.4, 1.5
    • Boundary: Interruptible Call Adapter
    • Depends: 1.4, 1.5, 3.1
  • 3.3 Wire the five destructive commands to the confirmation gate

    • Gate the job-kill, shutdown, queue-close/inactivate, host-close, and configuration-upgrade actions behind the confirmation gate built in 2.1, using each command's now-correctly-parsed flags to decide whether a given invocation counts as destructive
    • Add the bypass flag to each of these commands via the shared flag-descriptor table
    • Observable completion: each of the five destructive actions prompts interactively by default, proceeds without prompting when the bypass flag or the session toggle is used, and proceeds without prompting when input is not interactive; separately, confirm that a reversal invocation (queue-active/open, host-open) and the fake-worker job-lifecycle commands never prompt at all, since they are excluded from the destructive set
    • Requirements: 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7, 9.8, 9.9
    • Boundary: Confirmation Gate
    • Depends: 2.1, 3.1
  • 3.4 Add JSON output mode to the three listing commands

    • Bring the worker-listing, job-listing, and resource-listing commands onto the shared flag-descriptor library from 1.1 for the first time (none currently accepts any flag), adding a consistently named output-format flag that uses the JSON list-output library from 2.2 to emit every field present in each command's human-readable table, unabbreviated
    • Leave each command's existing human-readable output unchanged when the new flag is not given
    • Observable completion: running each of the three listing commands with the new flag against a fixture with a long field value produces valid JSON containing every table field with no truncation, while the default (no flag) invocation still prints the existing table
    • Requirements: 5.1, 5.2
    • Boundary: JSON List Formatter
    • Depends: 1.1, 2.2, 2.5
  • 3.5 Generate and consolidate every command's help content

    • Regenerate each command's flag-listing help text from the shared descriptor table (built in 1.2), and revise each command's remaining help text to include a purpose description, at least one example invocation, which contexts it is valid in when shared across contexts, and any related command or scripting side effect
    • Where a shared command's behavior differs by active context, describe the current context's specific behavior rather than a generic description
    • Ensure a help lookup for a name not found in the active context reports it as not found there without checking other contexts, and correct the existing malformed not-found message text
    • Document the join command's corrected default from 2.6, and the JSON output flag added in 3.4, in their respective commands' help text
    • Observable completion: looking up help for a representative command in each context shows a description, full flag documentation, an example, and (where applicable) related-command notes; looking up help for one of the three listing commands explicitly shows how to request its JSON output mode; and looking up an unrecognized name shows the corrected not-found message with no other-context hint
    • Requirements: 5.4, 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.7, 11.2, 12.2
    • Boundary: Help Renderer
    • Depends: 1.2, 3.1, 2.6, 3.4
  • 3.6 Audit and correct stdout/stderr stream routing across existing error paths

    • Review every command's remaining error paths not already covered by the shared flag-rejection path built in 1.1 — request failures and internal errors — and ensure diagnostic/error text goes to stderr while a command's successful result data goes to stdout only
    • Confirm the same routing holds identically whether stdout and stderr are both attached to a terminal or redirected to separate destinations (a script or pipe)
    • Observable completion: redirecting only stdout to a file while triggering a request failure or an internal error on a representative command shows the diagnostic text still appearing on the terminal while the redirected file contains no diagnostic text; separately, running a successful command with only stdout redirected shows its result data captured in the redirected file with nothing sent to stderr
    • Requirements: 3.1, 3.2, 3.3, 3.4
    • Boundary: Command Dispatcher
    • Depends: 3.1, 3.2, 3.3, 3.4, 3.5
  • 4. Validation: end-to-end verification

  • 4.1 Verify Ctrl-C cancellation end-to-end

    • Confirm interrupting a command blocked on a real request abandons the wait well before its own timeout, reports the command as failed, and leaves the session's context and connection usable
    • Confirm that issuing a second command on the same connection immediately after an interrupt does not race the abandoned request, and that a command on a different connection is unaffected
    • Observable completion: a live end-to-end run demonstrates both the immediate-cancellation behavior and the no-overlap behavior on the same connection
    • Requirements: 1.1, 1.2, 1.3, 1.4, 1.5
    • Depends: 3.2, 1.7
  • 4.2 Verify the confirmation gate end-to-end

    • Confirm each of the five destructive commands prompts by default, respects its bypass flag and the session toggle, and auto-skips under non-interactive input
    • Confirm that a real interrupt signal delivered while the confirmation prompt is displayed is observed as an interruption and reported as not run, not as failed
    • Observable completion: a live end-to-end run against each destructive command demonstrates the prompt, the bypass, the toggle, the non-interactive auto-skip, and the real-signal-during-prompt case
    • Requirements: 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7, 9.8, 9.9
    • Depends: 3.3, 1.7
  • 4.3 Verify source's interaction with the confirmation gate

    • Confirm a destructive command inside a sourced file, run within an otherwise-interactive session, still prompts for confirmation unless the session toggle is set, proving the gate's interactive check is based on the top-level session rather than on whether the current line came from a file
    • Observable completion: a live end-to-end run sources a file containing a destructive command from an interactive session and observes the prompt appear, then observes it skipped once the toggle is set
    • Requirements: 13.7
    • Depends: 2.4, 3.3, 1.7
  • 4.4 Verify process-level help, tab completion, and consolidated help content end-to-end

    • Confirm the help/version flags and in-session help behavior from 1.6 and 3.5, and the tab-completion behavior from 2.3, hold together in the full built client, not just in isolation
    • Observable completion: a black-box run confirms the help flag's output describes the in-session help command, a representative command's help output includes its generated flag block and hand-authored example, and tab completion in two different contexts offers the expected candidates
    • Requirements: 4.1, 4.2, 4.3, 7.1, 7.2, 7.3, 7.4, 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.7, 11.2
    • Depends: 1.6, 2.3, 3.5, 1.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)
  • 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)
  • argp has no built-in -h alias (its default short help flag is -?) — Requirement 4.1 literally names -h, so it needed an explicit OPTION_HIDDEN option added, not just verification of existing defaults. Task 1.6's new test_xcetest_process_entry.c already covers "-h/--help/--version exit without entering the REPL" end-to-end against the real binary; task 1.7's test_test_cli.c should either skip re-adding this exact scenario or consolidate the two rather than duplicating coverage. (task 1.6)
  • The per-connection lock is a POSIX semaphore (sem_t), not a pthread_mutex_t — a mutex was tried first but rejected on review (POSIX classifies a non-owning-thread pthread_mutex_unlock() as UB for every mutex type, not just error-checking ones, and this design needs the worker thread to release a lock the caller thread acquired). sem_t has no ownership concept and is fully POSIX-defined for this pattern. See bug-1791. (task 1.5)
  • interruptcall.c's forced-abandonment test proves -EINTR via return-value disambiguation against a slow fn, not a direct wall-clock elapsed-time assertion — sound today (poll interval is 50ms, matching design.md) but a future regression widening the poll interval wouldn't be caught unless the test also asserts an elapsed-time bound. Non-blocking; noted for task 4.1's end-to-end Ctrl-C validation to consider. (task 1.4)
  • After task 1.3's SIGINT/EINTR fix alone (before task 3.2 wires the REPL loop to check xcetest_interrupted()), a real Ctrl-C at xce_test's top-level interactive prompt now causes the process to EXIT (readline's EINTR-driven NULL return is currently treated the same as EOF) rather than gracefully redisplaying the prompt. This is expected/deferred, not a regression — no current test exercises that path — but task 3.2's implementer must fix it as part of wiring the REPL loop, not just the RPC call sites. (task 1.3)