diff --git a/.github/workflows/java-sdk-tests.yml b/.github/workflows/java-sdk-tests.yml index 0b6b62a0d9..514f17c758 100644 --- a/.github/workflows/java-sdk-tests.yml +++ b/.github/workflows/java-sdk-tests.yml @@ -410,6 +410,7 @@ jobs: mvn -B -pl copilot-native deploy -Prelease -DskipTests \ -Dcopilot.native.libc=glibc \ -Dcopilot.native.test.local.publication=true \ + -DskipPublishing=true \ "-Dcopilot.native.external.linux.arm64.classifier.path=$LINUX_ARM64_JAR" \ "-Dcopilot.native.external.win32.classifier.path=$WINDOWS_JAR" \ "-Dcopilot.native.external.win32.arm64.classifier.path=$WINDOWS_ARM64_JAR" \ diff --git a/go/internal/e2e/rpc_shell_and_fleet_e2e_test.go b/go/internal/e2e/rpc_shell_and_fleet_e2e_test.go index 81b8471dac..a426f6bdc9 100644 --- a/go/internal/e2e/rpc_shell_and_fleet_e2e_test.go +++ b/go/internal/e2e/rpc_shell_and_fleet_e2e_test.go @@ -30,12 +30,16 @@ func TestRPCShellAndFleetE2E(t *testing.T) { t.Fatalf("Failed to create session: %v", err) } - markerPath := filepath.Join(ctx.WorkDir, "shell-rpc-"+randomHex(t)+".txt") + commandDir := filepath.Join(ctx.WorkDir, "shell-rpc-"+randomHex(t)) + if err := os.Mkdir(commandDir, 0755); err != nil { + t.Fatalf("Failed to create shell command directory: %v", err) + } + markerPath := filepath.Join(commandDir, "marker.txt") const marker = "copilot-sdk-shell-rpc" - cwd := ctx.WorkDir + cwd := commandDir result, err := session.RPC.Shell.Exec(t.Context(), &rpc.ShellExecRequest{ - Command: writeFileCommand(markerPath, marker), + Command: writeFileCommand(filepath.Base(markerPath), marker), Cwd: &cwd, }) if err != nil { @@ -174,11 +178,11 @@ func randomHex(t *testing.T) string { return hex.EncodeToString(buf[:]) } -func writeFileCommand(markerPath, marker string) string { +func writeFileCommand(markerName, marker string) string { if runtime.GOOS == "windows" { - return fmt.Sprintf("powershell -NoLogo -NoProfile -Command \"Set-Content -LiteralPath '%s' -Value '%s'\"", markerPath, marker) + return fmt.Sprintf("echo %s>\"%s\"", marker, markerName) } - return fmt.Sprintf("sh -c \"printf '%%s' '%s' > '%s'\"", marker, markerPath) + return fmt.Sprintf("sh -c \"printf '%%s' '%s' > '%s'\"", marker, markerName) } func waitForFileText(t *testing.T, path, expected string) { diff --git a/go/internal/e2e/testharness/context.go b/go/internal/e2e/testharness/context.go index 0374cc5130..951adfc291 100644 --- a/go/internal/e2e/testharness/context.go +++ b/go/internal/e2e/testharness/context.go @@ -163,6 +163,15 @@ func NewTestContext(t *testing.T) *TestContext { os.RemoveAll(workDir) t.Fatalf("Failed to start proxy: %v", err) } + // Initialize the proxy before any client can start runtime requests. Tests that + // use a snapshot replace this empty configuration before model traffic begins. + dummySnapshotPath := filepath.Join(workDir, "__no_snapshot__.yaml") + if err := proxy.Configure(dummySnapshotPath, workDir); err != nil { + proxy.StopWithOptions(true) + os.RemoveAll(homeDir) + os.RemoveAll(workDir) + t.Fatalf("Failed to initialize proxy: %v", err) + } if err := proxy.SetCopilotUserByToken(defaultGitHubToken, map[string]interface{}{ "login": "e2e-test-user", "copilot_plan": "individual_pro", diff --git a/python/copilot/_cli_download.py b/python/copilot/_cli_download.py index 4477fcfff3..2d9076f6b8 100644 --- a/python/copilot/_cli_download.py +++ b/python/copilot/_cli_download.py @@ -27,6 +27,7 @@ import tempfile import time import zipfile +from http.client import IncompleteRead from pathlib import Path, PurePosixPath from urllib.error import HTTPError, URLError from urllib.request import urlopen @@ -43,6 +44,7 @@ _CACHE_DIR_NAME = "github-copilot-sdk" _MAX_RETRIES = 3 +_RETRIABLE_DOWNLOAD_ERRORS = (HTTPError, URLError, IncompleteRead) def _sanitize_version(version: str) -> str: @@ -128,7 +130,7 @@ def _fetch_checksums(version: str) -> dict[str, str]: with urlopen(url, timeout=30) as response: text = response.read().decode("utf-8") break - except (HTTPError, URLError) as exc: + except _RETRIABLE_DOWNLOAD_ERRORS as exc: last_exc = exc if attempt < _MAX_RETRIES - 1: time.sleep(2**attempt) @@ -255,7 +257,7 @@ def download_cli(version: str | None = None, *, force: bool = False) -> str: with urlopen(url, timeout=120) as response: data = response.read() break - except (HTTPError, URLError) as exc: + except _RETRIABLE_DOWNLOAD_ERRORS as exc: last_exc = exc if attempt < _MAX_RETRIES - 1: time.sleep(2**attempt) @@ -315,7 +317,7 @@ def _fetch_url_bytes(url: str, *, timeout: int) -> bytes: try: with urlopen(url, timeout=timeout) as response: return response.read() - except (HTTPError, URLError) as exc: + except _RETRIABLE_DOWNLOAD_ERRORS as exc: last_exc = exc if attempt < _MAX_RETRIES - 1: time.sleep(2**attempt) diff --git a/python/test_cli_download.py b/python/test_cli_download.py index aac652daf8..4ae4fe6bc3 100644 --- a/python/test_cli_download.py +++ b/python/test_cli_download.py @@ -7,7 +7,8 @@ import io import os import tarfile -from unittest.mock import patch +from http.client import IncompleteRead +from unittest.mock import MagicMock, patch import pytest @@ -41,6 +42,26 @@ def _runtime_package(npm_platform: str) -> bytes: return buffer.getvalue() +def test_fetch_url_bytes_retries_truncated_response(): + truncated_response = MagicMock() + truncated_response.__enter__.return_value.read.side_effect = IncompleteRead(b"partial", 4) + complete_response = MagicMock() + complete_response.__enter__.return_value.read.return_value = b"complete" + + with ( + patch.object( + _cli_download, + "urlopen", + side_effect=[truncated_response, complete_response], + ) as urlopen, + patch.object(_cli_download.time, "sleep") as sleep, + ): + assert _cli_download._fetch_url_bytes("https://example/runtime", timeout=30) == b"complete" + + assert urlopen.call_count == 2 + sleep.assert_called_once_with(1) + + class TestVerifyIntegrity: def test_accepts_matching_checksum(self): data = b"native-library-bytes" diff --git a/test/harness/replayingCapiProxy.test.ts b/test/harness/replayingCapiProxy.test.ts index c5ba6195b1..062fe89ae9 100644 --- a/test/harness/replayingCapiProxy.test.ts +++ b/test/harness/replayingCapiProxy.test.ts @@ -981,7 +981,7 @@ Always include PINEAPPLE_COCONUT_42. } }); - test("matches semantically equivalent interrupted shell results", async () => { + test("matches semantically equivalent interrupted tool results", async () => { const originalShellConfig = process.platform === "win32" ? ShellConfig.powerShell @@ -1056,8 +1056,7 @@ Always include PINEAPPLE_COCONUT_42. { role: "tool", tool_call_id: "runtime-call-id", - content: - "", + content: "Session aborted", }, ], }, diff --git a/test/harness/replayingCapiProxy.ts b/test/harness/replayingCapiProxy.ts index ca7f6d0f3f..9bcafcdad4 100644 --- a/test/harness/replayingCapiProxy.ts +++ b/test/harness/replayingCapiProxy.ts @@ -1556,7 +1556,7 @@ function normalizeAvailableToolNames(result: string): string { function normalizeInterruptedToolResult(result: string): string { return result.replace( - /^(?:Failed to execute `[^`]+` tool(?: with arguments: [\s\S]*?)? due to error: (?:Error: )?Session aborted||unknown attachedShellSession handle \d+)$/, + /^(?:Session aborted|Failed to execute `[^`]+` tool(?: with arguments: [\s\S]*?)? due to error: (?:Error: )?Session aborted||unknown attachedShellSession handle \d+)$/, "The execution of this tool, or a previous tool was interrupted.", ); }