using Microsoft.Extensions.Options; using TermRemoteCtl.Agent.Configuration; using TermRemoteCtl.Agent.History; using TermRemoteCtl.Agent.Sessions; using TermRemoteCtl.Agent.Terminal; namespace TermRemoteCtl.Agent.Tests.Terminal; public class PowerShellSessionHostTests { [Fact] public async Task ResizeAsync_Forwards_To_ConPty_Session() { var factory = new FakeConPtySessionFactory(); using var harness = HostHarness.Create(factory); await using var host = harness.Host; await host.StartAsync("alpha", CancellationToken.None); await host.ResizeAsync("alpha", 120, 40, CancellationToken.None); Assert.Equal((120, 40), factory.Session.ResizeCalls.Single()); } [Fact] public async Task Session_Output_Is_Captured_In_Registry_History() { var factory = new FakeConPtySessionFactory(); using var harness = HostHarness.Create(factory, lineLimit: 3); var session = harness.Registry.Create("Shell", DateTimeOffset.UtcNow); await using var host = harness.Host; await host.StartAsync(session.SessionId, CancellationToken.None); factory.Session.EmitOutput(session.SessionId, "one\ntwo\nthree\n"); var history = harness.Registry.GetHistory(session.SessionId, 2); Assert.Equal(["two", "three"], history.Lines); Assert.True(history.HasMoreAbove); } private sealed class HostHarness : IDisposable { private HostHarness(string dataRoot, SessionRegistry registry, PowerShellSessionHost host) { DataRoot = dataRoot; Registry = registry; Host = host; } public string DataRoot { get; } public SessionRegistry Registry { get; } public PowerShellSessionHost Host { get; } public static HostHarness Create(FakeConPtySessionFactory factory, int lineLimit = 4000) { var dataRoot = Path.Combine(Path.GetTempPath(), "TermRemoteCtl.Tests", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(dataRoot); var options = Options.Create(new AgentOptions { DataRoot = dataRoot, RingBufferLineLimit = lineLimit, }); var registry = new SessionRegistry(new SessionHistoryStore(dataRoot), options); var host = new PowerShellSessionHost(factory, registry); return new HostHarness(dataRoot, registry, host); } public void Dispose() { if (Directory.Exists(DataRoot)) { Directory.Delete(DataRoot, true); } } } private sealed class FakeConPtySessionFactory : IConPtySessionFactory { public FakeConPtySession Session { get; } = new(); public IConPtySession Create(string sessionId) { Session.SessionId = sessionId; return Session; } } private sealed class FakeConPtySession : IConPtySession { public string SessionId { get; set; } = string.Empty; public event EventHandler? OutputReceived; public List<(int Columns, int Rows)> ResizeCalls { get; } = new(); public Task StartAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } public Task WriteInputAsync(string input, CancellationToken cancellationToken) { return Task.CompletedTask; } public Task ResizeAsync(int columns, int rows, CancellationToken cancellationToken) { ResizeCalls.Add((columns, rows)); return Task.CompletedTask; } public void EmitOutput(string sessionId, string chunk) { OutputReceived?.Invoke(this, new TerminalOutputEventArgs(sessionId, chunk)); } public ValueTask DisposeAsync() { return ValueTask.CompletedTask; } } }