Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions modules/composition/composition.tests/GlobalUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,18 @@
global using Tools;
global using Tools.Handlers;
global using Xunit.Abstractions;
global using JoinCode.Reasoning.Cone;
global using JoinCode.Reasoning.Engine;
global using JoinCode.Reasoning.Evidence;
global using JoinCode.Reasoning.State;
global using JoinCode.Abstractions.Security.Sandbox;
global using JoinCode.Abstractions.Models.Voice;
global using PluginContext = JoinCode.Abstractions.Entity.PluginContext;
global using Infrastructure.Utils.Resilience;
global using AgentInfo = Core.Agents.Coordinator.AgentInfo;
global using AgentMsg = JoinCode.Abstractions.Models.Agent.CoordinatorMessage;
global using CompressionContentType = JoinCode.Abstractions.Interfaces.Context.ContentType;
global using BudgetStatus = JoinCode.Reasoning.Engine.BudgetStatus;
global using CostTracker = Core.CostTracking.CostTracker;
global using IMcpClient = JoinCode.Abstractions.Mcp.Client.IMcpClient;
global using IToolRegistry = JoinCode.Abstractions.Tools.IToolRegistry;
Expand Down
164 changes: 164 additions & 0 deletions modules/composition/composition.tests/commands/AddDirCommandTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
namespace Composition.Tests.Commands;

