# task-2788+2 — A-0 라이브 실행 직전 최종 micro-fix (3건)

- **팀**: dev5-team (마르둑)
- **레벨**: Lv.2 / same-branch `task/task-2788-dev5` (**PR·merge·배포 없음** — 지시서 준수)
- **작업 시각**: 2026-07-18 21:19 ~ 21:35 (KST)
- **워크트리**: `/home/jay/projects/InsuRo/.worktrees/task-2788-dev5`
- **커밋**: `2fb7492`(구현), `d44022d`(테스트)

---

## S — Situation

task-2788+1 보안가드(S1~S5)가 495 PASS·delta 2파일로 검증 완료되었다. 이제 실 JWT를 붙여 **라이브 실행 직전** 단계다.

## C — Complication

GPT 지적 + 회장 지시 + Codex AGREE 로 3건이 확인되었다.

1. **T1 (회장 지시)**: 현 설계는 `?token=` 이 URL 에 있다는 **가정**에 의존한다. 사용자마다 ohmymanager 진입 경로가 달라(북마크·SPA 라우팅·SSO·쿠키세션) **토큰이 URL 에 아예 없을 수 있다.** 이때 코드가 조용히 STOP 하면 "왜 안 됐는지"를 사후 판별할 수 없다.
2. **T2 (GPT 지적)**: `isAllowedSpikeUrl` 이 `protocol/hostname/pathname` 만 검사해 **`https://mmlfcp.ohmymanager.com:8443/api/ProductPremiums` 가 통과**한다. 동일 호스트의 다른 포트로 실 JWT 가 전송될 수 있다.
3. **T3**: 선택된 resource timing entry 가 **얼마나 오래된 것인지** 알 수 없어, 조회 없이 버튼을 눌렀을 때 오래된 URL 을 썼는지 사후 판별이 불가능하다.

## Q — Question

포트 우회를 막으면서 **정상 경로(443·포트 생략)를 막지 않고**, 토큰 부재를 **우회 조립 없이** fail-closed 로 처리할 수 있는가? 그리고 그 테스트들이 **실제로 결함을 잡는가**(헛돌지 않는가)?

## A — Answer

**세 건 모두 구현·관측 검증했다.** 전체 515 PASS(495→+20, 감소 0). 추가로 **뮤테이션 검증**으로 신규 테스트가 결함을 실제로 잡음을 확인했고(결함 주입 시 정확히 6건 FAIL), L1 스모크로 **fetch 실호출 0회 / sendMessage 실호출 0회**를 런타임에서 직접 관측했다.

---

## 구현 내역 (T1·T2·T3)

### 설계 결정 (팀장)
- **T1 은 content.js 에 배치**. 근거: "토큰이 URL query 에서 왔는가"를 아는 유일한 지점이 `readTokenFromSearch` 호출부다. background 는 토큰의 출처를 구조적으로 알 수 없으므로 background 에서 판정하면 추측이 된다.
- **ABSENT fail-closed 는 기존 `if (!token || !url)` 조건을 그대로 재사용**. 근거: 이 조건이 이미 `sendMessage` 도달 자체를 막는 **구조적** 차단이다. 새 분기를 만들면 우회 경로가 하나 늘어난다. 조건을 약화시키지 않는 것이 핵심이며, 이를 뮤테이션 테스트로 검증했다.
- **T3 는 기존 함수 계약을 깨지 않는 방식으로 분리**. `findProductPremiumsUrlFromResourceTiming` 의 시그니처/반환값(문자열|null)은 기존 테스트가 의존하므로 불변으로 두고, entry 를 반환하는 함수를 신설해 기존 함수가 위임하게 했다.

