diff --git a/src/ClawdDotNet.Core/Backup/BackupService.cs b/src/ClawdDotNet.Core/Backup/BackupService.cs
index 64d7c6c..47ff283 100644
--- a/src/ClawdDotNet.Core/Backup/BackupService.cs
+++ b/src/ClawdDotNet.Core/Backup/BackupService.cs
@@ -157,7 +157,10 @@ public sealed class BackupService
var connectionString = new SqliteConnectionStringBuilder
{
DataSource = sourcePath,
- Mode = SqliteOpenMode.ReadOnly
+ Mode = SqliteOpenMode.ReadOnly,
+ // Eine einmalige Kopie: Aus dem Pool heraus hielte die Verbindung die Datei
+ // danach offen, und unter Windows ließe sie sich nicht mehr ersetzen oder löschen.
+ Pooling = false
}.ToString();
using var conn = new SqliteConnection(connectionString);
diff --git a/src/ClawdDotNet.Core/Storage/SqliteStorage.cs b/src/ClawdDotNet.Core/Storage/SqliteStorage.cs
index 712da89..db31519 100644
--- a/src/ClawdDotNet.Core/Storage/SqliteStorage.cs
+++ b/src/ClawdDotNet.Core/Storage/SqliteStorage.cs
@@ -1,3 +1,4 @@
+using System.Collections.Concurrent;
using Microsoft.Data.Sqlite;
namespace ClawdDotNet.Core.Storage;
@@ -19,6 +20,9 @@ public sealed class SqliteStorage
private readonly string _connectionString;
private readonly SemaphoreSlim _writeGate = new(1, 1);
+ /// Every database opened in this process: full path → connection string.
+ private static readonly ConcurrentDictionary OpenedDatabases = new(StringComparer.OrdinalIgnoreCase);
+
public string DatabasePath { get; }
public SqliteStorage(string databasePath)
@@ -37,6 +41,7 @@ public sealed class SqliteStorage
Pooling = true,
DefaultTimeout = 30
}.ToString();
+ OpenedDatabases[Path.GetFullPath(databasePath)] = _connectionString;
Initialize();
@@ -51,6 +56,33 @@ public sealed class SqliteStorage
}
}
+ ///
+ /// Closes the pooled connections of the databases at — a
+ /// database file, or a directory and everything below it — so the files can be deleted
+ /// or replaced.
+ ///
+ /// Not : that is process-wide and also
+ /// takes the connections of every other database in use. The tests used it to clean up
+ /// their temporary databases, and a test running in parallel then lost its connection
+ /// while opening it — an ObjectDisposedException in ApplyPragmas, one in every
+ /// few full runs, always in a different test.
+ ///
+ public static void ReleasePooledConnections(string path)
+ {
+ var full = Path.GetFullPath(path);
+ var below = full.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
+
+ foreach (var (database, connectionString) in OpenedDatabases)
+ {
+ if (!string.Equals(database, full, StringComparison.OrdinalIgnoreCase)
+ && !database.StartsWith(below, StringComparison.OrdinalIgnoreCase))
+ continue;
+
+ using var connection = new SqliteConnection(connectionString);
+ SqliteConnection.ClearPool(connection);
+ }
+ }
+
public SqliteConnection OpenConnection()
{
var connection = new SqliteConnection(_connectionString);
diff --git a/tests/ClawdDotNet.App.Tests/Services/TaskAdministrationTests.cs b/tests/ClawdDotNet.App.Tests/Services/TaskAdministrationTests.cs
index b192dcf..ba25080 100644
--- a/tests/ClawdDotNet.App.Tests/Services/TaskAdministrationTests.cs
+++ b/tests/ClawdDotNet.App.Tests/Services/TaskAdministrationTests.cs
@@ -28,7 +28,7 @@ public sealed class TaskAdministrationTests : IDisposable
public void Dispose()
{
- Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
+ ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_dir);
try { Directory.Delete(_dir, recursive: true); }
catch { /* Aufraeumen ist Nebensache */ }
}
diff --git a/tests/ClawdDotNet.Core.Tests/Audit/AuditRepositoryTests.cs b/tests/ClawdDotNet.Core.Tests/Audit/AuditRepositoryTests.cs
index 629501c..14621ba 100644
--- a/tests/ClawdDotNet.Core.Tests/Audit/AuditRepositoryTests.cs
+++ b/tests/ClawdDotNet.Core.Tests/Audit/AuditRepositoryTests.cs
@@ -23,7 +23,7 @@ public sealed class AuditRepositoryTests : IDisposable
public void Dispose()
{
- Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
+ ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
try { Directory.Delete(_directory, recursive: true); }
catch { /* Aufräumen ist Nebensache */ }
}
diff --git a/tests/ClawdDotNet.Core.Tests/Backup/BackupServiceTests.cs b/tests/ClawdDotNet.Core.Tests/Backup/BackupServiceTests.cs
index a21e861..57b19cf 100644
--- a/tests/ClawdDotNet.Core.Tests/Backup/BackupServiceTests.cs
+++ b/tests/ClawdDotNet.Core.Tests/Backup/BackupServiceTests.cs
@@ -35,7 +35,7 @@ public sealed class BackupServiceTests : IDisposable
public void Dispose()
{
- Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
+ ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_root);
try { Directory.Delete(_root, recursive: true); }
catch { /* Aufräumen ist Nebensache */ }
}
@@ -95,7 +95,7 @@ public sealed class BackupServiceTests : IDisposable
}, default);
// Verbindungen schließen, damit VACUUM INTO auf eine ruhige Datei trifft.
- Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
+ ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_instanceDir);
}
private string ZipPath => Path.Combine(_root, "sicherung.zip");
diff --git a/tests/ClawdDotNet.Core.Tests/Backup/LicenseBackupTests.cs b/tests/ClawdDotNet.Core.Tests/Backup/LicenseBackupTests.cs
index e05d1b4..5da8455 100644
--- a/tests/ClawdDotNet.Core.Tests/Backup/LicenseBackupTests.cs
+++ b/tests/ClawdDotNet.Core.Tests/Backup/LicenseBackupTests.cs
@@ -40,7 +40,7 @@ public sealed class LicenseBackupTests : IDisposable
public void Dispose()
{
SecretKeyStore.UseDirectory(null);
- Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
+ ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_root);
try { Directory.Delete(_root, recursive: true); }
catch { /* Aufräumen ist Nebensache */ }
}
diff --git a/tests/ClawdDotNet.Core.Tests/Budget/BudgetGuardTests.cs b/tests/ClawdDotNet.Core.Tests/Budget/BudgetGuardTests.cs
index 5f533f5..c27a1cd 100644
--- a/tests/ClawdDotNet.Core.Tests/Budget/BudgetGuardTests.cs
+++ b/tests/ClawdDotNet.Core.Tests/Budget/BudgetGuardTests.cs
@@ -25,7 +25,7 @@ public sealed class BudgetGuardTests : IDisposable
public void Dispose()
{
- Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
+ ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
try { Directory.Delete(_directory, recursive: true); }
catch { /* Aufräumen ist Nebensache */ }
}
@@ -220,7 +220,7 @@ public sealed class UsageRepositoryTests : IDisposable
public void Dispose()
{
- Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
+ ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
try { Directory.Delete(_directory, recursive: true); }
catch { }
}
diff --git a/tests/ClawdDotNet.Core.Tests/Memory/MemoryRepositoryTests.cs b/tests/ClawdDotNet.Core.Tests/Memory/MemoryRepositoryTests.cs
index e7aede1..3a0ee2d 100644
--- a/tests/ClawdDotNet.Core.Tests/Memory/MemoryRepositoryTests.cs
+++ b/tests/ClawdDotNet.Core.Tests/Memory/MemoryRepositoryTests.cs
@@ -29,7 +29,7 @@ public sealed class MemoryRepositoryTests : IDisposable
public void Dispose()
{
- Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
+ ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
try { Directory.Delete(_directory, recursive: true); }
catch { /* Aufräumen ist Nebensache */ }
}
diff --git a/tests/ClawdDotNet.Core.Tests/Staging/StagingRepositoryTests.cs b/tests/ClawdDotNet.Core.Tests/Staging/StagingRepositoryTests.cs
index c8fa26e..2076e99 100644
--- a/tests/ClawdDotNet.Core.Tests/Staging/StagingRepositoryTests.cs
+++ b/tests/ClawdDotNet.Core.Tests/Staging/StagingRepositoryTests.cs
@@ -23,7 +23,7 @@ public sealed class StagingRepositoryTests : IDisposable
public void Dispose()
{
- Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
+ ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
try { Directory.Delete(_directory, recursive: true); }
catch { /* Aufräumen ist Nebensache */ }
}
diff --git a/tests/ClawdDotNet.Core.Tests/Staging/StagingServiceTests.cs b/tests/ClawdDotNet.Core.Tests/Staging/StagingServiceTests.cs
index 049dddc..5a1d96f 100644
--- a/tests/ClawdDotNet.Core.Tests/Staging/StagingServiceTests.cs
+++ b/tests/ClawdDotNet.Core.Tests/Staging/StagingServiceTests.cs
@@ -35,7 +35,7 @@ public sealed class StagingServiceTests : IDisposable
public void Dispose()
{
- Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
+ ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
try { Directory.Delete(_directory, recursive: true); }
catch { /* Aufräumen ist Nebensache */ }
}
diff --git a/tests/ClawdDotNet.Core.Tests/Staging/WerkzeugrechteTests.cs b/tests/ClawdDotNet.Core.Tests/Staging/WerkzeugrechteTests.cs
index 95eeceb..8365a4e 100644
--- a/tests/ClawdDotNet.Core.Tests/Staging/WerkzeugrechteTests.cs
+++ b/tests/ClawdDotNet.Core.Tests/Staging/WerkzeugrechteTests.cs
@@ -33,7 +33,7 @@ public sealed class WerkzeugrechteTests : IDisposable
public void Dispose()
{
- Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
+ ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
try { Directory.Delete(_directory, recursive: true); }
catch { /* Aufräumen ist Nebensache */ }
}
diff --git a/tests/ClawdDotNet.Core.Tests/Storage/PoolReleaseTests.cs b/tests/ClawdDotNet.Core.Tests/Storage/PoolReleaseTests.cs
new file mode 100644
index 0000000..e3f23bc
--- /dev/null
+++ b/tests/ClawdDotNet.Core.Tests/Storage/PoolReleaseTests.cs
@@ -0,0 +1,61 @@
+using ClawdDotNet.Core.Storage;
+using Shouldly;
+
+namespace ClawdDotNet.Core.Tests.Storage;
+
+///
+/// Releasing the pool of one database must leave every other database alone. The tests used
+/// the process-wide ClearAllPools to clean up, and a test running in parallel lost its
+/// connection in the middle of opening it — one red run in every few, always another test.
+///
+public sealed class PoolReleaseTests : IDisposable
+{
+ private readonly string _root = Path.Combine(Path.GetTempPath(), "clawd-tests", "pool-" + Guid.NewGuid().ToString("N")[..8]);
+
+ public void Dispose()
+ {
+ SqliteStorage.ReleasePooledConnections(_root);
+ try { Directory.Delete(_root, recursive: true); }
+ catch { /* cleaning up is secondary */ }
+ }
+
+ private static async Task TouchAsync(SqliteStorage storage)
+ => await storage.WriteAsync(async connection =>
+ {
+ await using var cmd = connection.CreateCommand();
+ cmd.CommandText = "INSERT OR REPLACE INTO ToolState (Key, Value) VALUES ('k', 'v')";
+ await cmd.ExecuteNonQueryAsync();
+ }, default);
+
+ [Fact]
+ public async Task Releasing_one_directory_frees_its_files_and_nothing_else()
+ {
+ var first = new SqliteStorage(Path.Combine(_root, "a", "state.db"));
+ var second = new SqliteStorage(Path.Combine(_root, "b", "state.db"));
+ await TouchAsync(first);
+ await TouchAsync(second);
+
+ SqliteStorage.ReleasePooledConnections(Path.Combine(_root, "a"));
+
+ // The released database can go — on Windows an open handle would prevent that.
+ Should.NotThrow(() => Directory.Delete(Path.Combine(_root, "a"), recursive: true));
+
+ // The other one keeps working, and so does its pool.
+ await TouchAsync(second);
+ await using var connection = await second.OpenConnectionAsync(default);
+ await using var cmd = connection.CreateCommand();
+ cmd.CommandText = "SELECT Value FROM ToolState WHERE Key = 'k'";
+ (await cmd.ExecuteScalarAsync()).ShouldBe("v");
+ }
+
+ [Fact]
+ public async Task A_database_file_can_be_named_directly()
+ {
+ var path = Path.Combine(_root, "c", "state.db");
+ await TouchAsync(new SqliteStorage(path));
+
+ SqliteStorage.ReleasePooledConnections(path);
+
+ Should.NotThrow(() => File.Delete(path));
+ }
+}
diff --git a/tests/ClawdDotNet.Core.Tests/Tasks/BudgetMissTests.cs b/tests/ClawdDotNet.Core.Tests/Tasks/BudgetMissTests.cs
index 44edfa7..6838c9a 100644
--- a/tests/ClawdDotNet.Core.Tests/Tasks/BudgetMissTests.cs
+++ b/tests/ClawdDotNet.Core.Tests/Tasks/BudgetMissTests.cs
@@ -32,7 +32,7 @@ public sealed class BudgetMissTests : IDisposable
public void Dispose()
{
- Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
+ ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
try { Directory.Delete(_directory, recursive: true); }
catch { /* cleanup is secondary */ }
}
diff --git a/tests/ClawdDotNet.Core.Tests/Tasks/CoordinationMigrationTests.cs b/tests/ClawdDotNet.Core.Tests/Tasks/CoordinationMigrationTests.cs
index 6289b39..c0c63a8 100644
--- a/tests/ClawdDotNet.Core.Tests/Tasks/CoordinationMigrationTests.cs
+++ b/tests/ClawdDotNet.Core.Tests/Tasks/CoordinationMigrationTests.cs
@@ -35,7 +35,7 @@ public sealed class CoordinationMigrationTests : IDisposable
public void Dispose()
{
- Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
+ ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
try { Directory.Delete(_directory, recursive: true); }
catch { /* Aufräumen ist Nebensache */ }
}
diff --git a/tests/ClawdDotNet.Core.Tests/Tasks/ScannerCapacityTests.cs b/tests/ClawdDotNet.Core.Tests/Tasks/ScannerCapacityTests.cs
index c42b213..ac17e37 100644
--- a/tests/ClawdDotNet.Core.Tests/Tasks/ScannerCapacityTests.cs
+++ b/tests/ClawdDotNet.Core.Tests/Tasks/ScannerCapacityTests.cs
@@ -30,7 +30,7 @@ public sealed class ScannerCapacityTests : IDisposable
public void Dispose()
{
- Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
+ ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
try { Directory.Delete(_directory, recursive: true); }
catch { /* cleanup is secondary */ }
}
diff --git a/tests/ClawdDotNet.Core.Tests/Tasks/TaskRepositoryTests.cs b/tests/ClawdDotNet.Core.Tests/Tasks/TaskRepositoryTests.cs
index 5671112..b4c3d75 100644
--- a/tests/ClawdDotNet.Core.Tests/Tasks/TaskRepositoryTests.cs
+++ b/tests/ClawdDotNet.Core.Tests/Tasks/TaskRepositoryTests.cs
@@ -27,7 +27,7 @@ public sealed class TaskRepositoryTests : IDisposable
public void Dispose()
{
- Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
+ ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
try { Directory.Delete(_directory, recursive: true); }
catch { /* Aufräumen ist Nebensache */ }
}
diff --git a/tests/ClawdDotNet.Core.Tests/Tasks/TaskScannerTests.cs b/tests/ClawdDotNet.Core.Tests/Tasks/TaskScannerTests.cs
index 042da8d..d3d7bf7 100644
--- a/tests/ClawdDotNet.Core.Tests/Tasks/TaskScannerTests.cs
+++ b/tests/ClawdDotNet.Core.Tests/Tasks/TaskScannerTests.cs
@@ -27,7 +27,7 @@ public sealed class TaskScannerTests : IDisposable
public void Dispose()
{
- Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
+ ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
try { Directory.Delete(_directory, recursive: true); }
catch { /* Aufräumen ist Nebensache */ }
}
diff --git a/tests/ClawdDotNet.Core.Tests/Tasks/TaskboardServiceTests.cs b/tests/ClawdDotNet.Core.Tests/Tasks/TaskboardServiceTests.cs
index abe883d..a7da9d7 100644
--- a/tests/ClawdDotNet.Core.Tests/Tasks/TaskboardServiceTests.cs
+++ b/tests/ClawdDotNet.Core.Tests/Tasks/TaskboardServiceTests.cs
@@ -28,7 +28,7 @@ public sealed class TaskboardServiceTests : IDisposable
public void Dispose()
{
- Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
+ ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
try { Directory.Delete(_directory, recursive: true); }
catch { /* Aufräumen ist Nebensache */ }
}
diff --git a/tests/ClawdDotNet.Core.Tests/Transcripts/PromptHistoryTests.cs b/tests/ClawdDotNet.Core.Tests/Transcripts/PromptHistoryTests.cs
index 150d8d2..b364725 100644
--- a/tests/ClawdDotNet.Core.Tests/Transcripts/PromptHistoryTests.cs
+++ b/tests/ClawdDotNet.Core.Tests/Transcripts/PromptHistoryTests.cs
@@ -26,7 +26,7 @@ public sealed class PromptHistoryTests : IDisposable
public void Dispose()
{
- Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
+ ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
try { Directory.Delete(_directory, recursive: true); }
catch { /* cleanup is best effort */ }
}
diff --git a/tests/ClawdDotNet.Manager.Tests/ManagerBackupsTests.cs b/tests/ClawdDotNet.Manager.Tests/ManagerBackupsTests.cs
index d9c3678..46632f9 100644
--- a/tests/ClawdDotNet.Manager.Tests/ManagerBackupsTests.cs
+++ b/tests/ClawdDotNet.Manager.Tests/ManagerBackupsTests.cs
@@ -34,7 +34,7 @@ public sealed class ManagerBackupsTests : IAsyncDisposable
public async ValueTask DisposeAsync()
{
await Task.CompletedTask;
- Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
+ ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_root);
try { Directory.Delete(_root, recursive: true); }
catch { /* Ein liegengebliebener Testordner faerbt keinen Lauf rot. */ }
}
diff --git a/tests/ClawdDotNet.Tools.Tests/Memory/MemoryToolTests.cs b/tests/ClawdDotNet.Tools.Tests/Memory/MemoryToolTests.cs
index b473236..8a211b5 100644
--- a/tests/ClawdDotNet.Tools.Tests/Memory/MemoryToolTests.cs
+++ b/tests/ClawdDotNet.Tools.Tests/Memory/MemoryToolTests.cs
@@ -28,7 +28,7 @@ public sealed class MemoryToolTests : IDisposable
public void Dispose()
{
- Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
+ ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_dir);
try { Directory.Delete(_dir, recursive: true); }
catch { /* Aufraeumen ist Nebensache */ }
}
diff --git a/tests/ClawdDotNet.Tools.Tests/Taskboard/AssignmentReachTests.cs b/tests/ClawdDotNet.Tools.Tests/Taskboard/AssignmentReachTests.cs
index 02f6162..a7fe20b 100644
--- a/tests/ClawdDotNet.Tools.Tests/Taskboard/AssignmentReachTests.cs
+++ b/tests/ClawdDotNet.Tools.Tests/Taskboard/AssignmentReachTests.cs
@@ -43,7 +43,7 @@ public sealed class AssignmentReachTests : IDisposable
public void Dispose()
{
- Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
+ ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
try { Directory.Delete(_directory, recursive: true); }
catch { /* cleanup is secondary */ }
}
diff --git a/tests/ClawdDotNet.Tools.Tests/Taskboard/DublettenTests.cs b/tests/ClawdDotNet.Tools.Tests/Taskboard/DublettenTests.cs
index 2c7e548..eac48a4 100644
--- a/tests/ClawdDotNet.Tools.Tests/Taskboard/DublettenTests.cs
+++ b/tests/ClawdDotNet.Tools.Tests/Taskboard/DublettenTests.cs
@@ -35,7 +35,7 @@ public sealed class DublettenTests : IDisposable
public void Dispose()
{
- Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
+ ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_verzeichnis);
try { Directory.Delete(_verzeichnis, recursive: true); }
catch { /* Aufräumen ist Nebensache */ }
}