/// <summary>
/// /add-dir 命令单元测试 — 覆盖空参列目录/目录不存在/成功添加(信任联动+会话提示)/
/// 重复添加/--remember 持久化(含重复幂等)/工作区服务缺失降级/已信任不重复信任
/// <para>IWorkspaceService 经 CommandServices 字段注入;ITrustFolderManager 与 IConfigurationService 经 DI 注入;
/// 目录存在性经 InMemoryFileSystem 验证</para>
/// </summary>
[Collection("command-output")]
public sealed class AddDirCommandTest {
private static (AddDirCommand Command, Mock<IWorkspaceService> Workspace, Mock<ITrustFolderManager> Trust, Mock<IConfigurationService> Config, ChatCommandContext Context, IFileSystem Fs) Arrange(
string arguments = "",
Mock<IWorkspaceService>? workspaceOverride = null,
bool withWorkspace = true,
Mock<ITrustFolderManager>? trustOverride = null,
Mock<IConfigurationService>? configOverride = null,
bool addReturns = true,
IReadOnlyList<string>? additionalDirs = null) {
var workspace = workspaceOverride ?? new Mock<IWorkspaceService>();
workspace.Setup(w => w.AddDirectory(It.IsAny<string>())).Returns(addReturns);
workspace.Setup(w => w.GetAdditionalDirectories()).Returns(additionalDirs ?? []);

var trust = trustOverride ?? new Mock<ITrustFolderManager>();
if (trustOverride is null) {
trust.Setup(t => t.IsTrusted(It.IsAny<string>())).Returns(false);
}

var config = configOverride ?? new Mock<IConfigurationService>();
if (configOverride is null) {
config.Setup(c => c.GetAsync("permissions.additionalDirectories", It.IsAny<CancellationToken>()))
.ReturnsAsync((string?)null);
}

var fs = TestFileSystem.Current;
var services = CommandTestHarness.CreateServices(
fileSystem: fs,
workspaceService: withWorkspace ? workspace.Object : null,
configureDi: di => {
di.AddSingleton(trust.Object);
di.AddSingleton(config.Object);
});
var context = CommandTestHarness.CreateContext(services, arguments: arguments);
return (new AddDirCommand(), workspace, trust, config, context, fs);
}

[Fact]
public async Task Execute_EmptyArgs_ListsCurrentDirectories() {
var (cmd, _, _, _, context, _) = Arrange(arguments: "", additionalDirs: [@"C:\extra\a", @"C:\extra\b"]);

var (result, output) = await CommandTestHarness.CaptureOutputAsync(() => cmd.ExecuteAsync(context));

result.ShouldContinue.Should().BeTrue();
result.IsHandled.Should().BeTrue();
output.Should().Contain("当前额外工作目录:");
output.Should().Contain(@"C:\extra\a");
output.Should().Contain(@"C:\extra\b");
}

[Fact]
public async Task Execute_MissingDirectory_ShowsErrorWithoutAdding() {
var (cmd, workspace, _, _, context, _) = Arrange(arguments: @"C:\no-such-dir");

var (result, output) = await CommandTestHarness.CaptureOutputAsync(() => cmd.ExecuteAsync(context));

result.ShouldContinue.Should().BeTrue();
output.Should().Contain("目录不存在: C:\\no-such-dir");
workspace.Verify(w => w.AddDirectory(It.IsAny<string>()), Times.Never, "目录不存在时不应添加");
}

[Fact]
public async Task Execute_ExistingDirectory_AddsAndTrustsDirectory() {
var dir = Path.Combine(Path.GetTempPath(), "adddir-basic");
var (cmd, workspace, trust, _, context, fs) = Arrange(arguments: dir);
fs.CreateDirectory(dir);

var (result, output) = await CommandTestHarness.CaptureOutputAsync(() => cmd.ExecuteAsync(context));

result.ShouldContinue.Should().BeTrue();
var fullPath = Path.GetFullPath(dir);
workspace.Verify(w => w.AddDirectory(fullPath), Times.Once, "应添加 GetFullPath 归一后的目录");
trust.Verify(t => t.Trust(fullPath), Times.Once, "添加成功时应联动信任该目录");
output.Should().Contain($"已添加工作目录: {fullPath} (仅本次会话)");
output.Should().Contain("/permissions workspace 管理工作区目录");
}

[Fact]
public async Task Execute_AlreadyAddedDirectory_ShowsExistsWithoutRetrust() {
var dir = Path.Combine(Path.GetTempPath(), "adddir-dup");
var (cmd, workspace, trust, _, context, fs) = Arrange(arguments: dir, addReturns: false);
fs.CreateDirectory(dir);

var (result, output) = await CommandTestHarness.CaptureOutputAsync(() => cmd.ExecuteAsync(context));

result.ShouldContinue.Should().BeTrue();
output.Should().Contain($"目录已存在: {Path.GetFullPath(dir)}");
trust.Verify(t => t.Trust(It.IsAny<string>()), Times.Never, "重复添加不应再次触发信任");
}

[Fact]
public async Task Execute_WithRemember_PersistsToLocalSettings() {
var dir = Path.Combine(Path.GetTempPath(), "adddir-remember");
var (cmd, _, _, config, context, fs) = Arrange(arguments: $"{dir} --remember");
fs.CreateDirectory(dir);

var (result, output) = await CommandTestHarness.CaptureOutputAsync(() => cmd.ExecuteAsync(context));

result.ShouldContinue.Should().BeTrue();
var fullPath = Path.GetFullPath(dir);
config.Verify(c => c.SetAsync("permissions.additionalDirectories", fullPath, It.IsAny<CancellationToken>()),
Times.Once, "--remember 应把目录追加持久化到本地设置");
output.Should().Contain($"已添加工作目录: {fullPath} (已保存到本地设置)");
}

[Fact]
public async Task Execute_RememberAlreadyPersisted_DoesNotDuplicate() {
var dir = Path.Combine(Path.GetTempPath(), "adddir-remember-dup");
var fullPath = Path.GetFullPath(dir);
var config = new Mock<IConfigurationService>();
config.Setup(c => c.GetAsync("permissions.additionalDirectories", It.IsAny<CancellationToken>()))
.ReturnsAsync(fullPath);
var (cmd, _, _, _, context, fs) = Arrange(arguments: $"{dir} --remember", configOverride: config);
fs.CreateDirectory(dir);

var (result, _) = await CommandTestHarness.CaptureOutputAsync(() => cmd.ExecuteAsync(context));

result.ShouldContinue.Should().BeTrue();
config.Verify(c => c.SetAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()),
Times.Never, "目录已在持久化列表时不应重复写入相同目录");
}

