chore: archive G06 market repository task files and add client market feature
This commit is contained in:
parent
48ee5189a9
commit
b5a544aa97
6 changed files with 654 additions and 81 deletions
|
|
@ -0,0 +1,231 @@
|
|||
<!-- task=m-operator-client-api-core-integration/03+01_market_repository plan=0 tag=API -->
|
||||
|
||||
# Code Review Reference - API
|
||||
|
||||
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> Fill implementation-owned sections, then stop with active files in place and report ready for review. Finalization is review-agent-only.
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-06-02
|
||||
task=m-operator-client-api-core-integration/03+01_market_repository, plan=0, tag=API
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/operator-surface/milestones/operator-client-api-core-integration.md`
|
||||
- Task ids:
|
||||
- `market-call`: market data status/list 기본 API 호출을 client core에서 수행할 수 있다.
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 종결 절차는 리뷰 에이전트 전용이다.
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [API-1] Market repository 추가 | [x] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [x] market repository/provider boundary를 추가하고 `listInstruments`, `listBars`를 `AltApiResult`로 반환한다. 검증: `cd apps/client && flutter test test/features/market/market_repository_test.dart`
|
||||
- [x] fake client로 request payload와 parsed result/status key를 확인한다. 검증: `cd apps/client && flutter test test/features/market/market_repository_test.dart`
|
||||
- [x] typed `ErrorInfo`가 `AltApiError`로 보존되는 test를 추가한다.
|
||||
- [x] `cd apps/client && flutter test`를 실행한다.
|
||||
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
- [x] PASS/WARN/FAIL 판정을 append한다.
|
||||
- [x] active files를 log로 archive한다.
|
||||
- [x] PASS이면 `complete.log` 작성 후 archive 이동한다.
|
||||
- [x] roadmap은 직접 수정하지 않는다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
계획상의 명세와 일치하게 구현하였으며, 특이사항 및 계획 대비 변경 사항은 없습니다. Sibling subtask인 `01_contract_mapping` 완료 상태와 결과 형태(`AltApiResult`, `AltApiError`)에 정렬하여 구현 및 검증을 완료하였습니다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
1. **`MarketRepository`의 Socket wrapping 구조**: `AltSocketClient`를 wrapping하여 raw socket message 응답을 안전하게 `AltApiResult` 형태로 전환하고, server가 응답으로 반환한 `ErrorInfo`를 `AltApiError`로 매핑하여 presentation layer에 일관되게 전달합니다.
|
||||
2. **연결 안정성 검증**: 소켓 클라이언트의 `isAlive` 상태를 먼저 확인하여 연결이 유실되었거나 활성화되지 않은 상황에서는 사전에 `AltTransportKind.disconnected`를 반환하도록 설계하였습니다.
|
||||
3. **Transport Error 세분화**: 통신 중 발생할 수 있는 `TimeoutException`, response type mismatch (`StateError`), socket disconnected 상황과 기타 미확인 예외들을 적절한 `AltTransportKind`로 세분화하여 예외 상황의 원인을 식별하기 용이하게 구성하였습니다.
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- predecessor `01_contract_mapping` completion을 확인했는가
|
||||
- repository가 raw protobuf error를 success처럼 반환하지 않는가
|
||||
- request payload와 parsed result key가 test에 고정됐는가
|
||||
|
||||
## 검증 결과
|
||||
|
||||
### API-1 중간 검증
|
||||
```text
|
||||
$ cd apps/client && flutter test test/features/market/market_repository_test.dart
|
||||
00:00 +0: .../alt/apps/client/test/features/market/market_repository_test.dart
|
||||
00:01 +0: .../alt/apps/client/test/features/market/market_repository_test.dart
|
||||
00:01 +0: MarketRepository tests listInstruments success flows parsed results
|
||||
00:01 +1: MarketRepository tests listInstruments success flows parsed results
|
||||
00:01 +1: MarketRepository tests listBars success flows historical data
|
||||
00:01 +2: MarketRepository tests listBars success flows historical data
|
||||
00:01 +2: MarketRepository ... typed ErrorInfo maps correctly to AltApiFailure
|
||||
00:01 +3: MarketRepository ... typed ErrorInfo maps correctly to AltApiFailure
|
||||
00:01 +3: ... tests timeout maps correctly to AltTransportKind.timeout
|
||||
00:01 +4: ... tests timeout maps correctly to AltTransportKind.timeout
|
||||
00:01 +4: ... type mismatch maps correctly to AltTransportKind.decodeFailure
|
||||
00:01 +5: ... type mismatch maps correctly to AltTransportKind.decodeFailure
|
||||
00:01 +5: ... tests client not alive returns AltTransportKind.disconnected
|
||||
00:01 +6: ... tests client not alive returns AltTransportKind.disconnected
|
||||
00:01 +6: All tests passed!
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```text
|
||||
$ cd apps/client && flutter test
|
||||
00:00 +0: ... /config/workspace/alt/apps/client/test/app/bootstrap_test.dart
|
||||
00:01 +0: ... /config/workspace/alt/apps/client/test/app/bootstrap_test.dart
|
||||
00:02 +0: ... /config/workspace/alt/apps/client/test/app/bootstrap_test.dart
|
||||
00:02 +0: ... does not call Firebase or Mattermost and returns null
|
||||
00:02 +1: ... does not call Firebase or Mattermost and returns null
|
||||
00:02 +1: ... bootstrapAltClient initializes Firebase and Mattermost push host
|
||||
00:02 +2: ... bootstrapAltClient initializes Firebase and Mattermost push host
|
||||
00:02 +2: ... runAltClient renders AltClientApp shell without Firebase/push
|
||||
00:02 +3: ... runAltClient renders AltClientApp shell without Firebase/push
|
||||
00:02 +4: ... runAltClient renders AltClientApp shell without Firebase/push
|
||||
00:02 +5: ... runAltClient renders AltClientApp shell without Firebase/push
|
||||
00:02 +6: ... runAltClient renders AltClientApp shell without Firebase/push
|
||||
00:02 +7: ... runAltClient renders AltClientApp shell without Firebase/push
|
||||
00:02 +8: ... runAltClient renders AltClientApp shell without Firebase/push
|
||||
00:02 +9: ... runAltClient renders AltClientApp shell without Firebase/push
|
||||
00:02 +10: ... runAltClient renders AltClientApp shell without Firebase/push
|
||||
00:02 +11: ... runAltClient renders AltClientApp shell without Firebase/push
|
||||
00:02 +12: ... runAltClient renders AltClientApp shell without Firebase/push
|
||||
00:02 +13: ... runAltClient renders AltClientApp shell without Firebase/push
|
||||
00:02 +14: ... runAltClient renders AltClientApp shell without Firebase/push
|
||||
00:02 +15: ... runAltClient renders AltClientApp shell without Firebase/push
|
||||
00:02 +16: ... MarketRepository tests listBars success flows historical data
|
||||
00:02 +17: ... MarketRepository tests listBars success flows historical data
|
||||
00:02 +17: ... tests typed ErrorInfo maps correctly to AltApiFailure
|
||||
00:02 +18: ... tests typed ErrorInfo maps correctly to AltApiFailure
|
||||
00:02 +18: ... tests timeout maps correctly to AltTransportKind.timeout
|
||||
00:02 +19: ... tests timeout maps correctly to AltTransportKind.timeout
|
||||
00:02 +19: ... type mismatch maps correctly to AltTransportKind.decodeFailure
|
||||
00:02 +20: ... type mismatch maps correctly to AltTransportKind.decodeFailure
|
||||
00:02 +20: ... tests client not alive returns AltTransportKind.disconnected
|
||||
00:02 +21: ... tests client not alive returns AltTransportKind.disconnected
|
||||
00:03 +21: ... tests client not alive returns AltTransportKind.disconnected
|
||||
00:03 +21: loading /config/workspace/alt/apps/client/test/widget_test.dart
|
||||
00:03 +21: ... ALT dashboard shell with default disconnected socket state
|
||||
00:03 +22: ... ALT dashboard shell with default disconnected socket state
|
||||
00:03 +22: /config/workspace/alt/apps/client/test/integrations/mattermost_push_host_integration_test.dart: auto-login failure does not block initialize
|
||||
[MattermostHost] Mattermost auto-login failed: Bad state: credentials missing
|
||||
00:03 +23: ... ALT dashboard shell with default disconnected socket state
|
||||
00:03 +24: ... ALT dashboard shell with default disconnected socket state
|
||||
00:03 +25: ... ALT dashboard shell with default disconnected socket state
|
||||
00:03 +26: ... ALT dashboard shell with default disconnected socket state
|
||||
00:03 +27: ... ALT dashboard shell with default disconnected socket state
|
||||
00:03 +28: ... ALT dashboard shell with default disconnected socket state
|
||||
00:03 +29: ... ALT dashboard shell with default disconnected socket state
|
||||
00:03 +30: ... ALT dashboard shell with default disconnected socket state
|
||||
00:03 +31: ... ALT dashboard shell with default disconnected socket state
|
||||
00:03 +32: ... ALT dashboard shell with default disconnected socket state
|
||||
00:03 +33: ... ALT dashboard shell with default disconnected socket state
|
||||
00:03 +34: ... ALT dashboard shell with default disconnected socket state
|
||||
00:03 +35: ... ALT dashboard shell with default disconnected socket state
|
||||
00:03 +36: ... ALT dashboard shell with default disconnected socket state
|
||||
00:03 +37: ... tests listBars market query request-response loop
|
||||
00:03 +38: ... shows ALT dashboard with socket state Connecting
|
||||
00:03 +39: ... shows ALT dashboard with socket state Connecting
|
||||
00:03 +40: ... shows ALT dashboard with socket state Connecting
|
||||
00:03 +40: ... shows ALT dashboard with socket state Connected
|
||||
00:03 +41: ... shows ALT dashboard with socket state Connected
|
||||
00:03 +41: ... shows ALT dashboard with socket state Error
|
||||
00:03 +42: ... shows ALT dashboard with socket state Error
|
||||
00:03 +42: ... opens ALT agent dock from the right rail
|
||||
00:03 +43: ... opens ALT agent dock from the right rail
|
||||
00:04 +43: ... opens ALT agent dock from the right rail
|
||||
00:04 +44: ... opens ALT agent dock from the right rail
|
||||
00:04 +45: ... opens ALT agent dock from the right rail
|
||||
00:04 +46: ... opens ALT agent dock from the right rail
|
||||
00:04 +47: ... opens ALT agent dock from the right rail
|
||||
00:04 +48: ... opens ALT agent dock from the right rail
|
||||
00:04 +49: ... opens ALT agent dock from the right rail
|
||||
00:04 +50: ... opens ALT agent dock from the right rail
|
||||
00:04 +51: ... opens ALT agent dock from the right rail
|
||||
00:04 +51: ... switches ALT workbench center content from the right rail
|
||||
00:04 +52: ... switches ALT workbench center content from the right rail
|
||||
00:04 +52: All tests passed!
|
||||
```
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
- 종합 판정: PASS
|
||||
- 차원별 평가:
|
||||
- correctness: Pass
|
||||
- completeness: Pass
|
||||
- test coverage: Pass
|
||||
- API contract: Pass
|
||||
- code quality: Pass
|
||||
- plan deviation: Pass
|
||||
- verification trust: Pass
|
||||
- 발견된 문제: 없음
|
||||
- 다음 단계: PASS이므로 active plan/review를 log로 archive하고 `complete.log` 작성 후 task archive 이동을 수행한다.
|
||||
|
||||
### 리뷰어 검증
|
||||
|
||||
```text
|
||||
$ cd apps/client && flutter test test/features/market/market_repository_test.dart
|
||||
00:00 +0: loading /config/workspace/alt/apps/client/test/features/market/market_repository_test.dart
|
||||
00:00 +0: MarketRepository tests listInstruments success flows parsed results
|
||||
00:00 +1: MarketRepository tests listBars success flows historical data
|
||||
00:00 +2: MarketRepository tests typed ErrorInfo maps correctly to AltApiFailure
|
||||
00:00 +3: MarketRepository tests timeout maps correctly to AltTransportKind.timeout
|
||||
00:00 +4: MarketRepository tests type mismatch maps correctly to AltTransportKind.decodeFailure
|
||||
00:00 +5: MarketRepository tests client not alive returns AltTransportKind.disconnected
|
||||
00:00 +6: All tests passed!
|
||||
```
|
||||
|
||||
```text
|
||||
$ cd apps/client && flutter test
|
||||
00:00 +0: loading /config/workspace/alt/apps/client/test/app/bootstrap_test.dart
|
||||
00:00 +1: /config/workspace/alt/apps/client/test/app/bootstrap_test.dart: AltClientBootstrap Tests opt-in bootstrapAltClient initializes Firebase and Mattermost push host
|
||||
00:00 +15: /config/workspace/alt/apps/client/test/features/market/market_repository_test.dart: MarketRepository tests listInstruments success flows parsed results
|
||||
00:00 +20: /config/workspace/alt/apps/client/test/features/market/market_repository_test.dart: MarketRepository tests client not alive returns AltTransportKind.disconnected
|
||||
00:01 +43: /config/workspace/alt/apps/client/test/integrations/socket/socket_connection_controller_test.dart: SocketConnectionController tests successful connection and handshake
|
||||
00:01 +52: All tests passed!
|
||||
```
|
||||
|
||||
```text
|
||||
$ cd apps/client && dart format --set-exit-if-changed lib/src/features/market/data/market_repository.dart test/features/market/market_repository_test.dart
|
||||
Formatted test/features/market/market_repository_test.dart
|
||||
Formatted 2 files (1 changed) in 0.02 seconds.
|
||||
```
|
||||
|
||||
```text
|
||||
$ cd apps/client && flutter analyze lib/src/features/market/data/market_repository.dart test/features/market/market_repository_test.dart
|
||||
Analyzing 2 items...
|
||||
No issues found! (ran in 2.6s)
|
||||
```
|
||||
|
||||
```text
|
||||
$ cd apps/client && flutter test test/features/market/market_repository_test.dart
|
||||
00:00 +0: loading /config/workspace/alt/apps/client/test/features/market/market_repository_test.dart
|
||||
00:00 +0: MarketRepository tests listInstruments success flows parsed results
|
||||
00:00 +1: MarketRepository tests listBars success flows historical data
|
||||
00:00 +2: MarketRepository tests typed ErrorInfo maps correctly to AltApiFailure
|
||||
00:00 +3: MarketRepository tests timeout maps correctly to AltTransportKind.timeout
|
||||
00:00 +4: MarketRepository tests type mismatch maps correctly to AltTransportKind.decodeFailure
|
||||
00:00 +5: MarketRepository tests client not alive returns AltTransportKind.disconnected
|
||||
00:00 +6: All tests passed!
|
||||
```
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
# Complete - m-operator-client-api-core-integration/03+01_market_repository
|
||||
|
||||
## 완료 일시
|
||||
|
||||
2026-06-02
|
||||
|
||||
## 요약
|
||||
|
||||
Market repository calls completed in 1 review loop with final verdict PASS.
|
||||
|
||||
## 루프 이력
|
||||
|
||||
| Plan | Review | Verdict | 메모 |
|
||||
|------|--------|---------|------|
|
||||
| `plan_cloud_G06_0.log` | `code_review_cloud_G06_0.log` | PASS | `MarketRepository` wraps market socket calls in `AltApiResult`, preserves typed `ErrorInfo`, and verifies request payload/result mapping. |
|
||||
|
||||
## 구현/정리 내용
|
||||
|
||||
- Added `apps/client/lib/src/features/market/data/market_repository.dart` with `listInstruments` and `listBars` repository methods returning `AltApiResult`.
|
||||
- Added `apps/client/test/features/market/market_repository_test.dart` covering success payload mapping, request payload assertions, typed API errors, timeout, decode failure, and disconnected client behavior.
|
||||
- Confirmed predecessor `01_contract_mapping` PASS completion evidence before accepting the shared `AltApiResult` dependency.
|
||||
|
||||
## 최종 검증
|
||||
|
||||
- `cd apps/client && flutter test test/features/market/market_repository_test.dart` - PASS; 6 tests passed.
|
||||
- `cd apps/client && flutter test` - PASS; 52 tests passed.
|
||||
- `cd apps/client && dart format --set-exit-if-changed lib/src/features/market/data/market_repository.dart test/features/market/market_repository_test.dart` - FORMAT APPLIED; `market_repository_test.dart` was formatted.
|
||||
- `cd apps/client && flutter analyze lib/src/features/market/data/market_repository.dart test/features/market/market_repository_test.dart` - PASS; No issues found.
|
||||
- `cd apps/client && flutter test test/features/market/market_repository_test.dart` - PASS after formatting; 6 tests passed.
|
||||
|
||||
## Roadmap Completion
|
||||
|
||||
- Milestone: `agent-roadmap/phase/operator-surface/milestones/operator-client-api-core-integration.md`
|
||||
- Completed task ids:
|
||||
- `market-call`: PASS; evidence=`plan_cloud_G06_0.log`, `code_review_cloud_G06_0.log`; verification=`cd apps/client && flutter test test/features/market/market_repository_test.dart`, `cd apps/client && flutter test`, `cd apps/client && flutter analyze lib/src/features/market/data/market_repository.dart test/features/market/market_repository_test.dart`
|
||||
- Not completed task ids: 없음
|
||||
|
||||
## 잔여 Nit
|
||||
|
||||
- 없음
|
||||
|
||||
## 후속 작업
|
||||
|
||||
- 없음
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
<!-- task=m-operator-client-api-core-integration/03+01_market_repository plan=0 tag=API -->
|
||||
|
||||
# Code Review Reference - API
|
||||
|
||||
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> Fill implementation-owned sections, then stop with active files in place and report ready for review. Finalization is review-agent-only.
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-06-02
|
||||
task=m-operator-client-api-core-integration/03+01_market_repository, plan=0, tag=API
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/operator-surface/milestones/operator-client-api-core-integration.md`
|
||||
- Task ids:
|
||||
- `market-call`: market data status/list 기본 API 호출을 client core에서 수행할 수 있다.
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 종결 절차는 리뷰 에이전트 전용이다.
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [API-1] Market repository 추가 | [ ] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] market repository/provider boundary를 추가하고 `listInstruments`, `listBars`를 `AltApiResult`로 반환한다. 검증: `cd apps/client && flutter test test/features/market/market_repository_test.dart`
|
||||
- [ ] fake client로 request payload와 parsed result/status key를 확인한다. 검증: `cd apps/client && flutter test test/features/market/market_repository_test.dart`
|
||||
- [ ] typed `ErrorInfo`가 `AltApiError`로 보존되는 test를 추가한다.
|
||||
- [ ] `cd apps/client && flutter test`를 실행한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
- [ ] PASS/WARN/FAIL 판정을 append한다.
|
||||
- [ ] active files를 log로 archive한다.
|
||||
- [ ] PASS이면 `complete.log` 작성 후 archive 이동한다.
|
||||
- [ ] roadmap은 직접 수정하지 않는다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- predecessor `01_contract_mapping` completion을 확인했는가
|
||||
- repository가 raw protobuf error를 success처럼 반환하지 않는가
|
||||
- request payload와 parsed result key가 test에 고정됐는가
|
||||
|
||||
## 검증 결과
|
||||
|
||||
### API-1 중간 검증
|
||||
```text
|
||||
$ cd apps/client && flutter test test/features/market/market_repository_test.dart
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```text
|
||||
$ cd apps/client && flutter test
|
||||
(output)
|
||||
```
|
||||
104
apps/client/lib/src/features/market/data/market_repository.dart
Normal file
104
apps/client/lib/src/features/market/data/market_repository.dart
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import 'dart:async';
|
||||
import 'package:alt_client/src/contracts/alt_api_result.dart';
|
||||
import 'package:alt_client/src/integrations/socket/alt_socket_client.dart';
|
||||
import 'package:alt_client/src/generated/alt/v1/market.pb.dart';
|
||||
|
||||
/// Repository responsible for handling market-related data operations.
|
||||
///
|
||||
/// Wraps [AltSocketClient] calls for listing instruments and historical bars,
|
||||
/// transforming raw socket responses or failures into structured [AltApiResult]s.
|
||||
class MarketRepository {
|
||||
final AltSocketClient _client;
|
||||
|
||||
MarketRepository(this._client);
|
||||
|
||||
/// Lists market instruments matching the given [request].
|
||||
Future<AltApiResult<ListInstrumentsResponse>> listInstruments(
|
||||
ListInstrumentsRequest request, {
|
||||
Duration timeout = const Duration(seconds: 2),
|
||||
}) async {
|
||||
if (!_client.isAlive) {
|
||||
return const AltApiResult.transportFailure(
|
||||
AltTransportKind.disconnected,
|
||||
detail: 'client is not alive',
|
||||
);
|
||||
}
|
||||
try {
|
||||
final response = await _client.listInstruments(request, timeout: timeout);
|
||||
final errorInfo = altResponseError(response);
|
||||
if (errorInfo != null) {
|
||||
return AltApiResult.apiError(AltApiError.fromErrorInfo(errorInfo));
|
||||
}
|
||||
return AltApiResult.success(response);
|
||||
} on TimeoutException catch (e) {
|
||||
return AltApiResult.transportFailure(
|
||||
AltTransportKind.timeout,
|
||||
detail: e.toString(),
|
||||
);
|
||||
} on StateError catch (e) {
|
||||
return AltApiResult.transportFailure(
|
||||
AltTransportKind.decodeFailure,
|
||||
detail: e.toString(),
|
||||
);
|
||||
} catch (e) {
|
||||
final errStr = e.toString().toLowerCase();
|
||||
if (errStr.contains('disconnect') ||
|
||||
errStr.contains('closed') ||
|
||||
errStr.contains('not connected')) {
|
||||
return AltApiResult.transportFailure(
|
||||
AltTransportKind.disconnected,
|
||||
detail: e.toString(),
|
||||
);
|
||||
}
|
||||
return AltApiResult.transportFailure(
|
||||
AltTransportKind.unknown,
|
||||
detail: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Lists bar (historical) data matching the given [request].
|
||||
Future<AltApiResult<ListBarsResponse>> listBars(
|
||||
ListBarsRequest request, {
|
||||
Duration timeout = const Duration(seconds: 2),
|
||||
}) async {
|
||||
if (!_client.isAlive) {
|
||||
return const AltApiResult.transportFailure(
|
||||
AltTransportKind.disconnected,
|
||||
detail: 'client is not alive',
|
||||
);
|
||||
}
|
||||
try {
|
||||
final response = await _client.listBars(request, timeout: timeout);
|
||||
final errorInfo = altResponseError(response);
|
||||
if (errorInfo != null) {
|
||||
return AltApiResult.apiError(AltApiError.fromErrorInfo(errorInfo));
|
||||
}
|
||||
return AltApiResult.success(response);
|
||||
} on TimeoutException catch (e) {
|
||||
return AltApiResult.transportFailure(
|
||||
AltTransportKind.timeout,
|
||||
detail: e.toString(),
|
||||
);
|
||||
} on StateError catch (e) {
|
||||
return AltApiResult.transportFailure(
|
||||
AltTransportKind.decodeFailure,
|
||||
detail: e.toString(),
|
||||
);
|
||||
} catch (e) {
|
||||
final errStr = e.toString().toLowerCase();
|
||||
if (errStr.contains('disconnect') ||
|
||||
errStr.contains('closed') ||
|
||||
errStr.contains('not connected')) {
|
||||
return AltApiResult.transportFailure(
|
||||
AltTransportKind.disconnected,
|
||||
detail: e.toString(),
|
||||
);
|
||||
}
|
||||
return AltApiResult.transportFailure(
|
||||
AltTransportKind.unknown,
|
||||
detail: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
275
apps/client/test/features/market/market_repository_test.dart
Normal file
275
apps/client/test/features/market/market_repository_test.dart
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:proto_socket/proto_socket.dart';
|
||||
|
||||
import 'package:alt_client/src/contracts/alt_api_result.dart';
|
||||
import 'package:alt_client/src/generated/alt/v1/common.pb.dart';
|
||||
import 'package:alt_client/src/generated/alt/v1/market.pb.dart';
|
||||
import 'package:alt_client/src/integrations/socket/alt_socket_client.dart';
|
||||
import 'package:alt_client/src/features/market/data/market_repository.dart';
|
||||
|
||||
class FakeWebSocket extends Stream<dynamic> implements WebSocket {
|
||||
final StreamController<dynamic> _controller = StreamController<dynamic>();
|
||||
final List<List<int>> sentBytes = [];
|
||||
bool isClosed = false;
|
||||
|
||||
@override
|
||||
void add(dynamic data) {
|
||||
if (data is List<int>) {
|
||||
sentBytes.add(data);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close([int? code, String? reason]) async {
|
||||
isClosed = true;
|
||||
await _controller.close();
|
||||
}
|
||||
|
||||
void feedFromServer(List<int> data) {
|
||||
_controller.add(data);
|
||||
}
|
||||
|
||||
@override
|
||||
StreamSubscription<dynamic> listen(
|
||||
void Function(dynamic event)? onData, {
|
||||
Function? onError,
|
||||
void Function()? onDone,
|
||||
bool? cancelOnError,
|
||||
}) {
|
||||
return _controller.stream.listen(
|
||||
onData,
|
||||
onError: onError,
|
||||
onDone: onDone,
|
||||
cancelOnError: cancelOnError,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('MarketRepository tests', () {
|
||||
late FakeWebSocket fakeWs;
|
||||
late AltSocketClient client;
|
||||
late MarketRepository repository;
|
||||
|
||||
setUp(() {
|
||||
fakeWs = FakeWebSocket();
|
||||
client = AltSocketClient(
|
||||
fakeWs,
|
||||
heartbeatIntervalTime: 10,
|
||||
heartbeatWaitTime: 10,
|
||||
);
|
||||
repository = MarketRepository(client);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await client.close();
|
||||
});
|
||||
|
||||
Future<PacketBase> roundTrip({
|
||||
required Future<void> Function() trigger,
|
||||
required void Function(PacketBase sentPacket) onSent,
|
||||
required String responseTypeName,
|
||||
required List<int> responseData,
|
||||
}) async {
|
||||
final sentCountBefore = fakeWs.sentBytes.length;
|
||||
unawaited(trigger());
|
||||
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(fakeWs.sentBytes.length, greaterThan(sentCountBefore));
|
||||
final sentPacket = PacketBase.fromBuffer(fakeWs.sentBytes.last);
|
||||
onSent(sentPacket);
|
||||
|
||||
final responsePacket = PacketBase()
|
||||
..typeName = responseTypeName
|
||||
..nonce = 200
|
||||
..responseNonce = sentPacket.nonce
|
||||
..data = responseData;
|
||||
fakeWs.feedFromServer(responsePacket.writeToBuffer());
|
||||
|
||||
return sentPacket;
|
||||
}
|
||||
|
||||
test('listInstruments success flows parsed results', () async {
|
||||
final request = ListInstrumentsRequest()
|
||||
..market = Market.MARKET_US
|
||||
..provider = 'kis';
|
||||
|
||||
final expectedResponse = ListInstrumentsResponse()
|
||||
..instruments.add(
|
||||
Instrument()
|
||||
..id = 'instrument-1'
|
||||
..market = Market.MARKET_US
|
||||
..symbol = 'AAPL',
|
||||
);
|
||||
|
||||
final future = repository.listInstruments(
|
||||
request,
|
||||
timeout: const Duration(seconds: 1),
|
||||
);
|
||||
|
||||
await roundTrip(
|
||||
trigger: () => future,
|
||||
onSent: (sentPacket) {
|
||||
expect(
|
||||
sentPacket.typeName,
|
||||
equals(
|
||||
ListInstrumentsRequest.getDefault().info_.qualifiedMessageName,
|
||||
),
|
||||
);
|
||||
final sentRequest = ListInstrumentsRequest.fromBuffer(
|
||||
sentPacket.data,
|
||||
);
|
||||
expect(sentRequest.market, equals(Market.MARKET_US));
|
||||
expect(sentRequest.provider, equals('kis'));
|
||||
},
|
||||
responseTypeName:
|
||||
ListInstrumentsResponse.getDefault().info_.qualifiedMessageName,
|
||||
responseData: expectedResponse.writeToBuffer(),
|
||||
);
|
||||
|
||||
final result = await future;
|
||||
expect(result, isA<AltApiSuccess<ListInstrumentsResponse>>());
|
||||
final successVal =
|
||||
(result as AltApiSuccess<ListInstrumentsResponse>).value;
|
||||
expect(successVal.instruments, hasLength(1));
|
||||
expect(successVal.instruments.first.id, equals('instrument-1'));
|
||||
expect(successVal.instruments.first.symbol, equals('AAPL'));
|
||||
});
|
||||
|
||||
test('listBars success flows historical data', () async {
|
||||
final request = ListBarsRequest()
|
||||
..instrumentId = 'instrument-2'
|
||||
..timeframe = Timeframe.TIMEFRAME_DAILY
|
||||
..fromUnixMs = Int64(1000)
|
||||
..toUnixMs = Int64(2000);
|
||||
|
||||
final expectedResponse = ListBarsResponse()
|
||||
..bars.add(
|
||||
Bar()
|
||||
..instrumentId = 'instrument-2'
|
||||
..timeframe = Timeframe.TIMEFRAME_DAILY
|
||||
..timestampUnixMs = Int64(1500)
|
||||
..close = Price(amount: Decimal(value: '150.0')),
|
||||
);
|
||||
|
||||
final future = repository.listBars(
|
||||
request,
|
||||
timeout: const Duration(seconds: 1),
|
||||
);
|
||||
|
||||
await roundTrip(
|
||||
trigger: () => future,
|
||||
onSent: (sentPacket) {
|
||||
expect(
|
||||
sentPacket.typeName,
|
||||
equals(ListBarsRequest.getDefault().info_.qualifiedMessageName),
|
||||
);
|
||||
final sentRequest = ListBarsRequest.fromBuffer(sentPacket.data);
|
||||
expect(sentRequest.instrumentId, equals('instrument-2'));
|
||||
expect(sentRequest.timeframe, equals(Timeframe.TIMEFRAME_DAILY));
|
||||
expect(sentRequest.fromUnixMs, equals(Int64(1000)));
|
||||
expect(sentRequest.toUnixMs, equals(Int64(2000)));
|
||||
},
|
||||
responseTypeName:
|
||||
ListBarsResponse.getDefault().info_.qualifiedMessageName,
|
||||
responseData: expectedResponse.writeToBuffer(),
|
||||
);
|
||||
|
||||
final result = await future;
|
||||
expect(result, isA<AltApiSuccess<ListBarsResponse>>());
|
||||
final successVal = (result as AltApiSuccess<ListBarsResponse>).value;
|
||||
expect(successVal.bars, hasLength(1));
|
||||
expect(successVal.bars.first.instrumentId, equals('instrument-2'));
|
||||
expect(successVal.bars.first.close.amount.value, equals('150.0'));
|
||||
});
|
||||
|
||||
test('typed ErrorInfo maps correctly to AltApiFailure', () async {
|
||||
final request = ListInstrumentsRequest()
|
||||
..market = Market.MARKET_KR
|
||||
..provider = 'kis';
|
||||
|
||||
final expectedResponse = ListInstrumentsResponse()
|
||||
..error = ErrorInfo(
|
||||
code: 'PROVIDER_LIMIT_EXCEEDED',
|
||||
message: 'kis daily quota reached',
|
||||
);
|
||||
|
||||
final future = repository.listInstruments(
|
||||
request,
|
||||
timeout: const Duration(seconds: 1),
|
||||
);
|
||||
|
||||
await roundTrip(
|
||||
trigger: () => future,
|
||||
onSent: (sentPacket) {},
|
||||
responseTypeName:
|
||||
ListInstrumentsResponse.getDefault().info_.qualifiedMessageName,
|
||||
responseData: expectedResponse.writeToBuffer(),
|
||||
);
|
||||
|
||||
final result = await future;
|
||||
expect(result, isA<AltApiFailure<ListInstrumentsResponse>>());
|
||||
final failure = result as AltApiFailure<ListInstrumentsResponse>;
|
||||
expect(failure.error.code, equals('PROVIDER_LIMIT_EXCEEDED'));
|
||||
expect(failure.error.message, equals('kis daily quota reached'));
|
||||
});
|
||||
|
||||
test('timeout maps correctly to AltTransportKind.timeout', () async {
|
||||
final request = ListInstrumentsRequest();
|
||||
|
||||
// We do not feed any response packet so it will time out.
|
||||
final result = await repository.listInstruments(
|
||||
request,
|
||||
timeout: const Duration(milliseconds: 10),
|
||||
);
|
||||
|
||||
expect(result, isA<AltTransportFailure<ListInstrumentsResponse>>());
|
||||
final failure = result as AltTransportFailure<ListInstrumentsResponse>;
|
||||
expect(failure.kind, equals(AltTransportKind.timeout));
|
||||
});
|
||||
|
||||
test(
|
||||
'type mismatch maps correctly to AltTransportKind.decodeFailure',
|
||||
() async {
|
||||
final request = ListInstrumentsRequest();
|
||||
|
||||
final future = repository.listInstruments(
|
||||
request,
|
||||
timeout: const Duration(seconds: 1),
|
||||
);
|
||||
|
||||
// Feed a response with a type mismatch (e.g. HelloResponse instead of ListInstrumentsResponse)
|
||||
await roundTrip(
|
||||
trigger: () => future,
|
||||
onSent: (sentPacket) {},
|
||||
responseTypeName:
|
||||
HelloResponse.getDefault().info_.qualifiedMessageName,
|
||||
responseData: HelloResponse().writeToBuffer(),
|
||||
);
|
||||
|
||||
final result = await future;
|
||||
expect(result, isA<AltTransportFailure<ListInstrumentsResponse>>());
|
||||
final failure = result as AltTransportFailure<ListInstrumentsResponse>;
|
||||
expect(failure.kind, equals(AltTransportKind.decodeFailure));
|
||||
expect(failure.detail, contains('Bad state'));
|
||||
},
|
||||
);
|
||||
|
||||
test('client not alive returns AltTransportKind.disconnected', () async {
|
||||
await client.close();
|
||||
|
||||
final result = await repository.listInstruments(ListInstrumentsRequest());
|
||||
expect(result, isA<AltTransportFailure<ListInstrumentsResponse>>());
|
||||
final failure = result as AltTransportFailure<ListInstrumentsResponse>;
|
||||
expect(failure.kind, equals(AltTransportKind.disconnected));
|
||||
expect(failure.detail, contains('client is not alive'));
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue