<!-- task=m-provider-resource-admission-ownership-alignment/15+14_controlplane_connected_mapping plan=5 tag=REVIEW_TEST -->

# OpenCode prompt-async abort 반환시점 assertion 순서 보정

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

구현 모델은 Plan/Review 모델과 달라야 한다. 지정된 assertion 순서만 수정하고 구현 소유 review stub 섹션을 채운 뒤 `review-ready`를 보고하고 종료한다. code-review를 실행하거나 다음 에이전트를 시작하지 않는다.

## 역할 모델

- Plan: `gpt-5.6`
- Implementation/Review: review stub의 runtime ledger에서 시작 시 기록한다. 세 identity는 모두 달라야 한다.

## 배경

직전 리뷰 FAIL은 `Execute` 반환 확인 뒤 event snapshot을 순회한 다음에야 abort count를 확인해, 그 사이 비동기 `/abort`가 완료되어도 통과할 수 있다고 판정했다. 이번 범위는 해당 count assertion을 `done` 수신 직후로 옮겨 반환시점의 동기 abort 완료 계약을 직접 고정한다. 제품 코드, gate handshake, event assertions와 provider admission 구현은 변경하지 않는다.

## 사용자 리뷰 요청 흐름

선택 Milestone의 구현 잠금 결정만 active review stub의 `사용자 리뷰 요청` 섹션에 workflow template 내용으로 기록한다. 구현 중 직접 사용자에게 묻지 않으며, code-review가 검증과 실제 `USER_REVIEW.md` 작성을 소유한다.

## Archive Evidence Snapshot

- 직전 reviewed pair: `agent-task/m-provider-resource-admission-ownership-alignment/15+14_controlplane_connected_mapping/plan_local_G03_0.log`, `agent-task/m-provider-resource-admission-ownership-alignment/15+14_controlplane_connected_mapping/code_review_local_G03_0.log`
- 판정: FAIL. Required는 `apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go:935-955`에서 `done` 수신 뒤 event snapshot/counting을 먼저 수행해 abort 완료가 반환 뒤에 일어나도 통과할 수 있다는 것이다.
- Required fix: `done` select가 `ErrRunCancelled`을 확인한 직후, `sink.Events()` 호출 전에 `fake.abortCalls.Load() == 1`을 단 한 번 직접 assert한다.
- 영향 파일: `apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go`만 변경한다. `NEXT_STATE.md`의 legacy marker 예외(2026-07-22 사용자 지시)는 이전 review continuation에만 적용한 근거이며, 새 pair는 현재 역할 ledger를 완전하게 사용한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-resource-admission-ownership-alignment.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/operational-observability-provider-management/milestones/provider-resource-admission-ownership-alignment.md)
- Task ids:
  - `offline-snapshot`: configured offline Node/provider와 reconnect snapshot 수렴
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go` (targeted lines 1-70, 900-970)
- `apps/node/internal/adapters/cli/opencode_sse.go` (targeted lines 140-205, 535-625)
- `agent-task/m-provider-resource-admission-ownership-alignment/15+14_controlplane_connected_mapping/NEXT_STATE.md`
- `agent-task/m-provider-resource-admission-ownership-alignment/15+14_controlplane_connected_mapping/plan_local_G03_0.log`
- `agent-task/m-provider-resource-admission-ownership-alignment/15+14_controlplane_connected_mapping/code_review_local_G03_0.log`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/operational-observability-provider-management/PHASE.md`
- `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-resource-admission-ownership-alignment.md`
- `agent-roadmap/sdd/operational-observability-provider-management/provider-resource-admission-ownership-alignment/SDD.md`
- `agent-spec/index.md`, `agent-spec/runtime/edge-node-execution.md`
- `agent-ops/rules/project/domain/node/rules.md`, `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`, `agent-test/local/node-smoke.md`, `agent-test/local/testing-smoke.md`

### SDD 기준

`agent-roadmap/sdd/operational-observability-provider-management/provider-resource-admission-ownership-alignment/SDD.md`는 `[승인됨]`, 잠금 해제, 사용자 리뷰 없음이다. `offline-snapshot`의 S14 및 Evidence Map은 deterministic test/race evidence를 요구한다. 이번은 S14 제품 동작을 바꾸지 않고 그 evidence의 OpenCode 취소 회귀가 `Execute` 반환 시점을 관찰하도록 보정하므로 checklist와 final verification을 기존 named regression, race, Node/전체 Go 회귀로 한정한다.

### 테스트 환경 규칙

`test_env=local`이다. `agent-test/local/rules.md`, `node-smoke.md`, `testing-smoke.md`를 적용한다. fresh 결과를 위해 `-count=1`, named regression 반복에는 `-count=50`을 사용하고 Go 기본 `GOCACHE`를 유지한다. 테스트 전용 assertion 순서 변경이므로 full-cycle/외부 OpenCode profile은 실행 대상이 아니며, 테스트 수와 기존 assertion 의미 보존을 review가 확인한다.

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

기존 named regression은 gate handshake, `ErrRunCancelled`, cancelled event 1회와 error event 0회를 확인한다. abort count가 event snapshot 뒤에 있어 `Execute` 반환 시점의 동기 abort 완료는 직접 관찰하지 못한다. direct assertion을 `done` 직후로 옮기면 별도 test 추가 없이 이 공백을 닫는다.

