정리: 에이전트 작업 로그를 보관한다
This commit is contained in:
parent
3a645f5f14
commit
9872ebac87
89 changed files with 1817 additions and 2780 deletions
|
|
@ -1,101 +0,0 @@
|
|||
<!-- 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)
|
||||
```
|
||||
|
|
@ -1,225 +0,0 @@
|
|||
<!-- 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 (신규 큐 테스트 포함)
|
||||
```
|
||||
|
|
@ -0,0 +1,262 @@
|
|||
<!-- task=01+browser_native_ws plan=0 tag=BNW -->
|
||||
|
||||
# Code Review Reference - BNW
|
||||
|
||||
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> The task is NOT complete until every implementation-owned section below is filled in.
|
||||
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
|
||||
> Do not modify or check the `코드리뷰 전용 체크리스트`; it is owned by the review agent only.
|
||||
> Follow the ownership table at the bottom of this file for which sections you own.
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-05-20
|
||||
task=01+browser_native_ws, plan=0, tag=BNW
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
review 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW-cloud-G08.md` → `code_review_cloud_G08_N.log` (N = 기존 code_review_*.log 수)
|
||||
2. `PLAN-cloud-G08.md` → `plan_cloud_G08_M.log` (M = 기존 plan_*.log 수)
|
||||
3. PASS인 경우 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/01+browser_native_ws/`로 task 디렉터리 이동. WARN/FAIL인 경우 새 routed plan + review 스텁 작성.
|
||||
|
||||
어떤 판정에서도 아카이브를 건너뛰지 마세요. PASS/WARN/FAIL 모두 `코드리뷰 결과` append 후 active plan/review 파일을 먼저 아카이브하고, 그 다음 `complete.log` 또는 다음 plan/review 파일을 작성해야 합니다.
|
||||
PASS에서는 `agent-ops/skills/common/code-review/templates/complete-log-template.md`의 섹션 순서와 필수 항목을 기준으로 `complete.log`를 작성하세요. 작성 후 현재 날짜의 `YYYY/MM` 기준으로 task 디렉터리를 `agent-task/archive/YYYY/MM/01+browser_native_ws/`로 이동하고, 최종 archive 경로의 `code_review_*.log`에서 `코드리뷰 전용 체크리스트`를 갱신한 다음 보고하세요.
|
||||
WARN/FAIL에서는 다음 상태 파일 작성 후 현재 task 경로의 archived `code_review_*.log`에서 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 체크한 다음 보고하세요.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [BNW-1] 브라우저 native WebSocket client 구현 | [x] |
|
||||
| [BNW-2] Browser-safe root export와 Node 전용 entrypoint 분리 | [x] |
|
||||
| [BNW-3] 테스트와 crosstest 갱신 | [x] |
|
||||
| [BNW-4] Browser boundary 최종 guard | [x] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [x] [BNW-1] 브라우저 native WebSocket client를 Node 라이브러리 없이 구현한다.
|
||||
- [x] [BNW-2] TypeScript entrypoint와 dependency manifest를 browser-safe root와 Node 전용 entrypoint로 분리한다.
|
||||
- [x] [BNW-3] 브라우저 WS unit test, Node WS 회귀 테스트, crosstest, 정적 guard 검증을 추가/갱신한다.
|
||||
- [x] [BNW-4] 중간 검증과 최종 검증 명령을 실행하고 실제 출력을 review stub에 기록한다.
|
||||
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G08_N.log`로 아카이브한다.
|
||||
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G08_M.log`로 아카이브한다.
|
||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 `agent-task/01+browser_native_ws/`를 `agent-task/archive/YYYY/MM/01+browser_native_ws/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [x] WARN/FAIL이면 다음 active `PLAN-cloud-G08.md`와 `CODE_REVIEW-cloud-G08.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
- 계획의 `수정 파일 요약`에는 `typescript/test/ws.test.ts`, `typescript/crosstest/go_typescript_client.ts`, `typescript/crosstest/typescript_go.ts`만 import/name 갱신 대상으로 나열되어 있다. 그러나 `tsconfig.json`의 `include`가 모든 `crosstest/**/*.ts`를 포함하기 때문에 다른 crosstest 파일(`typescript_dart.ts`, `typescript_python.ts`, `typescript_kotlin.ts`, `dart_typescript_client.ts`, `python_typescript_client.ts`, `kotlin_typescript_client.ts`)도 동일한 import/name으로 갱신하지 않으면 `npm run check`가 실패한다. 따라서 동일 패턴으로 이 6개 파일도 같이 갱신했다.
|
||||
- BNW-4 정적 guard 패턴 `Buffer`는 browser-safe 코드가 사용하는 globally-scoped `ArrayBuffer` 식별자에도 매치된다. 이는 의도된 매치가 아니므로, 동일한 명령을 그대로 실행하면 항상 `ArrayBuffer` 라인이 출력된다. 해당 출력을 그대로 검증 결과에 기록했고, 진짜 의도(Node `Buffer` 식별자 검출)는 `\bBuffer\b` 패턴으로 재확인했다. 추가 검증 결과는 검증 섹션에 함께 기록했다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- Browser native client는 `globalThis.WebSocket`을 기본으로 사용하되 테스트 주입용 `WebSocketCtor?: typeof WebSocket`만 옵션으로 노출한다. `connectBrowserWs(url, ...)`는 `ws://`/`wss://` 모두 지원하므로 별도 wss 함수를 두지 않는다.
|
||||
- Node 전용 transport(`tcp_client`, `tcp_server`, `node_ws_client`, `node_ws_server`)는 `proto-socket/node` subpath로만 노출한다. root export(`proto-socket`)는 `base_client`, `communicator`, generated protobuf, `browser_ws_client`만 포함하여 번들러가 Node 모듈을 끌어오지 않도록 한다.
|
||||
- `ws`는 `dependencies`에서 제거하고 `devDependencies`로 이동했다. 브라우저 사용자는 `ws` 패키지를 설치할 필요가 없고, Node 사용자는 자신의 환경에 `ws`를 함께 두면 된다(`@types/ws`는 그대로 devDep 유지).
|
||||
- `WsClient`/`WsServer`/`connectWs`/`connectWss`는 Node 명칭으로 명확히 분리하기 위해 `NodeWsClient`/`NodeWsServer`/`connectNodeWs`/`connectNodeWss`로 이름을 바꾸고 파일을 `src/node_ws_client.ts`, `src/node_ws_server.ts`로 옮겼다. 기존 파일은 제거해 root export 경계가 흐려지는 것을 막았다.
|
||||
- `tsconfig.json`에 `lib: ["ES2022", "DOM"]`을 추가해 browser native WebSocket 타입을 사용 가능하게 했다. `types: ["node"]`는 Node 전용 파일 컴파일을 위해 유지했다.
|
||||
- Browser client는 `MessageEvent.data`로 `ArrayBuffer`, `Uint8Array`, `Blob`을 받아 `Uint8Array`로 정규화한다. text frame이나 알 수 없는 payload는 protocol 위반으로 처리해 즉시 disconnect 한다.
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- root/browser entrypoint가 `ws`, `node:*`, `Buffer`를 import하지 않는지 확인한다.
|
||||
- native browser client가 WebSocket binary frame에 `PacketBase` protobuf만 싣고 TCP length header를 추가하지 않는지 확인한다.
|
||||
- Node TCP/WS/server 기능은 browser root가 아니라 Node 전용 entrypoint로만 노출되는지 확인한다.
|
||||
- browser unit test가 fake native WebSocket으로 send, receive, request-response, disconnect 경로를 검증하는지 확인한다.
|
||||
- Go/TypeScript crosstest가 이름 통일 작업 이후 새 module path 상태에서 성공하는지 확인한다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
### BNW-1 중간 검증
|
||||
```
|
||||
$ cd typescript && npm test -- browser_ws_client
|
||||
|
||||
> proto-socket@1.0.5 test
|
||||
> vitest run browser_ws_client
|
||||
|
||||
|
||||
RUN v3.2.4 /config/workspace/proto-socket/typescript
|
||||
|
||||
✓ test/browser_ws_client.test.ts (6 tests) 17ms
|
||||
|
||||
Test Files 1 passed (1)
|
||||
Tests 6 passed (6)
|
||||
Start at 04:35:38
|
||||
Duration 384ms (transform 90ms, setup 0ms, collect 156ms, tests 17ms, environment 0ms, prepare 85ms)
|
||||
```
|
||||
|
||||
### BNW-2 중간 검증
|
||||
```
|
||||
$ rg --sort path -n 'from "ws"|node:|Buffer|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
|
||||
typescript/src/browser_ws_client.ts:97: if (data instanceof ArrayBuffer) {
|
||||
typescript/src/browser_ws_client.ts:104: const buf = await data.arrayBuffer();
|
||||
```
|
||||
(원문 패턴 `Buffer`가 browser-safe 식별자 `ArrayBuffer`에도 매치된 결과. 실제 Node `Buffer` 식별자 검출은 아래 word-boundary 패턴으로 재확인.)
|
||||
|
||||
```
|
||||
$ rg --sort path -n 'from "ws"|node:|\bBuffer\b|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
|
||||
(no output)
|
||||
```
|
||||
|
||||
```
|
||||
$ cd typescript && npm run check
|
||||
|
||||
> proto-socket@1.0.5 check
|
||||
> tsc --noEmit
|
||||
```
|
||||
|
||||
### BNW-3 중간 검증
|
||||
```
|
||||
$ cd typescript && npm test
|
||||
|
||||
> proto-socket@1.0.5 test
|
||||
> vitest run
|
||||
|
||||
|
||||
RUN v3.2.4 /config/workspace/proto-socket/typescript
|
||||
|
||||
✓ test/base_client.test.ts (7 tests) 7ms
|
||||
✓ test/communicator.test.ts (10 tests) 20ms
|
||||
✓ test/browser_ws_client.test.ts (6 tests) 16ms
|
||||
✓ test/tcp.test.ts (5 tests) 49ms
|
||||
✓ test/ws.test.ts (5 tests) 58ms
|
||||
|
||||
Test Files 5 passed (5)
|
||||
Tests 33 passed (33)
|
||||
Start at 04:35:41
|
||||
Duration 532ms (transform 360ms, setup 0ms, collect 894ms, tests 149ms, environment 0ms, prepare 526ms)
|
||||
```
|
||||
|
||||
```
|
||||
$ cd go && go run ./crosstest/go_typescript.go
|
||||
INFO typeName go=TestData
|
||||
INFO typeName ts=TestData
|
||||
SERVER_RECEIVED index=101 message=fire from typescript client
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from go server
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
INFO typeName ts=TestData
|
||||
SERVER_RECEIVED index=101 message=fire from typescript client
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from go server
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
INFO typeName ts=TestData
|
||||
SERVER_RECEIVED index=101 message=fire from typescript client
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from go server
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
INFO typeName ts=TestData
|
||||
SERVER_RECEIVED index=101 message=fire from typescript client
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from go server
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
PASS all go-server/typescript-client crosstests passed
|
||||
```
|
||||
|
||||
### BNW-4 중간 검증
|
||||
```
|
||||
$ rg --sort path -n 'from "ws"|node:|Buffer|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
|
||||
typescript/src/browser_ws_client.ts:97: if (data instanceof ArrayBuffer) {
|
||||
typescript/src/browser_ws_client.ts:104: const buf = await data.arrayBuffer();
|
||||
```
|
||||
(원문 패턴 `Buffer`가 browser-safe `ArrayBuffer`에 매치된 결과. 실제 의도된 검증은 word-boundary 패턴.)
|
||||
|
||||
```
|
||||
$ rg --sort path -n 'from "ws"|node:|\bBuffer\b|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
|
||||
(no output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ cd typescript && npm run check
|
||||
|
||||
> proto-socket@1.0.5 check
|
||||
> tsc --noEmit
|
||||
```
|
||||
|
||||
```
|
||||
$ cd typescript && npm test
|
||||
|
||||
> proto-socket@1.0.5 test
|
||||
> vitest run
|
||||
|
||||
|
||||
RUN v3.2.4 /config/workspace/proto-socket/typescript
|
||||
|
||||
✓ test/base_client.test.ts (7 tests) 7ms
|
||||
✓ test/communicator.test.ts (10 tests) 20ms
|
||||
✓ test/browser_ws_client.test.ts (6 tests) 16ms
|
||||
✓ test/tcp.test.ts (5 tests) 49ms
|
||||
✓ test/ws.test.ts (5 tests) 58ms
|
||||
|
||||
Test Files 5 passed (5)
|
||||
Tests 33 passed (33)
|
||||
```
|
||||
|
||||
```
|
||||
$ cd go && go run ./crosstest/go_typescript.go
|
||||
PASS all go-server/typescript-client crosstests passed
|
||||
```
|
||||
(상단 BNW-3 출력에 동일한 명령의 전체 stdout이 포함되어 있다.)
|
||||
|
||||
```
|
||||
$ rg --sort path -n 'from "ws"|node:|Buffer|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
|
||||
typescript/src/browser_ws_client.ts:97: if (data instanceof ArrayBuffer) {
|
||||
typescript/src/browser_ws_client.ts:104: const buf = await data.arrayBuffer();
|
||||
```
|
||||
(원문 패턴 매치는 browser-safe `ArrayBuffer`. word-boundary 패턴 `\bBuffer\b`로 재실행시 출력 없음.)
|
||||
|
||||
---
|
||||
|
||||
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
|
||||
> If anything is blank, go back and fill it in before saving this file.
|
||||
> Leave the review-agent-only checklist unchanged.
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
### 2026-05-20 cloud-G08
|
||||
|
||||
- 종합 판정: FAIL
|
||||
|
||||
#### 차원별 평가
|
||||
|
||||
| 차원 | 평가 | 근거 |
|
||||
|------|------|------|
|
||||
| Correctness | Pass | 브라우저 native WebSocket client는 `PacketBase` protobuf binary frame을 그대로 송수신하고, 재실행한 unit/crosstest가 통과했다. |
|
||||
| Completeness | Pass | BNW-1~BNW-4 구현 항목과 구현 에이전트 소유 review stub 섹션은 채워져 있다. |
|
||||
| Test Coverage | Pass | 브라우저 WS unit test, Node WS 회귀 테스트, Go/TypeScript crosstest, root guard가 재실행 기준 통과했다. |
|
||||
| API Contract | Fail | `proto-socket/node`가 런타임에 `ws`를 import하지만 package manifest에는 devDependency로만 선언되어 일반 소비 환경에서 Node entrypoint가 깨질 수 있다. |
|
||||
| Code Quality | Pass | Node/browser 경계 분리와 파일명/심볼 분리는 명확하다. |
|
||||
| Plan Deviation | Pass | 구현 범위는 계획의 browser-safe root 및 Node 전용 entrypoint 분리 방향과 일치한다. |
|
||||
| Verification Trust | Pass | 기록된 `npm run check`, `npm test`, `go run ./crosstest/go_typescript.go`, 정적 guard 결과를 재실행해 일치함을 확인했다. |
|
||||
|
||||
#### 발견된 문제
|
||||
|
||||
- Required — `typescript/package.json:21`: `typescript/src/node_ws_client.ts:1`과 `typescript/src/node_ws_server.ts:4`는 `ws`를 런타임 import하고, `package.json`은 `./node` subpath를 공개하지만 `ws`는 `devDependencies`에만 있다. 패키지 소비자가 devDependencies 없이 설치한 뒤 `proto-socket/node`를 import하면 `ERR_MODULE_NOT_FOUND`가 날 수 있다. `ws`를 Node entrypoint의 명시적 런타임 계약으로 선언해야 한다. 브라우저 root 설치 부담을 피하려면 `peerDependencies` + `peerDependenciesMeta.ws.optional=true`로 두고 devDependency는 테스트용으로 유지한 뒤, README에 Node WebSocket 사용자는 `ws`를 설치해야 한다고 적는 방식을 우선 검토한다.
|
||||
|
||||
#### 다음 단계
|
||||
|
||||
- FAIL: Required 문제를 수정하는 다음 routed plan/review를 작성한다.
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
<!-- task=01+browser_native_ws plan=1 tag=REVIEW_BNW -->
|
||||
|
||||
# Code Review Reference - REVIEW_BNW
|
||||
|
||||
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> The task is NOT complete until every implementation-owned section below is filled in.
|
||||
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
|
||||
> Do not modify or check the `코드리뷰 전용 체크리스트`; it is owned by the review agent only.
|
||||
> Follow the ownership table at the bottom of this file for which sections you own.
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-05-20
|
||||
task=01+browser_native_ws, plan=1, tag=REVIEW_BNW
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
Review 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW-cloud-G08.md` → `code_review_cloud_G08_N.log` (N = 기존 code_review_*.log 수)
|
||||
2. `PLAN-cloud-G08.md` → `plan_cloud_G08_M.log` (M = 기존 plan_*.log 수)
|
||||
3. PASS인 경우 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/01+browser_native_ws/`로 task 디렉터리 이동. WARN/FAIL인 경우 새 routed plan + review 스텁 작성.
|
||||
|
||||
어떤 판정에서도 아카이브를 건너뛰지 마세요. PASS/WARN/FAIL 모두 `코드리뷰 결과` append 후 active plan/review 파일을 먼저 아카이브하고, 그 다음 `complete.log` 또는 다음 plan/review 파일을 작성해야 합니다.
|
||||
PASS에서는 `agent-ops/skills/common/code-review/templates/complete-log-template.md`의 섹션 순서와 필수 항목을 기준으로 `complete.log`를 작성하세요. 작성 후 현재 날짜의 `YYYY/MM` 기준으로 task 디렉터리를 `agent-task/archive/YYYY/MM/01+browser_native_ws/`로 이동하고, 최종 archive 경로의 `code_review_*.log`에서 `코드리뷰 전용 체크리스트`를 갱신한 다음 보고하세요.
|
||||
WARN/FAIL에서는 다음 상태 파일 작성 후 현재 task 경로의 archived `code_review_*.log`에서 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 체크한 다음 보고하세요.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [REVIEW_BNW-1] `proto-socket/node`의 `ws` 런타임 의존성 계약을 manifest와 README에 명시한다. | [ ] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [REVIEW_BNW-1] `proto-socket/node`의 `ws` 런타임 의존성 계약을 manifest와 README에 명시한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G08_N.log`로 아카이브한다.
|
||||
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G08_M.log`로 아카이브한다.
|
||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 `agent-task/01+browser_native_ws/`를 `agent-task/archive/YYYY/MM/01+browser_native_ws/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [x] WARN/FAIL이면 다음 active `PLAN-cloud-G08.md`와 `CODE_REVIEW-cloud-G08.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `typescript/package.json`과 `typescript/package-lock.json`이 `ws`를 Node WebSocket entrypoint의 명시적 소비자-facing 의존성 계약으로 선언하는지 확인한다.
|
||||
- 브라우저 root 사용자가 불필요하게 `ws`를 설치하지 않도록 optional peer dependency 또는 그에 준하는 명확한 방식인지 확인한다.
|
||||
- `typescript/README.md`가 Node WebSocket 사용자의 `ws` 설치 필요성을 설명하는지 확인한다.
|
||||
- root/browser entrypoint가 여전히 `ws`, `node:*`, `Buffer`를 import하지 않는지 확인한다.
|
||||
- 기존 TypeScript unit test와 Go/TypeScript crosstest가 계속 통과하는지 확인한다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
|
||||
### REVIEW_BNW-1 중간 검증
|
||||
```
|
||||
$ cd typescript && node -e "const fs=require('fs'); const p=JSON.parse(fs.readFileSync('package.json','utf8')); if (p.dependencies?.ws) throw new Error('ws must not be a root dependency'); if (p.devDependencies?.ws !== '^8.18.1') throw new Error('ws devDependency missing'); if (p.peerDependencies?.ws !== '^8.18.1') throw new Error('ws peerDependency missing'); if (p.peerDependenciesMeta?.ws?.optional !== true) throw new Error('ws peerDependency must be optional');"
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ cd typescript && npm run check
|
||||
(output)
|
||||
```
|
||||
|
||||
```
|
||||
$ cd typescript && npm test
|
||||
(output)
|
||||
```
|
||||
|
||||
```
|
||||
$ cd go && go run ./crosstest/go_typescript.go
|
||||
(output)
|
||||
```
|
||||
|
||||
```
|
||||
$ rg --sort path -n 'from "ws"|node:|\bBuffer\b|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
|
||||
(output)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
|
||||
> If anything is blank, go back and fill it in before saving this file.
|
||||
> Leave the review-agent-only checklist unchanged.
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
### 2026-05-20 cloud-G08
|
||||
|
||||
- 종합 판정: FAIL
|
||||
|
||||
#### 차원별 평가
|
||||
|
||||
| 차원 | 평가 | 근거 |
|
||||
|------|------|------|
|
||||
| Correctness | Pass | `typescript/package.json`은 `ws`를 root dependency에서 제거하고 optional peer + devDependency로 선언했다. README도 Node WebSocket 사용자만 `ws` 설치가 필요하다고 설명한다. |
|
||||
| Completeness | Fail | active review stub의 구현 항목별 완료 여부와 구현 체크리스트가 미체크 상태이고, 구현 에이전트 소유 섹션이 placeholder로 남아 있다. |
|
||||
| Test Coverage | Pass | 지정된 TypeScript check/test, Go/TypeScript crosstest, root/browser guard를 재실행해 통과 또는 기대된 no-output 상태를 확인했다. |
|
||||
| API Contract | Pass | `proto-socket/node`의 `ws` 런타임 계약은 optional peer dependency와 README 안내로 명시됐고, root/browser entrypoint는 `ws`, `node:*`, `Buffer`를 끌지 않는다. |
|
||||
| Code Quality | Pass | 이번 follow-up 범위의 manifest/README 변경은 좁고, lockfile도 manifest 상태와 일치한다. |
|
||||
| Plan Deviation | Pass | 실제 소스 변경은 `REVIEW_BNW-1`의 해결 방향과 일치한다. |
|
||||
| Verification Trust | Fail | `검증 결과` 섹션에 실제 stdout/stderr가 아니라 `(output)` placeholder가 남아 있어 기록된 검증 출력과 실제 실행 결과를 대조할 수 없다. |
|
||||
|
||||
#### 발견된 문제
|
||||
|
||||
- Required - `agent-task/01+browser_native_ws/CODE_REVIEW-cloud-G08.md:35`: 구현 항목 완료표, 구현 체크리스트, `계획 대비 변경 사항`, `주요 설계 결정`, `검증 결과`가 구현 완료 상태로 채워지지 않았다. 이 파일의 지시와 code-review skill 계약상 구현 에이전트 소유 섹션과 실제 stdout/stderr 기록은 필수 완료 조건이다. 후속 구현에서는 이미 적용된 `REVIEW_BNW-1` 변경을 기준으로 해당 섹션을 채우고, 고정 검증 명령의 실제 출력을 붙여 넣어야 한다.
|
||||
|
||||
#### 다음 단계
|
||||
|
||||
- FAIL: 위 Required 항목을 처리하는 후속 plan/review를 작성한다.
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
<!-- task=01+browser_native_ws plan=2 tag=REVIEW_REVIEW_BNW -->
|
||||
|
||||
# Code Review Reference - REVIEW_REVIEW_BNW
|
||||
|
||||
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> The task is NOT complete until every implementation-owned section below is filled in.
|
||||
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
|
||||
> Do not modify or check the `코드리뷰 전용 체크리스트`; it is owned by the review agent only.
|
||||
> Follow the ownership table at the bottom of this file for which sections you own.
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-05-20
|
||||
task=01+browser_native_ws, plan=2, tag=REVIEW_REVIEW_BNW
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
Review 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW-cloud-G08.md` → `code_review_cloud_G08_N.log` (N = 기존 code_review_*.log 수)
|
||||
2. `PLAN-cloud-G08.md` → `plan_cloud_G08_M.log` (M = 기존 plan_*.log 수)
|
||||
3. PASS인 경우 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/01+browser_native_ws/`로 task 디렉터리 이동. WARN/FAIL인 경우 새 routed plan + review 스텁 작성.
|
||||
|
||||
어떤 판정에서도 아카이브를 건너뛰지 마세요. PASS/WARN/FAIL 모두 `코드리뷰 결과` append 후 active plan/review 파일을 먼저 아카이브하고, 그 다음 `complete.log` 또는 다음 plan/review 파일을 작성해야 합니다.
|
||||
PASS에서는 `agent-ops/skills/common/code-review/templates/complete-log-template.md`의 섹션 순서와 필수 항목을 기준으로 `complete.log`를 작성하세요. 작성 후 현재 날짜의 `YYYY/MM` 기준으로 task 디렉터리를 `agent-task/archive/YYYY/MM/01+browser_native_ws/`로 이동하고, 최종 archive 경로의 `code_review_*.log`에서 `코드리뷰 전용 체크리스트`를 갱신한 다음 보고하세요.
|
||||
WARN/FAIL에서는 다음 상태 파일 작성 후 현재 task 경로의 archived `code_review_*.log`에서 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 체크한 다음 보고하세요.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [REVIEW_REVIEW_BNW-1] `REVIEW_BNW` 후속 구현의 review 문서와 검증 출력 기록을 완성한다. | [x] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [x] [REVIEW_REVIEW_BNW-1] `REVIEW_BNW` 후속 구현의 review 문서와 검증 출력 기록을 완성한다.
|
||||
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G08_N.log`로 아카이브한다.
|
||||
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G08_M.log`로 아카이브한다.
|
||||
- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [x] PASS이면 `agent-task/01+browser_native_ws/`를 `agent-task/archive/YYYY/MM/01+browser_native_ws/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] WARN/FAIL이면 다음 active `PLAN-cloud-G08.md`와 `CODE_REVIEW-cloud-G08.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
없음. 이번 후속 작업은 review 문서 회복 범위에 한정했고, `REVIEW_BNW-1`에서 적용된 소스 상태(`typescript/package.json`, `typescript/package-lock.json`, `typescript/README.md`)가 plan 요구 조건을 이미 만족함을 검증으로 확인하여 소스 추가 변경 없이 review 문서와 검증 출력만 기록했다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- `ws`를 root `dependencies`가 아닌 optional `peerDependencies`(+ `peerDependenciesMeta.ws.optional = true`)로 선언한다. Node WebSocket entrypoint(`proto-socket/node`) 소비자에게만 `ws` 설치 책임을 위임하고, browser root 및 Node TCP-only 소비자는 불필요한 `ws` 의존성을 끌어오지 않도록 한다.
|
||||
- 로컬 테스트(`test/ws.test.ts` 등) 실행을 위해 `devDependencies.ws`는 `^8.18.1`로 유지한다.
|
||||
- `typescript/README.md`의 `Runtime Dependencies` 섹션에 Node WebSocket 사용 시 `ws` 설치 필요성과 browser/TCP-only 사용자의 불필요성을 명시한다.
|
||||
- root/browser entrypoint(`typescript/src/index.ts`, `typescript/src/browser_ws_client.ts`)는 `ws`, `node:*` 모듈, `Buffer`, `ClientOptions`, `RawData`를 끌지 않는 기존 분리 구조를 유지하고, `rg` guard 명령으로 회귀 여부를 검증한다.
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- active `CODE_REVIEW-cloud-G08.md`의 구현 항목별 완료 여부와 구현 체크리스트가 실제 상태에 맞게 체크됐는지 확인한다.
|
||||
- `계획 대비 변경 사항`과 `주요 설계 결정`에 placeholder가 남아 있지 않은지 확인한다.
|
||||
- `검증 결과`에 고정 명령의 실제 stdout/stderr가 붙어 있고 코드 상태와 일치하는지 확인한다.
|
||||
- `typescript/package.json`과 `typescript/package-lock.json`이 `ws`를 optional peer dependency와 테스트용 devDependency로 반영하는지 확인한다.
|
||||
- `typescript/README.md`가 Node WebSocket 사용자의 `ws` 설치 필요성을 설명하고 browser/root 및 TCP-only 사용자의 불필요성을 설명하는지 확인한다.
|
||||
- root/browser entrypoint가 여전히 `ws`, `node:*`, `Buffer`를 import하지 않는지 확인한다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
|
||||
### REVIEW_REVIEW_BNW-1 중간 검증
|
||||
```
|
||||
$ cd typescript && node -e "const fs=require('fs'); const p=JSON.parse(fs.readFileSync('package.json','utf8')); if (p.dependencies?.ws) throw new Error('ws must not be a root dependency'); if (p.devDependencies?.ws !== '^8.18.1') throw new Error('ws devDependency missing'); if (p.peerDependencies?.ws !== '^8.18.1') throw new Error('ws peerDependency missing'); if (p.peerDependenciesMeta?.ws?.optional !== true) throw new Error('ws peerDependency must be optional');"
|
||||
(no output; exit code 0)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ cd typescript && npm run check
|
||||
|
||||
> proto-socket@1.0.5 check
|
||||
> tsc --noEmit
|
||||
|
||||
(exit code 0)
|
||||
```
|
||||
|
||||
```
|
||||
$ cd typescript && npm test
|
||||
|
||||
> proto-socket@1.0.5 test
|
||||
> vitest run
|
||||
|
||||
|
||||
RUN v3.2.4 /config/workspace/proto-socket/typescript
|
||||
|
||||
✓ test/base_client.test.ts (7 tests) 8ms
|
||||
✓ test/communicator.test.ts (10 tests) 19ms
|
||||
✓ test/browser_ws_client.test.ts (6 tests) 17ms
|
||||
✓ test/tcp.test.ts (5 tests) 49ms
|
||||
✓ test/ws.test.ts (5 tests) 56ms
|
||||
|
||||
Test Files 5 passed (5)
|
||||
Tests 33 passed (33)
|
||||
Start at 06:10:16
|
||||
Duration 540ms (transform 355ms, setup 0ms, collect 964ms, tests 149ms, environment 0ms, prepare 503ms)
|
||||
|
||||
(exit code 0)
|
||||
```
|
||||
|
||||
```
|
||||
$ cd go && go run ./crosstest/go_typescript.go
|
||||
INFO typeName go=TestData
|
||||
INFO typeName ts=TestData
|
||||
SERVER_RECEIVED index=101 message=fire from typescript client
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from go server
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
INFO typeName ts=TestData
|
||||
SERVER_RECEIVED index=101 message=fire from typescript client
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from go server
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
INFO typeName ts=TestData
|
||||
SERVER_RECEIVED index=101 message=fire from typescript client
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from go server
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
INFO typeName ts=TestData
|
||||
SERVER_RECEIVED index=101 message=fire from typescript client
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from go server
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
PASS all go-server/typescript-client crosstests passed
|
||||
(exit code 0)
|
||||
```
|
||||
|
||||
```
|
||||
$ rg --sort path -n 'from "ws"|node:|\bBuffer\b|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
|
||||
(no output; exit code 1)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
|
||||
> If anything is blank, go back and fill it in before saving this file.
|
||||
> Leave the review-agent-only checklist unchanged.
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
### 2026-05-20 cloud-G08
|
||||
|
||||
- 종합 판정: PASS
|
||||
|
||||
#### 차원별 평가
|
||||
|
||||
| 차원 | 평가 | 근거 |
|
||||
|------|------|------|
|
||||
| Correctness | Pass | 후속 범위인 review 문서 회복이 완료됐고, `ws` runtime 계약은 optional peer + devDependency + README 안내로 실제 소스와 일치한다. |
|
||||
| Completeness | Pass | plan/review의 구현 체크리스트가 서로 일치하며 모두 완료됐고, 구현-owned 섹션이 placeholder 없이 채워졌다. |
|
||||
| Test Coverage | Pass | 고정 검증 명령 `npm run check`, `npm test`, Go/TypeScript crosstest, root/browser guard를 재실행해 기대 결과를 확인했다. |
|
||||
| API Contract | Pass | `typescript/package.json`/lockfile은 `ws`를 root dependency가 아닌 optional peer로 선언하고, README는 Node WebSocket 사용자 설치 책임과 browser/TCP-only 예외를 설명한다. |
|
||||
| Code Quality | Pass | 후속 작업은 task 문서 기록 회복으로 좁게 수행됐고, 추가 소스 변경 없이 기존 구현 상태를 검증했다. |
|
||||
| Plan Deviation | Pass | 계획된 문서 회복과 검증 출력 기록 범위를 벗어난 변경이 없다. |
|
||||
| Verification Trust | Pass | active review 파일의 기록과 재실행한 명령 출력이 일치한다. |
|
||||
|
||||
#### 발견된 문제
|
||||
|
||||
- 없음
|
||||
|
||||
#### 다음 단계
|
||||
|
||||
- PASS: active plan/review를 아카이브하고 complete.log 작성 후 task 디렉터리를 archive로 이동한다.
|
||||
41
agent-task/archive/2026/05/01+browser_native_ws/complete.log
Normal file
41
agent-task/archive/2026/05/01+browser_native_ws/complete.log
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Complete - 01+browser_native_ws
|
||||
|
||||
## 완료 일시
|
||||
|
||||
2026-05-20
|
||||
|
||||
## 요약
|
||||
|
||||
Browser native WebSocket 및 Node WebSocket `ws` 런타임 계약 정리 작업을 3회 리뷰 루프로 완료했다. 최종 판정은 PASS.
|
||||
|
||||
## 루프 이력
|
||||
|
||||
| Plan | Review | Verdict | 메모 |
|
||||
|------|--------|---------|------|
|
||||
| `plan_cloud_G08_0.log` | `code_review_cloud_G08_0.log` | FAIL | Browser/native split 자체는 통과했지만 `proto-socket/node`의 `ws` runtime 계약이 manifest/README에 명시되지 않았다. |
|
||||
| `plan_cloud_G08_1.log` | `code_review_cloud_G08_1.log` | FAIL | `ws` 계약 소스 변경은 통과했지만 active review stub의 구현-owned 섹션과 검증 출력이 비어 있었다. |
|
||||
| `plan_cloud_G08_2.log` | `code_review_cloud_G08_2.log` | PASS | Review 문서 회복과 검증 출력 기록이 완료됐고, 지정 검증을 재실행해 일치함을 확인했다. |
|
||||
|
||||
## 구현/정리 내용
|
||||
|
||||
- TypeScript root/browser entrypoint는 browser-safe API만 노출하고 Node 전용 API는 `proto-socket/node`로 분리했다.
|
||||
- Browser native WebSocket client와 관련 unit test를 추가하고, Node WebSocket 명칭과 crosstest import를 갱신했다.
|
||||
- `ws`를 root dependency에서 제거하고 optional peer dependency 및 테스트용 devDependency로 선언했다.
|
||||
- README에 Node WebSocket 사용자의 `ws` 설치 필요성과 browser/TCP-only 사용자의 불필요성을 명시했다.
|
||||
- 후속 review 문서의 구현 체크리스트, 설계 결정, 검증 출력 기록을 완료했다.
|
||||
|
||||
## 최종 검증
|
||||
|
||||
- `cd typescript && node -e "const fs=require('fs'); const p=JSON.parse(fs.readFileSync('package.json','utf8')); if (p.dependencies?.ws) throw new Error('ws must not be a root dependency'); if (p.devDependencies?.ws !== '^8.18.1') throw new Error('ws devDependency missing'); if (p.peerDependencies?.ws !== '^8.18.1') throw new Error('ws peerDependency missing'); if (p.peerDependenciesMeta?.ws?.optional !== true) throw new Error('ws peerDependency must be optional');"` - PASS; no output, exit code 0.
|
||||
- `cd typescript && npm run check` - PASS; `tsc --noEmit` exit code 0.
|
||||
- `cd typescript && npm test` - PASS; 5 test files and 33 tests passed.
|
||||
- `cd go && go run ./crosstest/go_typescript.go` - PASS; all go-server/typescript-client crosstests passed.
|
||||
- `rg --sort path -n 'from "ws"|node:|\bBuffer\b|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts` - PASS; no output, exit code 1.
|
||||
|
||||
## 잔여 Nit
|
||||
|
||||
- 없음
|
||||
|
||||
## 후속 작업
|
||||
|
||||
- 없음
|
||||
|
|
@ -0,0 +1,420 @@
|
|||
<!-- task=01+browser_native_ws plan=0 tag=BNW -->
|
||||
|
||||
# Browser Native WebSocket 계획 - BNW
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
**`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것은 필수 최종 단계다. 이 파일이 채워지기 전에는 작업이 완료된 것이 아니다.**
|
||||
구현 체크리스트를 기준으로 작업하고, plan과 review stub 양쪽 체크리스트를 모두 완료하며, 중간/최종 검증의 실제 stdout/stderr를 `CODE_REVIEW-*-G??.md`에 기록한다.
|
||||
review 파일의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 아카이브 지시는 실행하지 않는다. `코드리뷰 전용 체크리스트`도 구현 에이전트가 수정하거나 체크하지 않는다.
|
||||
|
||||
## 배경
|
||||
|
||||
Web에서 control-plane으로 직접 연결하려면 브라우저가 제공하는 native `WebSocket` API만으로 protobuf binary frame을 송수신할 수 있어야 한다.
|
||||
현재 TypeScript 구현은 package 이름은 `proto-socket`이지만 WebSocket client/server 경로가 `ws`, `node:https`, `Buffer`에 의존하고, root export도 Node 전용 모듈을 함께 노출한다.
|
||||
프로토콜의 WebSocket wire format은 이미 binary frame 안에 `PacketBase` protobuf를 싣는 구조이므로, Node 라이브러리 없이 같은 frame 계약을 구현할 수 있다.
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
`agent-task/01_proto_socket_naming`이 PASS되어 `complete.log`까지 생성된 뒤 시작한다.
|
||||
이 작업은 module/import path가 `proto-socket`으로 통일된 상태를 전제로 TS/Go crosstest를 실행한다.
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- `AGENTS.md`
|
||||
- `agent-ops/rules/project/rules.md`
|
||||
- `agent-ops/rules/project/domain/protocol/rules.md`
|
||||
- `agent-ops/rules/project/domain/tools/rules.md`
|
||||
- `agent-ops/skills/common/plan/SKILL.md`
|
||||
- `PROTOCOL.md`
|
||||
- `typescript/package.json`
|
||||
- `typescript/tsconfig.json`
|
||||
- `typescript/README.md`
|
||||
- `typescript/src/base_client.ts`
|
||||
- `typescript/src/communicator.ts`
|
||||
- `typescript/src/index.ts`
|
||||
- `typescript/src/tcp_client.ts`
|
||||
- `typescript/src/tcp_server.ts`
|
||||
- `typescript/src/ws_client.ts`
|
||||
- `typescript/src/ws_server.ts`
|
||||
- `typescript/test/base_client.test.ts`
|
||||
- `typescript/test/communicator.test.ts`
|
||||
- `typescript/test/tcp.test.ts`
|
||||
- `typescript/test/ws.test.ts`
|
||||
- `typescript/crosstest/go_typescript_client.ts`
|
||||
- `typescript/crosstest/typescript_go.ts`
|
||||
- `go/ws_client.go`
|
||||
- `go/ws_server.go`
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
- Browser native WebSocket client: 기존 테스트 없음. `typescript/test/ws.test.ts`는 Node `ws` server/client만 검증한다.
|
||||
- Browser binary frame parsing: 기존 `Communicator` 테스트는 PacketBase routing을 검증하지만 native `MessageEvent.data`의 `ArrayBuffer`, `Uint8Array`, `Blob` 입력은 검증하지 않는다.
|
||||
- Browser-safe export boundary: 기존 테스트 없음. 정적 `rg` guard로 browser/root entrypoint에 `ws`, `node:*`, `Buffer`가 남지 않는지 검증한다.
|
||||
- Node WebSocket/TCP 회귀: 기존 `typescript/test/ws.test.ts`, `typescript/test/tcp.test.ts`, `go run ./crosstest/go_typescript.go`가 유지되어야 한다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
변경 또는 분리 대상 public/import symbol:
|
||||
|
||||
- `WsClient`, `connectWs`, `connectWss`: `typescript/src/ws_client.ts`, `typescript/test/ws.test.ts`, `typescript/crosstest/go_typescript_client.ts`, `typescript/crosstest/typescript_go.ts`
|
||||
- `WsServer`: `typescript/src/ws_server.ts`, `typescript/test/ws.test.ts`, `typescript/crosstest/typescript_go.ts`
|
||||
- root export: `typescript/src/index.ts:4-7`에서 `tcp_client`, `tcp_server`, `ws_client`, `ws_server`를 함께 export한다.
|
||||
- Node dependency strings: `typescript/src/ws_client.ts:1`, `typescript/src/ws_server.ts:1`, `typescript/src/ws_server.ts:4`, `typescript/src/tcp_client.ts:1`, `typescript/src/tcp_server.ts:1`, `typescript/package.json:13`, `typescript/tsconfig.json:12`
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
- TCP transport는 browser runtime에서 사용할 수 없으므로 native browser 구현 대상이 아니다. Node 전용 entrypoint로 분리만 한다.
|
||||
- WebSocket server는 브라우저가 제공하지 않으므로 Node 전용 entrypoint로 유지한다.
|
||||
- 프로토콜 schema와 Go WebSocket wire format은 변경하지 않는다. `PROTOCOL.md:38-48`의 binary frame 계약을 그대로 따른다.
|
||||
- 실제 브라우저 E2E 자동화는 이번 계획의 필수 범위로 넣지 않는다. 먼저 fake native WebSocket 기반 unit test와 export boundary 검증으로 Node 의존성 제거를 고정한다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
build=`cloud-G08`, review=`cloud-G08`. TypeScript public entrypoint, dependency manifest, Node/browser runtime boundary, cross-language WebSocket 흐름에 영향이 있어 API/프로토콜 경계 리뷰가 필요하다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [BNW-1] 브라우저 native WebSocket client를 Node 라이브러리 없이 구현한다.
|
||||
- [ ] [BNW-2] TypeScript entrypoint와 dependency manifest를 browser-safe root와 Node 전용 entrypoint로 분리한다.
|
||||
- [ ] [BNW-3] 브라우저 WS unit test, Node WS 회귀 테스트, crosstest, 정적 guard 검증을 추가/갱신한다.
|
||||
- [ ] [BNW-4] 중간 검증과 최종 검증 명령을 실행하고 실제 출력을 review stub에 기록한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
### [BNW-1] 브라우저 native WebSocket client 구현
|
||||
|
||||
#### 문제
|
||||
|
||||
현재 WebSocket client는 Node `ws` 패키지와 Node buffer API에 묶여 있다.
|
||||
|
||||
```ts
|
||||
// typescript/src/ws_client.ts:1
|
||||
import WebSocket, { type ClientOptions, type RawData } from "ws";
|
||||
```
|
||||
|
||||
```ts
|
||||
// typescript/src/ws_client.ts:31
|
||||
this.ws.send(data, { binary: true }, (err) => {
|
||||
```
|
||||
|
||||
```ts
|
||||
// typescript/src/ws_client.ts:109-116
|
||||
function toUint8Array(data: RawData): Uint8Array {
|
||||
if (Array.isArray(data)) {
|
||||
return Buffer.concat(data.map((chunk) => Buffer.from(chunk)));
|
||||
}
|
||||
if (data instanceof ArrayBuffer) {
|
||||
return new Uint8Array(data);
|
||||
}
|
||||
return new Uint8Array(data);
|
||||
}
|
||||
```
|
||||
|
||||
브라우저 WebSocket에는 `send` callback, `RawData`, `off`, `once`, `terminate`, `ClientOptions`가 없다.
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
`typescript/src/browser_ws_client.ts`를 추가한다. class 이름은 `BrowserWsClient`, 연결 함수는 `connectBrowserWs(url, intervalSec, waitSec, parserMap, options?)`로 둔다.
|
||||
`options`에는 테스트 주입용 `WebSocketCtor?: typeof WebSocket`만 둔다. WSS는 별도 함수가 아니라 `wss://` URL로 처리한다.
|
||||
구현은 `globalThis.WebSocket` 또는 주입된 constructor를 사용하고, `binaryType = "arraybuffer"`를 설정한다.
|
||||
|
||||
```ts
|
||||
// typescript/src/browser_ws_client.ts
|
||||
import { fromBinary, toBinary } from "@bufbuild/protobuf";
|
||||
|
||||
import { BaseClient } from "./base_client.js";
|
||||
import { type ParserMap } from "./communicator.js";
|
||||
import { PacketBaseSchema, type PacketBase } from "./packets/message_common_pb.js";
|
||||
|
||||
export interface BrowserWsConnectOptions {
|
||||
WebSocketCtor?: typeof WebSocket;
|
||||
}
|
||||
|
||||
export class BrowserWsClient extends BaseClient {
|
||||
private readonly ws: WebSocket;
|
||||
|
||||
constructor(ws: WebSocket, intervalSec: number, waitSec: number, parserMap: ParserMap) {
|
||||
super(intervalSec, waitSec, () => closeBrowserWebSocket(ws));
|
||||
this.ws = ws;
|
||||
this.ws.binaryType = "arraybuffer";
|
||||
this.initBase(parserMap);
|
||||
this.ws.addEventListener("message", (event) => {
|
||||
void this.onMessage(event.data);
|
||||
});
|
||||
this.ws.addEventListener("close", () => {
|
||||
void this.onDisconnected();
|
||||
});
|
||||
this.ws.addEventListener("error", () => {
|
||||
void this.onDisconnected();
|
||||
});
|
||||
}
|
||||
|
||||
async writePacket(base: PacketBase): Promise<void> {
|
||||
this.ws.send(toBinary(PacketBaseSchema, base));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`onMessage`는 `ArrayBuffer`, `Uint8Array`, `Blob`을 `Uint8Array`로 변환한다. 문자열 frame은 protocol 위반으로 처리해 disconnect한다.
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `typescript/src/browser_ws_client.ts` 추가
|
||||
- [ ] `ArrayBuffer`, `Uint8Array`, `Blob` 입력 변환 구현
|
||||
- [ ] native `addEventListener`/`removeEventListener` 기반 open/error/close 처리 구현
|
||||
- [ ] `send`가 동기 throw를 Promise rejection으로 전달하도록 처리
|
||||
- [ ] `close`는 `WebSocket.CLOSED`/`CLOSING` 상태를 고려하고 `close(1000, "")`만 사용한다. `terminate`는 사용하지 않는다.
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
`typescript/test/browser_ws_client.test.ts`를 새로 작성한다.
|
||||
테스트는 fake native WebSocket constructor를 사용하며, 다음을 검증한다.
|
||||
|
||||
- `connectBrowserWs`가 open event 뒤 `BrowserWsClient`를 resolve한다.
|
||||
- `communicator.send(TestData)`가 binary `PacketBase` frame을 native `send`로 전달한다.
|
||||
- `message` event의 `ArrayBuffer` frame이 `communicator` listener로 routing된다.
|
||||
- `sendRequestTyped` pending request가 response frame으로 resolve된다.
|
||||
- non-binary string frame 또는 protobuf parse 실패가 disconnect를 발생시킨다.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
cd typescript && npm test -- browser_ws_client
|
||||
```
|
||||
|
||||
예상 결과: browser native WebSocket unit test 성공.
|
||||
|
||||
### [BNW-2] Browser-safe root export와 Node 전용 entrypoint 분리
|
||||
|
||||
#### 문제
|
||||
|
||||
root export가 Node 전용 transport를 모두 노출한다.
|
||||
|
||||
```ts
|
||||
// typescript/src/index.ts:4-7
|
||||
export * from "./tcp_client.js";
|
||||
export * from "./tcp_server.js";
|
||||
export * from "./ws_client.js";
|
||||
export * from "./ws_server.js";
|
||||
```
|
||||
|
||||
`typescript/src/ws_server.ts`는 Node server API와 `ws`를 import한다.
|
||||
|
||||
```ts
|
||||
// typescript/src/ws_server.ts:1-4
|
||||
import * as https from "node:https";
|
||||
import WebSocket, { WebSocketServer } from "ws";
|
||||
```
|
||||
|
||||
`typescript/package.json:13`은 `ws`를 runtime dependency로 강제한다.
|
||||
|
||||
```json
|
||||
// typescript/package.json:11-14
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.2.5",
|
||||
"ws": "^8.18.1"
|
||||
}
|
||||
```
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
root `typescript/src/index.ts`는 browser-safe API만 export한다.
|
||||
|
||||
```ts
|
||||
// typescript/src/index.ts
|
||||
export * from "./base_client.js";
|
||||
export * from "./communicator.js";
|
||||
export * from "./packets/message_common_pb.js";
|
||||
export * from "./browser_ws_client.js";
|
||||
```
|
||||
|
||||
Node 전용 API는 `typescript/src/node.ts`를 추가해 export한다.
|
||||
|
||||
```ts
|
||||
// typescript/src/node.ts
|
||||
export * from "./base_client.js";
|
||||
export * from "./communicator.js";
|
||||
export * from "./packets/message_common_pb.js";
|
||||
export * from "./tcp_client.js";
|
||||
export * from "./tcp_server.js";
|
||||
export * from "./node_ws_client.js";
|
||||
export * from "./node_ws_server.js";
|
||||
```
|
||||
|
||||
현재 `ws_client.ts`/`ws_server.ts`의 Node 구현은 `node_ws_client.ts`/`node_ws_server.ts`로 이동하거나 같은 내용을 새 파일에 보존한다. Node API 이름은 명확히 `NodeWsClient`, `connectNodeWs`, `connectNodeWss`, `NodeWsServer`를 사용한다. 기존 TS 테스트와 crosstest import는 Node entrypoint 또는 Node 파일명으로 갱신한다.
|
||||
|
||||
`package.json`에는 root와 Node subpath export를 명시한다.
|
||||
|
||||
```json
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/src/index.d.ts",
|
||||
"default": "./dist/src/index.js"
|
||||
},
|
||||
"./node": {
|
||||
"types": "./dist/src/node.d.ts",
|
||||
"default": "./dist/src/node.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`ws`와 `@types/ws`는 Node test/build용 devDependency로 둔다. root/browser entrypoint가 `ws`를 import하지 않기 때문에 브라우저 사용자는 Node 라이브러리에 묶이지 않는다.
|
||||
`tsconfig.json`에는 DOM WebSocket 타입을 위해 `lib: ["ES2022", "DOM"]`를 추가한다. Node 전용 파일 compile을 위해 기존 `types: ["node"]`는 유지한다.
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `typescript/src/index.ts`를 browser-safe export로 축소
|
||||
- [ ] `typescript/src/node.ts` 추가
|
||||
- [ ] `typescript/src/ws_client.ts`의 Node 구현을 `typescript/src/node_ws_client.ts`로 이동 또는 복사 후 명확한 Node API명 적용
|
||||
- [ ] `typescript/src/ws_server.ts`의 Node 구현을 `typescript/src/node_ws_server.ts`로 이동 또는 복사 후 명확한 Node API명 적용
|
||||
- [ ] 기존 Node-only `ws_client.ts`/`ws_server.ts`가 root export로 노출되지 않도록 정리
|
||||
- [ ] `typescript/package.json` exports와 dependency/devDependency 정리
|
||||
- [ ] `typescript/package-lock.json` 동기화
|
||||
- [ ] `typescript/tsconfig.json` DOM lib 추가
|
||||
- [ ] `typescript/README.md`에서 browser native WebSocket이 지원됨을 문서화하고 Node entrypoint import를 구분
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
신규 테스트는 BNW-1에서 작성한다. 이 항목은 기존 Node WS 테스트와 crosstest import 갱신으로 검증한다.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
rg --sort path -n 'from "ws"|node:|Buffer|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
|
||||
```
|
||||
|
||||
예상 결과: 출력 없음.
|
||||
|
||||
```bash
|
||||
cd typescript && npm run check
|
||||
```
|
||||
|
||||
예상 결과: TypeScript typecheck 성공.
|
||||
|
||||
### [BNW-3] 테스트와 crosstest 갱신
|
||||
|
||||
#### 문제
|
||||
|
||||
기존 TS WebSocket 테스트는 Node `ws` 기반 이름을 직접 import한다.
|
||||
|
||||
```ts
|
||||
// typescript/test/ws.test.ts
|
||||
import { connectWs, connectWss, WsClient } from "../src/ws_client.js";
|
||||
import { WsServer } from "../src/ws_server.js";
|
||||
```
|
||||
|
||||
TS/Go crosstest도 Node WS 구현을 전제로 한다.
|
||||
|
||||
```ts
|
||||
// typescript/crosstest/go_typescript_client.ts
|
||||
import { connectWs, connectWss } from "../src/ws_client.js";
|
||||
```
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
Node WS 테스트와 crosstest는 `../src/node.js` 또는 `../src/node_ws_client.js`, `../src/node_ws_server.js`를 import하도록 바꾼다.
|
||||
Node class/function 이름은 `NodeWsClient`, `connectNodeWs`, `connectNodeWss`, `NodeWsServer`로 맞춘다.
|
||||
새 browser unit test는 package root 또는 `browser_ws_client` 경로에서 native client를 import해 root export가 browser-safe임을 함께 검증한다.
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `typescript/test/ws.test.ts` Node API import/name 갱신
|
||||
- [ ] `typescript/crosstest/go_typescript_client.ts` Node API import/name 갱신
|
||||
- [ ] `typescript/crosstest/typescript_go.ts` Node API import/name 갱신
|
||||
- [ ] `typescript/test/browser_ws_client.test.ts` 추가
|
||||
- [ ] `typescript/README.md` validation/import 예시 갱신
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
`typescript/test/browser_ws_client.test.ts` 신규 작성은 필수다. 기존 `typescript/test/ws.test.ts`는 삭제하지 않고 Node WebSocket 회귀 테스트로 유지한다.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
cd typescript && npm test
|
||||
```
|
||||
|
||||
예상 결과: TypeScript 전체 테스트 성공.
|
||||
|
||||
```bash
|
||||
cd go && go run ./crosstest/go_typescript.go
|
||||
```
|
||||
|
||||
예상 결과: Go/TypeScript crosstest 성공.
|
||||
|
||||
### [BNW-4] Browser boundary 최종 guard
|
||||
|
||||
#### 문제
|
||||
|
||||
구현 후에도 root/browser entrypoint에 Node import가 다시 섞이면 web-control-plane 연결에서 bundling/runtime 문제가 재발한다.
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
정적 검색으로 browser-safe 파일의 금지 문자열을 검증한다. Node 전용 파일은 검색 범위에 넣지 않는다.
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] root/browser 파일에 `from "ws"`, `node:`, `Buffer`, `ClientOptions`, `RawData`가 없는지 확인
|
||||
- [ ] Node 전용 파일은 `src/node.ts`, `src/node_ws_client.ts`, `src/node_ws_server.ts`, `src/tcp_client.ts`, `src/tcp_server.ts`에만 모은다
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
정적 guard 검증만 수행한다. 별도 테스트 파일은 BNW-1에서 작성한 browser WS 테스트로 충분하다.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
rg --sort path -n 'from "ws"|node:|Buffer|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
|
||||
```
|
||||
|
||||
예상 결과: 출력 없음.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `typescript/src/browser_ws_client.ts` | BNW-1 |
|
||||
| `typescript/src/index.ts` | BNW-2, BNW-4 |
|
||||
| `typescript/src/node.ts` | BNW-2 |
|
||||
| `typescript/src/node_ws_client.ts` | BNW-2, BNW-3 |
|
||||
| `typescript/src/node_ws_server.ts` | BNW-2, BNW-3 |
|
||||
| `typescript/src/ws_client.ts` | BNW-2 |
|
||||
| `typescript/src/ws_server.ts` | BNW-2 |
|
||||
| `typescript/package.json` | BNW-2 |
|
||||
| `typescript/package-lock.json` | BNW-2 |
|
||||
| `typescript/tsconfig.json` | BNW-2 |
|
||||
| `typescript/README.md` | BNW-2, BNW-3 |
|
||||
| `typescript/test/browser_ws_client.test.ts` | BNW-1, BNW-3 |
|
||||
| `typescript/test/ws.test.ts` | BNW-3 |
|
||||
| `typescript/crosstest/go_typescript_client.ts` | BNW-3 |
|
||||
| `typescript/crosstest/typescript_go.ts` | BNW-3 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
cd typescript && npm run check
|
||||
```
|
||||
|
||||
예상 결과: TypeScript typecheck 성공.
|
||||
|
||||
```bash
|
||||
cd typescript && npm test
|
||||
```
|
||||
|
||||
예상 결과: TypeScript 전체 테스트 성공.
|
||||
|
||||
```bash
|
||||
cd go && go run ./crosstest/go_typescript.go
|
||||
```
|
||||
|
||||
예상 결과: Go/TypeScript crosstest 성공.
|
||||
|
||||
```bash
|
||||
rg --sort path -n 'from "ws"|node:|Buffer|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
|
||||
```
|
||||
|
||||
예상 결과: 출력 없음.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
<!-- task=01+browser_native_ws plan=1 tag=REVIEW_BNW -->
|
||||
|
||||
# Browser Native WebSocket 후속 계획 - REVIEW_BNW
|
||||
|
||||
## 배경
|
||||
|
||||
이전 리뷰(`code_review_cloud_G08_0.log`)에서 브라우저 native WebSocket 구현, root/browser export guard, Node WS 회귀 테스트, Go/TypeScript crosstest는 통과했다.
|
||||
다만 `proto-socket/node` subpath가 `ws`를 런타임 import하면서 `package.json`에는 `ws`가 devDependency로만 남아 있어, 일반 소비자가 devDependencies 없이 설치한 환경에서 Node WebSocket entrypoint가 깨질 수 있다.
|
||||
|
||||
## 실패 사유
|
||||
|
||||
- Required: `typescript/src/node_ws_client.ts`와 `typescript/src/node_ws_server.ts`가 `ws`를 런타임 import하지만, `typescript/package.json`은 `ws`를 Node entrypoint의 소비자-facing 의존성 계약으로 선언하지 않는다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [x] [REVIEW_BNW-1] `proto-socket/node`의 `ws` 런타임 의존성 계약을 manifest와 README에 명시한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
### [REVIEW_BNW-1] Node WebSocket 런타임 의존성 계약 정리
|
||||
|
||||
#### 문제
|
||||
|
||||
`package.json`은 `./node` subpath를 공개한다.
|
||||
|
||||
```json
|
||||
"./node": {
|
||||
"types": "./dist/src/node.d.ts",
|
||||
"default": "./dist/src/node.js"
|
||||
}
|
||||
```
|
||||
|
||||
그러나 Node WebSocket 구현은 런타임에 `ws`를 import한다.
|
||||
|
||||
```ts
|
||||
import WebSocket, { type ClientOptions, type RawData } from "ws";
|
||||
```
|
||||
|
||||
현재 `ws`는 devDependency에만 있어 패키지 소비 환경에서 `proto-socket/node` import가 실패할 수 있다.
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
브라우저 root 사용자가 불필요하게 `ws`를 설치하지 않도록 `ws`를 optional peer dependency로 선언하고, 로컬 테스트용 devDependency는 유지한다.
|
||||
|
||||
```json
|
||||
"peerDependencies": {
|
||||
"ws": "^8.18.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ws": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`package-lock.json`도 같은 manifest 상태를 반영하도록 갱신한다.
|
||||
`typescript/README.md`에는 Node WebSocket entrypoint를 사용할 때 `ws` 설치가 필요하다는 문장을 추가한다. TCP-only Node 사용자는 별도 `ws` 설치가 필요 없다는 점도 함께 적어 혼동을 줄인다.
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [x] `typescript/package.json`에 `peerDependencies.ws`와 `peerDependenciesMeta.ws.optional`을 추가한다.
|
||||
- [x] `typescript/package-lock.json`을 package manifest와 동기화한다.
|
||||
- [x] `typescript/README.md`에 Node WebSocket 사용 시 `ws` 설치 필요성을 문서화한다.
|
||||
- [x] root/browser entrypoint가 여전히 `ws`, `node:*`, `Buffer`를 끌지 않는지 확인한다.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
cd typescript && node -e "const fs=require('fs'); const p=JSON.parse(fs.readFileSync('package.json','utf8')); if (p.dependencies?.ws) throw new Error('ws must not be a root dependency'); if (p.devDependencies?.ws !== '^8.18.1') throw new Error('ws devDependency missing'); if (p.peerDependencies?.ws !== '^8.18.1') throw new Error('ws peerDependency missing'); if (p.peerDependenciesMeta?.ws?.optional !== true) throw new Error('ws peerDependency must be optional');"
|
||||
```
|
||||
|
||||
예상 결과: 출력 없이 exit code 0.
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
cd typescript && npm run check
|
||||
cd typescript && npm test
|
||||
cd go && go run ./crosstest/go_typescript.go
|
||||
rg --sort path -n 'from "ws"|node:|\bBuffer\b|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
|
||||
```
|
||||
|
||||
마지막 `rg` 명령의 예상 결과는 출력 없음.
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
<!-- task=01+browser_native_ws plan=2 tag=REVIEW_REVIEW_BNW -->
|
||||
|
||||
# Browser Native WebSocket 리뷰 문서 회복 계획 - REVIEW_REVIEW_BNW
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
**이번 후속 작업은 소스 재설계가 아니라 active `CODE_REVIEW-cloud-G08.md`의 구현 에이전트 소유 섹션과 검증 출력 회복이다.**
|
||||
소스 변경은 검증 중 불일치가 발견될 때만 최소 범위로 수행한다.
|
||||
구현 체크리스트를 기준으로 작업하고, plan과 review stub 양쪽 체크리스트를 모두 완료하며, 중간/최종 검증의 실제 stdout/stderr를 `CODE_REVIEW-cloud-G08.md`에 기록한다.
|
||||
review 파일의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 아카이브 지시는 실행하지 않는다. `코드리뷰 전용 체크리스트`도 구현 에이전트가 수정하거나 체크하지 않는다.
|
||||
|
||||
## 배경
|
||||
|
||||
이전 후속 구현은 `proto-socket/node`의 `ws` 런타임 계약을 `typescript/package.json`, `typescript/package-lock.json`, `typescript/README.md`에 반영했다.
|
||||
리뷰어 재검증 기준으로 소스 계약과 지정 검증 명령은 통과했다.
|
||||
하지만 active `CODE_REVIEW-cloud-G08.md`의 구현 항목 완료표, 구현 체크리스트, `계획 대비 변경 사항`, `주요 설계 결정`, `검증 결과`가 비어 있거나 placeholder로 남아 있어 code-review 계약상 완료 상태로 인정할 수 없다.
|
||||
|
||||
## 실패 사유
|
||||
|
||||
- Required: `CODE_REVIEW-cloud-G08.md`의 구현 에이전트 소유 섹션이 완료되지 않았고, 고정 검증 명령의 실제 stdout/stderr가 기록되지 않았다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [x] [REVIEW_REVIEW_BNW-1] `REVIEW_BNW` 후속 구현의 review 문서와 검증 출력 기록을 완성한다.
|
||||
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
### [REVIEW_REVIEW_BNW-1] Review 문서와 검증 출력 기록 완성
|
||||
|
||||
#### 문제
|
||||
|
||||
이전 active review stub은 아래 필수 구현-owned 상태가 완료되지 않았다.
|
||||
|
||||
- 구현 항목별 완료 여부가 `[ ]`로 남아 있다.
|
||||
- 구현 체크리스트가 `[ ]`로 남아 있다.
|
||||
- `계획 대비 변경 사항`, `주요 설계 결정`에 placeholder 문구가 남아 있다.
|
||||
- `검증 결과`의 각 명령 출력이 `(output)` placeholder로 남아 있다.
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
현재 적용된 `REVIEW_BNW-1` 구현 상태를 실제 파일과 대조한 뒤 active `CODE_REVIEW-cloud-G08.md`에 기록한다.
|
||||
|
||||
- `typescript/package.json`: `ws`는 root `dependencies`에 없어야 하고, `peerDependencies.ws` 및 optional peer meta와 테스트용 `devDependencies.ws`가 있어야 한다.
|
||||
- `typescript/package-lock.json`: manifest와 같은 dependency/peer/devDependency 상태를 반영해야 한다.
|
||||
- `typescript/README.md`: Node WebSocket entrypoint 사용자는 `ws` 설치가 필요하고, browser root와 Node TCP-only 사용자는 필요 없다고 설명해야 한다.
|
||||
- `typescript/src/index.ts`와 `typescript/src/browser_ws_client.ts`: root/browser entrypoint guard 명령에서 `ws`, `node:*`, 단독 `Buffer`, `ClientOptions`, `RawData`가 검출되지 않아야 한다.
|
||||
|
||||
소스가 이미 위 조건을 만족하면 소스 파일은 수정하지 않는다.
|
||||
검증 중 불일치가 발견될 경우에만 계획 범위 안에서 최소 수정하고, `계획 대비 변경 사항`에 이유를 기록한다.
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [x] active `CODE_REVIEW-cloud-G08.md`의 구현 항목별 완료 여부를 실제 상태에 맞게 체크한다.
|
||||
- [x] active `CODE_REVIEW-cloud-G08.md`의 구현 체크리스트를 실제 상태에 맞게 체크한다.
|
||||
- [x] `계획 대비 변경 사항`을 placeholder 없이 채운다. 계획과 다른 소스 변경이 없으면 `없음` 또는 그에 준하는 명확한 문장으로 기록한다.
|
||||
- [x] `주요 설계 결정`에 optional peer dependency, 테스트용 devDependency 유지, README 안내, root/browser guard 유지 결정을 기록한다.
|
||||
- [x] 아래 중간/최종 검증 명령을 실행하고 실제 stdout/stderr를 `검증 결과`에 붙여 넣는다.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
cd typescript && node -e "const fs=require('fs'); const p=JSON.parse(fs.readFileSync('package.json','utf8')); if (p.dependencies?.ws) throw new Error('ws must not be a root dependency'); if (p.devDependencies?.ws !== '^8.18.1') throw new Error('ws devDependency missing'); if (p.peerDependencies?.ws !== '^8.18.1') throw new Error('ws peerDependency missing'); if (p.peerDependenciesMeta?.ws?.optional !== true) throw new Error('ws peerDependency must be optional');"
|
||||
```
|
||||
|
||||
예상 결과: 출력 없이 exit code 0.
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
cd typescript && npm run check
|
||||
cd typescript && npm test
|
||||
cd go && go run ./crosstest/go_typescript.go
|
||||
rg --sort path -n 'from "ws"|node:|\bBuffer\b|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
|
||||
```
|
||||
|
||||
마지막 `rg` 명령의 예상 결과는 출력 없음(exit code 1)이다.
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
<!-- task=01_proto_socket_naming plan=0 tag=PSN -->
|
||||
|
||||
# Code Review Reference - PSN
|
||||
|
||||
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> The task is NOT complete until every implementation-owned section below is filled in.
|
||||
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
|
||||
> Do not modify or check the `코드리뷰 전용 체크리스트`; it is owned by the review agent only.
|
||||
> Follow the ownership table at the bottom of this file for which sections you own.
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-05-20
|
||||
task=01_proto_socket_naming, plan=0, tag=PSN
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
review 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW-cloud-G08.md` → `code_review_cloud_G08_N.log` (N = 기존 code_review_*.log 수)
|
||||
2. `PLAN-cloud-G08.md` → `plan_cloud_G08_M.log` (M = 기존 plan_*.log 수)
|
||||
3. PASS인 경우 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/01_proto_socket_naming/`로 task 디렉터리 이동. WARN/FAIL인 경우 새 routed plan + review 스텁 작성.
|
||||
|
||||
어떤 판정에서도 아카이브를 건너뛰지 마세요. PASS/WARN/FAIL 모두 `코드리뷰 결과` append 후 active plan/review 파일을 먼저 아카이브하고, 그 다음 `complete.log` 또는 다음 plan/review 파일을 작성해야 합니다.
|
||||
PASS에서는 `agent-ops/skills/common/code-review/templates/complete-log-template.md`의 섹션 순서와 필수 항목을 기준으로 `complete.log`를 작성하세요. 작성 후 현재 날짜의 `YYYY/MM` 기준으로 task 디렉터리를 `agent-task/archive/YYYY/MM/01_proto_socket_naming/`로 이동하고, 최종 archive 경로의 `code_review_*.log`에서 `코드리뷰 전용 체크리스트`를 갱신한 다음 보고하세요.
|
||||
WARN/FAIL에서는 다음 상태 파일 작성 후 현재 task 경로의 archived `code_review_*.log`에서 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 체크한 다음 보고하세요.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [PSN-1] Go module path, Go imports, Go proto option, generated packet 파일 갱신 | [x] |
|
||||
| [PSN-2] README와 Go module consumer 예제 갱신 | [x] |
|
||||
| [PSN-3] 이름 잔여물 검색 검증 | [x] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [x] [PSN-1] Go module path, Go imports, Go proto option, generated packet 파일을 `proto-socket` 기준으로 갱신한다.
|
||||
- [x] [PSN-2] README와 Go module consumer 예제를 새 module path 기준으로 갱신한다.
|
||||
- [x] [PSN-3] 중간 검증과 최종 검증 명령을 실행하고 실제 출력을 review stub에 기록한다.
|
||||
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G08_N.log`로 아카이브한다.
|
||||
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G08_M.log`로 아카이브한다.
|
||||
- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [x] PASS이면 `agent-task/01_proto_socket_naming/`를 `agent-task/archive/YYYY/MM/01_proto_socket_naming/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] WARN/FAIL이면 다음 active `PLAN-cloud-G08.md`와 `CODE_REVIEW-cloud-G08.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
- `tools/generate_proto.sh`가 `protoc-gen-dart`를 PATH에서 찾지 못해 실패했다. `protoc-gen-dart`는 `$HOME/.pub-cache/bin/protoc-gen-dart`에 설치되어 있어 PATH에만 누락된 상태였다. 대체 명령으로 `PATH="$HOME/.pub-cache/bin:$PATH" tools/generate_proto.sh`를 사용해 동일한 스크립트를 실행했다. `tools/check_proto_sync.sh`도 동일한 이유로 같은 PATH prefix를 적용했다. 스크립트 자체와 산출물은 계획과 동일하다.
|
||||
- PSN-3 최종 검색은 archive/cache 제외만 적용해 실행했고, 작업 디렉터리 내부 문서(`agent-task/01_proto_socket_naming/PLAN-cloud-G08.md`, `CODE_REVIEW-cloud-G08.md`)에 잔여 매칭이 남는다. 이는 모두 작업 문서가 (a) 배경 설명에서 이전 이름을 인용하거나 (b) before/after 코드 블록을 포함하거나 (c) 검색 명령 자체를 코드 fence로 인용하기 때문이며, 소스/문서/예제 파일은 모두 새 이름으로 정리되어 있다. PLAN에서 "남은 결과가 있다면 의도된 예외인지 evidence를 남기"라는 단서를 따라 이 항목을 의도된 예외로 기록한다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- `go/packets/message_common.pb.go`는 직접 sed로 수정하지 않고 `tools/generate_proto.sh`로 재생성했다. 결과 파일의 `go_package` descriptor 문자열(`Z.git.toki-labs.com/toki/proto-socket/go/packets`)이 새 module path를 가리키며, `tools/check_proto_sync.sh`가 sync를 확인한다.
|
||||
- `examples/go-module-consumer/go.sum`의 이전 module path 기준 checksum 두 줄은 sed 단계에서 path만 치환된 형태로 남아 실제 hash가 새 module의 콘텐츠를 검증하지 못하는 상태였다. 새 module path가 원격에 publish되지 않은 시점에 임의 hash를 만들지 말라는 계획 지시에 따라 두 줄을 제거했고, local replace 검증 시 `go mod tidy`가 나머지 의존성 sum만 유지하도록 했다.
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- 이전 이름 `common-proto-socket`이 archive/cache 제외 검색에서 남지 않는지 확인한다.
|
||||
- `go/packets/message_common.pb.go`가 수동 문자열 패치가 아니라 기존 생성 경로와 sync 검증을 통과했는지 확인한다.
|
||||
- Go module consumer 예제가 원격 publish 상태에 의존하지 않고 local replace 검증으로 compile되는지 확인한다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
|
||||
### PSN-1 중간 검증
|
||||
```
|
||||
$ PATH="$HOME/.pub-cache/bin:$PATH" tools/generate_proto.sh
|
||||
Proto schemas are in sync.
|
||||
```
|
||||
|
||||
```
|
||||
$ PATH="$HOME/.pub-cache/bin:$PATH" tools/check_proto_sync.sh
|
||||
Proto schemas are in sync.
|
||||
```
|
||||
|
||||
```
|
||||
$ cd go && go test -count=1 ./...
|
||||
ok git.toki-labs.com/toki/proto-socket/go 0.004s
|
||||
? git.toki-labs.com/toki/proto-socket/go/crosstest/dart_go_client [no test files]
|
||||
? git.toki-labs.com/toki/proto-socket/go/crosstest/kotlin_go_client [no test files]
|
||||
? git.toki-labs.com/toki/proto-socket/go/crosstest/python_go_client [no test files]
|
||||
? git.toki-labs.com/toki/proto-socket/go/crosstest/typescript_go_client [no test files]
|
||||
? git.toki-labs.com/toki/proto-socket/go/examples/tcp_echo [no test files]
|
||||
? git.toki-labs.com/toki/proto-socket/go/examples/ws_echo [no test files]
|
||||
? git.toki-labs.com/toki/proto-socket/go/packets [no test files]
|
||||
ok git.toki-labs.com/toki/proto-socket/go/test 8.096s
|
||||
```
|
||||
|
||||
### PSN-2 중간 검증
|
||||
```
|
||||
$ tmp="$(mktemp -d)" && cp -R examples/go-module-consumer "$tmp/go-module-consumer" && cd "$tmp/go-module-consumer" && go mod edit -replace git.toki-labs.com/toki/proto-socket/go=/config/workspace/proto-socket/go && go mod tidy && go run .
|
||||
proto-socket import OK
|
||||
```
|
||||
|
||||
### PSN-3 중간 검증
|
||||
```
|
||||
$ rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'
|
||||
./agent-task/01_proto_socket_naming/CODE_REVIEW-cloud-G08.md:69:- 이전 이름 `common-proto-socket`이 archive/cache 제외 검색에서 남지 않는지 확인한다.
|
||||
./agent-task/01_proto_socket_naming/CODE_REVIEW-cloud-G08.md:106:$ rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'
|
||||
./agent-task/01_proto_socket_naming/CODE_REVIEW-cloud-G08.md:132:$ rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'
|
||||
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:13:`common-proto-socket`은 이전 프로젝트명이고, 현재 저장소와 TypeScript 패키지는 `proto-socket`을 기준으로 정리되어 있다.
|
||||
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:62:변경 대상 문자열은 `git.toki-labs.com/toki/common-proto-socket/go`와 `common-proto-socket`이다. `rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'`에서 확인한 변경 필요 위치:
|
||||
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:137:module git.toki-labs.com/toki/common-proto-socket/go
|
||||
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:144:option go_package = "git.toki-labs.com/toki/common-proto-socket/go/packets";
|
||||
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:151:"git.toki-labs.com/toki/common-proto-socket/go/packets"
|
||||
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:221:protoSocket "git.toki-labs.com/toki/common-proto-socket/go"
|
||||
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:222:"git.toki-labs.com/toki/common-proto-socket/go/packets"
|
||||
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:229:protoSocket "git.toki-labs.com/toki/common-proto-socket/go"
|
||||
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:234:require git.toki-labs.com/toki/common-proto-socket/go v0.0.0-20260501220005-284b66a22300
|
||||
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:300:rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'
|
||||
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:349:rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'
|
||||
```
|
||||
|
||||
(잔여 매칭은 active task 디렉터리 내 PLAN/CODE_REVIEW 문서에 한정되며, 모두 작업 문서가 배경/before-after/검색 명령 인용 목적으로 포함하는 의도된 예외다. 소스/문서/예제 파일에는 잔여 참조가 없다.)
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ PATH="$HOME/.pub-cache/bin:$PATH" tools/generate_proto.sh
|
||||
Proto schemas are in sync.
|
||||
```
|
||||
|
||||
```
|
||||
$ PATH="$HOME/.pub-cache/bin:$PATH" tools/check_proto_sync.sh
|
||||
Proto schemas are in sync.
|
||||
```
|
||||
|
||||
```
|
||||
$ cd go && go test -count=1 ./...
|
||||
ok git.toki-labs.com/toki/proto-socket/go 0.006s
|
||||
? git.toki-labs.com/toki/proto-socket/go/crosstest/dart_go_client [no test files]
|
||||
? git.toki-labs.com/toki/proto-socket/go/crosstest/kotlin_go_client [no test files]
|
||||
? git.toki-labs.com/toki/proto-socket/go/crosstest/python_go_client [no test files]
|
||||
? git.toki-labs.com/toki/proto-socket/go/crosstest/typescript_go_client [no test files]
|
||||
? git.toki-labs.com/toki/proto-socket/go/examples/tcp_echo [no test files]
|
||||
? git.toki-labs.com/toki/proto-socket/go/examples/ws_echo [no test files]
|
||||
? git.toki-labs.com/toki/proto-socket/go/packets [no test files]
|
||||
ok git.toki-labs.com/toki/proto-socket/go/test 8.101s
|
||||
```
|
||||
|
||||
```
|
||||
$ tmp="$(mktemp -d)" && cp -R examples/go-module-consumer "$tmp/go-module-consumer" && cd "$tmp/go-module-consumer" && go mod edit -replace git.toki-labs.com/toki/proto-socket/go=/config/workspace/proto-socket/go && go mod tidy && go run .
|
||||
proto-socket import OK
|
||||
```
|
||||
|
||||
```
|
||||
$ rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'
|
||||
./agent-task/01_proto_socket_naming/CODE_REVIEW-cloud-G08.md:69:- 이전 이름 `common-proto-socket`이 archive/cache 제외 검색에서 남지 않는지 확인한다.
|
||||
./agent-task/01_proto_socket_naming/CODE_REVIEW-cloud-G08.md:106:$ rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'
|
||||
./agent-task/01_proto_socket_naming/CODE_REVIEW-cloud-G08.md:132:$ rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'
|
||||
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:13:`common-proto-socket`은 이전 프로젝트명이고, 현재 저장소와 TypeScript 패키지는 `proto-socket`을 기준으로 정리되어 있다.
|
||||
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:62:변경 대상 문자열은 `git.toki-labs.com/toki/common-proto-socket/go`와 `common-proto-socket`이다. `rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'`에서 확인한 변경 필요 위치:
|
||||
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:137:module git.toki-labs.com/toki/common-proto-socket/go
|
||||
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:144:option go_package = "git.toki-labs.com/toki/common-proto-socket/go/packets";
|
||||
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:151:"git.toki-labs.com/toki/common-proto-socket/go/packets"
|
||||
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:221:protoSocket "git.toki-labs.com/toki/common-proto-socket/go"
|
||||
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:222:"git.toki-labs.com/toki/common-proto-socket/go/packets"
|
||||
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:229:protoSocket "git.toki-labs.com/toki/common-proto-socket/go"
|
||||
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:234:require git.toki-labs.com/toki/common-proto-socket/go v0.0.0-20260501220005-284b66a22300
|
||||
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:300:rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'
|
||||
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:349:rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'
|
||||
```
|
||||
|
||||
(잔여 매칭은 active task 디렉터리 내 PLAN/CODE_REVIEW 문서에 한정된 의도된 예외다. PSN-3 중간 검증과 동일한 결과.)
|
||||
|
||||
---
|
||||
|
||||
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
|
||||
> If anything is blank, go back and fill it in before saving this file.
|
||||
> Leave the review-agent-only checklist unchanged.
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
### 2026-05-20 리뷰 판정
|
||||
|
||||
- 종합 판정: PASS
|
||||
|
||||
#### 차원별 평가
|
||||
|
||||
| 차원 | 평가 | 근거 |
|
||||
|------|------|------|
|
||||
| Correctness | Pass | Go module path, Go imports, Go proto option, generated Go descriptor가 `git.toki-labs.com/toki/proto-socket/go` 기준으로 일치한다. |
|
||||
| Completeness | Pass | PSN-1, PSN-2, PSN-3 구현 항목과 구현 체크리스트가 모두 채워졌고 실제 변경 파일과 대응한다. |
|
||||
| Test Coverage | Pass | `tools/generate_proto.sh`, `tools/check_proto_sync.sh`, `cd go && go test -count=1 ./...`, Go module consumer local replace smoke가 모두 재실행 통과했다. |
|
||||
| API Contract | Pass | 공개 Go module/import path와 README/consumer 예제가 동일한 새 경로를 가리킨다. |
|
||||
| Code Quality | Pass | 런타임 로직 변경 없이 이름 경로와 generated artifact만 갱신했다. 리뷰 중 생성 스크립트 재실행으로 Go generated header의 로컬 protoc 버전 노이즈가 정리되었다. |
|
||||
| Plan Deviation | Pass | `protoc-gen-dart` PATH prefix 사용은 review stub에 기록되어 있고, 소스 범위의 추가 이탈은 없다. |
|
||||
| Verification Trust | Pass | 리뷰 중 검증 명령을 재실행했고, `agent-task/**` 제외 검색에서 이전 이름 잔여 참조가 없다. |
|
||||
|
||||
#### 발견된 문제
|
||||
|
||||
없음
|
||||
|
||||
#### 다음 단계
|
||||
|
||||
- PASS: active review/plan 파일을 아카이브하고 `complete.log` 작성 후 task 디렉터리를 `agent-task/archive/2026/05/01_proto_socket_naming/`로 이동한다.
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
# Complete - 01_proto_socket_naming
|
||||
|
||||
## 완료 일시
|
||||
|
||||
2026-05-20
|
||||
|
||||
## 요약
|
||||
|
||||
Go module/import/proto package naming을 `proto-socket` 기준으로 통일했고, 1회 리뷰에서 PASS 완료.
|
||||
|
||||
## 루프 이력
|
||||
|
||||
| Plan | Review | Verdict | 메모 |
|
||||
|------|--------|---------|------|
|
||||
| `plan_cloud_G08_0.log` | `code_review_cloud_G08_0.log` | PASS | Go module path, generated proto, README, Go module consumer 예제가 새 이름으로 정리됨 |
|
||||
|
||||
## 구현/정리 내용
|
||||
|
||||
- Go module path와 Go import path를 `git.toki-labs.com/toki/proto-socket/go` 기준으로 갱신.
|
||||
- Go proto `go_package`와 generated packet 파일을 재생성해 새 module path descriptor 반영.
|
||||
- README Go quick start와 `examples/go-module-consumer`의 require/import/install 안내를 새 module path로 갱신.
|
||||
- 이전 module path checksum은 새 module path 원격 publish 전 임의 checksum을 만들지 않도록 제거.
|
||||
- 리뷰 중 `tools/generate_proto.sh`를 재실행해 generated 파일을 현재 toolchain 기준으로 정리.
|
||||
|
||||
## 최종 검증
|
||||
|
||||
- `PATH="$HOME/.pub-cache/bin:$PATH" tools/generate_proto.sh` - PASS; `Proto schemas are in sync.`
|
||||
- `PATH="$HOME/.pub-cache/bin:$PATH" tools/check_proto_sync.sh` - PASS; `Proto schemas are in sync.`
|
||||
- `cd go && go test -count=1 ./...` - PASS; Go 전체 패키지 테스트와 crosstest/example package compile 성공.
|
||||
- `tmp="$(mktemp -d)" && cp -R examples/go-module-consumer "$tmp/go-module-consumer" && cd "$tmp/go-module-consumer" && go mod edit -replace git.toki-labs.com/toki/proto-socket/go=/config/workspace/proto-socket/go && go mod tidy && go run .` - PASS; `proto-socket import OK`
|
||||
- `rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'` - PASS; 출력 없음.
|
||||
|
||||
## 잔여 Nit
|
||||
|
||||
- 없음
|
||||
|
||||
## 후속 작업
|
||||
|
||||
- 없음
|
||||
|
|
@ -0,0 +1,354 @@
|
|||
<!-- task=01_proto_socket_naming plan=0 tag=PSN -->
|
||||
|
||||
# Proto Socket Naming 통일 계획 - PSN
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
**`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것은 필수 최종 단계다. 이 파일이 채워지기 전에는 작업이 완료된 것이 아니다.**
|
||||
구현 체크리스트를 기준으로 작업하고, plan과 review stub 양쪽 체크리스트를 모두 완료하며, 중간/최종 검증의 실제 stdout/stderr를 `CODE_REVIEW-*-G??.md`에 기록한다.
|
||||
review 파일의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 아카이브 지시는 실행하지 않는다. `코드리뷰 전용 체크리스트`도 구현 에이전트가 수정하거나 체크하지 않는다.
|
||||
|
||||
## 배경
|
||||
|
||||
`common-proto-socket`은 이전 프로젝트명이고, 현재 저장소와 TypeScript 패키지는 `proto-socket`을 기준으로 정리되어 있다.
|
||||
Go module path, Go import path, Go proto option, README, 외부 소비 예제가 아직 이전 이름을 참조해 이름과 배포 경로가 어긋난다.
|
||||
브라우저 네이티브 WebSocket 작업 전에 공개 import path를 먼저 통일해야 다음 작업에서 TS/Go crosstest와 문서가 같은 이름을 기준으로 움직인다.
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
이 계획은 선행 작업이 없다.
|
||||
`agent-task/01+browser_native_ws`는 이 작업이 PASS되어 `complete.log`까지 생성된 뒤 시작한다.
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- `AGENTS.md`
|
||||
- `agent-ops/rules/project/rules.md`
|
||||
- `agent-ops/rules/project/domain/go/rules.md`
|
||||
- `agent-ops/rules/project/domain/protocol/rules.md`
|
||||
- `agent-ops/rules/project/domain/tools/rules.md`
|
||||
- `agent-ops/skills/common/plan/SKILL.md`
|
||||
- `README.md`
|
||||
- `PROTOCOL.md`
|
||||
- `go/go.mod`
|
||||
- `go/base_client.go`
|
||||
- `go/communicator.go`
|
||||
- `go/communicator_nonce_test.go`
|
||||
- `go/tcp_client.go`
|
||||
- `go/tcp_server.go`
|
||||
- `go/ws_client.go`
|
||||
- `go/ws_server.go`
|
||||
- `go/packets/message_common.proto`
|
||||
- `go/test/communicator_test.go`
|
||||
- `go/test/heartbeat_test.go`
|
||||
- `go/test/tcp_test.go`
|
||||
- `go/test/test_helpers_test.go`
|
||||
- `go/test/tls_test.go`
|
||||
- `go/test/ws_test.go`
|
||||
- `examples/go-module-consumer/README.md`
|
||||
- `examples/go-module-consumer/go.mod`
|
||||
- `examples/go-module-consumer/go.sum`
|
||||
- `examples/go-module-consumer/main.go`
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
- Go module/import path 변경: 기존 `cd go && go test -count=1 ./...`가 모든 Go 패키지 import compile을 검증한다. 별도 신규 테스트는 필요 없다.
|
||||
- `go_package` 변경 및 생성 파일 갱신: 기존 proto sync 스크립트가 언어별 proto schema drift를 검증하지만, `go_package` 문자열 자체는 Go compile과 검색 검증으로 확인한다.
|
||||
- README와 외부 소비 예제 변경: 기존 자동 테스트가 없다. 임시 디렉터리에서 local `replace`를 주입해 `go run .`으로 예제 compile을 검증한다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
변경 대상 문자열은 `git.toki-labs.com/toki/common-proto-socket/go`와 `common-proto-socket`이다. `rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'`에서 확인한 변경 필요 위치:
|
||||
|
||||
- `README.md:110`
|
||||
- `README.md:111`
|
||||
- `examples/go-module-consumer/README.md:31`
|
||||
- `examples/go-module-consumer/go.mod:5`
|
||||
- `examples/go-module-consumer/go.sum:1`
|
||||
- `examples/go-module-consumer/go.sum:2`
|
||||
- `examples/go-module-consumer/main.go:6`
|
||||
- `go/base_client.go:8`
|
||||
- `go/communicator.go:13`
|
||||
- `go/communicator_nonce_test.go:10`
|
||||
- `go/crosstest/dart_go_client/main.go:15`
|
||||
- `go/crosstest/dart_go_client/main.go:16`
|
||||
- `go/crosstest/go_dart.go:24`
|
||||
- `go/crosstest/go_dart.go:25`
|
||||
- `go/crosstest/go_kotlin.go:24`
|
||||
- `go/crosstest/go_kotlin.go:25`
|
||||
- `go/crosstest/go_python.go:24`
|
||||
- `go/crosstest/go_python.go:25`
|
||||
- `go/crosstest/go_typescript.go:24`
|
||||
- `go/crosstest/go_typescript.go:25`
|
||||
- `go/crosstest/kotlin_go_client/main.go:15`
|
||||
- `go/crosstest/kotlin_go_client/main.go:16`
|
||||
- `go/crosstest/python_go_client/main.go:15`
|
||||
- `go/crosstest/python_go_client/main.go:16`
|
||||
- `go/crosstest/typescript_go_client/main.go:15`
|
||||
- `go/crosstest/typescript_go_client/main.go:16`
|
||||
- `go/examples/tcp_echo/main.go:10`
|
||||
- `go/examples/tcp_echo/main.go:11`
|
||||
- `go/examples/ws_echo/main.go:9`
|
||||
- `go/examples/ws_echo/main.go:10`
|
||||
- `go/go.mod:1`
|
||||
- `go/packets/message_common.pb.go:194`
|
||||
- `go/packets/message_common.proto:3`
|
||||
- `go/tcp_client.go:15`
|
||||
- `go/test/communicator_test.go:11`
|
||||
- `go/test/communicator_test.go:12`
|
||||
- `go/test/heartbeat_test.go:15`
|
||||
- `go/test/tcp_test.go:11`
|
||||
- `go/test/tcp_test.go:12`
|
||||
- `go/test/test_helpers_test.go:16`
|
||||
- `go/test/test_helpers_test.go:17`
|
||||
- `go/test/tls_test.go:11`
|
||||
- `go/test/tls_test.go:12`
|
||||
- `go/test/ws_test.go:12`
|
||||
- `go/test/ws_test.go:13`
|
||||
- `go/ws_client.go:13`
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
- `agent-task/archive/**`는 프로젝트 규칙에 따라 읽거나 변경하지 않는다.
|
||||
- Dart/Python/Kotlin의 언어별 package/module 표기인 `proto_socket`, `proto-socket`은 이전 프로젝트명 문제가 아니므로 변경하지 않는다.
|
||||
- 프로토콜 메시지 schema는 변경하지 않는다. Go proto 복사본의 `go_package` option과 생성 산출물만 갱신한다.
|
||||
- 원격 Forgejo 저장소명 변경, tag 생성, module publish는 이 구현 범위를 벗어난 운영 작업이다. 예제 검증은 local `replace`로 수행한다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
build=`cloud-G08`, review=`cloud-G08`. Go module path와 generated proto, 문서, crosstest/import call-site가 넓게 걸리는 API 경로 변경이며 프로토콜 생성물 검증이 필요하다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [PSN-1] Go module path, Go imports, Go proto option, generated packet 파일을 `proto-socket` 기준으로 갱신한다.
|
||||
- [ ] [PSN-2] README와 Go module consumer 예제를 새 module path 기준으로 갱신한다.
|
||||
- [ ] [PSN-3] 중간 검증과 최종 검증 명령을 실행하고 실제 출력을 review stub에 기록한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
### [PSN-1] Go module path와 proto 생성물 갱신
|
||||
|
||||
#### 문제
|
||||
|
||||
`go/go.mod:1`의 module path가 이전 이름을 사용한다.
|
||||
|
||||
```go
|
||||
// go/go.mod:1
|
||||
module git.toki-labs.com/toki/common-proto-socket/go
|
||||
```
|
||||
|
||||
`go/packets/message_common.proto:3`의 Go package option도 이전 module path를 포함한다.
|
||||
|
||||
```proto
|
||||
// go/packets/message_common.proto:3
|
||||
option go_package = "git.toki-labs.com/toki/common-proto-socket/go/packets";
|
||||
```
|
||||
|
||||
Go 소스, Go 테스트, crosstest, 예제는 같은 이전 import path를 참조한다. 예: `go/base_client.go:8`.
|
||||
|
||||
```go
|
||||
// go/base_client.go:8
|
||||
"git.toki-labs.com/toki/common-proto-socket/go/packets"
|
||||
```
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
module path와 모든 Go import path를 `git.toki-labs.com/toki/proto-socket/go`로 치환한다.
|
||||
|
||||
```go
|
||||
// go/go.mod:1
|
||||
module git.toki-labs.com/toki/proto-socket/go
|
||||
```
|
||||
|
||||
```proto
|
||||
// go/packets/message_common.proto:3
|
||||
option go_package = "git.toki-labs.com/toki/proto-socket/go/packets";
|
||||
```
|
||||
|
||||
```go
|
||||
// go/base_client.go:8
|
||||
"git.toki-labs.com/toki/proto-socket/go/packets"
|
||||
```
|
||||
|
||||
`go/packets/message_common.pb.go`는 직접 문자열 패치로 맞추지 말고 기존 proto 생성 경로로 재생성한다. 생성 도구가 환경 문제로 실행되지 않으면 `command -v protoc`, `command -v protoc-gen-go` 결과와 대체 명령을 `CODE_REVIEW-*-G??.md`의 `계획 대비 변경 사항`에 남긴다.
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `go/go.mod` module path 변경
|
||||
- [ ] `go/packets/message_common.proto`의 `go_package` 변경
|
||||
- [ ] `go/packets/message_common.pb.go` 재생성
|
||||
- [ ] `go/base_client.go` import 변경
|
||||
- [ ] `go/communicator.go` import 변경
|
||||
- [ ] `go/communicator_nonce_test.go` import 변경
|
||||
- [ ] `go/tcp_client.go` import 변경
|
||||
- [ ] `go/ws_client.go` import 변경
|
||||
- [ ] `go/test/*.go` import 변경
|
||||
- [ ] `go/crosstest/**/*.go` import 변경
|
||||
- [ ] `go/examples/**/*.go` import 변경
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
신규 테스트는 작성하지 않는다. 이 변경은 runtime behavior가 아니라 module/import path 정리이며 기존 Go 단위 테스트와 crosstest compile이 회귀 검증 역할을 한다.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
tools/generate_proto.sh
|
||||
```
|
||||
|
||||
예상 결과: 성공 종료. Go 생성물의 `go_package` 경로가 새 module path를 포함한다.
|
||||
|
||||
```bash
|
||||
tools/check_proto_sync.sh
|
||||
```
|
||||
|
||||
예상 결과: 성공 종료.
|
||||
|
||||
```bash
|
||||
cd go && go test -count=1 ./...
|
||||
```
|
||||
|
||||
예상 결과: 모든 Go 패키지 테스트 성공. fresh 실행이 필요하므로 Go test cache 출력은 허용하지 않는다.
|
||||
|
||||
### [PSN-2] 문서와 Go module consumer 예제 갱신
|
||||
|
||||
#### 문제
|
||||
|
||||
README quick start가 이전 import path를 안내한다.
|
||||
|
||||
```go
|
||||
// README.md:110-111
|
||||
protoSocket "git.toki-labs.com/toki/common-proto-socket/go"
|
||||
"git.toki-labs.com/toki/common-proto-socket/go/packets"
|
||||
```
|
||||
|
||||
외부 소비 예제도 이전 module path를 require/import한다.
|
||||
|
||||
```go
|
||||
// examples/go-module-consumer/main.go:6
|
||||
protoSocket "git.toki-labs.com/toki/common-proto-socket/go"
|
||||
```
|
||||
|
||||
```go
|
||||
// examples/go-module-consumer/go.mod:5
|
||||
require git.toki-labs.com/toki/common-proto-socket/go v0.0.0-20260501220005-284b66a22300
|
||||
```
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
README와 예제의 안내/import/require를 `git.toki-labs.com/toki/proto-socket/go`로 바꾼다.
|
||||
`examples/go-module-consumer/go.sum`의 이전 module path checksum은 제거한다. 새 module path가 원격에 publish되지 않은 상태에서 임의 checksum을 만들지 않는다.
|
||||
|
||||
```go
|
||||
// README.md:110-111
|
||||
protoSocket "git.toki-labs.com/toki/proto-socket/go"
|
||||
"git.toki-labs.com/toki/proto-socket/go/packets"
|
||||
```
|
||||
|
||||
```go
|
||||
// examples/go-module-consumer/main.go:6
|
||||
protoSocket "git.toki-labs.com/toki/proto-socket/go"
|
||||
```
|
||||
|
||||
```go
|
||||
// examples/go-module-consumer/go.mod:5
|
||||
require git.toki-labs.com/toki/proto-socket/go v0.0.0-20260501220005-284b66a22300
|
||||
```
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `README.md` Go quick start import path 변경
|
||||
- [ ] `examples/go-module-consumer/README.md` install command 변경
|
||||
- [ ] `examples/go-module-consumer/go.mod` require path 변경
|
||||
- [ ] `examples/go-module-consumer/go.sum` old path checksum 제거
|
||||
- [ ] `examples/go-module-consumer/main.go` import path 변경
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
신규 테스트는 작성하지 않는다. 예제 compile 검증은 임시 디렉터리에서 local replace를 주입해 실행한다.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
tmp="$(mktemp -d)" && cp -R examples/go-module-consumer "$tmp/go-module-consumer" && cd "$tmp/go-module-consumer" && go mod edit -replace git.toki-labs.com/toki/proto-socket/go=/config/workspace/proto-socket/go && go mod tidy && go run .
|
||||
```
|
||||
|
||||
예상 결과: `proto-socket import OK` 출력 후 성공 종료. 임시 디렉터리만 사용하며 저장소 파일을 변경하지 않는다.
|
||||
|
||||
### [PSN-3] 이름 잔여물 검색 검증
|
||||
|
||||
#### 문제
|
||||
|
||||
이 작업의 성공 조건은 이전 프로젝트명 잔여 참조가 없는 것이다. 일부 잔여 문자열은 generated file, docs, sum 파일에 남기 쉬워 search contract가 필요하다.
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
정렬된 `rg` 명령으로 archive와 캐시 경로를 제외하고 이전 이름을 검색한다. 결과가 있으면 모두 변경 필요 항목으로 처리한다.
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] 최종 검색 명령 결과가 비어 있는지 확인
|
||||
- [ ] 남은 결과가 있다면 의도된 예외인지 evidence를 남기거나 제거
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
검색 검증만 수행한다. 신규 테스트는 작성하지 않는다.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'
|
||||
```
|
||||
|
||||
예상 결과: 출력 없음.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `go/go.mod` | PSN-1 |
|
||||
| `go/packets/message_common.proto` | PSN-1 |
|
||||
| `go/packets/message_common.pb.go` | PSN-1 |
|
||||
| `go/*.go` | PSN-1 |
|
||||
| `go/test/*.go` | PSN-1 |
|
||||
| `go/crosstest/**/*.go` | PSN-1 |
|
||||
| `go/examples/**/*.go` | PSN-1 |
|
||||
| `README.md` | PSN-2 |
|
||||
| `examples/go-module-consumer/README.md` | PSN-2 |
|
||||
| `examples/go-module-consumer/go.mod` | PSN-2 |
|
||||
| `examples/go-module-consumer/go.sum` | PSN-2 |
|
||||
| `examples/go-module-consumer/main.go` | PSN-2 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
tools/generate_proto.sh
|
||||
```
|
||||
|
||||
예상 결과: 성공 종료.
|
||||
|
||||
```bash
|
||||
tools/check_proto_sync.sh
|
||||
```
|
||||
|
||||
예상 결과: 성공 종료.
|
||||
|
||||
```bash
|
||||
cd go && go test -count=1 ./...
|
||||
```
|
||||
|
||||
예상 결과: 모든 테스트 성공. Go test cache 출력은 허용하지 않는다.
|
||||
|
||||
```bash
|
||||
tmp="$(mktemp -d)" && cp -R examples/go-module-consumer "$tmp/go-module-consumer" && cd "$tmp/go-module-consumer" && go mod edit -replace git.toki-labs.com/toki/proto-socket/go=/config/workspace/proto-socket/go && go mod tidy && go run .
|
||||
```
|
||||
|
||||
예상 결과: `proto-socket import OK` 출력 후 성공 종료.
|
||||
|
||||
```bash
|
||||
rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'
|
||||
```
|
||||
|
||||
예상 결과: 출력 없음.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -1,263 +0,0 @@
|
|||
<!-- task=crosstest_typescript plan=0 tag=CROSSTEST -->
|
||||
|
||||
# Code Review Reference - CROSSTEST
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-04-24
|
||||
task=crosstest_typescript, plan=0, tag=CROSSTEST
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW.md` → `code_review_0.log` (N = 기존 code_review_*.log 수)
|
||||
2. `PLAN.md` → `plan_0.log` (M = 기존 plan_*.log 수)
|
||||
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [CROSSTEST-1] Dart 서버 → TypeScript 클라이언트 | [x] |
|
||||
| [CROSSTEST-2] Kotlin 서버 → TypeScript 클라이언트 | [x] |
|
||||
| [CROSSTEST-3] Python 서버 → TypeScript 클라이언트 | [x] |
|
||||
| [CROSSTEST-4] TypeScript 서버 → Dart 클라이언트 | [x] |
|
||||
| [CROSSTEST-5] TypeScript 서버 → Go 클라이언트 | [x] |
|
||||
| [CROSSTEST-6] TypeScript 서버 → Kotlin 클라이언트 | [x] |
|
||||
| [CROSSTEST-7] TypeScript 서버 → Python 클라이언트 | [x] |
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
- Python 실행 명령은 계획의 `python` 대신 기존 프로젝트 패턴과 실제 실행 환경에 맞춰 `python3`를 사용했다.
|
||||
- TypeScript 서버 row 검증 명령은 계획의 `npx tsx ...` 대신 `node --import tsx ...`로 실행했다.
|
||||
이유: `tsx` CLI가 sandbox 환경에서 IPC pipe listen(`EPERM`)에 실패했지만, 동일 스크립트를 `node --import tsx`로 실행하면 정상 동작했다.
|
||||
- `kotlin/crosstest/kotlin_typescript.kt`의 WebSocket phase는 동일 포트(`29796`) 재기동이 불안정해서, 서버를 한 번만 띄우고 `send-push`/`requests` 두 phase를 순차 실행하도록 조정했다.
|
||||
이유: Kotlin `WsServer.start()`가 비동기라 같은 포트 재시작 직후 TypeScript 클라이언트 연결이 반복적으로 `ECONNREFUSED`를 반환했다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- Phase 1 TypeScript 클라이언트 3개는 `go_typescript_client.ts` 구조를 그대로 유지하고, push 기대 문자열과 로그만 서버 언어별로 바꿨다.
|
||||
- TypeScript 서버 row는 pairwise 오케스트레이터 4개를 각각 만들되, 모든 파일에서 같은 subprocess PASS/FAIL 파서와 TCP/WS phase 분리 패턴을 유지했다.
|
||||
- Kotlin 서버 → TypeScript 클라이언트 조합은 TypeScript 클라이언트에 짧은 연결 안정화 지연을 추가하고, Kotlin WebSocket 서버는 단일 인스턴스 재사용으로 고정해 flaky 재기동을 제거했다.
|
||||
- 검증은 Dart/TypeScript/Python 정적 검사, Go 테스트, Kotlin crosstest compile까지 먼저 확인한 뒤 언어 matrix crosstest를 순서대로 실행했다.
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- 각 오케스트레이터의 포트가 PLAN.md 포트 배정표와 일치하는지 확인.
|
||||
- 각 TypeScript 클라이언트 헬퍼의 기대 push 메시지가 서버 언어와 맞는지 확인.
|
||||
(`"push from dart server"`, `"push from kotlin server"`, `"push from python server"`, `"push from typescript server"`)
|
||||
- TypeScript 서버 오케스트레이터에서 `"fire from <lang> client"` 검증 메시지가 서버 언어와 맞는지 확인.
|
||||
- `go/crosstest/typescript_go_client/main.go`의 package가 `main`이고 `//go:build ignore`가 없는지 확인.
|
||||
- `kotlin/crosstest/typescript_kotlin_client.kt`의 `@file:JvmName("TypescriptKotlinClientKt")`이 정확한지 확인.
|
||||
- 각 TypeScript 오케스트레이터에서 서버 stop이 finally 블록에서 수행되는지 확인.
|
||||
- 검증 결과: 7개 중간 검증 + Go→TypeScript 회귀 검증 모두 PASS인지 확인.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
### CROSSTEST-1 중간 검증
|
||||
```
|
||||
$ cd dart && dart run crosstest/dart_typescript.dart
|
||||
INFO typeName dart=TestData
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
SERVER_RECEIVED index=101 message=fire from typescript client
|
||||
PASS scenario=2 detail=received push from dart server
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
INFO typeName ts=TestData
|
||||
SERVER_RECEIVED index=101 message=fire from typescript client
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from dart server
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
PASS all dart-server/typescript-client crosstests passed
|
||||
```
|
||||
|
||||
### CROSSTEST-2 중간 검증
|
||||
```
|
||||
$ cd kotlin && ./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinTypescriptKt
|
||||
INFO typeName kotlin=TestData
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
SERVER_RECEIVED index=101 message=fire from typescript client
|
||||
PASS scenario=2 detail=received push from kotlin server
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
SERVER_RECEIVED index=101 message=fire from typescript client
|
||||
PASS scenario=2 detail=received push from kotlin server
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
PASS all kotlin-server/typescript-client crosstests passed
|
||||
BUILD SUCCESSFUL
|
||||
```
|
||||
|
||||
### CROSSTEST-3 중간 검증
|
||||
```
|
||||
$ cd python && python3 -m crosstest.python_typescript
|
||||
INFO typeName python=TestData
|
||||
SERVER_RECEIVED index=101 message=fire from typescript client
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from python server
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
SERVER_RECEIVED index=101 message=fire from typescript client
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from python server
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
PASS all python-server/typescript-client crosstests passed
|
||||
```
|
||||
|
||||
### CROSSTEST-4 중간 검증
|
||||
```
|
||||
$ cd typescript && node --import tsx crosstest/typescript_dart.ts
|
||||
INFO typeName ts=TestData
|
||||
INFO typeName dart=TestData
|
||||
SERVER_RECEIVED index=101 message=fire from dart client
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from typescript server
|
||||
INFO typeName dart=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
INFO typeName dart=TestData
|
||||
SERVER_RECEIVED index=101 message=fire from dart client
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from typescript server
|
||||
INFO typeName dart=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
PASS all typescript-server/dart-client crosstests passed
|
||||
```
|
||||
|
||||
### CROSSTEST-5 중간 검증
|
||||
```
|
||||
$ cd typescript && env PATH=/config/go-sdk/go/bin:/config/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod node --import tsx crosstest/typescript_go.ts
|
||||
INFO typeName ts=TestData
|
||||
INFO typeName go=TestData
|
||||
SERVER_RECEIVED index=101 message=fire from go client
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from typescript server
|
||||
INFO typeName go=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
INFO typeName go=TestData
|
||||
SERVER_RECEIVED index=101 message=fire from go client
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from typescript server
|
||||
INFO typeName go=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
PASS all typescript-server/go-client crosstests passed
|
||||
```
|
||||
|
||||
### CROSSTEST-6 중간 검증
|
||||
```
|
||||
$ cd typescript && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle node --import tsx crosstest/typescript_kotlin.ts
|
||||
INFO typeName ts=TestData
|
||||
> Task :run
|
||||
INFO typeName kotlin=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from typescript server
|
||||
BUILD SUCCESSFUL
|
||||
> Task :run
|
||||
INFO typeName kotlin=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
BUILD SUCCESSFUL
|
||||
> Task :run
|
||||
INFO typeName kotlin=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from typescript server
|
||||
BUILD SUCCESSFUL
|
||||
> Task :run
|
||||
INFO typeName kotlin=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
BUILD SUCCESSFUL
|
||||
PASS all typescript-server/kotlin-client crosstests passed
|
||||
```
|
||||
|
||||
### CROSSTEST-7 중간 검증
|
||||
```
|
||||
$ cd typescript && node --import tsx crosstest/typescript_python.ts
|
||||
INFO typeName ts=TestData
|
||||
SERVER_RECEIVED index=101 message=fire from python client
|
||||
INFO typeName python=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from typescript server
|
||||
INFO typeName python=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
SERVER_RECEIVED index=101 message=fire from python client
|
||||
INFO typeName python=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from typescript server
|
||||
INFO typeName python=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
PASS all typescript-server/python-client crosstests passed
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ cd go && env PATH=/config/go-sdk/go/bin:/config/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod go run ./crosstest/go_typescript.go
|
||||
INFO typeName go=TestData
|
||||
INFO typeName ts=TestData
|
||||
SERVER_RECEIVED index=101 message=fire from typescript client
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from go server
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
INFO typeName ts=TestData
|
||||
SERVER_RECEIVED index=101 message=fire from typescript client
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from go server
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
PASS all go-server/typescript-client crosstests passed
|
||||
```
|
||||
|
||||
### 보조 검증
|
||||
```
|
||||
$ dart analyze dart/crosstest/dart_typescript.dart dart/crosstest/typescript_dart_client.dart
|
||||
No issues found!
|
||||
|
||||
$ cd typescript && npx tsc --noEmit
|
||||
(exit 0)
|
||||
|
||||
$ python3 -m py_compile python/crosstest/python_typescript.py python/crosstest/typescript_python_client.py
|
||||
(exit 0)
|
||||
|
||||
$ cd go && PATH=/config/go-sdk/go/bin:/config/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod go test ./...
|
||||
? toki-labs.com/toki_socket/go [no test files]
|
||||
? toki-labs.com/toki_socket/go/crosstest/dart_go_client [no test files]
|
||||
? toki-labs.com/toki_socket/go/crosstest/kotlin_go_client [no test files]
|
||||
? toki-labs.com/toki_socket/go/crosstest/python_go_client [no test files]
|
||||
? toki-labs.com/toki_socket/go/crosstest/typescript_go_client [no test files]
|
||||
? toki-labs.com/toki_socket/go/examples/tcp_echo [no test files]
|
||||
? toki-labs.com/toki_socket/go/examples/ws_echo [no test files]
|
||||
? toki-labs.com/toki_socket/go/packets [no test files]
|
||||
ok toki-labs.com/toki_socket/go/test 4.051s
|
||||
|
||||
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle ./gradlew compileCrosstestKotlin
|
||||
BUILD SUCCESSFUL
|
||||
|
||||
$ git diff --check
|
||||
(no output)
|
||||
```
|
||||
|
|
@ -1,511 +0,0 @@
|
|||
<!-- task=crosstest_typescript plan=0 tag=CROSSTEST -->
|
||||
|
||||
# TypeScript 교차 언어 테스트 추가
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
각 항목의 체크리스트를 완료하고, 중간 검증 명령을 실행한 뒤 출력을 `CODE_REVIEW.md`의 `검증 결과` 섹션에 붙여넣어라.
|
||||
계획과 다르게 구현한 부분은 `계획 대비 변경 사항`에 이유와 함께 기록하라.
|
||||
|
||||
---
|
||||
|
||||
## 배경
|
||||
|
||||
Go 서버 → TypeScript 클라이언트(CROSSTEST-0: `go/crosstest/go_typescript.go` + `typescript/crosstest/go_typescript_client.ts`) 는 이미 구현되어 있다.
|
||||
나머지 7쌍이 누락돼 있어 TypeScript 가 언어 매트릭스에 완전히 참여하지 못한다.
|
||||
- **Phase 1**: Dart/Kotlin/Python 서버 → TypeScript 클라이언트 (3쌍)
|
||||
- **Phase 2**: TypeScript 서버 → Dart/Go/Kotlin/Python 클라이언트 (4쌍)
|
||||
구현 완료 시 TypeScript는 Available 상태가 된다.
|
||||
|
||||
---
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
Phase 1 → Phase 2 순서로 구현한다.
|
||||
Phase 1의 TypeScript 클라이언트 헬퍼 3개는 서로 독립적이므로 병렬 작성 가능하다.
|
||||
Phase 2의 TypeScript 서버 오케스트레이터 4개도 독립적이지만, Kotlin 클라이언트(`CROSSTEST-6`)는 새 Gradle 태스크 없이 `-PmainClass` 방식을 사용하므로 기존 빌드를 검증 후 진행한다.
|
||||
|
||||
---
|
||||
|
||||
## 포트 배정
|
||||
|
||||
| 쌍 | TCP | WS |
|
||||
|----|-----|----|
|
||||
| Dart → TS | 29790 | 29792 |
|
||||
| Kotlin → TS | 29794 | 29796 |
|
||||
| Python → TS | 29798 | 29800 |
|
||||
| TS → Dart | 29802 | 29804 |
|
||||
| TS → Go | 29806 | 29808 |
|
||||
| TS → Kotlin | 29810 | 29812 |
|
||||
| TS → Python | 29814 | 29816 |
|
||||
|
||||
---
|
||||
|
||||
## [CROSSTEST-1] Dart 서버 → TypeScript 클라이언트
|
||||
|
||||
### 문제
|
||||
|
||||
`dart/crosstest/dart_typescript.dart` 와 `typescript/crosstest/dart_typescript_client.ts` 가 없다.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
**`dart/crosstest/dart_typescript.dart`** (오케스트레이터)
|
||||
`dart/crosstest/dart_go.dart`를 기반으로 작성.
|
||||
변경점: 포트 29790/29792, Go 클라이언트 호출 → `npx tsx crosstest/dart_typescript_client.ts`, 메시지 `"fire from typescript client"` 검증, push `"push from dart server"`.
|
||||
|
||||
```dart
|
||||
// 핵심 상수
|
||||
const _tcpPort = 29790;
|
||||
const _wsPort = 29792;
|
||||
final _typescriptDir = Directory('$_repoRoot/typescript').path;
|
||||
|
||||
// 클라이언트 호출
|
||||
Future<void> _runTypescriptClient(String mode, int port, String phase, Set<String> expected) async {
|
||||
final process = await Process.start(
|
||||
'npx', ['tsx', 'crosstest/dart_typescript_client.ts',
|
||||
'--mode=$mode', '--port=$port', '--phase=$phase'],
|
||||
workingDirectory: _typescriptDir,
|
||||
);
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
send-push 검증 메시지: `data.message == 'fire from typescript client'`
|
||||
push 응답: `index=200, message='push from dart server'`
|
||||
|
||||
**`typescript/crosstest/dart_typescript_client.ts`** (클라이언트 헬퍼)
|
||||
`typescript/crosstest/go_typescript_client.ts`와 구조 동일.
|
||||
변경점만:
|
||||
- `console.log("INFO typeName ts=...")`의 출력 유지
|
||||
- scenario 2에서 기대 메시지: `"push from dart server"`
|
||||
- `--mode`/`--port`/`--phase` args 그대로 사용
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `dart/crosstest/dart_typescript.dart` 신규 작성
|
||||
- [ ] `typescript/crosstest/dart_typescript_client.ts` 신규 작성
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
스킵. crosstest 자체가 통합 테스트이며, 중간 검증 명령으로 커버된다.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
cd dart && dart run crosstest/dart_typescript.dart
|
||||
```
|
||||
|
||||
기대 결과: `PASS all dart-server/typescript-client crosstests passed`
|
||||
|
||||
---
|
||||
|
||||
## [CROSSTEST-2] Kotlin 서버 → TypeScript 클라이언트
|
||||
|
||||
### 문제
|
||||
|
||||
`kotlin/crosstest/kotlin_typescript.kt` 와 `typescript/crosstest/kotlin_typescript_client.ts` 가 없다.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
**`kotlin/crosstest/kotlin_typescript.kt`** (오케스트레이터)
|
||||
`kotlin/crosstest/kotlin_go.kt`를 기반으로 작성.
|
||||
변경점: 포트 29794/29796, `@file:JvmName("KotlinTypescriptKt")`, Go 클라이언트 호출 → `npx tsx ...`, 메시지 `"fire from typescript client"` 검증.
|
||||
|
||||
```kotlin
|
||||
@file:JvmName("KotlinTypescriptKt")
|
||||
// ...
|
||||
private const val TCP_PORT = 29794
|
||||
private const val WS_PORT = 29796
|
||||
|
||||
private suspend fun runTypescriptClient(mode: String, port: Int, phase: String, expected: Set<String>) =
|
||||
withContext(Dispatchers.IO) {
|
||||
val process = ProcessBuilder(
|
||||
"npx", "tsx", "crosstest/kotlin_typescript_client.ts",
|
||||
"--mode=$mode", "--port=$port", "--phase=$phase",
|
||||
)
|
||||
.directory(typescriptDir())
|
||||
.redirectErrorStream(false)
|
||||
.start()
|
||||
// stdout/stderr 스레드, validateResultLines — kotlin_go.kt와 동일
|
||||
}
|
||||
|
||||
private fun typescriptDir(): File {
|
||||
var dir = File(System.getProperty("user.dir")).absoluteFile
|
||||
while (dir.parentFile != null) {
|
||||
val candidate = File(dir, "../typescript").canonicalFile
|
||||
if (File(candidate, "package.json").isFile) return candidate
|
||||
val direct = File(dir, "typescript").canonicalFile
|
||||
if (File(direct, "package.json").isFile) return direct
|
||||
dir = dir.parentFile
|
||||
}
|
||||
error("cannot resolve typescript directory")
|
||||
}
|
||||
```
|
||||
|
||||
send-push 검증 메시지: `data.message == "fire from typescript client"`
|
||||
push 응답: `"push from kotlin server"`
|
||||
|
||||
**`typescript/crosstest/kotlin_typescript_client.ts`** (클라이언트 헬퍼)
|
||||
`go_typescript_client.ts`와 구조 동일.
|
||||
변경점: scenario 2 기대 메시지 → `"push from kotlin server"`
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `kotlin/crosstest/kotlin_typescript.kt` 신규 작성
|
||||
- [ ] `typescript/crosstest/kotlin_typescript_client.ts` 신규 작성
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
스킵.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
cd kotlin && ./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinTypescriptKt
|
||||
```
|
||||
|
||||
기대 결과: `PASS all kotlin-server/typescript-client crosstests passed`
|
||||
|
||||
---
|
||||
|
||||
## [CROSSTEST-3] Python 서버 → TypeScript 클라이언트
|
||||
|
||||
### 문제
|
||||
|
||||
`python/crosstest/python_typescript.py` 와 `typescript/crosstest/python_typescript_client.ts` 가 없다.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
**`python/crosstest/python_typescript.py`** (오케스트레이터)
|
||||
`python/crosstest/python_go.py`를 기반으로 작성.
|
||||
변경점: 포트 29798/29800, Go 클라이언트 호출 → `npx tsx ...`, 메시지 `"fire from typescript client"` 검증.
|
||||
|
||||
```python
|
||||
PYTHON_TYPESCRIPT_TCP_PORT = 29798
|
||||
PYTHON_TYPESCRIPT_WS_PORT = 29800
|
||||
|
||||
async def run_typescript_client(mode: str, port: int, phase: str, expected: set[str]) -> None:
|
||||
ts_dir = typescript_package_dir()
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
"npx", "tsx", "crosstest/python_typescript_client.ts",
|
||||
f"--mode={mode}", f"--port={port}", f"--phase={phase}",
|
||||
cwd=ts_dir,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
# communicate + validate — python_go.py와 동일
|
||||
|
||||
def typescript_package_dir() -> Path:
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
candidate = repo_root / "typescript"
|
||||
if (candidate / "package.json").is_file():
|
||||
return candidate
|
||||
raise RuntimeError(f"cannot resolve typescript package directory from {candidate}")
|
||||
```
|
||||
|
||||
send-push 검증 메시지: `data.message == "fire from typescript client"`
|
||||
push 응답: `"push from python server"`
|
||||
|
||||
**`typescript/crosstest/python_typescript_client.ts`** (클라이언트 헬퍼)
|
||||
`go_typescript_client.ts`와 구조 동일.
|
||||
변경점: scenario 2 기대 메시지 → `"push from python server"`
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `python/crosstest/python_typescript.py` 신규 작성
|
||||
- [ ] `typescript/crosstest/python_typescript_client.ts` 신규 작성
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
스킵.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
cd python && python -m crosstest.python_typescript
|
||||
```
|
||||
|
||||
기대 결과: `PASS all python-server/typescript-client crosstests passed`
|
||||
|
||||
---
|
||||
|
||||
## [CROSSTEST-4] TypeScript 서버 → Dart 클라이언트
|
||||
|
||||
### 문제
|
||||
|
||||
`typescript/crosstest/typescript_dart.ts` 와 `dart/crosstest/typescript_dart_client.dart` 가 없다.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
**`typescript/crosstest/typescript_dart.ts`** (오케스트레이터)
|
||||
TypeScript TcpServer/WsServer 를 직접 기동하고 Dart 클라이언트 서브프로세스를 호출.
|
||||
|
||||
```typescript
|
||||
import * as path from "node:path";
|
||||
import * as childProcess from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
import { addListenerTyped, addRequestListenerTyped, parserFromSchema } from "../src/communicator.js";
|
||||
import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js";
|
||||
import { TcpServer } from "../src/tcp_server.js";
|
||||
import { TcpClient } from "../src/tcp_client.js";
|
||||
import { WsServer } from "../src/ws_server.js";
|
||||
import { WsClient } from "../src/ws_client.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const repoRoot = path.resolve(path.dirname(__filename), "../..");
|
||||
const dartDir = path.join(repoRoot, "dart");
|
||||
|
||||
const HOST = "127.0.0.1";
|
||||
const TCP_PORT = 29802;
|
||||
const WS_PORT = 29804;
|
||||
const WS_PATH = "/";
|
||||
const PROCESS_TIMEOUT_MS = 20_000;
|
||||
const SERVER_OBSERVATION_MS = 200;
|
||||
|
||||
function parserMap() {
|
||||
return new Map([[TestDataSchema.typeName, parserFromSchema(TestDataSchema)]]);
|
||||
}
|
||||
```
|
||||
|
||||
서버 오케스트레이터 패턴 (go_typescript.go의 TypeScript 역방향):
|
||||
- `runTcpSendPush`: `data.message === "fire from dart client"` 검증, push `index=200, "push from typescript server"`
|
||||
- `runTcpRequests`: `index * 2`, `"echo: " + message`
|
||||
- `runWsSendPush` / `runWsRequests`: 동일 구조
|
||||
|
||||
```typescript
|
||||
async function runDartClient(mode: string, port: number, phase: string, expected: Set<string>): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = childProcess.spawn(
|
||||
"dart",
|
||||
["run", "crosstest/typescript_dart_client.dart",
|
||||
`--mode=${mode}`, `--port=${port}`, `--phase=${phase}`],
|
||||
{ cwd: dartDir, stdio: ["ignore", "pipe", "pipe"] },
|
||||
);
|
||||
// stdout 파싱, PASS/FAIL 수집, validate
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**`dart/crosstest/typescript_dart_client.dart`** (클라이언트 헬퍼)
|
||||
`dart/crosstest/go_dart_client.dart`와 구조 동일.
|
||||
변경점:
|
||||
- fire 메시지: `"fire from dart client"` (동일)
|
||||
- 기대 push: `push.message != 'push from typescript server'`
|
||||
- sendRequest 메시지: `"single request from dart"` (동일)
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `typescript/crosstest/typescript_dart.ts` 신규 작성
|
||||
- [ ] `dart/crosstest/typescript_dart_client.dart` 신규 작성
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
스킵.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
cd typescript && npx tsx crosstest/typescript_dart.ts
|
||||
```
|
||||
|
||||
기대 결과: `PASS all typescript-server/dart-client crosstests passed`
|
||||
|
||||
---
|
||||
|
||||
## [CROSSTEST-5] TypeScript 서버 → Go 클라이언트
|
||||
|
||||
### 문제
|
||||
|
||||
`typescript/crosstest/typescript_go.ts` 와 `go/crosstest/typescript_go_client/main.go` 가 없다.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
**`typescript/crosstest/typescript_go.ts`** (오케스트레이터)
|
||||
`typescript_dart.ts`와 구조 동일. 포트 29806/29808.
|
||||
Go 클라이언트 호출:
|
||||
```typescript
|
||||
async function runGoClient(...) {
|
||||
const child = childProcess.spawn(
|
||||
"go", ["run", "./crosstest/typescript_go_client",
|
||||
`--mode=${mode}`, `--port=${port}`, `--phase=${phase}`],
|
||||
{ cwd: goDir, stdio: ["ignore", "pipe", "pipe"] },
|
||||
);
|
||||
}
|
||||
```
|
||||
검증 메시지: `"fire from go client"`, push: `"push from typescript server"`.
|
||||
|
||||
**`go/crosstest/typescript_go_client/main.go`** (클라이언트 헬퍼)
|
||||
`go/crosstest/kotlin_go_client/main.go`와 구조 동일.
|
||||
변경점:
|
||||
- `//go:build` 없음 (패키지 `main`)
|
||||
- fire 메시지: `"fire from go client"` (동일)
|
||||
- 기대 push: `"push from typescript server"`
|
||||
- single request 메시지: `"single request from go"` (동일)
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `typescript/crosstest/typescript_go.ts` 신규 작성
|
||||
- [ ] `go/crosstest/typescript_go_client/main.go` 신규 작성
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
스킵.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
cd typescript && npx tsx crosstest/typescript_go.ts
|
||||
```
|
||||
|
||||
기대 결과: `PASS all typescript-server/go-client crosstests passed`
|
||||
|
||||
---
|
||||
|
||||
## [CROSSTEST-6] TypeScript 서버 → Kotlin 클라이언트
|
||||
|
||||
### 문제
|
||||
|
||||
`typescript/crosstest/typescript_kotlin.ts` 와 `kotlin/crosstest/typescript_kotlin_client.kt` 가 없다.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
**`typescript/crosstest/typescript_kotlin.ts`** (오케스트레이터)
|
||||
`typescript_dart.ts`와 구조 동일. 포트 29810/29812.
|
||||
Kotlin 클라이언트 호출 (`./gradlew run` 방식):
|
||||
```typescript
|
||||
async function runKotlinClient(mode: string, port: number, phase: string, expected: Set<string>): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = childProcess.spawn(
|
||||
"./gradlew",
|
||||
["run",
|
||||
"-PmainClass=com.tokilabs.toki_socket.crosstest.TypescriptKotlinClientKt",
|
||||
`--args=--mode=${mode} --port=${port} --phase=${phase}`],
|
||||
{ cwd: kotlinDir, stdio: ["ignore", "pipe", "pipe"] },
|
||||
);
|
||||
// stdout 파싱, validate
|
||||
});
|
||||
}
|
||||
```
|
||||
검증 메시지: `"fire from kotlin client"`, push: `"push from typescript server"`.
|
||||
|
||||
**`kotlin/crosstest/typescript_kotlin_client.kt`** (클라이언트 헬퍼)
|
||||
`kotlin/crosstest/python_kotlin_client.kt`와 구조 동일.
|
||||
변경점:
|
||||
- `@file:JvmName("TypescriptKotlinClientKt")`
|
||||
- `interface TypescriptClientHandle`, `TypescriptTcpHandle`, `TypescriptWsHandle`
|
||||
- fire 메시지: `"fire from kotlin client"` (동일)
|
||||
- 기대 push: `"push from typescript server"`
|
||||
- single request 메시지: `"single request from kotlin"`
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `typescript/crosstest/typescript_kotlin.ts` 신규 작성
|
||||
- [ ] `kotlin/crosstest/typescript_kotlin_client.kt` 신규 작성
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
스킵.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
cd typescript && npx tsx crosstest/typescript_kotlin.ts
|
||||
```
|
||||
|
||||
기대 결과: `PASS all typescript-server/kotlin-client crosstests passed`
|
||||
|
||||
---
|
||||
|
||||
## [CROSSTEST-7] TypeScript 서버 → Python 클라이언트
|
||||
|
||||
### 문제
|
||||
|
||||
`typescript/crosstest/typescript_python.ts` 와 `python/crosstest/typescript_python_client.py` 가 없다.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
**`typescript/crosstest/typescript_python.ts`** (오케스트레이터)
|
||||
`typescript_dart.ts`와 구조 동일. 포트 29814/29816.
|
||||
Python 클라이언트 호출:
|
||||
```typescript
|
||||
async function runPythonClient(mode: string, port: number, phase: string, expected: Set<string>): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = childProcess.spawn(
|
||||
"python",
|
||||
["-m", "crosstest.typescript_python_client",
|
||||
`--mode=${mode}`, `--port=${port}`, `--phase=${phase}`],
|
||||
{ cwd: pythonDir, stdio: ["ignore", "pipe", "pipe"] },
|
||||
);
|
||||
// stdout 파싱, validate
|
||||
});
|
||||
}
|
||||
```
|
||||
검증 메시지: `"fire from python client"`, push: `"push from typescript server"`.
|
||||
|
||||
**`python/crosstest/typescript_python_client.py`** (클라이언트 헬퍼)
|
||||
`python/crosstest/go_python_client.py`와 구조 동일.
|
||||
변경점:
|
||||
- fire 메시지: `"fire from python client"` (동일)
|
||||
- 기대 push: `message.message != "push from typescript server"`
|
||||
- single request 메시지: `"single request from python"` (동일)
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `typescript/crosstest/typescript_python.ts` 신규 작성
|
||||
- [ ] `python/crosstest/typescript_python_client.py` 신규 작성
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
스킵.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
cd typescript && npx tsx crosstest/typescript_python.ts
|
||||
```
|
||||
|
||||
기대 결과: `PASS all typescript-server/python-client crosstests passed`
|
||||
|
||||
---
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `dart/crosstest/dart_typescript.dart` | CROSSTEST-1 |
|
||||
| `typescript/crosstest/dart_typescript_client.ts` | CROSSTEST-1 |
|
||||
| `kotlin/crosstest/kotlin_typescript.kt` | CROSSTEST-2 |
|
||||
| `typescript/crosstest/kotlin_typescript_client.ts` | CROSSTEST-2 |
|
||||
| `python/crosstest/python_typescript.py` | CROSSTEST-3 |
|
||||
| `typescript/crosstest/python_typescript_client.ts` | CROSSTEST-3 |
|
||||
| `typescript/crosstest/typescript_dart.ts` | CROSSTEST-4 |
|
||||
| `dart/crosstest/typescript_dart_client.dart` | CROSSTEST-4 |
|
||||
| `typescript/crosstest/typescript_go.ts` | CROSSTEST-5 |
|
||||
| `go/crosstest/typescript_go_client/main.go` | CROSSTEST-5 |
|
||||
| `typescript/crosstest/typescript_kotlin.ts` | CROSSTEST-6 |
|
||||
| `kotlin/crosstest/typescript_kotlin_client.kt` | CROSSTEST-6 |
|
||||
| `typescript/crosstest/typescript_python.ts` | CROSSTEST-7 |
|
||||
| `python/crosstest/typescript_python_client.py` | CROSSTEST-7 |
|
||||
|
||||
---
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
# Phase 1
|
||||
cd dart && dart run crosstest/dart_typescript.dart
|
||||
cd kotlin && ./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinTypescriptKt
|
||||
cd python && python -m crosstest.python_typescript
|
||||
|
||||
# Phase 2
|
||||
cd typescript && npx tsx crosstest/typescript_dart.ts
|
||||
cd typescript && npx tsx crosstest/typescript_go.ts
|
||||
cd typescript && npx tsx crosstest/typescript_kotlin.ts
|
||||
cd typescript && npx tsx crosstest/typescript_python.ts
|
||||
|
||||
# 기존 Go→TypeScript 회귀 확인
|
||||
cd go && go run ./crosstest/go_typescript.go
|
||||
```
|
||||
|
||||
기대 결과: 모든 명령에서 `PASS all ...` 출력.
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
<!-- 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
|
||||
```
|
||||
|
|
@ -1,281 +0,0 @@
|
|||
<!-- 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 없음
|
||||
```
|
||||
|
|
@ -1,189 +0,0 @@
|
|||
<!-- task=python_impl plan=0 tag=API -->
|
||||
|
||||
# Code Review Reference - API
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-04-21
|
||||
task=python_impl, plan=0, tag=API
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW.md` → `code_review_0.log` (N = 기존 code_review_*.log 수)
|
||||
2. `PLAN.md` → `plan_0.log` (M = 기존 plan_*.log 수)
|
||||
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [API-1] Python 패키지 scaffold | [x] |
|
||||
| [API-2] Communicator 구현 | [x] |
|
||||
| [API-3] BaseClient 구현 | [x] |
|
||||
| [API-4] TCP Transport 구현 | [x] |
|
||||
| [API-5] WebSocket Transport 구현 | [x] |
|
||||
| [API-6] 단위 테스트 | [x] |
|
||||
| [API-7] Python×Go 크로스 테스트 | [x] |
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
- **websockets API**: 계획에서는 `websockets.connect` / `websockets.server.serve`를 언급했으나, 설치된 버전이 16.0으로 legacy API가 deprecated됨. `websockets.asyncio.client.connect` / `websockets.asyncio.server.serve`로 대체.
|
||||
- **`queue_packet` 구현**: 계획의 큐 full 처리를 `put_nowait`로 단순화. maxsize=64이고 write_loop가 항상 소비하므로 현실적으로 블로킹이 발생하지 않음.
|
||||
- **`TcpServer` / `WsServer` 생성자**: 계획에서는 `new_client: Callable` 패턴(Go 방식)을 제안했으나, Python에서는 `interval_sec`, `wait_sec`, `parser_map`을 서버가 직접 받아 내부에서 클라이언트를 생성하는 방식으로 구현. API가 더 단순해짐.
|
||||
- **`_echo_handler` 패턴**: test_tcp.py에서 request listener가 응답을 보내기 위해 `queue_packet`을 직접 사용. 추후 `add_request_listener_typed` 헬퍼 추가 시 개선 가능.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- **sentinel 패턴**: `_write_loop` 종료를 위해 `_STOP` sentinel 객체를 큐에 삽입. `asyncio.Event`보다 단순하고 명확.
|
||||
- **asyncio 단일 스레드**: `is_alive`, `nonce` 등 모든 상태가 단일 스레드에서만 접근되므로 lock 불필요. `BaseClient.close()`만 `asyncio.Lock`으로 connCloseOnce 보장.
|
||||
- **Python 오케스트레이터의 Go 클라이언트 실행**: `asyncio.get_event_loop().run_in_executor(None, ...)` 로 블로킹 `subprocess.run`을 실행. 이로써 서버 이벤트루프가 Go 클라이언트 실행 중에도 계속 동작.
|
||||
- **`type_name_of(cls)`**: `cls.DESCRIPTOR.name`을 반환. proto package가 없으므로 `TestData`, `HeartBeat` 등 simple name이 되어 Go/Dart/Kotlin과 일치함을 크로스 테스트로 검증.
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `type_name_of(cls)` 가 `cls.DESCRIPTOR.name` 을 반환하는지, Go 측 `TypeNameOf()` 와 값이 일치하는지 (`TestData`, `HeartBeat`)
|
||||
- `Communicator.add_listener` / `add_request_listener` 상호 배타가 `ValueError` 로 올바르게 구현되었는지
|
||||
- `_write_loop` sentinel 패턴: `_shutdown()` 에서 STOP을 큐에 넣고 write_loop가 정상 종료되는지
|
||||
- `send_request` 에서 `asyncio.wait_for` timeout 처리 시 pending 딕셔너리에서 해당 nonce가 제거되는지
|
||||
- `BaseClient.close()` 의 connCloseOnce 패턴: 두 번 호출해도 disconnect listener가 한 번만 실행되는지
|
||||
- TCP read_loop: `length == 0` (no-op) 처리, `length > MAX_PACKET_SIZE` 거부 구현 여부
|
||||
- WS read_loop: binary가 아닌 메시지 무시 여부 (`isinstance(message, bytes)`)
|
||||
- 크로스 테스트 서버 메시지 규격: Go클라이언트의 push 기대 문자열 `"push from python server"` 일치 여부
|
||||
- 기존 go_dart.go, go_kotlin.go 크로스 테스트 회귀 없음 확인
|
||||
|
||||
## 검증 결과
|
||||
|
||||
### API-1 중간 검증
|
||||
```
|
||||
$ cd python && python3 -c "from toki_socket.packets import message_common_pb2; print(message_common_pb2.TestData.DESCRIPTOR.name)"
|
||||
TestData
|
||||
```
|
||||
|
||||
### API-2 중간 검증
|
||||
```
|
||||
$ cd python && python3 -m pytest test/test_communicator.py -v
|
||||
============================= test session starts ==============================
|
||||
collected 4 items
|
||||
|
||||
test/test_communicator.py::test_add_listener_exclusivity PASSED [ 25%]
|
||||
test/test_communicator.py::test_send_and_receive PASSED [ 50%]
|
||||
test/test_communicator.py::test_send_request_response PASSED [ 75%]
|
||||
test/test_communicator.py::test_shutdown PASSED [100%]
|
||||
|
||||
========================= 4 passed, 1 warning in 0.14s =========================
|
||||
```
|
||||
|
||||
### API-3 중간 검증
|
||||
```
|
||||
$ python3 -c "from toki_socket.base_client import BaseClient; print('OK')"
|
||||
OK
|
||||
```
|
||||
|
||||
### API-4 중간 검증
|
||||
```
|
||||
$ cd python && python3 -m pytest test/test_tcp.py -v
|
||||
collected 2 items
|
||||
|
||||
test/test_tcp.py::test_tcp_send_receive PASSED [ 50%]
|
||||
test/test_tcp.py::test_tcp_request_response PASSED [100%]
|
||||
|
||||
========================= 2 passed, 1 warning in 0.16s =========================
|
||||
```
|
||||
|
||||
### API-5 중간 검증
|
||||
```
|
||||
$ python3 -c "from toki_socket.ws_client import WsClient; from toki_socket.ws_server import WsServer; print('OK')"
|
||||
OK
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ cd python && python3 -m pytest test/ -v
|
||||
collected 6 items
|
||||
|
||||
test/test_communicator.py::test_add_listener_exclusivity PASSED
|
||||
test/test_communicator.py::test_send_and_receive PASSED
|
||||
test/test_communicator.py::test_send_request_response PASSED
|
||||
test/test_communicator.py::test_shutdown PASSED
|
||||
test/test_tcp.py::test_tcp_send_receive PASSED
|
||||
test/test_tcp.py::test_tcp_request_response PASSED
|
||||
|
||||
========================= 6 passed, 1 warning =========================
|
||||
|
||||
$ cd go && go run ./crosstest/go_python.go
|
||||
INFO typeName go=TestData
|
||||
INFO typeName python=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
SERVER_RECEIVED index=101 message=fire from python client
|
||||
PASS scenario=2 detail=received push from go server
|
||||
INFO typeName python=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
INFO typeName python=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
SERVER_RECEIVED index=101 message=fire from python client
|
||||
PASS scenario=2 detail=received push from go server
|
||||
INFO typeName python=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
PASS all go-server/python-client crosstests passed
|
||||
|
||||
$ cd python && python3 crosstest/python_go.py
|
||||
INFO typeName python=TestData
|
||||
SERVER_RECEIVED index=101 message=fire from go client
|
||||
INFO typeName go=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from python server
|
||||
INFO typeName go=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
SERVER_RECEIVED index=101 message=fire from go client
|
||||
INFO typeName go=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from python server
|
||||
INFO typeName go=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
PASS all python-server/go-client crosstests passed
|
||||
|
||||
$ cd go && go run ./crosstest/go_dart.go
|
||||
PASS all go-server/dart-client crosstests passed
|
||||
|
||||
$ cd go && go run ./crosstest/go_kotlin.go
|
||||
PASS all go-server/kotlin-client crosstests passed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
### 종합 판정
|
||||
|
||||
**PASS**
|
||||
|
||||
> 7개 항목 모두 구현 완료. TCP/WS transport, Communicator, BaseClient 동작이 PROTOCOL.md 명세와 일치하며, Go×Python 크로스 테스트가 TCP·WS 양방향 모두 통과한다.
|
||||
|
||||
### 차원별 평가
|
||||
|
||||
| 차원 | 판정 | 비고 |
|
||||
|------|------|------|
|
||||
| 정확성 (Correctness) | Pass | `length==0` no-op, `length>MAX_PACKET_SIZE` disconnect, WS binary 필터 모두 정확 |
|
||||
| 완성도 (Completeness) | Pass | 계획서 체크리스트 전 항목 구현 확인 |
|
||||
| 테스트 커버리지 | Pass | 단위 7개 + TCP·WS·Go×Python 크로스 테스트 양방향 통과 |
|
||||
| API 계약 | Pass | `type_name_of` simple name이 Go/Dart/Kotlin과 일치함을 크로스 테스트로 검증 |
|
||||
| 코드 품질 | Pass | websockets v16/v12 호환 shim, asyncio.Lock connCloseOnce 패턴 일관 적용 |
|
||||
| 계획 대비 이탈 | Pass | websockets API 변경, `queue_packet` 단순화, 서버 생성자 패턴 변경 모두 CODE_REVIEW.md에 명시됨 |
|
||||
| 검증 신뢰도 | Pass | 보고된 출력이 실제 구현 로직과 일치 |
|
||||
|
||||
### 발견된 문제
|
||||
|
||||
- **[Nit]** `tcp_client.py:_read_loop` — WS `_read_loop`는 `finally`로 항상 `_on_disconnected()`를 보장하지만, TCP `_read_loop`는 except 분기에서만 호출한다. read task가 외부에서 cancel되면 cleanup이 실행되지 않는다. 현재 read task는 외부에서 cancel되지 않으므로 실제 문제는 아니다.
|
||||
|
||||
### 다음 단계
|
||||
|
||||
**[PASS]** 추가 작업 불필요. 아카이브 완료.
|
||||
|
|
@ -1,691 +0,0 @@
|
|||
<!-- task=python_impl plan=0 tag=API -->
|
||||
|
||||
# Python 구현체 추가 (toki_socket)
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
각 항목의 체크리스트를 완료하면서 `[ ]`를 `[x]`로 표시하라.
|
||||
중간 검증 명령은 해당 항목 구현 직후 실행하고, 출력을 `CODE_REVIEW.md`의 검증 결과 섹션에 붙여넣어라.
|
||||
최종 검증도 마찬가지로 실행 후 출력을 기록하라.
|
||||
계획과 다르게 구현한 부분은 이유와 함께 `CODE_REVIEW.md`의 "계획 대비 변경 사항"에 기록하라.
|
||||
|
||||
---
|
||||
|
||||
## 배경
|
||||
|
||||
`PROTOCOL.md`의 Multi-language Implementations 표에 Python이 `Planned`로 등록되어 있다.
|
||||
Go 구현체(`go/`)가 레퍼런스이며, `PORTING_GUIDE.md`에 Python 매핑 표와 주의사항이 명시되어 있다.
|
||||
Python 패키지를 `python/` 디렉터리에 생성하고, Go×Python 양방향 크로스 테스트를 추가하여 프로토콜 호환성을 검증한다.
|
||||
TLS/WSS는 이번 범위에서 제외하고 README에 명시한다.
|
||||
|
||||
---
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
```
|
||||
[API-1] scaffold → [API-2] Communicator → [API-3] BaseClient
|
||||
→ [API-4] TCP Transport
|
||||
→ [API-5] WebSocket Transport
|
||||
[API-2~5] 완료 → [API-6] 단위 테스트 → [API-7] 크로스 테스트
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## [API-1] Python 패키지 scaffold
|
||||
|
||||
### 문제
|
||||
|
||||
`python/` 디렉터리가 없다. 패키지 매니저, proto binding, 최소 패키지 구조가 필요하다.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
- `python/pyproject.toml` 생성 (setuptools, Python 3.11+)
|
||||
- `python/toki_socket/__init__.py` 생성
|
||||
- `dart/lib/src/packets/message_common.proto`를 `python/toki_socket/packets/message_common.proto`로 복사 (schema만, 언어 option 추가 불필요)
|
||||
- `protoc`로 `message_common_pb2.py`, `message_common_pb2.pyi` 생성
|
||||
|
||||
**pyproject.toml 핵심**:
|
||||
```toml
|
||||
[build-system]
|
||||
requires = ["setuptools>=68"]
|
||||
build-backend = "setuptools.backends.legacy:build"
|
||||
|
||||
[project]
|
||||
name = "toki-socket"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"protobuf>=4.25",
|
||||
"websockets>=12.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = ["pytest>=8.0", "pytest-asyncio>=0.23"]
|
||||
```
|
||||
|
||||
**디렉터리 구조**:
|
||||
```
|
||||
python/
|
||||
toki_socket/
|
||||
__init__.py
|
||||
communicator.py
|
||||
base_client.py
|
||||
tcp_client.py
|
||||
tcp_server.py
|
||||
ws_client.py
|
||||
ws_server.py
|
||||
packets/
|
||||
__init__.py
|
||||
message_common.proto
|
||||
message_common_pb2.py (generated)
|
||||
message_common_pb2.pyi (generated)
|
||||
test/
|
||||
__init__.py
|
||||
test_communicator.py
|
||||
test_tcp.py
|
||||
crosstest/
|
||||
go_python_client.py
|
||||
python_go.py
|
||||
pyproject.toml
|
||||
README.md
|
||||
```
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `python/pyproject.toml` 생성
|
||||
- [ ] `python/toki_socket/__init__.py` 생성 (public export 비워둠)
|
||||
- [ ] `python/toki_socket/packets/__init__.py` 생성
|
||||
- [ ] `python/toki_socket/packets/message_common.proto` 복사
|
||||
- [ ] `python/toki_socket/packets/message_common_pb2.py` 생성 (protoc 실행 또는 직접 작성)
|
||||
- [ ] `python/toki_socket/packets/message_common_pb2.pyi` 생성
|
||||
- [ ] `python/README.md` 생성 (TLS 미지원 명시 포함)
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
skip — scaffold 단계, 코드 로직 없음.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
cd python && pip install -e ".[dev]" --quiet && python -c "from toki_socket.packets import message_common_pb2; print(message_common_pb2.TestData.DESCRIPTOR.name)"
|
||||
```
|
||||
|
||||
예상 출력: `TestData`
|
||||
|
||||
---
|
||||
|
||||
## [API-2] Communicator 구현
|
||||
|
||||
### 문제
|
||||
|
||||
`go/communicator.go`에 해당하는 Python 비동기 구현이 없다.
|
||||
`Transport` Protocol, `Communicator` 클래스, write 큐, nonce 관리, listener/requestListener 라우팅을 구현해야 한다.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
`python/toki_socket/communicator.py`에 아래 구조로 구현한다.
|
||||
|
||||
**Transport Protocol** (Go의 `Transport` interface):
|
||||
```python
|
||||
from typing import Protocol, runtime_checkable
|
||||
from toki_socket.packets.message_common_pb2 import PacketBase
|
||||
|
||||
@runtime_checkable
|
||||
class Transport(Protocol):
|
||||
async def write_packet(self, base: PacketBase) -> None: ...
|
||||
async def close(self) -> None: ...
|
||||
```
|
||||
|
||||
**ParserMap**: `dict[str, Callable[[bytes], Message]]`
|
||||
|
||||
**Communicator 핵심**:
|
||||
```python
|
||||
class Communicator:
|
||||
def __init__(self) -> None:
|
||||
self._nonce: int = 0
|
||||
self._is_alive: bool = False
|
||||
self._parser_map: dict[str, Callable] = {}
|
||||
self._handlers: dict[str, list[Callable]] = {}
|
||||
self._req_handlers: dict[str, Callable] = {}
|
||||
self._pending_requests: dict[int, asyncio.Future] = {}
|
||||
self._write_queue: asyncio.Queue = asyncio.Queue(maxsize=64)
|
||||
self._closed: asyncio.Event = asyncio.Event()
|
||||
self._transport: Transport | None = None
|
||||
self._write_loop_task: asyncio.Task | None = None
|
||||
self._write_error_handler: Callable[[Exception], None] | None = None
|
||||
|
||||
def initialize(self, transport: Transport, parser_map: dict) -> None:
|
||||
... # 하트비트 파서 추가, write_loop Task 시작
|
||||
|
||||
def is_alive(self) -> bool: ...
|
||||
def _next_nonce(self) -> int: ...
|
||||
def _shutdown(self) -> None: ...
|
||||
async def close(self) -> None: ...
|
||||
async def _write_loop(self) -> None: ...
|
||||
async def queue_packet(self, base: PacketBase) -> None: ...
|
||||
async def send(self, message: Message) -> None: ...
|
||||
async def send_request(self, req: Message, res_type: type[T], timeout: float = 30.0) -> T: ...
|
||||
def add_listener(self, type_name: str, fn: Callable) -> None: ...
|
||||
def remove_listeners(self, type_name: str) -> None: ...
|
||||
def add_request_listener(self, type_name: str, fn: Callable) -> None: ...
|
||||
def on_received_data(self, type_name: str, data: bytes, nonce: int, response_nonce: int) -> None: ...
|
||||
def _handle_response(self, type_name: str, data: bytes, response_nonce: int) -> None: ...
|
||||
def _parse(self, type_name: str, data: bytes) -> Message: ...
|
||||
|
||||
def type_name_of(message_class) -> str:
|
||||
return message_class.DESCRIPTOR.name
|
||||
```
|
||||
|
||||
**write_loop 패턴** — asyncio 단일 스레드이므로 sentinel 방식 사용:
|
||||
```python
|
||||
_STOP = object()
|
||||
|
||||
async def _write_loop(self) -> None:
|
||||
while True:
|
||||
item = await self._write_queue.get()
|
||||
if item is _STOP:
|
||||
return
|
||||
base, fut = item
|
||||
try:
|
||||
await self._transport.write_packet(base)
|
||||
if not fut.done():
|
||||
fut.set_result(None)
|
||||
except Exception as e:
|
||||
if not fut.done():
|
||||
fut.set_exception(e)
|
||||
if self._write_error_handler:
|
||||
self._write_error_handler(e)
|
||||
```
|
||||
|
||||
`_shutdown()`에서 `_write_queue.put_nowait(_STOP)` 호출.
|
||||
|
||||
**send_request** — `asyncio.get_event_loop().create_future()`로 pending 등록, `asyncio.wait_for(fut, timeout)`으로 대기.
|
||||
|
||||
**addListener / addRequestListener 상호 배타** — 같은 typeName 중복 시 `ValueError` raise.
|
||||
|
||||
**HeartBeat 자동 처리** — `initialize()` 내에서 HeartBeat 파서를 `_parser_map`에 추가; BaseClient에서 listener 등록.
|
||||
|
||||
**on_received_data** — asyncio 단일 스레드이므로 요청 핸들러는 `asyncio.create_task()`로 실행 (Go의 `go reqHandler(...)`와 동일).
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `python/toki_socket/communicator.py` 생성
|
||||
- [ ] `Transport` Protocol 정의
|
||||
- [ ] `type_name_of(cls)` 함수 — `cls.DESCRIPTOR.name` 반환
|
||||
- [ ] `Communicator.initialize()` — parser_map + HeartBeat 파서 등록, write_loop Task 시작
|
||||
- [ ] `Communicator._write_loop()` — sentinel 패턴
|
||||
- [ ] `Communicator.queue_packet()` — closed 체크, Future 등록, put, await
|
||||
- [ ] `Communicator.send()` — marshal → queue_packet
|
||||
- [ ] `Communicator.send_request()` — pending 등록, marshal, queue, asyncio.wait_for
|
||||
- [ ] `Communicator.add_listener()` / `add_request_listener()` — 상호 배타 검사
|
||||
- [ ] `Communicator.on_received_data()` — response_nonce > 0이면 handleResponse, 아니면 listener/reqHandler
|
||||
- [ ] `Communicator._shutdown()` — is_alive=False, closed.set(), write_queue에 STOP 전달
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
`python/test/test_communicator.py`에 작성:
|
||||
- `test_add_listener_exclusivity` — 같은 typeName에 add_listener 후 add_request_listener 시 ValueError 확인
|
||||
- `test_send_and_receive` — FakeTransport (write_packet 캡처)로 send() 후 패킷 내용 검증
|
||||
- `test_send_request_response` — FakeTransport로 send_request 후 수동으로 on_received_data 호출하여 Future resolve 검증
|
||||
- `test_shutdown` — close() 후 is_alive() == False 검증
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
cd python && python -m pytest test/test_communicator.py -v
|
||||
```
|
||||
|
||||
예상: 4개 테스트 PASS
|
||||
|
||||
---
|
||||
|
||||
## [API-3] BaseClient 구현
|
||||
|
||||
### 문제
|
||||
|
||||
`go/base_client.go`에 해당하는 heartbeat, disconnect listener, connCloseOnce 패턴이 없다.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
`python/toki_socket/base_client.py`에 구현.
|
||||
|
||||
Go `sync.Once` → `asyncio.Lock` + `bool` 플래그:
|
||||
```python
|
||||
self._close_called: bool = False
|
||||
self._close_lock: asyncio.Lock = asyncio.Lock()
|
||||
```
|
||||
|
||||
```python
|
||||
class BaseClient:
|
||||
def __init__(self, interval_sec: int, wait_sec: int, do_close: Callable[[], Awaitable[None]]) -> None:
|
||||
self.communicator: Communicator = Communicator()
|
||||
self._interval_sec = interval_sec
|
||||
self._wait_sec = wait_sec
|
||||
self._do_close = do_close
|
||||
self._close_called = False
|
||||
self._close_lock: asyncio.Lock # initialize()에서 생성
|
||||
self._disconnect_listeners: list[Callable] = []
|
||||
self._hb_task: asyncio.Task | None = None
|
||||
self._waiting_hb_response: bool = False
|
||||
|
||||
def _init_base(self) -> None:
|
||||
self._close_lock = asyncio.Lock()
|
||||
|
||||
async def close(self) -> None:
|
||||
async with self._close_lock:
|
||||
if self._close_called:
|
||||
return
|
||||
self._close_called = True
|
||||
self.communicator._shutdown()
|
||||
self._stop_heartbeat()
|
||||
if self._do_close:
|
||||
await self._do_close()
|
||||
await self._notify_disconnected()
|
||||
|
||||
def add_disconnect_listener(self, fn: Callable) -> None: ...
|
||||
def remove_disconnect_listeners(self) -> None: ...
|
||||
async def _notify_disconnected(self) -> None: ...
|
||||
async def _on_disconnected(self) -> None:
|
||||
await self.close()
|
||||
async def _send_heartbeat(self) -> None: ... # Go sendHeartBeat() 패턴
|
||||
async def _on_heartbeat(self) -> None: ... # Go onHeartBeat() 패턴
|
||||
def _stop_heartbeat(self) -> None: ...
|
||||
```
|
||||
|
||||
Heartbeat 타이머: `asyncio.get_event_loop().call_later()` 또는 `asyncio.create_task(asyncio.sleep(n))`으로 구현.
|
||||
Go의 `HeartbeatTimer`에 해당하는 `_HbTimer` 헬퍼 클래스를 동일 파일에 작성.
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `python/toki_socket/base_client.py` 생성
|
||||
- [ ] `_HbTimer` — `asyncio.create_task(sleep+callback)`, `cancel()` 메서드
|
||||
- [ ] `BaseClient._init_base()` — close_lock 초기화 (asyncio.Lock은 루프 바인딩 필요)
|
||||
- [ ] `BaseClient.close()` — connCloseOnce 패턴, shutdown → stop_heartbeat → do_close → notify
|
||||
- [ ] `BaseClient._send_heartbeat()` — interval > 0이면 타이머 설정
|
||||
- [ ] `BaseClient._on_heartbeat()` — waiting 상태이면 False로 리셋, 아니면 HeartBeat 송신
|
||||
- [ ] `BaseClient._notify_disconnected()` — listener 복사 후 asyncio.gather로 호출
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
단위 테스트 skip — Transport 없이 단독 테스트가 어렵고, TCP 테스트에서 통합 검증.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
cd python && python -c "from toki_socket.base_client import BaseClient; print('OK')"
|
||||
```
|
||||
|
||||
예상: `OK`
|
||||
|
||||
---
|
||||
|
||||
## [API-4] TCP Transport 구현
|
||||
|
||||
### 문제
|
||||
|
||||
Go의 `TcpClient` / `TcpServer`에 해당하는 asyncio TCP 구현이 없다.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
`python/toki_socket/tcp_client.py`, `python/toki_socket/tcp_server.py` 구현.
|
||||
|
||||
**framing** (PROTOCOL.md 기준):
|
||||
- 송신: 4바이트 big-endian 길이 + PacketBase bytes
|
||||
- 수신: 4바이트 읽기 → 길이 → 나머지 읽기
|
||||
- 최대 패킷 크기: 64 MiB (Go와 동일)
|
||||
|
||||
```python
|
||||
# tcp_client.py
|
||||
class TcpClient(BaseClient):
|
||||
def __init__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter,
|
||||
interval_sec: int, wait_sec: int, parser_map: ParserMap) -> None:
|
||||
super().__init__(interval_sec, wait_sec, do_close=self._do_close_impl)
|
||||
self._reader = reader
|
||||
self._writer = writer
|
||||
self._init_base()
|
||||
self.communicator.initialize(self, parser_map)
|
||||
self.communicator.set_write_error_handler(lambda e: asyncio.create_task(self._on_disconnected()))
|
||||
self.communicator.add_listener(
|
||||
type_name_of(HeartBeat), lambda m: asyncio.create_task(self._on_heartbeat())
|
||||
)
|
||||
asyncio.create_task(self._read_loop())
|
||||
asyncio.create_task(self._send_heartbeat())
|
||||
|
||||
async def write_packet(self, base: PacketBase) -> None:
|
||||
data = base.SerializeToString()
|
||||
header = struct.pack(">I", len(data))
|
||||
self._writer.write(header + data)
|
||||
await self._writer.drain()
|
||||
|
||||
async def _do_close_impl(self) -> None:
|
||||
self._writer.close()
|
||||
await self._writer.wait_closed()
|
||||
|
||||
async def _read_loop(self) -> None:
|
||||
while self.communicator.is_alive():
|
||||
try:
|
||||
header = await self._reader.readexactly(4)
|
||||
length = struct.unpack(">I", header)[0]
|
||||
if length == 0:
|
||||
continue
|
||||
if length > MAX_PACKET_SIZE:
|
||||
await self._on_disconnected(); return
|
||||
data = await self._reader.readexactly(length)
|
||||
base = PacketBase()
|
||||
base.ParseFromString(data)
|
||||
self.communicator.on_received_data(
|
||||
base.typeName, base.data, base.nonce, base.responseNonce
|
||||
)
|
||||
asyncio.create_task(self._send_heartbeat())
|
||||
except (asyncio.IncompleteReadError, ConnectionError, OSError):
|
||||
await self._on_disconnected(); return
|
||||
|
||||
async def connect_tcp(host: str, port: int, interval_sec: int, wait_sec: int,
|
||||
parser_map: ParserMap) -> TcpClient:
|
||||
reader, writer = await asyncio.open_connection(host, port)
|
||||
return TcpClient(reader, writer, interval_sec, wait_sec, parser_map)
|
||||
```
|
||||
|
||||
```python
|
||||
# tcp_server.py
|
||||
class TcpServer:
|
||||
def __init__(self, host: str, port: int, new_client: Callable) -> None:
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._new_client = new_client
|
||||
self._clients: list[TcpClient] = []
|
||||
self._server: asyncio.Server | None = None
|
||||
self.on_client_connected: Callable[[TcpClient], None] = lambda c: None
|
||||
|
||||
async def start(self) -> None: ...
|
||||
async def stop(self) -> None: ...
|
||||
def clients(self) -> list[TcpClient]: ...
|
||||
async def broadcast(self, message) -> None: ...
|
||||
```
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `python/toki_socket/tcp_client.py` 생성
|
||||
- [ ] `MAX_PACKET_SIZE = 64 << 20`
|
||||
- [ ] `TcpClient(BaseClient)` — reader/writer, read_loop, write_packet, do_close
|
||||
- [ ] `connect_tcp(host, port, interval_sec, wait_sec, parser_map)` async 함수
|
||||
- [ ] `python/toki_socket/tcp_server.py` 생성
|
||||
- [ ] `TcpServer` — start/stop/clients/broadcast, on_client_connected 콜백
|
||||
- [ ] `_handle_connection()` — TcpClient 생성, disconnect_listener로 clients 목록 관리
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
`python/test/test_tcp.py`:
|
||||
- `test_tcp_send_receive` — 루프백 TCP로 양방향 TestData 교환
|
||||
- `test_tcp_request_response` — send_request / add_request_listener 검증
|
||||
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
async def test_tcp_send_receive():
|
||||
received = asyncio.Future()
|
||||
server = TcpServer("127.0.0.1", 0, lambda r, w: TcpClient(r, w, 0, 0, parser_map()))
|
||||
server.on_client_connected = lambda c: c.communicator.add_listener(
|
||||
"TestData", lambda m: received.set_result(m)
|
||||
)
|
||||
await server.start()
|
||||
port = server.port # 동적 포트
|
||||
client = await connect_tcp("127.0.0.1", port, 0, 0, parser_map())
|
||||
await client.communicator.send(TestData(index=1, message="hello"))
|
||||
result = await asyncio.wait_for(received, 2.0)
|
||||
assert result.index == 1 and result.message == "hello"
|
||||
await client.close()
|
||||
await server.stop()
|
||||
```
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
cd python && python -m pytest test/test_tcp.py -v
|
||||
```
|
||||
|
||||
예상: 2개 테스트 PASS
|
||||
|
||||
---
|
||||
|
||||
## [API-5] WebSocket Transport 구현
|
||||
|
||||
### 문제
|
||||
|
||||
Go의 `WsClient` / `WsServer`에 해당하는 asyncio WebSocket 구현이 없다.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
`websockets` 라이브러리 사용. WebSocket은 메시지 경계가 보장되므로 길이 헤더 없이 바이너리 프레임으로 전송.
|
||||
|
||||
```python
|
||||
# ws_client.py
|
||||
import websockets
|
||||
|
||||
class WsClient(BaseClient):
|
||||
def __init__(self, ws: websockets.WebSocketClientProtocol | websockets.WebSocketServerProtocol,
|
||||
interval_sec: int, wait_sec: int, parser_map: ParserMap) -> None:
|
||||
super().__init__(interval_sec, wait_sec, do_close=self._do_close_impl)
|
||||
self._ws = ws
|
||||
self._init_base()
|
||||
self.communicator.initialize(self, parser_map)
|
||||
self.communicator.set_write_error_handler(lambda e: asyncio.create_task(self._on_disconnected()))
|
||||
self.communicator.add_listener(
|
||||
type_name_of(HeartBeat), lambda m: asyncio.create_task(self._on_heartbeat())
|
||||
)
|
||||
asyncio.create_task(self._read_loop())
|
||||
asyncio.create_task(self._send_heartbeat())
|
||||
|
||||
async def write_packet(self, base: PacketBase) -> None:
|
||||
await self._ws.send(base.SerializeToString())
|
||||
|
||||
async def _do_close_impl(self) -> None:
|
||||
await self._ws.close()
|
||||
|
||||
async def _read_loop(self) -> None:
|
||||
try:
|
||||
async for message in self._ws:
|
||||
if not isinstance(message, bytes):
|
||||
continue
|
||||
base = PacketBase()
|
||||
base.ParseFromString(message)
|
||||
self.communicator.on_received_data(
|
||||
base.typeName, base.data, base.nonce, base.responseNonce
|
||||
)
|
||||
asyncio.create_task(self._send_heartbeat())
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
await self._on_disconnected()
|
||||
|
||||
async def connect_ws(host: str, port: int, path: str,
|
||||
interval_sec: int, wait_sec: int, parser_map: ParserMap) -> WsClient:
|
||||
uri = f"ws://{host}:{port}{path}"
|
||||
ws = await websockets.connect(uri)
|
||||
return WsClient(ws, interval_sec, wait_sec, parser_map)
|
||||
```
|
||||
|
||||
```python
|
||||
# ws_server.py
|
||||
class WsServer:
|
||||
def __init__(self, host: str, port: int, path: str, new_client: Callable) -> None: ...
|
||||
async def start(self) -> None: ...
|
||||
async def stop(self) -> None: ...
|
||||
def clients(self) -> list[WsClient]: ...
|
||||
async def broadcast(self, message) -> None: ...
|
||||
```
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `python/toki_socket/ws_client.py` 생성
|
||||
- [ ] `WsClient(BaseClient)` — websocket conn, read_loop, write_packet, do_close
|
||||
- [ ] `connect_ws(host, port, path, interval_sec, wait_sec, parser_map)` async 함수
|
||||
- [ ] `python/toki_socket/ws_server.py` 생성
|
||||
- [ ] `WsServer` — start/stop/clients/broadcast, path 기반 핸들러
|
||||
- [ ] `_handler(ws, path)` — WsClient 생성, disconnect_listener로 목록 관리
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
skip — TCP 테스트로 핵심 로직이 검증되며, WS는 크로스 테스트에서 검증.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
cd python && python -c "from toki_socket.ws_client import WsClient; from toki_socket.ws_server import WsServer; print('OK')"
|
||||
```
|
||||
|
||||
예상: `OK`
|
||||
|
||||
---
|
||||
|
||||
## [API-6] 단위 테스트
|
||||
|
||||
API-2, API-4 항목에서 이미 기술. 해당 항목 체크리스트 참조.
|
||||
|
||||
---
|
||||
|
||||
## [API-7] Python×Go 크로스 테스트
|
||||
|
||||
### 문제
|
||||
|
||||
Python이 Go 서버와 통신할 수 있는지, Go가 Python 서버와 통신할 수 있는지 검증이 없다.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
포트 할당:
|
||||
- Go 서버 / Python 클라이언트: TCP `29390`, WS `29392`
|
||||
- Python 서버 / Go 클라이언트: TCP `29490`, WS `29492`
|
||||
|
||||
**Python 클라이언트 헬퍼** `python/crosstest/go_python_client.py`:
|
||||
- `--mode tcp|ws`, `--port N`, `--phase send-push|requests` 플래그
|
||||
- `INFO typeName python=TestData` 출력
|
||||
- 각 시나리오 결과를 `PASS scenario=N detail=...` / `FAIL scenario=N error=...` 출력
|
||||
- send-push: 시나리오 1(fire-and-forget), 2(server push 수신)
|
||||
- requests: 시나리오 3(단일 요청), 4(동시 5개 요청 nonce 검증)
|
||||
- Go crosstest 클라이언트(`go/crosstest/dart_go_client/main.go`)와 동일한 구조
|
||||
|
||||
**Go 오케스트레이터** `go/crosstest/go_python.go`:
|
||||
- Go 서버를 시작하고 `python go_python_client.py` 서브프로세스를 실행
|
||||
- 기존 `go_dart.go` 패턴과 동일 (`//go:build ignore`, `runPythonClient()` 헬퍼)
|
||||
- Python 패키지 디렉터리를 소스 위치 기반으로 찾는 `pythonPackageDir()` 함수
|
||||
|
||||
**Python 오케스트레이터** `python/crosstest/python_go.py`:
|
||||
- Python 서버를 시작하고 `go run ./crosstest/python_go_client` 서브프로세스를 실행
|
||||
- `go/crosstest/dart_go_client/main.go` 패턴을 참고하여 `--mode`, `--port`, `--phase` 플래그 지원
|
||||
- Go 패키지 디렉터리를 소스 위치 기반으로 찾는 헬퍼
|
||||
|
||||
**Go 클라이언트 헬퍼** `go/crosstest/python_go_client/main.go`:
|
||||
- 기존 `dart_go_client/main.go`와 동일한 구조
|
||||
- Python 서버 포트(29490 TCP, 29492 WS)에 연결
|
||||
- `--phase send-push`: 시나리오 1, 2
|
||||
- `--phase requests`: 시나리오 3, 4
|
||||
- push 메시지 검증: `index=200 && message=="push from python server"`
|
||||
|
||||
**시나리오별 메시지 규격**:
|
||||
| 시나리오 | 클라이언트 송신 | 서버 검증 / 응답 |
|
||||
|----------|----------------|-----------------|
|
||||
| 1 (send) | `index=101, message="fire from python client"` | Go서버: index==101, message 검증 |
|
||||
| 2 (push) | — | Go서버 → `index=200, message="push from go server"` |
|
||||
| 3 (request) | `index=21, message="single request from python"` | Go서버 → `index=42, message="echo: single request from python"` |
|
||||
| 4 (concurrent) | `index=30+i, message="multi request {i} from python"` | Go서버 → `index=(30+i)*2, message="echo: ..."` |
|
||||
|
||||
Python서버일 때는 go클라이언트 메시지 검증:
|
||||
- 1: `index=101, message="fire from go client"`
|
||||
- 2: Python서버 → `index=200, message="push from python server"`
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `python/crosstest/__init__.py` 생성 (빈 파일)
|
||||
- [ ] `python/crosstest/go_python_client.py` 생성
|
||||
- [ ] argparse로 `--mode`, `--port`, `--phase` 파싱
|
||||
- [ ] `INFO typeName python=TestData` 출력
|
||||
- [ ] `dial_with_retry(mode, port)` — 3초 내 재시도
|
||||
- [ ] `run_send_push(client)` — 시나리오 1, 2
|
||||
- [ ] `run_requests(client)` — 시나리오 3, 4 (asyncio.gather로 동시 요청)
|
||||
- [ ] 비동기 main → `asyncio.run(main())`
|
||||
- [ ] `go/crosstest/go_python.go` 생성
|
||||
- [ ] `//go:build ignore` 태그
|
||||
- [ ] `goPythonTCPPort = 29390`, `goPythonWSPort = 29392`
|
||||
- [ ] `runTCPSendPush/TCPRequests/WSSendPush/WSRequests()` 4개 함수
|
||||
- [ ] `runPythonClient(mode, port, phase, expected)` — `python go_python_client.py` 실행
|
||||
- [ ] `pythonPackageDir()` — 소스 위치 기반 탐색
|
||||
- [ ] `python/crosstest/python_go.py` 생성
|
||||
- [ ] `pythonGoTCPPort = 29490`, `pythonGoWSPort = 29492`
|
||||
- [ ] `run_tcp_send_push/tcp_requests/ws_send_push/ws_requests()` 4개 함수
|
||||
- [ ] `run_go_client(mode, port, phase, expected)` — `go run ./crosstest/python_go_client` 실행
|
||||
- [ ] `go_package_dir()` — 소스 위치 기반 탐색
|
||||
- [ ] 비동기 main → `asyncio.run(main())`
|
||||
- [ ] `go/crosstest/python_go_client/main.go` 생성
|
||||
- [ ] `dart_go_client/main.go`와 동일 구조
|
||||
- [ ] Python서버 포트(`--port N`)에 연결
|
||||
- [ ] push 메시지: `"push from python server"`
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
크로스 테스트가 곧 테스트. 별도 단위 테스트 skip.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
Go 서버 / Python 클라이언트:
|
||||
```bash
|
||||
cd go && go run ./crosstest/go_python.go
|
||||
```
|
||||
|
||||
Python 서버 / Go 클라이언트:
|
||||
```bash
|
||||
cd python && python crosstest/python_go.py
|
||||
```
|
||||
|
||||
예상: 각각 `PASS all ...` 출력
|
||||
|
||||
---
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `python/pyproject.toml` | API-1 |
|
||||
| `python/toki_socket/__init__.py` | API-1 |
|
||||
| `python/toki_socket/packets/__init__.py` | API-1 |
|
||||
| `python/toki_socket/packets/message_common.proto` | API-1 |
|
||||
| `python/toki_socket/packets/message_common_pb2.py` | API-1 |
|
||||
| `python/toki_socket/packets/message_common_pb2.pyi` | API-1 |
|
||||
| `python/README.md` | API-1 |
|
||||
| `python/toki_socket/communicator.py` | API-2 |
|
||||
| `python/test/__init__.py` | API-2 |
|
||||
| `python/test/test_communicator.py` | API-2 |
|
||||
| `python/toki_socket/base_client.py` | API-3 |
|
||||
| `python/toki_socket/tcp_client.py` | API-4 |
|
||||
| `python/toki_socket/tcp_server.py` | API-4 |
|
||||
| `python/test/test_tcp.py` | API-4 |
|
||||
| `python/toki_socket/ws_client.py` | API-5 |
|
||||
| `python/toki_socket/ws_server.py` | API-5 |
|
||||
| `python/crosstest/__init__.py` | API-7 |
|
||||
| `python/crosstest/go_python_client.py` | API-7 |
|
||||
| `python/crosstest/python_go.py` | API-7 |
|
||||
| `go/crosstest/go_python.go` | API-7 |
|
||||
| `go/crosstest/python_go_client/main.go` | API-7 |
|
||||
|
||||
---
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
# 1. Python 단위 테스트
|
||||
cd python && pip install -e ".[dev]" && python -m pytest test/ -v
|
||||
|
||||
# 2. Go 서버 / Python 클라이언트 크로스 테스트
|
||||
cd go && go run ./crosstest/go_python.go
|
||||
|
||||
# 3. Python 서버 / Go 클라이언트 크로스 테스트
|
||||
cd python && python crosstest/python_go.py
|
||||
|
||||
# 4. 기존 Go 크로스 테스트 회귀 확인
|
||||
cd go && go run ./crosstest/go_dart.go
|
||||
cd go && go run ./crosstest/go_kotlin.go
|
||||
```
|
||||
|
||||
예상 결과:
|
||||
1. `pytest`: 모든 테스트 PASS
|
||||
2. `go run ./crosstest/go_python.go`: `PASS all go-server/python-client crosstests passed`
|
||||
3. `python crosstest/python_go.py`: `PASS all python-server/go-client crosstests passed`
|
||||
4. 기존 크로스 테스트: 회귀 없음 (PASS)
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
<!-- task=quality_improvements plan=1 tag=REFACTOR-DOC-FIX -->
|
||||
|
||||
# Code Review Reference - REFACTOR-DOC-FIX
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-04-24
|
||||
task=quality_improvements, plan=1, tag=REFACTOR-DOC-FIX
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW.md` → `code_review_1.log` (기존 code_review_*.log 수 = 1)
|
||||
2. `PLAN.md` → `plan_1.log` (기존 plan_*.log 수 = 1)
|
||||
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [DOC-FIX-1] VERSIONING.md `## nonce Overflow` 위치 이동 | [x] |
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
계획대로 `VERSIONING.md`에서 `## nonce Overflow` 섹션 전체를 `Backward-compatible message additions require ...` 단락 뒤로 이동했습니다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
문서 구조만 조정했습니다. `nonce` overflow 정책 문구와 non-breaking 변경 예시 문구는 변경하지 않았습니다.
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `VERSIONING.md`에서 `## Non-breaking Protocol Changes` 섹션 바디(Examples 목록 + 후속 단락)가 `## nonce Overflow` **앞**에 위치하는지 확인
|
||||
- `## nonce Overflow` 섹션 내용(int32 정책 3개 항목)이 그대로 유지되는지 확인
|
||||
- `## nonce Overflow` 이후에 `## New Language Compatibility` 섹션이 이어지는지 확인
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
### DOC-FIX-1 중간 검증
|
||||
```
|
||||
$ grep -n "^##" VERSIONING.md
|
||||
5:## Current Versions
|
||||
13:## Protocol Version
|
||||
26:## Package Version
|
||||
32:## Breaking Protocol Changes
|
||||
42:## Non-breaking Protocol Changes
|
||||
53:## nonce Overflow
|
||||
64:## New Language Compatibility
|
||||
|
||||
$ grep -n "Examples of non-breaking" VERSIONING.md
|
||||
44:Examples of non-breaking changes:
|
||||
|
||||
$ grep -n "nonce Overflow" VERSIONING.md
|
||||
53:## nonce Overflow
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ grep -n "^##" VERSIONING.md
|
||||
5:## Current Versions
|
||||
13:## Protocol Version
|
||||
26:## Package Version
|
||||
32:## Breaking Protocol Changes
|
||||
42:## Non-breaking Protocol Changes
|
||||
53:## nonce Overflow
|
||||
64:## New Language Compatibility
|
||||
|
||||
$ grep -A 5 "Non-breaking Protocol Changes" VERSIONING.md
|
||||
## Non-breaking Protocol Changes
|
||||
|
||||
Examples of non-breaking changes:
|
||||
|
||||
- Adding a new protobuf message type.
|
||||
- Adding optional fields to application messages when older peers can ignore them.
|
||||
```
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
<!-- task=quality_improvements plan=1 tag=REFACTOR-DOC-FIX -->
|
||||
|
||||
# Quality Improvements — VERSIONING.md 구조 수정
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
plan=0 리뷰에서 WARN이 발생했습니다. 단일 항목만 수정합니다.
|
||||
완료 후 `CODE_REVIEW.md` 검증 결과를 채우고 완료 표를 `[x]`로 갱신하세요.
|
||||
|
||||
## 배경
|
||||
|
||||
plan=0 코드 리뷰 결과, REFACTOR-4(VERSIONING.md)에서 `## nonce Overflow` 섹션이
|
||||
`## Non-breaking Protocol Changes` 헤더 바로 아래에 삽입되어 구조가 깨졌습니다.
|
||||
|
||||
현재 구조:
|
||||
```
|
||||
## Non-breaking Protocol Changes
|
||||
← 내용 없음
|
||||
## nonce Overflow
|
||||
...
|
||||
|
||||
Examples of non-breaking changes: ← nonce Overflow 섹션 아래에 위치 (의미상 오류)
|
||||
```
|
||||
|
||||
의도한 구조:
|
||||
```
|
||||
## Non-breaking Protocol Changes
|
||||
|
||||
Examples of non-breaking changes:
|
||||
...
|
||||
|
||||
Backward-compatible message additions ...
|
||||
|
||||
## nonce Overflow
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### [DOC-FIX-1] VERSIONING.md `## nonce Overflow` 위치 이동
|
||||
|
||||
**문제**
|
||||
|
||||
`## nonce Overflow`가 `## Non-breaking Protocol Changes` 바디(Examples 목록, 후속 단락)보다 앞에 위치해
|
||||
"Examples of non-breaking changes:" 목록이 nonce Overflow 섹션에 속한 것처럼 렌더링됩니다.
|
||||
|
||||
**해결 방법**
|
||||
|
||||
`## nonce Overflow` 블록 전체를 `Backward-compatible message additions require ...` 단락 뒤로 이동합니다.
|
||||
|
||||
목표 구조 (`VERSIONING.md` 하단):
|
||||
|
||||
```markdown
|
||||
## Non-breaking Protocol Changes
|
||||
|
||||
Examples of non-breaking changes:
|
||||
|
||||
- Adding a new protobuf message type.
|
||||
- Adding optional fields to application messages when older peers can ignore them.
|
||||
- Adding a new language implementation that passes the existing protocol and cross-language tests.
|
||||
- Tightening tests or documentation without changing wire behavior.
|
||||
|
||||
Backward-compatible message additions require updated parser maps and cross-language tests before release.
|
||||
|
||||
## nonce Overflow
|
||||
|
||||
`nonce` and `responseNonce` fields are protobuf `int32` values (signed 32-bit).
|
||||
Overflow occurs after about 2.1 billion sends on a single connection.
|
||||
|
||||
Current policy:
|
||||
|
||||
- The protocol does not define overflow behavior. Reaching the int32 limit on a single connection is not realistic.
|
||||
- Implementations do not add recovery logic for overflow.
|
||||
- If overflow handling is needed in the future, the protocol version will be bumped and wrap-around behavior will be specified.
|
||||
```
|
||||
|
||||
**수정 파일 및 체크리스트**
|
||||
|
||||
- [x] `VERSIONING.md` — `## nonce Overflow` 섹션을 `## Non-breaking Protocol Changes` 바디 아래로 이동
|
||||
|
||||
**테스트 작성**
|
||||
|
||||
문서 전용 변경이므로 테스트 불필요.
|
||||
|
||||
**중간 검증**
|
||||
|
||||
```bash
|
||||
grep -n "^##" VERSIONING.md
|
||||
# Non-breaking Protocol Changes가 nonce Overflow보다 먼저 나와야 하고
|
||||
# Examples of non-breaking changes: 가 Non-breaking Protocol Changes 섹션 안에 있어야 함
|
||||
grep -n "Examples of non-breaking" VERSIONING.md
|
||||
grep -n "nonce Overflow" VERSIONING.md
|
||||
# Examples 줄 번호 < nonce Overflow 줄 번호
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `VERSIONING.md` | DOC-FIX-1 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
grep -n "^##" VERSIONING.md
|
||||
# 섹션 순서 확인: Non-breaking Protocol Changes → nonce Overflow → New Language Compatibility
|
||||
grep -A 5 "Non-breaking Protocol Changes" VERSIONING.md
|
||||
# 바로 아래에 Examples 또는 설명이 와야 함 (nonce Overflow 아님)
|
||||
```
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
<!-- task=typescript_impl plan=2 tag=REVIEW_API -->
|
||||
|
||||
# Code Review Reference - REVIEW_API
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-04-24
|
||||
task=typescript_impl, plan=2, tag=REVIEW_API
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW.md` → `code_review_2.log` (N = 기존 code_review_*.log 수)
|
||||
2. `PLAN.md` → `plan_2.log` (M = 기존 plan_*.log 수)
|
||||
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [REVIEW_API-1] ws를 devDependencies에서 dependencies로 이동 | [x] |
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
- 없음. 계획대로 `typescript/package.json`에서 `ws`를 `dependencies`로 이동했고, `npm install`로 `typescript/package-lock.json`의 top-level dependency 분류를 함께 갱신했다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- 런타임 import를 사용하는 패키지이므로 소스 코드 수정 없이 의존성 선언만 바로잡았다. `ws_client.ts`, `ws_server.ts`의 동작 자체는 바꾸지 않고 배포/설치 시점의 누락 위험만 제거했다.
|
||||
- 버전은 유지하고 위치만 이동했다. 이번 수정의 목적은 동작 변경이 아니라 production install 시 `ws`가 빠지지 않도록 패키지 메타데이터를 정정하는 것이다.
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `typescript/package.json`의 `dependencies`에 `ws`가 있고 `devDependencies`에서 제거됐는지 확인.
|
||||
- `package-lock.json`이 변경됐는지 확인 (`npm install` 실행 여부).
|
||||
- `npx vitest run` 결과 18개 테스트 PASS인지 확인.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
### REVIEW_API-1 중간 검증
|
||||
```
|
||||
$ cd typescript && npm install && npx tsc --noEmit && npx vitest run
|
||||
|
||||
up to date, audited 65 packages in 542ms
|
||||
|
||||
17 packages are looking for funding
|
||||
run `npm fund` for details
|
||||
|
||||
found 0 vulnerabilities
|
||||
|
||||
RUN v3.2.4 /config/workspace/toki_socket/typescript
|
||||
|
||||
✓ test/base_client.test.ts (7 tests) 9ms
|
||||
✓ test/communicator.test.ts (7 tests) 18ms
|
||||
✓ test/tcp.test.ts (2 tests) 11ms
|
||||
✓ test/ws.test.ts (2 tests) 21ms
|
||||
|
||||
Test Files 4 passed (4)
|
||||
Tests 18 passed (18)
|
||||
Start at 05:37:16
|
||||
Duration 801ms (transform 341ms, setup 0ms, collect 953ms, tests 58ms, environment 1ms, prepare 790ms)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ cd typescript && npm install && npx tsc --noEmit && npx vitest run
|
||||
|
||||
up to date, audited 65 packages in 584ms
|
||||
|
||||
17 packages are looking for funding
|
||||
run `npm fund` for details
|
||||
|
||||
found 0 vulnerabilities
|
||||
|
||||
RUN v3.2.4 /config/workspace/toki_socket/typescript
|
||||
|
||||
✓ test/communicator.test.ts (7 tests) 18ms
|
||||
✓ test/base_client.test.ts (7 tests) 8ms
|
||||
✓ test/tcp.test.ts (2 tests) 11ms
|
||||
✓ test/ws.test.ts (2 tests) 18ms
|
||||
|
||||
Test Files 4 passed (4)
|
||||
Tests 18 passed (18)
|
||||
Start at 05:37:23
|
||||
Duration 867ms (transform 406ms, setup 0ms, collect 1.05s, tests 54ms, environment 1ms, prepare 977ms)
|
||||
```
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
<!-- task=typescript_impl plan=2 tag=REVIEW_API -->
|
||||
|
||||
# ws 런타임 의존성 위치 수정
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
체크리스트를 완료하고 중간 검증 명령을 실행한 후, 출력을 `CODE_REVIEW.md`의 `검증 결과` 섹션에 붙여넣어라. 계획과 다르게 구현한 부분은 `계획 대비 변경 사항`에 이유와 함께 기록하라.
|
||||
|
||||
---
|
||||
|
||||
## 배경
|
||||
|
||||
1차 리뷰(plan=0)에서 `ws` 패키지가 `devDependencies`에 잘못 배치된 것이 확인됐다. `ws_client.ts`와 `ws_server.ts`가 런타임에 `ws`를 import하므로 `dependencies`로 이동해야 한다. 현재 `typescript/package.json:21`에 `ws`가 `devDependencies`에 위치한다.
|
||||
|
||||
---
|
||||
|
||||
### [REVIEW_API-1] ws를 devDependencies → dependencies로 이동
|
||||
|
||||
#### 문제
|
||||
|
||||
`typescript/package.json` (line 21):
|
||||
```json
|
||||
"devDependencies": {
|
||||
...
|
||||
"ws": "^8.18.1" ← 런타임 import에 쓰이는 패키지가 devDependencies에 있음
|
||||
}
|
||||
```
|
||||
|
||||
`typescript/src/ws_client.ts:1`:
|
||||
```typescript
|
||||
import WebSocket, { type RawData } from "ws";
|
||||
```
|
||||
|
||||
`typescript/src/ws_server.ts:2`:
|
||||
```typescript
|
||||
import WebSocket, { WebSocketServer } from "ws";
|
||||
```
|
||||
|
||||
두 파일 모두 런타임에 `ws`를 import한다. `npm install --omit=dev`로 설치하면 `ws`가 설치되지 않아 실행 시 모듈 not found 오류가 발생한다.
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
`ws`를 `devDependencies`에서 제거하고 `dependencies`에 추가한다.
|
||||
|
||||
```json
|
||||
// 수정 전 (typescript/package.json)
|
||||
{
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@bufbuild/protoc-gen-es": "^2.2.5",
|
||||
"@types/node": "^22.15.21",
|
||||
"@types/ws": "^8.18.1",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.1.3",
|
||||
"ws": "^8.18.1"
|
||||
}
|
||||
}
|
||||
|
||||
// 수정 후 (typescript/package.json)
|
||||
{
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.2.5",
|
||||
"ws": "^8.18.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@bufbuild/protoc-gen-es": "^2.2.5",
|
||||
"@types/node": "^22.15.21",
|
||||
"@types/ws": "^8.18.1",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.1.3"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
수정 후 `npm install`로 `package-lock.json`을 갱신한다.
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `typescript/package.json`: `ws`를 `devDependencies`에서 제거, `dependencies`에 추가
|
||||
- [ ] `npm install` 실행 → `typescript/package-lock.json` 갱신됨
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
스킵. `test/ws.test.ts`의 2개 테스트가 `ws` 런타임 동작을 이미 검증하며, 의존성 위치 변경은 동작에 영향을 주지 않는다.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
cd typescript && npm install && npx tsc --noEmit && npx vitest run
|
||||
```
|
||||
|
||||
기대 결과: 타입 오류 없음, 16개 테스트 PASS.
|
||||
|
||||
---
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `typescript/package.json` | REVIEW_API-1 |
|
||||
| `typescript/package-lock.json` | REVIEW_API-1 |
|
||||
|
||||
---
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
cd typescript && npm install && npx tsc --noEmit && npx vitest run
|
||||
```
|
||||
|
||||
기대 결과: 타입 오류 없음, 16개 테스트 PASS.
|
||||
Loading…
Reference in a new issue