From ecee3d7aabbe83ac0e30d9ed54f6a591b8e7ddb6 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Wed, 28 Oct 2026 16:38:45 -0400 Subject: [PATCH 1/3] test(compat): fail when sdk compat diverges from internal/compat; note 101 header rule in proxy-swap matrix (D-0705, D-0769) --- internal/compat/sdk_parity_test.go | 32 ++++++++++++++++++++++++++++++ scripts/e2e/proxy-swap.sh | 3 +++ 2 files changed, 35 insertions(+) create mode 100644 internal/compat/sdk_parity_test.go diff --git a/internal/compat/sdk_parity_test.go b/internal/compat/sdk_parity_test.go new file mode 100644 index 000000000..47a5f6851 --- /dev/null +++ b/internal/compat/sdk_parity_test.go @@ -0,0 +1,32 @@ +package compat + +import ( + "testing" + + sdkcompat "github.com/nself-org/cli/sdk/go/v2/compat" +) + +// TestSDKCopyAgreesInCLIProcess pins D-0705. The CLI links sdk/go/compat (the +// pin store ancestor gate in sdk/go/remote reads it), and that copy reads only +// NSELF_V15. Today both copies apply the same rule, so they agree for every +// value. If P7-SHIP flips this package's default without the sdk copy following +// (or the CLI exporting its resolved mode), the CLI would print the v1.4 +// ancestor warning where it must refuse. This test fails the moment they +// diverge, so the flip has to handle the sdk copy in the same change. +func TestSDKCopyAgreesInCLIProcess(t *testing.T) { + for _, v := range []string{"", "", "0", "1", "true", "TRUE", "True", "yes", "on", " 1"} { + t.Run(v, func(t *testing.T) { + if v == "" { + t.Setenv(EnvVar, "x") + unset(t) + } else { + t.Setenv(EnvVar, v) + } + if cli, sdk := V15(), sdkcompat.V15(); cli != sdk { + t.Fatalf("NSELF_V15=%q: internal/compat.V15() = %v but sdk/go/compat.V15() = %v; "+ + "the CLI process links both (sdk/go/remote/knownhosts_compat.go, D-0484): "+ + "export the resolved mode to the sdk copy or delete the v1.4 branch in the same change (D-0705)", v, cli, sdk) + } + }) + } +} diff --git a/scripts/e2e/proxy-swap.sh b/scripts/e2e/proxy-swap.sh index a40e5009a..3ecf338a4 100755 --- a/scripts/e2e/proxy-swap.sh +++ b/scripts/e2e/proxy-swap.sh @@ -300,6 +300,9 @@ matrix() { import pathlib, sys # Accepted differences (P7-DEPL-03 lead ruling 2026-10-12): transport framing and Vary are proxy-owned, and Traefik # adds no response headers to a 101 upgrade, so a websocket row keeps only its handshake headers. +# Browsers do not apply CSP, HSTS or X-Frame-Options from a websocket handshake response (they govern the document), so +# the missing 101 security headers have no browser effect. Vary (D-0769) stays open until the traefik renderer decides +# whether to add Accept-Encoding; the matrix is tightened in the same change as the renderer. skip={'date','server','connection','content-length','transfer-encoding','keep-alive','vary'} handshake={'upgrade','sec-websocket-accept'} headers=[] From 607f6f3a3d17eb9bf2956f693233cdf1dfe873d8 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Wed, 28 Oct 2026 17:47:04 -0400 Subject: [PATCH 2/3] test: move sdk parity test out of L0 internal/compat to fix layering ratchet internal/compat is L0 and may not import the SDK module (sdk/go/v2/compat is unclassified). Moving TestSDKCopyAgreesInCLIProcess to test/e2e (L3) resolves the D1 violation without loosening the ratchet. --- .../e2e/sdk_compat_parity_test.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) rename internal/compat/sdk_parity_test.go => test/e2e/sdk_compat_parity_test.go (69%) diff --git a/internal/compat/sdk_parity_test.go b/test/e2e/sdk_compat_parity_test.go similarity index 69% rename from internal/compat/sdk_parity_test.go rename to test/e2e/sdk_compat_parity_test.go index 47a5f6851..bbbee1672 100644 --- a/internal/compat/sdk_parity_test.go +++ b/test/e2e/sdk_compat_parity_test.go @@ -1,8 +1,10 @@ -package compat +package e2e import ( + "os" "testing" + internalcompat "github.com/nself-org/cli/internal/compat" sdkcompat "github.com/nself-org/cli/sdk/go/v2/compat" ) @@ -13,16 +15,21 @@ import ( // (or the CLI exporting its resolved mode), the CLI would print the v1.4 // ancestor warning where it must refuse. This test fails the moment they // diverge, so the flip has to handle the sdk copy in the same change. +// +// Lives in test/e2e (L3) rather than internal/compat (L0) because L0 may not +// import the SDK module; see internal/repoqa/testdata/layers.yaml D1. func TestSDKCopyAgreesInCLIProcess(t *testing.T) { for _, v := range []string{"", "", "0", "1", "true", "TRUE", "True", "yes", "on", " 1"} { t.Run(v, func(t *testing.T) { if v == "" { - t.Setenv(EnvVar, "x") - unset(t) + t.Setenv(internalcompat.EnvVar, "x") + if err := os.Unsetenv(internalcompat.EnvVar); err != nil { + t.Fatalf("unset %s: %v", internalcompat.EnvVar, err) + } } else { - t.Setenv(EnvVar, v) + t.Setenv(internalcompat.EnvVar, v) } - if cli, sdk := V15(), sdkcompat.V15(); cli != sdk { + if cli, sdk := internalcompat.V15(), sdkcompat.V15(); cli != sdk { t.Fatalf("NSELF_V15=%q: internal/compat.V15() = %v but sdk/go/compat.V15() = %v; "+ "the CLI process links both (sdk/go/remote/knownhosts_compat.go, D-0484): "+ "export the resolved mode to the sdk copy or delete the v1.4 branch in the same change (D-0705)", v, cli, sdk) From 91925331156aa6a19ea345938fe23a0faad2ccc9 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Wed, 28 Oct 2026 18:28:50 -0400 Subject: [PATCH 3/3] fix(e2e): correct HSTS claim for WebSocket handshakes in proxy-swap Chromium learns HSTS from secure WebSocket handshakes and enforces it on subsequent requests. Document the missing HSTS header on 101 responses as an unresolved difference instead of claiming it has no browser effect. --- scripts/e2e/proxy-swap.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/e2e/proxy-swap.sh b/scripts/e2e/proxy-swap.sh index 3ecf338a4..a8e19f382 100755 --- a/scripts/e2e/proxy-swap.sh +++ b/scripts/e2e/proxy-swap.sh @@ -300,8 +300,10 @@ matrix() { import pathlib, sys # Accepted differences (P7-DEPL-03 lead ruling 2026-10-12): transport framing and Vary are proxy-owned, and Traefik # adds no response headers to a 101 upgrade, so a websocket row keeps only its handshake headers. -# Browsers do not apply CSP, HSTS or X-Frame-Options from a websocket handshake response (they govern the document), so -# the missing 101 security headers have no browser effect. Vary (D-0769) stays open until the traefik renderer decides +# Browsers do not apply CSP or X-Frame-Options from a websocket handshake response (they govern the document). However, +# Chromium learns HSTS from secure WebSocket handshakes and enforces it on subsequent requests +# (https://github.com/chromium/chromium/blob/main/net/websockets/websocket_end_to_end_test.cc#L670); the missing HSTS +# header on 101 responses is therefore an unresolved difference. Vary (D-0769) stays open until the traefik renderer decides # whether to add Accept-Encoding; the matrix is tightened in the same change as the renderer. skip={'date','server','connection','content-length','transfer-encoding','keep-alive','vary'} handshake={'upgrade','sec-websocket-accept'}