### 심볼 참조

rename/remove symbol은 없다. 변경 대상은 `TestCLIExecuteOpencodeSSE_ContextCancelAfterPromptAcceptedAbortsSession` 내부 assertion block 하나다.

### 분할 판단

선택 candidate는 archived FAIL의 단일 Required다. immediate sibling set은 기존 `15+14_controlplane_connected_mapping` 하나이며, write set은 `apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go`의 한 assertion block, shared mutable state는 fake server의 atomic `abortCalls`뿐이다. 병행 sibling이나 추가 predecessor는 없고, 더 나눌 독립 구현/검증 단위가 없다.

### 범위 결정 근거

`apps/node/internal/adapters/cli/opencode_sse.go`, fake gate lifecycle, cancelled/error event assertions, 다른 polling test, Edge/Control Plane/provider snapshot, 계약·spec·config는 Required가 지목하지 않은 제품 동작 또는 기존 evidence이므로 제외한다. `time` import는 같은 파일의 다른 timeout test가 사용하므로 유지한다.

### 최종 라우팅

`evaluation_mode=isolated-reassessment`이다. build/review 모두 scope/context/verification/evidence/ownership/decision closure가 true다: 정확한 한 assertion 위치, source와 FAIL evidence, deterministic local commands, 단일 write owner, 미해결 사용자 결정 없음이 확인됐다. build scores는 scope coupling 0, state concurrency 1, blast/irreversibility 0, evidence/diagnosis 1, verification complexity 1이며 `local`, `G03`, `PLAN-local-G03.md`다. review도 독립적으로 동일 점수와 `local`, `G03`, `CODE_REVIEW-local-G03.md`로 route됐다.

## 구현 체크리스트

- [ ] `done` 수신으로 `ErrRunCancelled`을 확인한 직후 `fake.abortCalls.Load() == 1`을 직접 assert하고, 그 뒤에만 `sink.Events()` event count를 확인한다.
- [ ] 기존 gate handshake, `ErrRunCancelled`, cancelled event 1회, error event 0 assertions를 보존하고 다른 test 또는 제품 코드를 수정하지 않는다.
- [ ] 아래 fresh targeted repeat/race/Node/전체 Go 검증의 실제 출력 또는 blocker를 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 채우고 review-ready를 보고한 뒤 현재 실행을 종료한다. code-review를 실행하거나 다음 에이전트를 시작하지 않는다.

### [REVIEW_TEST-1] Execute 반환 직후 abort 완료를 단언

문제: `apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go:935-955`는 `done` select 뒤 `sink.Events()`를 복사·순회하고 event count를 확인한 후에 `fake.abortCalls.Load()`를 확인한다. 이 관찰 간격은 비동기 abort completion을 허용한다.

해결 방법: `done` select의 직후에 atomic abort count를 읽어 정확히 1인지 assert하고, 기존 event snapshot/counting block을 그 뒤에 둔다.

Before (`apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go:935-955`):

```go
// Exactly one cancelled event, no error event.
var cancelCount, errCount int
for _, e := range sink.Events() {
	// ...
}
// Execute returns only after the in-flight cancellation path completes /abort.
if got := fake.abortCalls.Load(); got != 1 {
	t.Fatalf("expected /abort call count == 1 when Execute returned, got %d", got)
}
```

After:

```go
// Execute returns only after the in-flight cancellation path completes /abort.
if got := fake.abortCalls.Load(); got != 1 {
	t.Fatalf("expected /abort call count == 1 when Execute returned, got %d", got)
}

// Exactly one cancelled event, no error event.
var cancelCount, errCount int
for _, e := range sink.Events() {
	// ...
}
```

수정 파일 및 체크리스트:

- [ ] `apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go`: named regression의 abort assertion block을 `done` select 직후로 이동한다.
- [ ] 같은 파일: event counting block의 내용과 다른 timeout/polling test를 보존한다.

테스트 작성: 새 test는 추가하지 않는다. 기존 `TestCLIExecuteOpencodeSSE_ContextCancelAfterPromptAcceptedAbortsSession`이 반환 순서, abort count, event 결과를 한 scenario에서 검증하는 regression test이며 assertion 관찰 시점만 보정한다.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go` | REVIEW_TEST-1 |

## 최종 검증

- `gofmt -d apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go` → 출력 없음.
- `go test -count=50 -timeout 120s ./apps/node/internal/adapters/cli -run '^TestCLIExecuteOpencodeSSE_ContextCancelAfterPromptAcceptedAbortsSession$'` → 50회 모두 PASS; timeout, hang, panic 없음.
- `go test -race -count=1 -timeout 120s ./apps/node/internal/adapters/cli` → PASS; race, deadlock, panic 없음.
- `go test -count=1 -timeout 300s ./apps/node/...` → PASS.
- `go test -count=1 -timeout 600s ./...` → PASS.
- repo 내부 edge-node 진단, 보조 E2E smoke, full-cycle 및 외부 OpenCode profile은 실행하지 않는다. 제품 실행 경로를 바꾸지 않는 test assertion 순서 보정이며, 기존 scenario의 targeted/race/Node/전체 Go 검증이 acceptance 범위다.

검증 결과를 review stub에 기록하고 review-ready를 보고한 뒤 현재 실행을 종료한다. code-review를 실행하거나 다음 에이전트를 시작하지 않는다.
