chore: archive G06 market repository task files and add client market feature

This commit is contained in:
toki 2026-06-02 10:41:41 +09:00
parent 48ee5189a9
commit b5a544aa97
6 changed files with 654 additions and 81 deletions

View file

@ -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!
```

View file

@ -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
- 없음
## 후속 작업
- 없음

View file

@ -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)
```

View 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(),
);
}
}
}

View 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'));
});
});
}