myxce/.kiro/specs/xce-test-rc-file/design.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

16 KiB

Design Document

Overview

Purpose: This feature adds a startup rc file to xce_test, src/controller/testsuite/test.c's REPL/scripting client, so a user's usual settings (set masters ..., set secret ...) and starting context are applied automatically instead of retyped every session.

Users: Engineers driving controller/worker/scheduler/HA soak and E2E testing through xce_test, both interactively and via piped scripts.

Impact: xce_test gains one new startup step (rc-file discovery and loading, before its read loop begins) and one new flag (--norc). No existing command, output format, or scripting primitive changes.

Goals

  • Auto-load and execute an rc file at a fixed, XDG-compliant path before xce_test's interactive/scripted read loop starts, for both session kinds.
  • Let --norc skip this entirely for a given invocation.
  • Make a failing rc-file command behave exactly like a failing regular command — no new failure-handling mechanism.

Non-Goals

  • A --rcfile <path> custom-location override (deferred; orthogonal addition if wanted later).
  • Any new scripting syntax/primitive beyond what execute_line/preprocess_and_run already supports.
  • A per-project/cwd rc file, or a legacy dotfile fallback (e.g. ~/.xce_testrc) — there is no prior rc-file convention to preserve.
  • xce_test creating, writing, or scaffolding the rc file or its directory.
  • Fixing xce_test's general -h/error-stream/flag-validation issues — those are owned by the separate xce-test-cli-ux spec.

Boundary Commitments

This Spec Owns

  • Resolving the rc-file path per the XDG Base Directory Specification's own defined precedence/fallback.
  • Reading the rc file (if present) and feeding each line into the existing command-execution path before the read loop starts.
  • The --norc flag and its argp/help wiring.
  • Non-fatal handling of an unreadable (but present) rc file.

Out of Boundary

  • The behavior of any individual xce_test command once invoked (unchanged — commands run exactly as they do today, whether typed, piped, or rc-sourced).
  • preprocess_and_run's/execute_line's abort-on-failure, continue-on-error, auto-variable, or exit-code-verdict logic (owned by xce-test-e2e-primitives; this spec only calls into it).
  • xce_test's process-level --help/-h completeness (owned by xce-test-cli-ux); this spec only guarantees --norc is documented via the existing --help/-? mechanism.

Allowed Dependencies

  • preprocess_and_run (test.c:955) as the per-line runner — called by function pointer, not reimplemented.
  • Standard C library only (getenv, fopen/getline/fclose, strdup/free) — no new third-party dependency.
  • argp (already a dependency of xce_test) for the --norc flag.

Revalidation Triggers

  • A change to preprocess_and_run's return-code contract (0 = continue, nonzero = abort) — rcfile_load's early-stop logic depends on it unchanged.
  • A change to how/when g_interactive/g_continue_on_error are computed in main() — the rc-file load call must stay after that computation and before the first read-loop iteration.
  • Introduction of a --rcfile <path> override or a legacy-dotfile fallback in a future spec — must compose with, not replace, this path-resolution contract.

Architecture

Existing Architecture Analysis

xce_test's testsuite/ directory already has an established pattern for a small, dependency-free capability: a standalone .c/.h module (e.g. waldump.c/waldump.h, configcompare.c/configcompare.h) linked into both the xce_test executable (meson.build:57) and, paired with its own test_<name>.c, into controller_unit_test (e.g. meson.build:166-167). test.c itself only wires a thin cmd_* wrapper or, in this feature's case, a call from main(). This feature follows that exact pattern rather than growing main()/test.c inline.

Architecture Pattern & Boundary Map

flowchart TD
    Start[main starts] --> ParseArgs[argp_parse fills norc flag]
    ParseArgs --> ComputeMode[Compute g_interactive and g_continue_on_error]
    ComputeMode --> NorcCheck{norc flag set}
    NorcCheck -- yes --> ReadLoop[Enter while not done read loop]
    NorcCheck -- no --> Resolve[rcfile_resolve_path]
    Resolve --> HasPath{Path resolved}
    HasPath -- no --> ReadLoop
    HasPath -- yes --> Load[rcfile_load path preprocess_and_run]
    Load --> Stopped{Runner returned nonzero}
    Stopped -- yes --> Exit[Return session_failed exit code]
    Stopped -- no --> ReadLoop

Architecture Integration:

  • Selected pattern: standalone module (rcfile.c/rcfile.h) + one call site in test.c's main().
  • Domain/feature boundaries: rcfile.c owns path resolution and line iteration only; it has no knowledge of commands, contexts, or client state — it receives a runner callback and calls it once per line.
  • Existing patterns preserved: configcompare.c-style module pairing in both build targets; preprocess_and_run's existing per-line contract untouched.
  • New components rationale: rcfile.c/rcfile.h (new capability, independently testable); one new argp option and one new call site in test.c (integration only).
  • Steering compliance: one domain per file (structure.md); test-first, testsuite/test_<unit>.c naming (testing.md).

Technology Stack

