fix(host): clear pooled SQLite handles before renaming the imported inbound-key file — Windows refused the rename, so the import re-ran every boot (QA vq03/vq04 2026-10-22)
This commit is contained in:
@@ -60,6 +60,35 @@ public sealed class InboundApiKeyStoreImportServiceTests : IDisposable
|
||||
Assert.True(File.Exists(path + "-shm.imported"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the rename succeeds after the importer read the file through a pooled
|
||||
/// SQLite connection — the library's real behaviour. The idle pooled handle stays open after
|
||||
/// the connection is disposed, and Windows refuses to rename a file with an open handle (the
|
||||
/// failure seen on the QA centrals 2026-10-22); the service must release pooled handles first.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task StartAsync_ImporterLeftPooledSqliteHandle_StillRenamesFile()
|
||||
{
|
||||
var path = Path.Combine(_dir, "inbound-api-keys.sqlite");
|
||||
var importer = new PooledSqliteReadingImporter();
|
||||
var service = CreateService(importer, sqlitePath: path);
|
||||
|
||||
await using (var create = new Microsoft.Data.Sqlite.SqliteConnection($"Data Source={path};Pooling=True"))
|
||||
{
|
||||
await create.OpenAsync();
|
||||
await using var cmd = create.CreateCommand();
|
||||
cmd.CommandText = "CREATE TABLE api_keys (key_id TEXT PRIMARY KEY)";
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
await service.StartAsync(CancellationToken.None);
|
||||
|
||||
Assert.True(importer.Read);
|
||||
Assert.False(File.Exists(path));
|
||||
Assert.True(File.Exists(path + ".imported"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the importer is not called when the file is absent.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
@@ -229,6 +258,25 @@ public sealed class InboundApiKeyStoreImportServiceTests : IDisposable
|
||||
Options.Create(new ApiKeyOptions { SqlitePath = sqlitePath }),
|
||||
NullLogger<InboundApiKeyStoreImportService>.Instance);
|
||||
|
||||
/// <summary>Reads the file through a pooled connection and disposes it, as the library importer does.</summary>
|
||||
private sealed class PooledSqliteReadingImporter : IInboundApiKeyStoreImporter
|
||||
{
|
||||
/// <summary>Gets a value indicating whether the file was read.</summary>
|
||||
public bool Read { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ApiKeyImportResult> ImportAsync(string sqlitePath, CancellationToken ct)
|
||||
{
|
||||
await using var connection = new Microsoft.Data.Sqlite.SqliteConnection($"Data Source={sqlitePath};Pooling=True");
|
||||
await connection.OpenAsync(ct);
|
||||
await using var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = "SELECT COUNT(*) FROM api_keys";
|
||||
await cmd.ExecuteScalarAsync(ct);
|
||||
Read = true;
|
||||
return new ApiKeyImportResult(0, 0, 0, 0, SourceFound: true);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeImporter : IInboundApiKeyStoreImporter
|
||||
{
|
||||
private int _calls;
|
||||
|
||||
Reference in New Issue
Block a user