myxce/.kiro/specs/xce-test-rc-file/research.md
Rongsong Shen 9de4db95d3
feat(xce-test-rc-file): task 1.1 rc-file path resolution with XDG precedence
Adds rcfile_resolve_path() (rcfile.c/rcfile.h): resolves $XDG_CONFIG_HOME/xce_test/rc
when set and non-empty, else $HOME/.config/xce_test/rc, else NULL. Standalone
libc-only module mirroring the configcompare.c pattern; wired into xce_test_src
and controller_unit_test_src, registered in controller_test.c's Unity runner.

6 unit tests in test_rcfile.c cover all XDG/HOME combinations plus a dedicated
env-var save/restore isolation test, addressing the cross-test leak risk found
during design review (controller_unit_test runs 80+ test files in one process).

Adds the xce-test-rc-file spec (requirements/design/tasks/research), including
design fixes from validation: getline()-based line reading (task 1.2, avoids
truncating long rc-file lines) and the env-isolation requirement above.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgYRU3zDTqNGV5Jk8aiKLg
2026-08-26 13:48:27 +00:00

6.9 KiB

Summary

  • Feature: xce-test-rc-file
  • Discovery Scope: Extension (existing xce_test REPL client)
  • Key Findings:
    • xce_test's own testsuite/ directory already has an established pattern for a small, dependency-free capability implemented as its own .c/.h module and linked into both xce_test and controller_unit_test (e.g. configcompare.c at meson.build:57 and meson.build:166-167). The new rc-file logic should follow this exact pattern rather than growing test.c (already ~4500 lines) with inline logic.
    • preprocess_and_run(char* line) (test.c:955) already implements the exact per-line contract this feature needs: blank/comment handling, !-shell-escape, auto-var substitution, execute_line dispatch, and a return-code convention — 0 = keep going, nonzero = abort-on-failure — that already differs by session mode (g_continue_on_error, set from isatty(STDIN_FILENO) at test.c:4476-4477). Feeding rc-file lines through this exact function, unmodified, satisfies Requirement 3 with no new failure-handling code.
    • main() computes g_interactive/g_continue_on_error at test.c:4476-4477, immediately before the while (!done) read loop at test.c:4479. This is the correct insertion point for rc-file loading: after mode detection (so the reused abort/continue behavior is correct) and before the first real input line is read (satisfying Requirement 1.4).

Research Log

XDG Base Directory Specification — default path resolution

  • Context: Requirements call for $XDG_CONFIG_HOME/xce_test/rc, falling back to ~/.config/xce_test/rc when $XDG_CONFIG_HOME is unset or empty.
  • Sources Consulted: XDG Base Directory Specification (freedesktop.org) — defines $XDG_CONFIG_HOME with a required fallback of $HOME/.config when unset.
  • Findings: The spec explicitly names $HOME/.config as the fallback, so the fallback in Requirement 1.1 is not a project-specific guess — it is the spec's own defined default. The spec is silent on what to do when even $HOME is unavailable.
  • Implications: Path resolution is a pure function of two environment variables. When neither is usable, there is no defined path — treat this the same as "rc file not found" (silent skip), not an error, since no plausible location exists to report.

Architecture Pattern Evaluation

Option Description Strengths Risks / Limitations Notes
New rcfile.c/rcfile.h module Path resolution + line-loading loop as a standalone, dependency-free pair, wired into test.c's main() Matches existing configcompare.c/waldump.c precedent; independently unit-testable via controller_unit_test; keeps test.c unchanged except for the call site and one new argp option None significant — smallest addition that satisfies all requirements Selected
Inline logic in test.c's main() Add the loop and path resolution directly in main() Fewer files Not unit-testable in isolation (would require driving the full main()/argp/readline stack); breaks from this directory's established module pattern Rejected

Design Decisions

Decision: Reuse preprocess_and_run as the rc-file line runner, unmodified

  • Context: Requirement 3 needs a failing rc-file command to be reported and handled exactly like any other failing command, without introducing a parallel failure-handling mechanism.
  • Alternatives Considered:
    1. Write rc-file-specific error handling (its own abort/continue logic, its own error formatting).
    2. Pass preprocess_and_run itself as a callback into a generic line-loading loop.
  • Selected Approach: (2). rcfile_load(path, runner) takes a int (*runner)(char*) function pointer; main() passes preprocess_and_run directly.
  • Rationale: preprocess_and_run's existing return contract (0 = continue, nonzero = abort) already encodes the mode-dependent abort-vs-continue decision (Requirement 3.2/3.3) and already updates g_last_status/g_session_failed (Requirement 3.4). Reusing it verbatim means Requirement 3 requires zero new logic — only a thin loop that stops when the runner says to.
  • Trade-offs: rcfile.c cannot be linked/tested with a real preprocess_and_run in isolation (it lives in test.c, coupled to the whole command-table/client-context state) — unit tests use a fake runner instead, which is sufficient since rcfile_load's own contract (call runner per line, stop on nonzero, handle ENOENT/other errors) is what needs proving, not preprocess_and_run itself (already covered by xce-test-e2e-primitives's tests).
  • Follow-up: None.

Decision: Distinguish ENOENT from other fopen failures

  • Context: Requirement 1.3 only specifies silence when the rc file does not exist. It does not say what happens on a real failure (e.g. permission denied, or the path is a directory).
  • Alternatives Considered:
    1. Treat every fopen failure identically (silent skip).
    2. Distinguish ENOENT (silent) from other errors (non-fatal stderr warning, then proceed).
  • Selected Approach: (2).
  • Rationale: A misconfigured rc file (wrong permissions, a directory at the expected path) is a real misconfiguration worth surfacing — silently ignoring it could leave a user wondering why their settings never took effect. It must still be non-fatal (startup must proceed) since this feature must not become a new way for xce_test to fail to start.
  • Trade-offs: One extra branch in rcfile_load; no downside identified.
  • Follow-up: None.

Decision: --norc as a long-only argp option

  • Context: Requirement 2 needs a flag that skips rc-file loading, documented in --help output.
  • Selected Approach: Add { "norc", OPT_NORC, 0, 0, "..." } to test.c's existing options[] (test.c:674-677), using a symbolic key above the printable-ASCII range (the standard argp idiom for a long-only flag with no short letter), and a new int norc field on struct arguments.
  • Rationale: Matches this project's existing single-flag argp setup (-v/--verbose) with minimal surface; argp auto-generates the --help/-? entry from the option's doc string, satisfying Requirement 2.2 with no additional code.
  • Trade-offs: None identified for this scope.

Risks & Mitigations

  • A user's rc file blocks on stdin (e.g. contains a command that itself prompts) — mitigated by the fact that every existing xce_test command is already non-interactive/non-prompting; this feature does not add any new blocking primitive.
  • A missing/unset $HOME in a stripped-down container changes rc-file behavior in a way an operator doesn't expect — mitigated by treating "no resolvable path" identically to "file not found" (silent, non-fatal), consistent with Requirement 1.3's spirit.

References