Add implementation plan for CLI and configuration test coverage

This commit is contained in:
2026-07-18 10:14:02 -05:00
parent 3c35124db4
commit 50191ee694
3 changed files with 715 additions and 8 deletions

View File

@@ -0,0 +1,642 @@
# 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 `<UserCacheDir>/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 18
- 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.

View File

@@ -1,6 +1,7 @@
package config
import (
"reflect"
"strings"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
@@ -126,17 +127,47 @@ func redactOptions(values map[string]any) map[string]any {
}
func redactOptionValue(value any) any {
switch typed := value.(type) {
case map[string]any:
return redactOptions(typed)
case []any:
items := make([]any, len(typed))
for i, item := range typed {
items[i] = redactOptionValue(item)
if value == nil {
return nil
}
reflected := reflect.ValueOf(value)
switch reflected.Kind() {
case reflect.Map:
if reflected.Type().Key().Kind() != reflect.String {
return value
}
if reflected.IsNil() {
return nil
}
out := make(map[string]any, reflected.Len())
iterator := reflected.MapRange()
for iterator.Next() {
key := iterator.Key().String()
if sensitiveConfigKey(key) {
out[key] = "[REDACTED]"
continue
}
out[key] = redactOptionValue(iterator.Value().Interface())
}
return out
case reflect.Slice:
if reflected.IsNil() {
return nil
}
if reflected.Type().Elem().Kind() == reflect.Uint8 {
out := reflect.MakeSlice(reflected.Type(), reflected.Len(), reflected.Len())
reflect.Copy(out, reflected)
return out.Interface()
}
fallthrough
case reflect.Array:
items := make([]any, reflected.Len())
for i := 0; i < reflected.Len(); i++ {
items[i] = redactOptionValue(reflected.Index(i).Interface())
}
return items
default:
return typed
return value
}
}

View File

@@ -117,6 +117,40 @@ func TestRedactedEffectiveConfigPayloadDoesNotAliasSource(t *testing.T) {
assertRedactionTestBindingUnchanged(t, effective.ResolvedPipeline.Input, "effective")
}
func TestRedactedResolvedPipelinePayloadHandlesTypedOptionContainers(t *testing.T) {
type optionMap map[string]string
type optionList []optionMap
typed := optionList{{
"api_key": "typed-container-secret",
"safe": "typed-container-safe",
}}
effective := EffectiveConfig{ResolvedPipeline: pipeline.ResolvedPipeline{
Input: pipeline.ModuleBinding{Options: map[string]any{"nested": typed}},
}}
payload := effective.RedactedResolvedPipelinePayload()
nested, ok := payload.Input.Options["nested"].([]any)
if !ok || len(nested) != 1 {
t.Fatalf("redacted typed list = %#v", payload.Input.Options["nested"])
}
item, ok := nested[0].(map[string]any)
if !ok {
t.Fatalf("redacted typed map = %#v", nested[0])
}
if got := item["api_key"]; got != "[REDACTED]" {
t.Fatalf("redacted api_key = %v", got)
}
if got := item["safe"]; got != "typed-container-safe" {
t.Fatalf("safe option = %v", got)
}
item["safe"] = "mutated"
if got := typed[0]["safe"]; got != "typed-container-safe" {
t.Fatalf("source typed map mutated through redacted payload: %q", got)
}
}
func redactionTestBinding(name string) pipeline.ModuleBinding {
return pipeline.ModuleBinding{
Module: "safe-" + name,