|
3 | 3 | *--------------------------------------------------------------------------------------------*/ |
4 | 4 |
|
5 | 5 | using System.Collections.Concurrent; |
| 6 | +using System.Net.Http; |
6 | 7 | using GitHub.Copilot.Test.Harness; |
7 | 8 | using Xunit; |
8 | 9 | using Xunit.Abstractions; |
9 | 10 |
|
10 | 11 | namespace GitHub.Copilot.Test.E2E; |
11 | 12 |
|
| 13 | +#pragma warning disable GHCP001 // The LLM inference surface is intentionally experimental. |
| 14 | + |
12 | 15 | public class SubagentHooksE2ETests(E2ETestFixture fixture, ITestOutputHelper output) |
13 | 16 | : E2ETestBase(fixture, "subagent_hooks", output) |
14 | 17 | { |
15 | 18 | [Fact] |
16 | 19 | public async Task Should_Invoke_PreToolUse_And_PostToolUse_Hooks_For_Sub_Agent_Tool_Calls() |
17 | 20 | { |
18 | 21 | var hookLog = new ConcurrentBag<(string Kind, string ToolName, string SessionId)>(); |
| 22 | + var requestHandler = new RecordingForwardingRequestHandler(); |
19 | 23 |
|
20 | 24 | // Create a client with the session-based subagents feature flag |
21 | 25 | var env = new Dictionary<string, string>(Ctx.GetEnvironment()); |
22 | 26 | env["COPILOT_EXP_COPILOT_CLI_SESSION_BASED_SUBAGENTS"] = "true"; |
23 | | - var client = Ctx.CreateClient(environment: env); |
| 27 | + var client = Ctx.CreateClient(new CopilotClientOptions |
| 28 | + { |
| 29 | + Connection = RuntimeConnection.ForStdio(), |
| 30 | + RequestHandler = requestHandler |
| 31 | + }, environment: env); |
24 | 32 |
|
25 | 33 | var session = await client.CreateSessionAsync(new SessionConfig |
26 | 34 | { |
@@ -69,5 +77,42 @@ await session.SendAndWaitAsync( |
69 | 77 |
|
70 | 78 | // input.SessionId distinguishes parent from sub-agent |
71 | 79 | Assert.NotEqual(viewPre[0].SessionId, taskPre[0].SessionId); |
| 80 | + AssertSubagentRequestMetadata(requestHandler.InferenceRequests); |
| 81 | + } |
| 82 | + |
| 83 | + private static void AssertSubagentRequestMetadata(IReadOnlyCollection<RequestRecord> records) |
| 84 | + { |
| 85 | + Assert.NotEmpty(records); |
| 86 | + var subagentRequest = records.FirstOrDefault(r => !string.IsNullOrEmpty(r.ParentAgentId)); |
| 87 | + Assert.NotNull(subagentRequest); |
| 88 | + Assert.False(string.IsNullOrEmpty(subagentRequest.AgentId), |
| 89 | + "Sub-agent inference request should carry an agent id"); |
| 90 | + Assert.False(string.IsNullOrEmpty(subagentRequest.InteractionType), |
| 91 | + "Sub-agent inference request should carry an interaction type"); |
| 92 | + Assert.NotEqual(subagentRequest.ParentAgentId, subagentRequest.AgentId); |
| 93 | + } |
| 94 | + |
| 95 | + private sealed class RecordingForwardingRequestHandler : CopilotRequestHandler |
| 96 | + { |
| 97 | + private readonly ConcurrentBag<RequestRecord> _records = []; |
| 98 | + |
| 99 | + public IReadOnlyCollection<RequestRecord> InferenceRequests => |
| 100 | + [.. _records.Where(r => RecordingRequestHandler.IsInferenceUrl(r.Url))]; |
| 101 | + |
| 102 | + protected override Task<HttpResponseMessage> SendRequestAsync(HttpRequestMessage request, CopilotRequestContext ctx) |
| 103 | + { |
| 104 | + _records.Add(new RequestRecord( |
| 105 | + request.RequestUri!.ToString(), |
| 106 | + ctx.AgentId, |
| 107 | + ctx.ParentAgentId, |
| 108 | + ctx.InteractionType)); |
| 109 | + return base.SendRequestAsync(request, ctx); |
| 110 | + } |
72 | 111 | } |
| 112 | + |
| 113 | + private sealed record RequestRecord( |
| 114 | + string Url, |
| 115 | + string? AgentId, |
| 116 | + string? ParentAgentId, |
| 117 | + string? InteractionType); |
73 | 118 | } |
0 commit comments