Repository navigation
Expand file tree
/
Copy pathRpcEventSideEffectsE2ETests.cs
More file actions
189 lines (148 loc) · 8.06 KB
/
Copy pathRpcEventSideEffectsE2ETests.cs
File metadata and controls
189 lines (148 loc) · 8.06 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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/
using GitHub.Copilot.Rpc;
using GitHub.Copilot.Test.Harness;
using Xunit;
using Xunit.Abstractions;
namespace GitHub.Copilot.Test.E2E;
/// <summary>
/// Verifies that session-scoped RPC calls emit the expected side-effect session events.
/// Most tests are pure RPC-only and need no replay snapshot, but the truncate tests
/// drive a real user.message first so the runtime persists events to disk
/// (LocalSessionManager.SessionWriter only flushes once a user.message is observed).
/// </summary>
public class RpcEventSideEffectsE2ETests(E2ETestFixture fixture, ITestOutputHelper output)
: E2ETestBase(fixture, "rpc_event_side_effects", output)
{
private static readonly TimeSpan EventTimeout = TimeSpan.FromSeconds(30);
[Fact]
public async Task Should_Emit_Mode_Changed_Event_When_Mode_Set()
{
var session = await CreateSessionAsync();
// Subscribe before invoking RPC; events may arrive after the RPC completes.
var modeChangedTask = TestHelper.GetNextEventOfTypeAsync<SessionModeChangedEvent>(
session,
evt => evt.Data.NewMode == SessionMode.Plan && evt.Data.PreviousMode == SessionMode.Interactive,
EventTimeout,
timeoutDescription: "session.mode_changed event for interactive→plan");
await session.Rpc.Mode.SetAsync(SessionMode.Plan);
var evt = await modeChangedTask;
Assert.Equal(SessionMode.Plan, evt.Data.NewMode);
Assert.Equal(SessionMode.Interactive, evt.Data.PreviousMode);
}
[Fact]
public async Task Should_Emit_Plan_Changed_Event_For_Update_And_Delete()
{
var session = await CreateSessionAsync();
var createTask = TestHelper.GetNextEventOfTypeAsync<SessionPlanChangedEvent>(
session,
evt => evt.Data.Operation == PlanChangedOperation.Create,
EventTimeout,
timeoutDescription: "session.plan_changed event for plan creation");
await session.Rpc.Plan.UpdateAsync("# Test plan\n- item");
var createEvent = await createTask;
Assert.Equal(PlanChangedOperation.Create, createEvent.Data.Operation);
var deleteTask = TestHelper.GetNextEventOfTypeAsync<SessionPlanChangedEvent>(
session,
evt => evt.Data.Operation == PlanChangedOperation.Delete,
EventTimeout,
timeoutDescription: "session.plan_changed event for plan deletion");
await session.Rpc.Plan.DeleteAsync();
var deleteEvent = await deleteTask;
Assert.Equal(PlanChangedOperation.Delete, deleteEvent.Data.Operation);
}
[Fact]
public async Task Should_Emit_Plan_Changed_Update_Operation_On_Second_Update()
{
var session = await CreateSessionAsync();
// First update creates the plan.
await session.Rpc.Plan.UpdateAsync("# initial");
// Second update should emit operation == "update".
var updateTask = TestHelper.GetNextEventOfTypeAsync<SessionPlanChangedEvent>(
session,
evt => evt.Data.Operation == PlanChangedOperation.Update,
EventTimeout,
timeoutDescription: "session.plan_changed event for plan update");
await session.Rpc.Plan.UpdateAsync("# updated content");
var updateEvent = await updateTask;
Assert.Equal(PlanChangedOperation.Update, updateEvent.Data.Operation);
}
[Fact]
public async Task Should_Emit_Workspace_File_Changed_Event_When_File_Created()
{
var session = await CreateSessionAsync();
var path = $"side-effect-{Guid.NewGuid():N}.txt";
var changedTask = TestHelper.GetNextEventOfTypeAsync<SessionWorkspaceFileChangedEvent>(
session,
evt => string.Equals(evt.Data.Path, path, StringComparison.Ordinal),
EventTimeout,
timeoutDescription: $"session.workspace_file_changed for '{path}'");
await session.Rpc.Workspaces.CreateFileAsync(path, "hello");
var evt = await changedTask;
Assert.Equal(path, evt.Data.Path);
// Operation must be one of the defined enum values; create or update are both runtime-acceptable.
Assert.Contains(
evt.Data.Operation,
new[] { WorkspaceFileChangedOperation.Create, WorkspaceFileChangedOperation.Update });
}
[Fact]
public async Task Should_Emit_Title_Changed_Event_When_Name_Set()
{
var session = await CreateSessionAsync();
var title = $"Renamed-{Guid.NewGuid():N}";
// session.title_changed is ephemeral; it never lands in persisted history,
// so we must subscribe before invoking name.set.
var titleChangedTask = TestHelper.GetNextEventOfTypeAsync<SessionTitleChangedEvent>(
session,
evt => string.Equals(evt.Data.Title, title, StringComparison.Ordinal),
EventTimeout,
timeoutDescription: "session.title_changed event after name.set");
await session.Rpc.Name.SetAsync(title);
var evt = await titleChangedTask;
Assert.Equal(title, evt.Data.Title);
}
[Fact]
public async Task Should_Emit_Snapshot_Rewind_Event_And_Remove_Events_On_Truncate()
{
var session = await CreateSessionAsync();
// Send a real user.message; only after one is observed does the runtime
// begin persisting buffered events to disk (LocalSessionManager.SessionWriter
// gates flushing on shouldSaveSession, which flips on the first user.message).
await session.SendAndWaitAsync(new MessageOptions { Prompt = "Say SNAPSHOT_REWIND_TARGET exactly." });
var messages = await session.GetEventsAsync();
var userEvent = messages.OfType<UserMessageEvent>().FirstOrDefault()
?? throw new InvalidOperationException("Expected at least one user.message in persisted history");
var targetEventId = userEvent.Id.ToString();
// session.snapshot_rewind is ephemeral; must subscribe before invoking truncate.
var rewindTask = TestHelper.GetNextEventOfTypeAsync<SessionSnapshotRewindEvent>(
session,
evt => string.Equals(evt.Data.UpToEventId, targetEventId, StringComparison.OrdinalIgnoreCase),
EventTimeout,
timeoutDescription: "session.snapshot_rewind event after truncate");
var truncateResult = await session.Rpc.History.TruncateAsync(targetEventId);
Assert.True(truncateResult.EventsRemoved >= 1, "Expected truncate to remove at least the targeted event");
var rewindEvent = await rewindTask;
Assert.Equal(targetEventId, rewindEvent.Data.UpToEventId, ignoreCase: true);
Assert.Equal(truncateResult.EventsRemoved, (long)rewindEvent.Data.EventsRemoved);
// Verify the truncated event is no longer in persisted history.
var messagesAfter = await session.GetEventsAsync();
Assert.DoesNotContain(messagesAfter, e => e.Id == userEvent.Id);
}
[Fact]
public async Task Should_Allow_Session_Use_After_Truncate()
{
var session = await CreateSessionAsync();
await session.SendAndWaitAsync(new MessageOptions { Prompt = "Say SNAPSHOT_REWIND_TARGET exactly." });
var messages = await session.GetEventsAsync();
var userEvent = messages.OfType<UserMessageEvent>().FirstOrDefault()
?? throw new InvalidOperationException("Expected at least one user.message in persisted history");
var truncateResult = await session.Rpc.History.TruncateAsync(userEvent.Id.ToString());
Assert.True(truncateResult.EventsRemoved >= 1);
// After truncation the session should still respond to RPC.
var afterMode = await session.Rpc.Mode.GetAsync();
Assert.True(afterMode == SessionMode.Interactive || afterMode == SessionMode.Plan || afterMode == SessionMode.Autopilot);
// Workspace surface still works.
_ = await session.Rpc.Workspaces.GetWorkspaceAsync();
}
}