Skip to content

Commit bc5cc24

Browse files
stephentoubCopilot
andcommitted
fix: resolve CI failures in expanded E2E coverage
- Format Node E2E test files with prettier (4 files: client_lifecycle, commands, session_fs, session_lifecycle). - Format/lint Python files with ruff: switched Callable import from typing to collections.abc in test_commands_and_elicitation.py; split a long comment in conftest.py to satisfy E501; removed unnecessary "r" open mode in test_hooks_e2e.py. - Resolve macOS /var -> /private/var symlinks in test harnesses so the paths match what spawned subprocesses see when they resolve their cwd: * Go: filepath.EvalSymlinks on home_dir / work_dir. * .NET: P/Invoke libc realpath (with CA2101-compliant marshaling) and a Windows fallback to Path.GetFullPath. * Python: os.path.realpath wrapping tempfile.mkdtemp in the shared E2ETestContext and in the per-test multi-client harnesses (test_commands_e2e, test_multi_client_e2e, test_ui_elicitation_multi_client_e2e). - Loosen per-session "no token" assertions in .NET and Python so they match Node/Go: in CI the process-level fake GITHUB_TOKEN can make IsAuthenticated true even without a per-session token, and the Login may surface as null/None on Linux/macOS but as an empty string on Windows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 736af4c commit bc5cc24

15 files changed

Lines changed: 89 additions & 50 deletions

dotnet/test/E2E/PerSessionAuthE2ETests.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,10 @@ public async Task ShouldBeUnauthenticatedWithoutToken()
9797
});
9898

9999
var status = await session.Rpc.Auth.GetStatusAsync();
100-
Assert.False(status.IsAuthenticated);
101-
Assert.Null(status.Login);
100+
// Without a per-session GitHub token, there is no per-session identity.
101+
// In CI the process-level fake token may still authenticate globally,
102+
// so we check Login rather than IsAuthenticated.
103+
Assert.True(string.IsNullOrEmpty(status.Login), $"Expected no per-session login without token, got {status.Login}");
102104
}
103105

104106
[Fact]

dotnet/test/Harness/E2ETestContext.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*--------------------------------------------------------------------------------------------*/
44

55
using System.Runtime.CompilerServices;
6+
using System.Runtime.InteropServices;
67
using System.Text.RegularExpressions;
78
using Microsoft.Extensions.Logging;
89

@@ -39,12 +40,53 @@ public static async Task<E2ETestContext> CreateAsync()
3940
Directory.CreateDirectory(homeDir);
4041
Directory.CreateDirectory(workDir);
4142

43+
// Resolve symlinks (e.g., macOS /var -> /private/var) so paths
44+
// match what spawned subprocesses see when they resolve their cwd.
45+
homeDir = ResolveSymlinks(homeDir);
46+
workDir = ResolveSymlinks(workDir);
47+
4248
var proxy = new CapiProxy();
4349
var proxyUrl = await proxy.StartAsync();
4450

4551
return new E2ETestContext(homeDir, workDir, proxyUrl, proxy, repoRoot);
4652
}
4753

