145 lines
6.7 KiB
C#
145 lines
6.7 KiB
C#
namespace BuildFlow.Architecture.Tests;
|
||
|
||
using System.Text.RegularExpressions;
|
||
using Xunit;
|
||
|
||
/// <summary>
|
||
/// 后端优化闭环 SOP 的可执行合同测试,防止脚本退化为不可复现或会污染开发环境的手工步骤。
|
||
/// </summary>
|
||
public sealed class BackendOptimizationSopTests
|
||
{
|
||
private static readonly string[] OrderedStages =
|
||
[
|
||
"01-preflight",
|
||
"02-static-guards",
|
||
"03-restore-build",
|
||
"04-unit-tests",
|
||
"05-start-infrastructure",
|
||
"06-database-migrations",
|
||
"07-integration-tests",
|
||
"08-recovery-tests",
|
||
"09-performance-tests",
|
||
"10-full-solution",
|
||
];
|
||
|
||
[Fact]
|
||
public void ScriptShouldFailFastAndCleanOnlyItsComposeProject()
|
||
{
|
||
var script = ReadRequiredFile("scripts", "backend-optimization-sop.sh");
|
||
|
||
Assert.Contains("#!/usr/bin/env bash", script, StringComparison.Ordinal);
|
||
Assert.Contains("set -euo pipefail", script, StringComparison.Ordinal);
|
||
Assert.Matches(new Regex(@"trap\s+cleanup\s+EXIT", RegexOptions.CultureInvariant), script);
|
||
Assert.Contains(
|
||
"docker compose -p \"$COMPOSE_PROJECT\" -f \"$COMPOSE_FILE\" down -v",
|
||
script,
|
||
StringComparison.Ordinal);
|
||
Assert.DoesNotContain("docker compose down", script, StringComparison.Ordinal);
|
||
}
|
||
|
||
[Fact]
|
||
public void ComposeShouldProvideFourPinnedRandomPortServices()
|
||
{
|
||
var compose = ReadRequiredFile("scripts", "backend-optimization-compose.yml");
|
||
|
||
Assert.Contains("postgres:", compose, StringComparison.Ordinal);
|
||
Assert.Contains("pgvector/pgvector:0.8.0-pg17", compose, StringComparison.Ordinal);
|
||
Assert.Contains("redis:", compose, StringComparison.Ordinal);
|
||
Assert.Contains("redis:7-alpine", compose, StringComparison.Ordinal);
|
||
Assert.Contains("mongo:", compose, StringComparison.Ordinal);
|
||
Assert.Contains("mongo:7", compose, StringComparison.Ordinal);
|
||
Assert.Contains("minio:", compose, StringComparison.Ordinal);
|
||
Assert.Contains("minio/minio:RELEASE.2025-04-22T22-12-26Z", compose, StringComparison.Ordinal);
|
||
Assert.Contains("127.0.0.1::5432", compose, StringComparison.Ordinal);
|
||
Assert.Contains("127.0.0.1::6379", compose, StringComparison.Ordinal);
|
||
Assert.Contains("127.0.0.1::27017", compose, StringComparison.Ordinal);
|
||
Assert.Contains("127.0.0.1::9000", compose, StringComparison.Ordinal);
|
||
}
|
||
|
||
[Fact]
|
||
public void ScriptShouldArchiveOrderedStageCommandsTestsAndMetrics()
|
||
{
|
||
var script = ReadRequiredFile("scripts", "backend-optimization-sop.sh");
|
||
var previousIndex = -1;
|
||
|
||
foreach (var stage in OrderedStages)
|
||
{
|
||
var currentIndex = script.IndexOf($"run_stage '{stage}'", StringComparison.Ordinal);
|
||
Assert.True(currentIndex > previousIndex, $"SOP 阶段缺失或顺序错误: {stage}");
|
||
previousIndex = currentIndex;
|
||
}
|
||
|
||
Assert.Contains("dotnet restore \"$SOLUTION\"", script, StringComparison.Ordinal);
|
||
Assert.Contains(
|
||
"dotnet build \"$SOLUTION\" --no-restore -warnaserror",
|
||
script,
|
||
StringComparison.Ordinal);
|
||
Assert.Contains("--logger \"trx;LogFileName=", script, StringComparison.Ordinal);
|
||
Assert.Contains("--results-directory \"$TRX_DIR\"", script, StringComparison.Ordinal);
|
||
Assert.Contains("assert_no_skipped_tests", script, StringComparison.Ordinal);
|
||
Assert.Contains("collect_sop_metrics", script, StringComparison.Ordinal);
|
||
Assert.Contains("\"$LOG_DIR\" \"$TRX_DIR\"", script, StringComparison.Ordinal);
|
||
Assert.Contains("SOP_METRIC", script, StringComparison.Ordinal);
|
||
Assert.Contains("summary.md", script, StringComparison.Ordinal);
|
||
Assert.Contains("date -u +%Y%m%dT%H%M%SZ", script, StringComparison.Ordinal);
|
||
}
|
||
|
||
[Fact]
|
||
public void ScriptShouldRunFinalDebtScansWithoutDeveloperSecretsOrDatabases()
|
||
{
|
||
var script = ReadRequiredFile("scripts", "backend-optimization-sop.sh");
|
||
|
||
Assert.Contains("EndpointConstructorsShouldNotDependOnDbContexts", script, StringComparison.Ordinal);
|
||
Assert.Contains("HostShouldNotContainInProcessBridges", script, StringComparison.Ordinal);
|
||
Assert.Contains("BusinessExceptionsShouldSpecifySemanticErrorCodes", script, StringComparison.Ordinal);
|
||
Assert.Contains("SourceFilesShouldContainAtMostOneTopLevelClassRecordOrEnum", script, StringComparison.Ordinal);
|
||
Assert.Contains("Password\\s*=", script, StringComparison.Ordinal);
|
||
Assert.Contains("DateTime\\.Now", script, StringComparison.Ordinal);
|
||
Assert.Contains("Task\\.Run", script, StringComparison.Ordinal);
|
||
Assert.DoesNotContain("appsettings.Secrets.json", script, StringComparison.Ordinal);
|
||
Assert.DoesNotContain("rag123", script, StringComparison.Ordinal);
|
||
Assert.DoesNotContain("Database=workflow;", script, StringComparison.Ordinal);
|
||
Assert.DoesNotContain("Database=im_system;", script, StringComparison.Ordinal);
|
||
}
|
||
|
||
[Fact]
|
||
public void OperatorGuideShouldDocumentInvocationIsolationAndEvidenceRules()
|
||
{
|
||
var guide = ReadRequiredFile("docs", "backend-optimization-sop.md");
|
||
|
||
Assert.Contains("scripts/backend-optimization-sop.sh", guide, StringComparison.Ordinal);
|
||
Assert.Contains("BACKEND_SOP_ARTIFACT_ROOT", guide, StringComparison.Ordinal);
|
||
Assert.Contains("随机端口", guide, StringComparison.Ordinal);
|
||
Assert.Contains("独立 Compose 项目", guide, StringComparison.Ordinal);
|
||
Assert.Contains("TRX", guide, StringComparison.Ordinal);
|
||
Assert.Contains("SOP_METRIC", guide, StringComparison.Ordinal);
|
||
Assert.Contains("summary.md", guide, StringComparison.Ordinal);
|
||
Assert.Contains("skipped", guide, StringComparison.OrdinalIgnoreCase);
|
||
Assert.Contains("失败", guide, StringComparison.Ordinal);
|
||
}
|
||
|
||
private static string ReadRequiredFile(params string[] pathParts)
|
||
{
|
||
var repoRoot = FindRepoRoot();
|
||
var path = Path.Combine([repoRoot.FullName, .. pathParts]);
|
||
Assert.True(System.IO.File.Exists(path), $"SOP 合同文件不存在: {Path.GetRelativePath(repoRoot.FullName, path)}");
|
||
return System.IO.File.ReadAllText(path);
|
||
}
|
||
|
||
private static DirectoryInfo FindRepoRoot()
|
||
{
|
||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||
while (current is not null)
|
||
{
|
||
if (System.IO.File.Exists(Path.Combine(current.FullName, "BuildFlow.slnx")))
|
||
{
|
||
return current;
|
||
}
|
||
|
||
current = current.Parent;
|
||
}
|
||
|
||
throw new InvalidOperationException("无法定位 BuildFlow.slnx,SOP 合同测试无法确定仓库边界。");
|
||
}
|
||
}
|