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

# OpenCode prompt-async abort 반환 순서 직접 검증

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

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

## 역할 모델

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

## 배경

직전 FAIL은 prompt-async in-flight 취소 회귀 테스트에 abort polling loop가 남아 있어 `Execute` 반환 전에 동기 `/abort`가 완료된다는 계약을 직접 검증하지 못한다고 판정했다. 제품 경로는 `opencodeAbort` 반환 뒤 `ErrRunCancelled`을 반환하므로, 이번 범위는 기존 테스트의 반환 직후 abort count assertion만 결정적으로 고정한다.

## 사용자 리뷰 요청 흐름

선택 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_G05_3.log`, `agent-task/m-provider-resource-admission-ownership-alignment/15+14_controlplane_connected_mapping/code_review_local_G05_3.log`
- 판정: FAIL. Required는 `apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go:952-958`의 abort polling loop가 비동기 abort도 통과시켜 반환 순서 계약을 직접 검증하지 못한다는 것이다.
- Required fix: `done` 수신으로 `Execute` 반환을 확인한 직후 `fake.abortCalls.Load() == 1`을 직접 assert하고 `abortDeadline`/`time.Sleep` polling loop를 제거한다.
- 영향 파일: `apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go`만 변경한다. 기존 gate handshake, 제품 코드, Control Plane/S14 mapping evidence는 보존한다.

## 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.go` (targeted lines 155-190, 543-581, 603-614)
- `apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go` (targeted lines 1-35, 885-960 and `time.Now`/`time.Sleep` references)
- `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_G05_3.log`
- `agent-task/m-provider-resource-admission-ownership-alignment/15+14_controlplane_connected_mapping/code_review_local_G05_3.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-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 기준

SDD는 `[승인됨]`, 잠금 해제, 사용자 리뷰 없음이다. 이 task는 `offline-snapshot`/S14의 기존 closure를 유지하며, Evidence Map의 S01-S15 deterministic test/race evidence에 연결된 후속 테스트 신뢰성 보정이다. 구현·계약 의미는 바꾸지 않고 review FAIL의 assertion만 닫는다.

### 테스트 환경 규칙

`test_env=local`이다. `agent-test/local/rules.md`, `node-smoke.md`, `testing-smoke.md`와 node/testing domain rule을 기준으로 대상 회귀, CLI race, Node 전체, 저장소 전체 Go 회귀를 실행한다. fresh 결과를 위해 `-count=1`을 사용하고 targeted 반복은 `-count=50`으로 수행한다. Go 기본 GOCACHE를 유지한다.

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

- 대상 test의 gate handshake와 취소 결과 assertion은 존재하지만 abort count만 polling하므로 `Execute` 반환 시점의 동기 abort 완료 계약을 직접 고정하지 못한다.
- polling 제거 후 직접 assertion은 제품 경로의 `opencodeAbort` 완료 → cancelled event → `ErrRunCancelled` 반환 순서를 회귀로 고정한다.

### 심볼 참조

rename/remove 심볼은 없다. `abortDeadline`은 대상 test 내부 지역 변수이며 제거 후 참조가 남지 않아야 한다. `time` package는 같은 파일의 다른 timeout/polling test들이 계속 사용하므로 import를 제거하지 않는다.

### 분할 판단

선택 candidate는 archived FAIL의 단일 Required이며 기존 sibling `15+14_controlplane_connected_mapping`에 후속 pair 하나로 유지한다. write set은 `apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go` 한 파일의 한 assertion block이고 shared mutable state는 기존 fake server의 atomic abort counter뿐이다. predecessor `14`의 완료 상태는 기존 task 경로 dependency와 prior archive snapshot이 유지한다. 더 나눌 독립 구현 단위가 없다.

### 범위 결정 근거

`apps/node/internal/adapters/cli/opencode_sse.go`의 동기 abort 경로, fake gate lifecycle, event assertions, 다른 polling 기반 tests는 직전 Required와 무관하므로 수정하지 않는다. Edge/Control Plane/provider snapshot, proto/config/contract/spec, smoke fixture도 제품 동작이나 S14 mapping을 바꾸지 않는 이번 테스트 assertion 범위 밖이다.

### 최종 라우팅

`evaluation_mode=isolated-reassessment`이다. build와 review 모두 scope/context/verification/evidence/ownership/decision closure가 true다: 한 test block의 정확한 Required, 동기 제품 경로, 결정적 명령, 단일 write owner가 확인되고 사용자 또는 외부 환경 결정이 없다. 각 target의 scores는 scope coupling 0, state concurrency 1, blast/irreversibility 0, evidence/diagnosis 1, verification complexity 1이다. formatter 결과는 build `local`, `G03`, `PLAN-local-G03.md`; review `local`, `G03`, `CODE_REVIEW-local-G03.md`다.

## 구현 체크리스트

- [ ] 대상 regression의 `done` 수신 직후 `fake.abortCalls.Load() == 1`을 직접 assert하고 `abortDeadline`/`time.Sleep` polling loop를 제거한다.
- [ ] 기존 `ErrRunCancelled`, cancelled event 1회, error event 0 assertion과 gate handshake를 보존하고 다른 test polling은 수정하지 않는다.
- [ ] 아래 fresh targeted repeat/race/Node/전체 Go 검증의 실제 출력 또는 blocker를 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 채우고 review-ready를 보고한 뒤 현재 실행을 종료한다. code-review를 실행하거나 다음 에이전트를 시작하지 않는다.

### [REVIEW_TEST-1] Execute 반환 직후 abort count를 직접 단언

문제: `apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go:952-958`은 `Execute` 반환을 이미 확인한 뒤에도 3초 polling으로 abort count를 기다린다. 이 구조는 abort가 반환 이후 비동기로 발생해도 통과하여 제품의 동기 abort 완료 계약을 검증하지 못한다.

해결 방법: polling comment, deadline, loop를 제거하고 기존 `done` select 바로 다음, event counting 전에 atomic count를 한 번 읽어 정확히 1인지 확인한다.

Before (`apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go:952`):

```go
// The abort POST must have been sent exactly once within the bounded window.
abortDeadline := time.Now().Add(3 * time.Second)
for fake.abortCalls.Load() == 0 && time.Now().Before(abortDeadline) {
	time.Sleep(10 * time.Millisecond)
}
if fake.abortCalls.Load() != 1 {
	t.Fatalf("expected /abort call count == 1, got %d", fake.abortCalls.Load())
}
```

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

수정 파일 및 체크리스트:

- [ ] `apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go`: 대상 test의 abort polling block만 직접 반환시점 assertion으로 교체한다.
- [ ] 같은 파일: `time` import와 다른 tests의 polling/timeout을 보존한다.

테스트 작성: 새 test를 추가하지 않고 기존 `TestCLIExecuteOpencodeSSE_ContextCancelAfterPromptAcceptedAbortsSession`을 수정한다. 이 named regression이 gate ordering, 취소 결과, event 수, 반환시점 abort count를 함께 검증한다.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `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, race/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, external OpenCode profile, full-cycle 실제 구동은 실행하지 않는다. 이번 변경은 기존 제품 동작을 바꾸지 않는 test assertion correction이며 deterministic local test/race evidence가 acceptance 범위다.

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