update: crosstest coverage and proto restructure changes
This commit is contained in:
parent
15d11f9ac5
commit
01de3f32bf
44 changed files with 3406 additions and 54 deletions
|
|
@ -24,7 +24,7 @@
|
|||
|
||||
1. 새 언어 디렉터리를 만들기 전에 `agent-ops/skills/project/add-toki-socket-crosstest-language/templates/`의 문서를 복사해 패키지 README와 체크리스트 초안으로 사용한다.
|
||||
2. `PROTOCOL.md`와 `VERSIONING.md`의 현재 protocol version을 기준으로 지원 범위를 정한다.
|
||||
3. canonical proto인 `dart/lib/src/packets/message_common.proto`에서 언어별 protobuf binding을 생성한다. Go처럼 generator 전용 option이 필요하면 언어 패키지 안에 copy를 둘 수 있지만, message schema는 `tools/check_proto_sync.sh`로 검증 가능해야 한다.
|
||||
3. canonical proto인 `proto/message_common.proto`에서 언어별 protobuf binding을 생성한다. Go처럼 generator 전용 option이 필요하면 언어 패키지 안에 copy를 둘 수 있지만, message schema는 `tools/check_proto_sync.sh`로 검증 가능해야 한다.
|
||||
4. `Transport`와 `Communicator`를 먼저 구현하고, 그 위에 TCP/TLS+TCP/WS/WSS client/server를 올린다.
|
||||
5. 같은 언어 단위 테스트를 작성한 뒤 `agent-ops/skills/project/add-toki-socket-crosstest-language/SKILL.md`의 runner 배치 규칙에 맞춰 양방향 crosstest를 추가한다.
|
||||
6. README의 Implementations 표를 `Available`로 바꾸기 전에 formatter/linter, 단위 테스트, cross-language tests를 모두 통과시킨다.
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ Sending `HeartBeat {}`:
|
|||
|
||||
## Proto Source
|
||||
|
||||
`dart/lib/src/packets/message_common.proto` is the canonical packet definition.
|
||||
All language implementations must keep the same message schema and generate bindings from it.
|
||||
`proto/message_common.proto` is the canonical packet definition.
|
||||
All language implementations generate bindings from it.
|
||||
|
||||
The Go implementation keeps a copy at `go/packets/message_common.proto` because Go generation needs `option go_package`. The Kotlin implementation keeps a copy at `kotlin/src/main/proto/message_common.proto` for Java/Kotlin generation options. Keep the message fields in sync with the canonical Dart proto before regenerating language bindings.
|
||||
Language-specific generation options such as `go_package` and Java/Kotlin package options are kept in each language's own proto copy under `go/packets/` and `kotlin/src/main/proto/`. Keep those copies' message schemas in sync with `proto/message_common.proto` before regenerating language bindings.
|
||||
|
|
|
|||
|
|
@ -84,14 +84,14 @@ await client.send(MyMessage()..text = 'hello');
|
|||
|
||||
## Adding Message Types
|
||||
|
||||
Edit the canonical proto at `dart/lib/src/packets/message_common.proto`, then regenerate all checked-in bindings:
|
||||
Edit the canonical proto at `proto/message_common.proto`, then regenerate all checked-in bindings:
|
||||
|
||||
```bash
|
||||
tools/generate_proto.sh
|
||||
tools/check_proto_sync.sh
|
||||
```
|
||||
|
||||
The Go and Kotlin proto copies are allowed to keep only language-specific options such as `option go_package` or Java package/class options. `tools/check_proto_sync.sh` fails with a diff when the message schema drifts.
|
||||
The Go and Kotlin proto copies are allowed to keep only language-specific options such as `option go_package` or Java package/class options. `tools/check_proto_sync.sh` fails with a diff when their message schema drifts from `proto/message_common.proto`.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ Toki Socket의 Dart/Flutter 구현체를 담당한다. ProtobufClient, ProtobufS
|
|||
|
||||
## 제외 경로
|
||||
|
||||
- `dart/lib/src/packets/` — protocol 도메인
|
||||
- `dart/lib/src/packets/` — protocol 도메인의 Dart protobuf 생성 코드
|
||||
|
||||
## 주요 구성 요소
|
||||
|
||||
|
|
@ -34,4 +34,4 @@ Toki Socket의 Dart/Flutter 구현체를 담당한다. ProtobufClient, ProtobufS
|
|||
## 금지 사항
|
||||
|
||||
- Dart 구현체에 Go/Kotlin 특화 로직을 추가하지 않는다
|
||||
- proto 원본 파일 변경 시 proto 도메인 규칙을 먼저 따른다
|
||||
- proto schema 변경 시 proto 도메인 규칙을 먼저 따른다
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
|
||||
## 포함 경로
|
||||
|
||||
- `dart/lib/src/packets/` — proto 원본 및 Dart 생성 코드 (정식 원본)
|
||||
- `proto/` — 언어 옵션 없는 proto 정식 원본
|
||||
- `dart/lib/src/packets/` — Dart protobuf 생성 코드
|
||||
- `go/packets/` — Go용 proto 복사본 (go_package 옵션만 추가)
|
||||
- `kotlin/src/**/packets/` — Kotlin용 proto 복사본 (Java 패키지 옵션만 추가)
|
||||
- `PROTOCOL.md` — 공식 와이어 포맷 명세
|
||||
|
|
@ -19,12 +20,12 @@
|
|||
|
||||
## 주요 구성 요소
|
||||
|
||||
- `message_common.proto` — PacketBase, 메시지 타입 정의 (Dart 원본)
|
||||
- `proto/message_common.proto` — PacketBase, 메시지 타입 정식 원본
|
||||
- `PacketBase` — 모든 패킷의 공통 래퍼 (type_name, payload, correlation_id)
|
||||
|
||||
## 유지할 패턴
|
||||
|
||||
- proto 원본은 `dart/lib/src/packets/`에서만 편집
|
||||
- proto 원본은 `proto/message_common.proto`에서만 편집
|
||||
- Go/Kotlin proto는 언어별 옵션만 추가. 메시지 스키마는 건드리지 않는다
|
||||
- 변경 후 반드시 `tools/generate_proto.sh` + `tools/check_proto_sync.sh` 실행
|
||||
|
||||
|
|
@ -35,5 +36,5 @@
|
|||
|
||||
## 금지 사항
|
||||
|
||||
- proto 파일을 Go/Kotlin 복사본에서 직접 편집하지 않는다 (메시지 스키마 변경은 Dart 원본에서만)
|
||||
- proto 파일을 Go/Kotlin 복사본에서 직접 편집하지 않는다 (메시지 스키마 변경은 `proto/message_common.proto`에서만)
|
||||
- PROTOCOL.md에 정의되지 않은 프레이밍 방식을 구현체에서 임의로 추가하지 않는다
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ VERSIONING.md — 프로토콜/패키지 버전 정책
|
|||
|
||||
## 프로젝트 특화 컨벤션
|
||||
|
||||
- proto 정의는 `dart/lib/src/packets/message_common.proto`가 정식 원본이다. Go/Kotlin은 언어별 옵션(go_package, java 패키지)만 추가 허용
|
||||
- proto 정의는 `proto/message_common.proto`가 정식 원본이다. Go/Kotlin은 언어별 옵션(go_package, java 패키지)만 추가 허용
|
||||
- proto 변경 시 반드시 `tools/generate_proto.sh` 실행 후 `tools/check_proto_sync.sh`로 동기화 확인
|
||||
- 새 언어 구현은 PORTING_GUIDE.md에 따라 동일 언어 테스트 + 크로스 언어 테스트 통과 후 Available 표시
|
||||
- 프로토콜 버전과 패키지 버전은 분리 관리 (VERSIONING.md 참조)
|
||||
|
|
@ -49,6 +49,7 @@ VERSIONING.md — 프로토콜/패키지 버전 정책
|
|||
|
||||
| 경로 패턴 | 도메인 | rules.md |
|
||||
|----------|--------|----------|
|
||||
| `proto/**` | protocol | `agent-ops/rules/project/domain/protocol/rules.md` |
|
||||
| `dart/lib/src/packets/**` | protocol | `agent-ops/rules/project/domain/protocol/rules.md` |
|
||||
| `go/packets/**` | protocol | `agent-ops/rules/project/domain/protocol/rules.md` |
|
||||
| `kotlin/src/**/packets/**` | protocol | `agent-ops/rules/project/domain/protocol/rules.md` |
|
||||
|
|
@ -62,7 +63,7 @@ VERSIONING.md — 프로토콜/패키지 버전 정책
|
|||
|
||||
## 기타 경로
|
||||
|
||||
- `agent-task/<task-name>/` — 에이전트 작업 로그 (plan, code_review 등). 소스 코드 아님. 수정하지 않는다.
|
||||
- `agent-task/<task-name>/` — 에이전트 작업 로그 (plan, code_review 등). 소스 코드 아님. PLAN.md 또는 CODE_REVIEW.md 파일을 읽고 작업하는 작업이 아닌이상. 수정하지 않는다.
|
||||
|
||||
## 스킬 라우팅
|
||||
|
||||
|
|
|
|||
|
|
@ -11,12 +11,12 @@ Use this package README to document the concrete files for:
|
|||
- Transport abstraction: TCP, TLS+TCP, WS, and WSS implementations.
|
||||
- Communicator core: parser map, listener routing, request-response correlation, and serialized writes.
|
||||
- Client/server layer: connection lifecycle, close behavior, and heartbeat ownership.
|
||||
- Generated protobuf bindings from `dart/lib/src/packets/message_common.proto`.
|
||||
- Generated protobuf bindings from `proto/message_common.proto`.
|
||||
- Unit tests and cross-language tests.
|
||||
|
||||
## Proto Generation
|
||||
|
||||
Document the package-local proto generation command here. The generated schema must match the canonical Dart proto except for language-specific generator options.
|
||||
Document the package-local proto generation command here. The generated schema must match `proto/message_common.proto` except for language-specific generator options.
|
||||
|
||||
```bash
|
||||
# <command to generate protobuf bindings>
|
||||
|
|
|
|||
101
agent-task/api_consistency/CODE_REVIEW.md
Normal file
101
agent-task/api_consistency/CODE_REVIEW.md
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<!-- task=api_consistency plan=0 tag=IMPROVE -->
|
||||
|
||||
# Code Review Reference - IMPROVE
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-04-24
|
||||
task=api_consistency, plan=0, tag=IMPROVE
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW.md` → `code_review_0.log` (기존 code_review_*.log 수 = 0)
|
||||
2. `PLAN.md` → `plan_0.log` (기존 plan_*.log 수 = 0)
|
||||
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [IMPROVE-1] Dart `sendRequest` timeout 파라미터 추가 | [x] |
|
||||
| [IMPROVE-2] TypeScript write queue maxsize 제한 | [x] |
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
- TypeScript write loop는 첫 packet을 즉시 in-flight로 소비하므로, 테스트는 `1개 in-flight + 64개 queued` 상태에서 66번째 `queuePacket()`이 waiter에 들어가는 방식으로 검증했다.
|
||||
- Dart `_pendingRequests` 정리 확인은 public API를 늘리지 않기 위해 테스트에서만 reflection으로 확인했다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- Dart `sendRequest`는 기존 호출 호환성을 유지하도록 named parameter 기본값 `Duration(seconds: 30)`을 사용했다.
|
||||
- Dart timeout 발생 시 해당 nonce를 `_pendingRequests`에서 제거하고 `TimeoutException`을 전파한다.
|
||||
- TypeScript write queue 제한은 queued item 수 기준 64개로 적용하고, queue item이 소비될 때 waiter를 하나씩 깨운다.
|
||||
- TypeScript `shutdown()`은 대기 중인 sender를 모두 깨워 재개 시 `NotConnectedError`로 종료되도록 했다.
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `dart/lib/src/communicator.dart` — `sendRequest` 시그니처에 `{Duration timeout = const Duration(seconds: 30)}` 파라미터 있는지
|
||||
- `dart/lib/src/communicator.dart` — timeout 발생 시 `_pendingRequests.remove(requestNonce)` 호출 후 `TimeoutException` 전파하는지
|
||||
- `dart/test/communicator_test.dart` — `TimeoutException` 테스트 추가 및 통과 여부
|
||||
- `typescript/src/communicator.ts` — `MAX_WRITE_QUEUE_SIZE = 64` 상수 존재 여부
|
||||
- `typescript/src/communicator.ts` — `queuePacket()`에서 queue full 시 대기 로직 구현 여부
|
||||
- `typescript/src/communicator.ts` — `shutdown()`에서 waiters 플러시 여부
|
||||
- `typescript/src/communicator.ts` — `initialize()`에서 `writeQueueWaiters = []` 초기화 여부
|
||||
- `typescript/test/communicator.test.ts` — 큐 full 대기 테스트 추가 및 통과 여부
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
### IMPROVE-1 중간 검증
|
||||
```
|
||||
$ cd dart && dart test test/communicator_test.dart
|
||||
00:00 +0: Communicator protocol guards response typeName mismatch completes sendRequest with error
|
||||
00:00 +1: Communicator protocol guards close 후 sendRequest는 StateError로 완료된다
|
||||
00:00 +2: Communicator protocol guards sendRequest가 timeout 내 응답 없으면 TimeoutException을 던진다
|
||||
00:00 +3: Communicator protocol guards cannot register addRequestListener for a type already using addListener
|
||||
00:00 +4: Communicator protocol guards cannot register addListener for a type already using addRequestListener
|
||||
00:00 +5: Communicator protocol guards queuePacket uses injected transport
|
||||
00:00 +6: All tests passed!
|
||||
|
||||
$ cd dart && dart analyze
|
||||
No issues found!
|
||||
```
|
||||
|
||||
### IMPROVE-2 중간 검증
|
||||
```
|
||||
$ cd typescript && npx tsc --noEmit
|
||||
(exit 0, no output)
|
||||
|
||||
$ cd typescript && npx vitest run test/communicator.test.ts
|
||||
✓ test/communicator.test.ts (8 tests) 20ms
|
||||
|
||||
Test Files 1 passed (1)
|
||||
Tests 8 passed (8)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ cd dart && dart analyze
|
||||
No issues found!
|
||||
|
||||
$ cd dart && dart test
|
||||
00:15 +43: All tests passed!
|
||||
|
||||
$ cd typescript && npx tsc --noEmit
|
||||
(exit 0, no output)
|
||||
|
||||
$ cd typescript && npx vitest run
|
||||
✓ test/base_client.test.ts (7 tests) 9ms
|
||||
✓ test/communicator.test.ts (8 tests) 19ms
|
||||
✓ test/tcp.test.ts (2 tests) 13ms
|
||||
✓ test/ws.test.ts (2 tests) 21ms
|
||||
|
||||
Test Files 4 passed (4)
|
||||
Tests 19 passed (19)
|
||||
```
|
||||
225
agent-task/api_consistency/PLAN.md
Normal file
225
agent-task/api_consistency/PLAN.md
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
<!-- task=api_consistency plan=0 tag=IMPROVE -->
|
||||
|
||||
# API 일관성 개선
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
각 항목의 체크리스트를 완료하면서 `[ ]`를 `[x]`로 표시하라.
|
||||
중간 검증 명령은 해당 항목 구현 직후 실행하고, 출력을 `CODE_REVIEW.md`의 검증 결과 섹션에 붙여넣어라.
|
||||
최종 검증도 마찬가지로 실행 후 출력을 기록하라.
|
||||
계획과 다르게 구현한 부분은 이유와 함께 `CODE_REVIEW.md`의 "계획 대비 변경 사항"에 기록하라.
|
||||
|
||||
---
|
||||
|
||||
## 배경
|
||||
|
||||
프로젝트 전반 평가에서 발견된 두 가지 API 비일관성을 수정한다.
|
||||
|
||||
| 항목 | 문제 | 영향 |
|
||||
|------|------|------|
|
||||
| IMPROVE-1 | Dart `sendRequest`에 timeout 파라미터 없음 | 응답 없으면 무한 대기 |
|
||||
| IMPROVE-2 | TypeScript write queue 크기 제한 없음 | await 없이 대량 send 시 무한 증가 가능 |
|
||||
|
||||
Go/Kotlin/Python/TypeScript는 `sendRequest`에 timeout을 지원한다. Dart만 없다.
|
||||
Go/Kotlin/Python은 write queue를 64개로 제한한다. TypeScript와 Dart는 제한이 없다.
|
||||
Dart write queue는 Promise chaining 방식이라 구조상 추가가 어려우므로 이번 범위에서 제외한다.
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
IMPROVE-1과 IMPROVE-2는 독립적이다. 이 순서로 진행한다.
|
||||
|
||||
---
|
||||
|
||||
### [IMPROVE-1] Dart `sendRequest` timeout 파라미터 추가
|
||||
|
||||
**문제**
|
||||
|
||||
[dart/lib/src/communicator.dart](../../dart/lib/src/communicator.dart) — `sendRequest`에 timeout 파라미터가 없다.
|
||||
|
||||
```dart
|
||||
Future<Res> sendRequest<Req extends GeneratedMessage, Res extends GeneratedMessage>(
|
||||
Req data) async {
|
||||
```
|
||||
|
||||
다른 언어 비교:
|
||||
|
||||
| 언어 | timeout 기본값 |
|
||||
|------|--------------|
|
||||
| Go | `time.Duration`, 기본 30s |
|
||||
| Kotlin | `timeoutMs: Long = 30_000L` |
|
||||
| Python | `timeout: float = 30.0` |
|
||||
| TypeScript | `timeoutMs = 30000` |
|
||||
| **Dart** | **없음 — 무한 대기** |
|
||||
|
||||
**해결 방법**
|
||||
|
||||
`sendRequest`에 named 파라미터 `{Duration timeout = const Duration(seconds: 30)}`를 추가한다.
|
||||
응답 대기를 `completer.future.timeout(timeout, onTimeout: ...)` 으로 감싼다.
|
||||
timeout 발생 시 `_pendingRequests`에서 해당 nonce를 제거하고 `TimeoutException`을 전파한다.
|
||||
|
||||
Before:
|
||||
```dart
|
||||
Future<Res> sendRequest<Req extends GeneratedMessage, Res extends GeneratedMessage>(
|
||||
Req data) async {
|
||||
if (!isAlive) return Future.error(StateError('not connected'));
|
||||
final requestNonce = ++nonce;
|
||||
// ...
|
||||
return completer.future;
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
```dart
|
||||
Future<Res> sendRequest<Req extends GeneratedMessage, Res extends GeneratedMessage>(
|
||||
Req data, {Duration timeout = const Duration(seconds: 30)}) async {
|
||||
if (!isAlive) return Future.error(StateError('not connected'));
|
||||
final requestNonce = ++nonce;
|
||||
// ...
|
||||
return completer.future.timeout(timeout, onTimeout: () {
|
||||
_pendingRequests.remove(requestNonce);
|
||||
throw TimeoutException(
|
||||
'sendRequest timeout for nonce $requestNonce', timeout);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
`dart:async`의 `TimeoutException`을 사용한다 (`import 'dart:async'`는 이미 있음).
|
||||
|
||||
**수정 파일 및 체크리스트**
|
||||
|
||||
- [x] `dart/lib/src/communicator.dart` — `sendRequest`에 `{Duration timeout = const Duration(seconds: 30)}` named 파라미터 추가
|
||||
- [x] `dart/lib/src/communicator.dart` — `completer.future.timeout(timeout, onTimeout: ...)` 적용, pending 정리 포함
|
||||
- [x] `dart/test/communicator_test.dart` — `sendRequest가 timeout 내 응답 없으면 TimeoutException을 던진다` 테스트 추가
|
||||
|
||||
**테스트 작성**
|
||||
|
||||
- 파일: `dart/test/communicator_test.dart`
|
||||
- 테스트명: `sendRequest가 timeout 내 응답 없으면 TimeoutException을 던진다`
|
||||
- 시나리오: `FakeTransport`에 packet을 보내지만 응답을 보내지 않음, `Duration(milliseconds: 50)` timeout으로 호출, `throwsA(isA<TimeoutException>())` 단언, timeout 후 `_pendingRequests`가 비어있는지 확인
|
||||
|
||||
**중간 검증**
|
||||
|
||||
```bash
|
||||
cd dart && dart test test/communicator_test.dart
|
||||
# 새 timeout 테스트 포함 전체 PASS
|
||||
cd dart && dart analyze
|
||||
# No issues found
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### [IMPROVE-2] TypeScript write queue maxsize 제한
|
||||
|
||||
**문제**
|
||||
|
||||
[typescript/src/communicator.ts](../../typescript/src/communicator.ts) — write queue가 크기 제한 없는 `Array`다.
|
||||
|
||||
```typescript
|
||||
private writeQueue: QueueItem[] = [];
|
||||
```
|
||||
|
||||
Go/Kotlin/Python은 64개 제한 도달 시 큐에 빈 자리가 생길 때까지 발신을 블로킹한다.
|
||||
TypeScript는 `await`하지 않고 대량 `send()`를 발행하면 큐가 무한 증가한다.
|
||||
|
||||
**해결 방법**
|
||||
|
||||
`MAX_WRITE_QUEUE_SIZE = 64` 상수를 추가한다.
|
||||
큐가 가득 찼을 때 waiter Promise를 등록하고, 항목이 소비될 때마다 waiter를 깨운다 (Python의 `asyncio.Queue`와 동일한 시맨틱).
|
||||
`shutdown()` 시 모든 waiter를 깨워 `NotConnectedError`로 종료한다.
|
||||
|
||||
추가할 멤버:
|
||||
```typescript
|
||||
private static readonly MAX_WRITE_QUEUE_SIZE = 64;
|
||||
private writeQueueWaiters: Array<() => void> = [];
|
||||
```
|
||||
|
||||
`queuePacket` 변경:
|
||||
```typescript
|
||||
async queuePacket(base: PacketBase): Promise<void> {
|
||||
if (!this.isAliveFlag || this.transport === null) {
|
||||
throw new NotConnectedError();
|
||||
}
|
||||
// 큐가 가득 찬 경우 빈 자리가 생길 때까지 대기
|
||||
while (this.writeQueue.length >= Communicator.MAX_WRITE_QUEUE_SIZE) {
|
||||
await new Promise<void>((resolve) => {
|
||||
this.writeQueueWaiters.push(resolve);
|
||||
});
|
||||
if (!this.isAliveFlag) throw new NotConnectedError();
|
||||
}
|
||||
// 이하 기존 코드 동일
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
`runWriteQueue`에서 항목 소비 후 waiter 해제:
|
||||
```typescript
|
||||
const item = this.writeQueue.shift();
|
||||
if (this.writeQueueWaiters.length > 0) {
|
||||
this.writeQueueWaiters.shift()?.();
|
||||
}
|
||||
```
|
||||
|
||||
`shutdown()`에서 waiters 플러시:
|
||||
```typescript
|
||||
while (this.writeQueueWaiters.length > 0) {
|
||||
this.writeQueueWaiters.shift()?.();
|
||||
}
|
||||
```
|
||||
|
||||
`initialize()`에서 `writeQueueWaiters` 초기화 추가:
|
||||
```typescript
|
||||
this.writeQueueWaiters = [];
|
||||
```
|
||||
|
||||
**수정 파일 및 체크리스트**
|
||||
|
||||
- [x] `typescript/src/communicator.ts` — `MAX_WRITE_QUEUE_SIZE = 64` 상수 추가
|
||||
- [x] `typescript/src/communicator.ts` — `writeQueueWaiters` 멤버 추가 및 `initialize()` 초기화
|
||||
- [x] `typescript/src/communicator.ts` — `queuePacket()`에 full 시 대기 로직 추가
|
||||
- [x] `typescript/src/communicator.ts` — `runWriteQueue()`에서 waiter 해제
|
||||
- [x] `typescript/src/communicator.ts` — `shutdown()`에서 waiters 플러시
|
||||
- [x] `typescript/test/communicator.test.ts` — 큐 full 시 대기 후 처리 테스트 추가
|
||||
|
||||
**테스트 작성**
|
||||
|
||||
- 파일: `typescript/test/communicator.test.ts`
|
||||
- 테스트명: `write queue가 MAX_WRITE_QUEUE_SIZE 초과 시 대기 후 순서대로 처리된다`
|
||||
- 시나리오: FakeTransport에서 write를 인위적으로 지연, 65번째 `queuePacket` 호출이 블로킹되다가 64번째가 처리되면 진행됨을 확인
|
||||
|
||||
**중간 검증**
|
||||
|
||||
```bash
|
||||
cd typescript && npx tsc --noEmit
|
||||
# 타입 오류 없음
|
||||
cd typescript && npx vitest run test/communicator.test.ts
|
||||
# 새 큐 테스트 포함 PASS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `dart/lib/src/communicator.dart` | IMPROVE-1 |
|
||||
| `dart/test/communicator_test.dart` | IMPROVE-1 |
|
||||
| `typescript/src/communicator.ts` | IMPROVE-2 |
|
||||
| `typescript/test/communicator.test.ts` | IMPROVE-2 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
# Dart
|
||||
cd dart && dart analyze
|
||||
# No issues found
|
||||
|
||||
cd dart && dart test
|
||||
# 전체 PASS (신규 timeout 테스트 포함)
|
||||
|
||||
# TypeScript
|
||||
cd typescript && npx tsc --noEmit
|
||||
# 타입 오류 없음
|
||||
|
||||
cd typescript && npx vitest run
|
||||
# 전체 PASS (신규 큐 테스트 포함)
|
||||
```
|
||||
101
agent-task/api_consistency/code_review_0.log
Normal file
101
agent-task/api_consistency/code_review_0.log
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<!-- task=api_consistency plan=0 tag=IMPROVE -->
|
||||
|
||||
# Code Review Reference - IMPROVE
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-04-24
|
||||
task=api_consistency, plan=0, tag=IMPROVE
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW.md` → `code_review_0.log` (기존 code_review_*.log 수 = 0)
|
||||
2. `PLAN.md` → `plan_0.log` (기존 plan_*.log 수 = 0)
|
||||
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [IMPROVE-1] Dart `sendRequest` timeout 파라미터 추가 | [x] |
|
||||
| [IMPROVE-2] TypeScript write queue maxsize 제한 | [x] |
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
- TypeScript write loop는 첫 packet을 즉시 in-flight로 소비하므로, 테스트는 `1개 in-flight + 64개 queued` 상태에서 66번째 `queuePacket()`이 waiter에 들어가는 방식으로 검증했다.
|
||||
- Dart `_pendingRequests` 정리 확인은 public API를 늘리지 않기 위해 테스트에서만 reflection으로 확인했다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- Dart `sendRequest`는 기존 호출 호환성을 유지하도록 named parameter 기본값 `Duration(seconds: 30)`을 사용했다.
|
||||
- Dart timeout 발생 시 해당 nonce를 `_pendingRequests`에서 제거하고 `TimeoutException`을 전파한다.
|
||||
- TypeScript write queue 제한은 queued item 수 기준 64개로 적용하고, queue item이 소비될 때 waiter를 하나씩 깨운다.
|
||||
- TypeScript `shutdown()`은 대기 중인 sender를 모두 깨워 재개 시 `NotConnectedError`로 종료되도록 했다.
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `dart/lib/src/communicator.dart` — `sendRequest` 시그니처에 `{Duration timeout = const Duration(seconds: 30)}` 파라미터 있는지
|
||||
- `dart/lib/src/communicator.dart` — timeout 발생 시 `_pendingRequests.remove(requestNonce)` 호출 후 `TimeoutException` 전파하는지
|
||||
- `dart/test/communicator_test.dart` — `TimeoutException` 테스트 추가 및 통과 여부
|
||||
- `typescript/src/communicator.ts` — `MAX_WRITE_QUEUE_SIZE = 64` 상수 존재 여부
|
||||
- `typescript/src/communicator.ts` — `queuePacket()`에서 queue full 시 대기 로직 구현 여부
|
||||
- `typescript/src/communicator.ts` — `shutdown()`에서 waiters 플러시 여부
|
||||
- `typescript/src/communicator.ts` — `initialize()`에서 `writeQueueWaiters = []` 초기화 여부
|
||||
- `typescript/test/communicator.test.ts` — 큐 full 대기 테스트 추가 및 통과 여부
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
### IMPROVE-1 중간 검증
|
||||
```
|
||||
$ cd dart && dart test test/communicator_test.dart
|
||||
00:00 +0: Communicator protocol guards response typeName mismatch completes sendRequest with error
|
||||
00:00 +1: Communicator protocol guards close 후 sendRequest는 StateError로 완료된다
|
||||
00:00 +2: Communicator protocol guards sendRequest가 timeout 내 응답 없으면 TimeoutException을 던진다
|
||||
00:00 +3: Communicator protocol guards cannot register addRequestListener for a type already using addListener
|
||||
00:00 +4: Communicator protocol guards cannot register addListener for a type already using addRequestListener
|
||||
00:00 +5: Communicator protocol guards queuePacket uses injected transport
|
||||
00:00 +6: All tests passed!
|
||||
|
||||
$ cd dart && dart analyze
|
||||
No issues found!
|
||||
```
|
||||
|
||||
### IMPROVE-2 중간 검증
|
||||
```
|
||||
$ cd typescript && npx tsc --noEmit
|
||||
(exit 0, no output)
|
||||
|
||||
$ cd typescript && npx vitest run test/communicator.test.ts
|
||||
✓ test/communicator.test.ts (8 tests) 20ms
|
||||
|
||||
Test Files 1 passed (1)
|
||||
Tests 8 passed (8)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ cd dart && dart analyze
|
||||
No issues found!
|
||||
|
||||
$ cd dart && dart test
|
||||
00:15 +43: All tests passed!
|
||||
|
||||
$ cd typescript && npx tsc --noEmit
|
||||
(exit 0, no output)
|
||||
|
||||
$ cd typescript && npx vitest run
|
||||
✓ test/base_client.test.ts (7 tests) 9ms
|
||||
✓ test/communicator.test.ts (8 tests) 19ms
|
||||
✓ test/tcp.test.ts (2 tests) 13ms
|
||||
✓ test/ws.test.ts (2 tests) 21ms
|
||||
|
||||
Test Files 4 passed (4)
|
||||
Tests 19 passed (19)
|
||||
```
|
||||
15
agent-task/api_consistency/complete.log
Normal file
15
agent-task/api_consistency/complete.log
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
task=api_consistency
|
||||
date=2026-04-25
|
||||
result=PASS
|
||||
|
||||
## 완료된 작업 (plan=0)
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| IMPROVE-1 | Dart `sendRequest`에 `{Duration timeout = const Duration(seconds: 30)}` named 파라미터 추가. timeout 발생 시 pending 정리 + `TimeoutException` 전파. `test_communicator_test.dart`에 reflection 기반 timeout 테스트 추가 |
|
||||
| IMPROVE-2 | TypeScript `Communicator`에 `MAX_WRITE_QUEUE_SIZE = 64` 제한 추가. waiter 패턴으로 queue full 시 대기 후 처리. `shutdown()` 시 waiters 플러시. `communicator.test.ts`에 `BlockingTransport` 기반 큐 포화 테스트 추가 |
|
||||
|
||||
## 최종 상태
|
||||
|
||||
- Dart: dart analyze No issues / dart test 43개 PASS (+1 timeout 테스트)
|
||||
- TypeScript: tsc --noEmit clean / vitest 19개 PASS (+1 큐 포화 테스트)
|
||||
225
agent-task/api_consistency/plan_0.log
Normal file
225
agent-task/api_consistency/plan_0.log
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
<!-- task=api_consistency plan=0 tag=IMPROVE -->
|
||||
|
||||
# API 일관성 개선
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
각 항목의 체크리스트를 완료하면서 `[ ]`를 `[x]`로 표시하라.
|
||||
중간 검증 명령은 해당 항목 구현 직후 실행하고, 출력을 `CODE_REVIEW.md`의 검증 결과 섹션에 붙여넣어라.
|
||||
최종 검증도 마찬가지로 실행 후 출력을 기록하라.
|
||||
계획과 다르게 구현한 부분은 이유와 함께 `CODE_REVIEW.md`의 "계획 대비 변경 사항"에 기록하라.
|
||||
|
||||
---
|
||||
|
||||
## 배경
|
||||
|
||||
프로젝트 전반 평가에서 발견된 두 가지 API 비일관성을 수정한다.
|
||||
|
||||
| 항목 | 문제 | 영향 |
|
||||
|------|------|------|
|
||||
| IMPROVE-1 | Dart `sendRequest`에 timeout 파라미터 없음 | 응답 없으면 무한 대기 |
|
||||
| IMPROVE-2 | TypeScript write queue 크기 제한 없음 | await 없이 대량 send 시 무한 증가 가능 |
|
||||
|
||||
Go/Kotlin/Python/TypeScript는 `sendRequest`에 timeout을 지원한다. Dart만 없다.
|
||||
Go/Kotlin/Python은 write queue를 64개로 제한한다. TypeScript와 Dart는 제한이 없다.
|
||||
Dart write queue는 Promise chaining 방식이라 구조상 추가가 어려우므로 이번 범위에서 제외한다.
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
IMPROVE-1과 IMPROVE-2는 독립적이다. 이 순서로 진행한다.
|
||||
|
||||
---
|
||||
|
||||
### [IMPROVE-1] Dart `sendRequest` timeout 파라미터 추가
|
||||
|
||||
**문제**
|
||||
|
||||
[dart/lib/src/communicator.dart](../../dart/lib/src/communicator.dart) — `sendRequest`에 timeout 파라미터가 없다.
|
||||
|
||||
```dart
|
||||
Future<Res> sendRequest<Req extends GeneratedMessage, Res extends GeneratedMessage>(
|
||||
Req data) async {
|
||||
```
|
||||
|
||||
다른 언어 비교:
|
||||
|
||||
| 언어 | timeout 기본값 |
|
||||
|------|--------------|
|
||||
| Go | `time.Duration`, 기본 30s |
|
||||
| Kotlin | `timeoutMs: Long = 30_000L` |
|
||||
| Python | `timeout: float = 30.0` |
|
||||
| TypeScript | `timeoutMs = 30000` |
|
||||
| **Dart** | **없음 — 무한 대기** |
|
||||
|
||||
**해결 방법**
|
||||
|
||||
`sendRequest`에 named 파라미터 `{Duration timeout = const Duration(seconds: 30)}`를 추가한다.
|
||||
응답 대기를 `completer.future.timeout(timeout, onTimeout: ...)` 으로 감싼다.
|
||||
timeout 발생 시 `_pendingRequests`에서 해당 nonce를 제거하고 `TimeoutException`을 전파한다.
|
||||
|
||||
Before:
|
||||
```dart
|
||||
Future<Res> sendRequest<Req extends GeneratedMessage, Res extends GeneratedMessage>(
|
||||
Req data) async {
|
||||
if (!isAlive) return Future.error(StateError('not connected'));
|
||||
final requestNonce = ++nonce;
|
||||
// ...
|
||||
return completer.future;
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
```dart
|
||||
Future<Res> sendRequest<Req extends GeneratedMessage, Res extends GeneratedMessage>(
|
||||
Req data, {Duration timeout = const Duration(seconds: 30)}) async {
|
||||
if (!isAlive) return Future.error(StateError('not connected'));
|
||||
final requestNonce = ++nonce;
|
||||
// ...
|
||||
return completer.future.timeout(timeout, onTimeout: () {
|
||||
_pendingRequests.remove(requestNonce);
|
||||
throw TimeoutException(
|
||||
'sendRequest timeout for nonce $requestNonce', timeout);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
`dart:async`의 `TimeoutException`을 사용한다 (`import 'dart:async'`는 이미 있음).
|
||||
|
||||
**수정 파일 및 체크리스트**
|
||||
|
||||
- [x] `dart/lib/src/communicator.dart` — `sendRequest`에 `{Duration timeout = const Duration(seconds: 30)}` named 파라미터 추가
|
||||
- [x] `dart/lib/src/communicator.dart` — `completer.future.timeout(timeout, onTimeout: ...)` 적용, pending 정리 포함
|
||||
- [x] `dart/test/communicator_test.dart` — `sendRequest가 timeout 내 응답 없으면 TimeoutException을 던진다` 테스트 추가
|
||||
|
||||
**테스트 작성**
|
||||
|
||||
- 파일: `dart/test/communicator_test.dart`
|
||||
- 테스트명: `sendRequest가 timeout 내 응답 없으면 TimeoutException을 던진다`
|
||||
- 시나리오: `FakeTransport`에 packet을 보내지만 응답을 보내지 않음, `Duration(milliseconds: 50)` timeout으로 호출, `throwsA(isA<TimeoutException>())` 단언, timeout 후 `_pendingRequests`가 비어있는지 확인
|
||||
|
||||
**중간 검증**
|
||||
|
||||
```bash
|
||||
cd dart && dart test test/communicator_test.dart
|
||||
# 새 timeout 테스트 포함 전체 PASS
|
||||
cd dart && dart analyze
|
||||
# No issues found
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### [IMPROVE-2] TypeScript write queue maxsize 제한
|
||||
|
||||
**문제**
|
||||
|
||||
[typescript/src/communicator.ts](../../typescript/src/communicator.ts) — write queue가 크기 제한 없는 `Array`다.
|
||||
|
||||
```typescript
|
||||
private writeQueue: QueueItem[] = [];
|
||||
```
|
||||
|
||||
Go/Kotlin/Python은 64개 제한 도달 시 큐에 빈 자리가 생길 때까지 발신을 블로킹한다.
|
||||
TypeScript는 `await`하지 않고 대량 `send()`를 발행하면 큐가 무한 증가한다.
|
||||
|
||||
**해결 방법**
|
||||
|
||||
`MAX_WRITE_QUEUE_SIZE = 64` 상수를 추가한다.
|
||||
큐가 가득 찼을 때 waiter Promise를 등록하고, 항목이 소비될 때마다 waiter를 깨운다 (Python의 `asyncio.Queue`와 동일한 시맨틱).
|
||||
`shutdown()` 시 모든 waiter를 깨워 `NotConnectedError`로 종료한다.
|
||||
|
||||
추가할 멤버:
|
||||
```typescript
|
||||
private static readonly MAX_WRITE_QUEUE_SIZE = 64;
|
||||
private writeQueueWaiters: Array<() => void> = [];
|
||||
```
|
||||
|
||||
`queuePacket` 변경:
|
||||
```typescript
|
||||
async queuePacket(base: PacketBase): Promise<void> {
|
||||
if (!this.isAliveFlag || this.transport === null) {
|
||||
throw new NotConnectedError();
|
||||
}
|
||||
// 큐가 가득 찬 경우 빈 자리가 생길 때까지 대기
|
||||
while (this.writeQueue.length >= Communicator.MAX_WRITE_QUEUE_SIZE) {
|
||||
await new Promise<void>((resolve) => {
|
||||
this.writeQueueWaiters.push(resolve);
|
||||
});
|
||||
if (!this.isAliveFlag) throw new NotConnectedError();
|
||||
}
|
||||
// 이하 기존 코드 동일
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
`runWriteQueue`에서 항목 소비 후 waiter 해제:
|
||||
```typescript
|
||||
const item = this.writeQueue.shift();
|
||||
if (this.writeQueueWaiters.length > 0) {
|
||||
this.writeQueueWaiters.shift()?.();
|
||||
}
|
||||
```
|
||||
|
||||
`shutdown()`에서 waiters 플러시:
|
||||
```typescript
|
||||
while (this.writeQueueWaiters.length > 0) {
|
||||
this.writeQueueWaiters.shift()?.();
|
||||
}
|
||||
```
|
||||
|
||||
`initialize()`에서 `writeQueueWaiters` 초기화 추가:
|
||||
```typescript
|
||||
this.writeQueueWaiters = [];
|
||||
```
|
||||
|
||||
**수정 파일 및 체크리스트**
|
||||
|
||||
- [x] `typescript/src/communicator.ts` — `MAX_WRITE_QUEUE_SIZE = 64` 상수 추가
|
||||
- [x] `typescript/src/communicator.ts` — `writeQueueWaiters` 멤버 추가 및 `initialize()` 초기화
|
||||
- [x] `typescript/src/communicator.ts` — `queuePacket()`에 full 시 대기 로직 추가
|
||||
- [x] `typescript/src/communicator.ts` — `runWriteQueue()`에서 waiter 해제
|
||||
- [x] `typescript/src/communicator.ts` — `shutdown()`에서 waiters 플러시
|
||||
- [x] `typescript/test/communicator.test.ts` — 큐 full 시 대기 후 처리 테스트 추가
|
||||
|
||||
**테스트 작성**
|
||||
|
||||
- 파일: `typescript/test/communicator.test.ts`
|
||||
- 테스트명: `write queue가 MAX_WRITE_QUEUE_SIZE 초과 시 대기 후 순서대로 처리된다`
|
||||
- 시나리오: FakeTransport에서 write를 인위적으로 지연, 65번째 `queuePacket` 호출이 블로킹되다가 64번째가 처리되면 진행됨을 확인
|
||||
|
||||
**중간 검증**
|
||||
|
||||
```bash
|
||||
cd typescript && npx tsc --noEmit
|
||||
# 타입 오류 없음
|
||||
cd typescript && npx vitest run test/communicator.test.ts
|
||||
# 새 큐 테스트 포함 PASS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `dart/lib/src/communicator.dart` | IMPROVE-1 |
|
||||
| `dart/test/communicator_test.dart` | IMPROVE-1 |
|
||||
| `typescript/src/communicator.ts` | IMPROVE-2 |
|
||||
| `typescript/test/communicator.test.ts` | IMPROVE-2 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
# Dart
|
||||
cd dart && dart analyze
|
||||
# No issues found
|
||||
|
||||
cd dart && dart test
|
||||
# 전체 PASS (신규 timeout 테스트 포함)
|
||||
|
||||
# TypeScript
|
||||
cd typescript && npx tsc --noEmit
|
||||
# 타입 오류 없음
|
||||
|
||||
cd typescript && npx vitest run
|
||||
# 전체 PASS (신규 큐 테스트 포함)
|
||||
```
|
||||
187
agent-task/crosstest_coverage_gaps/code_review_0.log
Normal file
187
agent-task/crosstest_coverage_gaps/code_review_0.log
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
<!-- task=crosstest_coverage_gaps plan=0 tag=CROSSTEST-COVERAGE -->
|
||||
|
||||
# Code Review Reference - CROSSTEST-COVERAGE
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-04-24
|
||||
task=crosstest_coverage_gaps, plan=0, tag=CROSSTEST-COVERAGE
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, 검증 결과가 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW.md` -> `code_review_0.log`
|
||||
2. `PLAN.md` -> `plan_0.log`
|
||||
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|-----------|
|
||||
| [SELF-1] Self-cell coverage audit 표준화 | [x] |
|
||||
| [SELF-2] Go self-cell 동시 request-response 보강 | [x] |
|
||||
| [SELF-3] Kotlin self-cell 동시 request-response 보강 | [x] |
|
||||
| [SELF-4] Python self-cell TCP/WS scenario 보강 | [x] |
|
||||
| [SELF-5] TypeScript self-cell 동시 request-response 보강 | [x] |
|
||||
| [SECURE-1] Dart <-> Go secure crosstest 추가 | [x] |
|
||||
| [SECURE-2] Kotlin/Python/TypeScript secure matrix 보류 사유 문서화 | [x] |
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
- Python `test_tcp.py`에 `_wait_for_clients` 헬퍼 추가 (broadcast 전 클라이언트 연결 대기). WS 테스트에도 동일 패턴 적용.
|
||||
- `go/crosstest/go_dart.go`의 `runDartClient`를 variadic `certFile ...string` 형식으로 변경 — 기존 TCP/WS 호출은 영향 없음.
|
||||
- `go/crosstest/go_dart.go`에 `loadServerTLS` 헬퍼 추가.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- **공유 인증서**: `dart/test/certs/server.crt` / `.key` 재사용. SAN에 `IP:127.0.0.1`과 `DNS:localhost` 모두 포함되어 있어 Go/Dart 양쪽 검증 통과 확인.
|
||||
- **인증서 경로 전달**: Dart 오케스트레이터(dart_go.dart)와 Go 오케스트레이터(go_dart.go)가 각자 cert 경로를 계산하여 서브프로세스 클라이언트에 `--cert=<path>` 인수로 전달. machine-global trust store 미사용.
|
||||
- **Go 서버 TLS**: `toki.NewTcpServerTLS` / `toki.NewWsServerTLS`에 `tls.LoadX509KeyPair`로 로드한 config 전달.
|
||||
- **Dart 클라이언트 TLS**: `ProtobufClient.connectSecure` / `WsProtobufClient.connectSecure`에 `SecurityContext()..setTrustedCertificates(certPath)` 전달.
|
||||
- **고정 포트**: TLS+TCP 29094/29194, WSS 29096/29196 (기존 plain 포트 29090/29092/29190/29192와 충돌 없음).
|
||||
|
||||
## Self-cell 감사 결과
|
||||
|
||||
시나리오 정의: 1=클라이언트→서버 fire-and-forget, 2=서버→클라이언트 push, 3=단일 request-response, 4=다중 동시 request-response
|
||||
|
||||
| Language | TCP scenario 1/2 | TCP scenario 3 | TCP scenario 4 | WS scenario 1/2 | WS scenario 3 | WS scenario 4 | 조치 |
|
||||
|----------|------------------|----------------|----------------|-----------------|---------------|---------------|------|
|
||||
| Dart | ✓ (socket_test.dart) | ✓ | ✓ (Future.wait) | ✓ | ✓ | ✓ (Future.wait) | 소스 변경 없음 → UT-ok |
|
||||
| Go | ✓ (tcp_test.go) | ✓ | 추가됨 (TestTcpConcurrentRequests) | ✓ (ws_test.go) | ✓ | 추가됨 (TestWsConcurrentRequests) | SELF-2 적용 |
|
||||
| Kotlin | ✓ (TcpTest.kt) | ✓ | 추가됨 (testTcpConcurrentRequests) | ✓ (WsTest.kt) | ✓ | 추가됨 (testWsConcurrentRequests) | SELF-3 적용 |
|
||||
| Python | ✓ (test_tcp.py) | ✓ | 추가됨 (test_tcp_concurrent_requests) | 신규 파일 생성 (test_ws.py) | 추가됨 | 추가됨 | SELF-4 적용 |
|
||||
| TypeScript | ✓ (tcp.test.ts/ws.test.ts) | ✓ | 추가됨 (concurrent roundtrip) | ✓ | ✓ | 추가됨 (concurrent roundtrip) | SELF-5 적용 |
|
||||
|
||||
**Python test_ws.py 신규 생성 사유**: Python에는 WS 테스트 파일 자체가 없었음. `tcp_server.py`와 대칭으로 `ws_server.py`/`ws_client.py`가 구현 완료되어 있어 `test_ws.py`가 자연스러운 위치.
|
||||
|
||||
## Secure Matrix 결과
|
||||
|
||||
| Server \ Client | Dart | Go | Kotlin | Python | TypeScript |
|
||||
|-----------------|------|----|--------|--------|------------|
|
||||
| Dart | 기존 단위 테스트 (socket_test.dart) | CT-secure (dart_go.dart) | deferred | deferred | deferred |
|
||||
| Go | CT-secure (go_dart.go) | 기존 단위 테스트 (tls_test.go) | deferred | deferred | deferred |
|
||||
| Kotlin | deferred | deferred | deferred | deferred | deferred |
|
||||
| Python | deferred | deferred | deferred | deferred | deferred |
|
||||
| TypeScript | deferred | deferred | deferred | deferred | deferred |
|
||||
|
||||
### Kotlin/Python/TypeScript secure 보류 사유
|
||||
|
||||
| 언어 | TLS+TCP 서버 | TLS+TCP 클라이언트 | WSS 서버 | WSS 클라이언트 | 보류 사유 |
|
||||
|------|------------|-----------------|---------|--------------|---------|
|
||||
| Kotlin | 없음 | 없음 | 없음 | 있음 (`wss://` URL 지원) | TLS+TCP API 및 WSS 서버 API 미구현. cross-language secure matrix 적용 불가. |
|
||||
| Python | 없음 | 없음 | 없음 | 없음 | `tcp_server.py`/`tcp_client.py`/`ws_server.py`/`ws_client.py` 모두 SSL context 경로 없음. |
|
||||
| TypeScript | 없음 | 없음 | 없음 | 없음 | `net` 모듈 plain TCP, plain `ws://`만 사용. `tls`/`https` wrapper 미구현. |
|
||||
|
||||
구현 자체는 이 계획의 범위 밖. README 과잉 주장 없음 — 언어별 README 수정 불필요.
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- 기존 cross-language TCP/WS 20개 directed 조합을 삭제하거나 약화하지 않았는지 확인
|
||||
- self-cell 보강 테스트가 scenario 1-4의 의미와 맞는지 확인
|
||||
- Python WS 테스트가 Python server/client 둘 다 실제 `WsServer`/`connect_ws`를 사용하는지 확인
|
||||
- Go/Kotlin/TypeScript concurrent request 테스트가 응답 순서가 아니라 nonce/responseNonce 매핑 결과를 검증하는지 확인
|
||||
- Dart <-> Go secure crosstest가 machine-global trust store에 의존하지 않는지 확인
|
||||
- Kotlin/Python/TypeScript secure deferred 사유가 실제 API 지원 상태와 일치하는지 확인
|
||||
|
||||
## 검증 결과
|
||||
|
||||
### Self-cell 보강 검증
|
||||
|
||||
```
|
||||
$ cd dart && dart test
|
||||
|
||||
$ cd go && go test ./...
|
||||
|
||||
$ cd kotlin && ./gradlew test
|
||||
|
||||
$ cd python && python3 -m pytest test/ -v
|
||||
|
||||
$ cd typescript && npm test
|
||||
|
||||
$ cd typescript && npm run check
|
||||
```
|
||||
|
||||
### Dart/Go secure crosstest 검증
|
||||
|
||||
```
|
||||
$ cd dart && dart run crosstest/dart_go.dart
|
||||
|
||||
$ cd go && go run ./crosstest/go_dart.go
|
||||
```
|
||||
|
||||
### Cross-language 회귀 검증
|
||||
|
||||
```
|
||||
$ cd go && go run ./crosstest/go_kotlin.go
|
||||
|
||||
$ cd go && go run ./crosstest/go_python.go
|
||||
|
||||
$ cd go && go run ./crosstest/go_typescript.go
|
||||
|
||||
$ cd dart && dart run crosstest/dart_kotlin.dart
|
||||
|
||||
$ cd dart && dart run crosstest/dart_python.dart
|
||||
|
||||
$ cd dart && dart run crosstest/dart_typescript.dart
|
||||
|
||||
$ cd kotlin && ./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinGoKt
|
||||
|
||||
$ cd kotlin && ./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinPythonKt
|
||||
|
||||
$ cd kotlin && ./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinTypescriptKt
|
||||
|
||||
$ cd python && python3 -m crosstest.python_dart
|
||||
|
||||
$ cd python && python3 -m crosstest.python_go
|
||||
|
||||
$ cd python && python3 -m crosstest.python_kotlin
|
||||
|
||||
$ cd python && python3 -m crosstest.python_typescript
|
||||
|
||||
$ cd typescript && node --import tsx crosstest/typescript_dart.ts
|
||||
|
||||
$ cd typescript && node --import tsx crosstest/typescript_go.ts
|
||||
|
||||
$ cd typescript && node --import tsx crosstest/typescript_kotlin.ts
|
||||
|
||||
$ cd typescript && node --import tsx crosstest/typescript_python.ts
|
||||
```
|
||||
|
||||
### 정리 검증
|
||||
|
||||
```
|
||||
$ git diff --check
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
### 종합 판정
|
||||
|
||||
FAIL
|
||||
|
||||
### 차원별 평가
|
||||
|
||||
| 차원 | 평가 | 근거 |
|
||||
|------|------|------|
|
||||
| correctness | Fail | Kotlin 전체 테스트가 재현 가능하게 완료되지 않아 SELF-3 결과를 신뢰할 수 없음 |
|
||||
| completeness | Fail | `CODE_REVIEW.md`에 기록된 `cd kotlin && ./gradlew test` 검증과 실제 재검증 결과가 불일치 |
|
||||
| test coverage | Warn | 추가된 Kotlin concurrent 테스트 단독 실행은 통과하지만 전체 suite 통과 근거가 없음 |
|
||||
| API contract | Pass | Dart/Go secure crosstest 및 Python/TypeScript self-cell 보강은 기존 API 계약과 일치 |
|
||||
| code quality | Warn | Kotlin 신규 테스트 cleanup이 `try/finally`가 아니어서 실패 시 서버/클라이언트 누수 가능 |
|
||||
| plan deviation | Pass | 계획된 변경 범위 자체는 대체로 유지됨 |
|
||||
| verification trust | Fail | `./gradlew test` 통과 기록을 재현하지 못함 |
|
||||
|
||||
### 발견된 문제
|
||||
|
||||
- Required: `kotlin/src/test/kotlin/com/tokilabs/toki_socket/TcpTest.kt:16` - `timeout 90s ./gradlew test`가 daemon 재시작 후에도 `:test` 단계에서 타임아웃됨. jstack 기준 `TcpTest.testTcpSendReceive`의 `received.await()`에서 대기 중이며, `CODE_REVIEW.md`의 Kotlin 전체 테스트 통과 검증과 불일치한다. 전체 suite가 안정적으로 통과하도록 원인을 수정하고 `./gradlew test`를 재검증해야 한다.
|
||||
- Required: `kotlin/src/test/kotlin/com/tokilabs/toki_socket/TcpTest.kt:131` / `kotlin/src/test/kotlin/com/tokilabs/toki_socket/WsTest.kt:116` - 새 concurrent 테스트가 `awaitAll()` 이후에만 `client.close()`와 `server.stop()`을 호출한다. 요청 실패/타임아웃 시 cleanup이 건너뛰어 후속 테스트 hang을 유발할 수 있으므로 `try/finally`로 정리 보장을 추가해야 한다.
|
||||
|
||||
### 다음 단계
|
||||
|
||||
FAIL: Required 이슈를 해결하는 새 `PLAN.md`와 `CODE_REVIEW.md` 스텁을 작성해 리뷰 루프를 계속한다.
|
||||
99
agent-task/crosstest_coverage_gaps/code_review_1.log
Normal file
99
agent-task/crosstest_coverage_gaps/code_review_1.log
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<!-- task=crosstest_coverage_gaps plan=1 tag=REVIEW_CROSSTEST-COVERAGE -->
|
||||
|
||||
# Code Review Reference - REVIEW_CROSSTEST-COVERAGE
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-04-25
|
||||
task=crosstest_coverage_gaps, plan=1, tag=REVIEW_CROSSTEST-COVERAGE
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW.md` → `code_review_N.log` (N = 기존 code_review_*.log 수)
|
||||
2. `PLAN.md` → `plan_M.log` (M = 기존 plan_*.log 수)
|
||||
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [REVIEW_CROSSTEST-COVERAGE-1] Kotlin 전체 test suite hang 원인 수정 | [x] |
|
||||
| [REVIEW_CROSSTEST-COVERAGE-2] Kotlin concurrent 테스트 cleanup 보장 | [x] |
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
- REVIEW_CROSSTEST-COVERAGE-1에서 `testWsSendReceive`에도 동일한 `waitForCondition` 추가. 계획은 TCP만 언급했으나 WS 테스트도 동일 레이스 구조를 가지므로 함께 수정.
|
||||
- 메서드 단위 Gradle 필터(`--tests *.testTcpConcurrentRequests`)가 해당 환경에서 동작하지 않아, 검증은 클래스 단위(`--tests com.tokilabs.toki_socket.TcpTest`) 및 전체 suite로 대체.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- **`waitForCondition` 추가 위치**: `testTcpSendReceive` / `testWsSendReceive`에서 `DialTcp`/`DialWs` 직후, `client.send()` 직전에 삽입. `onClientConnected` 콜백이 listener를 등록하기 전에 send가 호출되는 레이스를 차단.
|
||||
- **`try/finally` 범위**: concurrent 테스트 두 곳(`testTcpConcurrentRequests`, `testWsConcurrentRequests`)만 변경. 계획이 명시한 범위 외 리팩터링은 하지 않음.
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `./gradlew test`가 timeout 없이 완료되는지 확인
|
||||
- `TcpTest.testTcpConcurrentRequests` / `WsTest.testWsConcurrentRequests`가 실패 경로에서도 cleanup을 보장하는지 확인
|
||||
- Kotlin self-cell scenario 1-4 검증 의미가 약화되지 않았는지 확인
|
||||
- plan=0에서 통과한 Dart/Go secure crosstest 및 Python/TypeScript 변경을 불필요하게 건드리지 않았는지 확인
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
### REVIEW_CROSSTEST-COVERAGE-1 중간 검증
|
||||
```
|
||||
$ cd kotlin && ./gradlew test --tests com.tokilabs.toki_socket.TcpTest
|
||||
BUILD SUCCESSFUL in 24s
|
||||
```
|
||||
|
||||
### REVIEW_CROSSTEST-COVERAGE-2 중간 검증
|
||||
```
|
||||
$ cd kotlin && ./gradlew test --tests com.tokilabs.toki_socket.TcpTest
|
||||
BUILD SUCCESSFUL in 13s
|
||||
$ cd kotlin && ./gradlew test --tests com.tokilabs.toki_socket.WsTest
|
||||
BUILD SUCCESSFUL in 4s
|
||||
(참고: 해당 환경에서 메서드 단위 --tests 필터가 동작하지 않아 클래스 단위로 대체)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ cd kotlin && ./gradlew test
|
||||
BUILD SUCCESSFUL in 12s
|
||||
$ git diff --check
|
||||
(출력 없음 — whitespace 이슈 없음)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
### 종합 판정
|
||||
|
||||
FAIL
|
||||
|
||||
### 차원별 평가
|
||||
|
||||
| 차원 | 평가 | 근거 |
|
||||
|------|------|------|
|
||||
| correctness | Fail | `server.clients().size == 1` 대기는 `onClientConnected` 내부 listener/request handler 등록 완료를 보장하지 못해 원래 race가 남아 있음 |
|
||||
| completeness | Fail | REVIEW_CROSSTEST-COVERAGE-1의 "listener 등록 전 send 호출 레이스 차단" 목표가 충분히 충족되지 않음 |
|
||||
| test coverage | Pass | Kotlin TCP/WS 클래스 테스트와 전체 `./gradlew test`는 재검증 통과 |
|
||||
| API contract | Pass | 공개 API/프로덕션 구현 변경 없이 테스트 범위 안에서 수정됨 |
|
||||
| code quality | Pass | concurrent 테스트 cleanup은 `try/finally`로 보강됨 |
|
||||
| plan deviation | Pass | WS send/receive 동일 레이스 보강은 계획 범위 안의 합리적 확장 |
|
||||
| verification trust | Warn | 검증 명령은 통과했지만 현재 assertion/대기 조건이 race 제거를 직접 검증하지는 않음 |
|
||||
|
||||
### 발견된 문제
|
||||
|
||||
- Required: `kotlin/src/test/kotlin/com/tokilabs/toki_socket/TcpTest.kt:28` / `kotlin/src/test/kotlin/com/tokilabs/toki_socket/WsTest.kt:26` - `waitForCondition { server.clients().size == 1 }`는 연결 객체가 서버 목록에 들어간 것만 확인한다. 실제 서버 구현은 `clients.add(client)` 이후 `onClientConnected(client)`를 호출하므로, 테스트가 깨어난 시점에 `addListenerTyped`가 아직 실행되지 않았을 수 있다. `CompletableDeferred<Unit>` 같은 readiness 신호를 `onClientConnected`에서 listener 등록 직후 complete하고, 테스트는 그 신호를 await한 뒤 send/request를 시작해야 한다.
|
||||
- Required: `kotlin/src/test/kotlin/com/tokilabs/toki_socket/TcpTest.kt:117` / `kotlin/src/test/kotlin/com/tokilabs/toki_socket/WsTest.kt:102` - 새 concurrent request 테스트도 클라이언트 연결 직후 요청을 시작하며, request listener 등록 완료를 기다리지 않는다. 동일하게 `onClientConnected`에서 request handler 등록 완료 신호를 내보내고, `awaitAll()` 시작 전에 해당 신호를 await해야 한다.
|
||||
|
||||
### 다음 단계
|
||||
|
||||
FAIL: Required 이슈를 해결하는 새 `PLAN.md`와 `CODE_REVIEW.md` 스텁을 작성해 리뷰 루프를 계속한다.
|
||||
95
agent-task/crosstest_coverage_gaps/code_review_2.log
Normal file
95
agent-task/crosstest_coverage_gaps/code_review_2.log
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
<!-- task=crosstest_coverage_gaps plan=2 tag=REVIEW_REVIEW_CROSSTEST-COVERAGE -->
|
||||
|
||||
# Code Review Reference - REVIEW_REVIEW_CROSSTEST-COVERAGE
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-04-25
|
||||
task=crosstest_coverage_gaps, plan=2, tag=REVIEW_REVIEW_CROSSTEST-COVERAGE
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW.md` → `code_review_N.log` (N = 기존 code_review_*.log 수)
|
||||
2. `PLAN.md` → `plan_M.log` (M = 기존 plan_*.log 수)
|
||||
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [REVIEW_REVIEW_CROSSTEST-COVERAGE-1] Kotlin 테스트 listener/request handler readiness 보장 | [x] |
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
없음. 계획과 동일하게 구현함.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- `testTcpSendReceive` / `testWsSendReceive`: `val listenerReady = CompletableDeferred<Unit>()`을 선언하고, `onClientConnected` 안에서 `addListenerTyped` 직후 `listenerReady.complete(Unit)` 호출. 기존 `waitForCondition { server.clients().size == 1 }`을 `listenerReady.await()`으로 교체.
|
||||
- `testTcpConcurrentRequests` / `testWsConcurrentRequests`: `val handlerReady = CompletableDeferred<Unit>()`을 선언하고, `onClientConnected` 안에서 `addRequestListenerTyped` 블록 이후 `handlerReady.complete(Unit)` 호출. 요청 시작(`map { async { ... } }`) 직전에 `handlerReady.await()` 삽입.
|
||||
- concurrent 테스트의 `try/finally` cleanup(`client.close()` / `server.stop()`)은 그대로 유지.
|
||||
- production 코드(`TcpServer`, `WsServer`, `TcpClient`, `WsClient` 등) 변경 없음.
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `testTcpSendReceive` / `testWsSendReceive`가 listener 등록 완료 신호를 await한 뒤 send하는지 확인
|
||||
- `testTcpConcurrentRequests` / `testWsConcurrentRequests`가 request listener 등록 완료 신호를 await한 뒤 요청을 시작하는지 확인
|
||||
- concurrent 테스트의 `try/finally` cleanup이 유지되는지 확인
|
||||
- Kotlin production server/client 동작이 불필요하게 변경되지 않았는지 확인
|
||||
- `./gradlew test`가 timeout 없이 완료되는지 확인
|
||||
|
||||
## 검증 결과
|
||||
|
||||
### REVIEW_REVIEW_CROSSTEST-COVERAGE-1 중간 검증
|
||||
```
|
||||
$ cd kotlin && ./gradlew test --tests com.tokilabs.toki_socket.TcpTest
|
||||
BUILD SUCCESSFUL in 6s
|
||||
10 actionable tasks: 2 executed, 8 up-to-date
|
||||
|
||||
$ cd kotlin && ./gradlew test --tests com.tokilabs.toki_socket.WsTest
|
||||
BUILD SUCCESSFUL in 4s
|
||||
10 actionable tasks: 1 executed, 9 up-to-date
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ cd kotlin && ./gradlew test
|
||||
BUILD SUCCESSFUL in 12s
|
||||
10 actionable tasks: 1 executed, 9 up-to-date
|
||||
|
||||
$ git diff --check
|
||||
(출력 없음 — whitespace 이상 없음)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
### 종합 판정
|
||||
|
||||
PASS
|
||||
|
||||
### 차원별 평가
|
||||
|
||||
| 차원 | 평가 | 근거 |
|
||||
|------|------|------|
|
||||
| correctness | Pass | listener/request handler 등록 완료 신호를 await한 뒤 send/request를 시작함 |
|
||||
| completeness | Pass | plan=2 체크리스트의 TCP/WS send 및 concurrent request readiness 보장이 모두 구현됨 |
|
||||
| test coverage | Pass | `TcpTest`, `WsTest`, 전체 `./gradlew test` 재검증 통과 |
|
||||
| API contract | Pass | Kotlin production server/client 코드는 변경하지 않고 테스트 안정성만 보강함 |
|
||||
| code quality | Pass | concurrent 테스트의 `try/finally` cleanup이 유지됨 |
|
||||
| plan deviation | Pass | 계획 대비 추가 변경 없음 |
|
||||
| verification trust | Pass | `CODE_REVIEW.md`의 검증 명령을 재실행해 통과 확인 |
|
||||
|
||||
### 발견된 문제
|
||||
|
||||
없음
|
||||
|
||||
### 다음 단계
|
||||
|
||||
PASS: `complete.log` 작성 후 종료한다.
|
||||
25
agent-task/crosstest_coverage_gaps/complete.log
Normal file
25
agent-task/crosstest_coverage_gaps/complete.log
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# 완료 로그 - crosstest_coverage_gaps
|
||||
|
||||
## 완료 일시
|
||||
|
||||
2026-04-25
|
||||
|
||||
## 요약
|
||||
|
||||
크로스테스트 coverage gap 보강 작업을 3회 리뷰 루프로 완료.
|
||||
|
||||
## 루프 이력
|
||||
|
||||
| Plan | Code Review | Verdict |
|
||||
|------|-------------|---------|
|
||||
| `plan_0.log` | `code_review_0.log` | FAIL |
|
||||
| `plan_1.log` | `code_review_1.log` | FAIL |
|
||||
| `plan_2.log` | `code_review_2.log` | PASS |
|
||||
|
||||
## 최종 리뷰 요약
|
||||
|
||||
- Go/Kotlin/Python/TypeScript self-cell concurrent request-response 테스트 보강을 확인했다.
|
||||
- Python WS self-cell 테스트 추가와 Dart/Go secure crosstest 보강을 확인했다.
|
||||
- Kotlin full test hang 원인을 테스트 readiness race로 좁히고, listener/request handler 등록 완료 신호를 await하도록 후속 수정했다.
|
||||
- Kotlin concurrent 테스트 cleanup이 `try/finally`로 보장되는 것을 확인했다.
|
||||
- 최종 검증에서 `TcpTest`, `WsTest`, 전체 `./gradlew test`, `git diff --check` 통과를 확인했다.
|
||||
263
agent-task/crosstest_coverage_gaps/plan_0.log
Normal file
263
agent-task/crosstest_coverage_gaps/plan_0.log
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
<!-- task=crosstest_coverage_gaps plan=0 tag=CROSSTEST-COVERAGE -->
|
||||
|
||||
# Crosstest Coverage Gaps - 작업 계획
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
이 계획은 언어별 크로스테스트 matrix에서 확인된 부족분만 보강한다.
|
||||
기존 20개 cross-language TCP/WS 조합은 이미 존재하므로, 새 matrix를 다시 만들기보다
|
||||
같은 언어 self-cell 시나리오 균일화와 secure transport 보강/보류 문서화에 집중한다.
|
||||
|
||||
완료 후 `CODE_REVIEW.md`에 실제 변경 파일, 검증 결과, 보류 사유를 채우고 체크박스를 `[x]`로 갱신한다.
|
||||
|
||||
## 배경
|
||||
|
||||
현재 crosstest 파일 기준으로 Dart, Go, Kotlin, Python, TypeScript의 서로 다른 언어 조합은
|
||||
서버 언어 row / 클라이언트 언어 cell 기준 20/20이 존재한다.
|
||||
|
||||
일반 TCP/WS cross-language 조합은 다음 4개 시나리오를 실행한다.
|
||||
|
||||
| Scenario | Requirement |
|
||||
|----------|-------------|
|
||||
| 1 | 클라이언트 fire-and-forget `TestData` 송신, 서버 수신 검증 |
|
||||
| 2 | 서버가 클라이언트로 `TestData(index=200, message=push from ... server)` push |
|
||||
| 3 | 단일 `sendRequest` 응답이 `index=req.index*2`, `message=echo: req.message`인지 검증 |
|
||||
| 4 | 여러 동시 `sendRequest`가 nonce/responseNonce로 올바르게 매핑되는지 검증 |
|
||||
|
||||
부족분은 두 종류다.
|
||||
|
||||
1. 같은 언어 self-cell이 위 4개 시나리오를 TCP/WS 양쪽에서 동일하게 보장한다는 근거가 언어별로 균일하지 않다.
|
||||
2. secure transport(TLS+TCP, WSS)는 Dart/Go 단위 테스트에는 있으나 cross-language matrix에는 없다. Kotlin/Python/TypeScript는 현재 구현 기준 secure server/client 지원이 부분적이거나 없다.
|
||||
|
||||
## 현재 Matrix 요약
|
||||
|
||||
범례:
|
||||
|
||||
- `CT`: package-local crosstest 존재
|
||||
- `UT-ok`: 같은 언어 테스트가 TCP/WS scenario 1-4를 충족하는 것으로 확인
|
||||
- `UT-gap`: 같은 언어 테스트가 일부 scenario를 빠뜨림
|
||||
|
||||
| Server \ Client | Dart | Go | Kotlin | Python | TypeScript |
|
||||
|-----------------|------|----|--------|--------|------------|
|
||||
| Dart | UT-ok 후보 | CT | CT | CT | CT |
|
||||
| Go | CT | UT-gap | CT | CT | CT |
|
||||
| Kotlin | CT | CT | UT-gap | CT | CT |
|
||||
| Python | CT | CT | CT | UT-gap | CT |
|
||||
| TypeScript | CT | CT | CT | CT | UT-gap |
|
||||
|
||||
구현 전 재확인 사항:
|
||||
|
||||
- Dart self-cell은 `dart/test/socket_test.dart`에서 TCP/WS request 동시성까지 확인된다. scenario 1/2까지 명확히 대조한 뒤 부족하면 같은 파일에만 보강한다.
|
||||
- Go/Kotlin/TypeScript self-cell은 TCP/WS send-push와 단일 request-response는 있으나 동시 request-response scenario가 별도 transport 테스트에 명확하지 않다.
|
||||
- Python self-cell은 TCP send/receive와 단일 request-response만 있고, Python-only WS 테스트와 동시 request-response 테스트가 부족하다.
|
||||
|
||||
## 작업 범위
|
||||
|
||||
### [SELF-1] Self-cell coverage audit 표준화
|
||||
|
||||
**목표**
|
||||
|
||||
각 언어의 같은 언어 셀이 TCP/WS scenario 1-4 중 무엇을 기존 테스트로 만족하는지 코드와 대조한다.
|
||||
|
||||
**수정/기록**
|
||||
|
||||
- `CODE_REVIEW.md`에 언어별 self-cell 감사 표를 작성한다.
|
||||
- Dart가 실제로 scenario 1-4를 모두 충족하면 소스 변경 없이 `UT-ok`로 기록한다.
|
||||
- 부족한 셀만 아래 항목에서 테스트를 추가한다.
|
||||
|
||||
### [SELF-2] Go self-cell 동시 request-response 보강
|
||||
|
||||
**문제**
|
||||
|
||||
`go/test/tcp_test.go`, `go/test/ws_test.go`에는 단일 request-response는 있으나,
|
||||
cross-language scenario 4와 같은 다중 동시 request-response 검증이 명확하지 않다.
|
||||
|
||||
**해결 방법**
|
||||
|
||||
- `go/test/tcp_test.go`에 TCP concurrent `SendRequestTyped` 테스트 추가
|
||||
- `go/test/ws_test.go`에 WS concurrent `SendRequestTyped` 테스트 추가
|
||||
- 기존 request listener 패턴을 재사용하고, 응답 index/message를 각 요청별로 검증
|
||||
|
||||
**수정 파일**
|
||||
|
||||
- [x] `go/test/tcp_test.go`
|
||||
- [x] `go/test/ws_test.go`
|
||||
|
||||
### [SELF-3] Kotlin self-cell 동시 request-response 보강
|
||||
|
||||
**문제**
|
||||
|
||||
`kotlin/src/test/kotlin/com/tokilabs/toki_socket/TcpTest.kt`,
|
||||
`kotlin/src/test/kotlin/com/tokilabs/toki_socket/WsTest.kt`에는 단일 request-response는 있으나,
|
||||
transport 레벨의 동시 request-response 검증이 명확하지 않다.
|
||||
|
||||
**해결 방법**
|
||||
|
||||
- TCP/WS 각각에 `async` 또는 `awaitAll` 기반 concurrent `sendRequestTyped` 테스트 추가
|
||||
- 기존 `testParserMap()`, `freePort()`, request listener 패턴 재사용
|
||||
|
||||
**수정 파일**
|
||||
|
||||
- [x] `kotlin/src/test/kotlin/com/tokilabs/toki_socket/TcpTest.kt`
|
||||
- [x] `kotlin/src/test/kotlin/com/tokilabs/toki_socket/WsTest.kt`
|
||||
|
||||
### [SELF-4] Python self-cell TCP/WS scenario 보강
|
||||
|
||||
**문제**
|
||||
|
||||
Python에는 `python/test/test_tcp.py`와 communicator 단위 테스트가 있으나,
|
||||
Python-only WS 테스트가 없고 TCP/WS concurrent request-response 검증도 부족하다.
|
||||
|
||||
**해결 방법**
|
||||
|
||||
- `python/test/test_tcp.py`에 TCP concurrent `send_request` 테스트 추가
|
||||
- Python WS 테스트 파일을 추가해 WS send-push, 단일 request-response, concurrent request-response를 검증
|
||||
- 새 파일은 기존 구조상 `python/test/test_ws.py`가 가장 자연스럽다. 새 파일 생성 사유를 `CODE_REVIEW.md`에 기록한다.
|
||||
|
||||
**수정 파일**
|
||||
|
||||
- [x] `python/test/test_tcp.py`
|
||||
- [x] `python/test/test_ws.py`
|
||||
|
||||
### [SELF-5] TypeScript self-cell 동시 request-response 보강
|
||||
|
||||
**문제**
|
||||
|
||||
`typescript/test/tcp.test.ts`, `typescript/test/ws.test.ts`에는 send-push와 단일 request-response는 있으나,
|
||||
transport 레벨의 동시 request-response 검증이 명확하지 않다.
|
||||
|
||||
**해결 방법**
|
||||
|
||||
- TCP/WS 각각에 `Promise.all` 기반 concurrent `sendRequestTyped` 테스트 추가
|
||||
- 기존 `parserMap()`, `TcpServer`, `WsServer`, cleanup 패턴 재사용
|
||||
|
||||
**수정 파일**
|
||||
|
||||
- [x] `typescript/test/tcp.test.ts`
|
||||
- [x] `typescript/test/ws.test.ts`
|
||||
|
||||
### [SECURE-1] Dart <-> Go secure crosstest 추가
|
||||
|
||||
**문제**
|
||||
|
||||
Dart와 Go는 TLS+TCP 및 WSS API와 단위 테스트가 있으나 cross-language secure transport 검증이 없다.
|
||||
|
||||
**해결 방법**
|
||||
|
||||
기존 Dart/Go pairwise crosstest를 확장한다.
|
||||
|
||||
- Dart server -> Go client:
|
||||
- `dart/crosstest/dart_go.dart`에 TLS+TCP, WSS phases 추가
|
||||
- `go/crosstest/dart_go_client/main.go`에 `tls`/`wss` mode 추가
|
||||
- Go server -> Dart client:
|
||||
- `go/crosstest/go_dart.go`에 TLS+TCP, WSS phases 추가
|
||||
- `dart/crosstest/go_dart_client.dart`에 `tls`/`wss` mode 추가
|
||||
- secure phase는 최소한 `TestData` exchange와 request-response를 검증한다.
|
||||
- 가능하면 일반 crosstest와 같은 scenario 1-4를 유지한다. 구현 난도가 커지면 scenario 1/3만 먼저 추가하지 말고 계획을 갱신한다.
|
||||
- 인증서는 test fixture 또는 실행 중 임시 생성 방식을 사용하고, machine-global trust store는 사용하지 않는다.
|
||||
|
||||
**수정 파일 후보**
|
||||
|
||||
- [x] `dart/crosstest/dart_go.dart`
|
||||
- [x] `go/crosstest/dart_go_client/main.go`
|
||||
- [x] `go/crosstest/go_dart.go`
|
||||
- [x] `dart/crosstest/go_dart_client.dart`
|
||||
- [x] `dart/test/certs/server.crt` / `dart/test/certs/server.key` 재사용 가능 여부 확인 → SAN에 IP:127.0.0.1, DNS:localhost 포함. 재사용 가능.
|
||||
|
||||
### [SECURE-2] Kotlin/Python/TypeScript secure matrix 보류 사유 문서화
|
||||
|
||||
**문제**
|
||||
|
||||
secure transport matrix를 모든 언어에 적용하려면 언어별 API 지원이 필요하다. 현재 코드 기준:
|
||||
|
||||
- Kotlin: WSS client API는 있으나 secure WS server와 TLS+TCP API가 없다.
|
||||
- Python: TCP/WS client/server에 SSL context 경로가 없다.
|
||||
- TypeScript: TCP는 `net`, WS는 plain `ws://`만 사용하며 TLS/WSS wrapper가 없다.
|
||||
|
||||
**해결 방법**
|
||||
|
||||
- `CODE_REVIEW.md`에 secure matrix 보류 표를 작성한다.
|
||||
- README나 언어별 README가 TLS/WSS 지원을 과하게 주장하는 경우에만 문서를 정정한다.
|
||||
- secure API 구현 자체는 이 계획의 범위를 넘긴다.
|
||||
|
||||
**수정 파일 후보**
|
||||
|
||||
- [x] `CODE_REVIEW.md`
|
||||
- [x] `README.md` 또는 언어별 README는 과한 지원 표기가 있을 때만 수정 → 과잉 주장 없음. 수정 불필요.
|
||||
|
||||
## 최종 목표 Matrix
|
||||
|
||||
일반 TCP/WS:
|
||||
|
||||
| Server \ Client | Dart | Go | Kotlin | Python | TypeScript |
|
||||
|-----------------|------|----|--------|--------|------------|
|
||||
| Dart | UT-ok | CT | CT | CT | CT |
|
||||
| Go | CT | UT-ok | CT | CT | CT |
|
||||
| Kotlin | CT | CT | UT-ok | CT | CT |
|
||||
| Python | CT | CT | CT | UT-ok | CT |
|
||||
| TypeScript | CT | CT | CT | CT | UT-ok |
|
||||
|
||||
Secure transport:
|
||||
|
||||
| Server \ Client | Dart | Go | Kotlin | Python | TypeScript |
|
||||
|-----------------|------|----|--------|--------|------------|
|
||||
| Dart | 기존 단위 테스트 | CT-secure | deferred | deferred | deferred |
|
||||
| Go | CT-secure | 기존 단위 테스트 | deferred | deferred | deferred |
|
||||
| Kotlin | deferred | deferred | deferred | deferred | deferred |
|
||||
| Python | deferred | deferred | deferred | deferred | deferred |
|
||||
| TypeScript | deferred | deferred | deferred | deferred | deferred |
|
||||
|
||||
## 검증 명령
|
||||
|
||||
Self-cell 보강 검증:
|
||||
|
||||
```bash
|
||||
cd dart && dart test
|
||||
cd go && go test ./...
|
||||
cd kotlin && ./gradlew test
|
||||
cd python && python3 -m pytest test/ -v
|
||||
cd typescript && npm test
|
||||
cd typescript && npm run check
|
||||
```
|
||||
|
||||
Dart/Go secure crosstest 보강 검증:
|
||||
|
||||
```bash
|
||||
cd dart && dart run crosstest/dart_go.dart
|
||||
cd go && go run ./crosstest/go_dart.go
|
||||
```
|
||||
|
||||
회귀 확인:
|
||||
|
||||
```bash
|
||||
cd go && go run ./crosstest/go_kotlin.go
|
||||
cd go && go run ./crosstest/go_python.go
|
||||
cd go && go run ./crosstest/go_typescript.go
|
||||
cd dart && dart run crosstest/dart_kotlin.dart
|
||||
cd dart && dart run crosstest/dart_python.dart
|
||||
cd dart && dart run crosstest/dart_typescript.dart
|
||||
cd kotlin && ./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinGoKt
|
||||
cd kotlin && ./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinPythonKt
|
||||
cd kotlin && ./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinTypescriptKt
|
||||
cd python && python3 -m crosstest.python_dart
|
||||
cd python && python3 -m crosstest.python_go
|
||||
cd python && python3 -m crosstest.python_kotlin
|
||||
cd python && python3 -m crosstest.python_typescript
|
||||
cd typescript && node --import tsx crosstest/typescript_dart.ts
|
||||
cd typescript && node --import tsx crosstest/typescript_go.ts
|
||||
cd typescript && node --import tsx crosstest/typescript_kotlin.ts
|
||||
cd typescript && node --import tsx crosstest/typescript_python.ts
|
||||
```
|
||||
|
||||
정리 검증:
|
||||
|
||||
```bash
|
||||
git diff --check
|
||||
```
|
||||
|
||||
## 범위 밖
|
||||
|
||||
- Kotlin/Python/TypeScript secure transport API 신규 구현
|
||||
- 새 언어 추가
|
||||
- proto/schema 변경
|
||||
- 기존 plain TCP/WS cross-language matrix 재작성
|
||||
101
agent-task/crosstest_coverage_gaps/plan_1.log
Normal file
101
agent-task/crosstest_coverage_gaps/plan_1.log
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<!-- task=crosstest_coverage_gaps plan=1 tag=REVIEW_CROSSTEST-COVERAGE -->
|
||||
|
||||
# Review Follow-up Plan - REVIEW_CROSSTEST-COVERAGE
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-04-25
|
||||
task=crosstest_coverage_gaps, plan=1, tag=REVIEW_CROSSTEST-COVERAGE
|
||||
|
||||
## 배경
|
||||
|
||||
plan=0 리뷰 결과 `FAIL` 판정이 나왔다. 구현 자체의 주요 범위는 맞지만, Kotlin self-cell 검증 신뢰도가 확보되지 않았다.
|
||||
|
||||
재검증 결과:
|
||||
|
||||
- `TcpTest.testTcpConcurrentRequests` 단독 실행 통과
|
||||
- `WsTest.testWsConcurrentRequests` 단독 실행 통과
|
||||
- `TcpTest.testTcpSendReceive` 단독 실행 통과
|
||||
- `./gradlew test` 및 `--tests com.tokilabs.toki_socket.TcpTest`는 `:test` 단계에서 타임아웃 재현
|
||||
|
||||
## 작업 범위
|
||||
|
||||
### [REVIEW_CROSSTEST-COVERAGE-1] Kotlin 전체 test suite hang 원인 수정
|
||||
|
||||
**문제**
|
||||
|
||||
`kotlin && ./gradlew test`가 전체 suite에서 완료되지 않는다. 리뷰 중 jstack은 `TcpTest.testTcpSendReceive`의 `received.await()` 대기와 TCP accept/readLoop가 남아 있는 상태를 보였다.
|
||||
|
||||
**해결 방향**
|
||||
|
||||
- Kotlin TCP/WS 테스트의 서버 시작, 클라이언트 연결, listener 등록 순서를 재검토한다.
|
||||
- 테스트 간 서버/클라이언트 상태가 남아 다음 테스트에 영향을 주지 않도록 정리 흐름을 통일한다.
|
||||
- 기존 구현 코어를 불필요하게 바꾸지 말고, 우선 테스트 안정성 문제를 좁혀 수정한다.
|
||||
|
||||
**체크리스트**
|
||||
|
||||
- [ ] `./gradlew test --tests com.tokilabs.toki_socket.TcpTest`가 타임아웃 없이 통과한다.
|
||||
- [ ] `./gradlew test`가 타임아웃 없이 통과한다.
|
||||
- [ ] 기존 TCP/WS self-cell 시나리오 1-4 의미가 약화되지 않는다.
|
||||
|
||||
**검증**
|
||||
|
||||
```bash
|
||||
cd kotlin && ./gradlew test --tests com.tokilabs.toki_socket.TcpTest
|
||||
cd kotlin && ./gradlew test
|
||||
```
|
||||
|
||||
### [REVIEW_CROSSTEST-COVERAGE-2] Kotlin concurrent 테스트 cleanup 보장
|
||||
|
||||
**문제**
|
||||
|
||||
`testTcpConcurrentRequests`와 `testWsConcurrentRequests`는 `awaitAll()` 뒤에만 `client.close()` / `server.stop()`을 호출한다. 요청 실패나 timeout이 발생하면 cleanup이 건너뛰어 후속 테스트가 hang 될 수 있다.
|
||||
|
||||
**해결 방향**
|
||||
|
||||
Before:
|
||||
|
||||
```kotlin
|
||||
deferred.awaitAll()
|
||||
client.close()
|
||||
server.stop()
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```kotlin
|
||||
try {
|
||||
deferred.awaitAll()
|
||||
} finally {
|
||||
client.close()
|
||||
server.stop()
|
||||
}
|
||||
```
|
||||
|
||||
필요하면 기존 다른 Kotlin 테스트도 같은 패턴으로 정리하되, 이번 리뷰 이슈와 무관한 리팩터링은 하지 않는다.
|
||||
|
||||
**체크리스트**
|
||||
|
||||
- [ ] TCP concurrent 테스트가 실패해도 client/server cleanup이 실행된다.
|
||||
- [ ] WS concurrent 테스트가 실패해도 client/server cleanup이 실행된다.
|
||||
- [ ] cleanup 보강으로 assertion 의미가 바뀌지 않는다.
|
||||
|
||||
**검증**
|
||||
|
||||
```bash
|
||||
cd kotlin && ./gradlew test --tests com.tokilabs.toki_socket.TcpTest.testTcpConcurrentRequests
|
||||
cd kotlin && ./gradlew test --tests com.tokilabs.toki_socket.WsTest.testWsConcurrentRequests
|
||||
```
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
cd kotlin && ./gradlew test
|
||||
git diff --check
|
||||
```
|
||||
|
||||
## 범위 밖
|
||||
|
||||
- Kotlin secure transport API 신규 구현
|
||||
- Dart/Go/Python/TypeScript 구현 변경
|
||||
- agent-ops 규칙/스킬 변경
|
||||
89
agent-task/crosstest_coverage_gaps/plan_2.log
Normal file
89
agent-task/crosstest_coverage_gaps/plan_2.log
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<!-- task=crosstest_coverage_gaps plan=2 tag=REVIEW_REVIEW_CROSSTEST-COVERAGE -->
|
||||
|
||||
# Review Follow-up Plan - REVIEW_REVIEW_CROSSTEST-COVERAGE
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-04-25
|
||||
task=crosstest_coverage_gaps, plan=2, tag=REVIEW_REVIEW_CROSSTEST-COVERAGE
|
||||
|
||||
## 배경
|
||||
|
||||
plan=1 리뷰 결과 `FAIL` 판정이 나왔다. Kotlin `./gradlew test`는 통과했지만, 테스트에서 추가한 `waitForCondition { server.clients().size == 1 }`는 listener/request handler 등록 완료를 보장하지 않는다.
|
||||
|
||||
Kotlin 서버 구현은 다음 순서로 동작한다.
|
||||
|
||||
```kotlin
|
||||
clients.add(client)
|
||||
onClientConnected(client)
|
||||
```
|
||||
|
||||
따라서 테스트가 `server.clients().size == 1` 조건을 만족하더라도 `onClientConnected` 안의 `addListenerTyped` 또는 `addRequestListenerTyped`가 아직 끝나지 않았을 수 있다.
|
||||
|
||||
## 작업 범위
|
||||
|
||||
### [REVIEW_REVIEW_CROSSTEST-COVERAGE-1] Kotlin 테스트 listener/request handler readiness 보장
|
||||
|
||||
**문제**
|
||||
|
||||
`testTcpSendReceive`, `testWsSendReceive`, `testTcpConcurrentRequests`, `testWsConcurrentRequests`가 연결 확인만 하고 listener/request handler 등록 완료를 기다리지 않는다. 원래 suite hang의 원인으로 본 race를 완전히 제거하지 못한다.
|
||||
|
||||
**해결 방향**
|
||||
|
||||
- 각 테스트에서 `CompletableDeferred<Unit>` 같은 readiness 신호를 둔다.
|
||||
- `server.onClientConnected` 안에서 listener 또는 request listener를 등록한 직후 readiness를 complete한다.
|
||||
- 클라이언트 연결 후 send/request 시작 전에 readiness를 await한다.
|
||||
- `server.clients().size` 대기는 연결 수 확인 용도로만 필요할 때 유지하고, listener 등록 완료 근거로 사용하지 않는다.
|
||||
|
||||
Before:
|
||||
|
||||
```kotlin
|
||||
server.onClientConnected = { client ->
|
||||
addListenerTyped<TestData>(client.communicator) { received.complete(it) }
|
||||
}
|
||||
val client = DialTcp(...)
|
||||
waitForCondition(message = "client did not connect") { server.clients().size == 1 }
|
||||
client.send(...)
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```kotlin
|
||||
val listenerReady = CompletableDeferred<Unit>()
|
||||
server.onClientConnected = { client ->
|
||||
addListenerTyped<TestData>(client.communicator) { received.complete(it) }
|
||||
listenerReady.complete(Unit)
|
||||
}
|
||||
val client = DialTcp(...)
|
||||
listenerReady.await()
|
||||
client.send(...)
|
||||
```
|
||||
|
||||
**체크리스트**
|
||||
|
||||
- [ ] TCP send/receive 테스트가 listener 등록 완료 후 send한다.
|
||||
- [ ] WS send/receive 테스트가 listener 등록 완료 후 send한다.
|
||||
- [ ] TCP concurrent request 테스트가 request handler 등록 완료 후 요청을 시작한다.
|
||||
- [ ] WS concurrent request 테스트가 request handler 등록 완료 후 요청을 시작한다.
|
||||
- [ ] concurrent 테스트의 `try/finally` cleanup은 유지한다.
|
||||
|
||||
**검증**
|
||||
|
||||
```bash
|
||||
cd kotlin && ./gradlew test --tests com.tokilabs.toki_socket.TcpTest
|
||||
cd kotlin && ./gradlew test --tests com.tokilabs.toki_socket.WsTest
|
||||
```
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
cd kotlin && ./gradlew test
|
||||
git diff --check
|
||||
```
|
||||
|
||||
## 범위 밖
|
||||
|
||||
- Kotlin production server/client 동작 변경
|
||||
- Dart/Go/Python/TypeScript 구현 변경
|
||||
- secure transport API 신규 구현
|
||||
9
|
||||
122
agent-task/proto_restructure/CODE_REVIEW.md
Normal file
122
agent-task/proto_restructure/CODE_REVIEW.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<!-- task=proto_restructure plan=0 tag=PROTO_MOVE -->
|
||||
|
||||
# Code Review Reference - PROTO_MOVE
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-04-25
|
||||
task=proto_restructure, plan=0, tag=PROTO_MOVE
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW.md` → `code_review_0.log` (기존 code_review_*.log 수 = 0)
|
||||
2. `PLAN.md` → `plan_0.log` (기존 plan_*.log 수 = 0)
|
||||
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [PROTO_MOVE-1] `proto/` 디렉터리 생성 및 generate_proto.sh 수정 | [x] |
|
||||
| [PROTO_MOVE-2] `check_proto_sync.sh` canonical 경로 변경 | [x] |
|
||||
| [PROTO_MOVE-3] `PROTOCOL.md` Proto Source 섹션 업데이트 | [x] |
|
||||
| [PROTO_MOVE-4] `dart/lib/src/packets/message_common.proto` 제거 | [x] |
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
- `README.md`, `PORTING_GUIDE.md`, `typescript/README.md`에도 이전 canonical 경로(`dart/lib/src/packets/message_common.proto`)가 남아 있어 함께 갱신했다. proto 원본 이동 후 사용자-facing 안내가 서로 충돌하지 않도록 하기 위한 변경이다.
|
||||
- `agent-ops/rules/project/**`와 새 언어 템플릿에도 이전 proto 원본 경로가 남아 있어, 실제 구조와 다음 작업 지시가 어긋나지 않도록 protocol/dart 규칙 및 템플릿 문구를 함께 갱신했다.
|
||||
- `tools/generate_proto.sh` 검증을 위해 로컬 PATH에 `protoc-gen-dart`와 임시 Go toolchain/protoc-gen-go를 추가해 실행했다. 스키마가 바뀐 것은 아니므로 생성 산출물(`*.pb.dart`, `*.pb.go`)은 최종 diff에 남기지 않았다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- `proto/message_common.proto`를 언어 옵션 없는 canonical proto로 둔다.
|
||||
- Go/Kotlin의 proto copy는 기존처럼 언어별 generator option만 보존한다.
|
||||
- Dart 생성은 `--proto_path="$repo_root/proto"`와 절대 출력 경로를 사용해 repo root의 proto를 입력으로 삼는다.
|
||||
- `check_proto_sync.sh`는 `canonical_proto`를 기준으로 Go/Kotlin copy를 normalize 비교하고, 오류 메시지에서 "Dart canonical" 표현을 제거한다.
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `proto/message_common.proto` 존재 여부, 내용이 기존 Dart canonical과 일치하는지
|
||||
- `tools/generate_proto.sh` — Dart 생성이 `proto/` 입력, `dart/lib/src/packets/` 출력으로 변경됐는지
|
||||
- `tools/check_proto_sync.sh` — `canonical_proto` 변수가 `proto/message_common.proto` 참조하는지, "Dart canonical" 문구 제거됐는지
|
||||
- `PROTOCOL.md` — "Proto Source" 섹션이 `proto/message_common.proto`를 canonical로 명시하는지
|
||||
- `dart/lib/src/packets/message_common.proto` 파일이 삭제됐는지
|
||||
- `dart/lib/src/packets/` 에 `.pb.dart` 생성 결과물은 여전히 존재하는지
|
||||
- `tools/check_proto_sync.sh` 실행 시 "Proto schemas are in sync." 출력되는지
|
||||
- Dart/Go/Kotlin 테스트 모두 통과하는지
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
### PROTO_MOVE-1 중간 검증
|
||||
```
|
||||
$ ls proto/
|
||||
message_common.proto
|
||||
|
||||
$ PATH="$HOME/.pub-cache/bin:/tmp/codex-gopath/bin:/tmp/codex-go/go/bin:$PATH" GOPATH=/tmp/codex-gopath tools/generate_proto.sh
|
||||
Proto schemas are in sync.
|
||||
|
||||
$ cd dart && dart analyze
|
||||
Analyzing dart...
|
||||
No issues found!
|
||||
```
|
||||
|
||||
### PROTO_MOVE-2 중간 검증
|
||||
```
|
||||
$ tools/check_proto_sync.sh
|
||||
Proto schemas are in sync.
|
||||
```
|
||||
|
||||
### PROTO_MOVE-3 중간 검증
|
||||
```
|
||||
$ grep "canonical" PROTOCOL.md
|
||||
`proto/message_common.proto` is the canonical packet definition.
|
||||
|
||||
$ grep "dart/lib/src/packets/message_common.proto" PROTOCOL.md
|
||||
(no output)
|
||||
```
|
||||
|
||||
### PROTO_MOVE-4 중간 검증
|
||||
```
|
||||
$ ls dart/lib/src/packets/
|
||||
message_common.pb.dart
|
||||
message_common.pbenum.dart
|
||||
message_common.pbjson.dart
|
||||
message_common.pbserver.dart
|
||||
|
||||
$ cd dart && dart analyze
|
||||
Analyzing dart...
|
||||
No issues found!
|
||||
|
||||
$ cd dart && dart test
|
||||
00:15 +43: All tests passed!
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ tools/check_proto_sync.sh
|
||||
Proto schemas are in sync.
|
||||
|
||||
$ rg -n "Dart canonical|canonical Dart|dart/lib/src/packets/message_common\.proto" --glob '!agent-task/**'
|
||||
(no output)
|
||||
|
||||
$ cd dart && dart analyze
|
||||
Analyzing dart...
|
||||
No issues found!
|
||||
|
||||
$ cd dart && dart test
|
||||
00:15 +43: All tests passed!
|
||||
|
||||
$ cd go && PATH="/tmp/codex-go/go/bin:$PATH" GOPATH=/tmp/codex-gopath go build ./...
|
||||
(no output; exit 0)
|
||||
|
||||
$ cd kotlin && env JAVA_HOME=... ./gradlew build
|
||||
BUILD SUCCESSFUL in 15s
|
||||
```
|
||||
281
agent-task/proto_restructure/PLAN.md
Normal file
281
agent-task/proto_restructure/PLAN.md
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
<!-- task=proto_restructure plan=0 tag=PROTO_MOVE -->
|
||||
|
||||
# Proto 루트 디렉터리 독립화
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
각 항목의 체크리스트를 완료하면서 `[ ]`를 `[x]`로 표시하라.
|
||||
중간 검증 명령은 해당 항목 구현 직후 실행하고, 출력을 `CODE_REVIEW.md`의 검증 결과 섹션에 붙여넣어라.
|
||||
최종 검증도 마찬가지로 실행 후 출력을 기록하라.
|
||||
계획과 다르게 구현한 부분은 이유와 함께 `CODE_REVIEW.md`의 "계획 대비 변경 사항"에 기록하라.
|
||||
|
||||
⚠️ **이 태스크는 모든 언어의 빌드 경로를 변경한다. 각 검증을 반드시 실행한 후 다음 단계로 넘어가라.**
|
||||
|
||||
---
|
||||
|
||||
## 배경
|
||||
|
||||
현재 proto canonical source가 `dart/lib/src/packets/message_common.proto`에 위치한다.
|
||||
Dart는 5개 언어 구현체 중 하나이지만 동시에 스펙 원본의 저장소 역할을 하고 있어,
|
||||
새 기여자에게 "proto = Dart 전용"으로 오해될 수 있다.
|
||||
`proto/message_common.proto` 루트 디렉터리로 이동하면:
|
||||
|
||||
- proto가 특정 언어 종속성 없이 독립 소스임이 명확해진다
|
||||
- 새 언어 포팅 시 proto 위치가 자명해진다
|
||||
- `check_proto_sync.sh`의 "canonical Dart proto"라는 명칭이 사라진다
|
||||
|
||||
## 언어별 proto 현황
|
||||
|
||||
| 언어 | 현재 위치 | 방식 | 변경 여부 |
|
||||
|------|----------|------|---------|
|
||||
| Dart | `dart/lib/src/packets/message_common.proto` (canonical) | `protoc --dart_out` (generate_proto.sh) | 제거 후 생성 경로 변경 |
|
||||
| Go | `go/packets/message_common.proto` (go_package 옵션 포함 복사본) | `protoc --go_out` (generate_proto.sh) | 유지 (언어 옵션 필요), sync 비교 대상 변경 |
|
||||
| Kotlin | `kotlin/src/main/proto/message_common.proto` (java 옵션 포함 복사본) | Gradle protobuf 플러그인 | 유지 (언어 옵션 필요), sync 비교 대상 변경 |
|
||||
| TypeScript | `typescript/src/packets/message_common_pb.ts` (생성 결과물만, .proto 없음) | `protoc-gen-es` 수동 실행 | 영향 없음 |
|
||||
| Python | `python/toki_socket/packets/message_common_pb2.py` (생성 결과물만, .proto 없음) | `protoc --python_out` 수동 실행 | 영향 없음 |
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
```
|
||||
[PROTO_MOVE-1] proto/ 생성 + generate_proto.sh 수정
|
||||
→ [PROTO_MOVE-2] check_proto_sync.sh canonical 경로 변경
|
||||
→ [PROTO_MOVE-3] PROTOCOL.md 업데이트
|
||||
→ [PROTO_MOVE-4] dart/lib/src/packets/message_common.proto 제거
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### [PROTO_MOVE-1] `proto/` 디렉터리 생성 및 generate_proto.sh 수정
|
||||
|
||||
**문제**
|
||||
|
||||
`tools/generate_proto.sh`가 Dart canonical에서 직접 생성한다.
|
||||
|
||||
```bash
|
||||
# 현재
|
||||
cd "$repo_root/dart"
|
||||
protoc --dart_out=lib/src/packets lib/src/packets/message_common.proto
|
||||
|
||||
cd "$repo_root/go"
|
||||
protoc --go_out=. --go_opt=paths=source_relative packets/message_common.proto
|
||||
```
|
||||
|
||||
**해결 방법**
|
||||
|
||||
1. `proto/message_common.proto` 생성: `dart/lib/src/packets/message_common.proto`에서 언어 옵션 없이 그대로 복사 (현재 Dart canonical에는 언어 옵션이 없으므로 내용 동일).
|
||||
2. `generate_proto.sh`의 Dart 생성 경로를 `proto/`에서 `dart/lib/src/packets/`로 출력하도록 변경.
|
||||
3. Go 생성은 Go copy(`go/packets/message_common.proto`)에서 그대로 진행 (go_package 옵션 보존 필요).
|
||||
|
||||
Before (`generate_proto.sh` Dart 섹션):
|
||||
```bash
|
||||
(
|
||||
cd "$repo_root/dart"
|
||||
protoc --dart_out=lib/src/packets lib/src/packets/message_common.proto
|
||||
)
|
||||
```
|
||||
|
||||
After:
|
||||
```bash
|
||||
(
|
||||
protoc \
|
||||
--proto_path="$repo_root/proto" \
|
||||
--dart_out="$repo_root/dart/lib/src/packets" \
|
||||
message_common.proto
|
||||
)
|
||||
```
|
||||
|
||||
Go 섹션은 변경 없음 (Go는 자체 복사본에서 생성).
|
||||
|
||||
**수정 파일 및 체크리스트**
|
||||
|
||||
- [x] `proto/message_common.proto` 생성 (dart canonical과 동일 내용)
|
||||
- [x] `tools/generate_proto.sh` — Dart 생성 섹션 경로 변경
|
||||
|
||||
**테스트 없음** — generate_proto.sh 실행으로 검증
|
||||
|
||||
**중간 검증**
|
||||
|
||||
```bash
|
||||
# protoc가 있는 환경에서 실행
|
||||
tools/generate_proto.sh
|
||||
# 오류 없이 완료
|
||||
|
||||
# 생성 결과가 기존과 동일한지 확인
|
||||
cd dart && dart analyze
|
||||
# No issues found
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### [PROTO_MOVE-2] `check_proto_sync.sh` canonical 경로 변경
|
||||
|
||||
**문제**
|
||||
|
||||
`tools/check_proto_sync.sh`의 canonical이 `dart_proto` 변수로 Dart 경로를 가리킨다.
|
||||
|
||||
```bash
|
||||
dart_proto="$repo_root/dart/lib/src/packets/message_common.proto"
|
||||
```
|
||||
|
||||
**해결 방법**
|
||||
|
||||
변수명과 경로를 `proto/` 기준으로 변경한다.
|
||||
|
||||
Before:
|
||||
```bash
|
||||
dart_proto="$repo_root/dart/lib/src/packets/message_common.proto"
|
||||
go_proto="$repo_root/go/packets/message_common.proto"
|
||||
kotlin_proto="$repo_root/kotlin/src/main/proto/message_common.proto"
|
||||
|
||||
if [[ ! -f "$dart_proto" ]]; then
|
||||
echo "Missing canonical Dart proto: $dart_proto" >&2
|
||||
exit 1
|
||||
fi
|
||||
# ...
|
||||
normalize_proto "$dart_proto" >"$tmp_dir/dart.proto"
|
||||
normalize_proto "$go_proto" >"$tmp_dir/go.proto"
|
||||
# ...
|
||||
if ! cmp -s "$tmp_dir/dart.proto" "$tmp_dir/go.proto"; then
|
||||
echo "Proto schema mismatch: go/packets/message_common.proto must match the Dart canonical proto ..."
|
||||
# ...
|
||||
```
|
||||
|
||||
After:
|
||||
```bash
|
||||
canonical_proto="$repo_root/proto/message_common.proto"
|
||||
go_proto="$repo_root/go/packets/message_common.proto"
|
||||
kotlin_proto="$repo_root/kotlin/src/main/proto/message_common.proto"
|
||||
|
||||
if [[ ! -f "$canonical_proto" ]]; then
|
||||
echo "Missing canonical proto: $canonical_proto" >&2
|
||||
exit 1
|
||||
fi
|
||||
# ...
|
||||
normalize_proto "$canonical_proto" >"$tmp_dir/canonical.proto"
|
||||
normalize_proto "$go_proto" >"$tmp_dir/go.proto"
|
||||
# ...
|
||||
if ! cmp -s "$tmp_dir/canonical.proto" "$tmp_dir/go.proto"; then
|
||||
echo "Proto schema mismatch: go/packets/message_common.proto must match proto/message_common.proto ..."
|
||||
# ...
|
||||
```
|
||||
|
||||
**수정 파일 및 체크리스트**
|
||||
|
||||
- [x] `tools/check_proto_sync.sh` — `dart_proto` → `canonical_proto`, 경로를 `proto/message_common.proto`로 변경
|
||||
- [x] `tools/check_proto_sync.sh` — 오류 메시지에서 "Dart canonical" 문구 제거
|
||||
- [x] `tools/check_proto_sync.sh` — normalize 비교 대상 변수명 일관성 수정
|
||||
|
||||
**중간 검증**
|
||||
|
||||
```bash
|
||||
tools/check_proto_sync.sh
|
||||
# Proto schemas are in sync.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### [PROTO_MOVE-3] `PROTOCOL.md` Proto Source 섹션 업데이트
|
||||
|
||||
**문제**
|
||||
|
||||
`PROTOCOL.md` 하단 "Proto Source" 섹션이 Dart 경로를 canonical로 명시한다.
|
||||
|
||||
```markdown
|
||||
`dart/lib/src/packets/message_common.proto` is the canonical packet definition.
|
||||
All language implementations must keep the same message schema and generate bindings from it.
|
||||
```
|
||||
|
||||
**해결 방법**
|
||||
|
||||
```markdown
|
||||
`proto/message_common.proto` is the canonical packet definition.
|
||||
All language implementations generate bindings from it.
|
||||
Language-specific generation options (go_package, java_package, etc.) are kept in each
|
||||
language's own proto copy under `go/packets/` and `kotlin/src/main/proto/`.
|
||||
```
|
||||
|
||||
**수정 파일 및 체크리스트**
|
||||
|
||||
- [x] `PROTOCOL.md` — "Proto Source" 섹션 경로 및 설명 업데이트
|
||||
|
||||
**테스트 없음** — 문서 전용
|
||||
|
||||
**중간 검증**
|
||||
|
||||
```bash
|
||||
grep "canonical" PROTOCOL.md
|
||||
# proto/message_common.proto 언급
|
||||
grep "dart/lib/src/packets/message_common.proto" PROTOCOL.md
|
||||
# 출력 없음이 정상
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### [PROTO_MOVE-4] `dart/lib/src/packets/message_common.proto` 제거
|
||||
|
||||
**문제**
|
||||
|
||||
PROTO_MOVE-1~3 완료 후 `dart/lib/src/packets/message_common.proto`는 더 이상 canonical이 아니다.
|
||||
그러나 여전히 파일로 남아 혼란을 줄 수 있다.
|
||||
|
||||
**해결 방법**
|
||||
|
||||
파일을 삭제한다. Dart 생성 결과물인 `.pb.dart` 파일들은 유지한다.
|
||||
|
||||
**수정 파일 및 체크리스트**
|
||||
|
||||
- [x] `dart/lib/src/packets/message_common.proto` 삭제
|
||||
- [x] `dart analyze` — proto 파일 제거 후 여전히 No issues인지 확인
|
||||
|
||||
**중간 검증**
|
||||
|
||||
```bash
|
||||
ls dart/lib/src/packets/
|
||||
# message_common.pb.dart, message_common.pbenum.dart 등 생성 결과물만 존재
|
||||
# message_common.proto 없음
|
||||
|
||||
cd dart && dart analyze
|
||||
# No issues found
|
||||
cd dart && dart test
|
||||
# 전체 PASS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `proto/message_common.proto` (신규) | PROTO_MOVE-1 |
|
||||
| `tools/generate_proto.sh` | PROTO_MOVE-1 |
|
||||
| `tools/check_proto_sync.sh` | PROTO_MOVE-2 |
|
||||
| `PROTOCOL.md` | PROTO_MOVE-3 |
|
||||
| `dart/lib/src/packets/message_common.proto` (삭제) | PROTO_MOVE-4 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
# proto sync 도구 검증
|
||||
tools/check_proto_sync.sh
|
||||
# Proto schemas are in sync.
|
||||
|
||||
# Dart
|
||||
cd dart && dart analyze
|
||||
# No issues found
|
||||
cd dart && dart test
|
||||
# 전체 PASS
|
||||
|
||||
# Go
|
||||
cd go && go build ./...
|
||||
# 빌드 성공
|
||||
|
||||
# Kotlin
|
||||
cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle ./gradlew build
|
||||
# BUILD SUCCESSFUL
|
||||
|
||||
# proto/ 파일 확인
|
||||
ls proto/
|
||||
# message_common.proto 존재
|
||||
ls dart/lib/src/packets/
|
||||
# *.pb.dart만 존재, message_common.proto 없음
|
||||
```
|
||||
122
agent-task/proto_restructure/code_review_0.log
Normal file
122
agent-task/proto_restructure/code_review_0.log
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<!-- task=proto_restructure plan=0 tag=PROTO_MOVE -->
|
||||
|
||||
# Code Review Reference - PROTO_MOVE
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-04-25
|
||||
task=proto_restructure, plan=0, tag=PROTO_MOVE
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW.md` → `code_review_0.log` (기존 code_review_*.log 수 = 0)
|
||||
2. `PLAN.md` → `plan_0.log` (기존 plan_*.log 수 = 0)
|
||||
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [PROTO_MOVE-1] `proto/` 디렉터리 생성 및 generate_proto.sh 수정 | [x] |
|
||||
| [PROTO_MOVE-2] `check_proto_sync.sh` canonical 경로 변경 | [x] |
|
||||
| [PROTO_MOVE-3] `PROTOCOL.md` Proto Source 섹션 업데이트 | [x] |
|
||||
| [PROTO_MOVE-4] `dart/lib/src/packets/message_common.proto` 제거 | [x] |
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
- `README.md`, `PORTING_GUIDE.md`, `typescript/README.md`에도 이전 canonical 경로(`dart/lib/src/packets/message_common.proto`)가 남아 있어 함께 갱신했다. proto 원본 이동 후 사용자-facing 안내가 서로 충돌하지 않도록 하기 위한 변경이다.
|
||||
- `agent-ops/rules/project/**`와 새 언어 템플릿에도 이전 proto 원본 경로가 남아 있어, 실제 구조와 다음 작업 지시가 어긋나지 않도록 protocol/dart 규칙 및 템플릿 문구를 함께 갱신했다.
|
||||
- `tools/generate_proto.sh` 검증을 위해 로컬 PATH에 `protoc-gen-dart`와 임시 Go toolchain/protoc-gen-go를 추가해 실행했다. 스키마가 바뀐 것은 아니므로 생성 산출물(`*.pb.dart`, `*.pb.go`)은 최종 diff에 남기지 않았다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- `proto/message_common.proto`를 언어 옵션 없는 canonical proto로 둔다.
|
||||
- Go/Kotlin의 proto copy는 기존처럼 언어별 generator option만 보존한다.
|
||||
- Dart 생성은 `--proto_path="$repo_root/proto"`와 절대 출력 경로를 사용해 repo root의 proto를 입력으로 삼는다.
|
||||
- `check_proto_sync.sh`는 `canonical_proto`를 기준으로 Go/Kotlin copy를 normalize 비교하고, 오류 메시지에서 "Dart canonical" 표현을 제거한다.
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `proto/message_common.proto` 존재 여부, 내용이 기존 Dart canonical과 일치하는지
|
||||
- `tools/generate_proto.sh` — Dart 생성이 `proto/` 입력, `dart/lib/src/packets/` 출력으로 변경됐는지
|
||||
- `tools/check_proto_sync.sh` — `canonical_proto` 변수가 `proto/message_common.proto` 참조하는지, "Dart canonical" 문구 제거됐는지
|
||||
- `PROTOCOL.md` — "Proto Source" 섹션이 `proto/message_common.proto`를 canonical로 명시하는지
|
||||
- `dart/lib/src/packets/message_common.proto` 파일이 삭제됐는지
|
||||
- `dart/lib/src/packets/` 에 `.pb.dart` 생성 결과물은 여전히 존재하는지
|
||||
- `tools/check_proto_sync.sh` 실행 시 "Proto schemas are in sync." 출력되는지
|
||||
- Dart/Go/Kotlin 테스트 모두 통과하는지
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
### PROTO_MOVE-1 중간 검증
|
||||
```
|
||||
$ ls proto/
|
||||
message_common.proto
|
||||
|
||||
$ PATH="$HOME/.pub-cache/bin:/tmp/codex-gopath/bin:/tmp/codex-go/go/bin:$PATH" GOPATH=/tmp/codex-gopath tools/generate_proto.sh
|
||||
Proto schemas are in sync.
|
||||
|
||||
$ cd dart && dart analyze
|
||||
Analyzing dart...
|
||||
No issues found!
|
||||
```
|
||||
|
||||
### PROTO_MOVE-2 중간 검증
|
||||
```
|
||||
$ tools/check_proto_sync.sh
|
||||
Proto schemas are in sync.
|
||||
```
|
||||
|
||||
### PROTO_MOVE-3 중간 검증
|
||||
```
|
||||
$ grep "canonical" PROTOCOL.md
|
||||
`proto/message_common.proto` is the canonical packet definition.
|
||||
|
||||
$ grep "dart/lib/src/packets/message_common.proto" PROTOCOL.md
|
||||
(no output)
|
||||
```
|
||||
|
||||
### PROTO_MOVE-4 중간 검증
|
||||
```
|
||||
$ ls dart/lib/src/packets/
|
||||
message_common.pb.dart
|
||||
message_common.pbenum.dart
|
||||
message_common.pbjson.dart
|
||||
message_common.pbserver.dart
|
||||
|
||||
$ cd dart && dart analyze
|
||||
Analyzing dart...
|
||||
No issues found!
|
||||
|
||||
$ cd dart && dart test
|
||||
00:15 +43: All tests passed!
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ tools/check_proto_sync.sh
|
||||
Proto schemas are in sync.
|
||||
|
||||
$ rg -n "Dart canonical|canonical Dart|dart/lib/src/packets/message_common\.proto" --glob '!agent-task/**'
|
||||
(no output)
|
||||
|
||||
$ cd dart && dart analyze
|
||||
Analyzing dart...
|
||||
No issues found!
|
||||
|
||||
$ cd dart && dart test
|
||||
00:15 +43: All tests passed!
|
||||
|
||||
$ cd go && PATH="/tmp/codex-go/go/bin:$PATH" GOPATH=/tmp/codex-gopath go build ./...
|
||||
(no output; exit 0)
|
||||
|
||||
$ cd kotlin && env JAVA_HOME=... ./gradlew build
|
||||
BUILD SUCCESSFUL in 15s
|
||||
```
|
||||
25
agent-task/proto_restructure/complete.log
Normal file
25
agent-task/proto_restructure/complete.log
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
task=proto_restructure
|
||||
date=2026-04-25
|
||||
result=PASS
|
||||
|
||||
## 완료된 작업 (plan=0)
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| PROTO_MOVE-1 | `proto/message_common.proto` 생성 (언어 옵션 없는 canonical). `tools/generate_proto.sh` Dart 섹션을 `--proto_path=proto --dart_out=dart/lib/src/packets/` 로 변경 |
|
||||
| PROTO_MOVE-2 | `tools/check_proto_sync.sh` canonical 변수를 `canonical_proto="$repo_root/proto/message_common.proto"` 로 변경, "Dart canonical" 문구 제거 |
|
||||
| PROTO_MOVE-3 | `PROTOCOL.md` "Proto Source" 섹션을 `proto/message_common.proto` 기준으로 갱신 |
|
||||
| PROTO_MOVE-4 | `dart/lib/src/packets/message_common.proto` 삭제 (`.pb.dart` 생성 결과물만 유지) |
|
||||
|
||||
## 계획 외 추가 변경
|
||||
|
||||
- `PORTING_GUIDE.md`, `README.md`, `typescript/README.md`의 구 canonical 경로 갱신
|
||||
- `agent-ops/rules/project/**`, 언어 템플릿의 proto 경로 참조 갱신
|
||||
|
||||
## 최종 상태
|
||||
|
||||
- `tools/check_proto_sync.sh`: Proto schemas are in sync.
|
||||
- 구 canonical 경로(`dart/lib/src/packets/message_common.proto`, "Dart canonical") 전역 잔존 없음
|
||||
- Dart: dart analyze No issues / dart test 43개 PASS
|
||||
- Go: go build ./... 성공
|
||||
- Kotlin: ./gradlew build BUILD SUCCESSFUL
|
||||
281
agent-task/proto_restructure/plan_0.log
Normal file
281
agent-task/proto_restructure/plan_0.log
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
<!-- task=proto_restructure plan=0 tag=PROTO_MOVE -->
|
||||
|
||||
# Proto 루트 디렉터리 독립화
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
각 항목의 체크리스트를 완료하면서 `[ ]`를 `[x]`로 표시하라.
|
||||
중간 검증 명령은 해당 항목 구현 직후 실행하고, 출력을 `CODE_REVIEW.md`의 검증 결과 섹션에 붙여넣어라.
|
||||
최종 검증도 마찬가지로 실행 후 출력을 기록하라.
|
||||
계획과 다르게 구현한 부분은 이유와 함께 `CODE_REVIEW.md`의 "계획 대비 변경 사항"에 기록하라.
|
||||
|
||||
⚠️ **이 태스크는 모든 언어의 빌드 경로를 변경한다. 각 검증을 반드시 실행한 후 다음 단계로 넘어가라.**
|
||||
|
||||
---
|
||||
|
||||
## 배경
|
||||
|
||||
현재 proto canonical source가 `dart/lib/src/packets/message_common.proto`에 위치한다.
|
||||
Dart는 5개 언어 구현체 중 하나이지만 동시에 스펙 원본의 저장소 역할을 하고 있어,
|
||||
새 기여자에게 "proto = Dart 전용"으로 오해될 수 있다.
|
||||
`proto/message_common.proto` 루트 디렉터리로 이동하면:
|
||||
|
||||
- proto가 특정 언어 종속성 없이 독립 소스임이 명확해진다
|
||||
- 새 언어 포팅 시 proto 위치가 자명해진다
|
||||
- `check_proto_sync.sh`의 "canonical Dart proto"라는 명칭이 사라진다
|
||||
|
||||
## 언어별 proto 현황
|
||||
|
||||
| 언어 | 현재 위치 | 방식 | 변경 여부 |
|
||||
|------|----------|------|---------|
|
||||
| Dart | `dart/lib/src/packets/message_common.proto` (canonical) | `protoc --dart_out` (generate_proto.sh) | 제거 후 생성 경로 변경 |
|
||||
| Go | `go/packets/message_common.proto` (go_package 옵션 포함 복사본) | `protoc --go_out` (generate_proto.sh) | 유지 (언어 옵션 필요), sync 비교 대상 변경 |
|
||||
| Kotlin | `kotlin/src/main/proto/message_common.proto` (java 옵션 포함 복사본) | Gradle protobuf 플러그인 | 유지 (언어 옵션 필요), sync 비교 대상 변경 |
|
||||
| TypeScript | `typescript/src/packets/message_common_pb.ts` (생성 결과물만, .proto 없음) | `protoc-gen-es` 수동 실행 | 영향 없음 |
|
||||
| Python | `python/toki_socket/packets/message_common_pb2.py` (생성 결과물만, .proto 없음) | `protoc --python_out` 수동 실행 | 영향 없음 |
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
```
|
||||
[PROTO_MOVE-1] proto/ 생성 + generate_proto.sh 수정
|
||||
→ [PROTO_MOVE-2] check_proto_sync.sh canonical 경로 변경
|
||||
→ [PROTO_MOVE-3] PROTOCOL.md 업데이트
|
||||
→ [PROTO_MOVE-4] dart/lib/src/packets/message_common.proto 제거
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### [PROTO_MOVE-1] `proto/` 디렉터리 생성 및 generate_proto.sh 수정
|
||||
|
||||
**문제**
|
||||
|
||||
`tools/generate_proto.sh`가 Dart canonical에서 직접 생성한다.
|
||||
|
||||
```bash
|
||||
# 현재
|
||||
cd "$repo_root/dart"
|
||||
protoc --dart_out=lib/src/packets lib/src/packets/message_common.proto
|
||||
|
||||
cd "$repo_root/go"
|
||||
protoc --go_out=. --go_opt=paths=source_relative packets/message_common.proto
|
||||
```
|
||||
|
||||
**해결 방법**
|
||||
|
||||
1. `proto/message_common.proto` 생성: `dart/lib/src/packets/message_common.proto`에서 언어 옵션 없이 그대로 복사 (현재 Dart canonical에는 언어 옵션이 없으므로 내용 동일).
|
||||
2. `generate_proto.sh`의 Dart 생성 경로를 `proto/`에서 `dart/lib/src/packets/`로 출력하도록 변경.
|
||||
3. Go 생성은 Go copy(`go/packets/message_common.proto`)에서 그대로 진행 (go_package 옵션 보존 필요).
|
||||
|
||||
Before (`generate_proto.sh` Dart 섹션):
|
||||
```bash
|
||||
(
|
||||
cd "$repo_root/dart"
|
||||
protoc --dart_out=lib/src/packets lib/src/packets/message_common.proto
|
||||
)
|
||||
```
|
||||
|
||||
After:
|
||||
```bash
|
||||
(
|
||||
protoc \
|
||||
--proto_path="$repo_root/proto" \
|
||||
--dart_out="$repo_root/dart/lib/src/packets" \
|
||||
message_common.proto
|
||||
)
|
||||
```
|
||||
|
||||
Go 섹션은 변경 없음 (Go는 자체 복사본에서 생성).
|
||||
|
||||
**수정 파일 및 체크리스트**
|
||||
|
||||
- [x] `proto/message_common.proto` 생성 (dart canonical과 동일 내용)
|
||||
- [x] `tools/generate_proto.sh` — Dart 생성 섹션 경로 변경
|
||||
|
||||
**테스트 없음** — generate_proto.sh 실행으로 검증
|
||||
|
||||
**중간 검증**
|
||||
|
||||
```bash
|
||||
# protoc가 있는 환경에서 실행
|
||||
tools/generate_proto.sh
|
||||
# 오류 없이 완료
|
||||
|
||||
# 생성 결과가 기존과 동일한지 확인
|
||||
cd dart && dart analyze
|
||||
# No issues found
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### [PROTO_MOVE-2] `check_proto_sync.sh` canonical 경로 변경
|
||||
|
||||
**문제**
|
||||
|
||||
`tools/check_proto_sync.sh`의 canonical이 `dart_proto` 변수로 Dart 경로를 가리킨다.
|
||||
|
||||
```bash
|
||||
dart_proto="$repo_root/dart/lib/src/packets/message_common.proto"
|
||||
```
|
||||
|
||||
**해결 방법**
|
||||
|
||||
변수명과 경로를 `proto/` 기준으로 변경한다.
|
||||
|
||||
Before:
|
||||
```bash
|
||||
dart_proto="$repo_root/dart/lib/src/packets/message_common.proto"
|
||||
go_proto="$repo_root/go/packets/message_common.proto"
|
||||
kotlin_proto="$repo_root/kotlin/src/main/proto/message_common.proto"
|
||||
|
||||
if [[ ! -f "$dart_proto" ]]; then
|
||||
echo "Missing canonical Dart proto: $dart_proto" >&2
|
||||
exit 1
|
||||
fi
|
||||
# ...
|
||||
normalize_proto "$dart_proto" >"$tmp_dir/dart.proto"
|
||||
normalize_proto "$go_proto" >"$tmp_dir/go.proto"
|
||||
# ...
|
||||
if ! cmp -s "$tmp_dir/dart.proto" "$tmp_dir/go.proto"; then
|
||||
echo "Proto schema mismatch: go/packets/message_common.proto must match the Dart canonical proto ..."
|
||||
# ...
|
||||
```
|
||||
|
||||
After:
|
||||
```bash
|
||||
canonical_proto="$repo_root/proto/message_common.proto"
|
||||
go_proto="$repo_root/go/packets/message_common.proto"
|
||||
kotlin_proto="$repo_root/kotlin/src/main/proto/message_common.proto"
|
||||
|
||||
if [[ ! -f "$canonical_proto" ]]; then
|
||||
echo "Missing canonical proto: $canonical_proto" >&2
|
||||
exit 1
|
||||
fi
|
||||
# ...
|
||||
normalize_proto "$canonical_proto" >"$tmp_dir/canonical.proto"
|
||||
normalize_proto "$go_proto" >"$tmp_dir/go.proto"
|
||||
# ...
|
||||
if ! cmp -s "$tmp_dir/canonical.proto" "$tmp_dir/go.proto"; then
|
||||
echo "Proto schema mismatch: go/packets/message_common.proto must match proto/message_common.proto ..."
|
||||
# ...
|
||||
```
|
||||
|
||||
**수정 파일 및 체크리스트**
|
||||
|
||||
- [x] `tools/check_proto_sync.sh` — `dart_proto` → `canonical_proto`, 경로를 `proto/message_common.proto`로 변경
|
||||
- [x] `tools/check_proto_sync.sh` — 오류 메시지에서 "Dart canonical" 문구 제거
|
||||
- [x] `tools/check_proto_sync.sh` — normalize 비교 대상 변수명 일관성 수정
|
||||
|
||||
**중간 검증**
|
||||
|
||||
```bash
|
||||
tools/check_proto_sync.sh
|
||||
# Proto schemas are in sync.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### [PROTO_MOVE-3] `PROTOCOL.md` Proto Source 섹션 업데이트
|
||||
|
||||
**문제**
|
||||
|
||||
`PROTOCOL.md` 하단 "Proto Source" 섹션이 Dart 경로를 canonical로 명시한다.
|
||||
|
||||
```markdown
|
||||
`dart/lib/src/packets/message_common.proto` is the canonical packet definition.
|
||||
All language implementations must keep the same message schema and generate bindings from it.
|
||||
```
|
||||
|
||||
**해결 방법**
|
||||
|
||||
```markdown
|
||||
`proto/message_common.proto` is the canonical packet definition.
|
||||
All language implementations generate bindings from it.
|
||||
Language-specific generation options (go_package, java_package, etc.) are kept in each
|
||||
language's own proto copy under `go/packets/` and `kotlin/src/main/proto/`.
|
||||
```
|
||||
|
||||
**수정 파일 및 체크리스트**
|
||||
|
||||
- [x] `PROTOCOL.md` — "Proto Source" 섹션 경로 및 설명 업데이트
|
||||
|
||||
**테스트 없음** — 문서 전용
|
||||
|
||||
**중간 검증**
|
||||
|
||||
```bash
|
||||
grep "canonical" PROTOCOL.md
|
||||
# proto/message_common.proto 언급
|
||||
grep "dart/lib/src/packets/message_common.proto" PROTOCOL.md
|
||||
# 출력 없음이 정상
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### [PROTO_MOVE-4] `dart/lib/src/packets/message_common.proto` 제거
|
||||
|
||||
**문제**
|
||||
|
||||
PROTO_MOVE-1~3 완료 후 `dart/lib/src/packets/message_common.proto`는 더 이상 canonical이 아니다.
|
||||
그러나 여전히 파일로 남아 혼란을 줄 수 있다.
|
||||
|
||||
**해결 방법**
|
||||
|
||||
파일을 삭제한다. Dart 생성 결과물인 `.pb.dart` 파일들은 유지한다.
|
||||
|
||||
**수정 파일 및 체크리스트**
|
||||
|
||||
- [x] `dart/lib/src/packets/message_common.proto` 삭제
|
||||
- [x] `dart analyze` — proto 파일 제거 후 여전히 No issues인지 확인
|
||||
|
||||
**중간 검증**
|
||||
|
||||
```bash
|
||||
ls dart/lib/src/packets/
|
||||
# message_common.pb.dart, message_common.pbenum.dart 등 생성 결과물만 존재
|
||||
# message_common.proto 없음
|
||||
|
||||
cd dart && dart analyze
|
||||
# No issues found
|
||||
cd dart && dart test
|
||||
# 전체 PASS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `proto/message_common.proto` (신규) | PROTO_MOVE-1 |
|
||||
| `tools/generate_proto.sh` | PROTO_MOVE-1 |
|
||||
| `tools/check_proto_sync.sh` | PROTO_MOVE-2 |
|
||||
| `PROTOCOL.md` | PROTO_MOVE-3 |
|
||||
| `dart/lib/src/packets/message_common.proto` (삭제) | PROTO_MOVE-4 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
# proto sync 도구 검증
|
||||
tools/check_proto_sync.sh
|
||||
# Proto schemas are in sync.
|
||||
|
||||
# Dart
|
||||
cd dart && dart analyze
|
||||
# No issues found
|
||||
cd dart && dart test
|
||||
# 전체 PASS
|
||||
|
||||
# Go
|
||||
cd go && go build ./...
|
||||
# 빌드 성공
|
||||
|
||||
# Kotlin
|
||||
cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle ./gradlew build
|
||||
# BUILD SUCCESSFUL
|
||||
|
||||
# proto/ 파일 확인
|
||||
ls proto/
|
||||
# message_common.proto 존재
|
||||
ls dart/lib/src/packets/
|
||||
# *.pb.dart만 존재, message_common.proto 없음
|
||||
```
|
||||
|
|
@ -7,12 +7,16 @@ import 'package:toki_socket/toki_socket.dart';
|
|||
const _host = '127.0.0.1';
|
||||
const _tcpPort = 29090;
|
||||
const _wsPort = 29092;
|
||||
const _tlsTcpPort = 29094;
|
||||
const _wssPort = 29096;
|
||||
const _heartbeatIntervalSeconds = 30;
|
||||
const _heartbeatWaitSeconds = 10;
|
||||
const _serverObservationWindow = Duration(milliseconds: 200);
|
||||
const _processTimeout = Duration(seconds: 20);
|
||||
final _repoRoot = File.fromUri(Platform.script).parent.parent.parent.path;
|
||||
final _goDir = Directory('$_repoRoot/go').path;
|
||||
final _certFile = '$_repoRoot/dart/test/certs/server.crt';
|
||||
final _keyFile = '$_repoRoot/dart/test/certs/server.key';
|
||||
|
||||
class _DartTcpClient extends ProtobufClient {
|
||||
_DartTcpClient(Socket socket)
|
||||
|
|
@ -38,6 +42,16 @@ class _DartTcpServer extends ProtobufServer {
|
|||
void onClientConnected(ProtobufClient client) => onConnected(client);
|
||||
}
|
||||
|
||||
class _DartTlsTcpServer extends ProtobufServer {
|
||||
final void Function(ProtobufClient) onConnected;
|
||||
|
||||
_DartTlsTcpServer(int port, SecurityContext ctx, this.onConnected)
|
||||
: super.secure(_host, port, ctx, (socket) => _DartTcpClient(socket));
|
||||
|
||||
@override
|
||||
void onClientConnected(ProtobufClient client) => onConnected(client);
|
||||
}
|
||||
|
||||
class _DartWsServer extends WsProtobufServer {
|
||||
final void Function(WsProtobufClient) onConnected;
|
||||
|
||||
|
|
@ -48,12 +62,23 @@ class _DartWsServer extends WsProtobufServer {
|
|||
void onClientConnected(WsProtobufClient client) => onConnected(client);
|
||||
}
|
||||
|
||||
class _DartWssServer extends WsProtobufServer {
|
||||
final void Function(WsProtobufClient) onConnected;
|
||||
|
||||
_DartWssServer(int port, SecurityContext ctx, this.onConnected)
|
||||
: super.secure(_host, port, ctx, (ws) => _DartWsClient(ws));
|
||||
|
||||
@override
|
||||
void onClientConnected(WsProtobufClient client) => onConnected(client);
|
||||
}
|
||||
|
||||
Future<void> main() async {
|
||||
print(
|
||||
'INFO typeName dart=${TestData.getDefault().info_.qualifiedMessageName}');
|
||||
try {
|
||||
await _runTcp();
|
||||
await _runWs();
|
||||
await _runTls();
|
||||
print('PASS all dart-server/go-client crosstests passed');
|
||||
} catch (error) {
|
||||
stderr.writeln('FAIL crosstest error=$error');
|
||||
|
|
@ -169,17 +194,113 @@ Future<void> _withServer(Future<void> Function() start,
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _runTls() async {
|
||||
final ctx = SecurityContext()
|
||||
..useCertificateChain(_certFile)
|
||||
..usePrivateKey(_keyFile);
|
||||
await _runTlsTcpSendPush(ctx);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 150));
|
||||
await _runTlsTcpRequests(ctx);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 150));
|
||||
await _runWssSendPush(ctx);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 150));
|
||||
await _runWssRequests(ctx);
|
||||
}
|
||||
|
||||
Future<void> _runTlsTcpSendPush(SecurityContext ctx) async {
|
||||
final received = Completer<bool>();
|
||||
final server = _DartTlsTcpServer(_tlsTcpPort, ctx, (client) {
|
||||
client.addListener<TestData>((data) {
|
||||
final valid = data.index == 101 && data.message == 'fire from go client';
|
||||
if (!received.isCompleted) received.complete(valid);
|
||||
if (valid) {
|
||||
unawaited(client.send(TestData()
|
||||
..index = 200
|
||||
..message = 'push from dart server'));
|
||||
}
|
||||
});
|
||||
});
|
||||
await _withServer(server.start, server.stop, () async {
|
||||
await _runGoClient('tls', _tlsTcpPort, 'send-push', {'1', '2'},
|
||||
certFile: _certFile);
|
||||
final ok = await received.future
|
||||
.timeout(_serverObservationWindow, onTimeout: () => false);
|
||||
if (!ok) {
|
||||
throw StateError('TLS TCP send-push server did not receive expected data');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _runTlsTcpRequests(SecurityContext ctx) async {
|
||||
final server = _DartTlsTcpServer(_tlsTcpPort, ctx, (client) {
|
||||
client.addRequestListener<TestData, TestData>((req) async {
|
||||
return TestData()
|
||||
..index = req.index * 2
|
||||
..message = 'echo: ${req.message}';
|
||||
});
|
||||
});
|
||||
await _withServer(
|
||||
server.start,
|
||||
server.stop,
|
||||
() => _runGoClient('tls', _tlsTcpPort, 'requests', {'3', '4'},
|
||||
certFile: _certFile),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _runWssSendPush(SecurityContext ctx) async {
|
||||
final received = Completer<bool>();
|
||||
final server = _DartWssServer(_wssPort, ctx, (client) {
|
||||
client.addListener<TestData>((data) {
|
||||
final valid = data.index == 101 && data.message == 'fire from go client';
|
||||
if (!received.isCompleted) received.complete(valid);
|
||||
if (valid) {
|
||||
unawaited(client.send(TestData()
|
||||
..index = 200
|
||||
..message = 'push from dart server'));
|
||||
}
|
||||
});
|
||||
});
|
||||
await _withServer(server.start, server.stop, () async {
|
||||
await _runGoClient('wss', _wssPort, 'send-push', {'1', '2'},
|
||||
certFile: _certFile);
|
||||
final ok = await received.future
|
||||
.timeout(_serverObservationWindow, onTimeout: () => false);
|
||||
if (!ok) {
|
||||
throw StateError('WSS send-push server did not receive expected data');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _runWssRequests(SecurityContext ctx) async {
|
||||
final server = _DartWssServer(_wssPort, ctx, (client) {
|
||||
client.addRequestListener<TestData, TestData>((req) async {
|
||||
return TestData()
|
||||
..index = req.index * 2
|
||||
..message = 'echo: ${req.message}';
|
||||
});
|
||||
});
|
||||
await _withServer(
|
||||
server.start,
|
||||
server.stop,
|
||||
() => _runGoClient('wss', _wssPort, 'requests', {'3', '4'},
|
||||
certFile: _certFile),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _runGoClient(
|
||||
String mode, int port, String phase, Set<String> expectedScenarios) async {
|
||||
String mode, int port, String phase, Set<String> expectedScenarios,
|
||||
{String? certFile}) async {
|
||||
final args = [
|
||||
'run',
|
||||
'./crosstest/dart_go_client',
|
||||
'--mode=$mode',
|
||||
'--port=$port',
|
||||
'--phase=$phase',
|
||||
if (certFile != null) '--cert=$certFile',
|
||||
];
|
||||
final process = await Process.start(
|
||||
'go',
|
||||
[
|
||||
'run',
|
||||
'./crosstest/dart_go_client',
|
||||
'--mode=$mode',
|
||||
'--port=$port',
|
||||
'--phase=$phase',
|
||||
],
|
||||
args,
|
||||
workingDirectory: _goDir,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ Future<void> main(List<String> args) async {
|
|||
final mode = _argValue(args, 'mode') ?? 'tcp';
|
||||
final phase = _argValue(args, 'phase') ?? 'send-push';
|
||||
final port = int.tryParse(_argValue(args, 'port') ?? '');
|
||||
final certPath = _argValue(args, 'cert');
|
||||
|
||||
print(
|
||||
'INFO typeName dart=${TestData.getDefault().info_.qualifiedMessageName}');
|
||||
|
|
@ -47,7 +48,7 @@ Future<void> main(List<String> args) async {
|
|||
|
||||
late _ClientHandle handle;
|
||||
try {
|
||||
handle = await _connectWithRetry(mode, port);
|
||||
handle = await _connectWithRetry(mode, port, certPath: certPath);
|
||||
} catch (error) {
|
||||
_fail('setup', error.toString());
|
||||
exitCode = 1;
|
||||
|
|
@ -76,7 +77,8 @@ Future<void> main(List<String> args) async {
|
|||
exit(exitCode);
|
||||
}
|
||||
|
||||
Future<_ClientHandle> _connectWithRetry(String mode, int port) async {
|
||||
Future<_ClientHandle> _connectWithRetry(String mode, int port,
|
||||
{String? certPath}) async {
|
||||
final deadline = DateTime.now().add(_connectWindow);
|
||||
Object? lastError;
|
||||
|
||||
|
|
@ -93,6 +95,20 @@ Future<_ClientHandle> _connectWithRetry(String mode, int port) async {
|
|||
.timeout(const Duration(milliseconds: 300));
|
||||
final client = _WsClient(ws);
|
||||
return _ClientHandle(client, client.close);
|
||||
case 'tls':
|
||||
final ctx = _buildClientSecurityContext(certPath, mode);
|
||||
final socket =
|
||||
await ProtobufClient.connectSecure(_host, port, context: ctx)
|
||||
.timeout(const Duration(milliseconds: 300));
|
||||
final client = _TcpClient(socket);
|
||||
return _ClientHandle(client, client.close);
|
||||
case 'wss':
|
||||
final ctx = _buildClientSecurityContext(certPath, mode);
|
||||
final ws = await WsProtobufClient.connectSecure(_host, port,
|
||||
path: _wsPath, context: ctx)
|
||||
.timeout(const Duration(milliseconds: 300));
|
||||
final client = _WsClient(ws);
|
||||
return _ClientHandle(client, client.close);
|
||||
default:
|
||||
throw ArgumentError('unknown mode "$mode"');
|
||||
}
|
||||
|
|
@ -106,6 +122,13 @@ Future<_ClientHandle> _connectWithRetry(String mode, int port) async {
|
|||
'connect $mode:$port timed out: $lastError', _connectWindow);
|
||||
}
|
||||
|
||||
SecurityContext _buildClientSecurityContext(String? certPath, String mode) {
|
||||
if (certPath == null || certPath.isEmpty) {
|
||||
throw ArgumentError('--cert is required for $mode mode');
|
||||
}
|
||||
return SecurityContext()..setTrustedCertificates(certPath);
|
||||
}
|
||||
|
||||
Future<bool> _runSendPush(Communicator client) async {
|
||||
final pushCompleter = Completer<TestData>();
|
||||
client.addListener<TestData>((data) {
|
||||
|
|
|
|||
|
|
@ -82,7 +82,8 @@ abstract class Communicator {
|
|||
/// ```
|
||||
Future<Res>
|
||||
sendRequest<Req extends GeneratedMessage, Res extends GeneratedMessage>(
|
||||
Req data) async {
|
||||
Req data,
|
||||
{Duration timeout = const Duration(seconds: 30)}) async {
|
||||
if (!isAlive) return Future.error(StateError('not connected'));
|
||||
final requestNonce = ++nonce;
|
||||
final completer = Completer<Res>();
|
||||
|
|
@ -104,7 +105,11 @@ abstract class Communicator {
|
|||
_pendingRequests.remove(requestNonce);
|
||||
completer.completeError(error, stackTrace);
|
||||
});
|
||||
return completer.future;
|
||||
return completer.future.timeout(timeout, onTimeout: () {
|
||||
_pendingRequests.remove(requestNonce);
|
||||
throw TimeoutException(
|
||||
'sendRequest timeout for nonce $requestNonce', timeout);
|
||||
});
|
||||
}
|
||||
|
||||
/// Registers a request handler for [Req] that returns [Res].
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:mirrors';
|
||||
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:toki_socket/toki_socket.dart';
|
||||
|
|
@ -47,6 +50,12 @@ class _FakeTransport implements Transport {
|
|||
Future<void> close() async {}
|
||||
}
|
||||
|
||||
Map<int, Object?> _pendingRequestsOf(Communicator communicator) {
|
||||
final library = reflectClass(Communicator).owner as LibraryMirror;
|
||||
final symbol = MirrorSystem.getSymbol('_pendingRequests', library);
|
||||
return reflect(communicator).getField(symbol).reflectee as Map<int, Object?>;
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('Communicator protocol guards', () {
|
||||
test('response typeName mismatch completes sendRequest with error',
|
||||
|
|
@ -96,6 +105,26 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('sendRequest가 timeout 내 응답 없으면 TimeoutException을 던진다', () async {
|
||||
final communicator = _FakeCommunicator();
|
||||
|
||||
final future = communicator.sendRequest<TestData, TestData>(
|
||||
TestData()
|
||||
..index = 1
|
||||
..message = 'wait',
|
||||
timeout: const Duration(milliseconds: 50),
|
||||
);
|
||||
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(communicator.transport.sentPackets, hasLength(1));
|
||||
|
||||
await expectLater(
|
||||
future,
|
||||
throwsA(isA<TimeoutException>()),
|
||||
);
|
||||
expect(_pendingRequestsOf(communicator), isEmpty);
|
||||
});
|
||||
|
||||
test(
|
||||
'cannot register addRequestListener for a type already using addListener',
|
||||
() {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
|
@ -37,9 +39,10 @@ func parserMap() toki.ParserMap {
|
|||
}
|
||||
|
||||
func main() {
|
||||
mode := flag.String("mode", "tcp", "transport mode: tcp or ws")
|
||||
mode := flag.String("mode", "tcp", "transport mode: tcp, ws, tls, or wss")
|
||||
port := flag.Int("port", 0, "server port")
|
||||
phase := flag.String("phase", "send-push", "test phase: send-push or requests")
|
||||
cert := flag.String("cert", "", "path to PEM certificate for TLS verification")
|
||||
flag.Parse()
|
||||
|
||||
fmt.Printf("INFO typeName go=%s\n", toki.TypeNameOf(&packets.TestData{}))
|
||||
|
|
@ -49,7 +52,7 @@ func main() {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
client, err := dialWithRetry(*mode, *port)
|
||||
client, err := dialWithRetry(*mode, *port, *cert)
|
||||
if err != nil {
|
||||
fail("setup", err.Error())
|
||||
os.Exit(1)
|
||||
|
|
@ -71,12 +74,24 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
func dialWithRetry(mode string, port int) (*clientHandle, error) {
|
||||
func buildClientTLS(certFile string) (*tls.Config, error) {
|
||||
certPEM, err := os.ReadFile(certFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read cert %s: %w", certFile, err)
|
||||
}
|
||||
pool := x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM(certPEM) {
|
||||
return nil, fmt.Errorf("no valid PEM certificate found in %s", certFile)
|
||||
}
|
||||
return &tls.Config{RootCAs: pool}, nil
|
||||
}
|
||||
|
||||
func dialWithRetry(mode string, port int, certFile string) (*clientHandle, error) {
|
||||
deadline := time.Now().Add(connectWindow)
|
||||
var lastErr error
|
||||
for time.Now().Before(deadline) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
|
||||
handle, err := dial(ctx, mode, port)
|
||||
handle, err := dial(ctx, mode, port, certFile)
|
||||
cancel()
|
||||
if err == nil {
|
||||
return handle, nil
|
||||
|
|
@ -87,7 +102,7 @@ func dialWithRetry(mode string, port int) (*clientHandle, error) {
|
|||
return nil, fmt.Errorf("connect %s:%d timed out: %w", mode, port, lastErr)
|
||||
}
|
||||
|
||||
func dial(ctx context.Context, mode string, port int) (*clientHandle, error) {
|
||||
func dial(ctx context.Context, mode string, port int, certFile string) (*clientHandle, error) {
|
||||
switch mode {
|
||||
case "tcp":
|
||||
client, err := toki.DialTcp(ctx, host, port, 0, 0, parserMap())
|
||||
|
|
@ -109,6 +124,40 @@ func dial(ctx context.Context, mode string, port int) (*clientHandle, error) {
|
|||
send: client.Send,
|
||||
close: client.Close,
|
||||
}, nil
|
||||
case "tls":
|
||||
if certFile == "" {
|
||||
return nil, fmt.Errorf("--cert is required for tls mode")
|
||||
}
|
||||
tlsCfg, err := buildClientTLS(certFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, err := toki.DialTcpTLS(ctx, host, port, tlsCfg, 0, 0, parserMap())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &clientHandle{
|
||||
communicator: &client.Communicator,
|
||||
send: client.Send,
|
||||
close: client.Close,
|
||||
}, nil
|
||||
case "wss":
|
||||
if certFile == "" {
|
||||
return nil, fmt.Errorf("--cert is required for wss mode")
|
||||
}
|
||||
tlsCfg, err := buildClientTLS(certFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, err := toki.DialWssWithHeartbeat(ctx, host, port, wsPath, tlsCfg, 0, 0, parserMap())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &clientHandle{
|
||||
communicator: &client.Communicator,
|
||||
send: client.Send,
|
||||
close: client.Close,
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown mode %q", mode)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ package main
|
|||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
|
@ -28,6 +29,8 @@ const (
|
|||
host = "127.0.0.1"
|
||||
goDartTCPPort = 29190
|
||||
goDartWSPort = 29192
|
||||
goDartTLSPort = 29194
|
||||
goDartWSSPort = 29196
|
||||
wsPath = "/"
|
||||
processTimeout = 20 * time.Second
|
||||
serverObservationWindow = 200 * time.Millisecond
|
||||
|
|
@ -64,7 +67,43 @@ func run() error {
|
|||
return err
|
||||
}
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
return runWSRequests()
|
||||
if err := runWSRequests(); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
|
||||
dartDir, err := dartPackageDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot find dart package dir: %w", err)
|
||||
}
|
||||
certFile := filepath.Join(dartDir, "test", "certs", "server.crt")
|
||||
keyFile := filepath.Join(dartDir, "test", "certs", "server.key")
|
||||
serverTLS, err := loadServerTLS(certFile, keyFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load TLS certs: %w", err)
|
||||
}
|
||||
|
||||
if err := runTLSTCPSendPush(serverTLS, certFile); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
if err := runTLSTCPRequests(serverTLS, certFile); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
if err := runWSSendPushSecure(serverTLS, certFile); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
return runWSSRequestsSecure(serverTLS, certFile)
|
||||
}
|
||||
|
||||
func loadServerTLS(certFile, keyFile string) (*tls.Config, error) {
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tls.Config{Certificates: []tls.Certificate{cert}}, nil
|
||||
}
|
||||
|
||||
func runTCPSendPush() error {
|
||||
|
|
@ -197,7 +236,133 @@ func runWSRequests() error {
|
|||
return runDartClient("ws", goDartWSPort, "requests", map[string]bool{"3": true, "4": true})
|
||||
}
|
||||
|
||||
func runDartClient(mode string, port int, phase string, expected map[string]bool) error {
|
||||
func runTLSTCPSendPush(serverTLS *tls.Config, certFile string) error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
received := make(chan bool, 1)
|
||||
server := toki.NewTcpServerTLS(host, goDartTLSPort, serverTLS, func(conn net.Conn) *toki.TcpClient {
|
||||
return toki.NewTcpClient(conn, 0, 0, parserMap())
|
||||
})
|
||||
server.OnClientConnected = func(client *toki.TcpClient) {
|
||||
toki.AddListenerTyped[*packets.TestData](&client.Communicator, func(data *packets.TestData) {
|
||||
fmt.Printf("SERVER_RECEIVED index=%d message=%s\n", data.GetIndex(), data.GetMessage())
|
||||
valid := data.GetIndex() == 101 && data.GetMessage() == "fire from dart client"
|
||||
select {
|
||||
case received <- valid:
|
||||
default:
|
||||
}
|
||||
if valid {
|
||||
_ = client.Send(&packets.TestData{Index: 200, Message: "push from go server"})
|
||||
}
|
||||
})
|
||||
}
|
||||
if err := server.Start(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
defer server.Stop()
|
||||
|
||||
if err := runDartClient("tls", goDartTLSPort, "send-push", map[string]bool{"1": true, "2": true}, certFile); err != nil {
|
||||
return err
|
||||
}
|
||||
select {
|
||||
case ok := <-received:
|
||||
if !ok {
|
||||
return errors.New("TLS TCP send-push server received unexpected data")
|
||||
}
|
||||
case <-time.After(serverObservationWindow):
|
||||
return errors.New("TLS TCP send-push server did not receive expected data")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runTLSTCPRequests(serverTLS *tls.Config, certFile string) error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
server := toki.NewTcpServerTLS(host, goDartTLSPort, serverTLS, func(conn net.Conn) *toki.TcpClient {
|
||||
return toki.NewTcpClient(conn, 0, 0, parserMap())
|
||||
})
|
||||
server.OnClientConnected = func(client *toki.TcpClient) {
|
||||
toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](&client.Communicator, func(req *packets.TestData) (*packets.TestData, error) {
|
||||
return &packets.TestData{
|
||||
Index: req.GetIndex() * 2,
|
||||
Message: "echo: " + req.GetMessage(),
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
if err := server.Start(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
defer server.Stop()
|
||||
|
||||
return runDartClient("tls", goDartTLSPort, "requests", map[string]bool{"3": true, "4": true}, certFile)
|
||||
}
|
||||
|
||||
func runWSSendPushSecure(serverTLS *tls.Config, certFile string) error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
received := make(chan bool, 1)
|
||||
server := toki.NewWsServerTLS(host, goDartWSSPort, wsPath, serverTLS, func(conn *websocket.Conn) *toki.WsClient {
|
||||
return toki.NewWsClient(conn, 0, 0, parserMap())
|
||||
})
|
||||
server.OnClientConnected = func(client *toki.WsClient) {
|
||||
toki.AddListenerTyped[*packets.TestData](&client.Communicator, func(data *packets.TestData) {
|
||||
fmt.Printf("SERVER_RECEIVED index=%d message=%s\n", data.GetIndex(), data.GetMessage())
|
||||
valid := data.GetIndex() == 101 && data.GetMessage() == "fire from dart client"
|
||||
select {
|
||||
case received <- valid:
|
||||
default:
|
||||
}
|
||||
if valid {
|
||||
_ = client.Send(&packets.TestData{Index: 200, Message: "push from go server"})
|
||||
}
|
||||
})
|
||||
}
|
||||
if err := server.Start(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
defer server.Stop()
|
||||
|
||||
if err := runDartClient("wss", goDartWSSPort, "send-push", map[string]bool{"1": true, "2": true}, certFile); err != nil {
|
||||
return err
|
||||
}
|
||||
select {
|
||||
case ok := <-received:
|
||||
if !ok {
|
||||
return errors.New("WSS send-push server received unexpected data")
|
||||
}
|
||||
case <-time.After(serverObservationWindow):
|
||||
return errors.New("WSS send-push server did not receive expected data")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runWSSRequestsSecure(serverTLS *tls.Config, certFile string) error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
server := toki.NewWsServerTLS(host, goDartWSSPort, wsPath, serverTLS, func(conn *websocket.Conn) *toki.WsClient {
|
||||
return toki.NewWsClient(conn, 0, 0, parserMap())
|
||||
})
|
||||
server.OnClientConnected = func(client *toki.WsClient) {
|
||||
toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](&client.Communicator, func(req *packets.TestData) (*packets.TestData, error) {
|
||||
return &packets.TestData{
|
||||
Index: req.GetIndex() * 2,
|
||||
Message: "echo: " + req.GetMessage(),
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
if err := server.Start(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
defer server.Stop()
|
||||
|
||||
return runDartClient("wss", goDartWSSPort, "requests", map[string]bool{"3": true, "4": true}, certFile)
|
||||
}
|
||||
|
||||
func runDartClient(mode string, port int, phase string, expected map[string]bool, certFile ...string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), processTimeout)
|
||||
defer cancel()
|
||||
|
||||
|
|
@ -206,11 +371,16 @@ func runDartClient(mode string, port int, phase string, expected map[string]bool
|
|||
return err
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "dart", "run", "crosstest/go_dart_client.dart",
|
||||
"--mode="+mode,
|
||||
args := []string{
|
||||
"run", "crosstest/go_dart_client.dart",
|
||||
"--mode=" + mode,
|
||||
fmt.Sprintf("--port=%d", port),
|
||||
"--phase="+phase,
|
||||
)
|
||||
"--phase=" + phase,
|
||||
}
|
||||
if len(certFile) > 0 && certFile[0] != "" {
|
||||
args = append(args, "--cert="+certFile[0])
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, "dart", args...)
|
||||
cmd.Dir = dartDir
|
||||
|
||||
stdoutPipe, err := cmd.StdoutPipe()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ package toki_socket_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -145,6 +147,66 @@ func TestTcpClientCloseIdempotent(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestTcpConcurrentRequests(t *testing.T) {
|
||||
port := freePort(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
server := toki.NewTcpServer("127.0.0.1", port, func(conn net.Conn) *toki.TcpClient {
|
||||
return toki.NewTcpClient(conn, 0, 0, testParserMap())
|
||||
})
|
||||
server.OnClientConnected = func(client *toki.TcpClient) {
|
||||
toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](&client.Communicator, func(req *packets.TestData) (*packets.TestData, error) {
|
||||
return &packets.TestData{
|
||||
Index: req.GetIndex() * 2,
|
||||
Message: "echo: " + req.GetMessage(),
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
if err := server.Start(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer server.Stop()
|
||||
|
||||
client, err := toki.DialTcp(ctx, "127.0.0.1", port, 0, 0, testParserMap())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
const count = 5
|
||||
var wg sync.WaitGroup
|
||||
errCh := make(chan error, count)
|
||||
for i := 0; i < count; i++ {
|
||||
i := i
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
index := int32(30 + i)
|
||||
message := fmt.Sprintf("request %d", i)
|
||||
res, err := toki.SendRequestTyped[*packets.TestData, *packets.TestData](
|
||||
&client.Communicator,
|
||||
&packets.TestData{Index: index, Message: message},
|
||||
2*time.Second,
|
||||
)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
if res.GetIndex() != index*2 || res.GetMessage() != "echo: "+message {
|
||||
errCh <- fmt.Errorf("request %d got index=%d message=%q", i, res.GetIndex(), res.GetMessage())
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
close(errCh)
|
||||
for err := range errCh {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTcpSendReceive(t *testing.T) {
|
||||
port := freePort(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package toki_socket_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -51,6 +53,66 @@ func TestWsRequestResponse(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestWsConcurrentRequests(t *testing.T) {
|
||||
port := freePort(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
server := toki.NewWsServer("127.0.0.1", port, "/", func(conn *websocket.Conn) *toki.WsClient {
|
||||
return toki.NewWsClient(conn, 0, 0, testParserMap())
|
||||
})
|
||||
server.OnClientConnected = func(client *toki.WsClient) {
|
||||
toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](&client.Communicator, func(req *packets.TestData) (*packets.TestData, error) {
|
||||
return &packets.TestData{
|
||||
Index: req.GetIndex() * 2,
|
||||
Message: "echo: " + req.GetMessage(),
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
if err := server.Start(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer server.Stop()
|
||||
|
||||
client, err := toki.DialWsWithHeartbeat(ctx, "127.0.0.1", port, "/", 0, 0, testParserMap())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
const count = 5
|
||||
var wg sync.WaitGroup
|
||||
errCh := make(chan error, count)
|
||||
for i := 0; i < count; i++ {
|
||||
i := i
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
index := int32(30 + i)
|
||||
message := fmt.Sprintf("request %d", i)
|
||||
res, err := toki.SendRequestTyped[*packets.TestData, *packets.TestData](
|
||||
&client.Communicator,
|
||||
&packets.TestData{Index: index, Message: message},
|
||||
2*time.Second,
|
||||
)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
if res.GetIndex() != index*2 || res.GetMessage() != "echo: "+message {
|
||||
errCh <- fmt.Errorf("request %d got index=%d message=%q", i, res.GetIndex(), res.GetMessage())
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
close(errCh)
|
||||
for err := range errCh {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWsSendReceive(t *testing.T) {
|
||||
port := freePort(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.tokilabs.toki_socket.packets.TestData
|
|||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.net.Socket
|
||||
import kotlin.test.Test
|
||||
|
|
@ -15,15 +16,18 @@ class TcpTest {
|
|||
fun testTcpSendReceive() = runBlocking {
|
||||
val port = freePort()
|
||||
val received = CompletableDeferred<TestData>()
|
||||
val listenerReady = CompletableDeferred<Unit>()
|
||||
val server = TcpServer("127.0.0.1", port) { socket ->
|
||||
TcpClient.fromSocket(socket, 0, 0, testParserMap())
|
||||
}
|
||||
server.onClientConnected = { client ->
|
||||
addListenerTyped<TestData>(client.communicator) { received.complete(it) }
|
||||
listenerReady.complete(Unit)
|
||||
}
|
||||
server.start()
|
||||
val client = DialTcp("127.0.0.1", port, 0, 0, testParserMap())
|
||||
|
||||
listenerReady.await()
|
||||
client.send(TestData.newBuilder().setIndex(42).setMessage("hello toki-socket").build())
|
||||
|
||||
assertEquals(42, received.await().index)
|
||||
|
|
@ -97,6 +101,47 @@ class TcpTest {
|
|||
assertFalse(client.communicator.isAlive())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTcpConcurrentRequests() = runBlocking {
|
||||
val port = freePort()
|
||||
val handlerReady = CompletableDeferred<Unit>()
|
||||
val server = TcpServer("127.0.0.1", port) { socket ->
|
||||
TcpClient.fromSocket(socket, 0, 0, testParserMap())
|
||||
}
|
||||
server.onClientConnected = { client ->
|
||||
addRequestListenerTyped<TestData, TestData>(client.communicator) { req ->
|
||||
TestData.newBuilder()
|
||||
.setIndex(req.index * 2)
|
||||
.setMessage("echo: ${req.message}")
|
||||
.build()
|
||||
}
|
||||
handlerReady.complete(Unit)
|
||||
}
|
||||
server.start()
|
||||
val client = DialTcp("127.0.0.1", port, 0, 0, testParserMap())
|
||||
|
||||
handlerReady.await()
|
||||
val deferred = (0 until 5).map { i ->
|
||||
async {
|
||||
val index = 30 + i
|
||||
val message = "request $i"
|
||||
val res = sendRequestTyped<TestData, TestData>(
|
||||
client.communicator,
|
||||
TestData.newBuilder().setIndex(index).setMessage(message).build(),
|
||||
timeoutMs = 2_000,
|
||||
)
|
||||
assertEquals(index * 2, res.index)
|
||||
assertEquals("echo: $message", res.message)
|
||||
}
|
||||
}
|
||||
try {
|
||||
deferred.awaitAll()
|
||||
} finally {
|
||||
client.close()
|
||||
server.stop()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTcpClientCloseIdempotent() = runBlocking {
|
||||
val serverSocket = java.net.ServerSocket(0)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package com.tokilabs.toki_socket
|
|||
|
||||
import com.tokilabs.toki_socket.packets.TestData
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
|
@ -12,15 +14,18 @@ class WsTest {
|
|||
fun testWsSendReceive() = runBlocking {
|
||||
val port = freePort()
|
||||
val received = CompletableDeferred<TestData>()
|
||||
val listenerReady = CompletableDeferred<Unit>()
|
||||
val server = WsServer("127.0.0.1", port, "/") { conn ->
|
||||
WsClient.forServer(conn, 0, 0, testParserMap())
|
||||
}
|
||||
server.onClientConnected = { client ->
|
||||
addListenerTyped<TestData>(client.communicator) { received.complete(it) }
|
||||
listenerReady.complete(Unit)
|
||||
}
|
||||
server.start()
|
||||
val client = DialWs("127.0.0.1", port, "/", 0, 0, testParserMap())
|
||||
|
||||
listenerReady.await()
|
||||
client.send(TestData.newBuilder().setIndex(42).setMessage("hello toki-socket ws").build())
|
||||
|
||||
assertEquals(42, received.await().index)
|
||||
|
|
@ -81,6 +86,47 @@ class WsTest {
|
|||
server.stop()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWsConcurrentRequests() = runBlocking {
|
||||
val port = freePort()
|
||||
val handlerReady = CompletableDeferred<Unit>()
|
||||
val server = WsServer("127.0.0.1", port, "/") { conn ->
|
||||
WsClient.forServer(conn, 0, 0, testParserMap())
|
||||
}
|
||||
server.onClientConnected = { client ->
|
||||
addRequestListenerTyped<TestData, TestData>(client.communicator) { req ->
|
||||
TestData.newBuilder()
|
||||
.setIndex(req.index * 2)
|
||||
.setMessage("echo: ${req.message}")
|
||||
.build()
|
||||
}
|
||||
handlerReady.complete(Unit)
|
||||
}
|
||||
server.start()
|
||||
val client = DialWs("127.0.0.1", port, "/", 0, 0, testParserMap())
|
||||
|
||||
handlerReady.await()
|
||||
val deferred = (0 until 5).map { i ->
|
||||
async {
|
||||
val index = 30 + i
|
||||
val message = "request $i"
|
||||
val res = sendRequestTyped<TestData, TestData>(
|
||||
client.communicator,
|
||||
TestData.newBuilder().setIndex(index).setMessage(message).build(),
|
||||
timeoutMs = 2_000,
|
||||
)
|
||||
assertEquals(index * 2, res.index)
|
||||
assertEquals("echo: $message", res.message)
|
||||
}
|
||||
}
|
||||
try {
|
||||
deferred.awaitAll()
|
||||
} finally {
|
||||
client.close()
|
||||
server.stop()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWsServerStopDisconnectsClients() = runBlocking {
|
||||
val port = freePort()
|
||||
|
|
|
|||
|
|
@ -10,6 +10,16 @@ from toki_socket.tcp_client import TcpClient, connect_tcp
|
|||
from toki_socket.tcp_server import TcpServer
|
||||
|
||||
|
||||
async def _wait_for_clients(server: TcpServer, count: int = 1, timeout: float = 1.0) -> None:
|
||||
loop = asyncio.get_running_loop()
|
||||
deadline = loop.time() + timeout
|
||||
while loop.time() < deadline:
|
||||
if len(server.clients()) >= count:
|
||||
return
|
||||
await asyncio.sleep(0.01)
|
||||
raise TimeoutError("server did not see expected client count")
|
||||
|
||||
|
||||
def parser_map():
|
||||
return {type_name_of(ProtoTestData): ProtoTestData.FromString}
|
||||
|
||||
|
|
@ -64,3 +74,59 @@ async def test_tcp_request_response():
|
|||
|
||||
await client.close()
|
||||
await server.stop()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tcp_server_push():
|
||||
pushed = asyncio.get_running_loop().create_future()
|
||||
server = TcpServer(
|
||||
"127.0.0.1",
|
||||
0,
|
||||
lambda reader, writer: TcpClient(reader, writer, 0, 0, parser_map()),
|
||||
)
|
||||
await server.start()
|
||||
client = await connect_tcp("127.0.0.1", server.port, 0, 0, parser_map())
|
||||
client.communicator.add_listener(
|
||||
type_name_of(ProtoTestData),
|
||||
lambda msg: pushed.set_result(msg) if not pushed.done() else None,
|
||||
)
|
||||
|
||||
await _wait_for_clients(server)
|
||||
await server.broadcast(ProtoTestData(index=200, message="push from python server"))
|
||||
result = await asyncio.wait_for(pushed, 2)
|
||||
|
||||
assert result.index == 200
|
||||
assert result.message == "push from python server"
|
||||
|
||||
await client.close()
|
||||
await server.stop()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tcp_concurrent_requests():
|
||||
server = TcpServer(
|
||||
"127.0.0.1",
|
||||
0,
|
||||
lambda reader, writer: TcpClient(reader, writer, 0, 0, parser_map()),
|
||||
)
|
||||
server.on_client_connected = lambda client: client.communicator.add_request_listener(
|
||||
type_name_of(ProtoTestData),
|
||||
lambda req: ProtoTestData(index=req.index * 2, message="echo: " + req.message),
|
||||
)
|
||||
await server.start()
|
||||
client = await connect_tcp("127.0.0.1", server.port, 0, 0, parser_map())
|
||||
|
||||
async def do_request(i: int) -> None:
|
||||
index = 30 + i
|
||||
message = f"request {i}"
|
||||
result = await client.communicator.send_request(
|
||||
ProtoTestData(index=index, message=message),
|
||||
ProtoTestData,
|
||||
timeout=2,
|
||||
)
|
||||
assert result.index == index * 2
|
||||
assert result.message == f"echo: {message}"
|
||||
|
||||
await asyncio.gather(*[do_request(i) for i in range(5)])
|
||||
await client.close()
|
||||
await server.stop()
|
||||
|
|
|
|||
136
python/test/test_ws.py
Normal file
136
python/test/test_ws.py
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from toki_socket.communicator import type_name_of
|
||||
from toki_socket.packets.message_common_pb2 import TestData as ProtoTestData
|
||||
from toki_socket.ws_client import WsClient, connect_ws
|
||||
from toki_socket.ws_server import WsServer
|
||||
|
||||
|
||||
def parser_map():
|
||||
return {type_name_of(ProtoTestData): ProtoTestData.FromString}
|
||||
|
||||
|
||||
async def _wait_for_clients(server: WsServer, count: int = 1, timeout: float = 1.0) -> None:
|
||||
loop = asyncio.get_running_loop()
|
||||
deadline = loop.time() + timeout
|
||||
while loop.time() < deadline:
|
||||
if len(server.clients()) >= count:
|
||||
return
|
||||
await asyncio.sleep(0.01)
|
||||
raise TimeoutError("server did not see expected client count")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ws_send_receive():
|
||||
received = asyncio.get_running_loop().create_future()
|
||||
server = WsServer(
|
||||
"127.0.0.1",
|
||||
0,
|
||||
"/",
|
||||
lambda ws: WsClient(ws, 0, 0, parser_map()),
|
||||
)
|
||||
server.on_client_connected = lambda client: client.communicator.add_listener(
|
||||
type_name_of(ProtoTestData),
|
||||
lambda message: received.set_result(message) if not received.done() else None,
|
||||
)
|
||||
await server.start()
|
||||
client = await connect_ws("127.0.0.1", server.port, "/", 0, 0, parser_map())
|
||||
|
||||
await client.communicator.send(ProtoTestData(index=1, message="hello ws"))
|
||||
result = await asyncio.wait_for(received, 2)
|
||||
|
||||
assert result.index == 1
|
||||
assert result.message == "hello ws"
|
||||
|
||||
await client.close()
|
||||
await server.stop()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ws_server_push():
|
||||
pushed = asyncio.get_running_loop().create_future()
|
||||
server = WsServer(
|
||||
"127.0.0.1",
|
||||
0,
|
||||
"/",
|
||||
lambda ws: WsClient(ws, 0, 0, parser_map()),
|
||||
)
|
||||
await server.start()
|
||||
client = await connect_ws("127.0.0.1", server.port, "/", 0, 0, parser_map())
|
||||
client.communicator.add_listener(
|
||||
type_name_of(ProtoTestData),
|
||||
lambda msg: pushed.set_result(msg) if not pushed.done() else None,
|
||||
)
|
||||
|
||||
await _wait_for_clients(server)
|
||||
await server.broadcast(ProtoTestData(index=200, message="push from python ws server"))
|
||||
result = await asyncio.wait_for(pushed, 2)
|
||||
|
||||
assert result.index == 200
|
||||
assert result.message == "push from python ws server"
|
||||
|
||||
await client.close()
|
||||
await server.stop()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ws_request_response():
|
||||
server = WsServer(
|
||||
"127.0.0.1",
|
||||
0,
|
||||
"/",
|
||||
lambda ws: WsClient(ws, 0, 0, parser_map()),
|
||||
)
|
||||
server.on_client_connected = lambda client: client.communicator.add_request_listener(
|
||||
type_name_of(ProtoTestData),
|
||||
lambda req: ProtoTestData(index=req.index * 2, message="echo: " + req.message),
|
||||
)
|
||||
await server.start()
|
||||
client = await connect_ws("127.0.0.1", server.port, "/", 0, 0, parser_map())
|
||||
|
||||
result = await client.communicator.send_request(
|
||||
ProtoTestData(index=21, message="ws request"),
|
||||
ProtoTestData,
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
assert result.index == 42
|
||||
assert result.message == "echo: ws request"
|
||||
|
||||
await client.close()
|
||||
await server.stop()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ws_concurrent_requests():
|
||||
server = WsServer(
|
||||
"127.0.0.1",
|
||||
0,
|
||||
"/",
|
||||
lambda ws: WsClient(ws, 0, 0, parser_map()),
|
||||
)
|
||||
server.on_client_connected = lambda client: client.communicator.add_request_listener(
|
||||
type_name_of(ProtoTestData),
|
||||
lambda req: ProtoTestData(index=req.index * 2, message="echo: " + req.message),
|
||||
)
|
||||
await server.start()
|
||||
client = await connect_ws("127.0.0.1", server.port, "/", 0, 0, parser_map())
|
||||
|
||||
async def do_request(i: int) -> None:
|
||||
index = 30 + i
|
||||
message = f"request {i}"
|
||||
result = await client.communicator.send_request(
|
||||
ProtoTestData(index=index, message=message),
|
||||
ProtoTestData,
|
||||
timeout=2,
|
||||
)
|
||||
assert result.index == index * 2
|
||||
assert result.message == f"echo: {message}"
|
||||
|
||||
await asyncio.gather(*[do_request(i) for i in range(5)])
|
||||
await client.close()
|
||||
await server.stop()
|
||||
|
|
@ -2,12 +2,12 @@
|
|||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
dart_proto="$repo_root/dart/lib/src/packets/message_common.proto"
|
||||
canonical_proto="$repo_root/proto/message_common.proto"
|
||||
go_proto="$repo_root/go/packets/message_common.proto"
|
||||
kotlin_proto="$repo_root/kotlin/src/main/proto/message_common.proto"
|
||||
|
||||
if [[ ! -f "$dart_proto" ]]; then
|
||||
echo "Missing canonical Dart proto: $dart_proto" >&2
|
||||
if [[ ! -f "$canonical_proto" ]]; then
|
||||
echo "Missing canonical proto: $canonical_proto" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -35,21 +35,21 @@ normalize_proto() {
|
|||
' "$1"
|
||||
}
|
||||
|
||||
normalize_proto "$dart_proto" >"$tmp_dir/dart.proto"
|
||||
normalize_proto "$canonical_proto" >"$tmp_dir/canonical.proto"
|
||||
normalize_proto "$go_proto" >"$tmp_dir/go.proto"
|
||||
if [[ -f "$kotlin_proto" ]]; then
|
||||
normalize_proto "$kotlin_proto" >"$tmp_dir/kotlin.proto"
|
||||
fi
|
||||
|
||||
if ! cmp -s "$tmp_dir/dart.proto" "$tmp_dir/go.proto"; then
|
||||
echo "Proto schema mismatch: go/packets/message_common.proto must match the Dart canonical proto except option go_package." >&2
|
||||
diff -u "$tmp_dir/dart.proto" "$tmp_dir/go.proto" >&2
|
||||
if ! cmp -s "$tmp_dir/canonical.proto" "$tmp_dir/go.proto"; then
|
||||
echo "Proto schema mismatch: go/packets/message_common.proto must match proto/message_common.proto except option go_package." >&2
|
||||
diff -u "$tmp_dir/canonical.proto" "$tmp_dir/go.proto" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -f "$tmp_dir/kotlin.proto" ]] && ! cmp -s "$tmp_dir/dart.proto" "$tmp_dir/kotlin.proto"; then
|
||||
echo "Proto schema mismatch: kotlin/src/main/proto/message_common.proto must match the Dart canonical proto except Java options." >&2
|
||||
diff -u "$tmp_dir/dart.proto" "$tmp_dir/kotlin.proto" >&2
|
||||
if [[ -f "$tmp_dir/kotlin.proto" ]] && ! cmp -s "$tmp_dir/canonical.proto" "$tmp_dir/kotlin.proto"; then
|
||||
echo "Proto schema mismatch: kotlin/src/main/proto/message_common.proto must match proto/message_common.proto except Java options." >&2
|
||||
diff -u "$tmp_dir/canonical.proto" "$tmp_dir/kotlin.proto" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -18,8 +18,10 @@ require_binary protoc-gen-dart "Install Dart plugin: dart pub global activate pr
|
|||
require_binary protoc-gen-go "Install Go plugin: go install google.golang.org/protobuf/cmd/protoc-gen-go@latest, then add GOPATH/bin to PATH."
|
||||
|
||||
(
|
||||
cd "$repo_root/dart"
|
||||
protoc --dart_out=lib/src/packets lib/src/packets/message_common.proto
|
||||
protoc \
|
||||
--proto_path="$repo_root/proto" \
|
||||
--dart_out="$repo_root/dart/lib/src/packets" \
|
||||
message_common.proto
|
||||
)
|
||||
|
||||
(
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ Generated bindings live in `src/packets/`.
|
|||
```bash
|
||||
cd typescript
|
||||
PATH="$PWD/node_modules/.bin:$PATH" protoc \
|
||||
--proto_path ../dart/lib/src/packets \
|
||||
--proto_path ../proto \
|
||||
--es_out src/packets \
|
||||
--es_opt target=ts \
|
||||
../dart/lib/src/packets/message_common.proto
|
||||
message_common.proto
|
||||
```
|
||||
|
||||
After generation, run from the repository root:
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ export function parserFromSchema<T extends Message>(schema: MessageType<T>): Mes
|
|||
}
|
||||
|
||||
export class Communicator {
|
||||
private static readonly MAX_WRITE_QUEUE_SIZE = 64;
|
||||
|
||||
private nonce = 0;
|
||||
private isAliveFlag = false;
|
||||
private parserMap: ParserMap = new Map();
|
||||
|
|
@ -60,6 +62,7 @@ export class Communicator {
|
|||
private reqHandlers = new Map<string, RequestHandler>();
|
||||
private pendingRequests = new Map<number, PendingRequest>();
|
||||
private writeQueue: QueueItem[] = [];
|
||||
private writeQueueWaiters: Array<() => void> = [];
|
||||
private writeQueueRunning = false;
|
||||
private transport: Transport | null = null;
|
||||
private writeErrorHandler: ((err: Error) => void) | null = null;
|
||||
|
|
@ -74,6 +77,7 @@ export class Communicator {
|
|||
this.reqHandlers = new Map();
|
||||
this.pendingRequests = new Map();
|
||||
this.writeQueue = [];
|
||||
this.writeQueueWaiters = [];
|
||||
this.writeQueueRunning = false;
|
||||
this.writeErrorHandler = null;
|
||||
this.nonce = 0;
|
||||
|
|
@ -127,6 +131,10 @@ export class Communicator {
|
|||
this.writeQueue.shift()?.reject(new NotConnectedError());
|
||||
}
|
||||
|
||||
while (this.writeQueueWaiters.length > 0) {
|
||||
this.writeQueueWaiters.shift()?.();
|
||||
}
|
||||
|
||||
this.finishClosed();
|
||||
}
|
||||
|
||||
|
|
@ -142,6 +150,15 @@ export class Communicator {
|
|||
throw new NotConnectedError();
|
||||
}
|
||||
|
||||
while (this.writeQueue.length >= Communicator.MAX_WRITE_QUEUE_SIZE) {
|
||||
await new Promise<void>((resolve) => {
|
||||
this.writeQueueWaiters.push(resolve);
|
||||
});
|
||||
if (!this.isAliveFlag || this.transport === null) {
|
||||
throw new NotConnectedError();
|
||||
}
|
||||
}
|
||||
|
||||
const promise = new Promise<void>((resolve, reject) => {
|
||||
this.writeQueue.push({ base, resolve, reject });
|
||||
});
|
||||
|
|
@ -355,6 +372,9 @@ export class Communicator {
|
|||
if (item === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (this.writeQueueWaiters.length > 0) {
|
||||
this.writeQueueWaiters.shift()?.();
|
||||
}
|
||||
if (!this.isAliveFlag || this.transport === null) {
|
||||
item.reject(new NotConnectedError());
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -23,10 +23,52 @@ class MockTransport implements Transport {
|
|||
async close(): Promise<void> {}
|
||||
}
|
||||
|
||||
class BlockingTransport implements Transport {
|
||||
packets: PacketBase[] = [];
|
||||
private writeResolvers: Array<() => void> = [];
|
||||
|
||||
async writePacket(base: PacketBase): Promise<void> {
|
||||
this.packets.push(base);
|
||||
await new Promise<void>((resolve) => {
|
||||
this.writeResolvers.push(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
releaseNext(): void {
|
||||
this.writeResolvers.shift()?.();
|
||||
}
|
||||
|
||||
releaseAll(): void {
|
||||
while (this.writeResolvers.length > 0) {
|
||||
this.releaseNext();
|
||||
}
|
||||
}
|
||||
|
||||
async close(): Promise<void> {}
|
||||
}
|
||||
|
||||
function parserMap() {
|
||||
return new Map([[TestDataSchema.typeName, parserFromSchema(TestDataSchema)]]);
|
||||
}
|
||||
|
||||
function packet(nonce: number): PacketBase {
|
||||
return create(PacketBaseSchema, {
|
||||
typeName: TestDataSchema.typeName,
|
||||
nonce,
|
||||
data: new Uint8Array(),
|
||||
});
|
||||
}
|
||||
|
||||
async function waitFor(predicate: () => boolean): Promise<void> {
|
||||
for (let i = 0; i < 20; i += 1) {
|
||||
if (predicate()) {
|
||||
return;
|
||||
}
|
||||
await Promise.resolve();
|
||||
}
|
||||
throw new Error("condition not met");
|
||||
}
|
||||
|
||||
describe("Communicator", () => {
|
||||
test("addListener/addRequestListener mutual exclusion", () => {
|
||||
const comm = new Communicator();
|
||||
|
|
@ -131,6 +173,53 @@ describe("Communicator", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test("write queue가 MAX_WRITE_QUEUE_SIZE 초과 시 대기 후 순서대로 처리된다", async () => {
|
||||
const transport = new BlockingTransport();
|
||||
const comm = new Communicator();
|
||||
comm.initialize(transport, parserMap());
|
||||
const state = comm as unknown as {
|
||||
writeQueue: unknown[];
|
||||
writeQueueWaiters: unknown[];
|
||||
};
|
||||
const promises: Array<Promise<unknown>> = [];
|
||||
|
||||
for (let nonce = 1; nonce <= 65; nonce += 1) {
|
||||
promises.push(
|
||||
comm.queuePacket(packet(nonce)).catch((err: unknown) => err),
|
||||
);
|
||||
}
|
||||
|
||||
expect(transport.packets.map((sentPacket) => sentPacket.nonce)).toEqual([
|
||||
1,
|
||||
]);
|
||||
expect(state.writeQueue).toHaveLength(64);
|
||||
expect(state.writeQueueWaiters).toHaveLength(0);
|
||||
|
||||
promises.push(comm.queuePacket(packet(66)).catch((err: unknown) => err));
|
||||
await waitFor(() => state.writeQueueWaiters.length === 1);
|
||||
|
||||
expect(transport.packets.map((sentPacket) => sentPacket.nonce)).toEqual([
|
||||
1,
|
||||
]);
|
||||
expect(state.writeQueue).toHaveLength(64);
|
||||
|
||||
transport.releaseNext();
|
||||
await waitFor(
|
||||
() =>
|
||||
transport.packets.length === 2 &&
|
||||
state.writeQueue.length === 64 &&
|
||||
state.writeQueueWaiters.length === 0,
|
||||
);
|
||||
|
||||
expect(transport.packets.map((sentPacket) => sentPacket.nonce)).toEqual([
|
||||
1, 2,
|
||||
]);
|
||||
|
||||
comm.shutdown();
|
||||
transport.releaseAll();
|
||||
await Promise.allSettled(promises);
|
||||
});
|
||||
|
||||
test("sendRequest stores protobuf packet payload", async () => {
|
||||
const transport = new MockTransport();
|
||||
const comm = new Communicator();
|
||||
|
|
|
|||
|
|
@ -89,4 +89,38 @@ describe("TCP", () => {
|
|||
message: "echo: request over tcp",
|
||||
});
|
||||
});
|
||||
|
||||
test("concurrent sendRequest/response roundtrip over TCP", async () => {
|
||||
const server = new TcpServer("127.0.0.1", 0, (socket) => new TcpClient(socket, 0, 0, parserMap()));
|
||||
cleanup.push(async () => server.stop());
|
||||
await server.start();
|
||||
|
||||
server.onClientConnected = (client) => {
|
||||
addRequestListenerTyped(client.communicator, TestDataSchema, (req) =>
|
||||
create(TestDataSchema, {
|
||||
index: req.index * 2,
|
||||
message: `echo: ${req.message}`,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const client = await connectTcp("127.0.0.1", server.port, 0, 0, parserMap());
|
||||
cleanup.push(async () => client.close());
|
||||
|
||||
const results = await Promise.all(
|
||||
Array.from({ length: 5 }, (_, i) =>
|
||||
sendRequestTyped(
|
||||
client.communicator,
|
||||
create(TestDataSchema, { index: 30 + i, message: `request ${i}` }),
|
||||
TestDataSchema,
|
||||
2000,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
results.forEach((res, i) => {
|
||||
expect(res.index).toBe((30 + i) * 2);
|
||||
expect(res.message).toBe(`echo: request ${i}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -89,4 +89,38 @@ describe("WS", () => {
|
|||
message: "echo: request over ws",
|
||||
});
|
||||
});
|
||||
|
||||
test("concurrent sendRequest/response roundtrip over WS", async () => {
|
||||
const server = new WsServer("127.0.0.1", 0, "/", (ws) => new WsClient(ws, 0, 0, parserMap()));
|
||||
cleanup.push(async () => server.stop());
|
||||
await server.start();
|
||||
|
||||
server.onClientConnected = (client) => {
|
||||
addRequestListenerTyped(client.communicator, TestDataSchema, (req) =>
|
||||
create(TestDataSchema, {
|
||||
index: req.index * 2,
|
||||
message: `echo: ${req.message}`,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const client = await connectWs("127.0.0.1", server.port, "/", 0, 0, parserMap());
|
||||
cleanup.push(async () => client.close());
|
||||
|
||||
const results = await Promise.all(
|
||||
Array.from({ length: 5 }, (_, i) =>
|
||||
sendRequestTyped(
|
||||
client.communicator,
|
||||
create(TestDataSchema, { index: 30 + i, message: `request ${i}` }),
|
||||
TestDataSchema,
|
||||
2000,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
results.forEach((res, i) => {
|
||||
expect(res.index).toBe((30 + i) * 2);
|
||||
expect(res.message).toBe(`echo: request ${i}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue