using Application.Helpers;
using Infrastructure.StockData;
using Microsoft.EntityFrameworkCore;
namespace Application.Tests;
///
/// SEIBro 수집 마커(SeibroMarkers/SeibroFetchMarker/SeibroSweptDate) 검증 — 리뷰 결함 #1 해소 배선의 핵심.
/// rolling: 조회 시도(0행 포함) 마커로 대상 정렬(미조회 최우선→오래된 순) → starvation 방지.
/// date-sweep: "데이터 존재 OR 마커 존재" 판정으로 조용한 날 재조회 제거. 실제 SQL Server(LocalDB). 고유 JobKey 로 공유 DB 오염 방지.
///
[TestClass]
public sealed class SeibroMarkersTests
{
private static string NewJob() => $"test-{Guid.NewGuid():N}"[..30];
[TestMethod]
public async Task FetchMarker_TouchThenLoad_UpsertsAndBumpsTime()
{
// 기대 행위: TouchFetchMarker — 없으면 생성, 있으면 시각 갱신. LoadFetchMarkers 로 (키→시각) 사전 반환.
var job = NewJob();
using (var db = TestDb.Create())
{
await SeibroMarkers.TouchFetchMarkerAsync(db, job, "ISIN_A", CancellationToken.None);
await SeibroMarkers.TouchFetchMarkerAsync(db, job, "ISIN_B", CancellationToken.None);
}
DateTime firstA;
using (var db = TestDb.Create())
{
var markers = await SeibroMarkers.LoadFetchMarkersAsync(db, job, CancellationToken.None);
Assert.AreEqual(2, markers.Count);
Assert.IsTrue(markers.ContainsKey("ISIN_A"));
firstA = markers["ISIN_A"];
}
await Task.Delay(15);
using (var db = TestDb.Create())
{
await SeibroMarkers.TouchFetchMarkerAsync(db, job, "ISIN_A", CancellationToken.None); // 재조회 → 시각 갱신
}
using (var db = TestDb.Create())
{
var markers = await SeibroMarkers.LoadFetchMarkersAsync(db, job, CancellationToken.None);
Assert.AreEqual(2, markers.Count, "재Touch 는 새 행 생성 없이 upsert");
Assert.IsTrue(markers["ISIN_A"] > firstA, "재조회로 LastCheckedAt 갱신");
}
}
[TestMethod]
public async Task FetchMarker_Ordering_UncheckedFirstThenOldest()
{
// 기대 행위(#1 핵심): rolling 대상 정렬 = 미조회(마커 없음, null) 최우선 → 이미 조회한 것 중 오래된 순.
// 정상 0행 키가 마커를 남겨 매 실행 최우선 재조회되던 문제 해소를 SeibroRollingSweep 정렬로 검증.
var job = NewJob();
using (var db = TestDb.Create())
{
await SeibroMarkers.TouchFetchMarkerAsync(db, job, "OLD", CancellationToken.None); // 오래됨
await Task.Delay(15);
await SeibroMarkers.TouchFetchMarkerAsync(db, job, "RECENT", CancellationToken.None); // 최근
}
using var dbLoad = TestDb.Create();
var markers = await SeibroMarkers.LoadFetchMarkersAsync(dbLoad, job, CancellationToken.None);
var universe = new[] { "RECENT", "OLD", "NEW" };
var targets = universe.Select(k => (Key: k, LastUpdatedAt: markers.TryGetValue(k, out var t) ? (DateTime?)t : null)).ToList();
var order = new List();
await SeibroRollingSweep.RunAsync(targets, (k, _) => { order.Add(k); return Task.CompletedTask; }, maxPerRun: 3, delayMs: 0, ct: CancellationToken.None);
Assert.AreEqual("NEW", order[0], "미조회(마커 없음) 최우선");
Assert.AreEqual("OLD", order[1], "그다음 오래된 조회");
Assert.AreEqual("RECENT", order[2], "최근 조회는 마지막");
}
[TestMethod]
public async Task SweptDate_MarkThenIsSwept_SkipsQuietDay()
{
// 기대 행위(#1 핵심): 0행 조회 후 MarkSwept → IsSwept 가 true → 다음 실행 재조회 스킵. 미기록 날은 false.
var job = NewJob();
var swept = new DateOnly(2020, 3, 14);
var notSwept = new DateOnly(2020, 3, 15);
using (var db = TestDb.Create())
{
Assert.IsFalse(await SeibroMarkers.IsSweptAsync(db, job, swept, null, CancellationToken.None), "초기엔 미조회");
await SeibroMarkers.MarkSweptAsync(db, job, swept, null, CancellationToken.None); // 0행이어도 조회완료 기록
await SeibroMarkers.MarkSweptAsync(db, job, swept, null, CancellationToken.None); // 재기록 idempotent
}
using (var db = TestDb.Create())
{
Assert.IsTrue(await SeibroMarkers.IsSweptAsync(db, job, swept, null, CancellationToken.None), "조회완료 → 재조회 스킵");
Assert.IsFalse(await SeibroMarkers.IsSweptAsync(db, job, notSwept, null, CancellationToken.None), "미기록 날은 여전히 조회 대상");
var count = await db.SeibroSweptDate.AsNoTracking().Where(c => c.JobKey == job && c.Date == swept).CountAsync();
Assert.AreEqual(1, count, "재기록해도 단일 행(idempotent)");
}
}
[TestMethod]
public async Task SweptDate_Discriminator_IsolatesAxes()
{
// 기대 행위: 축(tpcd/kacd/nation) 구분 마커는 독립. 같은 날짜라도 축이 다르면 별도 조회 대상.
var job = NewJob();
var day = new DateOnly(2020, 5, 1);
using (var db = TestDb.Create())
{
await SeibroMarkers.MarkSweptAsync(db, job, day, "12", CancellationToken.None); // 구분 12 만 조회완료
}
using (var db = TestDb.Create())
{
Assert.IsTrue(await SeibroMarkers.IsSweptAsync(db, job, day, "12", CancellationToken.None), "구분 12 조회완료");
Assert.IsFalse(await SeibroMarkers.IsSweptAsync(db, job, day, "13", CancellationToken.None), "구분 13 은 여전히 대상");
Assert.IsFalse(await SeibroMarkers.IsSweptAsync(db, job, day, null, CancellationToken.None), "축 없는(빈) 판정도 별개");
}
}
}