Skip to content

test: E2E tests for Go SDK - #3338

Open
jiripetrlik wants to merge 1 commit into
NVIDIA:mainfrom
jiripetrlik:go-sdk-e2e-tests
Open

jiripetrlik wants to merge 1 commit into
NVIDIA:mainfrom
jiripetrlik:go-sdk-e2e-tests

Conversation

@jiripetrlik

Copy link
Copy Markdown

Summary

This PR adds E2E tests for Go SDK. New E2E Go SDK test suite should be able to catch issues with:

  • protocol schema mismatches between the Go SDK and the actual gateway build
  • sandbox lifecycle can be managed by the Go SDK
  • provider credentials management using the Go SDK
  • workspace management

This PR should replace: jiripetrlik#1

Related Issue

Changes

  • Add E2E tests for Go SDK
  • Update Github actions to run Go SDK E2E tests

Testing

  • [*] mise run pre-commit passes
  • [] Unit tests added/updated - issue is about adding E2E tests. No unit tests are needed
  • [*] E2E tests added/updated (if applicable)

Checklist

  • [*] Follows Conventional Commits
  • [*] Commits are signed off (DCO)
  • [*] Architecture docs updated (if applicable)

@copy-pr-bot

copy-pr-bot Bot commented Sep 15, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@jiripetrlik

Copy link
Copy Markdown
Author

Hello @rhuss , I've tried to fix your comments related to assertion depth from #3028 (comment) and rebased PR. Can you please take a look again?

@rhuss rhuss left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc-review Summary

What Went Well

  • The response-contract helpers are the real fix from the last round. requireProviderResponse (providers_test.go:38-47) and requireWorkspaceResponse (workspace_test.go:18-25) assert the full field set, including that credentials come back empty, which is exactly what the earlier review asked for.
  • attachProviderRetry and detachProviderRetry (providers_test.go:92-131) handle optimistic-concurrency conflicts by re-fetching ResourceVersion, rather than assuming the first write wins.
  • Test isolation is sound: uniqueName (harness_test.go:128-130) plus consistent t.Cleanup registration makes t.Parallel() safe, and cleanup detaches providers before deleting sandboxes so no credential material is left attached.
  • TestProviderCRUDResponseContract (providers_test.go:259-311) exercises Create, Get, Update and ListAll in sequence and checks ResourceVersion monotonicity at line 298.
  • A correctness pass over every SDK call against the signatures in sdk/go/openshell/v1/ found no misuse: no context lifetime errors, no resource leaks on failure paths, correct cleanup ordering under LIFO t.Cleanup.
  • The credential assertion tests a real security boundary correctly. Checked against crates/openshell-core/src/secrets.rs, the suite genuinely fails if the gateway returns a raw secret, an empty value, or an encoded copy.

Findings

Severity File Description Source
Important e2e/go/harness_test.go:104 waitForPersistenceReady uses context.Background() with no deadline production
Important e2e/go/providers_test.go:238 AttachProviderResult.Sandbox and DetachProviderResult.Sandbox never asserted test-quality
Important e2e/go/sandbox_lifecycle_test.go:27 No sandbox error-path tests (NotFound, AlreadyExists) test-quality, goal-alignment
Important e2e/go/sandbox_lifecycle_test.go:30 Sandbox Create under-asserted vs the workspace and provider helpers test-quality
Important e2e/go/sandbox_lifecycle_test.go:51 No failing-command exec test goal-alignment
Important tasks/test.toml:64 e2e:go puts a Podman requirement into the Docker-only e2e aggregate production, architecture, goal-alignment
Important tasks/test.toml:64 PR claims GitHub Actions were updated, but no .github/ file is touched goal-alignment, codex
Important tasks/test.toml:140 No -timeout on go test production, goal-alignment
Minor (10 findings) helper placement, poll-helper duplication, stale comments, retry diagnostics, stderr and placeholder-matching details various
Notable (4 findings) Spec round-trip and uncovered SDK surfaces, stable-handle placeholder form, RFC 0015 divergence various

Four of these were raised in the review of the predecessor implementation and are still open: the failing-exec test, the sandbox error-path tests, the Podman and Docker mismatch in the aggregate, and the missing -timeout. The one item that was fixed, discarded response values, was fixed thoroughly for providers and workspaces. The sandbox tests did not get the same treatment.

Not posted inline (outside the diff hunks):

  • tasks/test.toml:4 and the [test] description on line 7 still read "Rust + Python + TypeScript SDK" and do not mention Go, although the [e2e] description on line 64 was updated.
  • mise.lock removes the sccache linux-x64 platform entry (5 lines). This is unrelated to the Go E2E work and undeclared. It looks like lock file churn from running mise on an ARM host; please confirm it was intentional.