### T1. `token_source` 기록 + ABSENT fail-closed — `extension/content.js`
```js
// task-2788+2 (T1) ★회장 지시: 사용자마다 ohmymanager 진입 경로가 달라(북마크·SPA
// 라우팅·SSO·쿠키세션) URL에 token이 아예 없을 수 있다. "?token= 이 있다"는 가정에
// 의존하지 않고 출처를 명시 기록한다. ABSENT면 fail-closed — 다른 곳에서 토큰을
// 긁어오는 우회 조립·과거 토큰 재사용 절대 금지. verdict만 반환하고 중단한다.
const token_source = token ? "URL_QUERY" : "ABSENT";
```
- STOP verdict·성공 verdict **양쪽 모두**에 `token_source` 기록.
- **우회 조립 0**: 다른 저장소(storage/cookie/DOM)에서 토큰을 읽는 코드를 **추가하지 않았다**. 과거 토큰 재사용 코드도 없다.
- **토큰 원문·부분문자열 로깅 0**: 기존 `describeToken`(존재여부·길이만) 유지. verdict 합성 시에도 토큰을 넣지 않는다.

### T2. 포트 조건 — `extension/background.js:270`
```js
// WHATWG URL은 https 기본포트 443을 ""로 정규화하므로 두 값 모두 허용.
if (u.port !== "" && u.port !== "443") return false;
```
- **정상 경로 미차단 근거(STOP_REPORT 조건 확인)**: WHATWG `new URL()` 은 `https://host:443/p` 의 `.port` 를 `""` 로 정규화한다. 따라서 실사용 URL(포트 생략)과 명시 `:443` 이 **둘 다 통과**함을 L1 에서 실측했다. 정상 경로가 막히지 않으므로 STOP_REPORT 조건 미해당.

### T3. `url_entry_age_ms` 관찰 기록 — `extension/content.js`
```js
// task-2788+2 (T3): 선택된 resource timing entry가 얼마나 오래된 것인지 관찰 기록.
// 차단하지 않는다 — 조회 없이 눌렀을 때 오래된 URL을 썼는지 사후 판별용.
function computeUrlEntryAgeMs(perf, entry) { ... perf.now() - entry.startTime ... }
```
- entry 미발견·`startTime` 비수치·`perf.now` 부재 → `null`. throw 하지 않는다.
- **차단하지 않음**: age 가 아무리 커도 fetch 경로에 영향 없음(테스트 M-5 로 명시 보장).

---

## 수정 파일 (delta 3 — allowed_resources 정확히 준수, 신규 파일 0)

| 파일 | 변경 |
|---|---|
| `extension/background.js` | +4행 (isAllowedSpikeUrl 포트 조건) |
| `extension/content.js` | +46/-6행 (T1 token_source, T3 entry 분리·age 계산, export 2건) |
| `extension/__tests__/direct_fetch_spike.test.ts` | +238행 (테스트 20건 추가, 기존 삭제/수정 0) |

`inject.js` / `manifest.json` / `src/**` / `server/**` **미수정 확인**(git status 상 위 3파일 외 변경 0).

---

## 테스트 결과

### 전체 스위트 (팀장 독립 재실행)
```
Test Files  35 passed (35)
     Tests  515 passed (515)
```
- 기준 495 → **515 (+20)**. **감소 0건.**
- `direct_fetch_spike.test.ts`: 83 → 103 tests.

### 신규 테스트 20건 (지시서 "검증" 항목 전수 대응)
- **K. token_source 2분기(5건)**: URL_QUERY / ABSENT(`?foo=1`·빈문자열·비문자열) / **ABSENT 시 sendMessage 호출 0회**
- **L. 포트(5건)**: `:8443` 거부 · `:80` 거부 · `:443` 허용 · 생략 허용 · `validateSpikeRequest` 경유 `URL_NOT_ALLOWED`
- **M. url_entry_age_ms(5건)**: 3800 산출 · entry 미발견 null · `perf.now` 부재 null · 성공 verdict 탑재 · **차단하지 않음**
- **N. 토큰 미유출(5건)**: ABSENT STOP · 성공 verdict · SENDER_NOT_ALLOWED · URL_NOT_ALLOWED · TOKEN_MALFORMED **전 경로**에서 토큰 문자열 미포함

### ★ 뮤테이션 검증 (테스트가 헛돌지 않음을 입증)
테스트가 통과한다는 사실만으로는 "테스트가 결함을 잡는다"는 보장이 없다. 결함을 의도적으로 주입해 확인했다.
- 주입 1: T2 포트 조건 삭제 / 주입 2: `if (!token || !url)` → `if (!url)` (fail-closed 약화)
- 결과: **6건 FAIL** (ABSENT fail-closed 1건 + 포트 거부 3건 + URL_NOT_ALLOWED 토큰 미유출 1건 + 포트 8443 게이트 1건)
- 복구 후 두 파일이 뮤테이션 이전본과 **바이트 동일**함을 `diff` 로 확인, 전체 515 PASS 재확인.

