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
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
namespace MockServer.E2E.Tests;

/// <summary>
/// P0 bug 回归锁定 E2E 覆盖测试 — TASK065 第六波
/// 锁定第三/五波修复的生产 bug 不复发: /vendor catalog 崩溃降级、/mcp add 子命令识别、/model 别名透传直切、/effort 设置
/// 真实 jcc 进程 + MockServer 模拟 LLM,NonInteractive 单轮形态
/// </summary>
public sealed class ChatCommandP0RegressionCoverageTests : CoverageTestBase {
public ChatCommandP0RegressionCoverageTests(ITestOutputHelper output) : base(output) { }

// ============================================================
// Phase 6: P0 bug 回归锁定 — 2026-10-23 新增
// ============================================================

/// <summary>/vendor 无参 — VendorCommand catalog 崩溃修复回归(无参列出供应商)</summary>
[Fact]
public async Task VendorCommand_ShouldListVendors() {
await RunScriptAsync(ChatCommandConversationScripts.VendorCommand).ConfigureAwait(true);
}

/// <summary>/vendor deepseek — VendorCommand 切换降级修复回归(catalog 可用时正常切换)</summary>
[Fact]
public async Task VendorSwitchDeepSeekCommand_ShouldSwitchVendor() {
await RunScriptAsync(ChatCommandConversationScripts.VendorSwitchDeepSeekCommand).ConfigureAwait(true);
}

/// <summary>/mcp add testsvr — MCP add 子命令识别修复回归(输出用法提示而非未知操作)</summary>
[Fact]
public async Task McpAddUsageCommand_ShouldShowAddUsage() {
await RunScriptAsync(ChatCommandConversationScripts.McpAddUsageCommand).ConfigureAwait(true);
}

/// <summary>/model gpt-4o — ModelCommand catalog 崩溃修复回归(别名透传直切)</summary>
[Fact]
public async Task ModelSwitchCommand_ShouldSwitchModelDirectly() {
await RunScriptAsync(ChatCommandConversationScripts.ModelSwitchCommand).ConfigureAwait(true);
}

/// <summary>/effort high — EffortCommand 设置形态回归(输出力度设置成功)</summary>
[Fact]
public async Task EffortSetHighCommand_ShouldSetEffortLevel() {
await RunScriptAsync(ChatCommandConversationScripts.EffortSetHighCommand).ConfigureAwait(true);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace MockServer.E2E.Tests;

/// <summary>
/// P1 高频命令 E2E 覆盖测试 — TASK065 第六波补覆盖
/// 包含: /debuglog -e(错误段查询)、/??(需求澄清)、/sampling(无参查询)、/switch(无参显示模式)、/diag recent(崩溃快照查询)
/// </summary>
public sealed class ChatCommandP1CoverageTests : CoverageTestBase {
public ChatCommandP1CoverageTests(ITestOutputHelper output) : base(output) { }

/// <summary>/debuglog -e — 断言错误段标题输出且无错误</summary>
[Fact]
public async Task DebugLogErrorCommand_ShouldShowErrorSection() {
await RunScriptAsync(ChatCommandConversationScripts.DebugLogErrorCommand).ConfigureAwait(true);
}

/// <summary>/?? 需求描述 — mock 响应含 ClarifyDoneMarker,断言澄清模式横幅输出且无错误</summary>
[Fact]
public async Task AskClarifyCommand_ShouldCompleteClarifyLoop() {
await RunScriptAsync(ChatCommandConversationScripts.AskClarifyCommand).ConfigureAwait(true);
}

/// <summary>/sampling 无参 — 断言采样参数查询输出且无错误</summary>
[Fact]
public async Task SamplingCommand_ShouldShowSamplingParams() {
await RunScriptAsync(ChatCommandConversationScripts.SamplingCommand).ConfigureAwait(true);
}

/// <summary>/switch 无参 — 断言当前模式输出且无错误</summary>
[Fact]
public async Task SwitchCommand_ShouldShowCurrentMode() {
await RunScriptAsync(ChatCommandConversationScripts.SwitchCommand).ConfigureAwait(true);
}

/// <summary>/diag recent — 断言崩溃快照查询输出且无错误</summary>
[Fact]
public async Task DiagRecentCommand_ShouldShowCrashSnapshotReport() {
await RunScriptAsync(ChatCommandConversationScripts.DiagRecentCommand).ConfigureAwait(true);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
namespace MockServer.E2E.Tests;

/// <summary>
/// P2 隔离写命令 E2E 覆盖测试 — TASK065 第六波补覆盖
/// 包含: /reset-config all(JCC_APP_DATA_FOLDER 隔离目录重置)、/code-index(fixture 目录索引重建)
/// 隔离验证: /reset-config 写入目标为 runner 设置的 %TEMP%\jcc_test_{guid}(Path.Combine 对绝对路径直接采用),不碰真实 ~/.jcc
/// </summary>
public sealed class ChatCommandP2IsolatedCoverageTests : CoverageTestBase {
public ChatCommandP2IsolatedCoverageTests(ITestOutputHelper output) : base(output) { }

/// <summary>/reset-config all — 断言隔离 AppData 配置重置输出(重置开始/配置树骨架/重置完成)且无错误</summary>
[Fact]
public async Task ResetConfigAllCommand_ShouldResetIsolatedAppData() {
await RunScriptAsync(ChatCommandConversationScripts.ResetConfigAllCommand).ConfigureAwait(true);
}

/// <summary>/code-index — 断言 fixture 目录索引构建完成(构建开始/完成提示/统计输出)且无错误</summary>
[Fact]
public async Task CodeIndexCommand_ShouldBuildIndexInFixtureDir() {
var fixtureDir = await CreateCodeIndexFixtureAsync().ConfigureAwait(true);
ChatCommandConversationScripts.CodeIndexFixtureDirectory = fixtureDir;
try {
await RunScriptAsync(ChatCommandConversationScripts.CodeIndexCommand).ConfigureAwait(true);
} finally {
ChatCommandConversationScripts.CodeIndexFixtureDirectory = null;
CleanupCodeIndexFixture(fixtureDir);
}
}

/// <summary>
/// 创建 /code-index fixture 目录 — %TEMP%\jcc_e2e_codeindex_{guid} + 2 个小 .cs 文件
/// 小目录控制 RebuildIndexAsync 耗时(Ast 集成测试 WorkingDirectory 指向 fixture 的先例)
/// </summary>
/// <returns>fixture 目录绝对路径</returns>
private static async Task<string> CreateCodeIndexFixtureAsync() {
var fs = TestConfiguration.FileSystem;
var fixtureDir = fs.CombinePath(Path.GetTempPath(), $"jcc_e2e_codeindex_{Guid.NewGuid():N}");
fs.CreateDirectory(fixtureDir);
await fs.WriteAllTextAsync(fs.CombinePath(fixtureDir, "Calculator.cs"), CalculatorFixtureSource).ConfigureAwait(true);
await fs.WriteAllTextAsync(fs.CombinePath(fixtureDir, "Greeter.cs"), GreeterFixtureSource).ConfigureAwait(true);
return fixtureDir;
}

/// <summary>
/// 清理 fixture 目录(含索引产物 {fixture}/.jcc/code-index/)— %TEMP% 下测试自建产物
/// 清理失败不阻断测试结果(%TEMP% 残留无害,同 DualRoleConversationRunner 清理临时目录先例)
/// </summary>
/// <param name="fixtureDir">fixture 目录绝对路径</param>
private void CleanupCodeIndexFixture(string fixtureDir) {
try {
var fs = TestConfiguration.FileSystem;
if (fs.DirectoryExists(fixtureDir)) {
fs.DeleteDirectory(fixtureDir, recursive: true);
}
} catch (Exception ex) {
Output.WriteLine($"[P2] ⚠ fixture 清理失败(%TEMP% 残留无害): {ex.Message}");
}
}

/// <summary>fixture 源码 1 — 计算器(含类/方法符号,供索引器解析)</summary>
private const string CalculatorFixtureSource = """
namespace CodeIndexFixture;

/// <summary>fixture 计算器 — /code-index E2E 索引目标</summary>
public static class Calculator {
/// <summary>两数相加</summary>
public static int Add(int a, int b) => a + b;

/// <summary>两数相乘</summary>
public static int Multiply(int a, int b) => a * b;
}
""";

/// <summary>fixture 源码 2 — 问候器(含类/方法符号,供索引器解析)</summary>
private const string GreeterFixtureSource = """
namespace CodeIndexFixture;

/// <summary>fixture 问候器 — /code-index E2E 索引目标</summary>
public static class Greeter {
/// <summary>生成问候语</summary>
public static string Greet(string name) => $"Hello, {name}!";
}
""";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
namespace MockServer.E2E.Tests.Scripts;

/// <summary>
/// Phase 6 E2E 脚本 — TASK065 第六波 P0 bug 回归锁定
/// 锁定第三/五波修复的 4 类 bug: /vendor catalog 崩溃降级、/mcp add 子命令识别、/model 别名透传直切、/effort max 降级
/// 断言文案全部对齐命令源码真实输出(VendorCommand/McpCommand/ModelCommand/EffortCommand)
/// </summary>
public static partial class ChatCommandConversationScripts {
/// <summary>
/// /vendor 命令(无参) — 列出可用供应商
/// 对齐 VendorCommand.cs 无参分支: "=== 可用供应商 ===" + 枚举值逐行输出(" openai ← 当前")
/// </summary>
public static ConversationScript VendorCommand => new() {
Name = "/vendor 命令(无参列表)",
Turns =
[
new ConversationTurn
{
UserInput = "/vendor",
AiResponse = new MockResponseScript
{
Type = MockResponseType.TextOnly,
TextResponse = ""
},
Asserts =
[
new OutputAssert { Type = AssertType.ContainsText, Expected = "可用供应商", Description = "应输出可用供应商列表标题" },
new OutputAssert { Type = AssertType.ContainsText, Expected = "openai", Description = "列表应包含 openai 供应商(VendorKind 枚举遍历)" },
new OutputAssert { Type = AssertType.NoErrors, Expected = "", Description = "不应有错误" },
]
}
]
};

/// <summary>
/// /vendor deepseek 命令 — 切换供应商
/// 对齐 VendorCommand.cs 切换分支: "已切换供应商: openai → deepseek"(E2E 环境 JCC_VENDOR=openai,catalog 全装配不崩溃)
/// </summary>
public static ConversationScript VendorSwitchDeepSeekCommand => new() {
Name = "/vendor deepseek 命令(切换供应商)",
Turns =
[
new ConversationTurn
{
UserInput = "/vendor deepseek",
AiResponse = new MockResponseScript
{
Type = MockResponseType.TextOnly,
TextResponse = ""
},
Asserts =
[
new OutputAssert { Type = AssertType.ContainsText, Expected = "已切换供应商", Description = "应输出切换成功文案" },
new OutputAssert { Type = AssertType.ContainsText, Expected = "deepseek", Description = "目标供应商 deepseek 应出现在切换输出中" },
new OutputAssert { Type = AssertType.NotContainsText, Expected = "未知供应商", Description = "deepseek 在 VendorKind 枚举内,不应触发未知供应商拒绝" },
new OutputAssert { Type = AssertType.NoErrors, Expected = "", Description = "切换不应有错误(catalog 崩溃修复回归锁定)" },
]
}
]
};

/// <summary>
/// /mcp add 命令(缺少 url/command 参数) — AddServerAsync 用法提示分支
/// 对齐 McpCommand.cs AddServerAsync: args.Length&lt;3 时输出 "用法: /mcp add <name> <command|url> ..."
/// 锁定 CrudAction.Add 识别修复 — 修复前 add 走 default 分支输出 "未知操作: add"
/// </summary>
public static ConversationScript McpAddUsageCommand => new() {
Name = "/mcp add 命令(用法提示)",
Turns =
[
new ConversationTurn
{
UserInput = "/mcp add testsvr",
AiResponse = new MockResponseScript
{
Type = MockResponseType.TextOnly,
TextResponse = ""
},
Asserts =
[
new OutputAssert { Type = AssertType.ContainsText, Expected = "用法: /mcp add", Description = "应输出 add 子命令用法提示(AddServerAsync 分支)" },
new OutputAssert { Type = AssertType.NotContainsText, Expected = "未知操作", Description = "add 应被 CrudAction.Add case 识别,不应走 default 未知操作分支(bug 回归锁定)" },
new OutputAssert { Type = AssertType.NoErrors, Expected = "", Description = "不应有错误" },
]
}
]
};

/// <summary>
/// /model 命令(带参直切) — 别名透传直切
/// 对齐 ModelCommand.cs SwitchToModelAsync: "已切换模型: gpt-4o"(E2E settings.json openai 无别名表,ResolveAlias 返回 null 透传输入)
/// </summary>
public static ConversationScript ModelSwitchCommand => new() {
Name = "/model gpt-4o 命令(带参直切)",
Turns =
[
new ConversationTurn
{
UserInput = "/model gpt-4o",
AiResponse = new MockResponseScript
{
Type = MockResponseType.TextOnly,
TextResponse = ""
},
Asserts =
[
new OutputAssert { Type = AssertType.ContainsText, Expected = "已切换模型: gpt-4o", Description = "应输出直切成功文案(catalog 可用,别名透传)" },
new OutputAssert { Type = AssertType.NoErrors, Expected = "", Description = "切换不应有错误(catalog 崩溃修复回归锁定)" },
]
}
]
};

/// <summary>
/// /effort 命令(设置形态) — 设置推理力度 high
/// 对齐 EffortCommand.cs 设置分支: "推理力度: high (全面,广泛测试)"
/// </summary>
public static ConversationScript EffortSetHighCommand => new() {
Name = "/effort high 命令(设置推理力度)",
Turns =
[
new ConversationTurn
{
UserInput = "/effort high",
AiResponse = new MockResponseScript
{
Type = MockResponseType.TextOnly,
TextResponse = ""
},
Asserts =
[
new OutputAssert { Type = AssertType.ContainsText, Expected = "推理力度: high", Description = "应输出 high 力度设置成功文案" },
new OutputAssert { Type = AssertType.ContainsText, Expected = "全面,广泛测试", Description = "应输出 high 等级对应描述(GetEffortDescription)" },
new OutputAssert { Type = AssertType.NoErrors, Expected = "", Description = "不应有错误" },
]
}
]
};
}
Loading