<!-- task=m-external-integration/03_mattermost_adapter plan=0 tag=MM -->

# Mattermost Adapter Plan

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것이 구현의 마지막 단계다. Mattermost secrets, FCM token, signing key, push payload signature는 tracked 파일이나 리뷰 출력에 기록하지 않는다. 외부 runner나 credential이 막히면 review stub의 `사용자 리뷰 요청`에 시도한 명령과 차단 근거를 남긴다.

## 배경

현재 Mattermost adapter는 log-only stub이다. 마일스톤은 core가 Mattermost REST 메시지 발송을 담당하고, `../nexo/packages/messaging_flutter`는 client-side FCM 수신/검증/notification을 담당한다고 정했다. 이 작업은 server-side REST post와 task-completed notification path를 구현하고, 가능한 경우 server-generated signed push smoke evidence를 수집한다.

## 사용자 리뷰 요청 흐름

Credential, remote Android runner, Mattermost bot 권한처럼 사용자가 소유한 환경이 없으면 active `CODE_REVIEW-cloud-G08.md`의 `사용자 리뷰 요청` 섹션에 정확한 blocker와 재개 조건을 기록한다. 단위 테스트 증거 공백은 user-review가 아니라 follow-up으로 처리한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/external-integration/milestones/external-integration.md`
- Task ids:
  - `mattermost-adapter`: Mattermost 메시지 발송 adapter 구현과 Nexo host notification boundary 정합성 유지
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/external-integration/PHASE.md`
- `agent-roadmap/phase/external-integration/milestones/external-integration.md`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-test/local/mattermost-server-generated-push-smoke.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `services/core/internal/adapters/mattermost/client.go`
- `services/core/internal/notification/service.go`
- `services/core/internal/notification/model.go`
- `services/core/internal/notification/service_test.go`
- `services/core/internal/config/config.go`
- `services/core/internal/config/config_test.go`
- `services/core/cmd/server/main.go`
- `services/core/internal/scheduler/jobs.go`
- `services/core/internal/scheduler/jobs_test.go`
- `services/core/go.mod`
- `services/core/README.md`
- `README.md`

### 테스트 환경 규칙

- test_env: `local`
- `agent-test/local/rules.md` 있음, 읽음.
- matched profiles:
  - `agent-test/local/core-smoke.md`: core 코드 변경 기본 검증 `cd services/core && go test ./...`.
  - `agent-test/local/mattermost-server-generated-push-smoke.md`: Mattermost server-generated signed push evidence 절차와 redaction 기준.
- 필수 local 검증: `cd services/core && go test ./...`, `cd services/core && go vet ./...`.
- external smoke: remote Android runner + private `agent-test/local/testing-secrets.md`가 있을 때만 실행. 없으면 blocker evidence를 review stub에 기록하되 unit-tested REST adapter는 계속 구현한다.

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

- Mattermost REST post: 기존 테스트 없음. httptest로 `POST /api/v4/posts`, bearer auth, JSON body, error body 검증 필요.
- Notification service only sends completed events to Mattermost at `services/core/internal/notification/service.go:47`. Existing tests do not observe Mattermost request body.
- Remote Android FCM evidence cannot be guaranteed locally; plan records blocker protocol.

### 심볼 참조

- renamed/removed symbols: none.
- changed symbols expected: `mattermost.NewClientWithHTTPClient`, `SendMessage`, `SendTaskNotification`.

### 분할 판단

- split decision policy evaluated.
- `03_mattermost_adapter` is independent from Plane/Jira because it touches notification and Mattermost REST only.
- It uses `cloud-G08` because external service and Android notification evidence can require remote runner diagnosis.
- `04+01,02,03_adapter_boundary` depends on this subtask PASS.

### 범위 결정 근거

- Exclude Flutter app changes, Nexo package changes, FCM signing implementation, push-proxy implementation, Mattermost plugin work, Plane/Jira adapters.
- Core only sends Mattermost REST messages and emits task notification events.
- Do not copy native push handling into `apps/client/android`.

### 빌드 등급

- build lane/grade: `cloud-G08` - REST adapter is bounded, but acceptance includes external Mattermost/Android push evidence or a well-evidenced blocker.
- review lane/grade: `cloud-G08` - reviewer must assess unit tests plus external evidence trust.

## 구현 체크리스트

- [ ] [MM-1] Mattermost REST client를 구현하고 task notification payload를 결정한다.
- [ ] [MM-2] Mattermost adapter와 notification tests를 추가한다.
- [ ] [MM-3] server-generated signed push smoke를 실행하거나 blocker evidence를 기록한다.
- [ ] `cd services/core && go test ./...`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [MM-1] Mattermost REST client

문제: `services/core/internal/adapters/mattermost/client.go:34` and `:42` only log skipped messages and return nil. This can falsely report success without sending a REST post.

Before:

```go
// services/core/internal/adapters/mattermost/client.go:34
func (c *Client) SendMessage(ctx context.Context, input MessageInput) error {
	_ = ctx
	if c.logger != nil {
		c.logger.Info("mattermost send message skipped", "channel_id", input.ChannelID, "text", input.Text)
	}
	return nil
}
```

해결 방법:

- Add `http.Client`, `NewClientWithHTTPClient`, config validation.
- `SendMessage` posts to `{BaseURL}/api/v4/posts` with `{"channel_id": "...", "message": "..."}` and `Authorization: Bearer <token>`.
- Missing config or empty channel/message returns explicit error; do not silently skip.
- `SendTaskNotification` formats a concise message and calls `SendMessage`. Channel ID must come from config, so add `ChannelID` to `mattermost.Config` and load `MATTERMOST_CHANNEL_ID` or equivalent only if documented.

수정 파일 및 체크리스트:

- [ ] `services/core/internal/adapters/mattermost/client.go`: REST implementation, errors, URL helper.
- [ ] `services/core/internal/config/config.go`: channel/env config if required.
- [ ] `services/core/cmd/server/main.go`: pass channel config.
- [ ] `services/core/README.md` and `README.md`: env table update without secrets.

테스트 작성:

- `services/core/internal/adapters/mattermost/client_test.go` 신규.
- Test success post path/header/body.
- Test missing config and empty input.
- Test non-2xx includes response body without leaking token.

중간 검증:

```bash
cd services/core && go test ./internal/adapters/mattermost ./internal/config
```

기대: Mattermost and config tests pass.

## [MM-2] Notification integration tests

문제: `services/core/internal/notification/service.go:47` calls `SendTaskNotification` only for completed events, but tests currently use nil Mattermost and do not verify outbound post behavior.

해결 방법:

- Keep notification service provider-agnostic if possible. If `mattermost.Client` remains concrete, use httptest-backed client in notification tests.
- Verify completed event sends one Mattermost post, while running/failed events do not trigger Mattermost REST.
- Preserve sink fanout and joined error behavior.

수정 파일 및 체크리스트:

- [ ] `services/core/internal/notification/service_test.go`: completed event Mattermost post test.
- [ ] `services/core/internal/notification/service.go`: only change if abstraction is needed for testability.

테스트 작성:

- `TestNotifyTaskEventSendsMattermostOnlyForCompleted`
- `TestNotifyTaskEventReturnsMattermostError`

중간 검증:

```bash
cd services/core && go test ./internal/notification ./internal/adapters/mattermost
```

기대: notification and mattermost tests pass.

## [MM-3] Server-generated signed push smoke or blocker

문제: 마일스톤 검증은 server-generated signed push evidence를 요구한다. 이 evidence depends on private env and remote Android runner described in `agent-test/local/mattermost-server-generated-push-smoke.md`.

해결 방법:

- If credentials and runner are available, run the smoke steps from the agent-test profile and record redacted evidence path/output only.
- If unavailable, record blocker in `사용자 리뷰 요청` with exact missing env/credential/runner checks. Do not mark this subtask complete unless code-review accepts the blocker path or smoke evidence exists.

수정 파일 및 체크리스트:

- [ ] No tracked secret files.
- [ ] Optional evidence path under `/tmp` or ignored `agent-test/runs/`.
- [ ] `CODE_REVIEW-cloud-G08.md` records PASS/PARTIAL/BLOCKED evidence checklist.

테스트 작성:

- No new automated test unless smoke helper is added.
- External smoke evidence must follow redaction rules.

중간 검증:

```bash
ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com 'zsh -lc "cd $HOME/agent-work/nomadcode && git rev-parse HEAD && flutter devices"'
```

기대: remote checkout and device are visible, or blocker evidence is recorded.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/adapters/mattermost/client.go` | MM-1 |
| `services/core/internal/adapters/mattermost/client_test.go` | MM-1 |
| `services/core/internal/config/config.go` | MM-1 |
| `services/core/internal/config/config_test.go` | MM-1 |
| `services/core/cmd/server/main.go` | MM-1 |
| `services/core/internal/notification/service.go` | MM-2 |
| `services/core/internal/notification/service_test.go` | MM-2 |
| `services/core/README.md` | MM-1, MM-3 |
| `README.md` | MM-1 |

## 최종 검증

```bash
cd services/core && go test ./...
cd services/core && go vet ./...
ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com 'zsh -lc "cd $HOME/agent-work/nomadcode && git rev-parse HEAD && flutter devices"'
```

기대: Go commands exit 0. Remote command either confirms smoke setup or produces blocker output. Go test cache output is acceptable for unit tests; use fresh smoke evidence for external verification.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