[Fact]
public async Task Execute_WithoutWorkspaceService_DoesNotThrow() {
var dir = Path.Combine(Path.GetTempPath(), "adddir-nows");
var (cmd, workspace, _, _, context, fs) = Arrange(arguments: dir, withWorkspace: false);
fs.CreateDirectory(dir);

var (result, _) = await CommandTestHarness.CaptureOutputAsync(() => cmd.ExecuteAsync(context));

result.ShouldContinue.Should().BeTrue("工作区服务缺失应优雅降级而非抛异常");
workspace.Verify(w => w.AddDirectory(It.IsAny<string>()), Times.Never);
}

[Fact]
public async Task Execute_TrustedDirectory_DoesNotRetrust() {
var dir = Path.Combine(Path.GetTempPath(), "adddir-trusted");
var trust = new Mock<ITrustFolderManager>();
trust.Setup(t => t.IsTrusted(It.IsAny<string>())).Returns(true);
var (cmd, _, trustMock, _, context, fs) = Arrange(arguments: dir, trustOverride: trust);
fs.CreateDirectory(dir);

var (result, output) = await CommandTestHarness.CaptureOutputAsync(() => cmd.ExecuteAsync(context));

result.ShouldContinue.Should().BeTrue();
trustMock.Verify(t => t.Trust(It.IsAny<string>()), Times.Never, "目录已信任时不应重复调用 Trust");
output.Should().Contain("已添加工作目录:", "已信任不影响目录添加主路径");
}

[Fact]
public void NameAndUsage_MetadataShouldBeConsistent() {
var cmd = new AddDirCommand();
cmd.Name.Should().Be("add-dir");
cmd.Usage.Should().Be("/add-dir <path> [--remember]");
}
}
136 changes: 136 additions & 0 deletions modules/composition/composition.tests/commands/AdvisorCommandTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
namespace Composition.Tests.Commands;

