55262b668e
ci / backend (push) Failing after 1s
ci / static (push) Failing after 11s
Build & Test with Secrets / build (push) Failing after 1s
deploy / deploy (push) Successful in 2m42s
Build & Test with Secrets / security-scan (push) Failing after 7s
deploy / notify (push) Successful in 1s
ci / frontend (push) Successful in 3m41s
ci / publish (push) Has been skipped
Build & Test with Secrets / frontend (push) Successful in 3m35s
Build & Test with Secrets / notification (push) Failing after 1s
Per AGENTS.md v16.0 principle: enforce blocking rules in code, not just documentation - Added DateTime_now_must_use_iclock_abstraction() test to RepositoryRulesTests * Runs on every build (not optional verification) * Detects any DateTime.Now/UtcNow/DateTimeOffset.UtcNow without IClock * Blocks build until all violations use IClock abstraction - Test identifies 11 violation files precisely: * ApiCallMetricsService.cs * VS02/03_SecurityMasterPolicy.cs + MarketDataPolicy.cs * VS03_IngestionEndpoint/Jobs.cs * VS04/05/06/08_Portfolio*.cs * VS02_SecurityMasterJobs.cs Rationale: AGENTS.md guidelines in documentation can be ignored. Test failures cannot. This harness makes rule #16 executable. **Key Principle:** Code-based guardrails > documentation. The test IS the rule now - LLM sees code + test, not just prose. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
201 lines
8.2 KiB
C#
201 lines
8.2 KiB
C#
using Xunit;
|
|
|
|
namespace KArtSell.ArchitectureTests;
|
|
|
|
public sealed class RepositoryRulesTests
|
|
{
|
|
[Fact]
|
|
public void Prohibited_source_patterns_are_not_introduced()
|
|
{
|
|
var repositoryRoot = FindRepositoryRoot();
|
|
var sourceFiles = Directory.EnumerateFiles(repositoryRoot, "*.cs", SearchOption.AllDirectories)
|
|
.Where(x => !IsGeneratedOrTestOutput(x))
|
|
.ToArray();
|
|
|
|
// Check anti-patterns
|
|
AssertNoPattern(sourceFiles, "IGenericRepository", "Generic repository is prohibited.");
|
|
AssertNoPattern(sourceFiles, "IServiceProvider.GetService", "Service locator is prohibited.");
|
|
}
|
|
|
|
[Fact]
|
|
public void DateTime_now_must_use_iclock_abstraction()
|
|
{
|
|
var repositoryRoot = FindRepositoryRoot();
|
|
var sourceFiles = Directory.EnumerateFiles(repositoryRoot, "*.cs", SearchOption.AllDirectories)
|
|
.Where(x => !IsGeneratedOrTestOutput(x))
|
|
.ToArray();
|
|
|
|
var violations = sourceFiles
|
|
.Where(path =>
|
|
{
|
|
var text = File.ReadAllText(path);
|
|
return (text.Contains("DateTime.Now", StringComparison.Ordinal)
|
|
|| text.Contains("DateTime.UtcNow", StringComparison.Ordinal)
|
|
|| text.Contains("DateTimeOffset.UtcNow", StringComparison.Ordinal))
|
|
&& !text.Contains("IClock", StringComparison.Ordinal);
|
|
})
|
|
.ToArray();
|
|
|
|
Assert.True(violations.Length == 0,
|
|
$"DateTime.Now/UtcNow must use IClock abstraction (not direct DateTime): {string.Join(", ", violations)}");
|
|
}
|
|
|
|
[Fact]
|
|
public void Domain_files_do_not_reference_infrastructure_frameworks()
|
|
{
|
|
var root = FindRepositoryRoot();
|
|
var domainFiles = Directory.EnumerateFiles(
|
|
Path.Combine(root, "src"),
|
|
"*.cs",
|
|
SearchOption.AllDirectories)
|
|
.Where(path => path.Contains(
|
|
$"{Path.DirectorySeparatorChar}Domain{Path.DirectorySeparatorChar}",
|
|
StringComparison.Ordinal))
|
|
.ToArray();
|
|
|
|
foreach (var prohibited in new[]
|
|
{
|
|
"using Dapper", "using Npgsql", "using FastEndpoints", "using Hangfire",
|
|
"HttpContext", "DbConnection", "IServiceCollection"
|
|
})
|
|
{
|
|
AssertNoPattern(domainFiles, prohibited,
|
|
$"Domain cannot depend on infrastructure framework: {prohibited}.");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Sql_does_not_use_select_star_or_unqualified_signal_tables()
|
|
{
|
|
var repositoryRoot = FindRepositoryRoot();
|
|
var files = Directory.EnumerateFiles(repositoryRoot, "*.*", SearchOption.AllDirectories)
|
|
.Where(x => x.EndsWith(".cs", StringComparison.OrdinalIgnoreCase)
|
|
|| x.EndsWith(".sql", StringComparison.OrdinalIgnoreCase))
|
|
.Where(x => !IsGeneratedOrTestOutput(x))
|
|
.ToArray();
|
|
|
|
var selectStar = files.Where(x =>
|
|
File.ReadAllText(x).Contains("select *", StringComparison.OrdinalIgnoreCase)).ToArray();
|
|
Assert.True(selectStar.Length == 0,
|
|
"SELECT * is prohibited: " + string.Join(", ", selectStar));
|
|
|
|
var unqualified = files.Where(x =>
|
|
{
|
|
var text = File.ReadAllText(x);
|
|
return text.Contains(" from signal_decision", StringComparison.OrdinalIgnoreCase)
|
|
|| text.Contains(" into signal_decision", StringComparison.OrdinalIgnoreCase)
|
|
|| text.Contains(" from evidence_snapshot", StringComparison.OrdinalIgnoreCase);
|
|
}).ToArray();
|
|
Assert.True(unqualified.Length == 0,
|
|
"SignalEngine SQL must be schema-qualified: " + string.Join(", ", unqualified));
|
|
}
|
|
|
|
[Fact]
|
|
public void Every_module_endpoint_declares_roles_or_policies()
|
|
{
|
|
var root = FindRepositoryRoot();
|
|
var endpoints = Directory.EnumerateFiles(
|
|
Path.Combine(root, "src"),
|
|
"Endpoint.cs",
|
|
SearchOption.AllDirectories)
|
|
.Where(path => path.Contains(
|
|
$"{Path.DirectorySeparatorChar}Modules{Path.DirectorySeparatorChar}",
|
|
StringComparison.Ordinal))
|
|
.ToArray();
|
|
|
|
var violations = endpoints.Where(path =>
|
|
{
|
|
var text = File.ReadAllText(path);
|
|
return !text.Contains("Roles(", StringComparison.Ordinal)
|
|
&& !text.Contains("Policies(", StringComparison.Ordinal);
|
|
}).ToArray();
|
|
|
|
Assert.True(violations.Length == 0,
|
|
"Every module endpoint must declare Roles or Policies: " + string.Join(", ", violations));
|
|
}
|
|
|
|
[Fact]
|
|
public void Accidental_placeholder_files_are_not_committed()
|
|
{
|
|
var root = FindRepositoryRoot();
|
|
var names = Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories)
|
|
.Where(path => !IsGeneratedOrTestOutput(path))
|
|
.Where(path => Path.GetFileName(path).Equals("testfile", StringComparison.OrdinalIgnoreCase)
|
|
|| Path.GetFileName(path).EndsWith(".tmp", StringComparison.OrdinalIgnoreCase))
|
|
.ToArray();
|
|
|
|
Assert.True(names.Length == 0,
|
|
"Placeholder files are prohibited: " + string.Join(", ", names));
|
|
}
|
|
|
|
[Fact]
|
|
public void Aggregate_ids_are_unique_across_modules()
|
|
{
|
|
var root = FindRepositoryRoot();
|
|
var aggregateIdFiles = Directory.EnumerateFiles(
|
|
Path.Combine(root, "src"),
|
|
"*.cs",
|
|
SearchOption.AllDirectories)
|
|
.Where(path => !IsGeneratedOrTestOutput(path))
|
|
.ToArray();
|
|
|
|
var aggregateIds = new Dictionary<string, List<string>>();
|
|
|
|
foreach (var file in aggregateIdFiles)
|
|
{
|
|
var text = File.ReadAllText(file);
|
|
|
|
// Match aggregate ID definitions: Guid("00000000-0000-0000-0000-...")
|
|
var pattern = @"Guid\(\""[a-f0-9\-]{36}\""";
|
|
var matches = System.Text.RegularExpressions.Regex.Matches(text, pattern);
|
|
|
|
foreach (System.Text.RegularExpressions.Match match in matches)
|
|
{
|
|
var id = match.Value;
|
|
if (!aggregateIds.TryGetValue(id, out var list))
|
|
{
|
|
list = new List<string>();
|
|
aggregateIds[id] = list;
|
|
}
|
|
list.Add(file);
|
|
}
|
|
}
|
|
|
|
var duplicates = aggregateIds
|
|
.Where(kvp => kvp.Value.Count > 1)
|
|
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
|
|
|
Assert.True(duplicates.Count == 0,
|
|
duplicates.Count > 0
|
|
? $"Duplicate aggregate IDs detected: {string.Join("; ", duplicates.Select(d => $"{d.Key} in {string.Join(", ", d.Value)}"))}"
|
|
: "No duplicate aggregate IDs found.");
|
|
}
|
|
|
|
private static void AssertNoPattern(IEnumerable<string> files, string pattern, string message)
|
|
{
|
|
var violations = files
|
|
.Where(path => File.ReadAllText(path).Contains(pattern, StringComparison.Ordinal))
|
|
.ToArray();
|
|
Assert.True(violations.Length == 0, message + " " + string.Join(", ", violations));
|
|
}
|
|
|
|
private static bool IsGeneratedOrTestOutput(string path)
|
|
=> path.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.Ordinal)
|
|
|| path.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal)
|
|
|| path.Contains($"{Path.DirectorySeparatorChar}tests{Path.DirectorySeparatorChar}", StringComparison.Ordinal)
|
|
|| path.Contains($"{Path.DirectorySeparatorChar}attachments{Path.DirectorySeparatorChar}", StringComparison.Ordinal)
|
|
|| path.Contains($"{Path.DirectorySeparatorChar}research{Path.DirectorySeparatorChar}original{Path.DirectorySeparatorChar}", StringComparison.Ordinal);
|
|
|
|
private static string FindRepositoryRoot()
|
|
{
|
|
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
|
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "Directory.Build.props")))
|
|
{
|
|
directory = directory.Parent;
|
|
}
|
|
|
|
return directory?.FullName
|
|
?? throw new InvalidOperationException("Repository root not found.");
|
|
}
|
|
}
|