refactor: 프론트엔드 리펙토링

This commit is contained in:
donghyeon-ka
2026-09-18 22:05:42 +09:00
parent 5cc41467ae
commit ec7f20e2ee
100 changed files with 6005 additions and 2867 deletions
@@ -247,6 +247,8 @@ domain, application state, query cache, global store, diagnostics에 넣지 않
### 3.2 picker baseline과 enhancement
- 접근 가능한 `<input type="file">`가 모든 browser의 canonical baseline이다.
baseline adapter는 `input.click()`으로 이 경로를 활성화한다. `showPicker()`
존재 여부는 portable baseline capability의 판정 기준으로 사용하지 않는다.
- `showOpenFilePicker()``showSaveFilePicker()`는 runtime method별 feature
detection을 거친 progressive enhancement다. UA sniffing을 사용하지 않는다.
- picker 호출은 click/keyboard handler의 첫 browser action이어야 한다. 그 전에
@@ -987,7 +989,7 @@ recipe의 공통 code는 UI/telemetry에 raw DOMException을 노출하지 않기
| `PERMISSION_DENIED` | picker/save permission | baseline/manual fallback |
| `LIMIT_EXCEEDED` | count/bytes/page/buffer budget | 입력 축소 |
| `POLICY_REJECTED` | type/cache/data policy | 저장·전송 금지 |
| `BLOCKED` | IndexedDB older context | 다른 탭 close/retry UI |
| `BLOCKED` | IndexedDB older context가 live open/upgrade를 막는 중 | 다른 탭 close/retry UI |
| `CONFLICT` | revision/generation/idempotency conflict | authoritative re-read |
| `MIGRATION_FAILED` | schema/data migration | read-only/online-only |
| `QUOTA_EXCEEDED` | actual write failure | rollback, reconstructable GC, bounded retry |
@@ -999,6 +1001,12 @@ recipe의 공통 code는 UI/telemetry에 raw DOMException을 노출하지 않기
| `UNAVAILABLE` | browser/worker/storage temporarily unavailable | documented fallback |
| `UNSUPPORTED` | capability absence | baseline/online-only |
`IndexedDbConnectionStatus.BLOCKED`는 대기 중인 live attempt의 observable
상태다. blocked deadline이 끝나 `open()`이 `BLOCKED` failure로 settle되면
그 attempt는 더 이상 열리는 중이 아니므로 status는
`CLOSED / NOT_OPENED`로 전이한다. 늦게 성공한 native connection은 즉시 닫고
settled status를 되살리지 않는다.
user dismissal은 failure가 아니라 outcome이다. browser DOMException name은
adapter에서 이 vocabulary로 mapping하고 raw message/stack은 local bounded
diagnostic에도 기본 저장하지 않는다.
@@ -1085,18 +1093,17 @@ Playwright Chromium, Firefox, WebKit에서 실제 secure-origin API를 검사한
`test:browser-capabilities`의 JUnit을
`verify:browser-capability-evidence`가 읽어 세 engine의 testcase 집합 동일성,
양수 실행 수, zero failure/error/skipped와 skipped/failure node 부재를 강제한다.
현재 checkout의 source suite는 engine마다 정확히 같은 14개 case(File 2,
IndexedDB 4, OPFS/Cache/StorageManager 각 1, cross-context invalidation 2,
presigned streaming download/multipart upload/Image CDN 각 1)를 정의한다.
promotion artifact는 Chromium/Firefox/WebKit의 14개씩, 총 42개가 모두
실행되어야 한다. 이 host의
WebKit은 필수 native libraries(예:
`libbacktrace.so.0`, `libevent-2.1.so.7`, `libjxl.so.0.8`,
`libavif.so.16`과 WPE 계열) 부재로 실행되지 않았고 현재 보존 artifact도
Chromium/Firefox 14개씩 총 28개만 통과한 상태다. 따라서 promotion evidence를
충족하지 않으며 verifier가 실패하는 것이 정상이다. `INSTALLED` 전에는 필요한
system dependency가 있는 CI/device에서 세 engine 전체 evidence를 새로 생성해야
한다.
browser capability promotion은 source suite에서 관찰된 동일 testcase set을
Chromium/Firefox/WebKit에서 모두 실행하는 규칙으로 관리한다. case 수를 문서
상수로 복제하지 않고 `verify:browser-capability-evidence`가 Chromium 결과를
baseline set으로 계산해 Firefox/WebKit의 set 동일성, 양수 실행 수,
failure/error/skipped 0과 failure/skipped node 부재를 검증한다.
capability absence도 skip 사유가 아니다. 예를 들어 어떤 engine에서
`navigator.storage`가 없다면 동일 testcase 안에서 adapter의
`UNSUPPORTED / STORAGE_ESTIMATE / ONLINE_ONLY` 결과를 browser truth로
검증한다. 세 engine을 실행할 수 없는 host의 artifact는 promotion evidence로
사용하지 않는다.
- native input keyboard/focus/same-file reselection/multiple/dismissal
- Chromium conditional picker/save enhancement와 다른 engine fallback
@@ -48,18 +48,39 @@ its business requirement.
## Scenario B — IndexedDB local draft
An executable consumer probe lives at:
The original executable consumer probe remains at:
`tests/contract/consumer-experience/indexeddb-local-draft.test.ts`
with the feature-owned fixture:
with its isolated fixture:
`tests/contract/consumer-experience/fixtures/local-draft-feature.ts`
The fixture models a small local-draft feature with save/find/remove and
optimistic revision checking.
The architecture is now also exercised by a real vertical slice under:
Measured result:
`src/features/local-draft-feature`
It owns the Local Draft domain/application API and binds an
`IndexedDbRepositoryPort<LocalDraft, never>` in its feature adapter. The
cross-capability composition contract lives at:
`tests/contract/reusable-capability/feature-adapter-composition.test.ts`
That test composes one HTTP-only contribution and the IndexedDB-only Local Draft
contribution through the same generic catalog path.
The browser-level composition proof lives at:
`tests/browser-capabilities/local-draft-composition.spec.ts`
It runs against native browser IndexedDB. The test creates the platform
`createIndexedDbRuntime`, exposes that runtime through the typed
`createIndexedDbRepositoryProvider`, composes
`LOCAL_DRAFT_FEATURE_ADAPTER_CONTRIBUTION`, then executes Local Draft
save/find/remove through the feature API. The Local Draft feature still does
not import the IndexedDB runtime or native browser API.
Measured probe result:
| Metric | Result |
| --- | ---: |
@@ -81,8 +102,11 @@ The contract test rejects feature source that reaches for
## What this does and does not prove
The probe confirms that **feature business code does not need native IndexedDB
knowledge** once an `IndexedDbRepositoryPort` has been composed.
The contract probe confirms that **feature business code does not need native
IndexedDB knowledge** once an `IndexedDbRepositoryPort` has been composed.
The browser-capability proof additionally confirms that the same feature
contribution works when that port is backed by the repository's real
`createIndexedDbRuntime` and native IndexedDB implementation.
It does not prove that composition of `createIndexedDbRuntime` is cheap.
That constructor still owns substantial infrastructure policy:
@@ -96,10 +120,15 @@ That constructor still owns substantial infrastructure policy:
- durability, scheduling and observation.
That complexity belongs at the composition/platform boundary, not in the
feature. A new `create...Repository<T>` convenience factory should be added
only when a second real product consumer demonstrates which subset is stable
enough to become a reusable profile. Creating one now would guess at policy and
risk producing a universal storage abstraction.
feature. The Local Draft vertical slice is now the second concrete consumer, and
it confirms that the stable seam is the typed repository provider plus a
feature-owned repository identity. It does **not** show that dataset scope,
retention, migration, codec or lifecycle-authority configuration can be safely
collapsed into one universal `create...Repository<T>` factory.
A convenience profile should therefore be introduced only after another
IndexedDB-backed product feature repeats the same infrastructure policy, not
merely because two features consume the same repository port.
## Consumer-quality review checklist
@@ -189,17 +189,18 @@ chunk별 `CapabilityResult<Uint8Array>`를 반환한다. backend upload example
실제 upload feature는 이 예시를 그대로 import하지 않고 purpose와 backend
protocol에 맞게 contract를 더 좁힌다.
현재 checkout의 browser source suite는 engine마다 같은 14개 case(File 2,
IndexedDB 4, OPFS/Cache/StorageManager 각 1, cross-context invalidation 2,
presigned streaming download/multipart upload/Image CDN 각 1)를 정의한다.
promotion artifact는 Chromium/Firefox/WebKit 각각 14개, 총 42개를 모두
실행해야 한다. WebKit은 현재
host의 필수 native libraries(예:
`libbacktrace.so.0`, `libevent-2.1.so.7`, `libjxl.so.0.8`,
`libavif.so.16`과 WPE 계열) 부재로 실행되지 않았다. 보존 artifact는
Chromium/Firefox 14개씩 총 28개만 통과했으므로
`verify:browser-capability-evidence`가 실패하는 것이 정상이다. 세 engine
evidence가 완성되기 전에는 product 상태를 `INSTALLED`로 올리지 않는다.
browser capability promotion은 source suite가 정의한 동일 testcase set을
Chromium/Firefox/WebKit에서 모두 실행해야 한다. 구체적인 case 개수는 이 결정
문서에 고정하지 않는다. `verify:browser-capability-evidence`가 Chromium
artifact에서 baseline set을 계산하고 Firefox/WebKit과의 set 동일성 및
failure/error/skipped 0을 기계적으로 검증한다.
engine별 native capability 차이는 testcase를 skip하는 이유가 아니다. capability가
없으면 adapter의 명시적 unsupported/degraded result를 같은 testcase에서
검증한다. baseline `<input type="file">` activation은 portable한
`input.click()` 경로를 사용하고, `showOpenFilePicker()` 같은 API는 별도
progressive enhancement로 유지한다. 세 engine evidence가 완성되기 전에는
product 상태를 `INSTALLED`로 올리지 않는다.
## 선택 이후 필요한 구현
@@ -105,6 +105,39 @@ Central installed catalogs are aggregation points only:
Feature-specific composition belongs under the feature itself. Central
catalogs must not grow feature-specific branching logic.
Adapter contributions declare the platform capabilities they consume through
`needs`. The generic contribution seam in
`src/features/feature-adapter-contribution.ts` derives the context from that
list, so an HTTP-only feature cannot reach IndexedDB and an IndexedDB-only
feature does not receive the HTTP executor. It also binds
`featureId -> ApplicationFeatureInputs[featureId]` at the contribution
definition site instead of recovering that relationship with a final catalog
cast.
A contribution becomes composable only through
`defineFeatureAdapterContribution()`. That factory adds the private
contribution brand required by `composeFeatureAdapterInputs()`; a raw object
with the same visible fields is rejected by TypeScript and checked again at
runtime. The negative type fixture
`invalid-direct-feature-composition.ts` is part of FE-GATE-003 so this
authority cannot be bypassed by calling the composer directly.
For IndexedDB-backed features, `createIndexedDbRepositoryProvider()` is the
composition helper that maps feature-owned repository identities to typed
`IndexedDbRepositoryPort` instances. It does not create a universal storage
repository or move dataset/migration/lifecycle policy into the feature.
The repository now has two executable contribution shapes:
- Reference feature: `needs: ["http"]`
- Local Draft feature: `needs: ["indexedDb"]`
Local Draft is a compiled architecture consumer used to prove the second
capability shape; it is not added to the default product manifest. Its
browser-capability test composes the feature over the real
`createIndexedDbRuntime` and native IndexedDB, proving that this seam is not
limited to an in-memory contract fixture.
## Presentation consumer surface
`ApplicationProvider` remains the composition root for presentation, but new
@@ -337,7 +337,14 @@ Run on the landed tree. Only what actually passed is claimed as passing.
| --- | --- | --- |
| `tests/unit/ci-workflow-generation.test.ts` | 82 failed / 325 passed | Identical on the pre-change baseline (`git stash` comparison). The subprocess gates it spawns cannot run in this sandbox. |
| `tests/unit/ci-artifact-contract.test.ts` | fails | Unchanged pre-existing sandbox, cgroup and `/tmp` permission behaviour. |
| `tests/unit/security-followup.test.ts`, `tests/unit/provider-guardian-transaction.test.ts`, `tests/unit/risk-coverage.test.ts` | flaky under full-suite load | All three pass in a fresh process (78 passed together). They spawn and reap process groups, so their timing assertions are load sensitive. |
| `tests/unit/security-followup.test.ts`, `tests/unit/provider-guardian-transaction.test.ts`, `tests/unit/risk-coverage.test.ts` | flaky under full-suite load | Historical baseline result. All three passed in a fresh process (78 passed together). |
> Current classification update (2026-09-18): the provider guardian transaction
> suite moved to `tests/integration/provider-guardian-transaction.test.ts`.
> It spawns child processes and exercises filesystem/IPC/process lifecycle, so it
> is not part of the pure unit pool. Its READY/PUBLISHED wait uses a dedicated
> test watchdog rather than treating a 1-second scheduler delay as a production
> protocol deadline.
### Destructive fixture hazard — fixed
@@ -375,7 +382,7 @@ files above plus the two flaky-under-load ones:
| `tests/unit/ci-workflow-generation.test.ts` | 82 | Identical on the pre-change baseline (`git stash` comparison). Its subprocess gates cannot run in this sandbox. |
| `tests/unit/ci-artifact-contract.test.ts` | 19 | Unchanged pre-existing sandbox, cgroup and `/tmp` permission behaviour. |
| `tests/unit/security-followup.test.ts` | 2 | Passes in isolation. |
| `tests/unit/provider-guardian-transaction.test.ts` | 1 | Passes in isolation. |
| `tests/unit/provider-guardian-transaction.test.ts` (historical path; now `tests/integration/provider-guardian-transaction.test.ts`) | 1 | Historical baseline: passed in isolation; current suite is classified as process integration. |
1619 passed / 1723 total, and `tests/unit/removal-fixture.test.ts`,
`tests/unit/supply-chain.test.ts` and
@@ -148,6 +148,9 @@ read-only나 online-only가 사용자 작성 내용을 잃게 한다면 먼저 e
### 신호
- `BLOCKED`, `UPGRADE_BLOCKED` 또는 blocked duration bucket 증가
- live attempt가 기다리는 동안 status는 `BLOCKED`
- blocked deadline settle 뒤에는 open result가 `BLOCKED` failure여도
runtime status는 `CLOSED / NOT_OPENED`
- `versionchange` 뒤 connection이 남음
- repeated reload/update loop
- open/maintenance의 `POLICY_REJECTED`: immutable dataset binding missing/mismatch
@@ -500,16 +503,13 @@ browser engine/version, fixture ID, fault phase, bounded counts/buckets와 PASS/
promotion 직전에는 다음 repository evidence도 함께 보존한다.
- `test:browser-capabilities`가 만든 JUnit에서 Chromium, Firefox, WebKit이 동일
14개 testcase set(File 2, IndexedDB 4, OPFS/Cache/StorageManager 각 1,
cross-context invalidation 2, presigned streaming download/multipart
upload/Image CDN 각 1)을 실제 실행해 총 42개이며 failure/error/skipped가 모두
0이어야 한다.
`verify:browser-capability-evidence`가 engine 집합과 testcase 동일성을
기계적으로 검증한다. 현재 artifact는 Chromium/Firefox 14개씩 총 28개
통과했지만 WebKit 실행에 필요한 native libraries(예:
`libbacktrace.so.0`, `libevent-2.1.so.7`, `libjxl.so.0.8`,
`libavif.so.16`과 WPE 계열)가 이 host에 없으므로 아직 promotion 가능 상태가
- `test:browser-capabilities`가 만든 JUnit에서 Chromium, Firefox, WebKit이
source suite의 동일 testcase set을 실제 실행하고 failure/error/skipped가 모두
0이어야 한다. 구체적인 case 수는 이 runbook에 복제하지 않는다.
`verify:browser-capability-evidence`가 Chromium을 baseline으로 testcase set과
engine 집합을 동적으로 검증한다. native capability가 없는 engine도 testcase를
skip하지 않고 adapter의 명시적 `UNSUPPORTED`/degraded 결과를 검증해야 한다.
세 engine 중 하나라도 실행되지 않은 artifact는 promotion 가능 상태
아니다.
- `artifacts/quality/vite-module-inventory.json`에서 optional runtime source root가
production chunk에 없음을 `check:optional-recipes`로 검증한다.
@@ -105,6 +105,24 @@ component suite로 분리되어 있다.
실제 QueryClient 위에서 검증한다. HTTP 자동 retry가 소유자이므로 이 adapter의
query/mutation vendor retry는 꺼져 있다.
같은 기준을 대형 unit suite에도 적용한다. 줄 수를 기준으로 자르지 않고 실패가
가리켜야 하는 behavior owner를 기준으로 분리한다.
- `public-response-cache.test.ts`: 일반 stage/activate/lookup/cache contract
- `public-response-cache-repair.test.ts`: active release repair의 failure atomicity
- `resumable-upload-runtime.test.ts`: upload/reconcile/control-plane 흐름
- `resumable-upload-runtime-teardown.test.ts`: bounded drain/raw provider teardown
- `security-followup.test.ts`: archived local evidence
- `security-provider-evidence.test.ts`: provider signature/supervision/process lifecycle
- `security-promotion-staging.test.ts`: private staging seal/replay/CLI identity
- `tests/integration/provider-guardian-transaction.test.ts`: child process,
filesystem, IPC frame, READY/PUBLISHED handshake와 process lifecycle. pure unit
pool에 두지 않으며 handshake budget은 protocol timeout이 아닌 test watchdog이다.
-`*-fixture.ts`: 해당 owner들 사이에서만 공유하는 deterministic test fixture
이 분리는 production owner와 test failure surface를 맞추기 위한 것이며, 단순
LOC 감축 목적이 아니다.
#### Form과 route 위험
form component/reference feature test가 error summary, 첫 오류 focus, Zod
+6 -2
View File
@@ -11,7 +11,7 @@ Each gate is blocking in its declared scope. Failures are not downgraded with
| unit | `pnpm test:unit` | Node-only domain/application/pure policy/runtime units; no systemd/bwrap/cgroup prerequisite | `artifacts/tests/unit.xml` |
| capability contract | `pnpm test:contract` | reusable capability consumer contracts | `artifacts/tests/contract.xml` |
| component | `pnpm test:component` | React/hook/UI behavior | `artifacts/tests/component.xml` |
| integration | `pnpm test:integration` | HTTP/MSW, IndexedDB and composed browser-runtime boundaries | `artifacts/tests/integration.xml` |
| integration | `pnpm test:integration` | HTTP/MSW, IndexedDB, composed browser-runtime boundaries, child-process/filesystem/IPC integration | `artifacts/tests/integration.xml` |
| system / CI runner | `pnpm test:system` | compatible Linux host with systemd, bubblewrap, cgroup v2 and CI-provider process controls | `artifacts/tests/system.xml` |
| end-to-end | `pnpm test:e2e` | pinned browser engines | `artifacts/tests/e2e/` |
| accessibility | `pnpm test:a11y` | pinned browser engines | `artifacts/tests/a11y.json` |
@@ -64,7 +64,11 @@ CI / release assurance:
A host-level process/sandbox test must not be placed in `tests/unit` merely
because it uses Vitest. The classification follows the system boundary and
prerequisites, not the test framework.
prerequisites, not the test framework. Process-heavy tests that spawn child
processes but do not require privileged host facilities belong in
`tests/integration`; for example the provider guardian transaction protocol
lives at `tests/integration/provider-guardian-transaction.test.ts`. Its
handshake timeout is a test watchdog, not a production protocol deadline.
End-to-end and automated accessibility scenarios run on the pinned Chromium,
Firefox, and WebKit engines. The responsive contract explicitly exercises