# task-2835 보고서 — PR-D1 item6: INVALIDATED/owner-change 시 pending ingest blob 폐기 (재동의 강제)

## Situation
InsuRo 확장 PR-D1(PR #119, 브랜치 `task/task-2831-dev1`)에서 Codex 검토가 보안 갭(item6)을 발견.
합의 item6 = "만료/INVALIDATED → PII bytes 폐기 + 재-동의". 그러나 `content.js invalidatePreviewState()`(888~899)는 **content쪽 preview 스냅샷/UI만** 지우고 **background의 `insuroPendingIngestV1`(storage.session PII blob)을 안 지웠음.**

## Complication
저장 실패 → pending blob 잔존 → 페이지 INVALIDATED(stale URL·DOM·계정전환) → 동의 UI 사라짐 →
**하지만 background의 PII bytes가 남아, 이후 재시도가 재-동의 없이 전송 가능** → item6 위반(PII 잔존 + 무동의 재시도).

## Question
INVALIDATED 및 owner-change 전환 시 background의 pending PII blob을 **원자적으로 폐기**하여 무동의 재시도 경로를 fail-closed로 차단하려면?

## Answer (구현)
content INVALIDATED = background pending 폐기를 원자화. owner-change(SET_INSURO_JWT)가 이미 사용하는 `clearStoredBlob()` 동일 함수를 재사용.

### 수정 파일 (extension/ 내부만 — 외부 변경 0)
1. **extension/content.js** `invalidatePreviewState()`(888~913): 함수 끝에 fire-and-forget `chrome.runtime.sendMessage({ type: "INSURO_INVALIDATE_PENDING_V1" }, cb)` 발송 추가. `typeof chrome/runtime/sendMessage` 가드 + try/catch 스왈로우(테스트/vm·컨텍스트 무효화 안전). content 상태 폐기는 항상 성공.
2. **extension/background.js** onMessage 리스너(283행): `INSURO_INVALIDATE_PENDING_V1` 핸들러 추가 — `isAllowedSender` 게이트 → `typeof clearStoredBlob === "function"` 가드 → `await clearStoredBlob()` → `{ ok: true, code: "PENDING_CLEARED" }`. RETRY 핸들러와 OHMY_PPD_FETCH 사이에 삽입.
3. **extension/background/ingest.js**: 수정 없음. 기존 `clearStoredBlob()`(storage.session.remove("insuroPendingIngestV1"))·`module.exports` 노출 그대로 재사용.

### 설계 결정
- **owner-change 폐기 함수 재사용**: item6 요구사항("이미 owner-change 폐기 로직 있으면 INVALIDATED도 같은 폐기 함수 재사용")대로 `clearStoredBlob` 단일 소스 사용.
- **전송차단 후 삭제 순서(계약 §저장소·수명)**: blob 제거가 우선되어, 진행 중 전송이 있어도 재시도 경로는 blob 부재로 `NO_PENDING`(fail-closed)로 차단. 폐기 후 재저장 = 명시적 재-preview·재-동의(새 스냅샷·새 idempotency_key)만 가능.
- **fire-and-forget**: content 상태 폐기가 background 응답에 블로킹되지 않도록. 폐기 명령은 PPD/PII payload 없는 순수 신호.

## 테스트 결과
### 신규 테스트 (extension/__tests__/prd1-background-dispatcher.test.ts, +3건)
describe "dispatcher — 항목6 INSURO_INVALIDATE_PENDING_V1: pending ingest 블롭 원자적 폐기":
1. **폐기 증명**: pending blob 저장 상태(사전 존재 assert=non-vacuous) → dispatch → `sessionRemoveSpy` 호출 + blob 소멸 + `{ ok:true, code:"PENDING_CLEARED" }`.
2. **무동의 재시도 0 증명**: 폐기 직후 `INSURO_CONSENT_RETRY_V1` → `{ ok:false, code:"NO_PENDING" }` + fetch(sendIngest 경유) **0회**.
3. **sender 게이트**: 미허용 sender → `SENDER_NOT_ALLOWED` + clearStoredBlob 미호출(blob 보존).

### 회귀 처리 (테스트 의도 갱신)
`track-a-preview.test.ts` 2건이 item6 변경으로 회귀. 원인: STALE_URL_MISMATCH 경로에서 "background sendMessage 미호출"을 단언했으나, item6이 정당하게 `INSURO_INVALIDATE_PENDING_V1`(PII 없는 폐기 명령)을 발송.
→ **프로덕션 로직 결함 아님.** 테스트 단언을 원 의도("PPD/PII 전송 0")에 맞춰 정밀화:
- `expect(sentTypes).not.toContain("OHMY_PPD_FETCH_V1")`
- `expect(sentTypes).not.toContain("INSURO_CONSENT_SAVE_V1")`
- `expect(sentTypes).toContain("INSURO_INVALIDATE_PENDING_V1")` (폐기 명령 허용·필수)
mock을 메시지 타입 기록형으로 개선. STALE verdict 단언·안내 문구 단언 유지(non-vacuous).

### 전체 스위트 (팀장 직접 재검증)
- **682 passed / 0 failed (45 test files)** — 베이스라인 677 대비 신규 +3, 회귀 0.
- 기존 PR-D1(canary·byte·consent·flag-OFF 66건) 및 확장 전체 회귀 0.

### non-vacuous 뮤테이션 증명
background.js의 `INSURO_INVALIDATE_PENDING_V1` 핸들러에서 `await clearStoredBlob()`만 무력화 → item6 폐기 증명·무동의 재시도 0 증명 **2건 정확히 FAIL**. 원복 후 clean 확인. → 테스트가 실제 보안 회귀를 잡음.

## L1 스모크테스트 결과 (필수 기록)
Chrome 확장(서버 없음) — L1 = 구문 파싱 + lint + 실소스 구동 테스트.
- **서버 재시작**: 해당없음 (Chrome 확장, 백엔드 서버 없음)
- **API 응답 확인**: 해당없음 (curl 대상 서버 없음). 대신 **실소스 구동 dispatcher 테스트**로 실제 메시지 핸들러 end-to-end 구동: `INSURO_INVALIDATE_PENDING_V1` dispatch → 실제 `clearStoredBlob` → sessionStore 제거 → RETRY → `NO_PENDING` → fetch 0. (background.js+ingest.js를 vm 병합 로딩 = 실 브라우저 classic-script 병합 재현)
- **구문 파싱**: `node --check` — background.js / content.js / ingest.js **전부 PARSE OK**
- **eslint**: 변경 확장 파일 2종 **exit=0**(경고/에러 0)
- **스크린샷**: 해당없음 (UI 변경 없음 — 메시징/상태 폐기 로직만)

## item8 확인 (부수·선택)
차단된 레거시 메시지(OHMY_CAPTURED / OHMY_MATRIX_CAPTURED)가 `storage.local` 로그에 쓰는 내용: **고정 문자열만**(`ok:false`, `type:"matrix"`, `error:"legacy...removed/ignored"`, `ts`). `msg` payload를 전혀 복사하지 않음.
- `appendLog` 전수 점검: 유일하게 payload 필드를 기록하는 곳은 `processQueue`(레거시 retryQueue 배수)의 `plan_id: item.payload?.plan_id`. **`plan_id`는 보험 상품 플랜 카탈로그 코드**(예: "000000111041)로 개인식별정보(이름/전화/주민번호/계좌) 아님. 이 경로는 큐를 폐기(`retryQueue:[]`)만 하고 서버전송 없음.**
- 결론: **로컬 로그 = 개인 PII 0, 서버전송 아님.** (레거시 로그 경로 대규모 리팩터는 범위 밖 — item8 명시.)

## 발견 이슈 및 해결
- **이슈**: item6 fire-and-forget sendMessage 추가로 `track-a-preview.test.ts` 2건 회귀.
- **해결**: 테스트 단언을 원 의도(PPD/PII 전송 0)에 맞춰 정밀화 + INVALIDATE_PENDING 폐기 명령 허용으로 갱신. 프로덕션 로직 무변경. 전체 682 통과 복구.

## 머지 판단
- **머지 필요**: **No** (task: merge_policy=none, 머지 금지, PR OPEN 유지 → ANU 독립검증 후 회장/아누 판정)
- **브랜치**: `task/task-2831-dev1` (PR #119, OPEN)
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2831-dev1`
- **push 반영**: `0ca6578..7cceef4` → PR #119 갱신(새 PR 생성 안 함)
- **머지 의견**: 코드 변경 최소(34 insertions, extension/ 내부만)·owner-change 폐기 함수 재사용으로 리스크 낮음. 테스트 682 통과·item6 뮤테이션 증명·회귀 0. 단 **같은 InsuRo repo의 dev2 PR #118(InfoKeyword)과 순차 머지** 원칙 적용 필요(무중첩이나 두 번째는 새 main 기준 재검증). 머지 결정은 ANU 독립검증(Codex 반증 + 아누 테스트/회귀/스코프) 후.

## 모델 사용 기록
- 불칸(백엔드) — sonnet: content.js/background.js 폐기 배선 구현
- 아르고스(테스터) — sonnet: 신규 테스트 3건 + 회귀 테스트 의도 갱신
- 헤르메스(팀장, Opus): 설계·분배·검토·통합·직접 재검증(전체 테스트/뮤테이션/구문/lint/push)
- haiku 미사용(보안 로직·테스트 정밀화 작업 — sonnet 이상 요구)

## 생성/수정 파일
- `extension/content.js` (수정, +14): invalidatePreviewState → INSURO_INVALIDATE_PENDING_V1 발송
- `extension/background.js` (수정, +20): INSURO_INVALIDATE_PENDING_V1 핸들러
- `extension/__tests__/prd1-background-dispatcher.test.ts` (수정, +118): item6 신규 테스트 3건
- `extension/__tests__/track-a-preview.test.ts` (수정, +28/-16): STALE 경로 테스트 의도 정밀화

## 세션 통계
- 총 도구 호출: 0회

