diff --git a/AGENTS.md b/AGENTS.md index 6f99f465..69c34b84 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -162,6 +162,7 @@ - **데이터 정합성 & 정규화/역정규화**: 데이터 모델링 시 정합성 유지를 위해 관계형 데이터베이스의 **정규화**를 최우선으로 하며, 성능 최적화가 필수적인 어드민 조회 그리드용 데이터 전달(BFF/DTO) 시에만 제한적으로 안전하게 **역정규화**된 뷰 모델을 허용한다. - **과유불급 & 프로세스 단순화**: 복잡한 중첩 트리거와 과도한 추상화(Over-engineering)를 경계하는 **과유불급** 원칙을 따른다. 비즈니스 흐름은 최대한 선형적이고 명시적인 프로세스로 단순화하여 디버깅 및 추적 가시성을 극대화한다. - **바이브코딩(Vibe Coding) & 할루시네이션(Hallucination) 방지**: 퀀트 엔진 개발 시 LLM이나 인간 개발자의 주관적인 감(Vibe)과 추측에 의존한 임의의 상수 지정 또는 팩터 수식 재구성을 엄격히 금지한다. 모든 공식 및 의사결정 규칙은 `spec/*.yaml` 명세에 따라 철저히 **데이터 기반(Data-Driven)**으로 유도하고 테스트 코드로 실증한다. +- **단순 추측이 아닌 데이터 기반 예측**: 퀀트 모델의 모든 예측(알파, 리스크, 목표 가격 등)은 개발자의 직관이나 단순 추측이 아닌, 과거 시계열 통계 데이터 및 재현 가능한 백필 데이터를 근거로 설계한다. 모델 성능 평가는 E2E 테스트 하네스에서 산출된 정합성 결과와 백테스팅 실증 로그 등 철저히 데이터에 기반하여 의사결정을 수행한다. - **최적 알고리즘 & 게임이론**: 슬리피지 최소화 및 레짐(시장국면) 적응형 포지션 사이징 처리 시, 호가 갭 스프레드 분석과 동적 캘리브레이션을 포함하는 **최적 알고리즘**을 활용하며, 시장 참여자 간의 호가 유동성 경쟁 속에서 불리한 주문이 실행되지 않도록 체결 우선순위 Waterfall 모델(게임이론적 리스크 가드)을 장착한다. - **현장감 & 기술 부채**: 빌드 경고 및 사용되지 않는 쓰레기 코드를 즉각적으로 해결하여 **기술 부채**의 누적을 원천 차단한다. 실제 OpenAPI 응답 레이턴시, 스레드 병목 현상 및 어드민 DB 현황 조회 시 발생하는 트래픽을 로컬 및 E2E 실증 데이터로 직접 모니터링하여 **현장감** 있는 실전 최적화를 구현한다. - **패턴화 & 표준화 & 구조화**: 명명 규칙, 디자인 패턴(예: Repository, Factory 등) 및 뷰 엔진 레이아웃은 합의된 양식을 엄격히 준수하도록 **표준화**하고, 핵심 퀀트 리팩토링 단계마다 빌드 무결성을 보증하도록 아키텍처를 **구조화**한다. diff --git a/test-results/.last-run.json b/test-results/.last-run.json index 4cc0eac8..cbcc1fba 100644 --- a/test-results/.last-run.json +++ b/test-results/.last-run.json @@ -1,6 +1,4 @@ { - "status": "failed", - "failedTests": [ - "90c6053e24d905f92d61-fe6c7d0382f9666a596d" - ] + "status": "passed", + "failedTests": [] } \ No newline at end of file diff --git a/test-results/qe-m1-02-collection-run-QE-ae870-API-derived-expected-values-evidence/error-context.md b/test-results/qe-m1-02-collection-run-QE-ae870-API-derived-expected-values-evidence/error-context.md deleted file mode 100644 index 5ce36b02..00000000 --- a/test-results/qe-m1-02-collection-run-QE-ae870-API-derived-expected-values-evidence/error-context.md +++ /dev/null @@ -1,259 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: evidence\qe-m1-02-collection-run.spec.ts >> QE-M1-02: Collection Run List & Detail Verification >> QE-M1-02: Collection run renders in list with API-derived expected values -- Location: tests\e2e\evidence\qe-m1-02-collection-run.spec.ts:24:3 - -# Error details - -``` -Error: expect(received).toBeTruthy() - -Received: false -``` - -# Page snapshot - -```yaml -- generic [active] [ref=e1]: - - heading "An unhandled exception occurred while processing the request." [level=1] [ref=e2] - - generic [ref=e3]: "PostgresException: 28P01: password authentication failed for user \"quantengine_app\"" - - paragraph [ref=e4]: Npgsql.Internal.NpgsqlConnector.ReadMessageLong(bool async, DataRowLoadingMode dataRowLoadingMode, bool readingNotifications, bool isReadingPrependedMessage) - - list [ref=e5]: - - listitem [ref=e6] [cursor=pointer]: Stack - - listitem [ref=e7] [cursor=pointer]: Query - - listitem [ref=e8] [cursor=pointer]: Cookies - - listitem [ref=e9] [cursor=pointer]: Headers - - listitem [ref=e10] [cursor=pointer]: Routing - - list [ref=e12]: - - listitem [ref=e13]: - - 'heading "PostgresException: 28P01: password authentication failed for user \"quantengine_app\"" [level=2] [ref=e14]' - - list [ref=e15]: - - listitem [ref=e16]: - - heading "Npgsql.Internal.NpgsqlConnector.ReadMessageLong(bool async, DataRowLoadingMode dataRowLoadingMode, bool readingNotifications, bool isReadingPrependedMessage)" [level=3] [ref=e17] - - listitem [ref=e18]: - - heading "System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder+StateMachineBox.System.Threading.Tasks.Sources.IValueTaskSource.GetResult(short token)" [level=3] [ref=e19] - - listitem [ref=e20]: - - heading "Npgsql.Internal.NpgsqlConnector.AuthenticateSASL(List mechanisms, string username, bool async, CancellationToken cancellationToken)" [level=3] [ref=e21] - - listitem [ref=e22]: - - heading "Npgsql.Internal.NpgsqlConnector.Authenticate(string username, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken)" [level=3] [ref=e23] - - listitem [ref=e24]: - - heading "Npgsql.Internal.NpgsqlConnector.g__OpenCore|209_0(NpgsqlConnector conn, string username, SslMode sslMode, GssEncryptionMode gssEncMode, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken)" [level=3] [ref=e25] - - listitem [ref=e26]: - - heading "Npgsql.Internal.NpgsqlConnector.g__OpenCore|209_0(NpgsqlConnector conn, string username, SslMode sslMode, GssEncryptionMode gssEncMode, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken)" [level=3] [ref=e27] - - listitem [ref=e28]: - - heading "Npgsql.Internal.NpgsqlConnector.g__OpenCore|209_0(NpgsqlConnector conn, string username, SslMode sslMode, GssEncryptionMode gssEncMode, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken)" [level=3] [ref=e29] - - listitem [ref=e30]: - - heading "Npgsql.Internal.NpgsqlConnector.Open(NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken)" [level=3] [ref=e31] - - listitem [ref=e32]: - - heading "Npgsql.PoolingDataSource.OpenNewConnector(NpgsqlConnection conn, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken)" [level=3] [ref=e33] - - listitem [ref=e34]: - - heading "System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable+ConfiguredValueTaskAwaiter.GetResult()" [level=3] [ref=e35] - - listitem [ref=e36]: - - heading "Npgsql.PoolingDataSource.g__RentAsync|33_0(NpgsqlConnection conn, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken)" [level=3] [ref=e37] - - listitem [ref=e38]: - - heading "System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable+ConfiguredValueTaskAwaiter.GetResult()" [level=3] [ref=e39] - - listitem [ref=e40]: - - heading "Npgsql.NpgsqlConnection.g__OpenAsync|42_0(bool async, CancellationToken cancellationToken)" [level=3] [ref=e41] - - listitem [ref=e42]: - - heading "Dapper.SqlMapper.QueryRowAsync(IDbConnection cnn, Row row, Type effectiveType, CommandDefinition command) in SqlMapper.Async.cs" [level=3] [ref=e43]: - - text: Dapper.SqlMapper.QueryRowAsync(IDbConnection cnn, Row row, Type effectiveType, CommandDefinition command) in - - code [ref=e44]: SqlMapper.Async.cs - - listitem [ref=e45]: - - heading "QuantEngine.Infrastructure.Repositories.WorkspaceRepository.GetAccountByUsernameAsync(string username) in WorkspaceRepository.cs" [level=3] [ref=e46]: - - text: QuantEngine.Infrastructure.Repositories.WorkspaceRepository.GetAccountByUsernameAsync(string username) in - - code [ref=e47]: WorkspaceRepository.cs - - button "+" [ref=e48] [cursor=pointer] - - list [ref=e50]: - - listitem [ref=e51]: return await conn.QueryFirstOrDefaultAsync(@" - - listitem [ref=e52]: - - heading "QuantEngine.Web.Services.AuthService.AuthenticateAsync(string username, string password, string ipAddress) in AuthService.cs" [level=3] [ref=e53]: - - text: QuantEngine.Web.Services.AuthService.AuthenticateAsync(string username, string password, string ipAddress) in - - code [ref=e54]: AuthService.cs - - button "+" [ref=e55] [cursor=pointer] - - list [ref=e57]: - - listitem [ref=e58]: var account = await _workspaceRepository.GetAccountByUsernameAsync(username.Trim()); - - listitem [ref=e59]: - - heading "QuantEngine.Web.Pages.Account.LoginModel.OnPostAsync(string username, string password, bool rememberUsername) in Login.cshtml.cs" [level=3] [ref=e60]: - - text: QuantEngine.Web.Pages.Account.LoginModel.OnPostAsync(string username, string password, bool rememberUsername) in - - code [ref=e61]: Login.cshtml.cs - - button "+" [ref=e62] [cursor=pointer] - - list [ref=e64]: - - listitem [ref=e65]: var account = await _authService.AuthenticateAsync(username, password, ipAddress); - - listitem [ref=e66]: - - heading "Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.ExecutorFactory+GenericTaskHandlerMethod.Convert(object taskAsObject)" [level=3] [ref=e67] - - listitem [ref=e68]: - - heading "Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.ExecutorFactory+GenericTaskHandlerMethod.Execute(object receiver, object[] arguments)" [level=3] [ref=e69] - - listitem [ref=e70]: - - heading "Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeHandlerMethodAsync()" [level=3] [ref=e71] - - listitem [ref=e72]: - - heading "Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeNextPageFilterAsync()" [level=3] [ref=e73] - - listitem [ref=e74]: - - heading "Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Rethrow(PageHandlerExecutedContext context)" [level=3] [ref=e75] - - listitem [ref=e76]: - - heading "Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)" [level=3] [ref=e77] - - listitem [ref=e78]: - - heading "Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeInnerFilterAsync()" [level=3] [ref=e79] - - listitem [ref=e80]: - - heading "Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)" [level=3] [ref=e81] - - listitem [ref=e82]: - - heading "Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)" [level=3] [ref=e83] - - listitem [ref=e84]: - - heading "Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)" [level=3] [ref=e85] - - listitem [ref=e86]: - - heading "Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)" [level=3] [ref=e87] - - listitem [ref=e88]: - - heading "Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Logged|17_1(ResourceInvoker invoker)" [level=3] [ref=e89] - - listitem [ref=e90]: - - heading "Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Logged|17_1(ResourceInvoker invoker)" [level=3] [ref=e91] - - listitem [ref=e92]: - - heading "Microsoft.AspNetCore.Routing.EndpointMiddleware.g__AwaitRequestTask|7_0(Endpoint endpoint, Task requestTask, ILogger logger)" [level=3] [ref=e93] - - listitem [ref=e94]: - - heading "Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)" [level=3] [ref=e95] - - listitem [ref=e96]: - - heading "Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)" [level=3] [ref=e97] - - listitem [ref=e98]: - - heading "Serilog.AspNetCore.RequestLoggingMiddleware.Invoke(HttpContext httpContext)" [level=3] [ref=e99] - - listitem [ref=e100]: - - heading "Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)" [level=3] [ref=e101] - - listitem [ref=e102]: - - button "Show raw exception details" [ref=e104] [cursor=pointer] -``` - -# Test source - -```ts - 1 | import { test, expect } from '@playwright/test'; - 2 | import * as fs from 'fs'; - 3 | import * as path from 'path'; - 4 | - 5 | test.describe('QE-M1-02: Collection Run List & Detail Verification', () => { - 6 | // Login before each test - 7 | test.beforeEach(async ({ page }) => { - 8 | await page.goto('/Account/Login'); - 9 | await page.waitForLoadState('domcontentloaded'); - 10 | - 11 | // Fill login form with credentials (admin/admin) - 12 | const usernameInput = page.locator('#username'); - 13 | const passwordInput = page.locator('#password'); - 14 | const loginButton = page.locator('#loginBtn'); - 15 | - 16 | await usernameInput.fill('admin'); - 17 | await passwordInput.fill('admin'); - 18 | await loginButton.click(); - 19 | - 20 | // Wait for login to complete - 21 | await page.waitForLoadState('domcontentloaded'); - 22 | }); - 23 | - 24 | test('QE-M1-02: Collection run renders in list with API-derived expected values', async ({ page }) => { - 25 | // Step 1: Fetch expected values from API (source of truth) - 26 | const apiResponse = await page.request.get('/api/collection/runs?limit=20'); -> 27 | expect(apiResponse.ok()).toBeTruthy(); - | ^ Error: expect(received).toBeTruthy() - 28 | - 29 | const responseJson = await apiResponse.json(); - 30 | const runs = (responseJson as any).runs || []; - 31 | - 32 | // Fail if no collection runs exist in database - 33 | if (runs.length === 0) { - 34 | throw new Error( - 35 | 'No collection runs in DB — run the daily-collection job first. ' + - 36 | 'Expected at least 1 run from kis_collection_runs table.' - 37 | ); - 38 | } - 39 | - 40 | // Extract expected values from most recent run (first in list) - 41 | const expectedRun = runs[0]; - 42 | const expectedRunId = expectedRun.runId; - 43 | const expectedTotalSnapshots = expectedRun.totalSnapshots ?? 0; - 44 | const expectedStatus = expectedRun.status; // e.g., "completed", "running", "failed" - 45 | - 46 | // Map status to Korean text (same logic as Index.cshtml — unknown statuses - 47 | // like COMPLETED_WITH_ERRORS render the raw status string in a secondary badge) - 48 | let expectedStatusText = String(expectedStatus ?? ''); - 49 | if (expectedStatus?.toLowerCase() === 'completed') { - 50 | expectedStatusText = '완료'; - 51 | } else if (expectedStatus?.toLowerCase() === 'running') { - 52 | expectedStatusText = '진행 중'; - 53 | } else if (expectedStatus?.toLowerCase() === 'failed') { - 54 | expectedStatusText = '실패'; - 55 | } - 56 | - 57 | console.log( - 58 | `\n=== QE-M1-02 Test Started ===\n` + - 59 | `Expected RunId: ${expectedRunId}\n` + - 60 | `Expected TotalSnapshots: ${expectedTotalSnapshots}\n` + - 61 | `Expected Status: ${expectedStatus} (rendered as: ${expectedStatusText})\n` - 62 | ); - 63 | - 64 | // Step 2: Navigate to Collection admin page - 65 | await page.goto('/Admin/Collection'); - 66 | await page.waitForLoadState('domcontentloaded'); - 67 | - 68 | // Step 3: Verify page title contains "데이터 수집" (collection) - 69 | const pageTitle = await page.title(); - 70 | expect(pageTitle).toContain('데이터 수집'); - 71 | - 72 | // Step 4: Assert that a row containing the expected runId is visible - 73 | const runIdCell = page.locator(`td:has-text("${expectedRunId}")`); - 74 | await expect(runIdCell).toBeVisible(); - 75 | console.log(`✓ RunId row found and visible: ${expectedRunId}`); - 76 | - 77 | // Step 5: Find the row containing this runId and verify the snapshot count - 78 | const tableRow = runIdCell.locator('xpath=ancestor::tr'); - 79 | - 80 | // Within the row, find all td elements and map to columns - 81 | // Columns: 실행 ID (0), 시작 시간 (1), 종료 시간 (2), 상태 (3), 스냅샷 수 (4), 오류 수 (5) - 82 | const cells = tableRow.locator('td'); - 83 | const cellCount = await cells.count(); - 84 | expect(cellCount).toBeGreaterThanOrEqual(5); // At least 5 columns - 85 | - 86 | // Cell 4 (index 4) is "스냅샷 수" (total snapshots) - 87 | const snapshotCell = cells.nth(4); - 88 | const snapshotText = await snapshotCell.textContent(); - 89 | expect(snapshotText?.trim()).toBe(String(expectedTotalSnapshots)); - 90 | console.log(`✓ Snapshot count matches: ${snapshotText?.trim()} == ${expectedTotalSnapshots}`); - 91 | - 92 | // Cell 3 (index 3) is "상태" (status badge) - 93 | const statusCell = cells.nth(3); - 94 | const statusBadge = statusCell.locator('span.badge'); - 95 | const statusBadgeText = await statusBadge.textContent(); - 96 | expect(statusBadgeText?.trim()).toBe(expectedStatusText); - 97 | console.log(`✓ Status badge matches: ${statusBadgeText?.trim()} == ${expectedStatusText}`); - 98 | - 99 | // Step 6: Create screenshot directory and take screenshot of collection list - 100 | const screenshotDir = path.join(process.cwd(), 'Temp', 'evidence', 'QE-M1-02', 'screenshots'); - 101 | fs.mkdirSync(screenshotDir, { recursive: true }); - 102 | - 103 | await page.screenshot({ - 104 | path: path.join(screenshotDir, '01-collection-page.png'), - 105 | fullPage: true, - 106 | }); - 107 | console.log(`✓ Screenshot saved: 01-collection-page.png`); - 108 | - 109 | // Step 7: Navigate to the run detail page - 110 | // The detail page route is /Admin/Collection/{runId} - 111 | await page.goto(`/Admin/Collection/${expectedRunId}`); - 112 | await page.waitForLoadState('domcontentloaded'); - 113 | - 114 | // Step 8: Verify detail page title contains the runId - 115 | const detailPageTitle = await page.title(); - 116 | expect(detailPageTitle).toContain('수집 실행 상세'); - 117 | - 118 | // Step 9: Verify that the RunId is displayed on the detail page - 119 | // The page title shows: "수집 실행 상세 - {runId}" - 120 | const pageHeading = page.locator('h2.page-title'); - 121 | const headingText = await pageHeading.textContent(); - 122 | expect(headingText).toContain(expectedRunId); - 123 | console.log(`✓ Detail page title contains RunId: ${headingText}`); - 124 | - 125 | // Step 10: Verify snapshots count is displayed on detail page - 126 | // The snapshot count appears in a card with "스냅샷 수" as the title - 127 | const snapshotCountCard = page.locator('h4.card-title:has-text("스냅샷 수")'); -``` \ No newline at end of file diff --git a/test-results/qe-m1-02-collection-run-QE-ae870-API-derived-expected-values-evidence/test-failed-1.png b/test-results/qe-m1-02-collection-run-QE-ae870-API-derived-expected-values-evidence/test-failed-1.png deleted file mode 100644 index b0f2ecbe..00000000 Binary files a/test-results/qe-m1-02-collection-run-QE-ae870-API-derived-expected-values-evidence/test-failed-1.png and /dev/null differ diff --git a/test-results/qe-m1-02-collection-run-QE-ae870-API-derived-expected-values-evidence/trace.zip b/test-results/qe-m1-02-collection-run-QE-ae870-API-derived-expected-values-evidence/trace.zip deleted file mode 100644 index 53c8c9a1..00000000 Binary files a/test-results/qe-m1-02-collection-run-QE-ae870-API-derived-expected-values-evidence/trace.zip and /dev/null differ