Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 0 additions & 57 deletions .github/workflows/build.yml

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using Microsoft.Data.Sqlite;

namespace Agentstration.Web.Hosting;

internal sealed class TestingDataDirectoryCleanupService(string directory, bool deleteOnShutdown) : IHostedService
{
internal string DirectoryPath { get; } = directory;

public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;

public async Task StopAsync(CancellationToken cancellationToken)
{
ClearPoolsInDirectory(DirectoryPath);
if (!deleteOnShutdown) return;

for (var attempt = 0; ; attempt++)
{
try
{
if (Directory.Exists(DirectoryPath)) Directory.Delete(DirectoryPath, recursive: true);
return;
}
catch (IOException) when (attempt < 4)
{
await Task.Delay(TimeSpan.FromMilliseconds(50), cancellationToken);
}
catch (UnauthorizedAccessException) when (attempt < 4)
{
await Task.Delay(TimeSpan.FromMilliseconds(50), cancellationToken);
}
}
}

private static void ClearPoolsInDirectory(string directory)
{
if (!Directory.Exists(directory)) return;

foreach (var databasePath in Directory.EnumerateFiles(directory, "*.db", SearchOption.AllDirectories))
{
using var connection = new SqliteConnection($"Data Source={databasePath}");
SqliteConnection.ClearPool(connection);
}
}
}
38 changes: 23 additions & 15 deletions src/Agentstration.Web/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,45 +39,53 @@
var toolExecutionCapture = builder.Configuration.GetSection("Agentstration:ToolExecution").Get<ToolExecutionCaptureOptions>() ?? new();
toolExecutionCapture.Validate();
builder.Services.AddSingleton(toolExecutionCapture);
var dataDirectory = builder.Configuration["Data:Directory"] ?? Path.Combine(builder.Environment.ContentRootPath, ".agentstration");
var isTesting = builder.Environment.IsEnvironment("Testing");
var configuredTestingDataDirectory = builder.Configuration["Data:TestingDirectory"];
var ownsTestingDataDirectory = isTesting && string.IsNullOrWhiteSpace(configuredTestingDataDirectory);
var dataDirectory = isTesting
? string.IsNullOrWhiteSpace(configuredTestingDataDirectory)
? Path.Combine(Path.GetTempPath(), $"agentstration-web-tests-{Guid.NewGuid():N}")
: configuredTestingDataDirectory
: builder.Configuration["Data:Directory"] ?? Path.Combine(builder.Environment.ContentRootPath, ".agentstration");
Directory.CreateDirectory(dataDirectory);
var identityConnectionString = builder.Configuration.GetConnectionString("Identity")
?? (builder.Environment.IsEnvironment("Testing")
? $"Data Source={Path.Combine(Path.GetTempPath(), $"agentstration-identity-tests-{Guid.NewGuid():N}.db")}"
: $"Data Source={Path.Combine(dataDirectory, "identity.db")}");
?? $"Data Source={Path.Combine(dataDirectory, "identity.db")}";
var dataProtectionKeysPath = configuredAuthentication.DataProtectionKeysPath;
if (string.IsNullOrWhiteSpace(dataProtectionKeysPath))
{
dataProtectionKeysPath = builder.Environment.IsEnvironment("Testing")
? Path.Combine(Path.GetTempPath(), $"agentstration-data-protection-tests-{Guid.NewGuid():N}")
: Path.Combine(dataDirectory, "data-protection-keys");
dataProtectionKeysPath = Path.Combine(dataDirectory, "data-protection-keys");
}
var aiProvider = builder.Configuration["AI:Provider"] ?? "Managed";
var useManagedProfileResolver = string.Equals(aiProvider, "Managed", StringComparison.OrdinalIgnoreCase);
const string defaultAiEndpoint = "http://localhost:11434/v1/";
var aiEndpoint = builder.Configuration["AI:Endpoint"] ?? defaultAiEndpoint;
if (!Uri.TryCreate(aiEndpoint.EndsWith('/') ? aiEndpoint : aiEndpoint + '/', UriKind.Absolute, out var parsedAiEndpoint)) throw new InvalidOperationException("AI:Endpoint must be an absolute URL.");
var aiOptions = new AiProviderOptions(aiProvider, parsedAiEndpoint, builder.Configuration["AI:Model"] ?? "phi4-mini", builder.Configuration["AI:ApiKey"]);
var controlPlanePath = builder.Environment.IsEnvironment("Testing")
? Path.Combine(Path.GetTempPath(), $"agentstration-tests-{Guid.NewGuid():N}.db")
var controlPlanePath = isTesting
? Path.Combine(dataDirectory, "control-plane.db")
: builder.Configuration["Data:ControlPlanePath"] ?? Path.Combine(builder.Environment.ContentRootPath, ".agentstration", "control-plane.db");
var controlPlaneDirectory = Path.GetDirectoryName(controlPlanePath);
if (!string.IsNullOrWhiteSpace(controlPlaneDirectory)) Directory.CreateDirectory(controlPlaneDirectory);
var workPlanePath = builder.Environment.IsEnvironment("Testing")
? Path.Combine(Path.GetTempPath(), $"agentstration-work-tests-{Guid.NewGuid():N}.db")
var workPlanePath = isTesting
? Path.Combine(dataDirectory, "work-plane.db")
: builder.Configuration["Data:WorkPlanePath"] ?? Path.Combine(builder.Environment.ContentRootPath, ".agentstration", "work-plane.db");
var workPlaneDirectory = Path.GetDirectoryName(workPlanePath);
if (!string.IsNullOrWhiteSpace(workPlaneDirectory)) Directory.CreateDirectory(workPlaneDirectory);
var flowPath = builder.Environment.IsEnvironment("Testing")
? Path.Combine(Path.GetTempPath(), $"agentstration-flow-tests-{Guid.NewGuid():N}.db")
var flowPath = isTesting
? Path.Combine(dataDirectory, "flow-plane.db")
: builder.Configuration["Data:FlowPath"] ?? Path.Combine(builder.Environment.ContentRootPath, ".agentstration", "flow-plane.db");
var flowDirectory = Path.GetDirectoryName(flowPath);
if (!string.IsNullOrWhiteSpace(flowDirectory)) Directory.CreateDirectory(flowDirectory);
var runtimePath = builder.Environment.IsEnvironment("Testing")
? Path.Combine(Path.GetTempPath(), $"agentstration-runtime-tests-{Guid.NewGuid():N}.db")
var runtimePath = isTesting
? Path.Combine(dataDirectory, "runtime-plane.db")
: builder.Configuration["Data:RuntimePath"] ?? Path.Combine(builder.Environment.ContentRootPath, ".agentstration", "runtime-plane.db");
var runtimeDirectory = Path.GetDirectoryName(runtimePath);
if (!string.IsNullOrWhiteSpace(runtimeDirectory)) Directory.CreateDirectory(runtimeDirectory);
if (isTesting)
{
builder.Services.AddSingleton(new TestingDataDirectoryCleanupService(dataDirectory, ownsTestingDataDirectory));
builder.Services.AddSingleton<IHostedService>(provider => provider.GetRequiredService<TestingDataDirectoryCleanupService>());
}
builder.Services.AddAgentstration(dataDirectory, aiOptions, $"Data Source={controlPlanePath}", $"Data Source={workPlanePath}", $"Data Source={flowPath}", $"Data Source={runtimePath}");
builder.Services.AddAgentstrationModelProviders(
builder.Configuration,
Expand Down
3 changes: 3 additions & 0 deletions tests/Agentstration.Management.Tests/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;

[assembly: Parallelize(Workers = 0, Scope = ExecutionScope.ClassLevel)]
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using Agentstration.Management.Storage.Sqlite;
using Agentstration.Resources;
using Agentstration.Runtime.Abstractions;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.DependencyInjection;

namespace Agentstration.Management.Tests;
Expand Down Expand Up @@ -261,7 +260,7 @@ public static async Task<StoreFixture> CreateAsync(ICurrentRequestContext? conte
public async ValueTask DisposeAsync()
{
await Provider.DisposeAsync();
SqliteConnection.ClearAllPools();
SqliteTestCleanup.ClearPoolsInDirectory(directory);
Directory.Delete(directory, true);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ private static WebApplicationFactory<Program> Factory(string path, string? passw
new WebApplicationFactory<Program>().WithWebHostBuilder(builder =>
{
builder.UseEnvironment("Testing");
builder.UseSetting("Data:TestingDirectory", Path.Combine(path, ".test-data"));
builder.UseSetting("Agentstration:Authentication:Mode", "Local");
builder.UseSetting("Agentstration:Bootstrap:Path", Directory.GetParent(path)!.FullName);
builder.UseSetting("Agentstration:Bootstrap:InitialBootstrapEnabled", "true");
Expand Down Expand Up @@ -433,6 +434,10 @@ public TemporaryDirectory()
}

public string Path { get; }
public void Dispose() => Directory.Delete(Path, recursive: true);
public void Dispose()
{
SqliteTestCleanup.ClearPoolsInDirectory(Path);
Directory.Delete(Path, recursive: true);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ public async Task ExecuteSqlAsync(string sql)
public async ValueTask DisposeAsync()
{
await Services.DisposeAsync();
SqliteConnection.ClearAllPools();
SqliteTestCleanup.ClearPoolsInDirectory(directory);
Directory.Delete(directory, recursive: true);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Agentstration.Security.AspNetCoreIdentity;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

Expand Down Expand Up @@ -145,7 +144,7 @@ private static string TemporaryDirectory()

private static void DeleteTemporaryDirectory(string path)
{
SqliteConnection.ClearAllPools();
SqliteTestCleanup.ClearPoolsInDirectory(path);
if (Directory.Exists(path)) Directory.Delete(path, recursive: true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using Agentstration.Management.Core;
using Agentstration.Management.Storage.Sqlite;
using Agentstration.Resources;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.DependencyInjection;

namespace Agentstration.Management.Tests;
Expand Down Expand Up @@ -71,7 +70,7 @@ public async Task CreateProjectStoresImmutableWorkspaceSnapshot()
}
finally
{
SqliteConnection.ClearAllPools();
SqliteTestCleanup.ClearPoolsInDirectory(directory);
if (Directory.Exists(directory)) Directory.Delete(directory, true);
}
}
Expand Down
3 changes: 1 addition & 2 deletions tests/Agentstration.Management.Tests/PackTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
using Agentstration.Runtime.Contracts;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.DependencyInjection;

namespace Agentstration.Management.Tests;
Expand Down Expand Up @@ -880,7 +879,7 @@ public static async Task<PackFixture> CreateAsync()
}
public async ValueTask DisposeAsync()
{
await provider.DisposeAsync(); SqliteConnection.ClearAllPools(); if (Directory.Exists(directory)) Directory.Delete(directory, true);
await provider.DisposeAsync(); SqliteTestCleanup.ClearPoolsInDirectory(directory); if (Directory.Exists(directory)) Directory.Delete(directory, true);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using Agentstration.Management.Abstractions;
using Agentstration.Management.Storage.Sqlite;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.DependencyInjection;

namespace Agentstration.Management.Tests;
Expand Down Expand Up @@ -45,7 +44,7 @@ public async Task AuditEventsSurviveControlPlaneProviderRestart()
}
finally
{
SqliteConnection.ClearAllPools();
SqliteTestCleanup.ClearPoolsInDirectory(directory);
if (Directory.Exists(directory)) Directory.Delete(directory, recursive: true);
}
}
Expand Down
22 changes: 22 additions & 0 deletions tests/Agentstration.Management.Tests/SqliteTestCleanup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Microsoft.Data.Sqlite;

namespace Agentstration.Management.Tests;

internal static class SqliteTestCleanup
{
public static void ClearPoolsInDirectory(string directory)
{
if (!Directory.Exists(directory)) return;

foreach (var databasePath in Directory.EnumerateFiles(directory, "*.db", SearchOption.AllDirectories))
{
ClearPool(databasePath);
}
}

public static void ClearPool(string databasePath)
{
using var connection = new SqliteConnection($"Data Source={databasePath}");
SqliteConnection.ClearPool(connection);
}
}
5 changes: 2 additions & 3 deletions tests/Agentstration.Management.Tests/TriggerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
using Agentstration.Management.Core;
using Agentstration.Management.Storage.Sqlite;
using Agentstration.Resources;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.DependencyInjection;

namespace Agentstration.Management.Tests;
Expand Down Expand Up @@ -114,7 +113,7 @@ public async Task OccurrenceStoreIsIdempotentWorkspaceScopedAndSurvivesRestartAs
Assert.IsEmpty(await restarted.Store.ListAsync(Guid.NewGuid(), trigger, 10, CancellationToken.None));
}
}
finally { SqliteConnection.ClearAllPools(); File.Delete(database); }
finally { SqliteTestCleanup.ClearPool(database); File.Delete(database); }
}

[TestMethod]
Expand Down Expand Up @@ -299,7 +298,7 @@ public static async Task<Fixture> CreateAsync()
public async ValueTask DisposeAsync()
{
await Services.DisposeAsync();
SqliteConnection.ClearAllPools();
SqliteTestCleanup.ClearPool(Database);
File.Delete(Database);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public async Task StartupDoesNotCreateLegacyDataJson()
new WebApplicationFactory<global::Program>().WithWebHostBuilder(builder =>
{
builder.UseEnvironment("Testing");
builder.UseSetting("Data:Directory", dataDirectory ?? Path.Combine(Path.GetTempPath(), $"agentstration-web-tests-{Guid.NewGuid():N}"));
if (dataDirectory is not null) builder.UseSetting("Data:TestingDirectory", dataDirectory);
builder.UseSetting("Logging:LogLevel:Default", "Warning");
});
}
Loading