<!-- task=m-forgejo-branch-event-mvp/01_consumer_smoke plan=0 tag=TEST -->

# Plan - TEST

## 이 파일을 읽는 구현 에이전트에게

구현이 끝났다고 판단하기 전에 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용, 설계 결정, 검증 출력으로 채운다. 검증 명령을 실행하고 출력 원문을 붙인 뒤 active 파일을 그대로 둔 채 리뷰 준비를 보고한다. 최종 판정, log archive, `complete.log`, task directory archive 이동은 code-review-skill 전용이다.

구현 중 사용자만 결정할 수 있는 범위 변경, 사용자 소유 외부 환경/secret/service 준비, 또는 계획 충돌이 발생하면 active review stub의 `사용자 리뷰 요청` 섹션에 정확한 증거를 채우고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과/계획 대비 변경 사항에 기록한다.

## 배경

Gito는 `branch.updated` event를 proto-socket `event` channel로 broadcast할 수 있지만, `../nomadcode`에는 이를 wakeup signal로 해석하는 typed consumer smoke가 없다. NomadCode의 기존 proto-socket client는 task channel 중심이고, event stream은 lifecycle diagnostics로만 흘러간다. 이 작업을 1차 MVP 완료선으로 앞당겨 실제 network socket 없이 fixture 기반으로 subscribe request와 `branch.updated` event 해석을 고정한다.

## 사용자 리뷰 요청 흐름

