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), "축 없는(빈) 판정도 별개"); } } }