---

## ★ L1 스모크테스트 결과

- **서버 재시작**: 해당없음 — 대상은 Chrome 확장(content script / service worker)으로 기동할 서버 프로세스가 없다.
- **API 응답 확인**: 해당없음(실 API 호출) — 실 ohmymanager API 호출은 실 JWT 가 필요하고, 본 task 는 **"라이브 실행 직전" 보강**이라 라이브 실행이 금지되어 있다. **대신 워크트리의 실제 파일을 런타임 로드해 함수를 직접 호출하고 fetch/sendMessage 실호출을 spy 로 관측**했다(`/tmp/l1_smoke.js`, 저장소 파일 미생성).
- **스크린샷**: 해당없음 — UI 변경 0건(버튼/DOM 변경 없음).

**실행 결과 (실관측)**
```
[L1-1] :8443 gate = {"rejected":"URL_NOT_ALLOWED"} | fetch 실호출 = 0회
[L1-2] :80=false :443=true 생략=true
[L1-3] 정상경로 gate={"ok":true} fetch 실호출=1회 verdict={"transport_ok":true,"http_status":200,
       "has_coverage_premiums":true,"company_count":3,...,"url_construction":"SOLVED"}
[L1-4] ABSENT verdict={...,"token_source":"ABSENT","url_entry_age_ms":3800} | sendMessage 실호출=0회
[L1-5] URL_QUERY verdict={...,"token_source":"URL_QUERY","url_entry_age_ms":3800} | sendMessage 실호출=1회
토큰 유출 0건 (모든 출력에 토큰 문자열 미포함)
```
**L1 통과.** 포트 우회 차단이 *테스트 관례*가 아니라 **실호출 0회**로, ABSENT fail-closed 가 **sendMessage 0회**로 런타임 관측되었다.

---

## MATCH / GAP

**MATCH (지시서 대비)**
- T1 `token_source: "URL_QUERY"|"ABSENT"` verdict 기록 — 구현·테스트·실관측 ✅
- T1 ABSENT fail-closed(우회 조립 0·과거 토큰 재사용 0·fetch 미도달) ✅
- T1 토큰 원문/부분문자열 로깅 0 (존재여부·길이만) ✅
- T2 포트 `""|"443"` 만 허용, 그 외 `URL_NOT_ALLOWED` ✅
- T3 `url_entry_age_ms` 기록, entry 미발견 시 `null`, **차단 안 함** ✅
- 기존 495 감소 0 / ANU 독립 재실행용 `npx vitest run` 통과 ✅
- 불변식 전건 유지: 서버 전송 0 · 자동 실행 0 · 인터셉터 추가 0 · inject.js/manifest 미수정 · 보안가드(S1~S5) 무력화 0 · PR/merge/배포 0 ✅

**GAP / 범위 외 (착수하지 않음 — Codex AGREE 대로 production 이식 task 로 이월)**
- 화면값(age/gender/insurance_type/plan_id) 대조, session owner 비교, 불일치 시 fetch 차단, query key allowlist, DOM 기반 URL 직접 구성, consultantid 세션전환 상태기계 — **전부 미착수**.

---

## 발견 이슈 및 해결

1. **`node -e "require('./extension/background.js')"` 가 빈 객체 반환** (엔키·이쉬타르 양쪽 보고)
   - 원인: 루트 `package.json` 에 `"type": "module"` 이 있고 `extension/` 에 오버라이드가 없어 Node 가 ESM 으로 취급 → `module` 미정의 → 파일 하단 `module.exports` 가드 블록 자체가 스킵.
   - **이번 변경과 무관**함을 확인: 원본 파일에서도 동일 재현. 실제 테스트는 Vitest(`import * as ...`)를 쓰므로 영향 없음.
   - 해결: L1 검증을 `vm` 으로 CJS 컨텍스트를 구성해 실행하는 방식으로 대체. **프로덕션 코드는 이 문제 때문에 수정하지 않았다**(범위 외이며, 고치면 delta 가 늘고 확장 런타임 동작이 바뀔 위험).
