Skip to content

Commit 0ab885b

Browse files
E2E infrastructure: skip writing snapshots on failure + update snapshots for extended thinking
1 parent dc49827 commit 0ab885b

27 files changed

Lines changed: 336 additions & 52 deletions

dotnet/test/Harness/CapiProxy.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,16 @@ async Task<string> StartCoreAsync()
8484
}
8585
}
8686

87-
public async Task StopAsync()
87+
public async Task StopAsync(bool skipWritingCache = false)
8888
{
8989
if (_startupTask != null)
9090
{
9191
try
9292
{
9393
var url = await _startupTask;
94+
var stopUrl = skipWritingCache ? $"{url}/stop?skipWritingCache=true" : $"{url}/stop";
9495
using var client = new HttpClient();
95-
await client.PostAsync($"{url}/stop", null);
96+
await client.PostAsync(stopUrl, null);
9697
}
9798
catch { /* Best effort */ }
9899
}

dotnet/test/Harness/E2ETestContext.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,9 @@ public IReadOnlyDictionary<string, string> GetEnvironment()
101101

102102
public async ValueTask DisposeAsync()
103103
{
104-
await _proxy.DisposeAsync();
104+
// Skip writing snapshots in CI to avoid corrupting them on test failures
105+
var isCI = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"));
106+
await _proxy.StopAsync(skipWritingCache: isCI);
105107

106108
try { if (Directory.Exists(HomeDir)) Directory.Delete(HomeDir, true); } catch { }
107109
try { if (Directory.Exists(WorkDir)) Directory.Delete(WorkDir, true); } catch { }

go/e2e/testharness/context.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ type TestContext struct {
4242
WorkDir string
4343
ProxyURL string
4444

45-
proxy *CapiProxy
45+
proxy *CapiProxy
46+
testFailed bool
4647
}
4748

4849
// NewTestContext creates a new test context with isolated directories and a replaying proxy.
@@ -82,7 +83,7 @@ func NewTestContext(t *testing.T) *TestContext {
8283
}
8384

8485
t.Cleanup(func() {
85-
ctx.Close()
86+
ctx.Close(t.Failed())
8687
})
8788

8889
return ctx
@@ -113,9 +114,9 @@ func (c *TestContext) ConfigureForTest(t *testing.T) {
113114
}
114115

115116
// Close cleans up the test context resources.
116-
func (c *TestContext) Close() {
117+
func (c *TestContext) Close(testFailed bool) {
117118
if c.proxy != nil {
118-
c.proxy.Stop()
119+
c.proxy.StopWithOptions(testFailed)
119120
}
120121
if c.HomeDir != "" {
121122
os.RemoveAll(c.HomeDir)

go/e2e/testharness/proxy.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ func (p *CapiProxy) Start() (string, error) {
7575

7676
// Stop gracefully shuts down the proxy server.
7777
func (p *CapiProxy) Stop() error {
78+
return p.StopWithOptions(false)
79+
}
80+
81+
// StopWithOptions gracefully shuts down the proxy server.
82+
// If skipWritingCache is true, the proxy won't write captured exchanges to disk.
83+
func (p *CapiProxy) StopWithOptions(skipWritingCache bool) error {
7884
p.mu.Lock()
7985
defer p.mu.Unlock()
8086

@@ -84,8 +90,12 @@ func (p *CapiProxy) Stop() error {
8490

8591
// Send stop request to the server
8692
if p.proxyURL != "" {
93+
stopURL := p.proxyURL + "/stop"
94+
if skipWritingCache {
95+
stopURL += "?skipWritingCache=true"
96+
}
8797
// Best effort - ignore errors
88-
resp, err := http.Post(p.proxyURL+"/stop", "application/json", nil)
98+
resp, err := http.Post(stopURL, "application/json", nil)
8999
if err == nil {
90100
resp.Body.Close()
91101
}

nodejs/test/e2e/harness/CapiProxy.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,11 @@ export class CapiProxy {
4343
return await response.json();
4444
}
4545

46-
async stop(): Promise<void> {
47-
const response = await fetch(`${this.proxyUrl}/stop`, { method: "POST" });
46+
async stop(skipWritingCache?: boolean): Promise<void> {
47+
const url = skipWritingCache
48+
? `${this.proxyUrl}/stop?skipWritingCache=true`
49+
: `${this.proxyUrl}/stop`;
50+
const response = await fetch(url, { method: "POST" });
4851
expect(response.ok).toBe(true);
4952
}
5053
}

nodejs/test/e2e/harness/sdkTestContext.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import os from "os";
88
import { basename, dirname, join, resolve } from "path";
99
import { rimraf } from "rimraf";
1010
import { fileURLToPath } from "url";
11-
import { afterAll, afterEach, beforeEach, TestContext } from "vitest";
11+
import { afterAll, afterEach, beforeEach, onTestFailed, TestContext } from "vitest";
1212
import { CopilotClient } from "../../../src";
1313
import { CapiProxy } from "./CapiProxy";
1414
import { retry } from "./sdkTestHelper";
@@ -17,7 +17,7 @@ const __filename = fileURLToPath(import.meta.url);
1717
const __dirname = dirname(__filename);
1818
const SNAPSHOTS_DIR = resolve(__dirname, "../../../../test/snapshots");
1919

20-
export const CLI_PATH = resolve(__dirname, "../../../node_modules/@github/copilot/index.js");
20+
export const CLI_PATH = process.env.COPILOT_CLI_PATH || resolve(__dirname, "../../../node_modules/@github/copilot/index.js");
2121

2222
export async function createSdkTestContext() {
2323
const homeDir = realpathSync(fs.mkdtempSync(join(os.tmpdir(), "copilot-test-config-")));
@@ -44,8 +44,15 @@ export async function createSdkTestContext() {
4444

4545
const harness = { homeDir, workDir, openAiEndpoint, copilotClient, env };
4646

47+
// Track if any test fails to avoid writing corrupted snapshots
48+
let anyTestFailed = false;
49+
4750
// Wire up to Vitest lifecycle
4851
beforeEach(async (testContext) => {
52+
onTestFailed(() => {
53+
anyTestFailed = true;
54+
});
55+
4956
await openAiEndpoint.updateConfig({
5057
filePath: getTrafficCapturePath(testContext),
5158
workDir,
@@ -63,7 +70,7 @@ export async function createSdkTestContext() {
6370

6471
afterAll(async () => {
6572
await copilotClient.stop();
66-
await openAiEndpoint.stop();
73+
await openAiEndpoint.stop(anyTestFailed);
6774
await rmDir("remove e2e test homeDir", homeDir);
6875
await rmDir("remove e2e test workDir", workDir);
6976
});

python/e2e/conftest.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,34 @@
11
"""Shared pytest fixtures for e2e tests."""
22

3+
import pytest
34
import pytest_asyncio
45

56
from .testharness import E2ETestContext
67

78

9+
# Track if any test failed to avoid writing corrupted snapshots
10+
_any_test_failed = False
11+
12+
13+
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
14+
def pytest_runtest_makereport(item, call):
15+
"""Track test failures to avoid writing corrupted snapshots."""
16+
global _any_test_failed
17+
outcome = yield
18+
rep = outcome.get_result()
19+
if rep.when == "call" and rep.failed:
20+
_any_test_failed = True
21+
22+
823
@pytest_asyncio.fixture(scope="module", loop_scope="module")
924
async def ctx():
1025
"""Create and teardown a test context shared across all tests in this module."""
26+
global _any_test_failed
27+
_any_test_failed = False # Reset for each module
1128
context = E2ETestContext()
1229
await context.setup()
1330
yield context
14-
await context.teardown()
31+
await context.teardown(test_failed=_any_test_failed)
1532

1633

1734
@pytest_asyncio.fixture(autouse=True, loop_scope="module")

python/e2e/testharness/context.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,18 @@ async def setup(self):
7373
}
7474
)
7575

76-
async def teardown(self):
77-
"""Clean up the test context."""
76+
async def teardown(self, test_failed: bool = False):
77+
"""Clean up the test context.
78+
79+
Args:
80+
test_failed: If True, skip writing snapshots to avoid corruption.
81+
"""
7882
if self._client:
7983
await self._client.stop()
8084
self._client = None
8185

8286
if self._proxy:
83-
await self._proxy.stop()
87+
await self._proxy.stop(skip_writing_cache=test_failed)
8488
self._proxy = None
8589

8690
if self.home_dir and os.path.exists(self.home_dir):

python/e2e/testharness/proxy.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,23 @@ async def start(self) -> str:
5959
self._proxy_url = match.group(1)
6060
return self._proxy_url
6161

62-
async def stop(self):
63-
"""Gracefully shut down the proxy server."""
62+
async def stop(self, skip_writing_cache: bool = False):
63+
"""Gracefully shut down the proxy server.
64+
65+
Args:
66+
skip_writing_cache: If True, the proxy won't write captured exchanges to disk.
67+
"""
6468
if not self._process:
6569
return
6670

6771
# Send stop request to the server
6872
if self._proxy_url:
6973
try:
74+
stop_url = f"{self._proxy_url}/stop"
75+
if skip_writing_cache:
76+
stop_url += "?skipWritingCache=true"
7077
async with httpx.AsyncClient() as client:
71-
await client.post(f"{self._proxy_url}/stop")
78+
await client.post(stop_url)
7279
except Exception:
7380
pass # Best effort
7481

test/harness/replayingCapiProxy.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,13 @@ export class ReplayingCapiProxy extends CapturingHttpProxy {
146146

147147
// Handle /stop endpoint for stopping the proxy
148148
if (
149-
options.requestOptions.path === "/stop" &&
149+
options.requestOptions.path?.startsWith("/stop") &&
150150
options.requestOptions.method === "POST"
151151
) {
152+
const skipWritingCache = options.requestOptions.path.includes("skipWritingCache=true");
152153
options.onResponseStart(200, {});
153154
options.onResponseEnd();
154-
await this.stop();
155+
await this.stop(skipWritingCache);
155156
process.exit(0);
156157
}
157158

0 commit comments

Comments
 (0)