# CLI and Configuration Contract Test Plan This plan adds deliberate contract and risk-based test coverage for `internal/cli` and `internal/core/config`. It does not pursue a statement coverage target. Coverage reports are diagnostic input: they may reveal an important untested branch, but a percentage alone does not justify a test. The audience is a relatively small LLM coding agent. Implement one stage per prompt, in order. Each stage must leave the repository compiling and passing all tests. ## Scope and fixed decisions Tests added by this plan protect: - external contracts documented in `docs/cli.md`, `docs/config.md`, `docs/operations.md`, and maintained examples; - internal contracts around configuration loading, precedence, validation, resolution, cloning, and redaction; - high-risk CLI composition paths involving run selection, references, LLM profiles, output, cache, resume, and debug state; and - production composition and the shortest maintained end-to-end workflows. The following decisions are fixed: - Do not set or enforce a repository-wide coverage percentage. - Do not restore deleted test files wholesale. The former suite predates ADR-0006 and includes obsolete workspace and diagnostics behavior. Historical tests immediately before commit `26142f0` may be consulted for fixture and harness ideas only; every restored assertion must be checked against current code and canonical documentation. - Prefer behavior tests at the narrowest useful boundary. Pure configuration rules belong in `internal/core/config`; CLI tests should prove parsing, composition, process-facing behavior, and cross-component wiring rather than repeat every configuration permutation. - Test stable observable facts: exit code, output stream, selected collaborator, logical artifact, path, or resolved field. Avoid exact whole-error strings, entire JSON snapshots, timestamps, random identifiers, and internal call sequences unless those are the contract under test. - Tests must be deterministic, offline, and secret-free. Inject catalogs, registries, clocks, run-ID generators, cache collaborators, and fake LLM clients. Use `t.TempDir`, `t.Setenv`, and `t.Chdir` where appropriate. Never use a real provider, credential, user cache directory, or shared output root. - Do not weaken a test to match an apparent defect. If current behavior conflicts with a canonical contract, stop the stage and report the conflict. Production fixes are outside this test-coverage plan unless separately authorized. - Preserve the focused ADR-0006 state tests already present. Consolidate helpers only when it materially reduces duplication and does not obscure the contract being tested. - Checkpoint wire compatibility is already owned by `internal/framework/checkpoint` tests. Do not add a second legacy checkpoint fixture in CLI tests unless a distinct CLI compatibility boundary is found. ## Execution rules for every stage Before editing: 1. Read `docs/development.md` and follow its task-specific reading guide. 2. Read both files under `docs/policy/`. 3. Read this plan and all current source, tests, and canonical documents named by the stage. 4. Run `git status --short`; preserve all existing changes. 5. Run the stage's package tests once to establish a baseline. While editing: - Add tests only for the cases listed in the stage. Do not expand into adjacent subsystem redesign. - Use table-driven tests for validation and syntax matrices when the setup and assertion are genuinely shared. - Give failures enough context to identify the contract case. - Assert both the positive outcome and important negative side effects, such as a root not being resolved or a file not being created. - Do not run environment- or working-directory-mutating tests in parallel. At the end of each stage: 1. Run the focused tests named by that stage. 2. Run `go test ./...`. 3. Run `go vet ./...`. 4. Run `go build ./cmd/notarius`. 5. Run `git diff --check`. 6. Review the diff for assertions tied to obsolete implementation details. Do not mark a stage complete until all checks pass. ## Stage 1: File schema, defaults, and decoding contracts **Status:** Not started ### Objective Protect the version 3 file format and the translation from YAML into the runtime configuration model. ### Read first - `docs/config.md`, through **Module Bindings** - `internal/core/config/config.go` - `internal/core/config/file_config.go` - `internal/core/config/v3_test.go` ### Implement Add focused tests in `internal/core/config/file_config_contract_test.go`. Reuse small helper constructors within that file; do not reproduce complete example files for unit cases. Cover these contracts: 1. `Default` returns the documented concurrency, output, cache-family, debug, and empty-pipeline defaults. Mutating maps in one returned configuration must not affect a later `Default` result. 2. A minimal `version: 3` file parses and applies over defaults. 3. Missing, version 2, and unsupported versions fail with actionable version context before ordinary field decoding. 4. Unknown top-level, pipeline, lane, and module-binding fields are rejected. Include removed `workspace`, `diagnostics`, and `llm_profiles` fields as representative compatibility failures, without testing every old field. 5. Shorthand and object module bindings preserve module, profile, retries, options, references, and the distinction between omitted validators and an explicit empty validator chain. 6. Pipeline, target-local, and lane compatibility reference maps apply with the documented precedence, including `extract.references` overriding the lane-level alias. 7. Chunk, extract, merge, and normalize stage-local validator bindings retain configured order and fields. 8. Scriptorium source, concurrency, output, chunk-plan cache, checkpoint cache, and debug sections apply without coupling their roots. 9. Keys that collide after trimming—pipeline IDs, lane IDs, and representative reference slots—are rejected. Do not enumerate the same normalization rule at every possible reference location. 10. `LoadFileConfig` reports both missing-file and malformed-YAML context. Where `v3_test.go` already proves a case completely, either leave it there or move it without duplicating it. ### Validation ```sh go test ./internal/core/config -run 'Test(FileConfig|Default|Version3)' ``` ### Exit criteria The maintained version 3 schema, defaults, binding forms, state sections, and normalization rules can be refactored without silently changing their contract. ## Stage 2: Precedence and validation matrices **Status:** Not started ### Objective Protect configuration precedence and high-risk invalid combinations after all sources have been applied. ### Read first - `docs/config.md`, especially **Discovery**, **Environment Overrides**, **Concurrency**, **Module Bindings**, and **State Surfaces** - `internal/core/config/env.go` - `internal/core/config/validation.go` - current tests from Stage 1 ### Implement Add `internal/core/config/env_contract_test.go` and `internal/core/config/validation_contract_test.go`. Precedence tests must prove: 1. File values override built-in defaults. 2. Every current `NOTARIUS_*` operational variable overrides the corresponding file value: total LLM concurrency, extract workers, output directory, chunk-plan mode and directory, checkpoint directory, and debug directory. 3. Extract workers default to the final effective total when not explicitly configured, but an explicit file or environment worker value is retained. 4. Empty cache-directory fields in a file remain valid and select deferred per-user defaults; empty directory environment overrides are errors. 5. Invalid integers and chunk-cache modes report the responsible environment variable and do not panic. 6. Removed provider environment variables are ignored. Do not place credential values in assertions or failure output. Validation tables must cover one representative case for each rule family: - non-positive total concurrency; extract worker below one, above total, and at both valid boundaries; unknown and blank worker keys; - mutually exclusive Scriptorium profile sources; - blank required output/debug roots, NUL in every physical root family, and an invalid chunk-plan mode; - empty and trim-duplicated pipeline/lane/reference identifiers; - negative retries and whitespace-only explicit LLM profiles; - references on unsupported input/output bindings; - invalid validator bindings: empty module, retries, references, nested validators, and validator chains on unsupported stages; and - deprecated non-empty lane-level validators rejected while stage-local omitted, empty, and non-empty overrides remain valid structurally. Assert stable contextual fragments rather than complete error text. ### Validation ```sh go test ./internal/core/config -run 'Test(Env|Precedence|Validate)' ``` ### Exit criteria Every documented precedence edge and every materially different validation rule family has a direct, readable regression test. ## Stage 3: Effective configuration, cloning, and redaction **Status:** Not started ### Objective Protect the internal contract that validated configuration resolves into an isolated, auditable pipeline without leaking sensitive options. ### Read first - `docs/policy/architecture.md`, especially **Pipeline Composition And Ownership**, **Configuration And Provenance**, and **State, Output, And Safety** - `internal/core/config/effective_config.go` - `internal/core/config/redaction.go` - `internal/core/config/redaction_test.go` - `internal/framework/pipeline/profile.go` ### Implement Add `internal/core/config/effective_config_contract_test.go` and extend `redaction_test.go` only for missing cases. Use a minimal fake `pipeline.ModuleCatalog` with enough variants and capabilities to make each resolution outcome explicit. Cover: 1. Empty and unknown pipeline IDs fail; a map key with harmless surrounding whitespace resolves to its normalized ID. 2. `Only` selects exactly the requested lanes, rejects unknown lanes, and does not mutate the source configuration. 3. Default chunk, merge, normalize, and output modules are materialized through the catalog. 4. Unknown modules, missing capabilities, missing artifact variants, invalid module options, and invalid validator options retain pipeline/lane/stage context. 5. An LLM-profile override applies to LLM-capable pipeline module bindings before digest calculation, does not override validator-specific profiles, and changes the digest when effective behavior changes. 6. Omitted, explicitly empty, and configured validator chains resolve distinctly and preserve configured order. 7. Returned `EffectiveConfig`, resolved bindings, nested options, reference slices/content, and validator chains do not alias the input configuration or resolution inputs. 8. Both redacted summary payloads cover every binding and recursively redact sensitive keys in untyped and typed/aliased option containers while preserving safe neighbors and excluding materialized reference content. Do not duplicate detailed reference-selector behavior owned by Stage 6. ### Validation ```sh go test ./internal/core/config -run 'Test(Resolve|Effective|Redacted)' ``` ### Exit criteria Resolution defaults, selection, compatibility failures, profile overrides, digests, deep-copy isolation, and redaction are directly protected. ## Stage 4: CLI command and configuration-loading contracts **Status:** Not started ### Objective Protect the public command surface, exit-code classification, configuration discovery, validation command, and pipeline listing. ### Read first - `docs/cli.md` - `docs/config.md#discovery` - command dispatch, `runConfigValidate`, `runPipelinesList`, and configuration loading in `internal/cli/run.go` - `internal/cli/state_surfaces_test.go` ### Implement Add `internal/cli/command_contract_test.go`. Use injected options and temporary files; no case may require production credentials or provider access. Cover: 1. No arguments and each documented help spelling write usage to stdout and return `0`. 2. Unknown commands/subcommands and malformed command syntax write to stderr and return `2`. 3. Configuration discovery precedence is explicit `--config`, then non-empty `NOTARIUS_CONFIG`, then the compiled default path. Test the first two with temporary files. For the compiled path, inspect its host state first: assert selection when it is a regular file, or the documented not-found error when it is absent. Never create, replace, or remove the system path. 4. Missing and malformed configuration files return `1` with actionable path or parse context. 5. `config validate` succeeds for a valid file, resolves a requested pipeline against an injected catalog, rejects an unknown pipeline/lane, requires `--pipeline` with `--only`, and returns `2` for malformed `--only` syntax. 6. `pipelines list` sorts normalized IDs in text and JSON forms; JSON is decoded and compared structurally rather than as raw formatting. 7. Removed structural/state flags such as `--diagnostics-dir` are rejected with exit `2` instead of being silently ignored. 8. Representative malformed run flags and valid-runtime failures establish the documented `2` versus `1` boundary. ### Validation ```sh go test ./internal/cli -run 'Test(Command|Help|ConfigDiscovery|ConfigValidate|PipelinesList|ExitCode)' ``` ### Exit criteria The documented command grammar, streams, discovery order, listing behavior, and exit-code classes are protected without invoking a real pipeline. ## Stage 5: Run controls, profile selection, and process-facing results **Status:** Not started ### Objective Protect the public run controls and the CLI-to-pipeline composition boundary. ### Read first - `docs/cli.md#run` - `internal/cli/run.go` - `internal/cli/scriptorium_profiles.go` - `internal/cli/state_hardening_test.go` and its fake harness ### Implement Add `internal/cli/run_contract_test.go`. Reuse or minimally generalize the existing deterministic state-test harness. Keep helper implementations at the bottom of a test file or in one clearly named `test_helpers_test.go` file. Cover: 1. Missing pipeline ID, missing `--input`, unknown flags, blank values, and multiple positional IDs return `2` without allocating output or debug state. 2. Unknown pipeline/lane and unreadable input are valid invocations that return `1`; when debug is requested after allocation, its path is reported. 3. A successful run writes the logical output files, reports normalized and rejected counts, and passes the same deterministic run ID and start time to the manifest and debug bundle. 4. `--only` executes and reports only selected lanes. 5. `--output-dir` and `--debug-dir` override environment, file, and default roots; `--debug-dir` without `--debug` returns `2` and creates nothing. 6. `--llm-profile` reaches every effective LLM-capable pipeline module binding, leaves validator-specific profiles unchanged, selects the expected factory profile when exactly one is effective, and validates explicit Scriptorium profile IDs without making provider calls. 7. `effectiveLLMProfileIDs` ignores deterministic stages and returns stable, deduplicated ordering across chunk, lane stages, and LLM-backed validators. 8. `--session-id` rejects missing/blank values and passes a trimmed explicit value through prompt-facing requests. With no override, the parsed source document ID is used as the session identifier. 9. LLM factory, preparation, pipeline execution, output persistence, and debug persistence failures return `1`, preserve the primary error, and do not print a success message. 10. Successful warnings remain exit `0`, are counted on stderr, and appear in durable output and requested debug summary. Do not repeat the output-collision and terminalization matrices already covered by `run_id_test.go` and `state_hardening_test.go`. ### Validation ```sh go test ./internal/cli -run 'TestRun' ``` ### Exit criteria Every public non-reference run flag and every major CLI-to-runner handoff has a contract test, including representative failures. ## Stage 6: CLI reference selector and override contracts **Status:** Not started ### Objective Protect the high-risk selector grammar and target-resolution behavior without duplicating reference materialization internals. ### Read first - reference sections of `docs/cli.md` and `docs/config.md` - reference parsing and resolution helpers in `internal/cli/run.go` - reference resolution/materialization tests under `internal/framework/pipeline` ### Implement Add `internal/cli/reference_contract_test.go`. Build a compact catalog with chunk, extractor, merger, and normalizer specs declaring distinct, shared, optional, and required slots, including at least two artifact kinds where variant lookup matters. Cover: 1. Parse and apply every documented selector form: flat, `chunk.slot`, `merge.slot`, `lane.slot`, and explicit lane extract/merge/normalize. 2. Flat and lane selectors succeed only with one eligible selected target. Ambiguity errors identify viable explicit selectors. 3. Selected lanes constrain reference discovery; selectors for unselected or unknown lanes fail before reference file materialization. 4. Malformed bindings—missing selector, path, separator, or excess selector segments—and malformed unbind selectors return `2`. 5. Repeated overrides use the final explicit binding for that exact target; target-specific overrides do not alter same-named slots on other targets. 6. `--without-reference` removes optional configured bindings and fails when a required chunk, extract, merge, or normalize slot would remain unbound. 7. CLI paths resolve from the working directory while config paths resolve from the config file directory. Assert recorded provenance, not private helper call order. 8. Target lookup uses the selected artifact-kind variant and returns useful context when the requested merger/normalizer/extractor variant is absent. Use small UTF-8 temporary reference files. Media-type and malformed-content details already proven by pipeline materialization tests need only one CLI smoke case. ### Validation ```sh go test ./internal/cli -run 'Test.*Reference' ``` ### Exit criteria All public selector forms, ambiguity boundaries, lane selection, binding precedence, unbinding, and path-origin rules are protected. ## Stage 7: Cache, resume, output, and debug integration risks **Status:** Not started ### Objective Fill state-integration gaps without duplicating the focused ADR-0005 and ADR-0006 tests already present. ### Read first - `docs/operations.md` - `docs/internal/state.md` - ADR-0005 and ADR-0006 - `internal/cli/run_id_test.go` - `internal/cli/state_surfaces_test.go` - `internal/cli/state_hardening_test.go` - tests under `internal/framework/chunkplan` and `internal/framework/checkpoint` ### Implement Add `internal/cli/cache_contract_test.go` only for uncovered CLI composition contracts. Extend an existing state test when it already owns the behavior. Cover or confirm existing coverage for: 1. Chunk-plan mode precedence: CLI flag, environment, file, default. Invalid CLI syntax returns `2`; invalid environment/file modes return `1`. 2. An explicit chunk-plan root is passed through exactly. An empty configured root selects `/notarius/chunk-plans`. `bypass` never calls `UserCacheDir`, constructs a store, or creates a root. 3. Root-resolution and store-construction failures preserve context and create no durable output. 4. `auto` reuses a plan across independent invocations with the same source digest even when pipeline, chunk module settings, references, or selected lanes differ. `refresh` replaces only after valid chunking; `bypass` does no cache I/O. Reuse details below the CLI remain owned by chunk-plan package tests. 5. `--resume` alone constructs checkpoint loader and recorder under the exact configured or per-user checkpoint root. Without it, neither root resolution nor checkpoint I/O occurs. 6. Chunk-plan reuse and checkpoint resume remain independent across the relevant mode matrix. Reuse a compact table rather than duplicating every existing state-surface assertion. 7. Existing tests continue to prove exclusive output allocation, atomic files, retained new partial output, shared run identity, debug opt-in, restrictive debug permissions, redacted summaries, and exactly-once terminal reports. Add a case only if one of these facts is not directly asserted. 8. Config-oriented commands do not resolve or create output, cache, or debug state. Do not introduce automatic cleanup tests: Notarius intentionally retains output and requested debug bundles. ### Validation ```sh go test ./internal/cli -run 'Test.*(Cache|ChunkPlan|Checkpoint|Resume|State|Output|Debug|RunID)' go test ./internal/framework/chunkplan ./internal/framework/checkpoint ``` ### Exit criteria CLI state-root selection, opt-in I/O, cache mode precedence, cross-run reuse, and cache-family independence are covered at their composition boundary. ## Stage 8: Production composition and maintained examples **Status:** Not started ### Objective Protect the small number of production and end-to-end contracts most likely to break while unit tests continue to pass. ### Read first - `README.md` - maintained files under `examples/` - `docs/config.md#implemented-production-modules` - `internal/cli/catalog.go` - `internal/modules/integration` tests and helpers ### Implement Add `internal/cli/production_contract_test.go` and `internal/cli/example_contract_test.go`. Reuse production registries and prompt assets, but always inject a deterministic structured fake LLM client. Production composition tests must prove: 1. The production catalog exposes every module, artifact codec, validator, default validator chain, and prompt asset named by current configuration and maintained examples. 2. Catalog/registry conversion preserves typed artifact codec variants and validator-chain registrations. 3. Production prompt assets allow the configured D&D scene chunker and spell extractor to prepare without reading provider credentials. 4. `config validate --pipeline` accepts the maintained production module graph and rejects representative unknown modules, unknown validators, invalid artifact variants, and deterministic validators with LLM profiles. 5. A small test configuration selecting `dnd/scenes` records that chunker and its expected warning/provenance fields during a fake-LLM run. Example tests must prove: 1. Every maintained example configuration parses, validates, resolves its documented pipeline, and appears correctly in `pipelines list`. 2. The README/minimal example invocation runs with a fake LLM and produces the documented logical JSON bundle. Decode manifest, index, warning, rejection, and artifact files and assert contract fields; do not snapshot whitespace or incidental timestamps. 3. The documented `--only spells` invocation selects exactly the `spells` lane. 4. One malformed input fixture returns `1`, produces no successful output bundle, and records a failure report only when debug is explicitly enabled. If an example requires a provider profile, supply a test-only fake through `cli.Options`; never modify the example to embed credentials. ### Validation ```sh go test ./internal/cli -run 'Test(Production|Example|Maintained)' go test ./internal/modules/integration ``` ### Exit criteria Production registration, prompts, maintained configuration, and the shortest end-to-end user workflow are protected by offline tests. ## Stage 9: Coverage audit and plan closure **Status:** Not started ### Objective Confirm that contract and high-risk coverage is complete without converting coverage percentage into a goal. ### Read first - all tests added by Stages 1–8 - current canonical CLI, configuration, operations, integration, and internal state documents - `docs/policy/documentation.md` ### Implement 1. Build a temporary checklist mapping every current CLI command/flag and every current configuration section/source to at least one owning test. Do not commit the checklist if the test names themselves make ownership clear. 2. Run statement coverage for `internal/cli` and `internal/core/config`. Inspect uncovered functions and branches. Add a test only when the uncovered code is a documented contract, safety boundary, destructive/stateful path, complex selector/precedence branch, or failure path likely to regress. 3. Remove redundant cases that prove no additional contract or risk. Keep a small end-to-end layer and more numerous narrow unit tests. 4. Confirm tests do not access the network, depend on host credentials, use the real user cache directory, write outside temporary roots, or depend on test ordering. 5. Run race-enabled tests for the two changed packages. Fix test races; report production races separately unless authorized to change production code. 6. Update `docs/development.md` only if a stable testing procedure beyond its existing validation commands is now necessary. Do not document a coverage threshold. 7. Once all checks pass, delete this completed implementation plan. The tests and canonical current-behavior documents remain authoritative. ### Validation ```sh go test -coverprofile=/tmp/notarius-cli.cover ./internal/cli go test -coverprofile=/tmp/notarius-config.cover ./internal/core/config go tool cover -func=/tmp/notarius-cli.cover go tool cover -func=/tmp/notarius-config.cover go test -race ./internal/cli ./internal/core/config go test ./... go vet ./... go build ./cmd/notarius git diff --check ``` The `/tmp` coverage profiles are inspection artifacts and must not be committed. ### Exit criteria Every current CLI and configuration contract has an identifiable owning test; high-risk state, safety, precedence, selector, and failure paths are covered; the suite remains deterministic and maintainable; and no numeric coverage target has been introduced.