Review Details

  • Findings posted: 20 inline, 2 summary only
  • Gate outcome: FAIL (8 Important, 0 Critical)
  • Participating agents: correctness, architecture, security, production, test-quality, goal-alignment, codex, coderabbit
  • Meta-review: 22 findings verified by a skeptic pass; 0 false positives removed, 6 marked weak, 1 severity raised. CodeRabbit reported no findings.

Comment thread e2e/go/harness_test.go
Comment on lines +104 to +124
func waitForPersistenceReady(client *v1.Client) error {
ctx := context.Background()
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)
}

Copy link
Copy Markdown
Contributor

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

Copy link
Copy Markdown
Author

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.

Copy link
Copy Markdown
Contributor

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.

Comment thread e2e/go/providers_test.go
Comment on lines +238 to +249
attachResult := attachProviderRetry(ctx, t, client, "default", sandboxName, providerName)
require.True(t, attachResult.Attached)

value = pollUntil(t, ctx, func() string {
v, err := readEnvVar(ctx, client, "default", sandboxName, "NVIDIA_API_KEY")
require.NoError(t, err)
return v
}, func(v string) bool { return v != "NOT_SET" })
require.True(t, isPlaceholderForEnvKey(value, "NVIDIA_API_KEY"), "expected a placeholder, got %q", value)

detachResult := detachProviderRetry(ctx, t, client, "default", sandboxName, providerName)
require.True(t, detachResult.Detached)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important: AttachProviderResult.Sandbox and DetachProviderResult.Sandbox are never asserted. Both tests check only the boolean flag (Attached, Detached).

Why this matters: A gateway returning Attached: true with a nil or zero-valued Sandbox would pass. The embedded Sandbox is the primary mechanism for clients to observe post-mutation state (updated provider list, new resource version), so serialization bugs in the attach and detach response path cannot be detected.

Suggested fix: After attachProviderRetry assert NotNil(attachResult.Sandbox), Equal(sandbox.ID, attachResult.Sandbox.ID) and that the provider list contains the attached provider. Make the symmetric assertions after detach.

Source: test-quality agent

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added assertions for the returned sandbox identity and provider attachment state.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, and you went further than the comment asked. Both results now run through requireSandboxResponse, assert the sandbox identity against the created ID, and check Spec.Providers membership with Contains after attach and NotContains after detach. That covers the post-mutation state the boolean flag was hiding.

Comment on lines +27 to +66
sb, err := client.Sandboxes().Create(ctx, "default", name, &v1.SandboxSpec{
Policy: defaultPolicy(),
}, nil)
require.NoError(t, err)
t.Cleanup(func() {
cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cleanupCancel()
require.NoError(t, client.Sandboxes().Delete(cleanupCtx, "default", name))
})
require.NotEmpty(t, sb.ID)
require.Equal(t, name, sb.Name)

ready, err := client.Sandboxes().WaitReady(ctx, "default", name)
require.NoError(t, err)
requireReadySandbox(t, ready, sb.ID)

fetched, err := client.Sandboxes().Get(ctx, "default", name)
require.NoError(t, err)
require.Equal(t, sb.ID, fetched.ID)

sandboxes, err := client.Sandboxes().ListAll(ctx, "default", v1.ListOptions{PageSize: 100})
require.NoError(t, err)
require.True(t, containsID(sandboxes, sb.ID))

result, err := client.Exec().Run(ctx, "default", name, []string{"sh", "-c", "printf sandbox-ok"}, v1.ExecOptions{})
require.NoError(t, err)
require.Equal(t, 0, result.ExitCode)
require.Equal(t, "sandbox-ok", string(result.Stdout))

// Exec launches share the same sandbox filesystem across calls.
writeResult, err := client.Exec().Run(ctx, "default", name,
[]string{"sh", "-c", "echo persisted > /sandbox/exec-persistence.txt"}, v1.ExecOptions{})
require.NoError(t, err)
require.Equal(t, 0, writeResult.ExitCode)

readResult, err := client.Exec().Run(ctx, "default", name,
[]string{"cat", "/sandbox/exec-persistence.txt"}, v1.ExecOptions{})
require.NoError(t, err)
require.Equal(t, 0, readResult.ExitCode)
require.Equal(t, "persisted\n", string(readResult.Stdout))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important: No error-path tests for sandbox operations: Get on a nonexistent sandbox (NotFound), Create with a duplicate name (AlreadyExists), Delete on a nonexistent sandbox. The workspace tests cover both NotFound cases.

Why this matters: The SDK error helpers IsNotFound and IsAlreadyExists depend on correct gRPC status mapping through the converter layer. If the gateway returns a different status for a sandbox 404 than for a workspace 404, the current suite would not catch it. Flagged as R3 in the prior review and unresolved.

Suggested fix: Add TestSandboxGetNonexistentNotFound and TestSandboxDeleteNonexistentNotFound mirroring the workspace tests; optionally TestSandboxCreateDuplicateAlreadyExists.

