Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/web-reading-lab-results-2026-10-26.md
Original file line number Diff line number Diff line change
@@ -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).
8 changes: 7 additions & 1 deletion experiments/web-read-lab/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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은 별도로 기록합니다.
Expand Down
2 changes: 1 addition & 1 deletion experiments/web-read-lab/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export default {
let status = 200;
if (test === 'index') {
body = html(
`<p>가상 상품: 실험용 지갑, 수량7. 실험번호: ${run}</p><ul>${CASES.map((c) => `<li><a href="/run/${run}/${encodeURIComponent(c)}${c === 'query' ? '?q=wallet' : ''}">${c}</a></li>`).join('')}</ul><a href="/run/${run}/query?q=%EC%A7%80%EA%B0%91">한글 쿼리</a><p>응답의 requestId와 generatedAt을 관측 기록과 비교하세요. received는 서버 도착, prepared는 응답 생성 완료이며 클라이언트 수신 성공을 증명하지 않습니다.</p><a href="/observations/${run}">관측 기록 JSON</a><p>현재 요청 번호 ${requestId}</p>`,
`<p>가상 상품: 실험용 지갑, 수량7. 실험번호: ${run}</p><p><a href="/run/${requestId}">새 실험 시작</a></p><ul>${CASES.map((c) => `<li><a href="/run/${run}/${encodeURIComponent(c)}${c === 'query' ? '?q=wallet' : ''}">${c}</a></li>`).join('')}</ul><a href="/run/${run}/query?q=%EC%A7%80%EA%B0%91">한글 쿼리</a><p>응답의 requestId와 generatedAt을 관측 기록과 비교하세요. received는 서버 도착, prepared는 응답 생성 완료이며 클라이언트 수신 성공을 증명하지 않습니다.</p><a href="/observations/${run}">관측 기록 JSON</a><p>현재 요청 번호 ${requestId}</p>`,
);
} else if (test === 'json' || test === 'text') {
headers.set(
Expand Down
12 changes: 12 additions & 0 deletions tests/scripts/web-read-lab.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')));
Expand Down
Loading