<!-- task=edge_node_unit_tests plan=1 tag=REVIEW_TEST -->

# Plan - REVIEW_TEST

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

각 항목의 체크리스트를 완료하고, 중간 검증과 최종 검증 명령을 실행한 뒤 출력을 `CODE_REVIEW.md`의 검증 결과 섹션에 붙여 넣는다. `CODE_REVIEW.md`의 모든 섹션(계획 대비 변경 사항, 주요 설계 결정, 검증 결과)을 실제 내용으로 채운다.

## 배경

plan=0(TEST) 리뷰에서 WARN 판정된 4개 항목이 있다. parser 테스트의 방어 코드 부재, session 동시성 미검증, node 테스트의 adapter 호출·store 기록 미검증, 그리고 프로덕션 코드의 store 에러 무시 문제다. 이를 후속 개선으로 처리한다.

## 의존 관계 및 구현 순서

REVIEW_TEST-1 → REVIEW_TEST-2는 독립적이다. REVIEW_TEST-3은 REVIEW_TEST-4(프로덕션 코드 변경) 이후에 검증해야 store 에러 로깅 테스트가 가능하므로 REVIEW_TEST-4 → REVIEW_TEST-3 순서를 권장한다.

---

## [REVIEW_TEST-1] parser_test.go 파서 맵 키 존재 확인 guard 추가

### 문제

