feat(ai): centralize durable usage
CI and Deploy / test (pull_request) Successful in 5m19s
CI and Deploy / deploy (pull_request) Has been skipped

Add a content-free usage ledger with legacy backfill. Reserve Workspace and durable Strategy/CV work before execution so deleted history or duplicate admission cannot reset limits.
This commit is contained in:
cesnimda
2026-08-15 20:03:06 +02:00
parent dbff0f8d49
commit 134aac7bcf
28 changed files with 3539 additions and 83 deletions
+7 -1
View File
@@ -34,6 +34,10 @@ public sealed class AiOperationQueueTests
Assert.Equal("local_only", first.Operation.PrivacyPolicy);
Assert.Equal("ai_queue_full", full.Code);
Assert.Equal(15, full.RetryAfterSeconds);
var usage = Assert.Single(await scope.ServiceProvider.GetRequiredService<JobTrackerContext>()
.AiUsageRecords.IgnoreQueryFilters().AsNoTracking().ToListAsync());
Assert.Equal(first.Operation.Id.ToString("D"), usage.SourceId);
Assert.Equal(4_000, usage.EstimatedTokenCount);
}
[Fact]
@@ -137,7 +141,8 @@ public sealed class AiOperationQueueTests
if (GenerationFailure is not null) throw GenerationFailure;
OwnerUserId = services.GetRequiredService<ICurrentUserService>().UserId;
PrivacyPolicy = context.EffectivePrivacyPolicy;
return Task.FromResult(new AiOperationExecutionResult("synthetic-result", "ollama", "qwen-test", "local_primary"));
return Task.FromResult(new AiOperationExecutionResult(
"synthetic-result", "ollama", "qwen-test", "local_primary", 120, 40));
}
}
@@ -178,6 +183,7 @@ public sealed class AiOperationQueueTests
services.AddScoped<UserOperationStore>();
services.AddSingleton<AiPrivacyPolicy>();
services.AddSingleton<AiOperationExecutionScope>();
services.AddScoped<AiUsageMeter>();
services.AddScoped<AiOperationAdmission>();
services.AddSingleton<IAiOperationHandler>(handler);
services.AddSingleton<AiOperationWorker>();