build-flow.api/tests/BuildFlow.Architecture.Tests/BackendOptimizationSopTests.cs
2026-07-24 13:29:29 +08:00

145 lines
6.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 合同测试无法确定仓库边界。");
}
}