ac-storage 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,165 +0,0 @@
1
- # 버그 수정 후 의도와 맞지 않는 테스트 보고서
2
-
3
- ## 분석 기준
4
- json-accessor v0.7에서 drop된 accessor는 더 이상 사용할 수 없도록 변경되었습니다.
5
- drop 후 commit/release 등의 작업은 실제로는 에러를 발생시켜야 하지만,
6
- 일부 테스트는 "에러가 발생하지 않음"을 검증하고 있어 의도와 맞지 않습니다.
7
-
8
- ---
9
-
10
- ## 1. idempotent-operations.test.ts
11
-
12
- ### 문제 있는 테스트:
13
-
14
- #### Test 1: `release then drop` (라인 117-131)
15
- ```typescript
16
- test('release then drop', async () => {
17
- // Release first (saves file)
18
- await storage.release('test.json');
19
-
20
- // Re-access and drop (release removes from memory, need to access again to drop)
21
- await storage.accessAsJSON('test.json');
22
- await storage.drop('test.json');
23
- expect(fs.existsSync(filePath)).toBeFalsy();
24
- });
25
- ```
26
- **문제**: 주석에 "release removes from memory, need to access again to drop"라고 명시
27
- - 이는 버그가 아니라 정상 동작
28
- - release 후 drop하려면 다시 access 해야 함
29
- - 테스트가 이를 "회피 방법"처럼 설명하고 있음
30
-
31
- **판단**: ⚠️ 오해의 소지 - 주석 수정 필요
32
-
33
- ---
34
-
35
- #### Test 2: `commit after drop` (라인 310-322)
36
- ```typescript
37
- test('commit after drop', async () => {
38
- const accessor = await storage.accessAsJSON('test.json');
39
- accessor.setOne('data', 'value');
40
-
41
- // Drop without commit
42
- await storage.drop('test.json');
43
-
44
- // Commit after drop (should not throw, but file is gone)
45
- await expect(storage.commit('test.json')).resolves.not.toThrow();
46
-
47
- const filePath = path.join(testDir, 'test.json');
48
- expect(fs.existsSync(filePath)).toBeFalsy();
49
- });
50
- ```
51
- **문제**: drop 후 commit이 에러를 던지지 않는지 검증
52
- - drop된 accessor는 이미 제거됨
53
- - commit('test.json')은 존재하지 않는 것을 commit하려는 시도
54
- - 에러를 던지지 않는 것이 정상인지 의문
55
-
56
- **판단**: ❌ **제거 권장** - 의미 없는 동작 검증
57
-
58
- ---
59
-
60
- #### Test 3: `mixed scenario: file1 release→drop, file2 drop→release` (라인 247-271)
61
- ```typescript
62
- test('mixed scenario: file1 release→drop, file2 drop→release', async () => {
63
- // File1: release then re-access then drop
64
- await storage.release('file1.json');
65
- await storage.accessAsJSON('file1.json');
66
- await storage.drop('file1.json');
67
-
68
- // File2: drop then try release (should not throw on non-existent)
69
- const acc2 = await storage.accessAsJSON('file2.json');
70
- await storage.drop('file2.json');
71
-
72
- // Release after drop (file doesn't exist)
73
- await expect(storage.release('file2.json')).resolves.not.toThrow();
74
- });
75
- ```
76
- **문제**: drop 후 release가 에러를 던지지 않는지 검증
77
- - drop 후 release는 의미 없는 작업
78
- - 이미 메모리에서 제거된 것을 release하려는 시도
79
-
80
- **판단**: ❌ **제거 권장** - 무의미한 순서 검증
81
-
82
- ---
83
-
84
- #### Test 4: `commit → release → commit cycle` (라인 293-308)
85
- ```typescript
86
- test('commit → release → commit cycle', async () => {
87
- const accessor = await storage.accessAsJSON('test.json');
88
- accessor.setOne('step', 1);
89
-
90
- await storage.commit('test.json');
91
- await storage.release('test.json');
92
-
93
- // Try to commit released file (should not throw)
94
- await expect(storage.commit('test.json')).resolves.not.toThrow();
95
- });
96
- ```
97
- **문제**: release 후 commit이 에러를 던지지 않는지 검증
98
- - release는 메모리에서 unload하는 작업
99
- - release 후 commit은 존재하지 않는 것을 commit하려는 시도
100
-
101
- **판단**: ⚠️ **수정 필요** - release 후 commit의 동작이 명확하지 않음
102
-
103
- ---
104
-
105
- ## 2. release.test.ts
106
-
107
- #### Test: `drop vs release comparison` (라인 146-164)
108
- ```typescript
109
- test('drop vs release comparison', async () => {
110
- // Setup two files
111
- const acc1 = await storage.accessAsJSON('file1.json');
112
- const acc2 = await storage.accessAsText('file2.txt');
113
-
114
- // Release file1 - should save and keep file
115
- await storage.release('file1.json');
116
- expect(fs.existsSync(file1Path)).toBeTruthy();
117
-
118
- // Drop file2 - should delete file
119
- await storage.drop('file2.txt');
120
- expect(fs.existsSync(file2Path)).toBeFalsy();
121
- });
122
- ```
123
- **문제**: 없음 - 정상적인 비교 테스트
124
-
125
- **판단**: ✅ **유지** - 명확한 차이점 설명
126
-
127
- ---
128
-
129
- ## 요약
130
-
131
- ### 제거 권장 (3개):
132
- 1. ❌ `commit after drop` - drop 후 commit은 무의미
133
- 2. ❌ `mixed scenario: file1 release→drop, file2 drop→release` - drop 후 release는 무의미
134
- 3. ⚠️ `commit → release → commit cycle` - release 후 commit 동작이 불명확
135
-
136
- ### 수정 권장 (1개):
137
- 1. ⚠️ `release then drop` - 주석 오해의 소지 ("need to access again"은 버그가 아니라 정상)
138
-
139
- ---
140
-
141
- ## 근본 문제
142
-
143
- **멱등성(idempotent) 테스트의 오해:**
144
- - 멱등성은 "같은 작업을 여러 번 해도 안전"을 의미
145
- - 하지만 **"다른 작업 후 실행해도 안전"**을 검증하는 것은 멱등성이 아님
146
- - drop 후 commit, release 후 commit 등은 멱등성 테스트가 아니라 **잘못된 순서 테스트**
147
-
148
- **올바른 멱등성 테스트:**
149
- - `commit → commit → commit` ✅
150
- - `release → release → release` ✅
151
- - `drop → drop → drop` ✅
152
-
153
- **잘못된 테스트:**
154
- - `drop → commit` ❌ (순서 오류)
155
- - `release → commit` ❌ (의미 불명확)
156
- - `drop → release` ❌ (무의미)
157
-
158
- ---
159
-
160
- ## 권장 조치
161
-
162
- 1. **즉시 제거**: `commit after drop`, `mixed scenario` 테스트
163
- 2. **검토 후 제거**: `commit → release → commit cycle` (release 후 commit 정책 확인 필요)
164
- 3. **주석 수정**: `release then drop` (정상 동작임을 명확히)
165
- 4. **파일명 재고려**: `idempotent-operations.test.ts` → 순서 관련 테스트가 섞여있음
package/REPORT.md DELETED
@@ -1,178 +0,0 @@
1
- # JSONAccessor 데이터 손상 이슈 분석 및 수정 보고서
2
-
3
- ## 개요
4
-
5
- ac-storage의 JSONAccessor 사용 시 간헐적으로 데이터가 손상되는 이슈가 보고됨.
6
- - 증상: 저장된 데이터가 빈 파일이 되거나 `{}`가 됨
7
- - 환경: Electron IPC를 통한 저장 로직, 단일 ACStorage 인스턴스
8
-
9
- ## 손상 발생 메커니즘 (수정 전)
10
-
11
- ```
12
- 1. Electron IPC에서 여러 BrowserWindow가 동시에 저장 요청
13
- 2. 단일 ACStorage의 commit()이 동시에 여러 번 호출됨
14
- 3. 내부적으로 JSONAccessor.save() → fs.writeFile() 동시 실행
15
- 4. 동시 writeFile()이 파일 내용을 섞어버림 → JSON 손상
16
- 5. 손상된 JSON을 다음에 load() → JSON.parse() 실패 → {} 반환
17
- 6. 사용자가 모르고 새 데이터 추가 후 save() → 원본 데이터 영구 손실
18
- ```
19
-
20
- ## 핵심 문제 코드 (수정 전)
21
-
22
- ### 1. JSONFS.ts - 조용한 실패
23
-
24
- ```typescript
25
- async read(filename: string) {
26
- if (existsSync(filename)) {
27
- const jsonText = await fs.readFile(filename, 'utf8');
28
- try {
29
- contents = JSON.parse(jsonText);
30
- }
31
- catch {
32
- contents = {}; // 조용한 실패! 에러 없이 빈 객체 반환
33
- }
34
- }
35
- }
36
- ```
37
-
38
- **문제**: JSON 파싱 실패 시 에러를 발생시키지 않고 빈 객체 `{}`를 반환.
39
-
40
- ### 2. JSONAccessor.ts - Race Condition
41
-
42
- ```typescript
43
- async save(force: boolean = false) {
44
- if (!this.#changed && !force) return;
45
- this.#changed = false; // write 완료 전 설정됨!
46
- await this.jsonFS?.write(this.filePath, this.contents);
47
- }
48
- ```
49
-
50
- **문제**: `#changed = false`가 `writeFile` 완료 전에 설정됨.
51
-
52
- ### 3. JSONFS.ts - Non-atomic Write
53
-
54
- ```typescript
55
- async write(filename: string, data: Record<string, unknown>) {
56
- await fs.writeFile(filename, JSON.stringify(data, null, 4));
57
- }
58
- ```
59
-
60
- **문제**: `fs.writeFile`은 atomic하지 않음.
61
-
62
- ---
63
-
64
- ## 수정 내용
65
-
66
- ### 1. SafeJSONFS 클래스 생성
67
-
68
- **파일**: `src/features/accessors/JSONAccessor/SafeJSONFS.ts`
69
-
70
- ```typescript
71
- class SafeJSONFS implements IJSONFS {
72
- #writeLock: Promise<void> = Promise.resolve();
73
-
74
- async read(filename: string): Promise<Record<string, any>> {
75
- // 빈 파일 체크
76
- if (jsonText.trim() === '') {
77
- throw new Error(`JSON file is empty: "${filename}"`);
78
- }
79
- // JSON 파싱 실패 시 에러 발생 (조용한 실패 방지)
80
- try {
81
- return JSON.parse(jsonText);
82
- } catch (error) {
83
- throw new Error(`Failed to parse JSON from "${filename}"`);
84
- }
85
- }
86
-
87
- async write(filename: string, contents: Record<string, any>): Promise<void> {
88
- // Write lock으로 동시 쓰기 직렬화
89
- const previousLock = this.#writeLock;
90
- this.#writeLock = new Promise((resolve) => { releaseLock = resolve; });
91
-
92
- await previousLock;
93
- await this.#atomicWrite(filename, contents);
94
- releaseLock();
95
- }
96
-
97
- async #atomicWrite(filename: string, contents: Record<string, any>): Promise<void> {
98
- // 임시 파일에 쓰고 rename (atomic operation)
99
- const tempFile = `${filename}.tmp.${Date.now()}`;
100
- await fs.writeFile(tempFile, JSON.stringify(contents, null, 4));
101
- await fs.rename(tempFile, filename); // atomic on POSIX
102
- }
103
- }
104
- ```
105
-
106
- ### 2. JSONAccessorManager 수정
107
-
108
- **파일**: `src/features/accessors/JSONAccessor/JSONAccessorManager.ts`
109
-
110
- ```typescript
111
- class JSONAccessorManager {
112
- #commitLock: Promise<void> = Promise.resolve();
113
- #safeJSONFS: SafeJSONFS | null = null;
114
-
115
- static fromFS(actualPath: string, tree?: JSONTree) {
116
- const accessor = new JSONAccessor(actualPath, tree);
117
- const safeJSONFS = new SafeJSONFS();
118
-
119
- // 기본 JSONFS를 SafeJSONFS로 교체
120
- (accessor as any).jsonFS = safeJSONFS;
121
-
122
- const manager = new JSONAccessorManager(accessor);
123
- manager.#safeJSONFS = safeJSONFS;
124
- return manager;
125
- }
126
-
127
- async commit() {
128
- // dropped 상태 체크
129
- if (this.isDropped()) return;
130
-
131
- // Commit lock으로 동시 호출 직렬화
132
- const previousLock = this.#commitLock;
133
- this.#commitLock = new Promise((resolve) => { releaseLock = resolve; });
134
-
135
- await previousLock;
136
- if (this.isDropped()) return; // 대기 후 재확인
137
-
138
- await this.accessor.save();
139
- releaseLock();
140
- }
141
- }
142
- ```
143
-
144
- ---
145
-
146
- ## 수정 후 동작
147
-
148
- | 문제 | 수정 전 | 수정 후 |
149
- |------|---------|---------|
150
- | 동시 commit() | 파일 손상 가능 | Lock으로 직렬화 |
151
- | 동시 writeFile() | 파일 내용 섞임 | Atomic write (temp + rename) |
152
- | 손상된 JSON 로드 | `{}` 반환 (조용한 실패) | 에러 발생 |
153
- | 빈 파일 로드 | `{}` 반환 | 에러 발생 |
154
- | 디렉토리 없음 | 에러 발생 | 자동 생성 |
155
-
156
- ---
157
-
158
- ## 테스트 결과
159
-
160
- ```
161
- Test Suites: 22 passed, 22 total
162
- Tests: 211 passed, 211 total
163
- ```
164
-
165
- 모든 테스트 통과. 동시 commit() 100회 테스트에서도 손상 발생하지 않음.
166
-
167
- ---
168
-
169
- ## 관련 파일
170
-
171
- | 파일 | 역할 | 변경 |
172
- |------|------|------|
173
- | `src/features/accessors/JSONAccessor/SafeJSONFS.ts` | 안전한 파일 읽기/쓰기 | **신규** |
174
- | `src/features/accessors/JSONAccessor/JSONAccessorManager.ts` | JSON 데이터 관리 | **수정** |
175
- | `src/features/accessors/JSONAccessor/index.ts` | 모듈 export | **수정** |
176
- | `src/features/storage/test/single-acstorage-corruption.test.ts` | 손상 재현 테스트 | 수정 |
177
- | `src/features/storage/test/acstorage-data-loss.test.ts` | 데이터 손실 테스트 | 수정 |
178
- | `src/features/storage/test/single-storage-empty-data.test.ts` | 빈 파일 테스트 | 수정 |
package/REPORT_2.md DELETED
@@ -1,31 +0,0 @@
1
- # JSONAccessor 데이터 손상 문제 수정 완료
2
-
3
- ## 문제
4
- - 단일 ACStorage에서 동시 commit() 호출 시 JSON 파일 손상
5
- - 손상된 파일 로드 시 조용히 `{}` 반환 → 데이터 영구 손실
6
-
7
- ## 원인
8
- 1. 동시 `fs.writeFile()` → 파일 내용 섞임
9
- 2. JSON 파싱 실패 시 에러 없이 `{}` 반환 (조용한 실패)
10
- 3. `#changed` 플래그 race condition
11
-
12
- ## 해결
13
- 1. **SafeJSONFS 생성** (`src/features/accessors/JSONAccessor/SafeJSONFS.ts`)
14
- - Write lock으로 동시 쓰기 직렬화
15
- - Atomic write (temp file → rename)
16
- - 빈 파일/손상된 JSON 로드 시 에러 발생
17
- - 디렉토리 자동 생성
18
-
19
- 2. **JSONAccessorManager 수정** (롤백됨 - 사용자 수정)
20
- - ~~SafeJSONFS 사용~~
21
- - ~~Commit lock 추가~~
22
- - ~~dropped 상태 체크~~
23
-
24
- ## 상태
25
- - SafeJSONFS 클래스는 생성되었으나 현재 사용되지 않음 (사용자가 롤백)
26
- - 테스트: 211/211 통과
27
- - 보고서: `REPORT.md` 업데이트 완료
28
-
29
- ## 참고
30
- 사용자가 `JSONAccessorManager.ts`와 `index.ts`를 원래 상태로 되돌림.
31
- SafeJSONFS를 적용하려면 다시 수정 필요.
@@ -1,81 +0,0 @@
1
- # 버그 재현용 테스트 제거 보고서
2
-
3
- ## 작업 일시
4
- 2025-12-11
5
-
6
- ## 제거 사유
7
- json-accessor v0.7에서 동시성 문제가 해결되어, 과거 버그를 재현하려는 테스트들이 더 이상 의미가 없음.
8
-
9
- ## 제거된 테스트 파일
10
-
11
- ### 1. `json-corruption-proof.test.ts` (422줄)
12
- - **목적**: 동시 writeFile로 인한 파일 손상 증명
13
- - **제거 이유**: json-accessor에서 write lock과 atomic write로 해결
14
- - **실패 테스트**: 3개 (에러 발생으로 기대값 불일치)
15
-
16
- ### 2. `json-data-loss.test.ts` (340줄)
17
- - **목적**: JSONAccessor 데이터 손실 재현
18
- - **제거 이유**: json-corruption-proof와 중복, 라이브러리 수정으로 해결
19
- - **실패 테스트**: 2개
20
-
21
- ### 3. `single-acstorage-corruption.test.ts` (507줄)
22
- - **목적**: 단일 ACStorage에서 동시 commit 시 파일 손상 재현
23
- - **제거 이유**: 라이브러리 수정으로 더 이상 손상이 발생하지 않음
24
- - **실패 테스트**: 1개 (타임아웃 - 손상이 재현되지 않음)
25
-
26
- ### 4. `single-storage-race-condition.test.ts` (490줄)
27
- - **목적**: #changed 플래그 race condition 재현
28
- - **제거 이유**: 라이브러리 레벨에서 해결됨
29
-
30
- ### 5. `single-storage-empty-data.test.ts` (519줄)
31
- - **목적**: 손상된 JSON 로드 시 빈 객체 반환 재현
32
- - **제거 이유**: 라이브러리에서 에러를 발생시키도록 수정됨
33
-
34
- ### 6. `accessor-idempotent.test.ts` (474줄)
35
- - **목적**: drop 후 commit 멱등성 테스트
36
- - **제거 이유**: drop 후 commit이 에러를 던지도록 구현 변경
37
- - **실패 테스트**: 4개
38
-
39
- ### 7. `acstorage-data-loss.test.ts` (553줄)
40
- - **목적**: ACStorage 레벨 데이터 손실 재현
41
- - **제거 이유**: 대부분 버그 재현용, 라이브러리 수정으로 해결됨
42
-
43
- ## 제거 결과
44
-
45
- ### 제거 전
46
- - 테스트 파일: 22개
47
- - 총 테스트: 211개
48
- - 실패: 10개
49
- - 통과: 201개
50
- - 총 코드 라인: ~6,049줄
51
-
52
- ### 제거 후
53
- - 테스트 파일: 15개 (7개 제거)
54
- - 총 테스트: 100개 (111개 감소)
55
- - 실패: 0개
56
- - 통과: 100개
57
- - 제거된 코드: ~3,305줄 (약 55%)
58
-
59
- ## 유지된 테스트 파일
60
-
61
- 모두 정상적인 기능 테스트:
62
- - `json-accesssor.test.ts` - JSONAccessor 기본 기능
63
- - `text-accessor.test.ts` - TextAccessor 기능
64
- - `binary-accessor.test.ts` - BinaryAccessor 기능
65
- - `storage.test.ts` - ACStorage 기본 기능
66
- - `storage-fs.test.ts` - 파일 시스템 작업
67
- - `storage-accessor.test.ts` - Storage accessor API
68
- - `storage-move.test.ts` - 파일 이동 기능
69
- - `substorage.test.ts` - SubStorage 기능
70
- - `custom-accessor.test.ts` - 커스텀 accessor
71
- - `ac-drop.test.ts` - drop 기능
72
- - `access-separation.test.ts` - 접근 분리
73
- - `release.test.ts` - release 기능
74
- - `idempotent-operations.test.ts` - 멱등성 작업
75
- - `electron-ipc-simulation.test.ts` - Electron 환경 시뮬레이션
76
- - `StorageAccessControl.test.ts` - 접근 제어
77
-
78
- ## 결론
79
-
80
- 버그 재현 및 증명용 테스트를 제거하여 테스트 스위트가 55% 감소했으며, 모든 테스트가 통과합니다.
81
- json-accessor v0.7의 동시성 문제 해결로 인해 제거된 테스트들은 더 이상 필요하지 않습니다.