test(zerocode): cover insecure-TLS confirmation flow (#7693)#8178
Conversation
) The zerocode TUI accepts insecure-TLS WSS connections only after an explicit operator confirmation prompt. Two existing test seams already cover the storage layer (config::persist_wss_route_ack dedup + section preservation, WssTlsSection::route_acked membership, and resolve_wss_target mode transitions), but the confirmation prompt itself β the function that reads stdin and decides between InsecureTlsChoice::{Once, Always, Abort} β was previously unreachable from tests because it hardcoded stdin().read_line + eprintln/eprint. Refactor: - Extract confirm_insecure_tls_with<R: BufRead, W: Write>(reader, writer, url) as the testable core that takes the prompt and answer I/O as generic arguments. - Keep confirm_insecure_tls(url) as a thin wrapper that locks stdin() and writes the prompt to stderr(), preserving the existing public signature used by run() at main.rs:289. No production behavior change. Tests added (10) in apps/zerocode/src/main.rs::confirm_insecure_tls_tests: - confirm_input_{y,yes,a,always,n,empty,junk}_returns_{once,always,abort} - confirm_input_uppercase_lowercases_before_match (covers Y/YES/ALWAYS/N/NO) - confirm_prompt_writes_url_and_choice_menu_to_writer (pins the WARNING banner, the [y/a/N] menu, and the URL being confirmed β operator cannot skim past an insecure-TLS confirmation without seeing the target) - abort_arm_of_confirm_match_must_not_call_persist β a static-source test that scans main.rs's "match confirm_insecure_tls(url)?" block and asserts the InsecureTlsChoice::Abort arm does not call config::persist_wss_route_ack. Acceptance criterion 2 of zeroclaw-labs#7693: decline/abort paths must leave no persisted insecure-TLS choice. The structural guard fires loudly if a future refactor moves the persist call into the Abort arm β a silent security regression that no other test in the suite catches. Acceptance criteria coverage for zeroclaw-labs#7693: 1. 'Insecure TLS cannot be accepted without explicit confirmation' β the empty / n / junk / uppercase-N / default branches all return InsecureTlsChoice::Abort (covered by 5 of the input-mapping tests). 2. 'Decline/abort paths leave no persisted insecure-TLS choice' β covered by abort_arm_of_confirm_match_must_not_call_persist. 3. 'Mode transition tests cover the quickstart/chat handoff' β already covered by connection_tests::{flag_connect_overrides_config_uri, config_uri_used_when_no_flag, no_uri_anywhere_is_local_socket, skip_verify_is_flag_or_config}; this issue does not duplicate them. 4. 'prompt persistence behavior needed to test those transitions deterministically' β already covered by config::tests::{ route_acked_membership, persist_wss_route_ack_dedups, persist_wss_route_ack_preserves_other_sections}; this issue does not duplicate them. Validation: - cargo fmt --all --check: clean - cargo clippy --locked -p zerocode --all-targets -- -D warnings: 0 warnings - cargo test --locked -p zerocode: 366 passed; 0 failed; 0 ignored (10 new tests in confirm_insecure_tls_tests) Pre-Push Hook note: the .githooks/pre-push provider-dispatch gate calls 'rg' which is not installed in this environment (exits with status 127 β a Claude-Shell wrapper masquerades as 'rg'). Applied Skill 10.1 documented exception: 'git diff upstream/master...HEAD -- *.rs' was explicitly checked against the protected-method regex (chat|stream_chat|simple_chat|chat_with_system|chat_with_history| chat_with_tools|list_models|list_models_with_pricing|warmup) β zero matches. Local fmt + clippy + test gates all green before push.
singlerider
left a comment
There was a problem hiding this comment.
Reviewed at head ff4cece. Test-only coverage for the insecure-TLS confirmation flow (#7693) plus a behavior-preserving test seam. I checked the assertions and the structural guard against the live run() match. No prior reviews, no other blocks.
π’ What looks good β the seam refactor is behavior-preserving
Extracting confirm_insecure_tls_with<R: BufRead, W: Write> and leaving confirm_insecure_tls as a thin stdin/stderr wrapper keeps production behavior byte-identical (same match arms, same lowercasing) while making the prompt logic unit-testable without touching real fds. That is the right way to add coverage to an interactive prompt.
π’ What looks good β the input mapping tests pin the safe-default contract
The y/yes to Once, a/always to Always, and empty/n/junk/uppercase-N/NO to Abort tests match the production match exactly, and the case-folding test defends against an accidental case-sensitive refactor. Pinning that only the affirmative set opts into verification-disabled transport is the security-relevant invariant for #7693 acceptance criterion 1. The prompt-content test pins URL, the [y/a/N] menu, and the WARNING banner so a refactor cannot silently truncate the warning.
π’ What looks good β the abort-no-persist guard is sound for the current source
abort_arm_of_confirm_match_must_not_call_persist checks the right invariant (a declined insecure-TLS route must not persist). I traced the brace scanner against the live run() block: the three arms are brace-balanced with no braces inside the string literals, so the depth scan isolates the match block correctly, and the Abort arm body (anyhow::bail!) does not contain the persist call. The config:: prefix on the production call is harmless here because the test only asserts absence in the Abort arm via substring. The author documents the structural fragility honestly and justifies it over spinning up the full CLI/daemon/config stack, which is a reasonable tradeoff for a security guard.
Verdict: APPROVED.
Summary
master(commit717845fd2)config::persist_wss_route_ackdedup + section preservation,WssTlsSection::route_ackedmembership, andresolve_wss_targetmode transitions), but the confirmation prompt itself β the function that reads stdin and decides betweenInsecureTlsChoice::{Once, Always, Abort}β was previously unreachable from tests because it hardcodedstdin().read_line+eprintln!/eprint!.confirm_insecure_tls_with<R: BufRead, W: Write>(reader, writer, url)as the testable core that takes the prompt and answer I/O as generic arguments. Keepconfirm_insecure_tls(url)as a thin wrapper that locksstdin()and writes the prompt tostderr(), preserving the existing public signature used byrun()atapps/zerocode/src/main.rs:289. No production behavior change.apps/zerocode/src/main.rs::confirm_insecure_tls_tests. Tests are zero-network, zero-credential, and zero-mock for the input-mapping cases; one static-source test pins the structural invariant that theInsecureTlsChoice::Abortarm of the productionmatchblock does not invokeconfig::persist_wss_route_ack.tests,risk: high(auto from v0.9.0 shard),security,onboard,priority:p2,status:acceptedValidation Evidence (required)
$ cargo fmt --all -- --check (no output β clean) $ cargo clippy --locked -p zerocode --all-targets -- -D warnings Compiling zerocode v0.8.1 (/home/0668000666/0668000666/AI/ZeroClaw/apps/zerocode) Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.18s 0 warnings. $ cargo test --locked -p zerocode running 10 tests test confirm_insecure_tls_tests::abort_arm_of_confirm_match_must_not_call_persist ... ok test confirm_insecure_tls_tests::confirm_input_always_returns_always ... ok test confirm_insecure_tls_tests::confirm_input_empty_returns_abort ... ok test confirm_insecure_tls_tests::confirm_input_a_returns_always ... ok test confirm_insecure_tls_tests::confirm_input_junk_returns_abort ... ok test confirm_insecure_tls_tests::confirm_input_n_returns_abort ... ok test confirm_insecure_tls_tests::confirm_input_uppercase_lowercases_before_match ... ok test confirm_insecure_tls_tests::confirm_input_y_returns_once ... ok test confirm_insecure_tls_tests::confirm_input_yes_returns_once ... ok test confirm_insecure_tls_tests::confirm_prompt_writes_url_and_choice_menu_to_writer ... ok test result: ok. 366 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.18sconfirm_insecure_tls_tests, all passing on first run; no existing test regressed (366 pre-existing tests still pass).abort_arm_of_confirm_match_must_not_call_persistparses the actualmain.rsbyte content viainclude_str!, locates thematch confirm_insecure_tls(url)? {block by brace-pair scan, slices theInsecureTlsChoice::Abortarm body, and asserts it does not contain the substringpersist_wss_route_ack(&local_config_dir, url)?. The test passes against the current production code (confirming the invariant holds today) and will fail loudly if a future refactor moves the persist call into the Abort arm.confirm_prompt_writes_url_and_choice_menu_to_writerdrivesconfirm_insecure_tls_withwith a fakeCursor<&[u8]>reader and aVec<u8>writer, then asserts the captured stderr text contains (a) the URL being confirmed, (b) the[y/a/N]choice menu, and (c) the leadingWARNINGbanner. Operator cannot skim past an insecure-TLS confirmation without seeing all three..githooks/pre-pushprovider-dispatch gate invokesrg(ripgrep), which is not installed in this environment β thergbinary is a Claude Code wrapper that exits with status 127 when invoked outside the Claude shell (Skill 10.1 documented exception). The dispatch-gate regex\.(chat|stream_chat|simple_chat|chat_with_system|chat_with_history|chat_with_tools|list_models|list_models_with_pricing|warmup)\(was explicitly checked againstgit diff upstream/master...HEAD -- '*.rs'β zero matches. Local fmt + clippy + test gates all green before push via--no-verify --force-with-lease. The exception is documented in the commit message as required.Security & Privacy Impact (required)
wss://example.test:1,wss://insecure-host.example:8443. No captured tenant fixtures.InsecureTlsChoice::Abortcannot silently callpersist_wss_route_ack(&local_config_dir, url)?. If a future refactor moves the persist call into the Abort arm, the operator's declined insecure-TLS choice would be silently stored on disk β a security regression with no other test coverage. This test fires loudly on that exact regression.Compatibility / Migration (required)
confirm_insecure_tls(url) -> anyhow::Result<InsecureTlsChoice>retains its original signature;confirm_insecure_tls_with<R: BufRead, W: Write>is a new private helper.InsecureTlsChoiceis unchanged.run()atmain.rs:289) is unaffected becauseconfirm_insecure_tls(url)still exists with the same signature and identical behavior.[y/a/N]menu, the same input mapping. The only change is that the prompt is now written viawriteln!/write!macros against a&mut W: Writeparameter instead ofeprintln!/eprint!against the hardcodedstderr()handle β byte-identical output.i18n Follow-Through (required)
No user-visible strings changed. The WARNING banner, the
[y/a/N]choice menu, and theContinue with verification disabled?prompt are emitted verbatim from the existing prompt source; only their delivery mechanism moved fromeprintln!towriteln!(writer, ...). Not!()macro calls were modified.Human Verification (required)
confirm_insecure_tls_withand theconfirm_insecure_tlsthin wrapper. Confirmed that the public signature is preserved, thematcharm logic is byte-identical to the pre-refactor inline implementation, and the?-propagatedwriteln!/write!I/O errors replace the previously-ignoredeprintln!panic-on-IO semantics withResult-flow that the existingrun()?chain already handles.Side Effects / Blast Radius (required)
confirm_insecure_tlsβ the function isfn(private), called only fromrun()atmain.rs:289. The refactor is invisible to all other modules.confirm_insecure_tls_with<R: BufRead, W: Write>helper is alsofn(private); no other module depends on its signature.#[cfg(test)] mod confirm_insecure_tls_testsblock appended after the existingconnection_testsblock. They do not interact with any production state β each test creates a freshCursorandVec<u8>and discards them after the assertion.abort_arm_of_confirm_match_must_not_call_persistreadsmain.rsviainclude_str!. The match block detection is by brace-pair depth scan, which is robust to whitespace, comments, and rustfmt rewrapping. If a future refactor substantially restructures thematch confirm_insecure_tls(url)? { ... }shape (e.g., extracts it into a helper function), the test'sfind(MATCH_OPEN)may fail and the test will need to be updated β this is a deliberate coupling so the invariant remains coupled to the actual production structure.Agent Collaboration Notes (optional)
--insecure-transportfor non-TLS WSS, or for--allow-downgrade) should follow the sameconfirm_*_with<R: BufRead, W: Write>seam pattern so the prompt logic can be unit-tested without touchingstdin/stderr. The thin-wrapper conventionconfirm_*(arg) -> Result<...> { let stdin = stdin(); let mut stderr = stderr(); confirm_*_with(stdin.lock(), &mut stderr, arg) }is now the project idiom for this crate.include_str!and asserting a structural property) is appropriate when the alternative would be spinning up an end-to-end fixture too expensive to maintain. Use it sparingly β fragile to large refactors β but prefer it over omitting the invariant entirely when the cost of end-to-end coverage is prohibitive.Rollback Plan (required)
Single commit.
git revert <commit-sha>restores the priorconfirm_insecure_tls(url)inline implementation and removes the 10 tests. No schema, config, or public API changes to roll back. No migration steps needed in either direction.Risks and Mitigations (required)
writeln!/write!?-propagation replaceseprintln!panic-on-IO semanticsrun()?chain already handlesanyhow::Errorfrom upstream I/O; a stderr write failure surfaces the same way as a stdin read failure. Equivalent observable behavior.yaora!thinking it should be Once/Alwaystrim().to_ascii_lowercase()then exact-match against"y" | "yes"/"a" | "always"/_. Anything else defaults toAbort. Tested explicitly withconfirm_input_junk_returns_abort.stdin().lock()holds the stdin lock for the duration of the promptconfirm_insecure_tls_withcall β noawaitis crossed while the lock is held. The lock is dropped whenconfirm_insecure_tlsreturns.rgexit-127 in this environment bypassed dispatch-gategit diff upstream/master...HEAD -- '*.rs'regex check returning 0 matches against all 9 protected method names. Documented in commit message body and PR "Validation Evidence" section. CI will re-run the gate from a clean environment on push.