From d5be18baed14bb616c3c68c06f9ccae4bacaff27 Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Fri, 30 Oct 2026 07:02:07 +0000 Subject: [PATCH] test(mapping): complete the decline-matrix contract suite (w5-decline-matrix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - coverage audit recorded in the task file: every mapping.md row × every build it can appear in × both directions → the pinning test - gap rows landed in tests/dispatch_core.rs: the streamlocal_forward decline, the cancel-form refusions, the git-build pty decline - never-features pinned in every build both directions; the auth-agent open recorded pin-bound upstream (russh 0.65 declines AgentForward opens pre-handler; no client initiator at the pin) - sftp-only build verified no-op + declining via the matrix run - matrix runner landed: scripts/matrix.sh (cargo test × 6 builds, clippy ×2 -D warnings, fmt, doc), joined AGENTS.md verification list Verification: scripts/matrix.sh green end to end --- AGENTS.md | 3 + scripts/matrix.sh | 25 ++++++++ tasks/implementation/w5-decline-matrix.md | 70 ++++++++++++++++++++++- tests/dispatch_core.rs | 65 +++++++++++++++++++-- 4 files changed, 155 insertions(+), 8 deletions(-) create mode 100755 scripts/matrix.sh diff --git a/AGENTS.md b/AGENTS.md index 5e8d4dc..258a3f0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -164,6 +164,9 @@ cargo clippy --all-targets -- -D warnings cargo fmt --check cargo doc --no-deps # if docs changed cargo publish --dry-run --allow-dirty # before a release +scripts/matrix.sh # the decline-matrix runner: the full suite per + # feature build (none / git / session / tunnel / + # sftp / --all-features), clippy ×2, fmt, doc ``` If feature flags are added, also run `cargo test --all-features` and diff --git a/scripts/matrix.sh b/scripts/matrix.sh new file mode 100755 index 0000000..b6b6145 --- /dev/null +++ b/scripts/matrix.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# The decline-matrix runner (tasks/implementation/w5-decline-matrix.md): +# the per-build verification set as one command — `cargo test` per +# feature build (none / git / session / tunnel / sftp-only / +# --all-features; the sftp-only run is the no-op-feature fact itself +# verified), clippy ×2 (-D warnings, with and without +# --all-features), fmt, doc. +set -euo pipefail +cd "$(dirname "$0")/.." + +default_build="none (default = [])" +for build in "" "--features git" "--features session" "--features tunnel" "--features sftp" "--all-features"; do + label="${build:-$default_build}" + echo "== cargo test [$label]" + cargo test $build +done + +echo "== clippy (no all-features)" +cargo clippy --all-targets -- -D warnings +echo "== clippy (all-features)" +cargo clippy --all-features --all-targets -- -D warnings +echo "== fmt" +cargo fmt --check +echo "== doc" +cargo doc --no-deps \ No newline at end of file diff --git a/tasks/implementation/w5-decline-matrix.md b/tasks/implementation/w5-decline-matrix.md index c9c43ac..ef5954f 100644 --- a/tasks/implementation/w5-decline-matrix.md +++ b/tasks/implementation/w5-decline-matrix.md @@ -1,7 +1,7 @@ --- id: w5-decline-matrix name: Wave 5 — the decline-matrix contract suite (every build × every row × both directions) -status: pending +status: completed depends_on: [review-wave-4] scope: moderate risk: low @@ -79,8 +79,72 @@ matrix **complete and systematic** rather than accreted: ## Notes -> To be filled by implementation agent +**The coverage audit** (every row × every build it can appear in × +both directions → the test that pins it; builds: none / `git` / +`session` / `tunnel` / `--all-features`, plus the `sftp`-only build +which has the none build's shape with the feature on). S = server +direction, C = client direction; `dispatch_core` = tests/dispatch_core.rs. + +| Row | Build(s) it can appear in | Direction | Pinned by | +|---|---|---|---| +| session channel open, unfeatured | none, tunnel, sftp-only | S | `dispatch_core::default_build::every_channel_open_declines_in_a_default_build`, `dispatch_core::tunnel_build::session_channels_decline_in_a_tunnel_build` | +| pty/shell decline (feature on, seam/args unset) | session, all-features | S | `dispatch_core::session_build::pty_and_shell_decline_seam_unconfigured_in_a_session_build`, `dispatch_core::all_features::pty_and_shell_decline_seam_unconfigured_with_all_features` (+ same rows in tests/tty_mapping.rs's `unset` mod for all session rows) | +| pty/shell decline, feature off | git | S | `dispatch_core::git_build::pty_shell_and_capability_requests_decline_in_a_git_build` (**gap closed**: pty added beside the accreted shell row) | +| pipe-exec decline (unfeatured / wrapper unset) | git, session, all-features | S | `git_build::plain_exec_declines_and_keeps_the_channel_in_a_git_build`, `session_build::plain_exec_declines_wrapper_unconfigured_in_a_session_build`, `all_features::plain_exec_declines_wrapper_unconfigured_with_all_features` | +| git-parseable exec (routes to the git row's seam-stub decline in this binary; served in git_mapping) | git, all-features | S | `git_parseable_exec_routes_to_the_git_row_stub`, `all_features::git_exec_routes_to_the_git_row_stub_with_all_features`, tests/git_mapping.rs | +| git-parseable exec declined (the `git` parse is session's gate) | session | S | `session_build::git_parseable_exec_declines_in_a_session_build` | +| unknown git services declined surface | git | S | `git_build::unknown_git_services_take_the_declined_surface`, tests/git_mapping.rs `unknown_services_decline_request_level_with_alive_channel` | +| subsystem `sftp` declines with reason (OQ-13) | git, session, all-features | S | `git_build::subsystems_decline_with_their_named_reasons_in_a_git_build`, `session_build::subsystems_decline_with_their_named_reasons_in_a_session_build`, `all_features::capability_declines_hold_with_all_features` | +| subsystem `sftp` declines (the feature gates nothing) | sftp-only, none, tunnel | S | session channels decline at open (row 1) — the subsystem row cannot appear; the sftp-only run exercises the identical none-build declining behavior below | +| subsystem `sftp` declines (client-side placeholder, before any wire op) | every build | C | tests/client_ops.rs `sftp_placeholder_declines_in_every_build_before_any_wire_op` (+ `sftp_feature_on_the_placeholder_still_declines`, session builds) | +| subsystem git service names decline (no repo id on a subsystem) | git, session, all-features | S | same subsystem rows as `sftp` + tests/git_mapping.rs `service_only_declines_request_level_with_alive_channel` | +| env_request declines unconditionally (OQ-09) | git, session, all-features | S | `git_build::env_requests_decline_in_a_git_build`, `session_build::env_requests_decline_in_a_session_build`, `all_features::capability_declines_hold_with_all_features` (+ C-side: env never sent ahead of any op — no client env op exists) | +| never-feature x11: channel open | every build | S | `dispatch_core::x11_channel_opens_decline_in_every_build` (shared) | +| never-feature x11: request shape | git, session, all-features (rows need a session channel) | S | `dispatch_core::x11_and_agent_requests_decline_wherever_sessions_open` | +| never-feature x11: server-initiated open | every build | C | tests/client_dial.rs `server_initiated_opens_decline_cleanly_on_the_wire` (the decline-all skeleton, `channel_open_x11` + the `never-feature` event row) | +| never-feature auth-agent: channel open | — | S/C | **pin-bound upstream**: russh 0.65 declines a server-received `ChannelType::AgentForward` open itself (`ADMINISTRATIVELY_PROHIBITED` pre-handler, russh-0.65/src/server/encrypted.rs), and exposes no client-initiated agent open at this pin — the row is unreachable through the public API in either direction; recorded here per ADR-002 (the pin's decline is part of the pin) | +| never-feature auth-agent: request shape | git, session, all-features | S | `x11_and_agent_requests_decline_wherever_sessions_open` (the agent-forward request) | +| never-feature auth-agent: server-initiated open | every build | C | tests/client_dial.rs `server_initiated_opens_decline_cleanly_on_the_wire` (`channel_open_agent`) | +| direct-tcpip open | unfeatured (none/git/session); seam-unset (tunnel/all-features) | S | `default_build::every_channel_open_declines_in_a_default_build`, `git_build::tunnel_opens_decline_unfeatured_in_a_git_build`, `session_build::tunnel_opens_decline_unfeatured_in_a_session_build`, `tunnel_build::tunnel_direct_rows_decline_seam_unconfigured_without_a_handle` (`ConnectFailed`), `all_features::tunnel_direct_rows_decline_seam_unconfigured_without_a_handle_with_all_features` | +| direct-streamlocal open (OQ-11) | same builds as direct-tcpip | S | same rows + tests/tunnel_mapping.rs `direct_streamlocal_declines_off_unix_before_any_fabric_call` (off-Unix builds) | +| direct rows serve (the matrix's serve side) | tunnel, all-features | S | tests/tunnel_mapping.rs `direct_tcpip_serves_through_the_route_1_bridge` etc. | +| forwarded-tcpip inbound open (unvanilla class, `ConnectFailed`) | — | S | **pin-bound**: russh 0.65's client `Handle` exposes no forwarded-tcpip open (only servers initiate it), so the wire row is unreachable from any vanilla-client driver; the hook's decline is recorded at `src/handler.rs::channel_open_forwarded_tcpip` (`w3-tunnel-listen`'s recorded wire classes). Both serve-side and decline-side of the `-R` data leg: tests/tunnel_mapping.rs's arrival rows | +| forwarded-tcpip / forwarded-streamlocal server-initiated opens, no acceptor | every build | C | tests/client_dial.rs `server_initiated_opens_decline_cleanly_on_the_wire` + tests/client_reverse_listen.rs (tunnel builds: `reverse_listen_without_an_acceptor_declines_before_any_wire_op`, `unregistered_forwarded_open_declines_cleanly`, the late-install/uninstall rows) | +| `-R` listen: `tcpip_forward` declines | every build (unfeatured gate / tunnel seam-unset) | S | `dispatch_core::tcpip_forward_listen_requests_decline_in_every_build` (shared); tunnel serving + cap + mint rows: tests/tunnel_mapping.rs | +| `-R` listen: `streamlocal_forward` declines | every build | S | `dispatch_core::streamlocal_forward_listen_requests_decline_in_every_build` (**gap closed**: the unix listen form's decline had no shared row) | +| `-R` cancel forms refuse unknown pairs | every build | S | `dispatch_core::global_request_cancels_refuse_unknown_pairs_in_every_build` (**gap closed**: the cancels had no row outside tunnel builds; tunnel builds' not-found parity: tests/tunnel_mapping.rs `cancel_tcpip_forward_tears_the_fabric_listener_down`, the unix twin) | +| listen seam unset declines the listen forms | tunnel, all-features | S | tests/tunnel_mapping.rs `listen_forms_decline_when_the_listen_seam_is_unset` | +| window_change / signal (fire-and-forget, no decline shape) | session, all-features | S | no wire row by contract (mapping.md §Request-level rows): the serving rows pin the path (tests/tty_mapping.rs resize/signal rows); unfeatured builds emit the decline event only | +| client op declines (the acceptor's `NoAcceptor` pre-wire, the op-row `OpError` classes) | session/tunnel/git builds respectively | C | tests/client_ops.rs `session_rows`/`tunnel_rows`/`git_rows` + tests/client_reverse_listen.rs | +| sftp-only build is the no-op feature | sftp-only | both | `scripts/matrix.sh` runs the full suite under `--features sftp`: the same none-build decline rows + the shared rows + the C-side placeholder run unmodified — the no-op fact (no dep, no behavior change) verified, not assumed | + +**Gaps found and landed** (tests/dispatch_core.rs, the single-writer +map: this task edits only dispatch_core's rows, `scripts/` + +AGENTS.md — the e2e tasks' new binaries stay disjoint): streamlocal +listen decline, cancel-form refusals, git-build pty. + +**The matrix runner vehicle decided**: the script route — +`scripts/matrix.sh` (the per-build verification set as one command; +the documented-command-list alternative rejected on the +one-invocation requirement, the alkgit corpus-gate posture). Joined +the verification list in AGENTS.md §Verification Commands. ## Summary -> To be filled on completion \ No newline at end of file +Completed 2026-10-30. The decline-matrix contract suite is now +complete and systematic: the coverage audit above is table-green in +every cell (in every build each row can appear in, both directions), +three gap rows landed in tests/dispatch_core.rs (the +`streamlocal_forward` decline, the cancel-form refusions, the +git-build pty decline closing the accreted shell-only row), the sftp +no-op-feature fact is verified by the `sftp`-only build running the +identical decline suite, the never-features are pinned in every build +both directions (the auth-agent open recorded pin-bound upstream with +the exact russh-0.65 mechanism), and the matrix runner landed as +`scripts/matrix.sh` (tests × 6 builds — none / git / session / tunnel +/ sftp / --all-features — then clippy ×2 `-D warnings`, fmt, doc) and +joined AGENTS.md's verification list. Verification: the full +`scripts/matrix.sh` run green — every `cargo test` build (84…114 lib +tests per build, all integration binaries included), both clippy runs +`-D warnings` clean, fmt clean, `cargo doc --no-deps` clean. No src/ +changes were needed — the gaps were all test-graph rows. \ No newline at end of file diff --git a/tests/dispatch_core.rs b/tests/dispatch_core.rs index ac174f5..65b1417 100644 --- a/tests/dispatch_core.rs +++ b/tests/dispatch_core.rs @@ -22,8 +22,9 @@ mod common; use std::sync::Arc; use common::harness::{ - LoopbackSession, adapter, authenticate_publickey, await_close, drive, next_message, - open_session_channel, open_x11_channel, tcpip_forward, + LoopbackSession, adapter, authenticate_publickey, await_close, cancel_streamlocal_forward, + cancel_tcpip_forward, drive, next_message, open_session_channel, open_x11_channel, + streamlocal_forward, tcpip_forward, }; use common::{MemHostKeyPolicy, MemIdentityProvider, fixture, identity}; use russh::client::Msg; @@ -135,6 +136,49 @@ async fn tcpip_forward_listen_requests_decline_in_every_build() { session.unmount().await; } +/// - *Contract stamp:* mapping.md §Global-request rows (the +/// `streamlocal-forward` unix listen form is the `tunnel` row's +/// other half: unfeatured builds decline before any fabric call, +/// a `tunnel` build declines on the unset listen seam — the same +/// refused-global-request wire shape both ways). +#[tokio::test] +async fn streamlocal_forward_listen_requests_decline_in_every_build() { + let session = authenticated().await; + let err = streamlocal_forward(&session.client, "/run/alkssh-matrix.sock") + .await + .expect_err("the unix listen request declines"); + assert!( + matches!(err, Error::RequestDenied), + "expected the declined unix listen's RequestDenied, got {err:?}" + ); + session.unmount().await; +} + +/// - *Contract stamp:* mapping.md §Global-request rows (the cancel +/// forms: `false` for an unknown pair — the sshd not-found parity; +/// unfeatured builds decline their gate before reaching the +/// listener map, a `tunnel` build's empty map misses the pair — +/// the same refused-cancel wire shape both ways). +#[tokio::test] +async fn global_request_cancels_refuse_unknown_pairs_in_every_build() { + let session = authenticated().await; + let denied = cancel_tcpip_forward(&session.client, "127.0.0.1", 8080) + .await + .expect_err("the tcpip cancel of a no-listen pair refuses"); + assert!( + matches!(denied, Error::RequestDenied), + "expected the refused cancel's RequestDenied, got {denied:?}" + ); + let denied = cancel_streamlocal_forward(&session.client, "/run/alkssh-matrix.sock") + .await + .expect_err("the streamlocal cancel of a no-listen pair refuses"); + assert!( + matches!(denied, Error::RequestDenied), + "expected the refused cancel's RequestDenied, got {denied:?}" + ); + session.unmount().await; +} + /// - *Contract stamp:* mapping.md §Decline matrix (git-build row, /// request side: `x11` / `auth-agent` requests decline; ADR-003 §5). #[cfg(any(feature = "git", feature = "session"))] @@ -222,8 +266,8 @@ mod default_build { mod git_build { use super::*; use common::harness::{ - open_direct_streamlocal, open_direct_tcpip, request_env, request_exec, request_shell, - request_subsystem, + open_direct_streamlocal, open_direct_tcpip, request_env, request_exec, request_pty, + request_shell, request_subsystem, }; /// - *Contract stamp:* the task's channel-open gate (session @@ -309,11 +353,22 @@ mod git_build { } /// - *Contract stamp:* mapping.md §Decline matrix (git-build row: - /// pty/shell and all session requests decline). + /// pty/shell and all session requests decline — the pty row + /// declines unfeatured (`channel_failure`, the channel alive) + /// exactly as the shell row does). #[tokio::test] async fn pty_shell_and_capability_requests_decline_in_a_git_build() { let session = authenticated().await; + let mut channel = open_session_channel(&session.client) + .await + .expect("the session channel opens"); + request_pty(&channel) + .await + .expect("the pty request rides the wire"); + expect_failure(&mut channel, "the pty row declines unfeatured").await; + expect_channel_alive(&mut channel).await; + let mut channel = open_session_channel(&session.client) .await .expect("the session channel opens");