Layer Choice / Version Role in Feature Notes
CLI (C) argp (existing dependency) Declares --norc Same mechanism as the existing -v/--verbose option (test.c:674-677)
Runtime C standard library (getenv, stdio.h, POSIX getline) Path resolution + file reading No new dependency; getline avoids a fixed-size line buffer

File Structure Plan

Directory Structure

src/controller/testsuite/
├── test.c              # Modified: --norc option, call site in main()
├── rcfile.c             # New: path resolution + line-loading loop
├── rcfile.h              # New: public interface for rcfile.c
└── test_rcfile.c        # New: Unity unit tests for rcfile.c (fake runner)

Modified Files

  • src/controller/testsuite/test.c — add int norc to struct arguments; add a norc entry to options[]; add a symbolic OPT_NORC key constant and its case in parse_opt; in main(), after g_continue_on_error is computed and before the while (!done) loop, call rcfile_resolve_path()/rcfile_load() when arguments.norc is not set, returning early with the existing g_session_failed ? 1 : 0 convention if rcfile_load signals abort.
  • src/controller/meson.build — add 'testsuite/rcfile.c' to xce_test_src (meson.build:57); add 'testsuite/test_rcfile.c' and 'testsuite/rcfile.c' to controller_unit_test_src, following the test_configcompare.c/configcompare.c pairing already there (meson.build:166-167).

New Files

  • src/controller/testsuite/rcfile.h — declares rcfile_resolve_path(void) and rcfile_load(const char* path, int (*runner)(char*)).
  • src/controller/testsuite/rcfile.c — implements both functions; no dependency beyond libc.
  • src/controller/testsuite/test_rcfile.c — Unity tests using a fake int (*)(char*) runner (records calls, returns a scripted result) plus setenv/unsetenv to drive rcfile_resolve_path.

Requirements Traceability

Requirement Summary Components Interfaces Flows
1.1 Resolve rc path with XDG precedence rcfile.c rcfile_resolve_path Resolve step
1.2 Execute rc lines through the existing command path rcfile.c, test.c::main rcfile_load(path, preprocess_and_run) Load step
1.3 Missing rc file is silent, not an error rcfile.c rcfile_load (ENOENT branch) HasPath=no / open ENOENT
1.4 Load before the first read-loop line, both session kinds test.c::main call site placement NorcCheck → Resolve → Load → ReadLoop
1.5 Context/settings carry into the read loop test.c (existing active_ctx/settings globals) none new implied by shared global state
2.1 --norc skips loading entirely test.c::main, parse_opt arguments.norc NorcCheck=yes
2.2 --norc documented in --help test.c::options[] argp doc string n/a
2.3 Default (no --norc) still loads test.c::main NorcCheck=no branch Resolve → Load
3.1 Rc failure reported like any command failure preprocess_and_run (reused, unmodified) rcfile_load's runner callback Load step
3.2 Interactive: continue past a failing rc line preprocess_and_run (existing g_continue_on_error branch) runner return 0 on recorded failure Load loop continues
3.3 Scripted: abort before piped script runs preprocess_and_run (existing abort branch), rcfile_load runner return nonzero → early stop Stopped=yes → Exit
3.4 Same session-verdict/exit-code accounting preprocess_and_run (existing g_last_status/g_session_failed) none new n/a

Components and Interfaces

Component Domain/Layer Intent Req Coverage Key Dependencies (P0/P1) Contracts
rcfile.c/rcfile.h CLI startup Resolve rc path; load and run its lines via a caller-supplied runner 1.1, 1.2, 1.3 none (P0: libc only) Service
test.c::main integration CLI startup Wire --norc, call rcfile.c, honor its abort signal 1.4, 1.5, 2.1, 2.3 rcfile.c (P0), preprocess_and_run (P0) Service
test.c::options[]/parse_opt CLI arg parsing Declare and parse --norc 2.1, 2.2 argp (P0) Service

CLI Startup

rcfile module

Field Detail
Intent Resolve the rc-file path and, if present, execute its lines through a caller-supplied runner
Requirements 1.1, 1.2, 1.3

Responsibilities & Constraints

  • Owns path resolution ($XDG_CONFIG_HOME / $HOME/.config precedence) and line iteration only.
  • Has no knowledge of xce_test commands, contexts, or client state — the runner is an opaque callback.
  • Never writes to the rc file or creates its directory.

Dependencies

  • Outbound: caller-supplied runner function pointer (P0) — invoked once per line, trailing newline stripped.
  • External: libc getenv/fopen/getline (P0).

Contracts: Service [x] / API [ ] / Event [ ] / Batch [ ] / State [ ]

Service Interface
/* Resolves the rc-file path per the XDG Base Directory Specification:
 * "$XDG_CONFIG_HOME/xce_test/rc" when XDG_CONFIG_HOME is set and non-empty,
 * else "$HOME/.config/xce_test/rc". Returns a heap string the caller must
 * free(), or NULL if neither variable yields a usable base directory. */
char* rcfile_resolve_path(void);