[parser_test.go](file:///Users/toki/work/go-iop/apps/node/internal/transport/parser_test.go) line 20, 40, 58에서 `parsers[toki.TypeNameOf(original)]`로 맵 키를 조회하지만, 키가 없을 경우 nil function 호출로 panic이 발생한다. `nodeParserMap()`이 특정 타입을 누락하면 panic 대신 명시적 실패 메시지를 출력해야 한다.

### 해결 방법

각 테스트에서 맵 조회 후 `ok` 확인을 추가한다.

Before (`parser_test.go` line 20):
```go
	parsed, err := parsers[toki.TypeNameOf(original)](payload)
```

After:
```go
	key := toki.TypeNameOf(original)
	parser, ok := parsers[key]
	if !ok {
		t.Fatalf("parser not found for key: %s", key)
	}
	parsed, err := parser(payload)
```

동일 패턴을 `TestNodeParserMap_CancelRequest` (line 40), `TestNodeParserMap_CapabilityRequest` (line 58)에도 적용한다.

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

- [ ] `apps/node/internal/transport/parser_test.go` 수정
  - [ ] `TestNodeParserMap_RunRequest`: 맵 키 존재 guard 추가
  - [ ] `TestNodeParserMap_CancelRequest`: 맵 키 존재 guard 추가
  - [ ] `TestNodeParserMap_CapabilityRequest`: 맵 키 존재 guard 추가

### 테스트 작성

기존 테스트 수정. 신규 테스트 없음.

### 중간 검증

```
go test ./apps/node/internal/transport/...
```

---

## [REVIEW_TEST-2] session_test.go 동시성 테스트 추가

### 문제

[session.go](file:///Users/toki/work/go-iop/apps/node/internal/transport/session.go) line 26에서 `cancelFns sync.Map`을 사용하는 이유가 concurrent access 대응인데, 이를 검증하는 테스트가 없다.

### 해결 방법

`TestSession_ConcurrentRegisterCancel`을 추가한다. 여러 goroutine에서 동시에 `RegisterCancel`, `CancelRun`, `DeregisterCancel`을 호출하고 `go test -race`로 race detector를 통과하는지 확인한다.

```go
func TestSession_ConcurrentRegisterCancel(t *testing.T) {
	sess := &transport.Session{}
	var wg sync.WaitGroup
	for i := 0; i < 100; i++ {
		id := fmt.Sprintf("run-%d", i)
		wg.Add(1)
		go func() {
			defer wg.Done()
			sess.RegisterCancel(id, func() {})
			sess.CancelRun(id)
			sess.DeregisterCancel(id)
		}()
	}
	wg.Wait()
}
```

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

- [ ] `apps/node/internal/transport/session_test.go` 수정
  - [ ] `import`에 `"fmt"`, `"sync"` 추가
  - [ ] `TestSession_ConcurrentRegisterCancel` 추가: 100개 goroutine에서 Register/Cancel/Deregister 동시 호출

### 테스트 작성

신규 추가. 동시성 로직(sync.Map) 사용의 정합성 검증 목적.

### 중간 검증

```
go test -race ./apps/node/internal/transport/...
```

---

## [REVIEW_TEST-3] node_test.go adapter 호출 검증 및 store 기록 검증 추가

### 문제

[node_test.go](file:///Users/toki/work/go-iop/apps/node/internal/node/node_test.go) `TestOnRunRequest_Success` (line 131)에서 `err == nil` 반환만 검증한다. adapter가 실제로 호출되었는지, store에 run 기록이 삽입·완료 처리되었는지 확인하지 않는다.

### 해결 방법

1. `noEmitAdapter`를 호출 카운터가 있는 `countingAdapter`로 교체한다.

```go
type countingAdapter struct {
	executeCalls int32
}

func (a *countingAdapter) Name() string { return "test" }
func (a *countingAdapter) Capabilities(_ context.Context) (runtime.Capabilities, error) {
	return runtime.Capabilities{AdapterName: "test", Models: []string{"v1"}, MaxConcurrency: 1}, nil
}
func (a *countingAdapter) Execute(_ context.Context, _ runtime.ExecutionSpec, _ runtime.EventSink) error {
	atomic.AddInt32(&a.executeCalls, 1)
	return nil
}
```

2. `TestOnRunRequest_Success`에서 `countingAdapter.executeCalls == 1` 검증을 추가한다.

3. store에서 run 기록을 조회하여 status가 `"completed"`인지 검증한다. 이를 위해 `makeNode`가 `*store.Store`를 반환하도록 시그니처를 변경한다.

```go
func makeNode(t *testing.T, rtr runtime.Router, reg *adapters.Registry) (*node.Node, *store.Store) {
	// ... 기존 코드 동일
	return node.New(cfg, rtr, reg, st, zap.NewNop()), st
}
```

4. store 조회를 위해 `store.Store`에 `GetRun(ctx, runID)` 메서드가 있는지 확인 필요. 없으면 store 패키지에 추가하거나, store DB를 직접 쿼리한다.

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

- [ ] `apps/node/internal/node/node_test.go` 수정
  - [ ] `countingAdapter` 정의 추가 (`sync/atomic` import)
  - [ ] `makeNode` 반환값에 `*store.Store` 추가
  - [ ] 기존 `makeNode` 호출부 모두 수정 (반환값 2개)
  - [ ] `TestOnRunRequest_Success`: `countingAdapter` 사용, `executeCalls == 1` 검증
  - [ ] `TestOnRunRequest_Success`: store에서 run 기록 조회 후 status 검증 (store API 확인 후 결정)
- [ ] `apps/node/internal/store/store.go` 확인
  - [ ] `GetRun` 또는 동등한 조회 메서드 존재 여부 확인. 없으면 추가.

### 테스트 작성

기존 테스트 강화. adapter 호출 여부 + store 기록 정합성은 `OnRunRequest` 핵심 경로의 end-to-end 신뢰성에 해당한다.

### 중간 검증

```
go test ./apps/node/internal/node/...
```

---

## [REVIEW_TEST-4] node.go store 에러 로깅 추가

### 문제

[node.go](file:///Users/toki/work/go-iop/apps/node/internal/node/node.go) line 76과 line 103에서 store 에러를 `_ = ...`로 무시한다. store 장애 시 실행 이력이 유실되더라도 로그에 흔적이 없어 디버깅이 불가능하다.

### 해결 방법

Before (`node.go` line 76):
```go
	_ = n.store.InsertRun(ctx, store.RunRecord{
```

After:
```go
	if err := n.store.InsertRun(ctx, store.RunRecord{
		RunID:     spec.RunID,
		Adapter:   spec.Adapter,
		Model:     spec.Model,
		Status:    "running",
		CreatedAt: time.Now(),
	}); err != nil {
		n.logger.Warn("store: insert run", zap.String("run_id", spec.RunID), zap.Error(err))
	}
```

Before (`node.go` line 103):
```go
	_ = n.store.CompleteRun(context.Background(), spec.RunID, status, errMsg)
```

After:
```go
	if err := n.store.CompleteRun(context.Background(), spec.RunID, status, errMsg); err != nil {
		n.logger.Warn("store: complete run", zap.String("run_id", spec.RunID), zap.Error(err))
	}
```

실행 흐름을 중단하지 않고 Warn 레벨 로깅만 추가한다. store 장애가 요청 처리를 막아서는 안 된다.

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

- [ ] `apps/node/internal/node/node.go` 수정
  - [ ] line 76: `InsertRun` 에러 로깅 추가
  - [ ] line 103: `CompleteRun` 에러 로깅 추가
  - [ ] 실행 흐름 변경 없음 확인 (return 값 변경 없음)

### 테스트 작성

별도 테스트 불필요. 기존 `TestOnRunRequest_Success`가 in-memory SQLite를 사용하므로 store 에러 경로가 발생하지 않는다. store 에러 경로를 검증하려면 store mock이 필요하지만 현재 `*store.Store`가 concrete type이므로 범위 밖이다.

### 중간 검증

```
go test ./apps/node/internal/node/...
```

---

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/node/internal/transport/parser_test.go` (수정) | REVIEW_TEST-1 |
| `apps/node/internal/transport/session_test.go` (수정) | REVIEW_TEST-2 |
| `apps/node/internal/node/node_test.go` (수정) | REVIEW_TEST-3 |
| `apps/node/internal/node/node.go` (수정) | REVIEW_TEST-4 |
| `apps/node/internal/store/store.go` (확인/수정) | REVIEW_TEST-3 (조건부) |

## 최종 검증

```
go test -race ./apps/node/...
go test ./...
```

모든 테스트 pass, race detector 경고 없음.