구현 중 차단 사유는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`에서 복사한 형식을 사용하며, 구현 에이전트는 직접 사용자 prompt를 띄우지 않는다. code-review가 요청의 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/control-plane-foundation/milestones/forgejo-branch-event-mvp.md`
- Task ids:
  - `consumer-smoke`: `../nomadcode`의 proto-socket client가 `branch.updated`를 수신하는 smoke 또는 fixture를 추가한다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `AGENTS.md`
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/private/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/rules/project/domain/contracts/rules.md`
- `agent-ops/rules/project/domain/workspace-ops/rules.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-test/local/contracts-smoke.md`
- `agent-test/local/workspace-ops-smoke.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/control-plane-foundation/milestones/forgejo-branch-event-mvp.md`
- `agent-contract/index.md`
- `agent-contract/provided/gito-forgejo-branch-events-v1.md`
- `services/core/internal/protosocket/envelope.go`
- `services/core/internal/protosocket/subscription.go`
- `services/core/internal/controlplane/runtime.go`
- `../nomadcode/AGENTS.md`
- `../nomadcode/agent-ops/rules/project/rules.md`
- `../nomadcode/agent-ops/rules/private/rules.md`
- `../nomadcode/agent-ops/rules/common/rules-roadmap.md`
- `../nomadcode/agent-ops/rules/project/domain/mobile/rules.md`
- `../nomadcode/agent-ops/rules/project/domain/contracts/rules.md`
- `../nomadcode/agent-ops/rules/project/domain/core/rules.md`
- `../nomadcode/agent-test/local/rules.md`
- `../nomadcode/agent-test/local/mobile-smoke.md`
- `../nomadcode/agent-test/local/contracts-smoke.md`
- `../nomadcode/agent-test/local/core-smoke.md`
- `../nomadcode/agent-test/local/workspace-ops-smoke.md`
- `../nomadcode/packages/contracts/notes/flutter-core-api-candidates.md`
- `../nomadcode/apps/client/pubspec.yaml`
- `../nomadcode/apps/client/analysis_options.yaml`
- `../nomadcode/apps/client/lib/src/integrations/proto_socket/proto_socket_envelope.dart`
- `../nomadcode/apps/client/lib/src/integrations/proto_socket/proto_socket_client.dart`
- `../nomadcode/apps/client/lib/src/integrations/proto_socket/proto_socket_lifecycle.dart`
- `../nomadcode/apps/client/lib/src/integrations/proto_socket/proto_socket_task_service.dart`
- `../nomadcode/apps/client/test/integrations/proto_socket_envelope_test.dart`
- `../nomadcode/apps/client/test/integrations/proto_socket_lifecycle_test.dart`
- `../nomadcode/apps/client/test/integrations/proto_socket_task_service_test.dart`
- `../nomadcode/apps/client/test/integrations/proto_socket_endpoint_config_test.dart`

### 테스트 환경 규칙

- Gito `test_env=local` rules와 core/contracts/workspace-ops profiles를 읽었다.
- NomadCode `test_env=local` rules와 mobile/contracts/core/workspace-ops profiles를 읽었다.
- NomadCode mobile 필수 검증은 remote runner 기준 `cd apps/client && flutter test`이며 analyzer 영향이 있으면 `flutter analyze --no-fatal-infos`도 실행한다.
- 이 계획은 NomadCode `apps/client/**`와 `packages/contracts/**`를 건드리므로 `../nomadcode/apps/client` focused Flutter tests, `flutter analyze --no-fatal-infos`, Gito checkout의 `git diff --check`를 최종 검증으로 둔다.
- No network socket smoke다. 기존 fake transport 패턴을 재사용하므로 live Gito/Forgejo endpoint나 secret은 필요 없다.

### 테스트 커버리지 공백

- `ProtoSocketEnvelope`는 Struct round-trip을 검증하지만 `event` channel의 branch payload 해석은 없다.
- `ProtoSocketTaskService`는 task channel request/response만 검증한다. `event.subscribe` request와 `branch.updated` wakeup signal 해석은 없다.
- `ProtoSocketLifecycle`은 event diagnostics를 기록하지만 typed consumer stream을 제공하지 않는다.
- NomadCode contract note는 신규 통신 운영 체크를 갖고 있으나 Gito `branch.updated` 소비자 fixture 기준을 짧게 가리키지 않는다.

### 심볼 참조

- 이름 변경은 계획하지 않는다.
- `ProtoSocketTransport.sendRequest`/`events`는 `../nomadcode/apps/client/lib/src/integrations/proto_socket/proto_socket_lifecycle.dart:64-70`에 정의되어 있고, task service와 lifecycle tests에서 fake로 구현된다.
- `ProtoSocketEnvelope`는 `../nomadcode/apps/client/lib/src/integrations/proto_socket/proto_socket_envelope.dart`와 기존 proto-socket tests에서 사용된다.

### 분할 판단

split decision policy를 계획 파일 선택 전에 평가했다. 공유 task group은 `m-forgejo-branch-event-mvp`이며 sibling은 다음과 같다.

- `01_consumer_smoke`: NomadCode consumer smoke/fixture. predecessor 없음. Gito의 이미 구현된 in-memory event/proto-socket contract만으로 fixture 작성이 가능하므로 1차 MVP 검증선으로 먼저 둔다.
- `02+01_watch_store`: Gito core storage/migration/runtime/idempotency. product sequencing상 `01_consumer_smoke` PASS 이후 진행한다.

NomadCode consumer smoke는 외부 프로젝트 mobile/contracts domain과 Flutter runner 검증을 사용하므로 Gito storage 작업과 분리한다.

### 범위 결정 근거

- NomadCode Plane mutation, roadmap mutation, provider work item sync 적용은 하지 않는다. `branch.updated`는 wakeup signal로만 해석한다.
- 실제 WebSocket 연결 E2E는 Gito server와 NomadCode runner 환경을 동시에 요구하므로 fixture smoke로 제한한다.
- Gito contract 원문은 복제하지 않고, NomadCode contracts note에는 pointer/요약만 추가한다.
- Flutter UI surface나 bootstrap auto-connect 변경은 범위 제외다.

### 빌드 등급

build lane은 `cloud-G06`, review lane은 `cloud-G06`이다. 구현 자체는 작지만 cross-repo mobile/contracts/proto-socket 계약과 fixture evidence가 함께 필요하다.

## 구현 체크리스트

- [ ] [TEST-1] NomadCode proto-socket event consumer service/DTO를 추가하고 `event.subscribe` request shape를 Gito contract와 맞춘다.
- [ ] [TEST-2] `branch.updated` fixture tests와 concise contract pointer를 추가한다. 검증: NomadCode client/core 테스트가 `branch.updated` envelope를 wakeup signal로 해석한다.
- [ ] [TEST-3] NomadCode focused Flutter tests, analyzer, Gito diff check를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [TEST-1] Branch Event Consumer Service

#### 문제

NomadCode에는 task channel 전용 service만 있다. `../nomadcode/apps/client/lib/src/integrations/proto_socket/proto_socket_task_service.dart:34-43`은 `task.list/get/enqueue/create`만 다루며, `event.subscribe` request와 `branch.updated` stream 해석이 없다.

Before (`../nomadcode/apps/client/lib/src/integrations/proto_socket/proto_socket_task_service.dart:34-43`):

```dart
/// Flutter client for the Core `task` channel proto-socket actions.
///
/// Sends request envelopes through a [ProtoSocketTransport] and maps responses
/// onto [WorkspaceTask] models. Action names match the Core dispatcher exactly:
/// `task.list`, `task.get`, `task.enqueue`, `task.create`.
class ProtoSocketTaskService {
  final ProtoSocketTransport _transport;
  int _messageCounter = 0;

  ProtoSocketTaskService(this._transport);
```

#### 해결 방법

`../nomadcode/apps/client/lib/src/integrations/proto_socket/proto_socket_branch_event_service.dart`를 추가한다. DTO는 `BranchUpdatedEvent`와 `ChangedFile`로 두고, service는 `subscribe(repoId, branch)`가 `channel: event`, `action: event.subscribe`, payload `events: ['branch.updated']`, `repo_id`, `branch` request를 전송하게 한다. `transport.events`에서 `type=event`, `channel=event`, `action=branch.updated` envelope만 필터링해 stream으로 노출한다.

After 방향:

```dart
class ProtoSocketBranchEventService {
  final ProtoSocketTransport _transport;
  final StreamController<BranchUpdatedEvent> _updates =
      StreamController<BranchUpdatedEvent>.broadcast();

  Stream<BranchUpdatedEvent> get branchUpdates => _updates.stream;

  Future<void> subscribe({required String repoId, required String branch}) async {
    await _transport.sendRequest(ProtoSocketEnvelope(
      id: _newMessageId(),
      type: 'request',
      channel: 'event',
      action: 'event.subscribe',
      payload: {'events': ['branch.updated'], 'repo_id': repoId, 'branch': branch},
    ));
  }
}
```

#### 수정 파일 및 체크리스트

- [ ] `../nomadcode/apps/client/lib/src/integrations/proto_socket/proto_socket_branch_event_service.dart`를 추가한다.
- [ ] `branch.updated`는 wakeup signal로만 해석하고 Plane/roadmap mutation은 호출하지 않는다.
- [ ] malformed payload는 stream error 또는 ignore 중 하나로 명확히 처리하고 test로 고정한다.
- [ ] raw token, endpoint, credential은 fixture에 넣지 않는다.

#### 테스트 작성

`../nomadcode/apps/client/test/integrations/proto_socket_branch_event_service_test.dart`를 추가한다. fake transport가 subscribe request를 기록하고 broadcast stream에 Gito contract shape의 event envelope를 넣으면 DTO가 repo/branch/before/after/changed_files를 보존하는지 확인한다.

#### 중간 검증

```bash
cd ../nomadcode/apps/client && flutter test test/integrations/proto_socket_branch_event_service_test.dart
```

기대 결과: branch event service focused tests가 PASS한다.

### [TEST-2] Fixture Contract Pointer

#### 문제

NomadCode contracts note의 신규 통신 운영 체크는 `../nomadcode/packages/contracts/notes/flutter-core-api-candidates.md:156-162`에서 "새 channel/action은 문서화하고 fixture로 고정"하라고 말하지만, Gito `branch.updated` 소비에 대한 pointer가 없다.

Before (`../nomadcode/packages/contracts/notes/flutter-core-api-candidates.md:156-162`):

```markdown
### 1.7 새 내부 통신 추가 운영 체크
- 신규 `apps/client` ↔ `services/core` 또는 내부 runtime 간 통신은 먼저 proto-socket channel/action으로 표현 가능한지 확인한다.
- REST/HTTP를 새로 추가하거나 유지해야 하면 health/readiness, 외부 provider 호환 API, 운영 smoke/curl, public callback, 단순 정적 조회 중 어느 예외인지 이 문서 또는 관련 domain rule에 기록한다.
- 새 channel/action은 envelope의 `protocol_version`, `type`, `channel`, `action`, `correlation_id`, `payload`, `error.retryable` 의미를 이 문서에 추가한다.
- 클라이언트가 소비하는 request/response/event shape는 구현 전에 후보 표와 테스트 fixture 기준을 남긴다.
- Core 구현은 기존 REST smoke/compat 표면을 제거하지 않고, 내부 UI/런타임 호출만 proto-socket 우선 경로로 전환한다.
- 검증은 최소한 channel/action dispatch, auth boundary, error envelope, event broadcast 또는 reconnect 영향 중 해당되는 항목을 테스트로 고정한다.
```

#### 해결 방법

NomadCode contracts note에 "외부 Gito branch event consumer" 후보를 짧게 추가한다. 원문 계약은 Gito `agent-contract/provided/gito-forgejo-branch-events-v1.md`이고, NomadCode는 `event.subscribe`와 `branch.updated` fixture만 소비한다는 pointer를 둔다. 계약 본문 전체를 복제하지 않는다.

#### 수정 파일 및 체크리스트

- [ ] `../nomadcode/packages/contracts/notes/flutter-core-api-candidates.md`에 Gito branch event consumer pointer를 추가한다.
- [ ] Gito contract id `gito.forgejo-branch-events.v1`와 event action `branch.updated`를 정확히 표기한다.
- [ ] NomadCode 책임을 "wakeup signal 후 branch revision 재검증"으로 제한한다.

#### 테스트 작성

문서 변경 자체는 자동 test가 없으므로 `git diff --check`와 Flutter fixture test로 검증한다. Contract body duplication 방지를 리뷰 체크포인트로 둔다.

#### 중간 검증

```bash
cd ../nomadcode && git diff --check
```

기대 결과: whitespace/patch check가 PASS한다.

### [TEST-3] Focused Consumer Verification

#### 문제

`../nomadcode/apps/client/test/integrations/proto_socket_task_service_test.dart:8-35`에는 fake transport pattern이 있지만 event consumer는 아직 없다. 전체 mobile test/analyze를 통과하지 않으면 새 service가 existing proto-socket integration과 어긋났는지 알 수 없다.

#### 해결 방법

새 fake transport는 기존 task/lifecycle test pattern을 따르되, event stream을 broadcast controller로 제공한다. Focused Flutter test와 analyzer를 실행해 API/lint 영향까지 확인한다.

#### 수정 파일 및 체크리스트

- [ ] 새 test file에서 fake transport가 `sendRequest`와 `events`를 모두 지원하도록 작성한다.
- [ ] `event.subscribe` request payload, subscribe response handling, matching event parsing, mismatched repo/branch filtering을 검증한다.
- [ ] analyzer warning이 생기면 기능 코드에서 해소한다.

#### 테스트 작성

`ProtoSocketBranchEventService sends event.subscribe request`, `emits branch.updated as wakeup signal`, `ignores non-matching branch event`, `does not mutate state beyond stream emission` tests를 작성한다.

#### 중간 검증

```bash
cd ../nomadcode/apps/client && flutter test test/integrations/proto_socket_branch_event_service_test.dart test/integrations/proto_socket_envelope_test.dart
```

기대 결과: 새 fixture와 envelope regression tests가 PASS한다.

## 수정 파일 요약

| 파일 | 항목 |
| --- | --- |
| `../nomadcode/apps/client/lib/src/integrations/proto_socket/proto_socket_branch_event_service.dart` | TEST-1 |
| `../nomadcode/apps/client/test/integrations/proto_socket_branch_event_service_test.dart` | TEST-1, TEST-3 |
| `../nomadcode/packages/contracts/notes/flutter-core-api-candidates.md` | TEST-2 |

## 최종 검증

```bash
cd ../nomadcode/apps/client && flutter test test/integrations/proto_socket_branch_event_service_test.dart test/integrations/proto_socket_envelope_test.dart test/integrations/proto_socket_lifecycle_test.dart
cd ../nomadcode/apps/client && flutter analyze --no-fatal-infos
cd ../nomadcode && git diff --check
git diff --check
```

기대 결과: focused Flutter tests PASS, analyzer PASS 또는 기존 non-fatal info만 유지, NomadCode/Gito diff whitespace check PASS. 모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