Source: test-quality agent (also flagged by: goal-alignment)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added NotFound coverage for get/delete and AlreadyExists coverage for duplicate creation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed. TestSandboxGetNonexistentNotFound, TestSandboxDeleteNonexistentNotFound and TestSandboxCreateDuplicateAlreadyExists now give the sandbox path the same error-classification coverage the workspace tests had.

Comment thread e2e/go/sandbox_lifecycle_test.go Outdated
Comment on lines +30 to +37
require.NoError(t, err)
t.Cleanup(func() {
cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cleanupCancel()
require.NoError(t, client.Sandboxes().Delete(cleanupCtx, "default", name))
})
require.NotEmpty(t, sb.ID)
require.Equal(t, name, sb.Name)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important: Sandbox Create response is under-asserted relative to the workspace and provider equivalents. Only ID (non-empty) and Name are checked; Workspace, CreatedAt and ResourceVersion are not.

Why this matters: A gateway returning an empty Workspace, zero CreatedAt or zero ResourceVersion on sandbox Create would pass every test. For a suite whose stated purpose is catching protocol schema mismatches, leaving the primary resource type less validated than the secondary ones is a real gap. requireProviderResponse and requireWorkspaceResponse prove the pattern already exists in this same suite.

Suggested fix: Add requireSandboxResponse(t, sandbox, workspace, name) asserting NotNil, NotEmpty(ID), Equal(workspace, Workspace), Equal(name, Name), False(CreatedAt.IsZero()), NotZero(ResourceVersion). Use it after Create and Get in TestSandboxCRUDAndExec and after each Create in TestSandboxListScopedAndAllWorkspaces.

Source: test-quality agent

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added shared assertions for ID, workspace, name, creation time, and resource version.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed. requireSandboxResponse in harness_test.go:137 asserts ID, workspace, name, CreatedAt and ResourceVersion, and it is applied after both Create and Get. The sandbox response contract now matches the provider and workspace helpers.

Comment on lines +51 to +54
result, err := client.Exec().Run(ctx, "default", name, []string{"sh", "-c", "printf sandbox-ok"}, v1.ExecOptions{})
require.NoError(t, err)
require.Equal(t, 0, result.ExitCode)
require.Equal(t, "sandbox-ok", string(result.Stdout))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important: Exec is tested only on the happy path. No test runs a failing command and asserts a non-zero ExitCode or stderr content.

Why this matters: Without a failing-command test the suite cannot detect regressions where the gateway swallows non-zero exit codes or drops stderr. Flagged as R2 in the prior review and unresolved.

Suggested fix: Add a case running sh -c 'echo err >&2; exit 42' asserting ExitCode == 42 and that stderr contains the sentinel.

Source: goal-alignment agent

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a nonzero-exit execution test. I still need to require exact stderr and enable NoLoginShell for RFC-0015 compliance.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed for the exit code and stdout: the new case asserts exit 42, empty stdout and the stderr sentinel. Agreed on your note that exact stderr equality and NoLoginShell should wait for the RFC 0015 exec definition, which now pins tty=false and no_login_shell=true. Worth doing in the same change that aligns the adapter.

Comment on lines +51 to +54
result, err := client.Exec().Run(ctx, "default", name, []string{"sh", "-c", "printf sandbox-ok"}, v1.ExecOptions{})
require.NoError(t, err)
require.Equal(t, 0, result.ExitCode)
require.Equal(t, "sandbox-ok", string(result.Stdout))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: Exec success results never assert that Stderr is empty. All exec calls check ExitCode and Stdout but discard Stderr (lines 51-54, 58-60, 62-66).

Why this matters: A gateway that erroneously includes error output in a successful execution would pass undetected. Note that this assertion is only well defined for non-TTY exec, since a PTY merges the two streams.

Suggested fix: Add require.Empty(t, result.Stderr) after each successful exec assertion, and confirm the exec runs without a TTY.

Source: test-quality agent

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added empty-stderr assertions to successful execution paths.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed. require.Empty on Stderr is present on all three successful exec paths.

