test(integration): add hermeticity tripwire (I-4) · Entire
test(integration): add hermeticity tripwire (I-4)
aec192c·
Soph·1w ago·3 files·+96 added/-1 removed
Integration/unit tests have historically made live github.com fetches and triggered macOS keychain prompts when checkpoint-token / checkpoint_remote resolution was in play (#1463, 53bc37a88). This adds a TestMain-level tripwire.
When ENTIRE_TEST_GIT_HERMETIC is set (the integration TestMain sets it, plus GIT_TERMINAL_PROMPT=0), GitIsolatedEnv's global git config routes HTTPS transport to github.com/gitlab.com through a dead loopback proxy, so any test that accidentally dials those hosts fails fast instead of reaching the network or prompting for credentials.
The config lives in the file GIT_CONFIG_GLOBAL points at because GitIsolatedEnv strips inherited GIT_CONFIG_* env, so the GIT_CONFIG_COUNT-injected insteadOf approach would be filtered out for every spawned binary. A dead per-host proxy is used rather than url.insteadOf: insteadOf rewrites the effective URL git reports on read, which broke origin-URL forge detection (trail_resume). The proxy blocks transport only and is scoped per host, so the in-process 127.0.0.1 HTTPS test server and the checkpoint-token GIT_CONFIG_* injection are unaffected.
A self-test proves the tripwire fires: git ls-remote https://github.com/... fails fast (~20ms) without network or prompt.
Co-Authored-By: Claude Fable 5 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_012yi3hHGAGepwfrfjPETjGq
Changes
3
cmd/entire/cli
integration_test
Ahermeticity_test.go+47
Msetup_test.go+13
testutil
Mtestutil.go+36/-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//go:build integration
package integration
import (
"context"
"os/exec"
"strings"
"testing"
"time"
"github.com/entireio/cli/cmd/entire/cli/testutil"
)
// TestHermeticityGuard_ExternalHostFailsFast proves the TestMain hermeticity
// tripwire fires: a git command that dials a real external host is redirected to
// an unroutable loopback address and fails fast, without reaching the network or
// prompting for credentials. Regression class: tests accidentally hitting live
// github.com / the macOS keychain (#1463, 53bc37a88).
func TestHermeticityGuard_ExternalHostFailsFast(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(t.Context(), 20*time.Second)
defer cancel()
// ls-remote against a public-looking github URL must be refused immediately
// by the insteadOf redirect to 127.0.0.1:1, not hang on DNS/network or block
// on a credential prompt.
cmd := exec.CommandContext(ctx, "git", "ls-remote", "https://github.com/example/example")
cmd.Env = testutil.GitIsolatedEnv()
start := time.Now()
out, err := cmd.CombinedOutput()
elapsed := time.Since(start)
if err == nil {
t.Fatalf("expected ls-remote to fail under the hermeticity guard, but it succeeded:\n%s", out)
}
if ctx.Err() != nil {
t.Fatalf("ls-remote did not fail fast (timed out after %s); the guard should refuse it immediately:\n%s", elapsed, out)
}
// The redirect target is the loopback refusal address, confirming the rewrite
// (not a real github.com dial) produced the failure.
if !strings.Contains(string(out), "127.0.0.1") {
t.Errorf("expected failure to mention the loopback redirect target 127.0.0.1, got:\n%s", out)
}
}
Mcmd/entire/cli/integration_test/hermeticity_test.go+47
7 unmodified lines
8
9
10
11
12
13
14
15
13 unmodified lines
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
7 unmodified lines
"os/exec"
"path/filepath"
"testing"
"github.com/entireio/cli/cmd/entire/cli/testutil"
)
// TestMain builds the CLI binary once before running all tests.
13 unmodified lines
// internal/testdirs fallback cannot protect it — isolation must come from
// the environment, which children inherit because all integration env
// building starts from os.Environ() (testutil.GitIsolatedEnv).
//
// GIT_TERMINAL_PROMPT=0 and ENTIRE_TEST_GIT_HERMETIC form the hermeticity
// tripwire: the latter makes GitIsolatedEnv's global git config route HTTPS
// transport to real external hosts (github.com, gitlab.com) through a dead
// loopback proxy, so any test whose git commands accidentally dial the network
// fails fast instead of reaching it or prompting for credentials (regressions
// #1463, 53bc37a88). The config lives in the file because GitIsolatedEnv strips
// inherited GIT_CONFIG_* env; it proxies transport only (not url.insteadOf, which
// would corrupt origin-URL forge detection) and leaves loopback servers untouched.
isolation := map[string]string{
"ENTIRE_CONFIG_DIR": filepath.Join(tmpDir, "entire-config"),
"XDG_CACHE_HOME": filepath.Join(tmpDir, "entire-cache"),
"ENTIRE_TOKEN_STORE": "file",
"ENTIRE_TOKEN_STORE_PATH": filepath.Join(tmpDir, "entire-tokens.json"),
"ENTIRE_TEST_AUTH_STORE_FILE": filepath.Join(tmpDir, "entire-auth-tokens.json"),
"GIT_TERMINAL_PROMPT": "0",
testutil.EnvGitHermetic: "1",
}
for k, v := range isolation {
if err := os.Setenv(k, v); err != nil {
Mcmd/entire/cli/integration_test/setup_test.go+13
221 unmodified lines
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
231
262
263
264
265
266
267
268
269
221 unmodified lines
var gitEmptyConfig string
var gitEmptyConfigOnce sync.Once
// EnvGitHermetic, when set to a non-empty value, makes gitEmptyConfigPath append
// per-host HTTP proxy config that routes git HTTPS transport to real external
// hosts (github.com, gitlab.com) through an unroutable loopback proxy. Any test
// whose git commands accidentally dial those hosts then fails fast (connection
// refused at 127.0.0.1:1) instead of reaching the network or prompting for
// credentials. It is opt-in per test process — the integration TestMain sets it
// — so unit test packages that don't set it are unaffected. Because
// GitIsolatedEnv strips all inherited GIT_CONFIG_* env, this config must live in
// the file GIT_CONFIG_GLOBAL points at (this one), not in GIT_CONFIG_* env
// entries.
//
// A dead proxy (not url.insteadOf) is used deliberately: insteadOf rewrites the
// effective URL that git reports on read, which breaks production code that
// resolves the origin URL to detect the forge (e.g. `entire trail`). The proxy
// blocks transport only, leaving the configured URL string intact, and is scoped
// per host so loopback (127.0.0.1) test servers are never proxied.
//
// Regression class: tests accidentally hitting live github.com / the macOS
// keychain (#1463, 53bc37a88).
const EnvGitHermetic = "ENTIRE_TEST_GIT_HERMETIC"
// hermeticGitConfig routes HTTPS transport to real external hosts through a dead
// loopback proxy. Loopback test servers (127.0.0.1) and file:// / bare-path
// remotes are not proxied, so the in-process HTTPS git server still works. Only
// HTTPS is covered — the accidental-dial regression class is HTTPS fetches; SSH
// (git@…) to a real host fails on its own without credentials.
const hermeticGitConfig = "[http \"https://github.com/\"]\n\tproxy = http://127.0.0.1:1\n" +
"[http \"https://gitlab.com/\"]\n\tproxy = http://127.0.0.1:1\n"
func gitEmptyConfigPath() string {
gitEmptyConfigOnce.Do(func() {
f, err := os.CreateTemp("", "git-isolation-config-*")
if err != nil {
panic("create git isolation config: " + err.Error())
}
_, err = f.WriteString("[gc]\n\tauto = 0\n\tautoDetach = false\n[maintenance]\n\tauto = false\n[fetch]\n\twriteCommitGraph = false\n")
content := "[gc]\n\tauto = 0\n\tautoDetach = false\n[maintenance]\n\tauto = false\n[fetch]\n\twriteCommitGraph = false\n"
if os.Getenv(EnvGitHermetic) != "" {
content += hermeticGitConfig
}
_, err = f.WriteString(content)
if err != nil {
panic("write git isolation config: " + err.Error())
}