forked from admin/VolcanicAgenticCore
- A speech or download timeout (TaskCanceledException nobody asked for) is a failure with the user notice, not a silent shutdown; OpenAiSpeechToText maps it to "the request timed out."; UpdateRouter only treats the caller's cancellation as shutdown. - send: WorkspacePaths resolves the real path component by component (realpath), closing the directory-link-inside-target bypass; `.` reports a directory. - Image documents are attached inline only when their magic bytes are JPEG/PNG/GIF/WebP, under the detected type. - Audio goes to STT as an ASCII audio.<ext> name. - MCP Browser section mentions send only when it is active. - RunInOrderAsync takes a cancellation token; queued work is skipped once disposal gives up; SendReplyAsync stays private behind ReplyInOrderAsync. - Speech HttpClient handler recycles pooled connections; BuiltInTools.Send(); IExtensionHost.Channel is abstract; WorkspacePaths in its own file. - Tests: timeouts, symlink chains (Linux), sniffing, channel delivery failures, queued-work cancellation, gated voice ordering. Spec and docs updated.
44 lines
2.7 KiB
C#
44 lines
2.7 KiB
C#
using System.Text.Json;
|
|
using Volcanic.AgenticCore.Abstractions.Extensions;
|
|
using Volcanic.AgenticCore.Abstractions.Messages;
|
|
using Volcanic.AgenticCore.Abstractions.Providers;
|
|
using Volcanic.AgenticCore.Abstractions.Sessions;
|
|
using Volcanic.AgenticCore.Abstractions.Ui;
|
|
using Volcanic.AgenticCore.Features.Extensions;
|
|
using Volcanic.AgenticCore.Features.Models;
|
|
|
|
namespace Volcanic.AgenticCore.Tests.Support;
|
|
|
|
public sealed class StubExtensionHost : IExtensionHost
|
|
{
|
|
public List<string> Calls { get; } = [];
|
|
public string Cwd { get; init; } = Path.GetTempPath();
|
|
public string SessionId => "session-1";
|
|
public string Scope { get; init; } = "test-scope";
|
|
public IUserInterface Ui { get; init; } = NullUserInterface.Instance;
|
|
public IChatChannel? Channel { get; init; }
|
|
public IReadOnlySessionManager SessionManager => throw new NotSupportedException();
|
|
public IModelRegistry ModelRegistry { get; init; } = new ModelRegistry(new ApiKeyResolver(null, _ => null));
|
|
public Model? Model { get; set; }
|
|
public ThinkingLevel ThinkingLevel { get; set; } = ThinkingLevel.Off;
|
|
public TimeZoneInfo TimeZone { get; init; } = TimeZoneInfo.Utc;
|
|
public string? TimeZoneId { get; init; }
|
|
public CancellationToken Signal => CancellationToken.None;
|
|
public bool IsIdle() => true;
|
|
public void Abort() => Calls.Add("abort");
|
|
public bool HasPendingMessages() => false;
|
|
public ContextUsage? GetContextUsage() => null;
|
|
public string GetSystemPrompt() => "prompt";
|
|
public void RequestCompaction(CompactOptions? options) => Calls.Add("compact");
|
|
public Task WaitForIdleAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
|
public Task<SwitchResult> NewSessionAsync(NewSessionOptions? options, CancellationToken cancellationToken) => Task.FromResult(new SwitchResult(false));
|
|
public Task<SwitchResult> SwitchSessionAsync(string sessionId, CancellationToken cancellationToken) => Task.FromResult(new SwitchResult(false));
|
|
public void SendMessage(CustomMessage message, MessageDelivery delivery, bool triggerTurn) => Calls.Add($"send:{message.CustomType}:{delivery}:{triggerTurn}");
|
|
public void SendUserMessage(string text, IReadOnlyList<ImageContent>? images, MessageDelivery delivery) => Calls.Add($"user:{text}:{delivery}");
|
|
public void AppendEntry(string customType, JsonElement? data) => Calls.Add($"entry:{customType}");
|
|
public Task<bool> SetModelAsync(Model model, CancellationToken cancellationToken) { Model = model; return Task.FromResult(true); }
|
|
public void SetThinkingLevel(ThinkingLevel level) => ThinkingLevel = level;
|
|
public string? GetSessionName() => "name";
|
|
public void SetSessionName(string? name) => Calls.Add($"name:{name}");
|
|
}
|