<!-- task=control_plane_phase_exit_followups/01_heartbeat_timeout_runtime plan=4 tag=REVIEW_REVIEW_REVIEW_REVIEW_CLOUD_G06 -->

# Follow-up Plan - REVIEW_REVIEW_REVIEW_REVIEW_CLOUD_G06

## Metadata

- date: 2026-06-10
- task: `control_plane_phase_exit_followups/01_heartbeat_timeout_runtime`
- plan: 4
- tag: `REVIEW_REVIEW_REVIEW_REVIEW_CLOUD_G06`
- parent plan log: `plan_cloud_G06_3.log`
- parent review log: `code_review_cloud_G06_3.log`
- verdict source: FAIL follow-up

## Problem

The new `Server.Start()` failure test exists and the focused/full Go tests pass, but the test does not prove the `Start()` or `StartListener()` serve-return path stops the timeout loop. Both failure tests call `server.Shutdown()` after the serve call returns, and `Shutdown()` itself calls `stopTimeoutLoop()`. That means the tests can pass even if `Start()` or `StartListener()` no longer clean up the loop on their own return path.

## Scope

- Change only `services/core/internal/httpserver/server_test.go` unless a test exposes a real production bug.
- Keep the production lifecycle implementation, timeout defaults, HTTP routes, runner protocol, and registry behavior unchanged unless the corrected tests fail.
- Do not modify sibling task files such as `apps/runner/test/oto_iop_connection_smoke_test.dart` or general test rules.

## Implementer Instructions

Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation. If a user-only decision, user-owned external prerequisite, or scope conflict blocks the work, fill the `사용자 리뷰 요청` section in `CODE_REVIEW-cloud-G06.md`, leave the active files in place, and stop for code review.

Evidence gaps that can be closed by rerunning commands or collecting output are normal implementation work, not user-review blockers.

## 구현 항목

### REVIEW_REVIEW_REVIEW_REVIEW_CLOUD_G06-1 - Make failure-path cleanup tests observe loopDone directly

Problem: `TestServerStartFailureStopsTimeoutLoop` and `TestServerStartListenerFailureStopsTimeoutLoop` currently use `Shutdown()` as the cleanup proof, but `Shutdown()` performs cleanup itself and masks the serve-return path under test.

Solution:

- After `Start()` returns its occupied-port error, copy `server.loopDone` while holding `server.mu`, verify it is non-nil, and assert that it is already closed with a bounded `select`.
- Apply the same direct `loopDone` observation to `TestServerStartListenerFailureStopsTimeoutLoop` after `StartListener()` returns.
- Do not call `Shutdown()` before the direct `loopDone` assertion. A later cleanup call is fine only after the direct assertion, if needed for test hygiene.
- Keep bounded waits and useful failure messages.
- Keep the existing focused and full Go verification commands.

Checklist:

- [ ] `TestServerStartFailureStopsTimeoutLoop` fails if `Start()` returns while `loopDone` remains open.
- [ ] `TestServerStartListenerFailureStopsTimeoutLoop` fails if `StartListener()` returns while `loopDone` remains open.
- [ ] The tests do not use `Shutdown()` as the evidence for serve-return cleanup.
- [ ] Existing shutdown and runtime timeout tests still pass.

Intermediate verification:

```bash
cd services/core && go test -count=1 -run 'TestServer(Start|StartListener).*Failure.*TimeoutLoop|TestServerShutdown' -v ./internal/httpserver
```

## 구현 체크리스트

- [ ] [REVIEW_REVIEW_REVIEW_REVIEW_CLOUD_G06-1] failure-path cleanup 테스트가 `loopDone`을 직접 관찰하도록 수정한다.
- [ ] `CODE_REVIEW-cloud-G06.md`에 구현 결과와 검증 출력, 사용자 리뷰 요청 상태를 기록한다.

## Verification

- Required: `cd services/core && go test -count=1 -run 'TestServer(Start|StartListener).*Failure.*TimeoutLoop|TestServerShutdown' -v ./internal/httpserver`
- Required: `cd services/core && go test -count=1 ./...`

## Handoff Notes

- Review failure is about verification trust, not a currently observed production regression.
- A robust test can use a small helper in `server_test.go` to copy `server.loopDone` under `server.mu` and wait for closure.
- `Shutdown()` should not be used as the proof that `Start()` / `StartListener()` cleaned up their own serve-return path because it calls `stopTimeoutLoop()` itself.