/* Opens `path` and calls `runner(line)` once per line (trailing newline
 * stripped), stopping early if `runner` returns nonzero. Lines are read via
 * getline() with no length cap -- a line is never truncated or split
 * regardless of length.
 *
 * Returns:
 *   0                    - path is NULL, the file does not exist (ENOENT),
 *                           the file could not be opened for another reason
 *                           (a warning is printed to stderr; non-fatal), or
 *                           every line ran and every runner call returned 0.
 *   nonzero (== runner's) - the runner requested an abort; no further lines
 *                           are read. */
int rcfile_load(const char* path, int (*runner)(char* line));
  • Preconditions: runner is non-NULL when path is non-NULL.
  • Postconditions: the file handle is always closed before returning.
  • Invariants: lines are run in file order; iteration stops at the first nonzero runner result.

Implementation Notes

  • Integration: main() calls rcfile_resolve_path(), and — if non-NULL — rcfile_load(path, preprocess_and_run), then free()s the path. A nonzero rcfile_load result short-circuits straight to return g_session_failed ? 1 : 0;, skipping the while (!done) loop entirely (Requirement 3.3).
  • Line reading: rcfile_load reads each line with POSIX getline() (a dynamically-growing heap buffer, freed once at loop exit), not a fixed-size fgets() buffer. rcfile.c deliberately has no dependency on test.c's MAX_LINE_LEN, so a fixed buffer sized independently of it risks silently truncating (or splitting into two runner calls) a long line — e.g. a multi-peer set masters host1:port,host2:port,... line — producing a mangled command with no error reported. getline() has no such cap and keeps rcfile.c dependency-free (libc only), consistent with its stated architecture.
  • Validation: test_rcfile.c uses a fake runner to prove the stop-on-nonzero and ENOENT-is-silent contracts without depending on preprocess_and_run or any client/network state, plus a case with a line well beyond a typical small fixed-buffer size (e.g. >4KB) to prove no truncation/splitting occurs.
  • Risks: none identified — the module has no I/O beyond one file read and no shared mutable state.

Error Handling

Error Strategy

  • Rc file absent (ENOENT): silent, non-fatal — rcfile_load returns 0 (Requirement 1.3).
  • Rc file present but unreadable (permission denied, is a directory, etc.): non-fatal — rcfile_load prints one stderr warning naming the path and strerror(errno), then returns 0 so startup proceeds normally.
  • A line in the rc file fails: handled entirely by the reused preprocess_and_run — its existing error output, g_last_status/g_session_failed accounting, and mode-dependent abort/continue decision apply unchanged (Requirement 3).
  • Neither $XDG_CONFIG_HOME nor $HOME resolves a base directory: rcfile_resolve_path returns NULL; main() treats this identically to "no rc file found" — no error reported.

Testing Strategy

Unit Tests (test_rcfile.c, added to controller_unit_test)

  • Test isolation: controller_unit_test links 80+ test_*.c files into one process running sequentially, so any setenv/unsetenv on $HOME/$XDG_CONFIG_HOME here must not leak into tests that run after it. Before each test case that touches either variable, capture the prior state with getenv() (distinguishing "was unset" from "was empty" — e.g. via a char* that is NULL vs a duplicated non-NULL string); after the test case, restore that exact prior state (unsetenv() if it had been unset, setenv(..., 1) with the saved value otherwise). Apply this consistently across every test below that sets either variable.
  • rcfile_resolve_path returns $XDG_CONFIG_HOME/xce_test/rc when XDG_CONFIG_HOME is set and non-empty.
  • rcfile_resolve_path returns $HOME/.config/xce_test/rc when XDG_CONFIG_HOME is unset or empty.
  • rcfile_resolve_path returns NULL when neither XDG_CONFIG_HOME nor HOME is usable.
  • rcfile_load returns 0 and never calls the runner when the path does not exist.
  • rcfile_load calls the runner once per line, in order, with the trailing newline stripped.
  • rcfile_load stops after a runner call that returns nonzero, propagates that value, and does not call the runner for remaining lines (proves Requirement 3.3's "no further input" contract at the module level).
  • rcfile_load on a non-ENOENT open failure (e.g. path is a directory) prints a warning and returns 0 without invoking the runner.

Integration Tests (new testsuite/test_rcfile_integration.sh, shell-driven against the built xce_test binary — mirrors the existing clean_failover_preservation.sh pattern)

  • With HOME/XDG_CONFIG_HOME pointed at a temp dir containing an rc file with set masters ..., piping a script into xce_test shows the overridden setting took effect.
  • --norc with the same rc file present leaves the default masters value untouched.
  • A deliberately failing rc-file line (e.g. malformed set masters) in piped mode causes xce_test to exit nonzero before any of the piped script's own commands produce output.
  • The same failing rc-file line in an interactive (TTY-simulated) session still reaches the normal prompt afterward.
  • Compatibility regression check: with no rc file present (the state every existing caller runs in today), the xce-test-ha and xce-test-resources skills' existing soak/verification scripts — the known live consumers of xce_test — produce the same pass/fail result they did before this feature, proving Requirement 1.3's "no rc file → unchanged behavior" guarantee holds for real, pre-existing automation and not only for this feature's own new integration script.