Инициализировать репозиторий «Дейл»
Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ, инструкция пользователя, техдокументация, код-стайл), бэклог, скрипты развёртывания и архив прототипа LeadRadar.
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
using Deal.Ml.Model;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace Deal.Ml.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Тесты персистентности модели (план Task 5): веса переживают перезапуск пула (второй инстанс
|
||||
/// на тот же SQLite-файл), журнал самооценки прунится до EVAL_KEEP=200, статус отдаёт окно
|
||||
/// EVAL_WINDOW=50 последних решений.
|
||||
/// </summary>
|
||||
public sealed class ModelPersistenceTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Перезапуск пула сохраняет веса: обучение на первом пуле → закрытие → второй пул на тот же
|
||||
/// каталог отдаёт те же классы/learned/eval и те же предсказания.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SecondPoolOnSameFile_ReloadsWeightsAndEval()
|
||||
{
|
||||
string dataDir = NewDataDir();
|
||||
try
|
||||
{
|
||||
MlStatusResult firstStatus;
|
||||
MlPredictResult firstPredict;
|
||||
using (var firstPool = new ModelPool(MlOptions.Create(dataDir)))
|
||||
{
|
||||
TenantModel model = firstPool.GetOrCreate("tenant-persist");
|
||||
List<LearnItem> items = LearningData.CanonicalTrainItems();
|
||||
model.LearnBatch(items.Take(24).ToList());
|
||||
model.LearnBatch(items.Skip(24).ToList());
|
||||
|
||||
firstStatus = model.Status();
|
||||
firstPredict = model.Predict(LearningData.DevMessage);
|
||||
}
|
||||
|
||||
using var secondPool = new ModelPool(MlOptions.Create(dataDir));
|
||||
TenantModel reloaded = secondPool.GetOrCreate("tenant-persist");
|
||||
|
||||
MlStatusResult secondStatus = reloaded.Status();
|
||||
MlPredictResult secondPredict = reloaded.Predict(LearningData.DevMessage);
|
||||
|
||||
Assert.True(secondStatus.Ready);
|
||||
Assert.Equal(firstStatus.Learned, secondStatus.Learned);
|
||||
Assert.Equal(firstStatus.Classes, secondStatus.Classes);
|
||||
Assert.Equal(firstStatus.Eval, secondStatus.Eval);
|
||||
Assert.True(secondPredict.Take);
|
||||
Assert.Equal(firstPredict.Label, secondPredict.Label);
|
||||
Assert.Equal(firstPredict.Scores, secondPredict.Scores);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TryDelete(dataDir);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Окно самооценки: журнал хранит не больше EVAL_KEEP (200) строк (проверка в файле), статус
|
||||
/// считает последние EVAL_WINDOW (50) решений. Сценарий — 250 дополнительных «реальных»
|
||||
/// действий поверх канонического батча.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EvalLog_PrunesToKeepAndWindowIsFifty()
|
||||
{
|
||||
string dataDir = NewDataDir();
|
||||
try
|
||||
{
|
||||
TenantModel model;
|
||||
using (var pool = new ModelPool(MlOptions.Create(dataDir)))
|
||||
{
|
||||
model = pool.GetOrCreate("tenant-eval-window");
|
||||
List<LearnItem> items = LearningData.CanonicalTrainItems();
|
||||
model.LearnBatch(items.Take(24).ToList());
|
||||
model.LearnBatch(items.Skip(24).ToList());
|
||||
|
||||
// 250 реальных действий (delta=1, метка совпадает с предсказанием) — самооценка пишется.
|
||||
model.LearnBatch(
|
||||
Enumerable.Repeat(LearningData.User(LearningData.ColumnDev, LearningData.DevMessage), 250).ToList());
|
||||
|
||||
MlStatusResult status = model.Status();
|
||||
|
||||
Assert.Equal(50, status.Eval.Count); // окно — последние EVAL_WINDOW
|
||||
Assert.Equal(50, status.Eval.Correct);
|
||||
Assert.Equal(1.0, status.Eval.Accuracy);
|
||||
|
||||
Assert.Equal(200, CountEvalRows(model.DatabasePath)); // журнал прунится до EVAL_KEEP
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
TryDelete(dataDir);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Прунинг переживает перезапуск: reload держит последние EVAL_KEEP и окно 50.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EvalLog_PrunePersistedAcrossPoolRestart()
|
||||
{
|
||||
string dataDir = NewDataDir();
|
||||
try
|
||||
{
|
||||
using (var pool = new ModelPool(MlOptions.Create(dataDir)))
|
||||
{
|
||||
TenantModel model = pool.GetOrCreate("tenant-eval-restart");
|
||||
List<LearnItem> items = LearningData.CanonicalTrainItems();
|
||||
model.LearnBatch(items.Take(24).ToList());
|
||||
model.LearnBatch(items.Skip(24).ToList());
|
||||
model.LearnBatch(
|
||||
Enumerable.Repeat(LearningData.User(LearningData.ColumnDev, LearningData.DevMessage), 250).ToList());
|
||||
}
|
||||
|
||||
using var secondPool = new ModelPool(MlOptions.Create(dataDir));
|
||||
TenantModel reloaded = secondPool.GetOrCreate("tenant-eval-restart");
|
||||
|
||||
MlStatusResult status = reloaded.Status();
|
||||
Assert.Equal(50, status.Eval.Count);
|
||||
Assert.Equal(1.0, status.Eval.Accuracy);
|
||||
Assert.Equal(200, CountEvalRows(reloaded.DatabasePath));
|
||||
}
|
||||
finally
|
||||
{
|
||||
TryDelete(dataDir);
|
||||
}
|
||||
}
|
||||
|
||||
// Читает число строк eval_log прямо из SQLite-файла модели.
|
||||
// dbPath: Путь к файлу модели.
|
||||
private static long CountEvalRows(string dbPath)
|
||||
{
|
||||
using var connection = new SqliteConnection($"Data Source={dbPath}");
|
||||
connection.Open();
|
||||
using SqliteCommand command = connection.CreateCommand();
|
||||
command.CommandText = "SELECT count(*) FROM eval_log";
|
||||
return (long)command.ExecuteScalar()!;
|
||||
}
|
||||
|
||||
// Новая временная папка данных моделей.
|
||||
private static string NewDataDir()
|
||||
{
|
||||
string dataDir = Path.Combine(Path.GetTempPath(), "deal-ml-persist-tests", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(dataDir);
|
||||
return dataDir;
|
||||
}
|
||||
|
||||
// Удаляет временную папку (мусор в temp допустим при сбое).
|
||||
// dataDir: Папка.
|
||||
private static void TryDelete(string dataDir)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(dataDir, recursive: true);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// Занятый каталог — тестовый мусор в temp допустим.
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user