Premiere commit - Omika

This commit is contained in:
2026-10-23 18:46:50 +02:00
commit 8ea76c1b24
2155 changed files with 984301 additions and 0 deletions
@@ -0,0 +1,95 @@
using System.Text.Json;
using Omika.Core.Observability.External;
using Omika.Core.Observability.Outbox;
using Omika.Diagnostics.Models;
namespace Omika.Diagnostics.Tests.Fakes;
/// <summary>Fabriques d'événements et d'enregistrements JSONL pour les tests.</summary>
public static class DiagnosticTestData
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
public static RemoteDiagnosticRecord Record(
Guid eventId,
string eventCode = "TEST",
DateTimeOffset? timestamp = null,
Guid? traceId = null,
Guid? sessionId = null,
Guid? operationId = null,
string? component = null,
string level = "Info",
string priority = "P2",
string? errorCode = null,
long? durationMs = null,
Dictionary<string, object?>? metadata = null,
string? actorKey = null,
int? actorKeyVersion = null,
string? clientCode = null,
string? serverInstance = null,
string? environment = null)
{
DateTimeOffset ts = timestamp ?? DateTimeOffset.UtcNow;
return new RemoteDiagnosticRecord
{
ReceivedAtUtc = ts,
BatchId = Guid.NewGuid(),
IngestionTraceId = Guid.NewGuid().ToString("D"),
ActorKey = actorKey,
ActorKeyVersion = actorKeyVersion,
ClientCode = clientCode,
ServerInstance = serverInstance,
Environment = environment,
Event = new DiagnosticEventDto
{
EventId = eventId,
EventCode = eventCode,
TimestampUtc = ts,
Level = level,
Priority = priority,
Component = component,
TraceId = traceId,
SessionId = sessionId,
OperationId = operationId,
ErrorCode = errorCode,
DurationMs = durationMs,
Metadata = metadata
}
};
}
public static DiagnosticEventRecord Event(
Guid eventId,
string eventCode = "TEST",
DateTimeOffset? timestamp = null,
Guid? traceId = null,
Guid? operationId = null,
string? component = null,
string level = "Info",
string? errorCode = null,
long? durationMs = null,
string? actorKey = null,
int? actorKeyVersion = null)
{
return new DiagnosticEventRecord
{
EventId = eventId,
EventCode = eventCode,
TimestampUtc = timestamp ?? DateTimeOffset.UtcNow,
TraceId = traceId,
OperationId = operationId,
Component = component,
Level = level,
Priority = "P2",
ErrorCode = errorCode,
DurationMs = durationMs,
ActorKey = actorKey,
ActorKeyVersion = actorKeyVersion,
Source = DiagnosticSources.Mobile,
IngestedAtUtc = DateTimeOffset.UtcNow
};
}
public static string SerializeLine(RemoteDiagnosticRecord record) =>
JsonSerializer.Serialize(record, JsonOptions);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,58 @@
using Omika.Core.Observability.External;
using Omika.Diagnostics.Import;
namespace Omika.Diagnostics.Tests.Fakes;
/// <summary>
/// Source JSONL en mémoire : modèle un flux de <see cref="RemoteDiagnosticRecord"/>
/// avec un checkpoint (LineIndex = index du dernier enregistrement lu). Permet de
/// tester l'importeur et son avancement de checkpoint sans PostgreSQL.
/// </summary>
public sealed class FakeDiagnosticJsonlSource : IDiagnosticJsonlSource
{
private readonly List<RemoteDiagnosticRecord> _records;
private DiagnosticImportCheckpoint? _checkpoint;
public FakeDiagnosticJsonlSource(IReadOnlyList<RemoteDiagnosticRecord> records)
{
_records = records.ToList();
}
public int CommitCount { get; private set; }
public DiagnosticImportCheckpoint? LastCommitted { get; private set; }
public DiagnosticImportCheckpoint? LoadCheckpoint() => _checkpoint;
public DiagnosticJsonlReadResult ReadNext(DiagnosticImportCheckpoint? after, int maxCount)
{
int start = after is null ? 0 : after.LineIndex + 1;
if (start >= _records.Count)
{
return new DiagnosticJsonlReadResult { ReachedEnd = true };
}
int count = Math.Min(maxCount, _records.Count - start);
List<RemoteDiagnosticRecord> records = _records.GetRange(start, count);
var next = new DiagnosticImportCheckpoint
{
FileName = "fake.jsonl",
LineIndex = start + count - 1
};
return new DiagnosticJsonlReadResult
{
Records = records,
NextPosition = next,
ReachedEnd = start + count >= _records.Count
};
}
public void Commit(DiagnosticImportCheckpoint position)
{
_checkpoint = position;
LastCommitted = position;
CommitCount++;
}
}
@@ -0,0 +1,158 @@
using Omika.Diagnostics.Referential;
namespace Omika.Diagnostics.Tests.Fakes;
/// <summary>
/// Fake en mémoire de <see cref="IDiagnosticReferenceRepository"/> pour les tests sans
/// PostgreSQL. Reproduit la sémantique du repository Npgsql : normalisation TRIM+UPPER,
/// unicité (type, code), code immuable, archivage (enabled=false) sans DELETE. Les codes
/// « observés » (pour <see cref="GetUnreferencedAsync"/>) sont injectés via
/// <see cref="SeedObserved"/> (miroir du DISTINCT diagnostic_event, sans base réelle).
/// </summary>
public sealed class FakeDiagnosticReferenceRepository : IDiagnosticReferenceRepository
{
private readonly List<DiagnosticReferenceRecord> _references = [];
private readonly List<(string Type, string Code, long Count, DateTimeOffset LastSeen)> _observed = [];
private long _nextId = 1;
public IReadOnlyList<DiagnosticReferenceRecord> Seed(params DiagnosticReferenceRecord[] records)
{
foreach (DiagnosticReferenceRecord r in records)
{
if (r.Id == 0)
{
r.Id = _nextId++;
}
else
{
_nextId = Math.Max(_nextId, r.Id + 1);
}
_references.Add(r);
}
return _references;
}
public void SeedObserved(string type, string code, long count, DateTimeOffset lastSeen) =>
_observed.Add((type, code, count, lastSeen));
public Task<IReadOnlyList<DiagnosticReferenceRecord>> ListAsync(
CancellationToken cancellationToken = default)
{
IReadOnlyList<DiagnosticReferenceRecord> ordered = _references
.OrderBy(r => r.ReferenceType, StringComparer.Ordinal)
.ThenBy(r => r.SortOrder)
.ThenBy(r => r.Code, StringComparer.Ordinal)
.ToList();
return Task.FromResult(ordered);
}
public Task<DiagnosticReferenceRecord?> GetByTypeAndCodeAsync(
string referenceType,
string code,
CancellationToken cancellationToken = default)
{
string? normalized = DiagnosticReferenceNormalization.NormalizeCode(code);
DiagnosticReferenceRecord? found = _references.FirstOrDefault(r =>
r.ReferenceType == referenceType && r.Code == normalized);
return Task.FromResult(found);
}
public Task<DiagnosticReferenceRecord?> GetByIdAsync(
long id,
CancellationToken cancellationToken = default)
{
return Task.FromResult(_references.FirstOrDefault(r => r.Id == id));
}
public Task<DiagnosticReferenceRecord?> CreateAsync(
string referenceType,
string code,
string label,
string? description,
bool enabled,
int sortOrder,
CancellationToken cancellationToken = default)
{
string? normalizedCode = DiagnosticReferenceNormalization.NormalizeCode(code);
string? normalizedLabel = DiagnosticReferenceNormalization.NormalizeLabel(label);
if (!DiagnosticReferenceTypes.IsSupported(referenceType) || normalizedCode is null || normalizedLabel is null)
{
return Task.FromResult<DiagnosticReferenceRecord?>(null);
}
if (_references.Any(r => r.ReferenceType == referenceType && r.Code == normalizedCode))
{
return Task.FromResult<DiagnosticReferenceRecord?>(null); // doublon.
}
DateTimeOffset now = DateTimeOffset.UtcNow;
var record = new DiagnosticReferenceRecord
{
Id = _nextId++,
ReferenceType = referenceType,
Code = normalizedCode,
Label = normalizedLabel,
Description = DiagnosticReferenceNormalization.NormalizeLabel(description),
Enabled = enabled,
SortOrder = sortOrder,
CreatedAtUtc = now,
UpdatedAtUtc = now
};
_references.Add(record);
return Task.FromResult<DiagnosticReferenceRecord?>(record);
}
public Task<bool> UpdateAsync(
long id,
string label,
string? description,
bool enabled,
int sortOrder,
CancellationToken cancellationToken = default)
{
string? normalizedLabel = DiagnosticReferenceNormalization.NormalizeLabel(label);
DiagnosticReferenceRecord? record = _references.FirstOrDefault(r => r.Id == id);
if (record is null || normalizedLabel is null)
{
return Task.FromResult(false);
}
record.Label = normalizedLabel;
record.Description = DiagnosticReferenceNormalization.NormalizeLabel(description);
record.Enabled = enabled;
record.SortOrder = sortOrder;
record.UpdatedAtUtc = DateTimeOffset.UtcNow;
return Task.FromResult(true);
}
public Task<IReadOnlyDictionary<string, string>> GetLabelsAsync(
string referenceType,
CancellationToken cancellationToken = default)
{
var labels = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (DiagnosticReferenceRecord r in _references.Where(r => r.ReferenceType == referenceType))
{
labels[r.Code] = r.Label;
}
return Task.FromResult<IReadOnlyDictionary<string, string>>(labels);
}
public Task<IReadOnlyList<DiagnosticUnreferencedCode>> GetUnreferencedAsync(
int limit,
CancellationToken cancellationToken = default)
{
var result = _observed
.Where(o => !_references.Any(r => r.ReferenceType == o.Type && r.Code == o.Code))
.Select(o => new DiagnosticUnreferencedCode(o.Type, o.Code, o.Count, o.LastSeen))
.OrderByDescending(x => x.ObservedCount)
.ThenBy(x => x.ReferenceType, StringComparer.Ordinal)
.ThenBy(x => x.Code, StringComparer.Ordinal)
.Take(Math.Clamp(limit, 1, 200))
.ToList();
return Task.FromResult<IReadOnlyList<DiagnosticUnreferencedCode>>(result);
}
}
@@ -0,0 +1,135 @@
using Omika.Diagnostics.Tasks;
namespace Omika.Diagnostics.Tests.Fakes;
/// <summary>
/// Fake en mémoire de <see cref="IDiagnosticTaskRepository"/> pour les tests sans
/// PostgreSQL. Reproduit la sémantique du repository Npgsql : unicité par signature
/// (group_url_identifier), statut normalisé, date de résolution dérivée du statut.
/// </summary>
public sealed class FakeDiagnosticTaskRepository : IDiagnosticTaskRepository
{
private readonly List<DiagnosticTaskRecord> _tasks = [];
private long _nextId = 1;
public IReadOnlyList<DiagnosticTaskRecord> Seed(params DiagnosticTaskRecord[] tasks)
{
foreach (DiagnosticTaskRecord t in tasks)
{
if (t.Id == 0)
{
t.Id = _nextId++;
}
else
{
_nextId = Math.Max(_nextId, t.Id + 1);
}
_tasks.Add(t);
}
return _tasks;
}
public Task<IReadOnlyList<DiagnosticTaskRecord>> ListAsync(
CancellationToken cancellationToken = default)
{
IReadOnlyList<DiagnosticTaskRecord> ordered = _tasks
.OrderByDescending(t => t.UpdatedAtUtc)
.ThenByDescending(t => t.Id)
.ToList();
return Task.FromResult(ordered);
}
public Task<DiagnosticTaskRecord?> GetByIdAsync(
long id,
CancellationToken cancellationToken = default) =>
Task.FromResult(_tasks.FirstOrDefault(t => t.Id == id));
public Task<DiagnosticTaskRecord?> GetByGroupAsync(
string groupUrlIdentifier,
CancellationToken cancellationToken = default) =>
Task.FromResult(_tasks.FirstOrDefault(t => t.GroupUrlIdentifier == groupUrlIdentifier));
public Task<DiagnosticTaskRecord?> CreateAsync(
string groupUrlIdentifier,
string title,
string? description,
string? cause,
string status,
string createdByUsername,
string? assigneeUsername,
CancellationToken cancellationToken = default)
{
string? group = Normalize(groupUrlIdentifier);
string? normalizedTitle = Normalize(title);
if (group is null || normalizedTitle is null)
{
return Task.FromResult<DiagnosticTaskRecord?>(null);
}
if (_tasks.Any(t => t.GroupUrlIdentifier == group))
{
return Task.FromResult<DiagnosticTaskRecord?>(null); // doublon.
}
string normalizedStatus = DiagnosticTaskStatuses.Normalize(status);
DateTimeOffset now = DateTimeOffset.UtcNow;
var record = new DiagnosticTaskRecord
{
Id = _nextId++,
GroupUrlIdentifier = group,
Title = normalizedTitle,
Description = Normalize(description),
Cause = Normalize(cause),
Status = normalizedStatus,
CreatedByUsername = createdByUsername,
AssigneeUsername = Normalize(assigneeUsername),
CreatedAtUtc = now,
UpdatedAtUtc = now,
ResolvedAtUtc = DiagnosticTaskStatuses.IsTerminal(normalizedStatus) ? now : null
};
_tasks.Add(record);
return Task.FromResult<DiagnosticTaskRecord?>(record);
}
public Task<bool> UpdateAsync(
long id,
string title,
string? description,
string? cause,
string status,
string? assigneeUsername,
CancellationToken cancellationToken = default)
{
DiagnosticTaskRecord? task = _tasks.FirstOrDefault(t => t.Id == id);
string? normalizedTitle = Normalize(title);
if (task is null || normalizedTitle is null)
{
return Task.FromResult(false);
}
string normalizedStatus = DiagnosticTaskStatuses.Normalize(status);
task.Title = normalizedTitle;
task.Description = Normalize(description);
task.Cause = Normalize(cause);
task.Status = normalizedStatus;
task.AssigneeUsername = Normalize(assigneeUsername);
task.UpdatedAtUtc = DateTimeOffset.UtcNow;
task.ResolvedAtUtc = DiagnosticTaskStatuses.IsTerminal(normalizedStatus)
? task.ResolvedAtUtc ?? DateTimeOffset.UtcNow
: null;
return Task.FromResult(true);
}
public Task<bool> DeleteAsync(
long id,
CancellationToken cancellationToken = default)
{
int removed = _tasks.RemoveAll(t => t.Id == id);
return Task.FromResult(removed > 0);
}
private static string? Normalize(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
@@ -0,0 +1,149 @@
using Omika.Diagnostics.Users;
namespace Omika.Diagnostics.Tests.Fakes;
/// <summary>
/// Fake en mémoire de <see cref="IDiagnosticUserRepository"/> pour les tests sans PostgreSQL.
/// Reproduit la sémantique du repository Npgsql : unicité insensible à la casse, tri par
/// username, hash stocké (jamais de mot de passe en clair).
/// </summary>
public sealed class FakeDiagnosticUserRepository : IDiagnosticUserRepository
{
private readonly List<DiagnosticUserRecord> _users = [];
private long _nextId = 1;
public IReadOnlyList<DiagnosticUserRecord> Seed(params DiagnosticUserRecord[] users)
{
foreach (DiagnosticUserRecord u in users)
{
if (u.Id == 0)
{
u.Id = _nextId++;
}
else
{
_nextId = Math.Max(_nextId, u.Id + 1);
}
_users.Add(u);
}
return _users;
}
public Task<DiagnosticUserRecord?> GetByUsernameAsync(
string username,
CancellationToken cancellationToken = default)
{
string? normalized = username?.Trim();
DiagnosticUserRecord? found = _users.FirstOrDefault(u =>
string.Equals(u.Username, normalized, StringComparison.OrdinalIgnoreCase));
return Task.FromResult(found);
}
public Task<IReadOnlyList<DiagnosticUserRecord>> ListAsync(
CancellationToken cancellationToken = default)
{
IReadOnlyList<DiagnosticUserRecord> ordered = _users
.OrderBy(u => u.Username, StringComparer.OrdinalIgnoreCase)
.ToList();
return Task.FromResult(ordered);
}
public Task<DiagnosticUserRecord?> CreateAsync(
string username,
string passwordHash,
string role,
CancellationToken cancellationToken = default)
{
string? normalized = username?.Trim();
if (string.IsNullOrWhiteSpace(normalized))
{
return Task.FromResult<DiagnosticUserRecord?>(null);
}
if (_users.Any(u => string.Equals(u.Username, normalized, StringComparison.OrdinalIgnoreCase)))
{
return Task.FromResult<DiagnosticUserRecord?>(null); // doublon.
}
DateTimeOffset now = DateTimeOffset.UtcNow;
var record = new DiagnosticUserRecord
{
Id = _nextId++,
Username = normalized,
PasswordHash = passwordHash,
Role = role,
Enabled = true,
CreatedAtUtc = now,
UpdatedAtUtc = now,
PasswordChangedAtUtc = now,
LastLoginAtUtc = null
};
_users.Add(record);
return Task.FromResult<DiagnosticUserRecord?>(record);
}
public Task<bool> SetEnabledAsync(
long id,
bool enabled,
CancellationToken cancellationToken = default)
{
DiagnosticUserRecord? user = _users.FirstOrDefault(u => u.Id == id);
if (user is null)
{
return Task.FromResult(false);
}
user.Enabled = enabled;
user.UpdatedAtUtc = DateTimeOffset.UtcNow;
return Task.FromResult(true);
}
public Task<bool> SetRoleAsync(
long id,
string role,
CancellationToken cancellationToken = default)
{
DiagnosticUserRecord? user = _users.FirstOrDefault(u => u.Id == id);
if (user is null)
{
return Task.FromResult(false);
}
user.Role = role;
user.UpdatedAtUtc = DateTimeOffset.UtcNow;
return Task.FromResult(true);
}
public Task<bool> UpdatePasswordHashAsync(
long id,
string passwordHash,
CancellationToken cancellationToken = default)
{
DiagnosticUserRecord? user = _users.FirstOrDefault(u => u.Id == id);
if (user is null)
{
return Task.FromResult(false);
}
user.PasswordHash = passwordHash;
user.PasswordChangedAtUtc = DateTimeOffset.UtcNow;
user.UpdatedAtUtc = DateTimeOffset.UtcNow;
return Task.FromResult(true);
}
public Task UpdateLastLoginAsync(
string username,
CancellationToken cancellationToken = default)
{
DiagnosticUserRecord? user = _users.FirstOrDefault(u =>
string.Equals(u.Username, username?.Trim(), StringComparison.OrdinalIgnoreCase));
if (user is not null)
{
user.LastLoginAtUtc = DateTimeOffset.UtcNow;
}
return Task.CompletedTask;
}
}
@@ -0,0 +1,88 @@
using Omika.Diagnostics.Models;
using Omika.Diagnostics.Repositories;
namespace Omika.Diagnostics.Tests.Fakes;
/// <summary>
/// Repository télémétrie in-memory (sans PostgreSQL) : reproduit le contrat
/// <c>ITelemetryMetricRepository</c> (upsert + purge bornée par bucket horaire).
/// </summary>
public sealed class FakeTelemetryMetricRepository : ITelemetryMetricRepository
{
private readonly List<TelemetryMetricHourly> _all = new();
private readonly object _sync = new();
private long _nextId = 1;
public IReadOnlyList<TelemetryMetricHourly> All
{
get
{
lock (_sync)
{
return _all.ToList();
}
}
}
public Task UpsertHourlyAsync(
IReadOnlyList<TelemetryMetricHourly> metrics,
CancellationToken cancellationToken = default)
{
lock (_sync)
{
foreach (TelemetryMetricHourly m in metrics)
{
m.Id = _nextId++;
_all.Add(m);
}
}
return Task.CompletedTask;
}
public Task<int> DeleteOlderThanAsync(
DateTimeOffset cutoff,
int batchSize,
CancellationToken cancellationToken = default)
{
if (batchSize <= 0)
{
return Task.FromResult(0);
}
lock (_sync)
{
List<TelemetryMetricHourly> toDelete = _all
.Where(m => m.BucketUtc < cutoff)
.OrderBy(m => m.BucketUtc)
.ThenBy(m => m.Id)
.Take(batchSize)
.ToList();
foreach (TelemetryMetricHourly m in toDelete)
{
_all.Remove(m);
}
return Task.FromResult(toDelete.Count);
}
}
}
/// <summary>
/// Simule PostgreSQL DOWN pour la télémétrie : toutes les opérations lèvent.
/// La boucle de rétention doit survivre (fail-open).
/// </summary>
public sealed class ThrowingTelemetryMetricRepository : ITelemetryMetricRepository
{
public Task UpsertHourlyAsync(
IReadOnlyList<TelemetryMetricHourly> metrics,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<int> DeleteOlderThanAsync(
DateTimeOffset cutoff,
int batchSize,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
}
@@ -0,0 +1,205 @@
using Omika.Diagnostics.Models;
using Omika.Diagnostics.Queries;
using Omika.Diagnostics.Repositories;
namespace Omika.Diagnostics.Tests.Fakes;
/// <summary>
/// Simule PostgreSQL DOWN (§52) : toutes les opérations lèvent, la sonde retourne
/// <c>false</c>. L'importeur doit survivre (résultat retryable), laisser le JSONL
/// intact et ne pas avancer le checkpoint.
/// </summary>
public sealed class ThrowingDiagnosticRepository : IDiagnosticEventRepository
{
public Task<int> InsertBatchAsync(
IReadOnlyList<DiagnosticEventRecord> events,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<DiagnosticQueryResult<DiagnosticEventRecord>> QueryAsync(
DiagnosticEventQuery query,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<IReadOnlyList<DiagnosticEventRecord>> GetByTraceIdAsync(
Guid traceId,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<IReadOnlyList<DiagnosticEventRecord>> GetByOperationIdAsync(
Guid operationId,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<bool> IsAvailableAsync(CancellationToken cancellationToken = default) =>
Task.FromResult(false);
public Task<int> DeleteOlderThanAsync(
DateTimeOffset cutoff,
int batchSize,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<long> DeleteErrorGroupAsync(
DiagnosticErrorGroupIdentity identity,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<IReadOnlyList<DiagnosticGroupCount>> CountGroupedAsync(
DiagnosticGroupDimension dimension,
DiagnosticAggregateQuery query,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<IReadOnlyList<DiagnosticBucketCount>> CountByTimeBucketAsync(
DiagnosticTimeBucket bucket,
DiagnosticAggregateQuery query,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<IReadOnlyList<DiagnosticBucketClientCount>> CountByTimeBucketAndClientAsync(
DiagnosticTimeBucket bucket,
DiagnosticAggregateQuery query,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<IReadOnlyList<DiagnosticLicenseErrorCount>> CountLicensesByErrorsAsync(
DiagnosticAggregateQuery query,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<IReadOnlyList<DiagnosticGroupCount>> CountErrorsByClientAsync(
DiagnosticAggregateQuery query,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<DiagnosticQueryResult<DiagnosticEventRecord>> SearchAsync(
DiagnosticSearchQuery query,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<IReadOnlyList<DiagnosticErrorGroup>> GroupErrorsAsync(
DiagnosticErrorGroupQuery query,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<long> CountAsync(
DiagnosticAggregateQuery query,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<long> CountErrorGroupsAsync(
DiagnosticErrorGroupQuery query,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<DiagnosticDurationStats?> GetDurationStatsAsync(
DiagnosticAggregateQuery query,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<IReadOnlyList<DiagnosticDurationByEventCode>> GetTopDurationByEventCodeAsync(
DiagnosticAggregateQuery query,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<IReadOnlyList<DiagnosticVersionSummary>> GetVersionSummariesAsync(
DiagnosticAggregateQuery query,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<DiagnosticVersionTotals> GetVersionTotalsAsync(
DiagnosticAggregateQuery query,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<DiagnosticSyncSummary> GetSyncSummaryAsync(
DiagnosticAggregateQuery query,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<DiagnosticErrorGroup?> GetErrorGroupAsync(
DiagnosticErrorGroupOccurrenceQuery query,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<IReadOnlyList<DiagnosticEventRecord>> GetErrorGroupOccurrencesAsync(
DiagnosticErrorGroupOccurrenceQuery query,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<IReadOnlyList<DiagnosticGroupCount>> CountErrorGroupByClientAsync(
DiagnosticErrorGroupOccurrenceQuery query,
int limit,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<IReadOnlyList<DiagnosticLicenseErrorCount>> CountErrorGroupByLicenseAsync(
DiagnosticErrorGroupOccurrenceQuery query,
int limit,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<DiagnosticEventRecord?> GetByIdAsync(
long id,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<IReadOnlyList<DiagnosticEventRecord>> GetCorrelatedAsync(
DiagnosticCorrelationQuery query,
CancellationToken cancellationToken = default)
{
query.Validate();
// Sans identifiant, le contrat exige « aucune requête » (§T3/17) : le résultat
// vide est retourné SANS toucher PostgreSQL, même si la base est down.
return query.HasAnyIdentifier
? throw new InvalidOperationException("PostgreSQL DOWN simulé")
: Task.FromResult<IReadOnlyList<DiagnosticEventRecord>>([]);
}
public Task<IReadOnlyList<string>> GetFilterOptionsAsync(
DiagnosticFilterDimension dimension,
DateTimeOffset fromUtc,
int limit,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<IReadOnlyList<DiagnosticRecentCorrelation>> GetRecentCorrelationsAsync(
DiagnosticCorrelationDimension dimension,
DateTimeOffset fromUtc,
int limit,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<long> CountActiveDevicesAsync(
DeviceFleetQuery query,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<IReadOnlyList<DeviceDistribution>> CountDevicesGroupedAsync(
DeviceFleetDimension dimension,
DeviceFleetQuery query,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<DeviceFleetQueryResult> GetDevicesAsync(
DeviceFleetQuery query,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<IReadOnlyList<DeviceDistribution>> CountDevicesByLicenseAsync(
DeviceFleetQuery query,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<IReadOnlyList<ScreenUsageRecord>> GetScreenUsageAsync(
UsageTelemetryQuery query,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
public Task<IReadOnlyList<FeatureUsageRecord>> GetFeatureUsageAsync(
UsageTelemetryQuery query,
CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("PostgreSQL DOWN simulé");
}