301 lines
12 KiB
C#
301 lines
12 KiB
C#
using System.Net;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using FreeCode.Core.Enums;
|
|
using FreeCode.Core.Interfaces;
|
|
using FreeCode.Core.Models;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace FreeCode.Tests.Unit.Helpers;
|
|
|
|
public sealed class MockHttpHandler : DelegatingHandler
|
|
{
|
|
private readonly string _response;
|
|
private readonly HttpStatusCode _statusCode;
|
|
private int _callCount;
|
|
|
|
public MockHttpHandler(string response, HttpStatusCode statusCode = HttpStatusCode.OK)
|
|
{
|
|
_response = response ?? string.Empty;
|
|
_statusCode = statusCode;
|
|
}
|
|
|
|
public HttpRequestMessage? LastRequest { get; private set; }
|
|
|
|
public string? LastRequestBody { get; private set; }
|
|
|
|
public int RequestCount => _callCount;
|
|
|
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
|
{
|
|
Interlocked.Increment(ref _callCount);
|
|
LastRequest = request;
|
|
LastRequestBody = request.Content is null
|
|
? null
|
|
: request.Content.ReadAsStringAsync(cancellationToken).GetAwaiter().GetResult();
|
|
|
|
var contentType = _response.StartsWith("data:", StringComparison.Ordinal)
|
|
? "text/event-stream"
|
|
: "application/json";
|
|
|
|
var response = new HttpResponseMessage(_statusCode)
|
|
{
|
|
Content = new StringContent(_response, Encoding.UTF8, contentType)
|
|
};
|
|
|
|
return Task.FromResult(response);
|
|
}
|
|
}
|
|
|
|
public static class TestHelper
|
|
{
|
|
public static ToolExecutionContext CreateContext(
|
|
string? workingDirectory = null,
|
|
PermissionMode permissionMode = PermissionMode.Default,
|
|
IPermissionEngine? permissionEngine = null,
|
|
ILspClientManager? lspManager = null,
|
|
IBackgroundTaskManager? taskManager = null,
|
|
IServiceProvider? services = null)
|
|
{
|
|
return new ToolExecutionContext(
|
|
WorkingDirectory: workingDirectory ?? Directory.GetCurrentDirectory(),
|
|
PermissionMode: permissionMode,
|
|
AdditionalWorkingDirectories: [],
|
|
PermissionEngine: permissionEngine ?? new StubPermissionEngine(),
|
|
LspManager: lspManager ?? new StubLspClientManager(),
|
|
TaskManager: taskManager ?? new StubBackgroundTaskManager(),
|
|
Services: services ?? new SimpleServiceProvider());
|
|
}
|
|
|
|
public static ApiRequest CreateApiRequest(
|
|
string systemPrompt,
|
|
JsonElement? messages = null,
|
|
JsonElement? tools = null,
|
|
string? model = null)
|
|
{
|
|
var resolvedMessages = messages ?? JsonDocument.Parse("""
|
|
[
|
|
{
|
|
"role": "user",
|
|
"content": "Hello"
|
|
}
|
|
]
|
|
""").RootElement.Clone();
|
|
|
|
var resolvedTools = tools ?? JsonDocument.Parse("""
|
|
[
|
|
{
|
|
"name": "test_tool",
|
|
"description": "A test tool",
|
|
"input_schema": {
|
|
"type": "object",
|
|
"properties": {}
|
|
}
|
|
}
|
|
]
|
|
""").RootElement.Clone();
|
|
|
|
return new ApiRequest(systemPrompt, resolvedMessages, resolvedTools, model);
|
|
}
|
|
|
|
public static ApiRequest CreateMinimalApiRequest()
|
|
{
|
|
return CreateApiRequest("You are a system prompt.");
|
|
}
|
|
}
|
|
|
|
public static class TestContextFactory
|
|
{
|
|
public static ToolExecutionContext CreateContext(
|
|
string? workingDirectory = null,
|
|
PermissionMode permissionMode = PermissionMode.Default,
|
|
IPermissionEngine? permissionEngine = null,
|
|
ILspClientManager? lspManager = null,
|
|
IBackgroundTaskManager? taskManager = null,
|
|
IServiceProvider? services = null)
|
|
=> TestHelper.CreateContext(workingDirectory, permissionMode, permissionEngine, lspManager, taskManager, services);
|
|
}
|
|
|
|
public sealed class SimpleServiceProvider : IServiceProvider
|
|
{
|
|
private readonly Dictionary<Type, object?> _services = new();
|
|
private readonly Dictionary<Type, int> _callCounts = new();
|
|
|
|
public SimpleServiceProvider AddService(Type type, object? instance)
|
|
{
|
|
_services[type] = instance;
|
|
return this;
|
|
}
|
|
|
|
public SimpleServiceProvider AddService<T>(T instance)
|
|
where T : class
|
|
=> AddService(typeof(T), instance);
|
|
|
|
public object? GetService(Type serviceType)
|
|
{
|
|
_callCounts[serviceType] = GetCallCount(serviceType) + 1;
|
|
return _services.GetValueOrDefault(serviceType);
|
|
}
|
|
|
|
public int GetCallCount(Type serviceType)
|
|
=> _callCounts.GetValueOrDefault(serviceType);
|
|
}
|
|
|
|
public sealed class StubPermissionEngine : IPermissionEngine
|
|
{
|
|
public Func<string, object, ToolExecutionContext, Task<PermissionResult>> Handler { get; set; }
|
|
= static (_, _, _) => Task.FromResult(PermissionResult.Allowed());
|
|
|
|
public Task<PermissionResult> CheckAsync(string toolName, object input, ToolExecutionContext context)
|
|
=> Handler(toolName, input, context);
|
|
}
|
|
|
|
public sealed class StubLspClientManager : ILspClientManager
|
|
{
|
|
public bool IsConnected => false;
|
|
public Task InitializeAsync(CancellationToken ct = default) => Task.CompletedTask;
|
|
public Task ShutdownAsync() => Task.CompletedTask;
|
|
public object? GetServerForFile(string filePath) => null;
|
|
public Task<object?> EnsureServerStartedAsync(string filePath) => Task.FromResult<object?>(null);
|
|
public Task<T?> SendRequestAsync<T>(string filePath, string method, object? parameters) => Task.FromResult<T?>(default);
|
|
public Task OpenFileAsync(string filePath, string content) => Task.CompletedTask;
|
|
public Task ChangeFileAsync(string filePath, string content) => Task.CompletedTask;
|
|
public Task SaveFileAsync(string filePath) => Task.CompletedTask;
|
|
public Task CloseFileAsync(string filePath) => Task.CompletedTask;
|
|
}
|
|
|
|
public sealed class StubBackgroundTaskManager : IBackgroundTaskManager
|
|
{
|
|
public event EventHandler<TaskStateChangedEventArgs>? TaskStateChanged;
|
|
|
|
public Task<LocalShellTask> CreateShellTaskAsync(string command, System.Diagnostics.ProcessStartInfo psi)
|
|
=> Task.FromResult(new LocalShellTask { TaskId = Guid.NewGuid().ToString("N"), Command = command, ProcessStartInfo = psi });
|
|
|
|
public Task<LocalAgentTask> CreateAgentTaskAsync(string prompt, string? agentType, string? model)
|
|
=> Task.FromResult(new LocalAgentTask { TaskId = Guid.NewGuid().ToString("N"), Prompt = prompt, AgentType = agentType, Model = model });
|
|
|
|
public Task<RemoteAgentTask> CreateRemoteAgentTaskAsync(string sessionUrl)
|
|
=> Task.FromResult(new RemoteAgentTask { TaskId = Guid.NewGuid().ToString("N"), SessionUrl = sessionUrl });
|
|
|
|
public Task<DreamTask> CreateDreamTaskAsync(string triggerReason)
|
|
=> Task.FromResult(new DreamTask { TaskId = Guid.NewGuid().ToString("N"), TriggerReason = triggerReason });
|
|
|
|
public Task StopTaskAsync(string taskId) => Task.CompletedTask;
|
|
public Task<string?> GetTaskOutputAsync(string taskId) => Task.FromResult<string?>(null);
|
|
public IReadOnlyList<BackgroundTask> ListTasks() => [];
|
|
public BackgroundTask? GetTask(string taskId) => null;
|
|
}
|
|
|
|
public sealed class StubAuthService : IAuthService
|
|
{
|
|
public bool IsAuthenticated { get; set; }
|
|
public bool IsClaudeAiUser { get; set; }
|
|
public bool IsInternalUser { get; set; }
|
|
public event EventHandler? AuthStateChanged;
|
|
public Task LoginAsync(string provider = "anthropic") => Task.CompletedTask;
|
|
public Task LogoutAsync() => Task.CompletedTask;
|
|
public Task<string?> GetOAuthTokenAsync() => Task.FromResult<string?>(null);
|
|
}
|
|
|
|
public sealed class StubToolRegistry : IToolRegistry
|
|
{
|
|
public IReadOnlyList<ITool> Tools { get; set; } = [];
|
|
public Task<IReadOnlyList<ITool>> GetToolsAsync(ToolPermissionContext? permissionContext = null) => Task.FromResult(Tools);
|
|
}
|
|
|
|
public sealed class StubCommandRegistry : ICommandRegistry
|
|
{
|
|
public IReadOnlyList<ICommand> Commands { get; set; } = [];
|
|
public IReadOnlyList<ICommand>? EnabledCommandsOverride { get; set; }
|
|
public Task<IReadOnlyList<ICommand>> GetCommandsAsync() => Task.FromResult(Commands);
|
|
public Task<IReadOnlyList<ICommand>> GetEnabledCommandsAsync() => Task.FromResult(EnabledCommandsOverride ?? Commands);
|
|
}
|
|
|
|
public sealed class StubSessionMemoryService : ISessionMemoryService
|
|
{
|
|
public string? CurrentMemory { get; set; }
|
|
public List<IReadOnlyList<Message>> ExtractedMessages { get; } = [];
|
|
public Task<string?> GetCurrentMemoryAsync() => Task.FromResult(CurrentMemory);
|
|
public Task TryExtractAsync(IReadOnlyList<Message> messages)
|
|
{
|
|
ExtractedMessages.Add(messages);
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
public sealed class StubFeatureFlagService : IFeatureFlagService
|
|
{
|
|
public HashSet<string> EnabledFlags { get; } = new(StringComparer.OrdinalIgnoreCase);
|
|
public bool IsEnabled(string featureFlag) => EnabledFlags.Contains(featureFlag);
|
|
public IReadOnlySet<string> GetEnabledFlags() => EnabledFlags;
|
|
}
|
|
|
|
public sealed class StubCompanionService : ICompanionService
|
|
{
|
|
public Func<string, Companion> Factory { get; set; } = static seed => new Companion(Species.Cat, Eye.Blue, Hat.None, Rarity.Common, seed);
|
|
public List<string> Seeds { get; } = [];
|
|
|
|
public Companion Create(string seed)
|
|
{
|
|
Seeds.Add(seed);
|
|
return Factory(seed);
|
|
}
|
|
}
|
|
|
|
public sealed class StubTool : ITool
|
|
{
|
|
public string Name { get; init; } = string.Empty;
|
|
public string[]? Aliases { get; init; }
|
|
public string? SearchHint { get; init; }
|
|
public ToolCategory Category { get; init; } = ToolCategory.FileSystem;
|
|
public bool Enabled { get; init; } = true;
|
|
public Func<object?, Task<string>> DescriptionFactory { get; init; } = static _ => Task.FromResult(string.Empty);
|
|
public Func<object, bool> ConcurrencySafeFactory { get; init; } = static _ => true;
|
|
public Func<object, bool> ReadOnlyFactory { get; init; } = static _ => true;
|
|
public JsonElement InputSchema { get; init; } = JsonDocument.Parse("{}").RootElement.Clone();
|
|
|
|
public bool IsEnabled() => Enabled;
|
|
public JsonElement GetInputSchema() => InputSchema;
|
|
public Task<string> GetDescriptionAsync(object? input = null) => DescriptionFactory(input);
|
|
public bool IsConcurrencySafe(object input) => ConcurrencySafeFactory(input);
|
|
public bool IsReadOnly(object input) => ReadOnlyFactory(input);
|
|
}
|
|
|
|
public sealed class StubQueryEngine : IQueryEngine
|
|
{
|
|
public Func<string, SubmitMessageOptions?, CancellationToken, IAsyncEnumerable<SDKMessage>> SubmitHandler { get; set; }
|
|
= static (_, _, _) => Empty();
|
|
|
|
public IReadOnlyList<Message> Messages { get; set; } = [];
|
|
public TokenUsage Usage { get; set; } = new(0, 0, 0, 0);
|
|
|
|
public IAsyncEnumerable<SDKMessage> SubmitMessageAsync(string content, SubmitMessageOptions? options = null, CancellationToken ct = default)
|
|
=> SubmitHandler(content, options, ct);
|
|
|
|
public Task CancelAsync() => Task.CompletedTask;
|
|
public IReadOnlyList<Message> GetMessages() => Messages;
|
|
public TokenUsage GetCurrentUsage() => Usage;
|
|
|
|
private static async IAsyncEnumerable<SDKMessage> Empty()
|
|
{
|
|
yield break;
|
|
}
|
|
}
|
|
|
|
public sealed class TestLogger<T> : ILogger<T>
|
|
{
|
|
public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullDisposable.Instance;
|
|
public bool IsEnabled(LogLevel logLevel) => false;
|
|
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
|
{
|
|
}
|
|
|
|
private sealed class NullDisposable : IDisposable
|
|
{
|
|
public static NullDisposable Instance { get; } = new();
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
}
|