Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/python-sdk-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ jobs:
# running independent modules concurrently in isolated workers.
run: uv run pytest -v -s -n 2 --dist=loadfile

- name: Upload Python test diagnostics
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: python-test-diagnostics-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.transport }}-${{ github.run_attempt }}
path: python/.pytest-diagnostics/
include-hidden-files: true
if-no-files-found: warn
retention-days: 7

# JavaScript actions use a glibc-linked Node runtime, so Alpine runs through Docker.
test-musl-arm64:
name: "Python SDK Tests (Alpine ARM64, ${{ matrix.transport }})"
Expand Down Expand Up @@ -136,3 +146,13 @@ jobs:
cd python
uv sync --all-extras --dev
uv run pytest -v -s -n 2 --dist=loadfile

- name: Upload Python test diagnostics
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: python-test-diagnostics-alpine-arm64-${{ matrix.transport }}-${{ github.run_attempt }}
path: python/.pytest-diagnostics/
include-hidden-files: true
if-no-files-found: warn
retention-days: 7
6 changes: 6 additions & 0 deletions go/internal/e2e/auto_tier_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ func TestAutoTierE2E(t *testing.T) {
}

t.Run("should stage and reset auto tier preference", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
session := newAutoSession(t)
assertNoPending(t, session)

Expand Down Expand Up @@ -107,6 +110,9 @@ func TestAutoTierE2E(t *testing.T) {
})

