-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Expand file tree
/
Copy pathcontext.py
More file actions
152 lines (121 loc) · 5.03 KB
/
context.py
File metadata and controls
152 lines (121 loc) · 5.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
"""
Test context for E2E tests.
Provides isolated directories and a replaying proxy for testing the SDK.
"""
import os
import re
import shutil
import tempfile
from pathlib import Path
from copilot import CopilotClient
from copilot.client import SubprocessConfig
from .proxy import CapiProxy
def get_cli_path_for_tests() -> str:
"""Get CLI path for E2E tests.
Uses COPILOT_CLI_PATH env var if set, otherwise node_modules CLI.
"""
env_path = os.environ.get("COPILOT_CLI_PATH")
if env_path and Path(env_path).exists():
return str(Path(env_path).resolve())
# Look for CLI in sibling nodejs directory's node_modules
base_path = Path(__file__).parents[3]
full_path = base_path / "nodejs" / "node_modules" / "@github" / "copilot" / "index.js"
if full_path.exists():
return str(full_path.resolve())
raise RuntimeError("CLI not found for tests. Run 'npm install' in the nodejs directory.")
CLI_PATH = get_cli_path_for_tests()
SNAPSHOTS_DIR = Path(__file__).parents[3] / "test" / "snapshots"
class E2ETestContext:
"""Holds shared resources for E2E tests."""
def __init__(self):
self.cli_path: str = ""
self.home_dir: str = ""
self.work_dir: str = ""
self.proxy_url: str = ""
self._proxy: CapiProxy | None = None
self._client: CopilotClient | None = None
async def setup(self):
"""Set up the test context with a shared client."""
self.cli_path = get_cli_path_for_tests()
self.home_dir = tempfile.mkdtemp(prefix="copilot-test-config-")
self.work_dir = tempfile.mkdtemp(prefix="copilot-test-work-")
self._proxy = CapiProxy()
self.proxy_url = await self._proxy.start()
# Create the shared client (like Node.js/Go do)
# Use fake token in CI to allow cached responses without real auth
github_token = (
"fake-token-for-e2e-tests" if os.environ.get("GITHUB_ACTIONS") == "true" else None
)
self._client = CopilotClient(
SubprocessConfig(
cli_path=self.cli_path,
cwd=self.work_dir,
env=self.get_env(),
github_token=github_token,
)
)
async def teardown(self, test_failed: bool = False):
"""Clean up the test context.
Args:
test_failed: If True, skip writing snapshots to avoid corruption.
"""
if self._client:
try:
await self._client.stop()
except ExceptionGroup:
pass # stop() completes all cleanup before raising; safe to ignore in teardown
self._client = None
if self._proxy:
await self._proxy.stop(skip_writing_cache=test_failed)
self._proxy = None
if self.home_dir and os.path.exists(self.home_dir):
shutil.rmtree(self.home_dir, ignore_errors=True)
if self.work_dir and os.path.exists(self.work_dir):
shutil.rmtree(self.work_dir, ignore_errors=True)
async def configure_for_test(self, test_file: str, test_name: str):
"""
Configure the proxy for a specific test.
Args:
test_file: The test file name (e.g., "session" from "test_session.py")
test_name: The test name (e.g., "should_have_stateful_conversation")
"""
sanitized_name = re.sub(r"[^a-zA-Z0-9]", "_", test_name).lower()
snapshot_path = SNAPSHOTS_DIR / test_file / f"{sanitized_name}.yaml"
abs_snapshot_path = str(snapshot_path.resolve())
if self._proxy:
await self._proxy.configure(abs_snapshot_path, self.work_dir)
# Clear temp directories between tests (but leave them in place)
# Use ignore_errors=True to handle race conditions where files may still
# be written by background processes during cleanup
for item in Path(self.home_dir).iterdir():
if item.is_dir():
shutil.rmtree(item, ignore_errors=True)
else:
item.unlink(missing_ok=True)
for item in Path(self.work_dir).iterdir():
if item.is_dir():
shutil.rmtree(item, ignore_errors=True)
else:
item.unlink(missing_ok=True)
def get_env(self) -> dict:
"""Return environment variables configured for isolated testing."""
env = os.environ.copy()
env.update(
{
"COPILOT_API_URL": self.proxy_url,
"XDG_CONFIG_HOME": self.home_dir,
"XDG_STATE_HOME": self.home_dir,
}
)
return env
@property
def client(self) -> CopilotClient:
"""Return the shared CopilotClient instance."""
if not self._client:
raise RuntimeError("Context not set up. Call setup() first.")
return self._client
async def get_exchanges(self):
"""Retrieve the captured HTTP exchanges from the proxy."""
if not self._proxy:
raise RuntimeError("Proxy not started")
return await self._proxy.get_exchanges()