Resolve Concelier/Excititor merge conflicts

This commit is contained in:
root
2025-10-20 14:19:25 +03:00
2687 changed files with 212646 additions and 85913 deletions

View File

@@ -0,0 +1,12 @@
global using System.Text.Json;
global using System.Text.Json.Nodes;
global using Microsoft.Extensions.Logging.Abstractions;
global using Microsoft.Extensions.Options;
global using Mongo2Go;
global using MongoDB.Bson;
global using MongoDB.Driver;
global using StellaOps.Scheduler.Models;
global using StellaOps.Scheduler.Storage.Mongo.Internal;
global using StellaOps.Scheduler.Storage.Mongo.Migrations;
global using StellaOps.Scheduler.Storage.Mongo.Options;
global using Xunit;

View File

@@ -0,0 +1,126 @@
using System.Text.Json.Nodes;
namespace StellaOps.Scheduler.Storage.Mongo.Tests.Integration;
public sealed class SchedulerMongoRoundTripTests : IDisposable
{
private readonly MongoDbRunner _runner;
private readonly SchedulerMongoContext _context;
public SchedulerMongoRoundTripTests()
{
_runner = MongoDbRunner.Start(additionalMongodArguments: "--quiet");
var options = new SchedulerMongoOptions
{
ConnectionString = _runner.ConnectionString,
Database = $"scheduler_roundtrip_{Guid.NewGuid():N}"
};
_context = new SchedulerMongoContext(Microsoft.Extensions.Options.Options.Create(options), NullLogger<SchedulerMongoContext>.Instance);
var migrations = new ISchedulerMongoMigration[]
{
new EnsureSchedulerCollectionsMigration(NullLogger<EnsureSchedulerCollectionsMigration>.Instance),
new EnsureSchedulerIndexesMigration()
};
var runner = new SchedulerMongoMigrationRunner(_context, migrations, NullLogger<SchedulerMongoMigrationRunner>.Instance);
runner.RunAsync(CancellationToken.None).GetAwaiter().GetResult();
}
[Fact]
public async Task SamplesRoundTripThroughMongoWithoutLosingCanonicalShape()
{
var samplesRoot = LocateSamplesRoot();
var scheduleJson = await File.ReadAllTextAsync(Path.Combine(samplesRoot, "schedule.json"), CancellationToken.None);
await AssertRoundTripAsync(
scheduleJson,
_context.Options.SchedulesCollection,
CanonicalJsonSerializer.Deserialize<Schedule>,
schedule => schedule.Id);
var runJson = await File.ReadAllTextAsync(Path.Combine(samplesRoot, "run.json"), CancellationToken.None);
await AssertRoundTripAsync(
runJson,
_context.Options.RunsCollection,
CanonicalJsonSerializer.Deserialize<Run>,
run => run.Id);
var impactJson = await File.ReadAllTextAsync(Path.Combine(samplesRoot, "impact-set.json"), CancellationToken.None);
await AssertRoundTripAsync(
impactJson,
_context.Options.ImpactSnapshotsCollection,
CanonicalJsonSerializer.Deserialize<ImpactSet>,
_ => null);
var auditJson = await File.ReadAllTextAsync(Path.Combine(samplesRoot, "audit.json"), CancellationToken.None);
await AssertRoundTripAsync(
auditJson,
_context.Options.AuditCollection,
CanonicalJsonSerializer.Deserialize<AuditRecord>,
audit => audit.Id);
}
private async Task AssertRoundTripAsync<TModel>(
string json,
string collectionName,
Func<string, TModel> deserialize,
Func<TModel, string?> resolveId)
{
ArgumentNullException.ThrowIfNull(deserialize);
ArgumentNullException.ThrowIfNull(resolveId);
var model = deserialize(json);
var canonical = CanonicalJsonSerializer.Serialize(model);
var document = BsonDocument.Parse(canonical);
var identifier = resolveId(model);
if (!string.IsNullOrEmpty(identifier))
{
document["_id"] = identifier;
}
var collection = _context.Database.GetCollection<BsonDocument>(collectionName);
await collection.InsertOneAsync(document, cancellationToken: CancellationToken.None);
var filter = identifier is null ? Builders<BsonDocument>.Filter.Empty : Builders<BsonDocument>.Filter.Eq("_id", identifier);
var stored = await collection.Find(filter).FirstOrDefaultAsync();
Assert.NotNull(stored);
var sanitized = stored!.DeepClone().AsBsonDocument;
sanitized.Remove("_id");
var storedJson = sanitized.ToJson();
var parsedExpected = JsonNode.Parse(canonical) ?? throw new InvalidOperationException("Canonical node null.");
var parsedActual = JsonNode.Parse(storedJson) ?? throw new InvalidOperationException("Stored node null.");
Assert.True(JsonNode.DeepEquals(parsedExpected, parsedActual), "Document changed shape after Mongo round-trip.");
}
private static string LocateSamplesRoot()
{
var current = AppContext.BaseDirectory;
while (!string.IsNullOrEmpty(current))
{
var candidate = Path.Combine(current, "samples", "api", "scheduler");
if (Directory.Exists(candidate))
{
return candidate;
}
var parent = Path.GetDirectoryName(current.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
if (string.Equals(parent, current, StringComparison.Ordinal))
{
break;
}
current = parent;
}
throw new DirectoryNotFoundException("Unable to locate samples/api/scheduler in repository tree.");
}
public void Dispose()
{
_runner.Dispose();
}
}

View File

@@ -0,0 +1,106 @@
namespace StellaOps.Scheduler.Storage.Mongo.Tests.Migrations;
public sealed class SchedulerMongoMigrationTests : IDisposable
{
private readonly MongoDbRunner _runner;
public SchedulerMongoMigrationTests()
{
_runner = MongoDbRunner.Start(additionalMongodArguments: "--quiet");
}
[Fact]
public async Task RunAsync_CreatesCollectionsAndIndexes()
{
var options = new SchedulerMongoOptions
{
ConnectionString = _runner.ConnectionString,
Database = $"scheduler_tests_{Guid.NewGuid():N}"
};
var context = new SchedulerMongoContext(Microsoft.Extensions.Options.Options.Create(options), NullLogger<SchedulerMongoContext>.Instance);
var migrations = new ISchedulerMongoMigration[]
{
new EnsureSchedulerCollectionsMigration(NullLogger<EnsureSchedulerCollectionsMigration>.Instance),
new EnsureSchedulerIndexesMigration()
};
var runner = new SchedulerMongoMigrationRunner(context, migrations, NullLogger<SchedulerMongoMigrationRunner>.Instance);
await runner.RunAsync(CancellationToken.None);
var cursor = await context.Database.ListCollectionNamesAsync(cancellationToken: CancellationToken.None);
var collections = await cursor.ToListAsync();
Assert.Contains(options.SchedulesCollection, collections);
Assert.Contains(options.RunsCollection, collections);
Assert.Contains(options.ImpactSnapshotsCollection, collections);
Assert.Contains(options.AuditCollection, collections);
Assert.Contains(options.LocksCollection, collections);
Assert.Contains(options.MigrationsCollection, collections);
await AssertScheduleIndexesAsync(context, options);
await AssertRunIndexesAsync(context, options);
await AssertImpactSnapshotIndexesAsync(context, options);
await AssertAuditIndexesAsync(context, options);
await AssertLockIndexesAsync(context, options);
}
private static async Task AssertScheduleIndexesAsync(SchedulerMongoContext context, SchedulerMongoOptions options)
{
var names = await ListIndexNamesAsync(context.Database.GetCollection<BsonDocument>(options.SchedulesCollection));
Assert.Contains("tenant_enabled", names);
Assert.Contains("cron_timezone", names);
}
private static async Task AssertRunIndexesAsync(SchedulerMongoContext context, SchedulerMongoOptions options)
{
var collection = context.Database.GetCollection<BsonDocument>(options.RunsCollection);
var indexes = await ListIndexesAsync(collection);
Assert.Contains(indexes, doc => string.Equals(doc["name"].AsString, "tenant_createdAt_desc", StringComparison.Ordinal));
Assert.Contains(indexes, doc => string.Equals(doc["name"].AsString, "state_lookup", StringComparison.Ordinal));
Assert.Contains(indexes, doc => string.Equals(doc["name"].AsString, "schedule_createdAt_desc", StringComparison.Ordinal));
var ttl = indexes.FirstOrDefault(doc => doc.TryGetValue("name", out var name) && name == "finishedAt_ttl");
Assert.NotNull(ttl);
Assert.Equal(options.CompletedRunRetention.TotalSeconds, ttl!["expireAfterSeconds"].ToDouble());
}
private static async Task AssertImpactSnapshotIndexesAsync(SchedulerMongoContext context, SchedulerMongoOptions options)
{
var names = await ListIndexNamesAsync(context.Database.GetCollection<BsonDocument>(options.ImpactSnapshotsCollection));
Assert.Contains("selector_tenant_scope", names);
Assert.Contains("snapshotId_unique", names);
}
private static async Task AssertAuditIndexesAsync(SchedulerMongoContext context, SchedulerMongoOptions options)
{
var names = await ListIndexNamesAsync(context.Database.GetCollection<BsonDocument>(options.AuditCollection));
Assert.Contains("tenant_occurredAt_desc", names);
Assert.Contains("correlation_lookup", names);
}
private static async Task AssertLockIndexesAsync(SchedulerMongoContext context, SchedulerMongoOptions options)
{
var names = await ListIndexNamesAsync(context.Database.GetCollection<BsonDocument>(options.LocksCollection));
Assert.Contains("tenant_resource_unique", names);
Assert.Contains("expiresAt_ttl", names);
}
private static async Task<IReadOnlyList<string>> ListIndexNamesAsync(IMongoCollection<BsonDocument> collection)
{
var documents = await ListIndexesAsync(collection);
return documents.Select(doc => doc["name"].AsString).ToArray();
}
private static async Task<List<BsonDocument>> ListIndexesAsync(IMongoCollection<BsonDocument> collection)
{
using var cursor = await collection.Indexes.ListAsync();
return await cursor.ToListAsync();
}
public void Dispose()
{
_runner.Dispose();
}
}

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseConcelierTestInfra>false</UseConcelierTestInfra>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../StellaOps.Scheduler.Storage.Mongo/StellaOps.Scheduler.Storage.Mongo.csproj" />
<ProjectReference Include="../StellaOps.Scheduler.Models/StellaOps.Scheduler.Models.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Mongo2Go" Version="4.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<None Include="../../samples/api/scheduler/*.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>