docs(constitution): add data-driven prediction principle to AGENTS.md
This commit is contained in:
@@ -162,6 +162,7 @@
|
||||
- **데이터 정합성 & 정규화/역정규화**: 데이터 모델링 시 정합성 유지를 위해 관계형 데이터베이스의 **정규화**를 최우선으로 하며, 성능 최적화가 필수적인 어드민 조회 그리드용 데이터 전달(BFF/DTO) 시에만 제한적으로 안전하게 **역정규화**된 뷰 모델을 허용한다.
|
||||
- **과유불급 & 프로세스 단순화**: 복잡한 중첩 트리거와 과도한 추상화(Over-engineering)를 경계하는 **과유불급** 원칙을 따른다. 비즈니스 흐름은 최대한 선형적이고 명시적인 프로세스로 단순화하여 디버깅 및 추적 가시성을 극대화한다.
|
||||
- **바이브코딩(Vibe Coding) & 할루시네이션(Hallucination) 방지**: 퀀트 엔진 개발 시 LLM이나 인간 개발자의 주관적인 감(Vibe)과 추측에 의존한 임의의 상수 지정 또는 팩터 수식 재구성을 엄격히 금지한다. 모든 공식 및 의사결정 규칙은 `spec/*.yaml` 명세에 따라 철저히 **데이터 기반(Data-Driven)**으로 유도하고 테스트 코드로 실증한다.
|
||||
- **단순 추측이 아닌 데이터 기반 예측**: 퀀트 모델의 모든 예측(알파, 리스크, 목표 가격 등)은 개발자의 직관이나 단순 추측이 아닌, 과거 시계열 통계 데이터 및 재현 가능한 백필 데이터를 근거로 설계한다. 모델 성능 평가는 E2E 테스트 하네스에서 산출된 정합성 결과와 백테스팅 실증 로그 등 철저히 데이터에 기반하여 의사결정을 수행한다.
|
||||
- **최적 알고리즘 & 게임이론**: 슬리피지 최소화 및 레짐(시장국면) 적응형 포지션 사이징 처리 시, 호가 갭 스프레드 분석과 동적 캘리브레이션을 포함하는 **최적 알고리즘**을 활용하며, 시장 참여자 간의 호가 유동성 경쟁 속에서 불리한 주문이 실행되지 않도록 체결 우선순위 Waterfall 모델(게임이론적 리스크 가드)을 장착한다.
|
||||
- **현장감 & 기술 부채**: 빌드 경고 및 사용되지 않는 쓰레기 코드를 즉각적으로 해결하여 **기술 부채**의 누적을 원천 차단한다. 실제 OpenAPI 응답 레이턴시, 스레드 병목 현상 및 어드민 DB 현황 조회 시 발생하는 트래픽을 로컬 및 E2E 실증 데이터로 직접 모니터링하여 **현장감** 있는 실전 최적화를 구현한다.
|
||||
- **패턴화 & 표준화 & 구조화**: 명명 규칙, 디자인 패턴(예: Repository, Factory 등) 및 뷰 엔진 레이아웃은 합의된 양식을 엄격히 준수하도록 **표준화**하고, 핵심 퀀트 리팩토링 단계마다 빌드 무결성을 보증하도록 아키텍처를 **구조화**한다.
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"status": "failed",
|
||||
"failedTests": [
|
||||
"90c6053e24d905f92d61-fe6c7d0382f9666a596d"
|
||||
]
|
||||
"status": "passed",
|
||||
"failedTests": []
|
||||
}
|
||||
-259
@@ -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<TResult>+StateMachineBox<TStateMachine>.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(short token)" [level=3] [ref=e19]
|
||||
- listitem [ref=e20]:
|
||||
- heading "Npgsql.Internal.NpgsqlConnector.AuthenticateSASL(List<string> 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.<Open>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.<Open>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.<Open>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<TResult>+ConfiguredValueTaskAwaiter.GetResult()" [level=3] [ref=e35]
|
||||
- listitem [ref=e36]:
|
||||
- heading "Npgsql.PoolingDataSource.<Get>g__RentAsync|33_0(NpgsqlConnection conn, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken)" [level=3] [ref=e37]
|
||||
- listitem [ref=e38]:
|
||||
- heading "System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable<TResult>+ConfiguredValueTaskAwaiter.GetResult()" [level=3] [ref=e39]
|
||||
- listitem [ref=e40]:
|
||||
- heading "Npgsql.NpgsqlConnection.<Open>g__OpenAsync|42_0(bool async, CancellationToken cancellationToken)" [level=3] [ref=e41]
|
||||
- listitem [ref=e42]:
|
||||
- heading "Dapper.SqlMapper.QueryRowAsync<T>(IDbConnection cnn, Row row, Type effectiveType, CommandDefinition command) in SqlMapper.Async.cs" [level=3] [ref=e43]:
|
||||
- text: Dapper.SqlMapper.QueryRowAsync<T>(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<WorkspaceAccount>(@"
|
||||
- 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<T>(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.<InvokeNextResourceFilter>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.<InvokeFilterPipelineAsync>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.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)" [level=3] [ref=e89]
|
||||
- listitem [ref=e90]:
|
||||
- heading "Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)" [level=3] [ref=e91]
|
||||
- listitem [ref=e92]:
|
||||
- heading "Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>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("스냅샷 수")');
|
||||
```
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 100 KiB |
BIN
Binary file not shown.
Reference in New Issue
Block a user