t.Run("should preserve auto tier when set model omits it", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
session := newAutoSession(t)

if _, err := session.SetAutoTier(t.Context(), autoTier(copilot.AutoTierBalance)); err != nil {
Expand Down
6 changes: 3 additions & 3 deletions go/internal/e2e/client_api_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ func TestClientAPIE2E(t *testing.T) {
})

t.Run("should get null last session id before any sessions exist", func(t *testing.T) {
// Use a fresh client with isolated COPILOT_HOME so other subtests don't pollute state.
freshCtx := testharness.NewTestContext(t)
freshClient := freshCtx.NewClient()
freshClient := ctx.NewClient(func(options *copilot.ClientOptions) {
options.BaseDirectory = t.TempDir()
})
t.Cleanup(func() { freshClient.ForceStop() })

if err := freshClient.Start(t.Context()); err != nil {
Expand Down
21 changes: 21 additions & 0 deletions go/internal/e2e/client_options_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import (
// in package-level unit tests.
func TestClientOptionsE2E(t *testing.T) {
t.Run("should listen on configured TCP port", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx := testharness.NewTestContext(t)
port := getAvailableTCPPort(t)

Expand All @@ -45,6 +48,9 @@ func TestClientOptionsE2E(t *testing.T) {
})

t.Run("should use client cwd for default workingdirectory", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx := testharness.NewTestContext(t)
ctx.ConfigureForTest(t)

Expand Down Expand Up @@ -85,6 +91,9 @@ func TestClientOptionsE2E(t *testing.T) {
})

t.Run("should propagate process options to spawned cli", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
// Mirrors: Should_Propagate_Process_Options_To_Spawned_Cli
// Spawns a fake stdio CLI (a Node.js script) so we can assert that the
// SDK passes the right argv / env / cwd / RPC params through to the
Expand Down Expand Up @@ -233,6 +242,9 @@ func TestClientOptionsE2E(t *testing.T) {
})

t.Run("should send empty-mode custom agent locality defaults in initial requests", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx := testharness.NewTestContext(t)
cliPath := filepath.Join(ctx.WorkDir, "fake-cli-empty-"+randomHex(t)+".js")
capturePath := filepath.Join(ctx.WorkDir, "fake-cli-empty-capture-"+randomHex(t)+".json")
Expand Down Expand Up @@ -301,6 +313,9 @@ func TestClientOptionsE2E(t *testing.T) {
})

t.Run("should forward advanced session creation options to the CLI", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx := testharness.NewTestContext(t)
cliPath := filepath.Join(ctx.WorkDir, "fake-cli-"+randomHex(t)+".js")
capturePath := filepath.Join(ctx.WorkDir, "fake-cli-capture-"+randomHex(t)+".json")
Expand Down Expand Up @@ -449,6 +464,9 @@ func TestClientOptionsE2E(t *testing.T) {
})

t.Run("should forward singular provider configuration on session creation", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx := testharness.NewTestContext(t)
cliPath := filepath.Join(ctx.WorkDir, "fake-cli-"+randomHex(t)+".js")
capturePath := filepath.Join(ctx.WorkDir, "fake-cli-capture-"+randomHex(t)+".json")
Expand Down Expand Up @@ -510,6 +528,9 @@ func TestClientOptionsE2E(t *testing.T) {
})

t.Run("should forward advanced session resume options to the CLI", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx := testharness.NewTestContext(t)
cliPath := filepath.Join(ctx.WorkDir, "fake-cli-"+randomHex(t)+".js")
capturePath := filepath.Join(ctx.WorkDir, "fake-cli-capture-"+randomHex(t)+".json")
Expand Down
12 changes: 12 additions & 0 deletions go/internal/e2e/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package e2e

import (
"os"
"testing"

"github.com/github/copilot-sdk/go/internal/e2e/testharness"
)

func TestMain(m *testing.M) {
os.Exit(testharness.RunWithInProcessIsolation(m))
}
22 changes: 18 additions & 4 deletions go/internal/e2e/pre_mcp_tool_call_hook_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ import (
)

func TestPreMCPToolCallHookE2E(t *testing.T) {
ctx := testharness.NewTestContext(t)
client := ctx.NewClient()
t.Cleanup(func() { client.ForceStop() })

testHarnessDir := testharness.RepoPath("test", "harness")
metaEchoServer := filepath.Join(testHarnessDir, "test-mcp-meta-echo-server.mjs")

Expand All @@ -30,7 +26,13 @@ func TestPreMCPToolCallHookE2E(t *testing.T) {
}

t.Run("should set meta via preMcpToolCall hook", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx := testharness.NewTestContext(t)
ctx.ConfigureForTest(t)
client := ctx.NewClient()
t.Cleanup(func() { client.ForceStop() })

var (
mu sync.Mutex
Expand Down Expand Up @@ -93,7 +95,13 @@ func TestPreMCPToolCallHookE2E(t *testing.T) {
})

t.Run("should replace meta via preMcpToolCall hook", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx := testharness.NewTestContext(t)
ctx.ConfigureForTest(t)
client := ctx.NewClient()
t.Cleanup(func() { client.ForceStop() })

var (
mu sync.Mutex
Expand Down Expand Up @@ -149,7 +157,13 @@ func TestPreMCPToolCallHookE2E(t *testing.T) {
})

t.Run("should remove meta via preMcpToolCall hook", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx := testharness.NewTestContext(t)
ctx.ConfigureForTest(t)
client := ctx.NewClient()
t.Cleanup(func() { client.ForceStop() })

var (
mu sync.Mutex
Expand Down
6 changes: 6 additions & 0 deletions go/internal/e2e/rpc_mcp_config_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import (
// Tests server-scoped MCP configuration management via MCP.Config.* RPCs.
func TestRPCMCPConfigE2E(t *testing.T) {
t.Run("should call server MCP config rpcs", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx := testharness.NewTestContext(t)
client := ctx.NewClient()
t.Cleanup(func() { client.ForceStop() })
Expand Down Expand Up @@ -105,6 +108,9 @@ func TestRPCMCPConfigE2E(t *testing.T) {
})

t.Run("should round trip http MCP oauth config rpc", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx := testharness.NewTestContext(t)
client := ctx.NewClient()
t.Cleanup(func() { client.ForceStop() })
Expand Down
45 changes: 45 additions & 0 deletions go/internal/e2e/rpc_server_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import (
// Tests server-scoped (non-session) RPCs.
func TestRPCServerE2E(t *testing.T) {
t.Run("should clear the managed settings cache", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx := testharness.NewTestContext(t)
ctx.ConfigureForTest(t)
client := ctx.NewClient()
Expand All @@ -33,6 +36,9 @@ func TestRPCServerE2E(t *testing.T) {
})

t.Run("should call rpc ping with typed params and result", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx := testharness.NewTestContext(t)
ctx.ConfigureForTest(t)
client := ctx.NewClient()
Expand All @@ -56,6 +62,9 @@ func TestRPCServerE2E(t *testing.T) {
})

t.Run("should call rpc models list with typed result", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx := testharness.NewTestContext(t)
ctx.ConfigureForTest(t)
const token = "rpc-models-token"
Expand Down Expand Up @@ -89,6 +98,9 @@ func TestRPCServerE2E(t *testing.T) {
})

t.Run("should call rpc account get quota when authenticated", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx := testharness.NewTestContext(t)
ctx.ConfigureForTest(t)
const token = "rpc-quota-token"
Expand Down Expand Up @@ -145,6 +157,9 @@ func TestRPCServerE2E(t *testing.T) {
})

t.Run("should call rpc tools list with typed result", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx := testharness.NewTestContext(t)
ctx.ConfigureForTest(t)
client := ctx.NewClient()
Expand All @@ -169,6 +184,9 @@ func TestRPCServerE2E(t *testing.T) {
})

t.Run("should call rpc session fs set provider with typed result", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx := testharness.NewTestContext(t)
client := ctx.NewClient()
t.Cleanup(func() { client.ForceStop() })
Expand All @@ -192,6 +210,9 @@ func TestRPCServerE2E(t *testing.T) {
})

t.Run("should add secret filter values", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx := testharness.NewTestContext(t)
client := ctx.NewClient(func(opts *copilot.ClientOptions) {
opts.Env = append(opts.Env, "COPILOT_ENABLE_SECRET_FILTERING=true")
Expand All @@ -213,6 +234,9 @@ func TestRPCServerE2E(t *testing.T) {
})

t.Run("should return false for missing LLM response frames", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx := testharness.NewTestContext(t)
client := ctx.NewClient()
t.Cleanup(func() { client.ForceStop() })
Expand Down Expand Up @@ -251,6 +275,9 @@ func TestRPCServerE2E(t *testing.T) {
})

t.Run("should list find and inspect persisted session state", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx := testharness.NewTestContext(t)
token := "rpc-server-list-token-" + randomHex(t)
registerProxyUser(t, ctx, token, "rpc-user", nil)
Expand Down Expand Up @@ -344,6 +371,9 @@ func TestRPCServerE2E(t *testing.T) {
})

t.Run("should enrich basic session metadata", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx := testharness.NewTestContext(t)
token := "rpc-server-enrich-token-" + randomHex(t)
registerProxyUser(t, ctx, token, "rpc-user", nil)
Expand Down Expand Up @@ -397,6 +427,9 @@ func TestRPCServerE2E(t *testing.T) {
})

t.Run("should close active session and release lock", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx := testharness.NewTestContext(t)
token := "rpc-server-close-token-" + randomHex(t)
registerProxyUser(t, ctx, token, "rpc-user", nil)
Expand Down Expand Up @@ -434,6 +467,9 @@ func TestRPCServerE2E(t *testing.T) {
})

t.Run("should prune dry run and bulk delete persisted session", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx := testharness.NewTestContext(t)
token := "rpc-server-delete-token-" + randomHex(t)
registerProxyUser(t, ctx, token, "rpc-user", nil)
Expand Down Expand Up @@ -500,6 +536,9 @@ func TestRPCServerE2E(t *testing.T) {
})

t.Run("should set additional plugins and reload deferred hooks", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx := testharness.NewTestContext(t)
client := ctx.NewClient()
t.Cleanup(func() { client.ForceStop() })
Expand Down Expand Up @@ -546,6 +585,9 @@ func TestRPCServerE2E(t *testing.T) {
})

t.Run("should report implemented error when connecting unknown remote session", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx := testharness.NewTestContext(t)
ctx.ConfigureWithoutSnapshot(t)
client := ctx.NewClient()
Expand All @@ -568,6 +610,9 @@ func TestRPCServerE2E(t *testing.T) {
})

t.Run("should discover server mcp and skills", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx := testharness.NewTestContext(t)
ctx.ConfigureForTest(t)
client := ctx.NewClient()
Expand Down
Loading
Loading