diff --git a/docs/superpowers/plans/2026-07-24-buildflow-architecture-and-sop.md b/docs/superpowers/plans/2026-07-24-buildflow-architecture-and-sop.md index b24fc4a..3ca7927 100644 --- a/docs/superpowers/plans/2026-07-24-buildflow-architecture-and-sop.md +++ b/docs/superpowers/plans/2026-07-24-buildflow-architecture-and-sop.md @@ -400,11 +400,11 @@ git commit -m "refactor: classify backend business errors" - Create `tests/BuildFlow.Workflow.Tests/Form/FormTestFixtureClassFixture.cs` from `FormTestFixture.cs`. - Create `tests/BuildFlow.Workflow.Tests/Notification/NoOpPlatformNotificationPublisher.cs` from `NotificationServiceTests.cs`. -- [ ] **Step 1: Move declarations without changing behavior** +- [x] **Step 1: Move declarations without changing behavior** Preserve namespaces, modifiers, XML comments, JSON attributes, extension methods, and member order. Each original file retains its namesake type; each moved declaration gets a same-named file. -- [ ] **Step 2: Run the file organization guard** +- [x] **Step 2: Run the file organization guard** ```bash dotnet test tests/BuildFlow.Architecture.Tests/BuildFlow.Architecture.Tests.csproj --no-restore --filter FullyQualifiedName~SourceFilesShouldContainAtMostOneTopLevelClassRecordOrEnum @@ -412,7 +412,7 @@ dotnet test tests/BuildFlow.Architecture.Tests/BuildFlow.Architecture.Tests.cspr Expected: PASS with zero violations under `src` and `tests`. -- [ ] **Step 3: Run affected projects** +- [x] **Step 3: Run affected projects** ```bash dotnet test tests/BuildFlow.Platform.Tests/BuildFlow.Platform.Tests.csproj --no-restore @@ -423,7 +423,7 @@ dotnet build src/BuildFlow.Host/BuildFlow.Host.csproj --no-restore Expected: PASS, 0 warning, 0 error. -- [ ] **Step 4: Commit** +- [x] **Step 4: Commit** ```bash git add src/BuildFlow.Host/Configuration src/modules/workflow/Domain/Engine tests diff --git a/src/BuildFlow.Host/Configuration/ConsulConfigurationProvider.cs b/src/BuildFlow.Host/Configuration/ConsulConfigurationProvider.cs index b2e5a53..fbfd50b 100644 --- a/src/BuildFlow.Host/Configuration/ConsulConfigurationProvider.cs +++ b/src/BuildFlow.Host/Configuration/ConsulConfigurationProvider.cs @@ -2,36 +2,6 @@ using System.Net.Http.Headers; namespace BuildFlow.Host.Configuration; -/// -/// 轻量 Consul KV 配置 Provider(自实现,替代 Winton.Extensions.Configuration.Consul)。 -/// 启动时用 Consul HTTP API 拉取 prefix 下所有 KV,扁平化注入 IConfiguration。 -/// 支持 ReloadOnChange(后台 long-poll watch)。 -/// -/// 与 Winton 的区别:完全可控、有日志、不黑盒;用 HttpClient 直接调 /v1/kv/?recurse=true。 -/// -/// KV key 映射:Consul key 去掉 root 前缀后,剩余部分直接作为 IConfiguration key。 -/// 如 Consul key "buildflow/Jwt:SigningKey",root="buildflow/",剩余 "Jwt:SigningKey" → IConfiguration key "Jwt:SigningKey"。 -/// -internal sealed class ConsulConfigurationSource : IConfigurationSource -{ - private readonly ConsulOptions _options; - - public ConsulConfigurationSource(ConsulOptions options) => _options = options; - - public IConfigurationProvider Build(IConfigurationBuilder builder) => - new ConsulConfigurationProvider(_options); -} - -internal sealed class ConsulOptions -{ - public required string Host { get; init; } // 如 http://43.134.235.58:8520 - public required string Token { get; init; } // ACL token - public required string KeyPrefix { get; init; } // 如 buildflow/ - public bool Optional { get; init; } = true; - public bool ReloadOnChange { get; init; } = true; - public TimeSpan PollInterval { get; init; } = TimeSpan.FromSeconds(30); -} - internal sealed class ConsulConfigurationProvider : ConfigurationProvider, IDisposable { private readonly ConsulOptions _options; @@ -160,14 +130,3 @@ internal sealed class ConsulConfigurationProvider : ConfigurationProvider, IDisp _loggerFactory?.Dispose(); } } - -// Consul KV API 响应项(recurse 接口) -internal sealed class ConsulKvItem -{ - public int CreateIndex { get; set; } - public int ModifyIndex { get; set; } - public string Key { get; set; } = ""; - public int LockIndex { get; set; } - public string? Value { get; set; } - public int Flags { get; set; } -} diff --git a/src/BuildFlow.Host/Configuration/ConsulConfigurationSource.cs b/src/BuildFlow.Host/Configuration/ConsulConfigurationSource.cs new file mode 100644 index 0000000..3475aa3 --- /dev/null +++ b/src/BuildFlow.Host/Configuration/ConsulConfigurationSource.cs @@ -0,0 +1,21 @@ +namespace BuildFlow.Host.Configuration; + +/// +/// 轻量 Consul KV 配置 Provider(自实现,替代 Winton.Extensions.Configuration.Consul)。 +/// 启动时用 Consul HTTP API 拉取 prefix 下所有 KV,扁平化注入 IConfiguration。 +/// 支持 ReloadOnChange(后台 long-poll watch)。 +/// +/// 与 Winton 的区别:完全可控、有日志、不黑盒;用 HttpClient 直接调 /v1/kv/?recurse=true。 +/// +/// KV key 映射:Consul key 去掉 root 前缀后,剩余部分直接作为 IConfiguration key。 +/// 如 Consul key "buildflow/Jwt:SigningKey",root="buildflow/",剩余 "Jwt:SigningKey" → IConfiguration key "Jwt:SigningKey"。 +/// +internal sealed class ConsulConfigurationSource : IConfigurationSource +{ + private readonly ConsulOptions _options; + + public ConsulConfigurationSource(ConsulOptions options) => _options = options; + + public IConfigurationProvider Build(IConfigurationBuilder builder) => + new ConsulConfigurationProvider(_options); +} diff --git a/src/BuildFlow.Host/Configuration/ConsulKvItem.cs b/src/BuildFlow.Host/Configuration/ConsulKvItem.cs new file mode 100644 index 0000000..84038f5 --- /dev/null +++ b/src/BuildFlow.Host/Configuration/ConsulKvItem.cs @@ -0,0 +1,12 @@ +namespace BuildFlow.Host.Configuration; + +// Consul KV API 响应项(recurse 接口) +internal sealed class ConsulKvItem +{ + public int CreateIndex { get; set; } + public int ModifyIndex { get; set; } + public string Key { get; set; } = ""; + public int LockIndex { get; set; } + public string? Value { get; set; } + public int Flags { get; set; } +} diff --git a/src/BuildFlow.Host/Configuration/ConsulOptions.cs b/src/BuildFlow.Host/Configuration/ConsulOptions.cs new file mode 100644 index 0000000..438dd18 --- /dev/null +++ b/src/BuildFlow.Host/Configuration/ConsulOptions.cs @@ -0,0 +1,11 @@ +namespace BuildFlow.Host.Configuration; + +internal sealed class ConsulOptions +{ + public required string Host { get; init; } // 如 http://43.134.235.58:8520 + public required string Token { get; init; } // ACL token + public required string KeyPrefix { get; init; } // 如 buildflow/ + public bool Optional { get; init; } = true; + public bool ReloadOnChange { get; init; } = true; + public TimeSpan PollInterval { get; init; } = TimeSpan.FromSeconds(30); +} diff --git a/src/modules/workflow/Domain/Engine/GraphEdge.cs b/src/modules/workflow/Domain/Engine/GraphEdge.cs new file mode 100644 index 0000000..1ea90e3 --- /dev/null +++ b/src/modules/workflow/Domain/Engine/GraphEdge.cs @@ -0,0 +1,49 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Workflow.Domain.Enums; + +namespace Workflow.Domain.Engine; + +/// +/// X6 边。source/target 用 { cell, port } 结构;几何信息(vertices/router/connector)放顶层; +/// 业务字段(edgeType/condition/order)放 data。 +/// +public sealed class GraphEdge +{ + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("shape")] + public string? Shape { get; set; } + + [JsonPropertyName("source")] + public GraphEndpoint Source { get; set; } = new(); + + [JsonPropertyName("target")] + public GraphEndpoint Target { get; set; } = new(); + + /// 业务数据:edgeType / condition / order。 + [JsonPropertyName("data")] + public JsonElement? Data { get; set; } + + [JsonPropertyName("label")] + public JsonElement? Label { get; set; } + + [JsonPropertyName("vertices")] + public List? Vertices { get; set; } + + [JsonPropertyName("router")] + public JsonElement? Router { get; set; } + + [JsonPropertyName("connector")] + public JsonElement? Connector { get; set; } + + /// 边类型(Normal=0/Approved=1/Rejected=2)。缺省为 Normal。 + public EdgeType GetEdgeType() => Data.TryGetInt32("edgeType") is { } v ? (EdgeType)v : EdgeType.Normal; + + /// 条件表达式(Condition/ExclusiveGateway 出边)。 + public string? GetCondition() => Data.TryGetString("condition"); + + /// 分支评估顺序(值小优先)。 + public int GetOrder() => Data.TryGetInt32("order") ?? 0; +} diff --git a/src/modules/workflow/Domain/Engine/GraphEndpoint.cs b/src/modules/workflow/Domain/Engine/GraphEndpoint.cs new file mode 100644 index 0000000..3a1a1ca --- /dev/null +++ b/src/modules/workflow/Domain/Engine/GraphEndpoint.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace Workflow.Domain.Engine; + +public sealed class GraphEndpoint +{ + /// 节点 id(对应 GraphNode.Id)。 + [JsonPropertyName("cell")] + public string? Cell { get; set; } + + /// 端口 id(top/right/bottom/left)。 + [JsonPropertyName("port")] + public string? Port { get; set; } +} diff --git a/src/modules/workflow/Domain/Engine/GraphJsonExtensions.cs b/src/modules/workflow/Domain/Engine/GraphJsonExtensions.cs new file mode 100644 index 0000000..a58eb12 --- /dev/null +++ b/src/modules/workflow/Domain/Engine/GraphJsonExtensions.cs @@ -0,0 +1,25 @@ +using System.Text.Json; + +namespace Workflow.Domain.Engine; + +/// +/// JsonElement? 安全取值扩展:统一处理 Nullable 解构与 ValueKind 校验, +/// 避免 WorkflowGraph 各 getter 重复 null 检查。 +/// +internal static class GraphJsonExtensions +{ + public static string? TryGetString(this JsonElement? element, string name) => + element is { ValueKind: JsonValueKind.Object } el + && el.TryGetProperty(name, out var p) + && p.ValueKind == JsonValueKind.String + ? p.GetString() + : null; + + public static int? TryGetInt32(this JsonElement? element, string name) => + element is { ValueKind: JsonValueKind.Object } el + && el.TryGetProperty(name, out var p) + && (p.ValueKind == JsonValueKind.Number || p.ValueKind == JsonValueKind.String) + && p.TryGetInt32(out var n) + ? n + : null; +} diff --git a/src/modules/workflow/Domain/Engine/GraphNode.cs b/src/modules/workflow/Domain/Engine/GraphNode.cs new file mode 100644 index 0000000..cee918f --- /dev/null +++ b/src/modules/workflow/Domain/Engine/GraphNode.cs @@ -0,0 +1,45 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Workflow.Domain.Engine; + +/// +/// X6 节点。shape 决定渲染器并反推 NodeType(见 ); +/// 业务配置统一进 data;x/y/width/height/ports 为几何与渲染信息。 +/// +public sealed class GraphNode +{ + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("shape")] + public string Shape { get; set; } = string.Empty; + + [JsonPropertyName("x")] + public double X { get; set; } + + [JsonPropertyName("y")] + public double Y { get; set; } + + [JsonPropertyName("width")] + public double? Width { get; set; } + + [JsonPropertyName("height")] + public double? Height { get; set; } + + [JsonPropertyName("angle")] + public double? Angle { get; set; } + + [JsonPropertyName("visible")] + public bool? Visible { get; set; } + + /// 业务数据(节点配置)。替代旧 WorkflowNode.Config 字符串字段。 + [JsonPropertyName("data")] + public JsonElement? Data { get; set; } + + /// 显示名(对应旧 WorkflowNode.Name)。 + public string? GetTitle() => Data.TryGetString("title"); + + [JsonPropertyName("ports")] + public JsonElement? Ports { get; set; } +} diff --git a/src/modules/workflow/Domain/Engine/GraphPoint.cs b/src/modules/workflow/Domain/Engine/GraphPoint.cs new file mode 100644 index 0000000..8a827ab --- /dev/null +++ b/src/modules/workflow/Domain/Engine/GraphPoint.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Workflow.Domain.Engine; + +public sealed class GraphPoint +{ + [JsonPropertyName("x")] + public double X { get; set; } + + [JsonPropertyName("y")] + public double Y { get; set; } +} diff --git a/src/modules/workflow/Domain/Engine/TreeBranch.cs b/src/modules/workflow/Domain/Engine/TreeBranch.cs new file mode 100644 index 0000000..e569091 --- /dev/null +++ b/src/modules/workflow/Domain/Engine/TreeBranch.cs @@ -0,0 +1,26 @@ +using System.Text.Json.Serialization; + +namespace Workflow.Domain.Engine; + +/// 条件/并行分支容器中的一条分支。 +public sealed class TreeBranch +{ + /// 分支 id,纯 Guid 字符串。 + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// 分支显示名(如"条件 1"/"默认分支"/"分支 A")。 + [JsonPropertyName("label")] + public string? Label { get; set; } + + /// + /// 条件表达式(仅 ConditionBranch 使用;复用 ConditionEvaluator 语法,如 金额 > 5000)。 + /// 默认分支(else)置空,编译时 order 最大,引擎兜底命中。 + /// + [JsonPropertyName("condition")] + public string? Condition { get; set; } + + /// 该分支内的首节点(分支内又是顺序链;null 表示空分支,编译时直通收口)。 + [JsonPropertyName("child")] + public TreeNode? Child { get; set; } +} diff --git a/src/modules/workflow/Domain/Engine/TreeNode.cs b/src/modules/workflow/Domain/Engine/TreeNode.cs new file mode 100644 index 0000000..3f0910e --- /dev/null +++ b/src/modules/workflow/Domain/Engine/TreeNode.cs @@ -0,0 +1,41 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Workflow.Domain.Enums; + +namespace Workflow.Domain.Engine; + +/// +/// 树节点。一条顺序链上的节点通过 链接; +/// 容器型节点(条件/并行)通过 挂多条分支。 +/// +public sealed class TreeNode +{ + /// 节点 id,必须是纯 Guid 字符串(引擎 ParseGuid 硬约束)。 + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// 节点类型。决定渲染卡片与配置面板。 + [JsonPropertyName("type")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public TreeNodeTypeEnum Type { get; set; } + + /// + /// 节点业务配置(自由对象)。沿用引擎 node.data 的字段约定: + /// 审批节点:approvalMode/assigneeRules/timeoutMinutes/fieldPermissions 等; + /// 抄送节点:recipients;分支内首节点的 condition 在 上。 + /// + [JsonPropertyName("config")] + public JsonElement? Config { get; set; } + + /// 显示标题(冗余于 config.title,便于编辑器直接读取)。 + [JsonPropertyName("title")] + public string? Title { get; set; } + + /// 顺序链上的下一个节点(null 表示链尾,编译器自动接 End)。 + [JsonPropertyName("child")] + public TreeNode? Child { get; set; } + + /// 分支集合(仅 ConditionBranch / ParallelBranch 容器节点使用)。 + [JsonPropertyName("branches")] + public List? Branches { get; set; } +} diff --git a/src/modules/workflow/Domain/Engine/WorkflowGraph.cs b/src/modules/workflow/Domain/Engine/WorkflowGraph.cs index 2993c65..1bd8df2 100644 --- a/src/modules/workflow/Domain/Engine/WorkflowGraph.cs +++ b/src/modules/workflow/Domain/Engine/WorkflowGraph.cs @@ -20,130 +20,3 @@ public sealed class WorkflowGraph /// 空图工厂(容错用)。 public static WorkflowGraph Empty() => new(); } - -/// -/// X6 节点。shape 决定渲染器并反推 NodeType(见 ); -/// 业务配置统一进 data;x/y/width/height/ports 为几何与渲染信息。 -/// -public sealed class GraphNode -{ - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - [JsonPropertyName("shape")] - public string Shape { get; set; } = string.Empty; - - [JsonPropertyName("x")] - public double X { get; set; } - - [JsonPropertyName("y")] - public double Y { get; set; } - - [JsonPropertyName("width")] - public double? Width { get; set; } - - [JsonPropertyName("height")] - public double? Height { get; set; } - - [JsonPropertyName("angle")] - public double? Angle { get; set; } - - [JsonPropertyName("visible")] - public bool? Visible { get; set; } - - /// 业务数据(节点配置)。替代旧 WorkflowNode.Config 字符串字段。 - [JsonPropertyName("data")] - public JsonElement? Data { get; set; } - - /// 显示名(对应旧 WorkflowNode.Name)。 - public string? GetTitle() => Data.TryGetString("title"); - - [JsonPropertyName("ports")] - public JsonElement? Ports { get; set; } -} - -/// -/// X6 边。source/target 用 { cell, port } 结构;几何信息(vertices/router/connector)放顶层; -/// 业务字段(edgeType/condition/order)放 data。 -/// -public sealed class GraphEdge -{ - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - [JsonPropertyName("shape")] - public string? Shape { get; set; } - - [JsonPropertyName("source")] - public GraphEndpoint Source { get; set; } = new(); - - [JsonPropertyName("target")] - public GraphEndpoint Target { get; set; } = new(); - - /// 业务数据:edgeType / condition / order。 - [JsonPropertyName("data")] - public JsonElement? Data { get; set; } - - [JsonPropertyName("label")] - public JsonElement? Label { get; set; } - - [JsonPropertyName("vertices")] - public List? Vertices { get; set; } - - [JsonPropertyName("router")] - public JsonElement? Router { get; set; } - - [JsonPropertyName("connector")] - public JsonElement? Connector { get; set; } - - /// 边类型(Normal=0/Approved=1/Rejected=2)。缺省为 Normal。 - public EdgeType GetEdgeType() => Data.TryGetInt32("edgeType") is { } v ? (EdgeType)v : EdgeType.Normal; - - /// 条件表达式(Condition/ExclusiveGateway 出边)。 - public string? GetCondition() => Data.TryGetString("condition"); - - /// 分支评估顺序(值小优先)。 - public int GetOrder() => Data.TryGetInt32("order") ?? 0; -} - -public sealed class GraphEndpoint -{ - /// 节点 id(对应 GraphNode.Id)。 - [JsonPropertyName("cell")] - public string? Cell { get; set; } - - /// 端口 id(top/right/bottom/left)。 - [JsonPropertyName("port")] - public string? Port { get; set; } -} - -public sealed class GraphPoint -{ - [JsonPropertyName("x")] - public double X { get; set; } - - [JsonPropertyName("y")] - public double Y { get; set; } -} - -/// -/// JsonElement? 安全取值扩展:统一处理 Nullable 解构与 ValueKind 校验, -/// 避免 WorkflowGraph 各 getter 重复 null 检查。 -/// -internal static class GraphJsonExtensions -{ - public static string? TryGetString(this JsonElement? element, string name) => - element is { ValueKind: JsonValueKind.Object } el - && el.TryGetProperty(name, out var p) - && p.ValueKind == JsonValueKind.String - ? p.GetString() - : null; - - public static int? TryGetInt32(this JsonElement? element, string name) => - element is { ValueKind: JsonValueKind.Object } el - && el.TryGetProperty(name, out var p) - && (p.ValueKind == JsonValueKind.Number || p.ValueKind == JsonValueKind.String) - && p.TryGetInt32(out var n) - ? n - : null; -} diff --git a/src/modules/workflow/Domain/Engine/WorkflowTree.cs b/src/modules/workflow/Domain/Engine/WorkflowTree.cs index 31a5cdf..dc74b57 100644 --- a/src/modules/workflow/Domain/Engine/WorkflowTree.cs +++ b/src/modules/workflow/Domain/Engine/WorkflowTree.cs @@ -27,62 +27,3 @@ public sealed class WorkflowTree /// 空树工厂:用于缺省/容错。 public static WorkflowTree Empty() => new(); } - -/// -/// 树节点。一条顺序链上的节点通过 链接; -/// 容器型节点(条件/并行)通过 挂多条分支。 -/// -public sealed class TreeNode -{ - /// 节点 id,必须是纯 Guid 字符串(引擎 ParseGuid 硬约束)。 - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - /// 节点类型。决定渲染卡片与配置面板。 - [JsonPropertyName("type")] - [JsonConverter(typeof(JsonStringEnumConverter))] - public TreeNodeTypeEnum Type { get; set; } - - /// - /// 节点业务配置(自由对象)。沿用引擎 node.data 的字段约定: - /// 审批节点:approvalMode/assigneeRules/timeoutMinutes/fieldPermissions 等; - /// 抄送节点:recipients;分支内首节点的 condition 在 上。 - /// - [JsonPropertyName("config")] - public JsonElement? Config { get; set; } - - /// 显示标题(冗余于 config.title,便于编辑器直接读取)。 - [JsonPropertyName("title")] - public string? Title { get; set; } - - /// 顺序链上的下一个节点(null 表示链尾,编译器自动接 End)。 - [JsonPropertyName("child")] - public TreeNode? Child { get; set; } - - /// 分支集合(仅 ConditionBranch / ParallelBranch 容器节点使用)。 - [JsonPropertyName("branches")] - public List? Branches { get; set; } -} - -/// 条件/并行分支容器中的一条分支。 -public sealed class TreeBranch -{ - /// 分支 id,纯 Guid 字符串。 - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - /// 分支显示名(如"条件 1"/"默认分支"/"分支 A")。 - [JsonPropertyName("label")] - public string? Label { get; set; } - - /// - /// 条件表达式(仅 ConditionBranch 使用;复用 ConditionEvaluator 语法,如 金额 > 5000)。 - /// 默认分支(else)置空,编译时 order 最大,引擎兜底命中。 - /// - [JsonPropertyName("condition")] - public string? Condition { get; set; } - - /// 该分支内的首节点(分支内又是顺序链;null 表示空分支,编译时直通收口)。 - [JsonPropertyName("child")] - public TreeNode? Child { get; set; } -} diff --git a/tests/BuildFlow.Platform.Tests/DictionaryConfiguration.cs b/tests/BuildFlow.Platform.Tests/DictionaryConfiguration.cs new file mode 100644 index 0000000..4cfb66b --- /dev/null +++ b/tests/BuildFlow.Platform.Tests/DictionaryConfiguration.cs @@ -0,0 +1,64 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Primitives; + +namespace BuildFlow.Platform.Tests; + +/// 极简字典实现的 IConfiguration,仅供测试使用。 +/// 完整实现 GetSection/GetChildren,让 GetValue<T> 扩展方法(走 ConfigurationBinder)能正确工作。 +/// 注意:ConfigurationBinder.GetValue 内部调用 GetSection(key).Value,故必须返回有效 section。 +internal sealed class DictionaryConfiguration : IConfiguration +{ + private readonly Dictionary _values = []; + + public string? this[string key] + { + get => _values.TryGetValue(key, out var v) ? v : null; + set => _values[key] = value; + } + + // 支持集合初始化器语法:new DictionaryConfiguration { ["k"] = "v" } + public void Add(string key, string? value) => _values[key] = value; + + IEnumerable IConfiguration.GetChildren() => + _values.Select(kv => (IConfigurationSection)new LeafSection(kv.Key, kv.Value)).ToList(); + + IChangeToken IConfiguration.GetReloadToken() => EmptyChangeToken.Instance; + + IConfigurationSection IConfiguration.GetSection(string key) => + new LeafSection(key, _values.TryGetValue(key, out var v) ? v : null); + + private sealed class LeafSection(string key, string? value) : IConfigurationSection + { + public string Key => key; + public string Path => key; + public string? Value { get; set; } = value; + + string? IConfiguration.this[string childKey] + { + get => null; + set { } + } + + IConfigurationSection IConfiguration.GetSection(string childKey) => + new LeafSection($"{key}:{childKey}", null); + + IEnumerable IConfiguration.GetChildren() => []; + + IChangeToken IConfiguration.GetReloadToken() => EmptyChangeToken.Instance; + } + + /// 永不变更的占位 IChangeToken,避免引入额外包依赖。 + private sealed class EmptyChangeToken : IChangeToken + { + public static readonly EmptyChangeToken Instance = new(); + public bool ActiveChangeCallbacks => false; + public bool HasChanged => false; + public IDisposable RegisterChangeCallback(Action callback, object? state) => + new NoopDisposable(); + + private sealed class NoopDisposable : IDisposable + { + public void Dispose() { } + } + } +} diff --git a/tests/BuildFlow.Platform.Tests/PlatformTestDb.cs b/tests/BuildFlow.Platform.Tests/PlatformTestDb.cs index 16966d5..9221faa 100644 --- a/tests/BuildFlow.Platform.Tests/PlatformTestDb.cs +++ b/tests/BuildFlow.Platform.Tests/PlatformTestDb.cs @@ -3,7 +3,6 @@ using BuildFlow.Platform.Infrastructure.Persistence; using BuildFlow.Shared.Domain.Common; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Primitives; using Microsoft.Extensions.DependencyInjection; namespace BuildFlow.Platform.Tests; @@ -106,63 +105,3 @@ internal static class PlatformTestDb return user; } } - -/// 极简字典实现的 IConfiguration,仅供测试使用。 -/// 完整实现 GetSection/GetChildren,让 GetValue<T> 扩展方法(走 ConfigurationBinder)能正确工作。 -/// 注意:ConfigurationBinder.GetValue 内部调用 GetSection(key).Value,故必须返回有效 section。 -internal sealed class DictionaryConfiguration : IConfiguration -{ - private readonly Dictionary _values = []; - - public string? this[string key] - { - get => _values.TryGetValue(key, out var v) ? v : null; - set => _values[key] = value; - } - - // 支持集合初始化器语法:new DictionaryConfiguration { ["k"] = "v" } - public void Add(string key, string? value) => _values[key] = value; - - IEnumerable IConfiguration.GetChildren() => - _values.Select(kv => (IConfigurationSection)new LeafSection(kv.Key, kv.Value)).ToList(); - - IChangeToken IConfiguration.GetReloadToken() => EmptyChangeToken.Instance; - - IConfigurationSection IConfiguration.GetSection(string key) => - new LeafSection(key, _values.TryGetValue(key, out var v) ? v : null); - - private sealed class LeafSection(string key, string? value) : IConfigurationSection - { - public string Key => key; - public string Path => key; - public string? Value { get; set; } = value; - - string? IConfiguration.this[string childKey] - { - get => null; - set { } - } - - IConfigurationSection IConfiguration.GetSection(string childKey) => - new LeafSection($"{key}:{childKey}", null); - - IEnumerable IConfiguration.GetChildren() => []; - - IChangeToken IConfiguration.GetReloadToken() => EmptyChangeToken.Instance; - } - - /// 永不变更的占位 IChangeToken,避免引入额外包依赖。 - private sealed class EmptyChangeToken : IChangeToken - { - public static readonly EmptyChangeToken Instance = new(); - public bool ActiveChangeCallbacks => false; - public bool HasChanged => false; - public IDisposable RegisterChangeCallback(Action callback, object? state) => - new NoopDisposable(); - - private sealed class NoopDisposable : IDisposable - { - public void Dispose() { } - } - } -} diff --git a/tests/BuildFlow.Rag.Tests/Stub/StubDocumentExtractor.cs b/tests/BuildFlow.Rag.Tests/Stub/StubDocumentExtractor.cs index 572bd8a..350a847 100644 --- a/tests/BuildFlow.Rag.Tests/Stub/StubDocumentExtractor.cs +++ b/tests/BuildFlow.Rag.Tests/Stub/StubDocumentExtractor.cs @@ -24,13 +24,3 @@ public sealed class StubDocumentExtractor : IDocumentExtractor SourcePath = filePath }); } - -/// -/// 工厂:统一返回 StubDocumentExtractor。 -/// -public sealed class StubDocumentExtractorFactory : IDocumentExtractorFactory -{ - public StubDocumentExtractor InnerExtractor { get; } = new(); - - public IDocumentExtractor GetExtractor(string fileNameOrExtension) => InnerExtractor; -} diff --git a/tests/BuildFlow.Rag.Tests/Stub/StubDocumentExtractorFactory.cs b/tests/BuildFlow.Rag.Tests/Stub/StubDocumentExtractorFactory.cs new file mode 100644 index 0000000..dafceb7 --- /dev/null +++ b/tests/BuildFlow.Rag.Tests/Stub/StubDocumentExtractorFactory.cs @@ -0,0 +1,13 @@ +using BuildFlow.Domain.Interfaces; + +namespace BuildFlow.Rag.Tests.Stub; + +/// +/// 工厂:统一返回 StubDocumentExtractor。 +/// +public sealed class StubDocumentExtractorFactory : IDocumentExtractorFactory +{ + public StubDocumentExtractor InnerExtractor { get; } = new(); + + public IDocumentExtractor GetExtractor(string fileNameOrExtension) => InnerExtractor; +} diff --git a/tests/BuildFlow.Workflow.Tests/Form/FormTestFixture.cs b/tests/BuildFlow.Workflow.Tests/Form/FormTestFixture.cs index 81e41aa..34afacd 100644 --- a/tests/BuildFlow.Workflow.Tests/Form/FormTestFixture.cs +++ b/tests/BuildFlow.Workflow.Tests/Form/FormTestFixture.cs @@ -43,8 +43,3 @@ public class FormTestFixture : IDisposable GC.SuppressFinalize(this); } } - -/// -/// xUnit ClassFixture,用于在测试类级别共享 Fixture 实例。 -/// -public class FormTestFixtureClassFixture : FormTestFixture; diff --git a/tests/BuildFlow.Workflow.Tests/Form/FormTestFixtureClassFixture.cs b/tests/BuildFlow.Workflow.Tests/Form/FormTestFixtureClassFixture.cs new file mode 100644 index 0000000..3f2d970 --- /dev/null +++ b/tests/BuildFlow.Workflow.Tests/Form/FormTestFixtureClassFixture.cs @@ -0,0 +1,6 @@ +namespace BuildFlow.Workflow.Tests.Form; + +/// +/// xUnit ClassFixture,用于在测试类级别共享 Fixture 实例。 +/// +public class FormTestFixtureClassFixture : FormTestFixture; diff --git a/tests/BuildFlow.Workflow.Tests/Notification/NoOpPlatformNotificationPublisher.cs b/tests/BuildFlow.Workflow.Tests/Notification/NoOpPlatformNotificationPublisher.cs new file mode 100644 index 0000000..40117dd --- /dev/null +++ b/tests/BuildFlow.Workflow.Tests/Notification/NoOpPlatformNotificationPublisher.cs @@ -0,0 +1,16 @@ +using Workflow.Domain.Common; + +namespace BuildFlow.Workflow.Tests.Notification; + +/// +/// 测试用 no-op 平台通知发布器:转发逻辑由 Host 层实现,单元测试无需真实推送, +/// 此实现空跑返回 0,避免 NotificationService 构造依赖阻断。 +/// +internal sealed class NoOpPlatformNotificationPublisher : IPlatformNotificationPublisher +{ + public Task PublishAsync( + string type, string title, string content, string source, + string? relatedId, string? relatedType, + IEnumerable recipientUserIds, CancellationToken ct = default) + => Task.FromResult(0); +} diff --git a/tests/BuildFlow.Workflow.Tests/Notification/NotificationServiceTests.cs b/tests/BuildFlow.Workflow.Tests/Notification/NotificationServiceTests.cs index 8e9f7ae..ed6392d 100644 --- a/tests/BuildFlow.Workflow.Tests/Notification/NotificationServiceTests.cs +++ b/tests/BuildFlow.Workflow.Tests/Notification/NotificationServiceTests.cs @@ -255,16 +255,3 @@ public class NotificationServiceTests (await db.WebhookDeliveries.CountAsync()).Should().Be(0); } } - -/// -/// 测试用 no-op 平台通知发布器:转发逻辑由 Host 层实现,单元测试无需真实推送, -/// 此实现空跑返回 0,避免 NotificationService 构造依赖阻断。 -/// -file sealed class NoOpPlatformNotificationPublisher : IPlatformNotificationPublisher -{ - public Task PublishAsync( - string type, string title, string content, string source, - string? relatedId, string? relatedType, - IEnumerable recipientUserIds, CancellationToken ct = default) - => Task.FromResult(0); -}