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

57 lines
6.9 KiB
Markdown

## 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
- [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) — defines `$XDG_CONFIG_HOME` and its `$HOME/.config` fallback.