From beb4081e8686ca1b59306f2b6b3cd25bbbb5d45c Mon Sep 17 00:00:00 2001 From: Penguin Date: Thu, 20 Aug 2026 15:39:42 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20=EB=8C=80=ED=98=95=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=9E=90=EB=8F=99=20=EC=A0=91=ED=9E=98=EC=9D=84=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EA=B8=B8=EC=9D=B4=EA=B0=80=20=EC=95=84?= =?UTF-8?q?=EB=8B=88=EB=9D=BC=20=EB=B3=80=EA=B2=BD=EB=9F=89=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=ED=8C=90=EC=A0=95=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 뷰어는 첫 등장한 대형 파일을 접어서 마운트하는데(main.ts의 isLargeFile), 그 판정에 넘기던 changedLines가 변경량이 아니라 파일 전량이었다. FileDiffMetadata의 additionLines/deletionLines는 이름과 달리 string[]이고, 뷰어처럼 파일 전량으로 diff를 만들면(isPartial === false) 각각 새 파일과 옛 파일의 전체 내용이다(types.ts가 그렇게 문서화한다). 즉 실효 판정식이 "파일 길이 x 2 > 1500", 사실상 "파일 길이 > 750줄"이었다. 실측: 1,166줄 CLAUDE.md의 +28/-2 변경이 2,306으로 읽혔고, 같은 워킹트리에서 5개 파일이 이렇게 잘못 접혔다. 실제 변경 줄 수는 Hunk 쪽 동명 number 필드에만 있다. 파일 헤더의 +N -M 배지도 같은 값을 세므로(createFileHeaderElement), 이제 화면 배지와 접힘 판정이 일치한다. 임계값 1500과 lockfile 이름 규칙은 그대로다. 이 종류의 버그는 유닛이 원리적으로 못 잡는다 — isLargeFile 자체는 옳았고 호출부가 틀린 값을 넘겼을 뿐이라 기존 테스트 다섯 개가 전부 통과한 채로 버그가 살아 있었다. 그래서 배선까지 태우는 e2e를 회귀망으로 함께 넣는다. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01ArwfmcvGjKnRWMiEFBfJzF --- CLAUDE.md | 1 + .../__tests__/viewer-large-file.test.ts | 39 ++++++++++++++++++ apps/viewer/browser/largeFile.ts | 14 +++++++ apps/viewer/browser/main.ts | 5 +-- apps/viewer/e2e/fixtures/repo.ts | 26 ++++++++++++ apps/viewer/e2e/large-file-collapse.e2e.ts | 41 +++++++++++++++++++ 6 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 apps/viewer/e2e/large-file-collapse.e2e.ts diff --git a/CLAUDE.md b/CLAUDE.md index e8dd187..07b8bda 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -121,6 +121,7 @@ cd scripts/parity && python3 -m http.server 8099 # http://127.0.0.1:8099/index.h - **"Loading…" 자가 치유 — 세 부분이 함께여야 동작한다.** Bun 1.3.12의 `$`(ShellPromise)는 `diff.ts`의 `BUILD_CONCURRENCY=8` git 서브프로세스 버스트가 외부 프로세스 생성 경합과 겹치면 resolve도 reject도 없이 영구 pending이 된다(자식은 사라지고 좀비도 없고 이벤트 루프도 정상인데 프라미스만 안 끝난다 — 실측). `ShellPromise`엔 `.timeout()`/`.kill()`이 없어 `Promise.race`가 유일한 레버다. 예전엔 `singleFlight`가 키를 `.finally()`에서만 지워 그 키가 **영구 오염**되고 이후 모든 요청이 죽은 프라미스에 합류했다. 지금은 ① `singleFlight`가 flight를 타임아웃과 race해 키를 풀고(`SingleFlightTimeoutError`로 호출자가 타임아웃을 구분한다), ② `awaitFlight`가 **타임아웃만** 503+`Retry-After`로 흡수하며(그 외 에러는 다시 던져 기존 동작 보존), ③ `browser/main.ts`의 `fetchDiff`가 503·네트워크 실패를 1회 재시도한다(403·400은 terminal). **따로 넣으면 어느 쪽도 동작하지 않는다** — 키를 안 풀고 재시도하면 같은 죽은 프라미스에 다시 합류한다. **상수 제약은 per-flight가 아니라 합이다**: `/api/diff`가 `resolveBaseCached` → `diffFlight`를 순차로 두 번 기다리므로 45+45=90 < `idleTimeout` 120(슬랙 30초). 재시도가 1회인 이유도 `BUILD_CONCURRENCY=8`이 호출당이라(전역 세마포어 아님) 시도가 겹치면 동시 git 서브프로세스가 배로 늘어 재시도가 스스로를 느리게 만들기 때문이다. **덮지 않는 곳**: `isGitRepo`는 flight 앞에서, `getRepoSummary`·`getFileBytes`는 뒤에서 돌고 flight로 안 감싸였다 — 거기서 매달리면 예전 실패 모드 그대로 `idleTimeout`이 소켓을 닫으며, `/api/blob`은 클라이언트 재시도가 없어 이미지가 그냥 안 뜬다. 회귀망: `diff-server.test.ts`의 실제 HTTP 503 2종 + `self-heal.e2e.ts`. - **`server.ts`의 `baseCache`는 반드시 모듈 스코프에 남아야 한다** (`diffCache`와 달리 `createHandler` 안으로 옮기지 마라). `diff-server.test.ts`의 "diffFlight 타임아웃" 테스트가 기본 타임아웃 서버로 캐시를 데운 뒤 **별도로 새로 띄운** `flightTimeoutMs:1` 서버가 그 warm 항목을 그대로 봐야만, `baseFlight`가 마이크로태스크로 1ms 레이스를 이기고 제어가 `diffFlight`까지 도달한다. 옮기면(구조적 격리라는 그럴듯한 이유로 그럴 수 있다) 두 번째 서버가 빈 캐시로 시작해 `baseFlight`가 miss로 되돌아가고, 그 테스트는 **조용히** 첫 번째 테스트와 똑같은 `baseFlight` 가드만 다시 증명한다 — 실패가 아니라 무증상 퇴화다. 두 테스트가 **정반대 조건**(하나는 캐시 미스, 하나는 히트)에 의존하므로 공용 픽스처로 합치지도 마라. - **커버리지 100%가 무엇을 뜻하지 않는지 알 것.** `bunfig.toml`의 게이트는 line·function·statement만 세고 **branch를 세지 않는다.** 그래서 `if (x instanceof Response) return x;` 같은 한 줄 가드는 `if`가 실행되기만 하면 covered로 찍히고, 그 `return`이 한 번도 안 나가도 100%가 유지된다(실제로 503 반환 경로 4개가 그 상태였다). 새 분기를 넣을 때 커버리지 초록을 증거로 받지 말고, **일부러 그 분기로 들어가는 테스트**를 따로 둘 것. +- **대형 파일 자동 접힘의 기준은 "변경량"이지 "파일 길이"가 아니다 — `FileDiffMetadata.additionLines`는 이름이 거짓말을 한다.** 뷰어는 첫 등장한 대형 파일을 접어서 마운트하는데(`main.ts`의 `isLargeFile`), 그 판정에 쓰는 `changedLines`를 예전엔 `fileDiff.additionLines.length + fileDiff.deletionLines.length`로 셌다. 그런데 `FileDiffMetadata`의 그 두 필드(**`string[]`**)는 변경된 줄이 아니다 — 뷰어는 `parseDiffFromFile`로 파일 **전량**에서 diff를 만들어 `isPartial === false`이고, 그때 둘은 각각 새/옛 파일의 **전체 내용**이다(`types.ts`가 그렇게 문서화한다). 즉 실효 판정식이 "파일 길이 × 2 > 1500" ≈ "파일 길이 > 750"이 되어, 몇 줄만 고친 긴 문서가 접힌 채로 떴다(실측: 1,166줄 `CLAUDE.md`의 +28/−2 변경이 2,306으로 읽혔다. 같은 워킹트리에서 5개 파일이 이렇게 잘못 접혔다). 실제 변경 줄 수는 **`Hunk` 쪽 동명 `number` 필드**(`hunk.additionLines`/`hunk.deletionLines` — `+`/`-` 프리픽스 줄 수)에만 있고, 파일 헤더에 뜨는 `+N −M` 배지도 정확히 그걸 센다(`createFileHeaderElement`) — 화면의 배지와 접힘 판정이 같은 값을 봐야 사용자에게 일관된다. `largeFile.ts`의 `countChangedLines(hunks)`가 그 합이다. **이 종류의 버그는 유닛이 원리적으로 못 잡는다**: `isLargeFile` 자체는 옳았고 호출부가 틀린 값을 넘겼을 뿐이라, 그 함수의 기존 테스트 다섯 개가 전부 통과한 채로 버그가 살아 있었다(같은 이유로 `main.ts`는 커버리지 게이트 밖이고 배선은 e2e가 맡는다). 회귀망: `large-file-collapse.e2e.ts`(2,000줄 파일에 3줄 변경 → 안 접힘). 반대 방향은 `retokenize-cache.e2e.ts`의 `big.ts`(전량 재작성 = 진짜 8,000줄 변경 → 여전히 접힘)와 `lockfile-freeze.e2e.ts`(이름 기반 lockfile 접힘)가 지킨다. - **포크 패키지는 import 경로 + 재구성 타입만 수정** — 렌더/로직 변경 금지 (Foundation 원칙). 오버홀은 별도 plan에서. 예외는 건별 합의 + `[diffdeck]` 주석으로 upstream 이탈을 코드에 표기 + e2e 회귀망 동반일 때만. 현재 예외 4건 (1~3은 `packages/diffs`, 4는 `packages/trees`): 1. `DiffHunksRenderer.recycle()`의 하이라이터 동기 재획득 — 빠른 스크롤 headerless blink 완치 (`header-mount.e2e.ts` 극한 프로브가 회귀망). 2. 빈 렌더 윈도우(totalLines 0 = collapsed) 렌더를 plain-text + zero-range로 — 하이라이트 렌더가 범위를 무시하고 전체 파일을 동기 토크나이즈해 대형 lockfile 마운트가 수 초 프리징하던 것 완치. `renderDiff` sync/async 두 경로 + `RenderedDiffASTCache.emptyWindow` 표식(빈 풀을 확장 렌더가 재사용하면 processDiffResult가 throw — 표식이 확장 시 재렌더를 강제). 회귀망: `lockfile-freeze.e2e.ts` (30k줄 프리징 게이트 + 8k줄 sub-cutoff 펼침 무오류). diff --git a/apps/viewer/__tests__/viewer-large-file.test.ts b/apps/viewer/__tests__/viewer-large-file.test.ts index b6daab0..b616cd9 100644 --- a/apps/viewer/__tests__/viewer-large-file.test.ts +++ b/apps/viewer/__tests__/viewer-large-file.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; import { + countChangedLines, isLargeFile, LARGE_FILE_LINE_THRESHOLD, } from "../browser/largeFile.ts"; @@ -25,3 +26,41 @@ describe("isLargeFile", () => { expect(isLargeFile("src/yarn.lock.ts", 5)).toBe(false); }); }); + +describe("countChangedLines", () => { + // FileDiffMetadata.additionLines/deletionLines(string[])는 이름과 달리 + // "변경된 줄"이 아니다: parseDiffFromFile로 만든 diff는 isPartial === false라 + // 그 둘이 각각 새/옛 파일의 **전량**이다. 실제 +/- 줄 수는 hunk 쪽 동명 + // 숫자 필드에만 있다. + test("sums the +/- line counts across hunks", () => { + expect( + countChangedLines([ + { additionLines: 28, deletionLines: 2 }, + { additionLines: 5, deletionLines: 11 }, + ]), + ).toBe(46); + }); + test("a diff with no hunks counts as zero", () => { + expect(countChangedLines([])).toBe(0); + }); + test("a long file with a small edit is not large", () => { + // headerlab의 CLAUDE.md 실측: 1166줄 파일에 +28/-2. + // 파일 전량(1166 + 1140 = 2306)을 세면 임계값을 넘어 접혔다. + expect( + isLargeFile( + "CLAUDE.md", + countChangedLines([{ additionLines: 28, deletionLines: 2 }]), + ), + ).toBe(false); + }); + test("a huge rewrite of the same file is still large", () => { + expect( + isLargeFile( + "CLAUDE.md", + countChangedLines([ + { additionLines: LARGE_FILE_LINE_THRESHOLD, deletionLines: 1 }, + ]), + ), + ).toBe(true); + }); +}); diff --git a/apps/viewer/browser/largeFile.ts b/apps/viewer/browser/largeFile.ts index 6d34b25..c0c46e2 100644 --- a/apps/viewer/browser/largeFile.ts +++ b/apps/viewer/browser/largeFile.ts @@ -1,3 +1,5 @@ +import type { Hunk } from "@diffdeck/diffs"; + export const LARGE_FILE_LINE_THRESHOLD = 1500; export const LOCKFILE_NAMES: ReadonlySet = new Set([ @@ -24,3 +26,15 @@ const basename = (path: string): string => { export const isLargeFile = (name: string, changedLines: number): boolean => LOCKFILE_NAMES.has(basename(name)) || changedLines > LARGE_FILE_LINE_THRESHOLD; + +// 한 파일에서 실제로 변경된(`+`/`-` 프리픽스가 붙은) 줄 수. +// +// `FileDiffMetadata`에도 동명의 `additionLines`/`deletionLines`가 있지만 그건 +// **string[]**이고, 뷰어처럼 파일 전량으로 파싱한 diff(`isPartial === false`)에서는 +// 각각 새/옛 파일의 전체 내용이다 — 세면 "변경량"이 아니라 "파일 길이 × 2"가 된다. +// 실제 변경 줄 수는 hunk 쪽 동명 **숫자** 필드에만 있고, 파일 헤더에 뜨는 +// `+N -M` 배지도 같은 값을 센다(`createFileHeaderElement`). +export const countChangedLines = ( + hunks: readonly Pick[], +): number => + hunks.reduce((sum, h) => sum + h.additionLines + h.deletionLines, 0); diff --git a/apps/viewer/browser/main.ts b/apps/viewer/browser/main.ts index 975af32..4dfa302 100644 --- a/apps/viewer/browser/main.ts +++ b/apps/viewer/browser/main.ts @@ -43,7 +43,7 @@ import { extractSnippet } from "./grab/snippet.ts"; import { resolveTextTarget, rowSide } from "./grab/textSelection.ts"; import { ensureImageCard, IMAGE_CARD_CSS } from "./imageCard.ts"; import { blobUrl, type ImageEntry, imageEntries } from "./imageDiff.ts"; -import { isLargeFile } from "./largeFile.ts"; +import { countChangedLines, isLargeFile } from "./largeFile.ts"; import { createParseCache } from "./parseCache.ts"; import { FLATTEN_KEY, @@ -887,8 +887,7 @@ const renderPatch = (unsorted: DiffFile[]): void => { // collapsed on first sight. if (!seenIds.has(f.name)) { seenIds.add(f.name); - const changedLines = - fileDiff.additionLines.length + fileDiff.deletionLines.length; + const changedLines = countChangedLines(fileDiff.hunks); if (isLargeFile(f.name, changedLines)) collapsedIds.add(f.name); } return { diff --git a/apps/viewer/e2e/fixtures/repo.ts b/apps/viewer/e2e/fixtures/repo.ts index 14aa60e..119f9eb 100644 --- a/apps/viewer/e2e/fixtures/repo.ts +++ b/apps/viewer/e2e/fixtures/repo.ts @@ -72,6 +72,13 @@ export interface FixtureRepoOptions { * opt-in (bulkFiles/lockfileLines와 같은 이유). */ bigFileLines?: number; + /** + * Opt-in: commit `src/long.ts` with LONG_FILE_LINES lines and edit only a + * handful of them in the working tree — a file that is long but whose diff + * is small. large-file-collapse.e2e.ts 전용 (다른 스펙의 픽스처를 바꾸지 + * 않도록 opt-in — bulkFiles/lockfileLines와 같은 이유). + */ + longFileSmallEdit?: boolean; /** * Opt-in: skip every working-tree edit so the diff is empty on launch * (the untracked `data.txt` is still written — hidden behind the @@ -107,6 +114,12 @@ const bulkFileLines = (marker: string, length = 200): string => `export const ${marker}_${i} = ${i}; // ${marker} filler line ${i}`, ).join("\n")}\n`; +// 길지만 변경은 작은 파일: 자동 접힘이 "파일 길이"가 아니라 "변경량"으로 +// 판정되는지 가르는 픽스처. 구 로직(파일 전량 카운트)이면 4,000줄로 읽혀 +// 접히고, 변경량(6줄)으로 읽으면 접히지 않는다. +const LONG_FILE_LINES = 2000; +const LONG_FILE_EDITED_LINES = 3; + // pnpm-lock.yaml 흉내: 실제 lockfile처럼 패키지 블록이 반복되는 YAML. // mutate 시 20줄마다 버전만 바꿔 수천 줄짜리 현실적인 diff를 만든다. const lockfileContents = (lines: number, mutate: boolean): string => { @@ -164,6 +177,12 @@ export const makeFixtureRepo = ( bulkFileLines("base", bigFileLines), ); } + if (options.longFileSmallEdit) { + writeFileSync( + join(dir, "src", "long.ts"), + bulkFileLines("base", LONG_FILE_LINES), + ); + } if (options.nestedChainFile) { mkdirSync(join(dir, "src", "mid", "deep"), { recursive: true }); writeFileSync( @@ -230,6 +249,13 @@ export const makeFixtureRepo = ( bulkFileLines("edited", bigFileLines), ); } + if (options.longFileSmallEdit) { + const lines = bulkFileLines("base", LONG_FILE_LINES).split("\n"); + for (let i = 0; i < LONG_FILE_EDITED_LINES; i++) { + lines[i] = lines[i]?.replaceAll("base", "edited") ?? ""; + } + writeFileSync(join(dir, "src", "long.ts"), lines.join("\n")); + } if (options.nestedChainFile) { writeFileSync( join(dir, "src", "mid", "deep", "nested.ts"), diff --git a/apps/viewer/e2e/large-file-collapse.e2e.ts b/apps/viewer/e2e/large-file-collapse.e2e.ts new file mode 100644 index 0000000..482eabd --- /dev/null +++ b/apps/viewer/e2e/large-file-collapse.e2e.ts @@ -0,0 +1,41 @@ +// 자동 접힘의 판정 기준은 "파일 길이"가 아니라 "변경량"이다. +// +// 뷰어는 첫 등장한 대형 파일을 접어서 마운트한다(main.ts의 isLargeFile). 그 +// "대형"의 기준이 되는 changedLines를 예전엔 `FileDiffMetadata`의 +// additionLines/deletionLines 길이 합으로 셌는데, 그 둘은 이름과 달리 변경된 +// 줄이 아니다: 뷰어는 파일 전량으로 diff를 만들어(`isPartial === false`) 두 +// 배열이 각각 새/옛 파일의 **전체 내용**이 된다. 즉 실효 판정식이 "파일 길이 +// × 2 > 1500"이라, 몇 줄만 고친 긴 문서가 접힌 채로 떴다(실측: 1,166줄 +// CLAUDE.md의 +28/-2 변경이 2,306으로 읽혔다). +// +// 이 계약은 유닛이 원리적으로 못 잡는다 — isLargeFile 자체는 옳았고 호출부가 +// 틀린 값을 넘겼을 뿐이라, 기존 isLargeFile 테스트 다섯 개가 전부 통과한 채로 +// 버그가 살아 있었다. 그래서 배선까지 태우는 e2e가 회귀망이다. +// +// 픽스처는 2,000줄 파일에서 3줄만 고친다: 구 로직이면 4,000으로 읽혀 접히고, +// 변경량(6줄)으로 읽으면 접히지 않는다. 반대 방향(변경량이 실제로 임계값을 +// 넘는 대형 재작성은 여전히 접힌다)은 retokenize-cache.e2e.ts의 big.ts가, +// 이름 기반 lockfile 접힘은 lockfile-freeze.e2e.ts가 이미 지킨다. +import { expect, hasCode, launchViewer, test } from "./fixtures/app.ts"; + +test("a long file whose diff is small must not start collapsed", async ({ + page, +}) => { + const viewer = await launchViewer([], { longFileSmallEdit: true }); + try { + await page.goto(viewer.url); + await expect(page.locator("#status")).toHaveText(/\d+ file\(s\)/, { + timeout: 15_000, + }); + await expect + .poll(() => hasCode(page, "src/long.ts"), { timeout: 20_000 }) + .toBe(true); + // 헤더 caret도 같은 말을 해야 한다: 펼쳐진 파일의 버튼은 "접기"를 제안한다. + await expect(page.locator('[data-fold="src/long.ts"]')).toHaveAttribute( + "aria-label", + "Collapse file", + ); + } finally { + await viewer.stop(); + } +}); From 6f03045dca51316c280bb494d3844522f5f18f5a Mon Sep 17 00:00:00 2001 From: Penguin Date: Thu, 20 Aug 2026 15:59:00 +0900 Subject: [PATCH 2/4] =?UTF-8?q?test:=20=EC=A0=91=ED=9E=98=20=ED=9A=8C?= =?UTF-8?q?=EA=B7=80=EB=A7=9D=EC=9D=98=20=EB=B0=98=EB=8C=80=20=EB=B0=A9?= =?UTF-8?q?=ED=96=A5=EC=9D=84=20=EC=8B=A4=EC=A0=9C=EB=A1=9C=20=EB=8B=A8?= =?UTF-8?q?=EC=96=B8=ED=95=98=EA=B3=A0=20=EB=AC=B8=EC=84=9C=20=EC=A3=BC?= =?UTF-8?q?=EC=9E=A5=20=EC=85=8B=EC=9D=84=20=EC=A0=95=EC=A0=95=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 앞 커밋은 "접히지 않는다" 한 방향만 단언하고, 반대 방향은 기존 스펙이 지킨다고 문서에 적었다. 실측해보니 그 주장이 거짓이었다. - retokenize-cache.e2e.ts와 lockfile-freeze.e2e.ts 어디에도 접힘 상태를 단언하는 코드가 없다. 전자는 헤더를 click해 펼친 뒤 하이라이트를 기다리고, 후자는 hasHeader만 보는데 헤더는 접혔든 펼쳐졌든 있다. 자동 접힘이 사라지면 그 click이 반대로 파일을 접어 뒤따르는 단언이 깨지는 부수효과로만 빨간불이 된다. - 게다가 lockfile 픽스처는 20줄마다 2줄을 바꿔 8k에서 1,596줄, 30k에서 5,996줄을 변경한다(실측). 둘 다 임계값 1,500을 넘으므로 크기 규칙만으로 이미 접힌다 — LOCKFILE_NAMES를 통째로 비워도 그 스펙은 초록이라 이름 규칙의 회귀망이 아니었다. 그래서 large-file-collapse.e2e.ts가 세 방향을 한 파일에서 지키게 한다: 1. 2,000줄 파일에 3줄 변경 -> 안 접힘 2. 800줄 전량 재작성(1,600줄 변경) -> 접힘 3. 1,000줄 lockfile(변경량 196줄로 임계값 아래) -> 이름 규칙으로만 접힘 셋 다 뮤테이션으로 판별력을 확인했다: countChangedLines를 0으로 퇴화시키면 2번만 죽고, LOCKFILE_NAMES를 비우면 3번만 죽는다. 기존 픽스처 옵션 (bigFileLines/lockfileLines)을 그대로 쓰므로 새 픽스처 배선은 없다. 함께 정정한 문서 주장 둘: - 헤더 배지 표기 순서는 `-M +N`이다. createFileHeaderElement가 삭제 span을 먼저 push하고 [data-metadata]에 order가 없어 DOM 순서가 곧 화면 순서다. - "1,166줄 CLAUDE.md" 실측의 출처가 headerlab임을 밝힌다. 이 레포의 CLAUDE.md는 147줄이라, 출처 없이 두면 수치가 틀린 것으로 오독된다. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01ArwfmcvGjKnRWMiEFBfJzF --- CLAUDE.md | 2 +- apps/viewer/browser/largeFile.ts | 3 +- apps/viewer/e2e/large-file-collapse.e2e.ts | 67 ++++++++++++++++++++-- 3 files changed, 64 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 07b8bda..59025f6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -121,7 +121,7 @@ cd scripts/parity && python3 -m http.server 8099 # http://127.0.0.1:8099/index.h - **"Loading…" 자가 치유 — 세 부분이 함께여야 동작한다.** Bun 1.3.12의 `$`(ShellPromise)는 `diff.ts`의 `BUILD_CONCURRENCY=8` git 서브프로세스 버스트가 외부 프로세스 생성 경합과 겹치면 resolve도 reject도 없이 영구 pending이 된다(자식은 사라지고 좀비도 없고 이벤트 루프도 정상인데 프라미스만 안 끝난다 — 실측). `ShellPromise`엔 `.timeout()`/`.kill()`이 없어 `Promise.race`가 유일한 레버다. 예전엔 `singleFlight`가 키를 `.finally()`에서만 지워 그 키가 **영구 오염**되고 이후 모든 요청이 죽은 프라미스에 합류했다. 지금은 ① `singleFlight`가 flight를 타임아웃과 race해 키를 풀고(`SingleFlightTimeoutError`로 호출자가 타임아웃을 구분한다), ② `awaitFlight`가 **타임아웃만** 503+`Retry-After`로 흡수하며(그 외 에러는 다시 던져 기존 동작 보존), ③ `browser/main.ts`의 `fetchDiff`가 503·네트워크 실패를 1회 재시도한다(403·400은 terminal). **따로 넣으면 어느 쪽도 동작하지 않는다** — 키를 안 풀고 재시도하면 같은 죽은 프라미스에 다시 합류한다. **상수 제약은 per-flight가 아니라 합이다**: `/api/diff`가 `resolveBaseCached` → `diffFlight`를 순차로 두 번 기다리므로 45+45=90 < `idleTimeout` 120(슬랙 30초). 재시도가 1회인 이유도 `BUILD_CONCURRENCY=8`이 호출당이라(전역 세마포어 아님) 시도가 겹치면 동시 git 서브프로세스가 배로 늘어 재시도가 스스로를 느리게 만들기 때문이다. **덮지 않는 곳**: `isGitRepo`는 flight 앞에서, `getRepoSummary`·`getFileBytes`는 뒤에서 돌고 flight로 안 감싸였다 — 거기서 매달리면 예전 실패 모드 그대로 `idleTimeout`이 소켓을 닫으며, `/api/blob`은 클라이언트 재시도가 없어 이미지가 그냥 안 뜬다. 회귀망: `diff-server.test.ts`의 실제 HTTP 503 2종 + `self-heal.e2e.ts`. - **`server.ts`의 `baseCache`는 반드시 모듈 스코프에 남아야 한다** (`diffCache`와 달리 `createHandler` 안으로 옮기지 마라). `diff-server.test.ts`의 "diffFlight 타임아웃" 테스트가 기본 타임아웃 서버로 캐시를 데운 뒤 **별도로 새로 띄운** `flightTimeoutMs:1` 서버가 그 warm 항목을 그대로 봐야만, `baseFlight`가 마이크로태스크로 1ms 레이스를 이기고 제어가 `diffFlight`까지 도달한다. 옮기면(구조적 격리라는 그럴듯한 이유로 그럴 수 있다) 두 번째 서버가 빈 캐시로 시작해 `baseFlight`가 miss로 되돌아가고, 그 테스트는 **조용히** 첫 번째 테스트와 똑같은 `baseFlight` 가드만 다시 증명한다 — 실패가 아니라 무증상 퇴화다. 두 테스트가 **정반대 조건**(하나는 캐시 미스, 하나는 히트)에 의존하므로 공용 픽스처로 합치지도 마라. - **커버리지 100%가 무엇을 뜻하지 않는지 알 것.** `bunfig.toml`의 게이트는 line·function·statement만 세고 **branch를 세지 않는다.** 그래서 `if (x instanceof Response) return x;` 같은 한 줄 가드는 `if`가 실행되기만 하면 covered로 찍히고, 그 `return`이 한 번도 안 나가도 100%가 유지된다(실제로 503 반환 경로 4개가 그 상태였다). 새 분기를 넣을 때 커버리지 초록을 증거로 받지 말고, **일부러 그 분기로 들어가는 테스트**를 따로 둘 것. -- **대형 파일 자동 접힘의 기준은 "변경량"이지 "파일 길이"가 아니다 — `FileDiffMetadata.additionLines`는 이름이 거짓말을 한다.** 뷰어는 첫 등장한 대형 파일을 접어서 마운트하는데(`main.ts`의 `isLargeFile`), 그 판정에 쓰는 `changedLines`를 예전엔 `fileDiff.additionLines.length + fileDiff.deletionLines.length`로 셌다. 그런데 `FileDiffMetadata`의 그 두 필드(**`string[]`**)는 변경된 줄이 아니다 — 뷰어는 `parseDiffFromFile`로 파일 **전량**에서 diff를 만들어 `isPartial === false`이고, 그때 둘은 각각 새/옛 파일의 **전체 내용**이다(`types.ts`가 그렇게 문서화한다). 즉 실효 판정식이 "파일 길이 × 2 > 1500" ≈ "파일 길이 > 750"이 되어, 몇 줄만 고친 긴 문서가 접힌 채로 떴다(실측: 1,166줄 `CLAUDE.md`의 +28/−2 변경이 2,306으로 읽혔다. 같은 워킹트리에서 5개 파일이 이렇게 잘못 접혔다). 실제 변경 줄 수는 **`Hunk` 쪽 동명 `number` 필드**(`hunk.additionLines`/`hunk.deletionLines` — `+`/`-` 프리픽스 줄 수)에만 있고, 파일 헤더에 뜨는 `+N −M` 배지도 정확히 그걸 센다(`createFileHeaderElement`) — 화면의 배지와 접힘 판정이 같은 값을 봐야 사용자에게 일관된다. `largeFile.ts`의 `countChangedLines(hunks)`가 그 합이다. **이 종류의 버그는 유닛이 원리적으로 못 잡는다**: `isLargeFile` 자체는 옳았고 호출부가 틀린 값을 넘겼을 뿐이라, 그 함수의 기존 테스트 다섯 개가 전부 통과한 채로 버그가 살아 있었다(같은 이유로 `main.ts`는 커버리지 게이트 밖이고 배선은 e2e가 맡는다). 회귀망: `large-file-collapse.e2e.ts`(2,000줄 파일에 3줄 변경 → 안 접힘). 반대 방향은 `retokenize-cache.e2e.ts`의 `big.ts`(전량 재작성 = 진짜 8,000줄 변경 → 여전히 접힘)와 `lockfile-freeze.e2e.ts`(이름 기반 lockfile 접힘)가 지킨다. +- **대형 파일 자동 접힘의 기준은 "변경량"이지 "파일 길이"가 아니다 — `FileDiffMetadata.additionLines`는 이름이 거짓말을 한다.** 뷰어는 첫 등장한 대형 파일을 접어서 마운트하는데(`main.ts`의 `isLargeFile`), 그 판정에 쓰는 `changedLines`를 예전엔 `fileDiff.additionLines.length + fileDiff.deletionLines.length`로 셌다. 그런데 `FileDiffMetadata`의 그 두 필드(**`string[]`**)는 변경된 줄이 아니다 — 뷰어는 `parseDiffFromFile`로 파일 **전량**에서 diff를 만들어 `isPartial === false`이고, 그때 둘은 각각 새/옛 파일의 **전체 내용**이다(`types.ts`가 그렇게 문서화한다). 즉 실효 판정식이 "파일 길이 × 2 > 1500" ≈ "파일 길이 > 750"이 되어, 몇 줄만 고친 긴 문서가 접힌 채로 떴다(실측: **headerlab**의 1,166줄 `CLAUDE.md`에 +28/−2를 낸 변경이 2,306으로 읽혔다 — 이 레포의 CLAUDE.md가 아니다. 그 워킹트리에서 5개 파일이 이렇게 잘못 접혔다). 실제 변경 줄 수는 **`Hunk` 쪽 동명 `number` 필드**(`hunk.additionLines`/`hunk.deletionLines` — `+`/`-` 프리픽스 줄 수)에만 있고, 파일 헤더에 뜨는 `-M +N` 배지도 정확히 그걸 센다(`createFileHeaderElement` — 삭제 span을 먼저 push하고 `[data-metadata]`에 `order`가 없어 화면 순서가 `-M +N`이지 `+N -M`이 아니다) — 화면의 배지와 접힘 판정이 같은 값을 봐야 사용자에게 일관된다. `largeFile.ts`의 `countChangedLines(hunks)`가 그 합이다. **이 종류의 버그는 유닛이 원리적으로 못 잡는다**: `isLargeFile` 자체는 옳았고 호출부가 틀린 값을 넘겼을 뿐이라, 그 함수의 기존 테스트 다섯 개가 전부 통과한 채로 버그가 살아 있었다(같은 이유로 `main.ts`는 커버리지 게이트 밖이고 배선은 e2e가 맡는다). 회귀망: `large-file-collapse.e2e.ts` 3종이 세 방향을 **한 파일에서** 지킨다 — ① 2,000줄 파일에 3줄 변경 → 안 접힘, ② 800줄 전량 재작성(1,600줄 변경) → 접힘, ③ 1,000줄 lockfile(20줄마다 2줄만 바뀌어 변경량 196줄 → 임계값 한참 아래) → 이름 규칙으로만 접힘. 셋 다 뮤테이션으로 판별력을 확인했다(`countChangedLines`를 0으로 퇴화시키면 ②만, `LOCKFILE_NAMES`를 비우면 ③만 죽는다). **②③을 기존 스펙에 맡기면 안 된다** — 한때 `retokenize-cache.e2e.ts`(big.ts)와 `lockfile-freeze.e2e.ts`가 그 둘을 지킨다고 여기 적혀 있었지만 **둘 다 접힘 상태를 단언하지 않는다**: 전자는 헤더를 click해 펼친 뒤 하이라이트를 기다리고, 후자는 `hasHeader`만 보는데 헤더는 접혔든 펼쳐졌든 있다. 자동 접힘이 사라지면 그 click이 반대로 파일을 접어 뒤따르는 단언이 깨지는 **부수효과**로만 빨간불이 될 뿐이다. 게다가 그 lockfile 픽스처는 8k에서 1,596줄·30k에서 5,996줄을 변경하므로(실측) **크기 규칙만으로 이미 접힌다** — `LOCKFILE_NAMES`를 통째로 비워도 그 스펙은 초록이라 이름 규칙의 회귀망이 아니었다. - **포크 패키지는 import 경로 + 재구성 타입만 수정** — 렌더/로직 변경 금지 (Foundation 원칙). 오버홀은 별도 plan에서. 예외는 건별 합의 + `[diffdeck]` 주석으로 upstream 이탈을 코드에 표기 + e2e 회귀망 동반일 때만. 현재 예외 4건 (1~3은 `packages/diffs`, 4는 `packages/trees`): 1. `DiffHunksRenderer.recycle()`의 하이라이터 동기 재획득 — 빠른 스크롤 headerless blink 완치 (`header-mount.e2e.ts` 극한 프로브가 회귀망). 2. 빈 렌더 윈도우(totalLines 0 = collapsed) 렌더를 plain-text + zero-range로 — 하이라이트 렌더가 범위를 무시하고 전체 파일을 동기 토크나이즈해 대형 lockfile 마운트가 수 초 프리징하던 것 완치. `renderDiff` sync/async 두 경로 + `RenderedDiffASTCache.emptyWindow` 표식(빈 풀을 확장 렌더가 재사용하면 processDiffResult가 throw — 표식이 확장 시 재렌더를 강제). 회귀망: `lockfile-freeze.e2e.ts` (30k줄 프리징 게이트 + 8k줄 sub-cutoff 펼침 무오류). diff --git a/apps/viewer/browser/largeFile.ts b/apps/viewer/browser/largeFile.ts index c0c46e2..aa51257 100644 --- a/apps/viewer/browser/largeFile.ts +++ b/apps/viewer/browser/largeFile.ts @@ -33,7 +33,8 @@ export const isLargeFile = (name: string, changedLines: number): boolean => // **string[]**이고, 뷰어처럼 파일 전량으로 파싱한 diff(`isPartial === false`)에서는 // 각각 새/옛 파일의 전체 내용이다 — 세면 "변경량"이 아니라 "파일 길이 × 2"가 된다. // 실제 변경 줄 수는 hunk 쪽 동명 **숫자** 필드에만 있고, 파일 헤더에 뜨는 -// `+N -M` 배지도 같은 값을 센다(`createFileHeaderElement`). +// `-M +N` 배지도 같은 값을 센다(`createFileHeaderElement` — 삭제 span을 먼저 +// push하므로 화면 순서가 `-M +N`이지 `+N -M`이 아니다). export const countChangedLines = ( hunks: readonly Pick[], ): number => diff --git a/apps/viewer/e2e/large-file-collapse.e2e.ts b/apps/viewer/e2e/large-file-collapse.e2e.ts index 482eabd..7dbbb01 100644 --- a/apps/viewer/e2e/large-file-collapse.e2e.ts +++ b/apps/viewer/e2e/large-file-collapse.e2e.ts @@ -5,19 +5,27 @@ // additionLines/deletionLines 길이 합으로 셌는데, 그 둘은 이름과 달리 변경된 // 줄이 아니다: 뷰어는 파일 전량으로 diff를 만들어(`isPartial === false`) 두 // 배열이 각각 새/옛 파일의 **전체 내용**이 된다. 즉 실효 판정식이 "파일 길이 -// × 2 > 1500"이라, 몇 줄만 고친 긴 문서가 접힌 채로 떴다(실측: 1,166줄 -// CLAUDE.md의 +28/-2 변경이 2,306으로 읽혔다). +// × 2 > 1500"이라, 몇 줄만 고친 긴 문서가 접힌 채로 떴다(실측: headerlab의 +// 1,166줄 CLAUDE.md에 +28/-2를 낸 변경이 2,306으로 읽혔다). // // 이 계약은 유닛이 원리적으로 못 잡는다 — isLargeFile 자체는 옳았고 호출부가 // 틀린 값을 넘겼을 뿐이라, 기존 isLargeFile 테스트 다섯 개가 전부 통과한 채로 // 버그가 살아 있었다. 그래서 배선까지 태우는 e2e가 회귀망이다. // -// 픽스처는 2,000줄 파일에서 3줄만 고친다: 구 로직이면 4,000으로 읽혀 접히고, -// 변경량(6줄)으로 읽으면 접히지 않는다. 반대 방향(변경량이 실제로 임계값을 -// 넘는 대형 재작성은 여전히 접힌다)은 retokenize-cache.e2e.ts의 big.ts가, -// 이름 기반 lockfile 접힘은 lockfile-freeze.e2e.ts가 이미 지킨다. +// **세 방향을 여기서 함께 지킨다.** 예전엔 ①만 두고 ②를 +// `retokenize-cache.e2e.ts`가, ③을 `lockfile-freeze.e2e.ts`가 지킨다고 적어 +// 뒀는데 둘 다 사실이 아니었다. 그 두 스펙은 접힘 상태를 **단언하지 않는다** +// (전자는 헤더 click으로 펼친 뒤 하이라이트를 기다리고, 후자는 헤더 존재만 +// 본다 — 헤더는 접혔든 펼쳐졌든 있다). 자동 접힘이 사라져도 클릭이 반대로 +// 동작해 뒤따르는 단언이 깨지는 **부수효과**로만 빨간불이 될 뿐이다. 게다가 +// lockfile 픽스처는 20줄마다 2줄을 바꿔 8k에서 1,596줄, 30k에서 5,996줄을 +// 변경하므로(실측) **크기 규칙만으로 이미 접힌다** — LOCKFILE_NAMES를 통째로 +// 비워도 그 스펙은 초록이라, 이름 규칙을 지키는 회귀망이 아니었다. import { expect, hasCode, launchViewer, test } from "./fixtures/app.ts"; +// ① 길지만 변경이 작은 파일: 2,000줄에서 3줄만 고친다. 구 로직이면 파일 +// 전량(2,000 + 1,997)이 4,000쯤으로 읽혀 접혔고, 변경량(6줄)으로 읽으면 +// 접히지 않는다. test("a long file whose diff is small must not start collapsed", async ({ page, }) => { @@ -39,3 +47,50 @@ test("a long file whose diff is small must not start collapsed", async ({ await viewer.stop(); } }); + +// ② 반대 방향: 변경량이 실제로 임계값을 넘으면 여전히 접혀야 한다. 800줄 +// 전량 재작성 = 1,600줄 변경으로 LARGE_FILE_LINE_THRESHOLD(1,500)를 넘긴다 +// (실측). 이 단언이 없으면 changedLines가 항상 0에 가까운 값으로 퇴화해도 +// ①은 그대로 초록이라(접히지 않는 게 기대값) 아무도 못 잡는다. +test("a diff larger than the threshold still starts collapsed", async ({ + page, +}) => { + const viewer = await launchViewer([], { bigFileLines: 800 }); + try { + await page.goto(viewer.url); + await expect(page.locator("#status")).toHaveText(/\d+ file\(s\)/, { + timeout: 15_000, + }); + await expect(page.locator('[data-fold="src/big.ts"]')).toHaveAttribute( + "aria-label", + "Expand file", + { timeout: 20_000 }, + ); + expect(await hasCode(page, "src/big.ts")).toBe(false); + } finally { + await viewer.stop(); + } +}); + +// ③ 이름 규칙: lockfile은 변경량이 임계값 아래여도 접힌다. 1,000줄 픽스처는 +// 20줄마다 2줄만 바꿔 변경량이 196줄에 그치므로(실측) 크기 규칙으로는 절대 +// 접히지 않는다 — 접히는 유일한 근거가 largeFile.ts의 LOCKFILE_NAMES다. +test("a lockfile collapses by name even when its diff is small", async ({ + page, +}) => { + const viewer = await launchViewer([], { lockfileLines: 1000 }); + try { + await page.goto(viewer.url); + await expect(page.locator("#status")).toHaveText(/\d+ file\(s\)/, { + timeout: 15_000, + }); + await expect(page.locator('[data-fold="pnpm-lock.yaml"]')).toHaveAttribute( + "aria-label", + "Expand file", + { timeout: 20_000 }, + ); + expect(await hasCode(page, "pnpm-lock.yaml")).toBe(false); + } finally { + await viewer.stop(); + } +}); From 6c1846898d71b0143cb55825d7ee96cf73d8e852 Mon Sep 17 00:00:00 2001 From: Penguin Date: Thu, 20 Aug 2026 16:06:32 +0900 Subject: [PATCH 3/4] =?UTF-8?q?docs:=20=EC=9E=84=EA=B3=84=EA=B0=92=20?= =?UTF-8?q?=EC=83=81=EC=88=98=EC=97=90=20=EA=B7=BC=EA=B1=B0=EB=A5=BC=20?= =?UTF-8?q?=EB=82=A8=EA=B8=B0=EA=B3=A0=20=ED=9B=84=EC=86=8D=20=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B=EC=9D=B4=20=EB=93=A4=EC=9D=B8=20=ED=8B=80=EB=A6=B0=20?= =?UTF-8?q?=EC=88=AB=EC=9E=90=EB=A5=BC=20=EA=B3=A0=EC=B9=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 코드 리뷰 지적 둘을 반영한다. 1. LARGE_FILE_LINE_THRESHOLD(1500)는 이번 수정으로 의미가 바뀌었는데 값과 주석이 그대로였다. 예전엔 "파일 길이 × 2"와 비교돼 사실상 750줄 파일을 걸렀고, 이제는 실제 변경 줄 수와 비교한다. 값은 의도적으로 유지했다는 것과, 1,600 이상으로 올리면 large-file-collapse.e2e.ts ②가 먼저 빨간불이 된다는 트립와이어 성질을 함께 적는다. 상수마다 근거를 남기는 레포에서 이 줄만 맨몸이었다. 2. 앞 커밋이 ① 주석에 "파일 전량(2,000 + 1,997)이 4,000쯤"이라고 적었는데 틀렸다. isPartial === false에서 deletionLines는 옛 파일 전량이고, 이 픽스처는 줄을 교체할 뿐이라 옛 파일도 정확히 2,000줄이다. 실측하면 2,000 + 2,000 = 정확히 4,000이라 1,997이 나올 자리가 없다. 틀린 숫자를 고치려던 커밋이 새 틀린 숫자를 들인 셈이라 짚어둘 값어치가 있다. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01ArwfmcvGjKnRWMiEFBfJzF --- apps/viewer/browser/largeFile.ts | 7 +++++++ apps/viewer/e2e/large-file-collapse.e2e.ts | 6 +++--- test-results/.last-run.json | 4 ++++ 3 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 test-results/.last-run.json diff --git a/apps/viewer/browser/largeFile.ts b/apps/viewer/browser/largeFile.ts index aa51257..fe9704b 100644 --- a/apps/viewer/browser/largeFile.ts +++ b/apps/viewer/browser/largeFile.ts @@ -1,5 +1,12 @@ import type { Hunk } from "@diffdeck/diffs"; +// 자동 접힘의 문턱 — 한 파일에서 **변경된** 줄 수(`+`와 `-`의 합)와 비교한다. +// `countChangedLines` 도입 전에는 같은 값이 "파일 길이 × 2"와 비교돼 사실상 +// 750줄짜리 파일을 걸렀다. 의미가 바뀌었지만 값은 의도적으로 유지했다 — +// 이제 대략 "750줄 파일을 통째로 다시 쓴 정도"가 접힘 기준이라 자동 접힘이 +// 훨씬 드물어지는데, 그게 이 수정이 노린 바다. 값을 1,600 이상으로 올리면 +// `large-file-collapse.e2e.ts` ②(800줄 전량 재작성 = 1,600줄 변경)가 먼저 +// 빨간불이 된다 — 우연이 아니라 의도된 트립와이어다. export const LARGE_FILE_LINE_THRESHOLD = 1500; export const LOCKFILE_NAMES: ReadonlySet = new Set([ diff --git a/apps/viewer/e2e/large-file-collapse.e2e.ts b/apps/viewer/e2e/large-file-collapse.e2e.ts index 7dbbb01..453956a 100644 --- a/apps/viewer/e2e/large-file-collapse.e2e.ts +++ b/apps/viewer/e2e/large-file-collapse.e2e.ts @@ -23,9 +23,9 @@ // 비워도 그 스펙은 초록이라, 이름 규칙을 지키는 회귀망이 아니었다. import { expect, hasCode, launchViewer, test } from "./fixtures/app.ts"; -// ① 길지만 변경이 작은 파일: 2,000줄에서 3줄만 고친다. 구 로직이면 파일 -// 전량(2,000 + 1,997)이 4,000쯤으로 읽혀 접혔고, 변경량(6줄)으로 읽으면 -// 접히지 않는다. +// ① 길지만 변경이 작은 파일: 2,000줄에서 3줄만 고친다. 줄을 교체할 뿐이라 +// 옛 파일도 2,000줄이므로, 구 로직이면 새/옛 전량을 더해 정확히 4,000으로 +// 읽혀 접혔다(실측). 변경량(6줄)으로 읽으면 접히지 않는다. test("a long file whose diff is small must not start collapsed", async ({ page, }) => { diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000..5fca3f8 --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "failed", + "failedTests": [] +} \ No newline at end of file From 21cd5d4b9cd37cbee90b419480e95dc2f6f9e98b Mon Sep 17 00:00:00 2001 From: Penguin Date: Thu, 20 Aug 2026 16:07:27 +0900 Subject: [PATCH 4/4] =?UTF-8?q?chore:=20=EC=8B=A4=EC=88=98=EB=A1=9C=20?= =?UTF-8?q?=EC=BB=A4=EB=B0=8B=EB=90=9C=20Playwright=20=EC=82=B0=EC=B6=9C?= =?UTF-8?q?=EB=AC=BC=EC=9D=84=20=EC=A0=9C=EA=B1=B0=ED=95=98=EA=B3=A0=20git?= =?UTF-8?q?ignore=EB=A5=BC=20=EC=9D=BC=EB=B0=98=ED=99=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 앞 커밋에 test-results/.last-run.json이 딸려 들어갔다. .gitignore가 apps/viewer/test-results/ 로 경로를 고정하고 있었는데, Playwright는 실행 cwd 기준으로 산출물을 만들어 레포 루트에서 돌리면 루트에도 생긴다. 두 규칙에서 경로 접두를 떼어 어느 위치에서 돌리든 무시되게 한다. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01ArwfmcvGjKnRWMiEFBfJzF --- .gitignore | 4 ++-- test-results/.last-run.json | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) delete mode 100644 test-results/.last-run.json diff --git a/.gitignore b/.gitignore index f2c04cb..32fa005 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,5 @@ dist/ scripts/parity/out/ .superpowers/ docs/superpowers/ -apps/viewer/test-results/ -apps/viewer/playwright-report/ +test-results/ +playwright-report/ diff --git a/test-results/.last-run.json b/test-results/.last-run.json deleted file mode 100644 index 5fca3f8..0000000 --- a/test-results/.last-run.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "status": "failed", - "failedTests": [] -} \ No newline at end of file