2. **기존 `stash@{0}` 발견** — `task-623.1` 시절 main 브랜치 항목으로 이번 작업과 무관. 손대지 않았다.

## 셀프 QC (8항목)
1. 영향 파일: 위 3파일 외 없음(git status 확인). 2. 엣지: token 부재/빈문자열/비문자열, entry 0건, `perf.now` 부재, 포트 4종, verdict 비객체 — 전부 테스트. 3. 지시 일치: T1·T2·T3 정확히 3건, 범위 외 0건. 4. 보안: 토큰 미유출 5경로 assert + L1 실관측, 기존 가드 무력화 0. 5. 커버리지: 신규 20건 + 뮤테이션으로 실효성 입증. 6. 발견 이슈 2건 모두 규명·기록. 7. 아키텍처: 기존 함수 계약 불변 유지(위임 패턴), 중복 로직 제거. 8. 인터페이스: 신규 export 2건을 본 보고서에 명시.

---

## 머지 판단
- **머지 필요**: **No** — 지시서가 **PR·merge·배포를 명시적으로 금지**(same-branch 커밋만).
- **브랜치**: `task/task-2788-dev5` (커밋 `2fb7492`, `d44022d`)
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2788-dev5`
- **머지 의견**: 머지하지 않음. 라이브 실행 판단은 회장/ANU 몫. 코드 품질 측면에서는 515 PASS + 뮤테이션 + L1 실관측으로 머지 가능 상태이나, **본 task 권한 밖**이라 실행하지 않는다.

---

## ★ 완료 절차 중 발견한 하네스 갭 (정직 기록 — ANU 판단 요청)

**증상**: `finish-task.sh` 의 GOAL-GATE(`goal_assertions: npx vitest run`)를 정상 통과시킬 수 있는 실행 경로가 없다.

**관측 (교차 확인)**
| 실행 cwd | GOAL-GATE 상태 | 근거 |
|---|---|---|
| `/home/jay/workspace` | **TIMEOUT → BLOCKED** | `bash -c "npx vitest run"` 을 workspace cwd 에서 30s 제한 실행 → `rc=124`. workspace 에는 vitest 프로젝트가 없다. |
| 워크트리(InsuRo) | **disabled → 스킵** | `is_gate_enabled('goal_assertions')` 가 `python3 -c "from utils.gate_config_loader ..."` 로 판정되는데, 워크트리 cwd 에서는 `ModuleNotFoundError: No module named 'utils'` → `\|\| echo "false"` 폴백으로 게이트 자체가 꺼진다. |

**직접 측정으로 가설 확정**: 동일 명령 `timeout 30s bash -c "npx vitest run"` → workspace `rc=124`, 워크트리 `rc=0`. 즉 **코드 결함이 아니라 cwd 불일치**다.
`load_gate_config('goal_assertions')` 에 cwd/project_dir 키가 없어(enabled/mode/max_assertions/allowed_commands 뿐) 프로젝트 디렉토리에서 실행시킬 지원 수단이 없다.

**따라서 `.done` 의 `gate_results.goal_assertions: "PASS"` 는 provenance 가 부정확하다** — 최종 실행에서 이 게이트는 **실행되지 않고 스킵**되었다. 스킵을 PASS 로 기록한 것이므로 그대로 신뢰해선 안 된다.

**단, 주장 자체(`npx vitest run` 통과)는 참이다** — 팀장이 워크트리에서 **독립 3회** 확인:
1. 구현 직후 전체 스위트 `515 passed (515)`
2. 뮤테이션 복구 후 전체 스위트 `515 passed (515)`
3. 게이트 진단 중 `timeout 30s bash -c "npx vitest run"` → `rc=0`

**범위 판단**: `scripts/finish-task.sh` / `utils/gate_config_loader` 수정은 본 task 의 `allowed_resources` 밖(시스템 코드)이며 지시서가 3건 외 수정을 금지하므로 **손대지 않았다.** ANU 판단 요청 사항으로만 남긴다.