/// <summary>
/// /advisor 命令单元测试 — 覆盖空参状态查询(启用/禁用)/off 与 unset 关闭/
/// 有效模型设置/未知模型警告仍设置/服务缺失静默/元数据一致性
/// <para>AdvisorService 用 Moq 桩经 DI 注入;ModelConfigLoader 用 Moq 桩构造注入</para>
/// </summary>
[Collection("command-output")]
public sealed class AdvisorCommandTest {
private static Mock<IModelConfigLoader> LoaderWithModels(params string[] modelIds) {
var loader = new Mock<IModelConfigLoader>();
loader.Setup(l => l.GetAllModelIds()).Returns(modelIds);
return loader;
}

private static (AdvisorCommand Command, ChatCommandContext Context, Mock<IAdvisorService> Service) Arrange(
string arguments = "",
Mock<IAdvisorService>? service = null,
Mock<IModelConfigLoader>? loader = null) {
var advisorService = service ?? new Mock<IAdvisorService>();
var services = CommandTestHarness.CreateServices(
configureDi: di => di.AddSingleton(advisorService.Object));
var context = CommandTestHarness.CreateContext(services, arguments: arguments);
return (new AdvisorCommand(loader?.Object), context, advisorService);
}

[Fact]
public async Task Execute_EmptyArgs_AdvisorEnabled_ShowsModelAndReviewHint() {
var service = new Mock<IAdvisorService>();
service.Setup(s => s.GetIsAdvisorEnabledAsync()).ReturnsAsync(true);
service.Setup(s => s.GetAdvisorModelAsync()).ReturnsAsync("gpt-4o");
var (cmd, context, _) = Arrange(service: service);

var (result, output) = await CommandTestHarness.CaptureOutputAsync(() => cmd.ExecuteAsync(context));

result.ShouldContinue.Should().BeTrue();
output.Should().Contain("顾问模型: gpt-4o");
output.Should().Contain("顾问将审查主模型的工作");
}

[Fact]
public async Task Execute_EmptyArgs_AdvisorDisabled_ShowsEnableHint() {
var service = new Mock<IAdvisorService>();
service.Setup(s => s.GetIsAdvisorEnabledAsync()).ReturnsAsync(false);
var (cmd, context, _) = Arrange(service: service);

var (result, output) = await CommandTestHarness.CaptureOutputAsync(() => cmd.ExecuteAsync(context));

result.ShouldContinue.Should().BeTrue();
output.Should().Contain("顾问模式:未启用");
output.Should().Contain("使用 /advisor <model> 启用顾问", "禁用状态应引导用户启用方式");
}

[Theory]
[InlineData("off")]
[InlineData("unset")]
[InlineData("OFF")]
[InlineData("Unset")]
public async Task Execute_OffOrUnset_ClearsAdvisorModel(string arguments) {
var service = new Mock<IAdvisorService>();
var (cmd, context, _) = Arrange(arguments: arguments, service: service);

var (result, output) = await CommandTestHarness.CaptureOutputAsync(() => cmd.ExecuteAsync(context));

result.ShouldContinue.Should().BeTrue();
service.Verify(s => s.ClearAdvisorModel(), Times.Once, $"{arguments} 应关闭顾问模式(大小写不敏感)");
output.Should().Contain("顾问模式:已禁用");
}

[Fact]
public async Task Execute_KnownModel_SetsAdvisorModelDirectly() {
var service = new Mock<IAdvisorService>();
var loader = LoaderWithModels("gpt-4o", "claude-3");
var (cmd, context, _) = Arrange(arguments: "claude-3", service: service, loader: loader);

var (result, output) = await CommandTestHarness.CaptureOutputAsync(() => cmd.ExecuteAsync(context));

result.ShouldContinue.Should().BeTrue();
service.Verify(s => s.SetAdvisorModel("claude-3"), Times.Once, "已知模型应直接设置");
output.Should().Contain("顾问模型: claude-3");
output.Should().Contain("顾问将审查主模型的工作");
output.Should().NotContain("已知支持模型:", "已知模型不应触发支持列表警告");
}

[Fact]
public async Task Execute_UnknownModel_WarnsButStillSets() {
var service = new Mock<IAdvisorService>();
var loader = LoaderWithModels("gpt-4o", "claude-3");
var (cmd, context, _) = Arrange(arguments: "llama-x", service: service, loader: loader);

var (result, output) = await CommandTestHarness.CaptureOutputAsync(() => cmd.ExecuteAsync(context));

result.ShouldContinue.Should().BeTrue();
output.Should().Contain("模型 'llama-x' 可能不支持顾问功能", "未知模型应给出警告");
output.Should().Contain("已知支持模型:");
output.Should().Contain(" gpt-4o");
output.Should().Contain(" claude-3", "警告应列出全部已知模型辅助选择");
output.Should().Contain("确定设置?输入 /advisor <model> 确认");
service.Verify(s => s.SetAdvisorModel("llama-x"), Times.Once,
"未知模型仍执行设置(警告不拦截,交由用户确认)");
}

[Fact]
public async Task Execute_UnknownModel_WithoutLoader_StillSetsModel() {
var service = new Mock<IAdvisorService>();
var (cmd, context, _) = Arrange(arguments: "any-model", service: service, loader: null);

var (result, output) = await CommandTestHarness.CaptureOutputAsync(() => cmd.ExecuteAsync(context));

result.ShouldContinue.Should().BeTrue();
service.Verify(s => s.SetAdvisorModel("any-model"), Times.Once,
"模型列表加载器缺失(空列表)时仍应设置模型");
output.Should().Contain("顾问模型: any-model");
}

[Fact]
public async Task Execute_WithoutService_ReturnsSilently() {
var services = CommandTestHarness.CreateServices();
var context = CommandTestHarness.CreateContext(services, arguments: "gpt-4o");

var (result, output) = await CommandTestHarness.CaptureOutputAsync(
() => new AdvisorCommand().ExecuteAsync(context));

result.ShouldContinue.Should().BeTrue("服务缺失应静默返回而非崩溃");
output.Should().NotContain("顾问模型:", "无服务时不应渲染顾问状态");
}

[Fact]
public void NameAndUsage_MetadataShouldBeConsistent() {
var cmd = new AdvisorCommand();
cmd.Name.Should().Be("advisor");
cmd.Usage.Should().Be("/advisor [model|off]");
cmd.Description.Should().Contain("顾问模型");
cmd.ArgumentHint.Should().Be("[model|off]");
}
}
Loading