54+
private static string ResolveSymlinks(string path)
55+
{
56+
if (OperatingSystem.IsWindows())
57+
{
58+
return Path.GetFullPath(path);
59+
}
60+
61+
IntPtr resolved = IntPtr.Zero;
62+
try
63+
{
64+
resolved = NativeRealpath(path, IntPtr.Zero);
65+
if (resolved == IntPtr.Zero)
66+
{
67+
return Path.GetFullPath(path);
68+
}
69+
return Marshal.PtrToStringAnsi(resolved) ?? Path.GetFullPath(path);
70+
}
71+
catch
72+
{
73+
return Path.GetFullPath(path);
74+
}
75+
finally
76+
{
77+
if (resolved != IntPtr.Zero)
78+
{
79+
NativeFree(resolved);
80+
}
81+
}
82+
}
83+
84+
[DllImport("libc", EntryPoint = "realpath", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)]
85+
private static extern IntPtr NativeRealpath([MarshalAs(UnmanagedType.LPStr)] string path, IntPtr resolved);
86+
87+
[DllImport("libc", EntryPoint = "free")]
88+
private static extern void NativeFree(IntPtr ptr);
89+
4890
private static string FindRepoRoot()
4991
{
5092
var dir = new DirectoryInfo(AppContext.BaseDirectory);

go/internal/e2e/testharness/context.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,20 @@ func NewTestContext(t *testing.T) *TestContext {
5959
if err != nil {
6060
t.Fatalf("Failed to create temp home dir: %v", err)
6161
}
62+
if resolved, err := filepath.EvalSymlinks(homeDir); err == nil {
63+
homeDir = resolved
64+
}
6265

6366
workDir, err := os.MkdirTemp("", "copilot-test-work-")
6467
if err != nil {
6568
os.RemoveAll(homeDir)
6669
t.Fatalf("Failed to create temp work dir: %v", err)
6770
}
71+
// Resolve symlinks (e.g., macOS /var -> /private/var) so paths
72+
// match what spawned subprocesses see when they resolve their cwd.
73+
if resolved, err := filepath.EvalSymlinks(workDir); err == nil {
74+
workDir = resolved
75+
}
6876

6977
proxy := NewCapiProxy()
7078
proxyURL, err := proxy.Start()

nodejs/test/e2e/client_lifecycle.e2e.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ describe("Client Lifecycle", async () => {
7272

7373
// Poll for the session-specific event rather than a hard 500ms wait.
7474
const deadline = Date.now() + 10_000;
75-
while (Date.now() < deadline && !events.some((e) => e.sessionId === session.sessionId)) {
75+
while (
76+
Date.now() < deadline &&
77+
!events.some((e) => e.sessionId === session.sessionId)
78+
) {
7679
await new Promise((r) => setTimeout(r, 50));
7780
}
7881

nodejs/test/e2e/commands.e2e.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,7 @@ describe("Commands", async () => {
8282

8383
const session2 = await client1.resumeSession(sessionId, {
8484
onPermissionRequest: approveAll,
85-
commands: [
86-
{ name: "deploy", description: "Deploy", handler: async () => {} },
87-
],
85+
commands: [{ name: "deploy", description: "Deploy", handler: async () => {} }],
8886
});
8987

9088
expect(session2).toBeDefined();

nodejs/test/e2e/session_fs.e2e.test.ts

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -242,18 +242,14 @@ describe("Session Fs Adapter", () => {
242242
async readdir(path: string): Promise<string[]> {
243243
return (await provider.readdir(path)) as string[];
244244
},
245-
async readdirWithTypes(
246-
path: string
247-
): Promise<SessionFsReaddirWithTypesEntry[]> {
245+
async readdirWithTypes(path: string): Promise<SessionFsReaddirWithTypesEntry[]> {
248246
const names = (await provider.readdir(path)) as string[];
249247
return Promise.all(
250248
names.map(async (name) => {
251249
const st = await provider.stat(`${path}/${name}`);
252250
return {
253251
name,
254-
type: st.isDirectory()
255-
? ("directory" as const)
256-
: ("file" as const),
252+
type: st.isDirectory() ? ("directory" as const) : ("file" as const),
257253
};
258254
})
259255
);
@@ -310,9 +306,7 @@ describe("Session Fs Adapter", () => {
310306
expect(entries.entries).toContain("file.txt");
311307
expect(entries.error).toBeUndefined();
312308

313-
const typedEntries = await handler.readdirWithTypes(
314-
params({ path: "/workspace/nested" })
315-
);
309+
const typedEntries = await handler.readdirWithTypes(params({ path: "/workspace/nested" }));
316310
expect(typedEntries.entries).toContainEqual({ name: "file.txt", type: "file" });
317311
expect(typedEntries.error).toBeUndefined();
318312

@@ -328,30 +322,20 @@ describe("Session Fs Adapter", () => {
328322
const oldPath = await handler.exists(params({ path: "/workspace/nested/file.txt" }));
329323
expect(oldPath.exists).toBe(false);
330324

331-
const renamed = await handler.readFile(
332-
params({ path: "/workspace/nested/renamed.txt" })
333-
);
325+
const renamed = await handler.readFile(params({ path: "/workspace/nested/renamed.txt" }));
334326
expect(renamed.content).toBe("hello world");
335327

336-
expect(
337-
await handler.rm(params({ path: "/workspace/nested/renamed.txt" }))
338-
).toBeUndefined();
328+
expect(await handler.rm(params({ path: "/workspace/nested/renamed.txt" }))).toBeUndefined();
339329

340-
const removed = await handler.exists(
341-
params({ path: "/workspace/nested/renamed.txt" })
342-
);
330+
const removed = await handler.exists(params({ path: "/workspace/nested/renamed.txt" }));
343331
expect(removed.exists).toBe(false);
344332

345333
// Forced removal of a missing file should not error.
346334
expect(
347-
await handler.rm(
348-
params({ path: "/workspace/nested/missing.txt", force: true })
349-
)
335+
await handler.rm(params({ path: "/workspace/nested/missing.txt", force: true }))
350336
).toBeUndefined();
351337

352-
const missing = await handler.stat(
353-
params({ path: "/workspace/nested/missing.txt" })
354-
);
338+
const missing = await handler.stat(params({ path: "/workspace/nested/missing.txt" }));
355339
expect(missing.error?.code).toBe("ENOENT");
356340
});
357341

@@ -421,13 +405,9 @@ describe("Session Fs Adapter", () => {
421405
assertEnoent((await handler.stat({ path: "missing.txt" } as never)).error);
422406
assertEnoent(await handler.mkdir({ path: "missing-dir" } as never));
423407
assertEnoent((await handler.readdir({ path: "missing-dir" } as never)).error);
424-
assertEnoent(
425-
(await handler.readdirWithTypes({ path: "missing-dir" } as never)).error
426-
);
408+
assertEnoent((await handler.readdirWithTypes({ path: "missing-dir" } as never)).error);
427409
assertEnoent(await handler.rm({ path: "missing.txt" } as never));
428-
assertEnoent(
429-
await handler.rename({ src: "missing.txt", dest: "dest.txt" } as never)
430-
);
410+
assertEnoent(await handler.rename({ src: "missing.txt", dest: "dest.txt" } as never));
431411

432412
// Non-ENOENT errors map to UNKNOWN.
433413
const unknown: SessionFsProvider = {

nodejs/test/e2e/session_lifecycle.e2e.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import { createSdkTestContext } from "./harness/sdkTestContext";
1111
* `setTimeout` waits for "session flushed to disk" so fast machines exit immediately
1212
* and slow CI machines still get up to `timeoutMs` before the test fails.
1313
*/
14-
async function waitFor(predicate: () => Promise<boolean> | boolean, timeoutMs = 10_000): Promise<void> {
14+
async function waitFor(
15+
predicate: () => Promise<boolean> | boolean,
16+
timeoutMs = 10_000
17+
): Promise<void> {
1518
const deadline = Date.now() + timeoutMs;
1619
while (Date.now() < deadline) {
1720
if (await predicate()) return;

python/e2e/conftest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ async def ctx(request):
3030
@pytest_asyncio.fixture(autouse=True, loop_scope="module")
3131
async def configure_test(request, ctx):
3232
"""Automatically configure the proxy for each test."""
33-
# Extract test file name from module (e.g., "test_session" -> "session", "test_session_e2e" -> "session")
33+
# Extract test file name from module
34+
# (e.g., "test_session" -> "session", "test_session_e2e" -> "session")
3435
module_name = request.module.__name__.split(".")[-1]
3536
if module_name.startswith("test_"):
3637
test_file = module_name[5:] # Remove "test_" prefix

python/e2e/test_commands_e2e.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ def __init__(self):
4444

4545
async def setup(self):
4646
self.cli_path = get_cli_path_for_tests()
47-
self.home_dir = tempfile.mkdtemp(prefix="copilot-cmd-config-")
48-
self.work_dir = tempfile.mkdtemp(prefix="copilot-cmd-work-")
47+
self.home_dir = os.path.realpath(tempfile.mkdtemp(prefix="copilot-cmd-config-"))
48+
self.work_dir = os.path.realpath(tempfile.mkdtemp(prefix="copilot-cmd-work-"))
4949

5050
self._proxy = CapiProxy()
5151
self.proxy_url = await self._proxy.start()

python/e2e/test_hooks_e2e.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ async def on_pre_tool_use(input_data, invocation):
146146
# Strengthen: verify the actual deny behavior — the protected file was NOT
147147
# modified by the runtime even though the LLM tried to edit it. The
148148
# pre-tool-use hook denial blocks tool execution before it can mutate state.
149-
with open(os.path.join(ctx.work_dir, "protected.txt"), "r") as f:
149+
with open(os.path.join(ctx.work_dir, "protected.txt")) as f:
150150
actual_content = f.read()
151151
assert actual_content == original_content, (
152152
f"protected.txt should be unchanged after deny; got: {actual_content!r}"

0 commit comments

Comments
 (0)