Comment on lines +227 to +244
func elementsMatch(a, b []string) bool {
if len(a) != len(b) {
return false
}
counts := make(map[string]int, len(a))
for _, v := range a {
counts[v]++
}
for _, v := range b {
counts[v]--
}
for _, c := range counts {
if c != 0 {
return false
}
}
return true
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: elementsMatch hand-rolls unordered set equality by map counting.

Why this matters: Go 1.25 provides slices.Sorted and slices.Equal in the standard library, which would replace the counting logic with a one-liner and remove the surface for off-by-one bugs. It exists because the poll helper needs a boolean predicate rather than testify's fatal require.ElementsMatch.

Suggested fix: Use slices.Equal(slices.Sorted(slices.Values(a)), slices.Sorted(slices.Values(b))), or fold it into the generic pollUntil[T].

Source: architecture agent

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced the counting implementation with sorted slice comparison.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed. slices.Equal(slices.Sorted(slices.Values(a)), slices.Sorted(slices.Values(b))) replaces the map counting.

Comment thread e2e/go/harness_test.go
Comment on lines +128 to +130
func uniqueName(prefix string) string {
return fmt.Sprintf("%s-%09d", prefix, time.Now().UnixNano()%1_000_000_000)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

Comment thread e2e/go/providers_test.go Outdated
Comment on lines +23 to +33
func isPlaceholderForEnvKey(value, key string) bool {
const prefix = "openshell:resolve:env:"
if value == prefix+key {
return true
}
token, ok := strings.CutPrefix(value, prefix)
if !ok {
return false
}
return strings.HasPrefix(token, "v") && strings.HasSuffix(token, "_"+key)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notable: isPlaceholderForEnvKey does not recognize the stable-handle placeholder variant openshell:resolve:env:s<64 lowercase hex>_KEY defined in crates/openshell-core/src/secrets.rs (split_stable_env_key, lines 802-815).

Why this matters: The security invariant is preserved: a stable-handle placeholder makes the test fail rather than silently pass, so a leaked secret is still caught. But if the gateway begins issuing stable handles for refresh-managed credentials, these tests produce false failures with a misleading diagnostic that does not indicate the value is a valid placeholder in another format.

Suggested fix: Extend isPlaceholderForEnvKey to accept the stable-handle form, ideally with one compiled regexp covering all three variants: ^openshell:resolve:env:(?:KEY|v[0-9]+_KEY|s[0-9a-f]{64}_KEY)$

Source: security agent

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added strict support for the s<64-lowercase-hex>_ placeholder form.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed. The s<64 lowercase hex>_KEY form is now accepted with a strict length and hex check, which matches split_stable_env_key in crates/openshell-core/src/secrets.rs. The separate TestIsPlaceholderForEnvKey unit test is a good addition, since the matcher is now the security assertion's load-bearing piece.

Comment thread e2e/go/sandbox_lifecycle_test.go Outdated
Comment on lines +27 to +37
sb, err := client.Sandboxes().Create(ctx, "default", name, &v1.SandboxSpec{
Policy: defaultPolicy(),
}, nil)
require.NoError(t, err)
t.Cleanup(func() {
cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cleanupCancel()
require.NoError(t, client.Sandboxes().Delete(cleanupCtx, "default", name))
})
require.NotEmpty(t, sb.ID)
require.Equal(t, name, sb.Name)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notable: Sandbox Spec round-trip is never verified, and several SDK surfaces are uncovered: Stop, Start, WaitStopped, Watch, GetLogs, ListProviders, Exec Stream and Interactive, and the paginated List path.

Why this matters: Sandboxes are created with defaultPolicy() containing filesystem, landlock and process policy, but the returned or fetched Spec.Policy is never compared. Policy serialization across the gRPC boundary is exactly the kind of schema mismatch this suite exists to catch. These are scope decisions rather than bugs and are reasonable for a first E2E PR.

Suggested fix: Consider a TestSandboxSpecRoundTrip that creates with a detailed policy, fetches via Get and deep-compares the Spec. Track the remaining interface methods and paginated List as follow-up coverage.

Source: test-quality agent

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added policy round-trip assertions and stronger CRUD coverage. Scenario IDs, coverage reporting, scavenging, and remaining RFC error cases are still outstanding.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed for the round trip: the policy is compared against Spec.Policy after both Create and Get, which is the schema-mismatch case I cared about most. The remaining surfaces (Stop, Start, Watch, GetLogs, paginated List, Exec streaming) stay as follow-up coverage, and scenario IDs, coverage reporting and scavenging belong with the RFC 0015 alignment.

@jiripetrlik

Copy link
Copy Markdown
Author

Hello @rhuss , thank you for your review. I've tried to fix all the issues. See comments for individual problems. Here is also how I've fixed additional comments:

  1. Test tasks. [test] remains unchanged because it does not run Go; go:ci is a separate dependency of ci.)
    Updated the file-level header. I left the [test] description unchanged because that task does not execute Go tests; the full ci task runs go:ci separately.
  2. The removal was unintended ARM-host lockfile churn. I restored the Linux x64 sccache entry.

@rhuss

rhuss commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Second pass on 83132744. All 20 inline findings are addressed and verified against the code, and both summary-only items are handled: mise.lock no longer differs from the base, and the tasks/test.toml header is updated with a reasonable argument for leaving the [test] description alone.

Three of the fixes are worth calling out because they went past what was asked.

The Podman and Docker split landed in the right shape: e2e:go:docker joins the aggregate while e2e:go stays a standalone Podman lane, mirroring how e2e:podman already sits outside it. The CI wiring actually takes effect, which was the part most likely to go wrong silently. branch-e2e.yml:202-213 calls the Docker workflow without overriding suite-matrix, so the new go entry is used, and Go 1.26.7 is locked for linux-arm64 in mise.lock to match the runner. And the placeholder matcher is now the strict parser the credential assertion deserves, with its own unit test.

Beyond the findings, the attach and detach assertions now check Spec.Providers membership in both directions, and the policy round trip is compared after both Create and Get. Those close the schema-mismatch gap the suite exists for.

The three items you deferred are the right ones to defer, and I agree with your reasoning on each: exact stderr with NoLoginShell, removing the conflict retries, and scenario identifiers with coverage reporting. All three are RFC 0015 alignment rather than defects in this PR, and RFC 0015 now pins tty = false and no_login_shell = true for exec scenarios, so the exec item has a definition to code against once that lands.

Nothing blocking remains from my side. I cannot resolve review threads on this repository, so the 20 threads need resolving on your end. After that this needs a vetter to comment /ok to test so the gated lanes, including the new Go suite, actually run.

@rhuss

rhuss commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

@mrunalp @sjenning @krishicks I think we are good to go with this PR (along with the associated RFC), to at least run it through the CI pipeline. It really would help to keep the Golang SDK stable over the long run and detect regressions earlier. (i'm off next week, therefor this eager ping :-)

Signed-off-by: Jiri Petrlik <jpetrlik@redhat.com>
@jiripetrlik

Copy link
Copy Markdown
Author

Hello @mrunalp , @sjenning , @krishicks , can you please take a look at this PR? I've rebased it with current main. It will help us to better test Go SDK using E2E tests.

@rhuss

rhuss commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

would be really great if we could integrate the tests, either now or when we moved to the CNFC repos (under another GitHub org then, I guess). @jiripetrlik wdyt, would it be better to wait to this move (should happen quite soon, now that OpenShell has been accepted for the CNCF sandbox) or does it not really matter?

@jiripetrlik

Copy link
Copy Markdown
Author

@rhuss I would prefer to merge soon. It is open for quite a long time and I don't want it to deteriorate.

@sjenning sjenning added the test:e2e Requires end-to-end coverage label Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

Label test:e2e applied, but pull-request/3338 does not exist yet. A maintainer needs to comment /ok to test 00cd9ae9f94c5b9f2aaf1705ce51ad1d36b20041 to mirror this PR. Once the mirror exists, re-apply the label or re-run Branch E2E Checks from the Actions tab.

@sjenning

sjenning commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator

/ok to test 00cd9ae

@jiripetrlik

Copy link
Copy Markdown
Author

Thank you @sjenning for running the PR checks. Everything looks green, so I think this is ready for review. Could you please take a look?

@github-actions

Copy link
Copy Markdown

This pull request has had no activity for 14 days and is now marked stale.

@github-actions github-actions Bot added the state:stale Inactive item at risk of automatic closure. label Oct 24, 2026
@rhuss

rhuss commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

@jiripetrlik Sorry for the long delay on this one. Things should get better once the Go SDK has its own repository, and I'm starting that migration this week.

I re-checked my 20 review threads against the rebased head 00cd9ae (agent-assisted pass). 19 are fixed, and the RFC 0015 naming note is fine to handle with the adapter alignment, as you suggested. Each thread's earlier confirmation still holds. I don't have permission to resolve review threads in this repo, so could you resolve them on your side? From my side, this is ready for a maintainer review.

@rhuss

rhuss commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

I would have another round of reviews with an improved team of agents, so I post it here. Not sure if we should wait until the migration, but you might point your agent on this to check.

@rhuss rhuss left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc-review Summary

New round with fresh agent eyes on 00cd9ae, the squashed head since my September round. The earlier threads are addressed. The coverage gaps still open from that round (stop/start, exec options and streaming) and the planned removal of the conflict retries per RFC 0015 stay in their existing threads, so I don't repeat them here. These findings come from an agent-assisted review; the failure scenarios were derived from the code, not executed.

What Went Well

  • The write-only credential contract is pinned from both sides: requireProviderResponse asserts an empty Spec.Credentials on every Create/Get/Update/List response, and TestAttachDetachProviderUpdatesCredentials checks from inside the sandbox that the variable is NOT_SET before attach, a placeholder (never the raw secret) after attach, and NOT_SET again after detach (e2e/go/providers_test.go).
  • isPlaceholderForEnvKey parses strictly (digit-only revision, 64 lowercase hex stable handle, exact key match), and the table-driven TestIsPlaceholderForEnvKey covers the overlapping-suffix case v12_LONG_API_KEY vs API_KEY (e2e/go/harness_test.go).
  • readEnvVar retries only the relay-open DeadlineExceeded, once and only while the parent ctx is live, and passes the env key as a positional argument to sh -c instead of interpolating it into the script (e2e/go/harness_test.go).
  • TestSandboxListScopedAndAllWorkspaces checks workspace scoping in both directions plus inclusion under AllWorkspaces, and TestSandboxCRUDAndExec checks the non-zero exit path precisely (exit code 42, empty stdout, stderr marker) (e2e/go/sandbox_lifecycle_test.go).
  • TestMain reuses gateway.LoadConfig/gateway.NewClient and mtlsClientOption adds the on-disk mTLS bundle only when present, so the suite takes the same SDK connection path as users across plaintext, OIDC and mTLS lanes (e2e/go/harness_test.go).
  • The separate e2e/go/go.mod with a replace onto ../../sdk/go tests the in-tree SDK while keeping testify and other e2e-only dependencies out of the SDK module.

Findings

Severity File Description Source
Minor e2e/go/harness_test.go:33 Missing OPENSHELL_GATEWAY turns into skips, so the e2e lane can go green with no coverage gator
Minor e2e/go/harness_test.go:131 uniqueName comment claims process-unique names; wall-clock nanos can collide architecture
Notable e2e/go/harness_test.go:217 TestIsPlaceholderForEnvKey is a unit test behind the e2e tag, so only gateway lanes run it architecture
Minor e2e/go/harness_test.go:245 No per-attempt timeout in pollUntil/waitForPersistenceReady; one hung RPC eats 3 minutes production
Minor e2e/go/providers_test.go:51 detachProviderBestEffort drops a Conflict, so the provider delete in cleanup can fail correctness
Minor e2e/go/providers_test.go:289 Provider update with nil Credentials never checks the stored credential survived test-quality
Minor e2e/go/sandbox_lifecycle_test.go:117 require assertions run before t.Cleanup is registered, leaking resources on failure correctness
Minor e2e/go/sandbox_lifecycle_test.go:125 Duplicate-create test does not check the existing sandbox stayed unchanged test-quality
Minor e2e/go/sandbox_lifecycle_test.go:148 Cleanups discard delete errors and the workspace drain gives up silently after 20s test-quality
Minor e2e/go/sandbox_lifecycle_test.go:155 Sandbox cleanup block copied into ~8 closures with different error policies architecture
Minor e2e/go/sandbox_lifecycle_test.go:158 Sandbox Delete errors in cleanup discarded with _, _ =, leaking sandboxes silently correctness
Notable e2e/go/workspace_test.go:27 Workspace tests skip the membership API (AddMember, RemoveMember, ListMembers) goal-alignment
Minor tasks/test.toml:164 e2e/go module is outside the Go lint and format gates (go:lint, go:format:check) architecture

All findings are posted as inline comments. Three of them (on sandbox_lifecycle_test.go lines 148, 155 and 158) look at the same cleanup error handling from different angles, so one helper with a single error policy would address all three.

Review Details

  • Findings posted: 13
  • Findings reviewed and not posted: 6
  • Gate outcome: PASS (no Critical or Important findings)
  • Participating reviewers: architecture, correctness, gator, goal-alignment, production, test-quality

Comment thread e2e/go/harness_test.go
)

func TestMain(m *testing.M) {
gatewayName = os.Getenv("OPENSHELL_GATEWAY")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: TestMain turns a missing OPENSHELL_GATEWAY into per-test skips. The e2e:go and e2e:go:docker lanes run go test without -v, so if the wrapper ever stops exporting the variable, the lane still reports a green ok with no gateway coverage at all.

Why this matters: An e2e lane should fail when it cannot reach the system under test. With the skip in requireClient (lines 95-101), the job prints only ok github.com/NVIDIA/OpenShell/e2e/go whether 16 tests ran or all of them were skipped; only TestIsPlaceholderForEnvKey always runs. This is not reachable today, since both wrappers export the variable (with-docker-gateway.sh:708, with-podman-gateway.sh:645). A later change to either wrapper, or a manual go test -tags=e2e ./..., would silently drop the Go SDK coverage from a required CI job.

Suggested fix: The file is already gated by the e2e build tag, so fail fast instead of skipping. In TestMain:

if gatewayName == "" {
    fmt.Fprintln(os.Stderr, "e2e: OPENSHELL_GATEWAY must be set (run via mise run e2e:go)")
    os.Exit(1)
}

and drop the t.Skip branch in requireClient. Adding -v to the go test call would at least make the skips visible in CI logs. Reproducer: cd e2e/go && env -u OPENSHELL_GATEWAY go test -tags=e2e ./... should print ok and exit 0 although no gateway test ran.

Source: gator agent

Comment thread e2e/go/harness_test.go
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: The doc comment says uniqueName returns a "process-unique" name, but the value is wall-clock UnixNano modulo 1e9. That is neither monotonic nor guaranteed unique, so the comment promises more than the code delivers.

Why this matters: Two calls with the same prefix in the same clock tick return the same name. macOS reports time at microsecond resolution, so UnixNano values move in steps of 1000. Parallel subtests or a future helper that calls uniqueName("sb-crud") twice could collide, although the comment says that cannot happen.

Suggested fix: Append a package-level atomic counter, for example fmt.Sprintf("%s-%d-%d", prefix, time.Now().UnixNano()%1e9, seq.Add(1)), so the comment becomes true. Otherwise reword the comment to say the name is probabilistically unique.

Source: architecture agent

Comment thread e2e/go/harness_test.go
}
}

func TestIsPlaceholderForEnvKey(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notable: TestIsPlaceholderForEnvKey is a pure unit test of a parsing helper, but it lives in the harness file behind the e2e build tag. It only runs through the gateway-booting e2e:go tasks and never in the fast unit-test path.

Why this matters: If a change to the placeholder grammar (the v<digits> and s<64 hex> discriminators) breaks isPlaceholderForEnvKey, the table test that would catch it runs only after a full Docker or Podman gateway has booted. go:test and go:ci never run it.

Suggested fix: Consider moving the parser and its test into a file without the build tag, or into sdk/go if the SDK should own placeholder recognition.

Source: architecture agent

Comment thread e2e/go/harness_test.go

// 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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: The 35s deadline in pollUntil and the attempt loop in waitForPersistenceReady only take effect between calls. Each fetch or ListAll call runs on the caller's long-lived context with no per-attempt timeout, so a single hung RPC blocks for the full outer context (3 minutes) and is then misreported.

Why this matters: In providers_test.go:235, pollUntil calls readEnvVar(ctx, ...) with the test's 3-minute ctx. If the exec relay hangs (for example during a supervisor reconnect while attaching), Exec().Run blocks until that ctx expires, and the 35s timer at harness_test.go:247 is never selected. The require.NoError inside the fetch closure then fails with DeadlineExceeded, which hides where the hang happened. waitForPersistenceReady (harness_test.go:112-127) has the same shape: a ListAll call against a gateway that accepts connections but never answers uses the whole budget, and because DeadlineExceeded is not IsUnavailable, it reports "unexpected error waiting for persistence" instead of a readiness timeout.

Suggested fix: Derive a short per-attempt context inside each fetch or ListAll attempt, for example context.WithTimeout(ctx, 10*time.Second), and check ctx.Err() before classifying the error.

Source: production agent

Comment thread e2e/go/providers_test.go
// synchronously first.
func detachProviderBestEffort(ctx context.Context, client *v1.Client, workspace, sandboxName, providerName string) {
if current, err := client.Sandboxes().Get(ctx, workspace, sandboxName); err == nil {
_, _ = client.Sandboxes().DetachProvider(ctx, workspace, sandboxName, providerName, current.ResourceVersion)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: detachProviderBestEffort makes a single detach attempt and discards its error. An optimistic-concurrency conflict therefore leaves the provider attached, and the later provider delete in cleanup fails with FailedPrecondition.

Why this matters: In the TestProviderCredentialsAvailableAsEnvVar cleanup, Get returns resource version N, a sandbox status update bumps it to N+1 before DetachProvider runs, and the resulting Conflict is dropped by _, _ =. Sandbox Delete removes the store row asynchronously, so when deleteProviderIgnoreNotFound calls DeleteProvider, delete_provider_record_with_credentials (crates/openshell-server/src/grpc/provider.rs:595-600) still finds the sandbox in sandboxes_using_provider and returns failed_precondition. That is not NotFound, so require.NoError fails the test during cleanup.

Suggested fix: The file already handles this race with attachProviderRetry/detachProviderRetry (50 attempts on IsConflict). Reuse a conflict-retrying detach in the cleanup path, or retry the provider delete on FailedPrecondition until the sandbox row is gone.

Source: correctness agent

// sandbox still references it, so wait for the workspace to drain
// before attempting the delete.
waitForWorkspaceSandboxesGone(cleanupCtx, client, otherWorkspace)
_, _ = client.Workspaces().Delete(cleanupCtx, otherWorkspace)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: Several cleanups throw away delete errors with _, _ =, and the workspace drain gives up silently after 20s, so sandboxes and workspaces can leak on the shared gateway without anyone noticing.

Why this matters: TestSandboxListScopedAndAllWorkspaces (lines 148, 158, 168) and TestSandboxLabelsAndSelectors (lines 211, 225) ignore Delete errors. waitForWorkspaceSandboxesGone returns quietly when its 20s deadline passes, and the workspace Delete after it is unchecked as well. Other tests in this file (lines 37, 121) and in workspace_test.go use require.NoError in cleanup. If sandbox deletion is slow or fails under -parallel 5, the extra workspace and its sandboxes stay on the gateway, the run reports green, and the leftovers pile up in later runs against a reused gateway.

Suggested fix: In cleanup, report anything other than IsNotFound with t.Errorf, and have waitForWorkspaceSandboxesGone log through t.Logf (or t.Errorf) when the deadline passes.

Source: test-quality agent

defaultSandbox, err := client.Sandboxes().Create(ctx, "default", defaultName, &v1.SandboxSpec{}, nil)
require.NoError(t, err)
requireSandboxResponse(t, defaultSandbox, "default", defaultName)
t.Cleanup(func() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: The sandbox cleanup block (a fresh context.WithTimeout, defer cancel, then Sandboxes().Delete) is copied into roughly eight t.Cleanup closures, and the copies follow different error policies. Some assert with require.NoError (lines 34-39, 118-123, providers_test.go:146-152), others discard the error with _, _ = (lines 155-159, 165-169, 205-226).

Why this matters: When a sandbox delete fails during cleanup, for example with a transient Unavailable error, TestSandboxCRUDAndExec and TestSandboxCreateDuplicateAlreadyExists fail, while TestSandboxListScopedAndAllWorkspaces and TestSandboxLabelsAndSelectors leak the sandbox without any signal. A reader cannot tell whether the difference is intentional.

Suggested fix: Add one cleanupSandbox(t, client, workspace, name) helper in harness_test.go, mirroring deleteProviderIgnoreNotFound, that owns the cleanup timeout and a single error policy (for example, ignore NotFound and fail on everything else), and use it everywhere.

Source: architecture agent

t.Cleanup(func() {
cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cleanupCancel()
_, _ = client.Sandboxes().Delete(cleanupCtx, "default", defaultName)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: Sandbox Delete errors in cleanup are discarded with _, _ = and nothing explains why, so a failed cleanup leaks sandboxes silently. Other tests in the same file assert NoError on the same call.

Why this matters: In TestSandboxListScopedAndAllWorkspaces (lines 158, 168) and TestSandboxLabelsAndSelectors (lines 211, 225), cleanup calls client.Sandboxes().Delete and drops the result. If the delete fails, for example with Unavailable or a cleanupCtx deadline, the test still passes and the sandbox stays on the gateway. In the workspace test, the workspace delete at line 148 then fails too, and that error is discarded as well. TestSandboxCRUDAndExec (line 37) and TestSandboxCreateDuplicateAlreadyExists (line 121) call require.NoError on the same cleanup.

Suggested fix: Ignore only NotFound, as deleteProviderIgnoreNotFound does, or pass DeleteOptions{AllowMissing: true}, and assert every other error.

Source: correctness agent

Comment thread e2e/go/workspace_test.go
require.NotZero(t, workspace.ResourceVersion)
}

func TestWorkspaceCRUD(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notable: The workspace management tests cover Create, Get, ListAll and Delete, but not the SDK's membership API (AddMember, RemoveMember, ListMembers in sdk/go/openshell/v1/workspace_client.go:98-168), and the PR does not say whether this is deliberate.

Why this matters: The PR says the suite catches protocol mismatches for workspace management, yet a schema change to the member RPCs (role enum, member identity fields) would go unnoticed.

Suggested fix: Membership may need an OIDC lane, which the Docker e2e lane does not run. If so, limit the goal in the PR description to workspace CRUD, or add a member test to an OIDC-enabled lane.

Source: goal-alignment agent

Comment thread tasks/test.toml
"OPENSHELL_E2E_DOCKER_SANDBOX_IMAGE=openshell/e2e-python:dev OPENSHELL_E2E_DOCKER_SANDBOX_IMAGE_PULL_POLICY=never e2e/with-docker-gateway.sh uv run pytest -o python_files='test_*.py *_test.py' -m 'not gpu' -n ${E2E_PARALLEL:-5} e2e/python",
]

["e2e:go"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: The new e2e/go module sits outside every Go lint and format gate. go:lint, go:format:check and go:build all run with dir = "sdk/go", so golangci-lint and gofmt never check the e2e module. The only static check is the go vet inside the gateway-wrapped e2e:go and e2e:go:docker commands.

Why this matters: An unformatted or lint-violating file under e2e/go/ (an unused helper, a shadowed err) still passes mise run go:ci in branch-checks.yml, because tasks/go.toml scopes those tasks to sdk/go and e2e/go/go.mod is a separate module. golangci-lint would also skip these files without --build-tags=e2e, since every file starts with //go:build e2e. The problem only shows up as a go vet failure after with-docker-gateway.sh has already booted a gateway.

Suggested fix: Add go:e2e:lint and go:e2e:format:check tasks with dir = "e2e/go" that run golangci-lint run --build-tags=e2e against sdk/go/.golangci.yml plus gofmt -l ., include them in go:ci, and remove go vet from the gateway-wrapped e2e commands.

Source: architecture agent

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state:stale Inactive item at risk of automatic closure. test:e2e Requires end-to-end coverage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants