# Track A — Gemini 지적 1·2번 마이크로픽스 (PR #111)

## ★ ID 규약
- logical ID = `TRACKA_GEMINI_MICROFIX`
- 숫자 task ID 는 dispatch 시스템이 할당한 값을 쓴다. ANU 가 미리 고른 번호는 예약이 아니다.

## allowed_resources (본 task의 capability)

```yaml
allowed_resources:
  paths:
    - "extension/content.js"
    - "extension/__tests__/**"
    - "memory/reports/**"
  forbidden_paths:
    - "server/**"
    - "src/**"
    - "docs/contracts/**"
    - ".github/**"
    - "extension/background.js"
    - "extension/manifest.json"
  commands:
    - "npx vitest"
    - "git"
  merge_policy: "tiered"
  ttl_hours: 12
```

## 대상
- 리포: `/home/jay/projects/InsuRo`
- 브랜치: **`task/task-2802-dev1`** (PR #111, 이미 push 됨 — 새 브랜치 만들지 말고 이 브랜치 위에 커밋)
- 수정 파일: **`extension/content.js` 단 1개** + 필요 시 `extension/__tests__/` 테스트 추가
- ⚠️ `extension/` 밖 변경 **0건**. `server/**`·`src/**`·문서·워크플로우 **절대 손대지 마라.**

## 배경
PR #111 에 Gemini 지적 3건. **3번(companyCode 대소문자 완화)은 회장 판단으로 거부 확정** — 건드리지 마라.
1·2번만 반영한다. 둘 다 "현재 동작은 이미 옳지만 방어/진단을 더한다" 성격이다. **기존 fail-closed 동작을 바꾸면 안 된다.**

---

## 작업 1 — `parseJwtPayload` UTF-8 안전 디코딩 (content.js ~L560)

현재:
```js
    const json = atob(base64);
    const payload = JSON.parse(json);
```
`atob` 는 각 바이트를 latin1 문자로 만든다. 지금 소비하는 필드(`consultantid`)는 ASCII 라 실동작 영향은 없지만,
**향후 한글 필드를 읽게 되면 값이 깨진다.** 지금 미리 올바른 디코딩으로 바꾼다.

변경:
```js
    const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
    const json = new TextDecoder("utf-8").decode(bytes);
    const payload = JSON.parse(json);
```
- 기존 `try { ... } catch (_) { return null; }` 구조 **유지**. 디코딩 실패 시 여전히 `null` 반환(fail-closed).
- `TextDecoder` 미지원 환경 고려 불필요(MV3 확장 = 최신 Chrome).

**테스트 추가 (필수)**: `extension/__tests__/track-a-preview.test.ts` 또는 신규 파일에
1. 한글 포함 payload(`{"consultantid":"c-1","name":"전종혁"}`)를 base64url 로 만들어 넣었을 때 → `name` 이 **정확히 `"전종혁"`** 으로 복원되고 `consultantid` 도 정상
2. 기존 ASCII payload 도 **회귀 없이** 동일 결과
3. base64 깨진 입력 → `null` (fail-closed 유지)

---

## 작업 2 — `chrome.runtime.lastError` 명시 확인 (content.js ~L1076)

현재 콜백 첫 줄:
```js
    (response) => {
      if (!response || response.ok !== true) {
        const code = (response && response.code) || "TRANSPORT_ERROR";
        recordVerdict({ code, token_source: "URL_QUERY" });
```
`lastError` 상황에서도 `response === undefined` 로 들어와 **이미 fail-closed 로 잡힌다.**
바꾸는 목적은 ① 콘솔의 "Unchecked runtime.lastError" 경고 제거 ② 실패 사유 문자열 확보. **분기 결과를 바꾸지 마라.**

변경:
```js
    (response) => {
      // chrome.runtime.lastError 는 읽어야 콘솔 경고가 사라진다. 읽기만 하고
      // 판정은 기존 가드에 그대로 맡긴다(동작 변경 금지).
      const transportError = chrome.runtime.lastError
        ? chrome.runtime.lastError.message || "unknown"
        : null;
      if (transportError || !response || response.ok !== true) {
        const code = (response && response.code) || "TRANSPORT_ERROR";
        recordVerdict({ code, token_source: "URL_QUERY", reason: transportError });
        setOhmyPpdStatus(userMessageForOhmyPpdCode(code));
        return;
      }
```
- ⚠️ `recordVerdict` 에 `reason` 키를 넣기 전에 **`recordVerdict` 구현과 verdict 스냅샷 스키마를 먼저 확인**하라.
  스키마가 `extra="forbid"` 성격이거나 계약 §2-1 에 없는 키면 **`reason` 을 넣지 말고** `transportError` 는 읽기만 하고 버려라.
  (계약 스키마 위반이 콘솔 경고 제거보다 훨씬 중대하다.)
- `lastError` 가 있는데 `response` 가 온 경우도 **실패로 처리**되도록(위 `transportError ||`) 한 것이 유일한 논리 추가다.

**테스트 추가 (필수)**: `chrome.runtime.lastError` 가 설정된 상태를 mock 해서
1. `TRANSPORT_ERROR` verdict 가 기록되고 사용자 메시지가 뜬다
2. `lastError` 없고 정상 response → **기존 성공 경로 회귀 없음**

---

## 검증 (전부 통과해야 완료)
```bash
cd /home/jay/projects/InsuRo
npx vitest run extension/__tests__/          # 기존 202 passed 유지 + 신규 테스트 추가분
```
- **기존 202건 중 단 1건도 깨지면 안 된다.**
- 보험나이 경계 테스트·뮤테이션 테스트 전부 그대로 통과해야 한다.

## 완료 조건
1. 위 2개 변경 + 테스트 커밋 → `git push origin task/task-2802-dev1`
2. `extension/` 밖 변경 0건임을 `git diff --stat` 으로 증명
3. **PR 을 새로 만들지 마라.** #111 에 자동 반영된다.
4. 결과를 `memory/reports/` 에 기록하고 **ANU 콜백 발사** (callback prompt UTF-8 ≤3900 bytes, envelope 만)

## 금지
- 3번 지적(`readCompanyTopTotal` 대소문자 완화) 반영 **금지** — 회장 거부 확정
- 테스트 skip·assertion 완화·required check 변경 **금지**
- 서버 전송 코드 추가 **금지** (Track A 불변식)
- rebase·force push·브랜치 재생성 **금지** (일반 커밋 + push 만)
