diff --git a/docs/web-reading-lab-results-2026-10-26.md b/docs/web-reading-lab-results-2026-10-26.md new file mode 100644 index 00000000..27cf433d --- /dev/null +++ b/docs/web-reading-lab-results-2026-10-26.md @@ -0,0 +1,49 @@ +# 웹 읽기 독립 실험 결과 (2026-10-26) + +실험 Worker: https://daiso-web-read-lab.hmmhmmhm.workers.dev/run/round2 + +가상 상품과 수량만 사용합니다. 실제 상품 API, 인증, 중계는 호출하지 않습니다. 이 결과는 해당 실행의 관측이며 모든 ChatGPT 계정이나 도구 버전의 영구 규격을 뜻하지 않습니다. + +## 결과 + +| 조건 | 관측 | +| ---------------------------------------------- | ----------------------------------------------------------------------------- | +| 새 URL을 web.run open에 직접 전달 | 16조건 모두 접근 오류, 해당 실험 번호 Worker 도착 기록0 | +| 읽힌 GitHub PR의 링크를 web.run click으로 선택 | 실험 목록과 루트 정상, ChatGPT-User 분류 요청 도착 | +| 목록의 HTML/text/JSON 링크 click | 모두 실제 데이터 반환. requestId가 서버 기록과 일치 | +| ASCII·한글 경로, ASCII·한글 쿼리 | click으로 모두 성공 | +| 302리다이렉트 | 목적지 HTML 반환 및 서버 재요청 확인 | +| 고정1초 지연 | 성공 | +| 고정5초·15초 지연 | TimeoutError. 서버 received는 있고 prepared는 없었음 | +| 64KiB·256KiB HTML | 페이지와 앞부분 데이터 열람 성공. 전체 본문 보존을 검증한 것은 아님 | +| no-store / public,max-age=300 / noindex | click으로 모두 성공 | +| JavaScript만으로 데이터 표시 | 페이지는 열리지만 웹 읽기 결과에 상품 데이터 없음. Work 브라우저에서는 표시됨 | +| no-store HTML 재열람 | 이전 requestId·생성 시각 재사용, 새 서버 기록 없음 | + +일반 HTTPS 대조군16조건은 모두 기대한200/302였고 지연 응답은 약1.29/5.31/15.54초였습니다. 짧은 HTML은427바이트, 약0.53초였으며 Work 브라우저도 정상으로 열었습니다. + +## 핵심 재현 순서 + +1. GitHub [PR329](https://github.com/hmmhmmhm/daiso-mcp/pull/329)를 URL 열기로 읽습니다. +2. 본문 실험 링크를 도구가 반환한 링크 ID로 click합니다. +3. 실험 목록의 html/json/한글 경로 링크를 click합니다. 새 URL 문자열을 임의로 open하는 시도와 구분합니다. +4. 응답의 requestId와 `/observations/{실험번호}`의 기록을 비교합니다. +5. 같은 HTML을 다시 읽고 번호·생성 시각·신규 서버 기록이 바뀌었는지 확인합니다. + +첫 실행번호 a-6aa69ea371/b-ec18b14f0e의 직접 open에서는 도착 기록이 없었습니다. 링크 경유 start 실행에서는 응답 번호가 서버 기록과 일치했습니다. html은 직접 open 실패 후 click 성공, 이후 직접 open에서도 이전 사본이 반환되는 변화를 보였습니다. 따라서 단순한 JSON 미지원이나 한글 URL 문제로 설명할 수 없습니다. + +## 해석과 한계 + +- 직접 URL 열기와 이미 읽힌 페이지의 링크 선택은 이 실행에서 서로 다른 결과를 보였습니다. 도구 내부 URL 처리·검색 사본 경로 차이를 의심할 수 있지만 내부 구현은 확인할 수 없습니다. +- 링크 click은 새 페이지를 실제 가져오기도 했으므로 모든 성공을 과거 검색 캐시라고 볼 수 없습니다. 반대로 재열람은 no-store여도 도구가 이전 결과를 재사용했습니다. no-store만으로 실시간 재조회를 보장할 수 없습니다. +- 지연 실험은1초 성공/5초 실패 사이의 제한을 시사합니다. 네트워크와 기록 비용이 포함되므로 정확한 타임아웃 초수로 일반화하지 않습니다. received만 남은 경우 서버 도착은 확인되지만 Worker 취소 사유는 이것만으로 확정하지 않습니다. +- 이 실험은 Cloudflare 앞단 기록 전체를 보지 않습니다. 도착 기록0만으로 WAF 차단과 도구 내부 거절을 구분할 수는 없습니다. 같은 호스트에서 click이 실제 도달한 점은 전면적인 호스트 차단 주장과 맞지 않습니다. +- 현재 에이전트 안내에는 실제 링크 ID로 click하는 방법을 구체적으로 명시할 가치가 있습니다. 느린 원본 조회는 별도 시간 측정이 필요하며 이번 실험만으로 운영 API의 타임아웃 원인을 단정하지 않습니다. + +## 새 실행 링크의 제한 + +목록의 새 실행 링크는 응답 생성 번호를 사용합니다. 새로 생성된 문서끼리는 다르지만 캐시된 문서는 같은 링크를 재사용합니다. 매 클릭마다 독립 실행을 보장하지 않으므로 실행번호와 신규 도착 시각을 비교하고 이전에 사용한 번호는 새 표본으로 세지 않습니다. + +## 보관 + +공개 관측 기록은 최대128개/24시간 후 삭제됩니다. 개인정보는 기록하지 않습니다. 운영 기록에는 안전한 원시 결과를 별도 보존했습니다. 재현 방법과 코드: [실험 README](../experiments/web-read-lab/README.md). diff --git a/experiments/web-read-lab/README.md b/experiments/web-read-lab/README.md index 550f3d91..42c36ad2 100644 --- a/experiments/web-read-lab/README.md +++ b/experiments/web-read-lab/README.md @@ -2,11 +2,17 @@ 독립 Worker에서 웹 읽기 도구의 실패 조건을 비교합니다. 모든 상품과 수량은 가상 데이터이며 운영 API를 호출하지 않습니다. 기존 MCP 도구와 인증·중계·캐시 바인딩을 공유하지 않습니다. +실험 목록: https://daiso-web-read-lab.hmmhmmhm.workers.dev/run/round2 + +[2026-10-26 실험 결과](../../docs/web-reading-lab-results-2026-10-26.md) + 배포: `.github/workflows/deploy-web-read-lab.yml`. Worker 이름은 `daiso-web-read-lab`이며 배포 결과의 workers.dev URL을 사용합니다. ## 실행 -1. 영문·숫자·하이픈으로 새로운 실험번호(최대48자)를 만들고 `/run/{번호}`를 엽니다. +1. 실험 목록을 연 뒤 **새 실험 시작** 링크를 한 번 따라 다음 실행으로 이동합니다. 웹 읽기에서는 URL을 다시 조립하지 말고 실제 링크 ID로 click합니다. 직접 open 실패도 별도 조건으로 기록합니다. + 새 링크는 문서를 새로 생성할 때 고유합니다. 웹 읽기가 캐시된 목록을 재사용하면 링크도 재사용되므로 독립 실행을 보장하지 않습니다. 이전에 본 실행번호이면 재사용하지 말고, 새로 열린 목록의 다음 링크를 한 번 따라가세요. 관측 기록에서 실행번호와 실제 신규 도착 시각을 대조하고 섞인 실행은 독립 표본에서 제외합니다. + 2. 같은 번호 아래 링크를 한 번씩 열어 응답 여부, requestId, generatedAt, 웹 도구의 Crawled 정보를 기록합니다. 3. `/observations/{번호}`를 일반 HTTPS로 읽고 도착 기록과 비교합니다. 관측 조회 자체는 기록하지 않습니다. 4. 독립된 두 번째 번호로 순서를 바꿔 재현합니다. 직접 open과 링크 click은 별도로 기록합니다. diff --git a/experiments/web-read-lab/worker.ts b/experiments/web-read-lab/worker.ts index 8d441df7..4c7add30 100644 --- a/experiments/web-read-lab/worker.ts +++ b/experiments/web-read-lab/worker.ts @@ -150,7 +150,7 @@ export default { let status = 200; if (test === 'index') { body = html( - `

