-
Notifications
You must be signed in to change notification settings - Fork 1.7k
test: E2E tests for Go SDK #3338
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| module github.com/NVIDIA/OpenShell/e2e/go | ||
|
|
||
| go 1.25.13 | ||
|
|
||
| require ( | ||
| github.com/NVIDIA/OpenShell/sdk/go v0.0.0-00010101000000-000000000000 | ||
| github.com/stretchr/testify v1.11.1 | ||
| ) | ||
|
|
||
| require ( | ||
| github.com/davecgh/go-spew v1.1.1 // indirect | ||
| github.com/pmezard/go-difflib v1.0.0 // indirect | ||
| golang.org/x/net v0.58.0 // indirect | ||
| golang.org/x/oauth2 v0.36.0 // indirect | ||
| golang.org/x/sync v0.22.0 // indirect | ||
| golang.org/x/sys v0.47.0 // indirect | ||
| golang.org/x/text v0.41.0 // indirect | ||
| google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect | ||
| google.golang.org/grpc v1.83.2 // indirect | ||
| google.golang.org/protobuf v1.36.11 // indirect | ||
| gopkg.in/yaml.v3 v3.0.1 // indirect | ||
| ) | ||
|
|
||
| replace github.com/NVIDIA/OpenShell/sdk/go => ../../sdk/go |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= | ||
| github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= | ||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
| github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= | ||
| github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= | ||
| github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= | ||
| github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= | ||
| github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= | ||
| github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= | ||
| github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= | ||
| github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= | ||
| github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | ||
| github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||
| github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= | ||
| github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= | ||
| go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= | ||
| go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= | ||
| go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= | ||
| go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= | ||
| go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= | ||
| go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= | ||
| go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= | ||
| go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= | ||
| go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= | ||
| go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= | ||
| go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= | ||
| go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= | ||
| golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= | ||
| golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= | ||
| golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= | ||
| golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= | ||
| golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= | ||
| golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= | ||
| golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= | ||
| golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= | ||
| golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= | ||
| golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= | ||
| gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= | ||
| gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= | ||
| google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= | ||
| google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= | ||
| google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= | ||
| google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= | ||
| google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= | ||
| google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= | ||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,279 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| //go:build e2e | ||
|
|
||
| // Package e2e contains end-to-end tests for the Go SDK, run against a real | ||
| // OpenShell gateway with `mise run e2e:go` (Podman) or | ||
| // `mise run e2e:go:docker` (Docker). | ||
| package e2e | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "os" | ||
| "path/filepath" | ||
| "strings" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
|
|
||
| v1 "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1" | ||
| "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/gateway" | ||
| "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types" | ||
| ) | ||
|
|
||
| var ( | ||
| sharedClient *v1.Client | ||
| gatewayName string | ||
| ) | ||
|
|
||
| func TestMain(m *testing.M) { | ||
| gatewayName = os.Getenv("OPENSHELL_GATEWAY") | ||
| if gatewayName == "" { | ||
| // No gateway configured; requireClient skips every test individually. | ||
| os.Exit(m.Run()) | ||
| } | ||
|
|
||
| cfg, err := gateway.LoadConfig(gatewayName) | ||
| if err != nil { | ||
| fmt.Fprintf(os.Stderr, "e2e: failed to load gateway config: %v\n", err) | ||
| os.Exit(1) | ||
| } | ||
|
|
||
| var opts []gateway.ClientOption | ||
| if opt := mtlsClientOption(cfg.Dir); opt != nil { | ||
| opts = append(opts, opt) | ||
| } | ||
|
|
||
| client, err := gateway.NewClient(gatewayName, opts...) | ||
| if err != nil { | ||
| fmt.Fprintf(os.Stderr, "e2e: failed to build gateway client: %v\n", err) | ||
| os.Exit(1) | ||
| } | ||
| sharedClient = client | ||
|
|
||
| if err := waitForPersistenceReady(sharedClient); err != nil { | ||
| fmt.Fprintf(os.Stderr, "e2e: %v\n", err) | ||
| _ = sharedClient.Close() | ||
| os.Exit(1) | ||
| } | ||
|
|
||
| code := m.Run() | ||
| _ = sharedClient.Close() | ||
| os.Exit(code) | ||
| } | ||
|
|
||
| // mtlsClientOption inspects <dir>/mtls for an on-disk certificate bundle | ||
| // written by e2e_register_mtls_gateway (see e2e/support/gateway-common.sh) | ||
| // and returns a ClientOption applying it. Returns nil when no bundle is | ||
| // present (e.g. plaintext or OIDC e2e lanes), so callers should skip it. | ||
| func mtlsClientOption(dir string) gateway.ClientOption { | ||
| mtlsDir := filepath.Join(dir, "mtls") | ||
| caPath := filepath.Join(mtlsDir, "ca.crt") | ||
| if _, err := os.Stat(caPath); err != nil { | ||
| return nil | ||
| } | ||
|
|
||
| cfg := &types.TLSConfig{CAFile: caPath} | ||
|
|
||
| certPath := filepath.Join(mtlsDir, "tls.crt") | ||
| keyPath := filepath.Join(mtlsDir, "tls.key") | ||
| if _, err := os.Stat(certPath); err == nil { | ||
| if _, err := os.Stat(keyPath); err == nil { | ||
| cfg.CertFile = certPath | ||
| cfg.KeyFile = keyPath | ||
| } | ||
| } | ||
|
|
||
| return gateway.WithTLS(cfg) | ||
| } | ||
|
|
||
| // requireClient returns the shared gateway client, skipping the calling test | ||
| // when OPENSHELL_GATEWAY was not set at process startup. | ||
| func requireClient(t *testing.T) *v1.Client { | ||
| t.Helper() | ||
| if gatewayName == "" { | ||
| t.Skip("OPENSHELL_GATEWAY not set") | ||
| } | ||
| return sharedClient | ||
| } | ||
|
|
||
| // waitForPersistenceReady polls the gateway until its persistence layer is | ||
| // initialized, tolerating the transient errors observed right after the | ||
| // gateway process starts (transport not yet listening, sqlite migrations | ||
| // not yet applied). | ||
| func waitForPersistenceReady(client *v1.Client) error { | ||
| ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) | ||
| defer cancel() | ||
|
|
||
| var lastErr error | ||
| for range 60 { | ||
| _, err := client.Sandboxes().ListAll(ctx, "default", v1.ListOptions{PageSize: 1}) | ||
| if err == nil { | ||
| return nil | ||
| } | ||
| lastErr = err | ||
| if v1.IsUnavailable(err) { | ||
| time.Sleep(2 * time.Second) | ||
| continue | ||
| } | ||
| if strings.Contains(err.Error(), "no such table: objects") { | ||
| time.Sleep(1 * time.Second) | ||
| continue | ||
| } | ||
| return fmt.Errorf("unexpected error waiting for persistence: %w", err) | ||
| } | ||
| return fmt.Errorf("openshell-server persistence is not initialized after 60 attempts: %w", lastErr) | ||
| } | ||
|
|
||
| // uniqueName returns a process-unique resource name so parallel tests never | ||
| // collide on the same sandbox/workspace/provider name. | ||
| func uniqueName(prefix string) string { | ||
| return fmt.Sprintf("%s-%09d", prefix, time.Now().UnixNano()%1_000_000_000) | ||
| } | ||
|
Comment on lines
+133
to
+135
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Notable: Draft RFC 0015 (PR #3238) proposes shared scenario identifiers across SDK E2E suites and a task name of the form e2e:sdk:go. The current task is e2e:go and the tests carry no scenario identifiers. Why this matters: The RFC is not normative yet so no action is required. The file-per-domain structure (sandbox, workspace, providers) aligns with the RFC's proposed groupings, which would ease a later migration. Suggested fix: No fix now. If RFC 0015 is adopted, rename the task and annotate each test with its scenario identifier. Source: architecture agent
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. The current task split predates the RFC contract; it still needs e2e:sdk:go:adapter, Docker-backed e2e:sdk:go, and the e2e:go alias.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Acknowledged, and your read matches mine. Nothing to do here until RFC 0015 is accepted; the task split and scenario identifiers belong in the same change that introduces the adapter contract. |
||
|
|
||
| func requireSandboxResponse(t *testing.T, sandbox *v1.Sandbox, workspace, name string) { | ||
| t.Helper() | ||
| require.NotNil(t, sandbox) | ||
| require.NotEmpty(t, sandbox.ID) | ||
| require.Equal(t, workspace, sandbox.Workspace) | ||
| require.Equal(t, name, sandbox.Name) | ||
| require.False(t, sandbox.CreatedAt.IsZero()) | ||
| require.NotZero(t, sandbox.ResourceVersion) | ||
| } | ||
|
|
||
| func requireReadySandbox(t *testing.T, sandbox *v1.Sandbox, id string) { | ||
| t.Helper() | ||
| require.NotNil(t, sandbox) | ||
| require.Equal(t, id, sandbox.ID) | ||
| require.Equal(t, v1.SandboxReady, sandbox.Status.Phase) | ||
| } | ||
|
|
||
| func readEnvVar(ctx context.Context, client *v1.Client, workspace, sandboxName, key string) (string, error) { | ||
| command := []string{"sh", "-c", `if value=$(printenv "$1"); then printf "%s" "$value"; else printf NOT_SET; fi`, "--", key} | ||
| result, err := client.Exec().Run(ctx, workspace, sandboxName, command, v1.ExecOptions{}) | ||
| // A supervisor reconnect can make one reverse relay miss its opening window. | ||
| // Retry only that transport timeout; repeated failures still fail the test. | ||
| if err != nil && ctx.Err() == nil && v1.IsDeadlineExceeded(err) && strings.Contains(err.Error(), "relay open timed out") { | ||
| select { | ||
| case <-ctx.Done(): | ||
| return "", ctx.Err() | ||
| case <-time.After(250 * time.Millisecond): | ||
| } | ||
| result, err = client.Exec().Run(ctx, workspace, sandboxName, command, v1.ExecOptions{}) | ||
| } | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| if result.ExitCode != 0 { | ||
| return "", fmt.Errorf("exec exited %d: %s", result.ExitCode, string(result.Stderr)) | ||
| } | ||
| if len(result.Stderr) != 0 { | ||
| return "", fmt.Errorf("exec wrote unexpected stderr: %s", string(result.Stderr)) | ||
| } | ||
| return string(result.Stdout), nil | ||
| } | ||
|
|
||
| func isPlaceholderForEnvKey(value, key string) bool { | ||
| const prefix = "openshell:resolve:env:" | ||
| suffix, ok := strings.CutPrefix(value, prefix) | ||
| if !ok { | ||
| return false | ||
| } | ||
| if suffix == key { | ||
| return true | ||
| } | ||
|
|
||
| discriminator, placeholderKey, ok := strings.Cut(suffix, "_") | ||
| if !ok || placeholderKey != key || len(discriminator) < 2 { | ||
| return false | ||
| } | ||
|
|
||
| switch discriminator[0] { | ||
| case 'v': | ||
| for _, char := range discriminator[1:] { | ||
| if char < '0' || char > '9' { | ||
| return false | ||
| } | ||
| } | ||
| return true | ||
| case 's': | ||
| if len(discriminator) != 65 { | ||
| return false | ||
| } | ||
| for _, char := range discriminator[1:] { | ||
| if !((char >= '0' && char <= '9') || (char >= 'a' && char <= 'f')) { | ||
| return false | ||
| } | ||
| } | ||
| return true | ||
| default: | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| func TestIsPlaceholderForEnvKey(t *testing.T) { | ||
| stableHandle := strings.Repeat("a1", 32) | ||
| tests := []struct { | ||
| name string | ||
| value string | ||
| key string | ||
| want bool | ||
| }{ | ||
| {name: "plain", value: "openshell:resolve:env:API_KEY", key: "API_KEY", want: true}, | ||
| {name: "revisioned", value: "openshell:resolve:env:v12_API_KEY", key: "API_KEY", want: true}, | ||
| {name: "stable handle", value: "openshell:resolve:env:s" + stableHandle + "_API_KEY", key: "API_KEY", want: true}, | ||
| {name: "overlapping key suffix", value: "openshell:resolve:env:v12_LONG_API_KEY", key: "API_KEY", want: false}, | ||
| {name: "revision requires digits", value: "openshell:resolve:env:vnext_API_KEY", key: "API_KEY", want: false}, | ||
| {name: "stable handle requires lowercase hex", value: "openshell:resolve:env:s" + strings.ToUpper(stableHandle) + "_API_KEY", key: "API_KEY", want: false}, | ||
| {name: "stable handle requires 64 characters", value: "openshell:resolve:env:sabc_API_KEY", key: "API_KEY", want: false}, | ||
| {name: "wrong key", value: "openshell:resolve:env:v12_OTHER_KEY", key: "API_KEY", want: false}, | ||
| {name: "raw secret", value: "secret", key: "API_KEY", want: false}, | ||
| } | ||
|
|
||
| for _, test := range tests { | ||
| t.Run(test.name, func(t *testing.T) { | ||
| require.Equal(t, test.want, isPlaceholderForEnvKey(test.value, test.key)) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // pollUntil calls fetch until the predicate is satisfied, the local polling | ||
| // timeout expires, or ctx is done, whichever happens first. | ||
| func pollUntil[T any](t *testing.T, ctx context.Context, fetch func() T, predicate func(T) bool) T { | ||
| t.Helper() | ||
| deadline := time.NewTimer(35 * time.Second) | ||
| defer deadline.Stop() | ||
| ticker := time.NewTicker(time.Second) | ||
| defer ticker.Stop() | ||
|
|
||
| for { | ||
| value := fetch() | ||
| if predicate(value) { | ||
| return value | ||
| } | ||
| select { | ||
| case <-ctx.Done(): | ||
| t.Fatalf("context done while polling; last observed %v: %v", value, ctx.Err()) | ||
| case <-deadline.C: | ||
| t.Fatalf("timed out waiting for expected value, last observed %v", value) | ||
| case <-ticker.C: | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // defaultPolicy returns a baseline sandbox policy sufficient for exec-based tests. | ||
| func defaultPolicy() *v1.SandboxPolicy { | ||
| return &v1.SandboxPolicy{ | ||
| Version: 1, | ||
| Filesystem: &v1.FilesystemPolicy{ | ||
| IncludeWorkdir: true, | ||
| ReadOnly: []string{"/usr", "/lib", "/lib64", "/proc", "/etc", "/app", "/dev/urandom"}, | ||
| ReadWrite: []string{"/sandbox", "/tmp", "/dev/null"}, | ||
| }, | ||
| Landlock: &v1.LandlockPolicy{Compatibility: "best_effort"}, | ||
| Process: &v1.ProcessPolicy{RunAsUser: "10001", RunAsGroup: "10001"}, | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Important: waitForPersistenceReady uses context.Background() with no deadline, so a hung gRPC call blocks TestMain indefinitely.
Why this matters: Line 105 creates ctx := context.Background(). The 60-iteration attempt cap on line 107 only guards against repeated short failures. If a single ListAll call hangs (TCP socket open but gateway process stuck), the loop blocks forever because the context has no deadline. This runs in TestMain before any test executes, so it blocks the whole binary with no diagnostics.
Suggested fix: ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute); defer cancel()
Source: production agent
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a three-minute deadline to gateway persistence readiness checks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Confirmed. harness_test.go:108 now wraps the readiness loop in context.WithTimeout(context.Background(), 3*time.Minute), so a hung ListAll can no longer block TestMain.