using MeetingAssistant.Speakers; using Microsoft.EntityFrameworkCore; namespace MeetingAssistant.Tests; public sealed class SpeakerVoiceVectorPersistenceTests { [Fact] public async Task IdentityStoresVersionedVoiceVectorAndDeletesItWithIdentity() { var databasePath = Path.Combine( Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"), "speaker-identities.db"); Directory.CreateDirectory(Path.GetDirectoryName(databasePath)!); var options = new DbContextOptionsBuilder() .UseSqlite($"Data Source={databasePath}") .Options; await using (var context = new SpeakerIdentityDbContext(options)) { await SpeakerIdentitySchema.EnsureCreatedOrUpdatedAsync(context, CancellationToken.None); context.SpeakerIdentities.Add(new SpeakerIdentity { CanonicalName = "Chris", CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow, VoiceVectors = [ new SpeakerVoiceVector { ModelId = "resemblyzer-0.1.4-pretrained", Dimensions = 256, VectorBytes = new byte[256 * sizeof(float)], Fingerprint = "vector-a", CreatedAt = DateTimeOffset.UtcNow } ] }); await context.SaveChangesAsync(); } await using (var context = new SpeakerIdentityDbContext(options)) { var identity = await context.SpeakerIdentities .Include(candidate => candidate.VoiceVectors) .SingleAsync(); var vector = Assert.Single(identity.VoiceVectors); Assert.Equal("resemblyzer-0.1.4-pretrained", vector.ModelId); Assert.Equal(256, vector.Dimensions); Assert.Equal(256 * sizeof(float), vector.VectorBytes.Length); context.SpeakerIdentities.Remove(identity); await context.SaveChangesAsync(); } await using (var context = new SpeakerIdentityDbContext(options)) { Assert.Empty(await context.SpeakerVoiceVectors.ToListAsync()); } } [Fact] public async Task IdentityRejectsDuplicateVoiceVectorFingerprint() { var databasePath = Path.Combine( Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"), "speaker-identities.db"); Directory.CreateDirectory(Path.GetDirectoryName(databasePath)!); var options = new DbContextOptionsBuilder() .UseSqlite($"Data Source={databasePath}") .Options; await using var context = new SpeakerIdentityDbContext(options); await SpeakerIdentitySchema.EnsureCreatedOrUpdatedAsync(context, CancellationToken.None); var identity = new SpeakerIdentity { CanonicalName = "Chris", CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }; context.SpeakerIdentities.Add(identity); await context.SaveChangesAsync(); context.SpeakerVoiceVectors.AddRange( CreateVector(identity.Id, "same-vector"), CreateVector(identity.Id, "same-vector")); await Assert.ThrowsAsync(() => context.SaveChangesAsync()); } private static SpeakerVoiceVector CreateVector(int identityId, string fingerprint) { return new SpeakerVoiceVector { SpeakerIdentityId = identityId, ModelId = "resemblyzer-0.1.4-pretrained", Dimensions = 256, VectorBytes = new byte[256 * sizeof(float)], Fingerprint = fingerprint, CreatedAt = DateTimeOffset.UtcNow }; } }