가상 상품: 실험용 지갑, 수량7. 실험번호: ${run}

한글 쿼리

응답의 requestId와 generatedAt을 관측 기록과 비교하세요. received는 서버 도착, prepared는 응답 생성 완료이며 클라이언트 수신 성공을 증명하지 않습니다.

관측 기록 JSON

현재 요청 번호 ${requestId}

`, + `

가상 상품: 실험용 지갑, 수량7. 실험번호: ${run}

새 실험 시작

한글 쿼리

응답의 requestId와 generatedAt을 관측 기록과 비교하세요. received는 서버 도착, prepared는 응답 생성 완료이며 클라이언트 수신 성공을 증명하지 않습니다.

관측 기록 JSON

현재 요청 번호 ${requestId}

`, ); } else if (test === 'json' || test === 'text') { headers.set( diff --git a/tests/scripts/web-read-lab.test.ts b/tests/scripts/web-read-lab.test.ts index 88aaa97f..096f97e1 100644 --- a/tests/scripts/web-read-lab.test.ts +++ b/tests/scripts/web-read-lab.test.ts @@ -30,6 +30,18 @@ function fixture() { } describe('독립 웹 읽기 실험', () => { + it('실험 목록에서 새 링크를 따라 독립 실행을 시작할 수 있다', async () => { + const { get } = fixture(); + const links: string[] = []; + for (let i = 0; i < 2; i++) { + const page = await (await get('/run/start')).text(); + const match = page.match(/href="([^" ]+)">새 실험 시작/); + expect(match).not.toBeNull(); + links.push(match![1]); + expect((await get(match![1])).status).toBe(200); + } + expect(links[0]).not.toBe(links[1]); + }); it('동시 요청의 도착과 완료 기록을 덮어쓰지 않는다', async () => { const { get } = fixture(); await Promise.all(Array.from({ length: 20 }, () => get('/run/concurrent/html')));