ac-storage 0.14.0 → 0.15.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.
- package/AMBIGUOUS_TESTS_REPORT.md +57 -0
- package/CLAUDE.md +415 -0
- package/COMMENTS_IMPROVEMENT_REPORT.md +259 -0
- package/COMPLETE_TEST_CLEANUP_SUMMARY.md +217 -0
- package/FINAL_TEST_CLEANUP_REPORT.md +116 -0
- package/INCONSISTENT_TESTS_REPORT.md +165 -0
- package/README.md +172 -32
- package/REPORT.md +178 -0
- package/REPORT_2.md +31 -0
- package/TEST_CLEANUP_REPORT.md +81 -0
- package/TEST_COMMENTS_REVIEW.md +283 -0
- package/TEST_REFACTORING_REPORT.md +209 -0
- package/TODO.md +167 -0
- package/_TESTPATH/access-separation-test/.acstorage +5 -0
- package/_TESTPATH/data-corruption-investigation/data/config.json +4 -0
- package/_TESTPATH/idempotent-test/.acstorage +4 -0
- package/_TESTPATH/idempotent-test/test.json +4 -0
- package/_TESTPATH/invalid-operation-order-test/.acstorage +3 -0
- package/_TESTPATH/release-test/.acstorage +6 -0
- package/_TESTPATH/release-test/dir/file4.json +5 -0
- package/_TESTPATH/release-test/dir/file5.txt +1 -0
- package/_TESTPATH/release-test/file1.json +5 -0
- package/_TESTPATH/release-test/file2.txt +1 -0
- package/_TESTPATH/single-acstorage-corruption/config.json +1263 -0
- package/dist/bundle.cjs +351 -147
- package/dist/bundle.cjs.map +1 -1
- package/dist/bundle.mjs +350 -146
- package/dist/bundle.mjs.map +1 -1
- package/dist/index.d.ts +38 -7
- package/package.json +45 -45
|
@@ -0,0 +1,165 @@
|
|
|
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/README.md
CHANGED
|
@@ -1,32 +1,172 @@
|
|
|
1
|
-
# AC-Storage
|
|
2
|
-
|
|
3
|
-
## Install
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
npm install ac-storage
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
## Example
|
|
10
|
-
|
|
11
|
-
```ts
|
|
12
|
-
import { ACStorage, StorageAccess } from 'ac-storage';
|
|
13
|
-
|
|
14
|
-
const storage = new ACStorage('./store');
|
|
15
|
-
storage.register({
|
|
16
|
-
'auth' : {
|
|
17
|
-
'default.json' : StorageAccess.JSON(),
|
|
18
|
-
},
|
|
19
|
-
'cache' : {
|
|
20
|
-
'last_access.txt' : StorageAccess.Text(),
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
const authAC = await storage.accessAsJSON('auth:default.json');
|
|
25
|
-
authAC.setOne('id', 'user');
|
|
26
|
-
|
|
27
|
-
const lastAccessAC = await storage.accessAsText('cache:last_access.txt');
|
|
28
|
-
lastAccessAC.write('20250607');
|
|
29
|
-
|
|
30
|
-
await storage.commit();
|
|
31
|
-
```
|
|
32
|
-
|
|
1
|
+
# AC-Storage
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install ac-storage
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Example
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { ACStorage, StorageAccess } from 'ac-storage';
|
|
13
|
+
|
|
14
|
+
const storage = new ACStorage('./store');
|
|
15
|
+
storage.register({
|
|
16
|
+
'auth' : {
|
|
17
|
+
'default.json' : StorageAccess.JSON(),
|
|
18
|
+
},
|
|
19
|
+
'cache' : {
|
|
20
|
+
'last_access.txt' : StorageAccess.Text(),
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const authAC = await storage.accessAsJSON('auth:default.json');
|
|
25
|
+
authAC.setOne('id', 'user');
|
|
26
|
+
|
|
27
|
+
const lastAccessAC = await storage.accessAsText('cache:last_access.txt');
|
|
28
|
+
lastAccessAC.write('20250607');
|
|
29
|
+
|
|
30
|
+
await storage.commit();
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## API
|
|
34
|
+
|
|
35
|
+
### ACStorage
|
|
36
|
+
|
|
37
|
+
Storage instance that manages file access with access control.
|
|
38
|
+
|
|
39
|
+
#### Constructor
|
|
40
|
+
```ts
|
|
41
|
+
new ACStorage(basePath: string, options?: {
|
|
42
|
+
cacheName?: string;
|
|
43
|
+
noCache?: boolean;
|
|
44
|
+
})
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
#### Methods
|
|
48
|
+
|
|
49
|
+
**`register(tree: AccessTree): void`**
|
|
50
|
+
Register file access permissions. Must be called before accessing files.
|
|
51
|
+
|
|
52
|
+
### File Access Methods
|
|
53
|
+
|
|
54
|
+
**`create(identifier: string, accessType: string): Promise<Accessor>`**
|
|
55
|
+
Create a new file. **Throws error if file already exists.**
|
|
56
|
+
|
|
57
|
+
**`createAsJSON(identifier: string): Promise<IJSONAccessor>`**
|
|
58
|
+
Create a new JSON file.
|
|
59
|
+
|
|
60
|
+
**`createAsText(identifier: string): Promise<ITextAccessor>`**
|
|
61
|
+
Create a new text file.
|
|
62
|
+
|
|
63
|
+
**`createAsBinary(identifier: string): Promise<IBinaryAccessor>`**
|
|
64
|
+
Create a new binary file.
|
|
65
|
+
|
|
66
|
+
**`open(identifier: string, accessType: string): Promise<Accessor>`**
|
|
67
|
+
Open an existing file. **Throws error if file does not exist.**
|
|
68
|
+
|
|
69
|
+
**`openAsJSON(identifier: string): Promise<IJSONAccessor>`**
|
|
70
|
+
Open an existing JSON file.
|
|
71
|
+
|
|
72
|
+
**`openAsText(identifier: string): Promise<ITextAccessor>`**
|
|
73
|
+
Open an existing text file.
|
|
74
|
+
|
|
75
|
+
**`openAsBinary(identifier: string): Promise<IBinaryAccessor>`**
|
|
76
|
+
Open an existing binary file.
|
|
77
|
+
|
|
78
|
+
**`access(identifier: string, accessType: string): Promise<Accessor>`**
|
|
79
|
+
Access a file - creates if missing, opens if exists (default behavior).
|
|
80
|
+
|
|
81
|
+
**`accessAsJSON(identifier: string): Promise<IJSONAccessor>`**
|
|
82
|
+
Access a JSON file.
|
|
83
|
+
|
|
84
|
+
**`accessAsText(identifier: string): Promise<ITextAccessor>`**
|
|
85
|
+
Access a text file.
|
|
86
|
+
|
|
87
|
+
**`accessAsBinary(identifier: string): Promise<IBinaryAccessor>`**
|
|
88
|
+
Access a binary file.
|
|
89
|
+
|
|
90
|
+
#### Access Methods Comparison
|
|
91
|
+
|
|
92
|
+
| Method | Creates if Missing | Loads if Exists | Error if Missing | Error if Exists |
|
|
93
|
+
|--------|-------------------|-----------------|------------------|-----------------|
|
|
94
|
+
| `create()` | ✅ | ❌ | N/A | ✅ |
|
|
95
|
+
| `open()` | ❌ | ✅ | ✅ | N/A |
|
|
96
|
+
| `access()` | ✅ | ✅ | ❌ | ❌ |
|
|
97
|
+
|
|
98
|
+
**Example:**
|
|
99
|
+
```typescript
|
|
100
|
+
// Create only - throws if file already exists
|
|
101
|
+
const config = await storage.createAsJSON('config.json');
|
|
102
|
+
|
|
103
|
+
// Open only - throws if file doesn't exist
|
|
104
|
+
const existing = await storage.openAsJSON('config.json');
|
|
105
|
+
|
|
106
|
+
// Access - creates if missing, loads if exists
|
|
107
|
+
const flexible = await storage.accessAsJSON('config.json');
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### File Operations
|
|
111
|
+
|
|
112
|
+
**`copy(oldIdentifier: string, newIdentifier: string): Promise<void>`**
|
|
113
|
+
Copy a file from old identifier to new identifier.
|
|
114
|
+
|
|
115
|
+
**`move(oldIdentifier: string, newIdentifier: string): Promise<void>`**
|
|
116
|
+
Move a file from old identifier to new identifier.
|
|
117
|
+
|
|
118
|
+
### Memory Management
|
|
119
|
+
|
|
120
|
+
**`drop(identifier: string): Promise<void>`**
|
|
121
|
+
Delete and unload a specific file from memory. **Does not commit changes.**
|
|
122
|
+
|
|
123
|
+
**`dropDir(identifier: string): Promise<void>`**
|
|
124
|
+
Delete and unload all files under the specified directory. **Does not commit changes.**
|
|
125
|
+
|
|
126
|
+
**`dropAll(): Promise<void>`**
|
|
127
|
+
Delete and unload all files. **Does not commit changes.**
|
|
128
|
+
|
|
129
|
+
**`release(identifier: string): Promise<void>`**
|
|
130
|
+
Commit changes and unload a file from memory. **File remains on disk.**
|
|
131
|
+
|
|
132
|
+
**`releaseDir(identifier: string): Promise<void>`**
|
|
133
|
+
Commit and unload all files under the specified directory. **Files remain on disk.**
|
|
134
|
+
|
|
135
|
+
**`releaseAll(): Promise<void>`**
|
|
136
|
+
Commit and unload all files from memory. **Files remain on disk.**
|
|
137
|
+
|
|
138
|
+
**`commit(identifier?: string): Promise<void>`**
|
|
139
|
+
Commit changes to the filesystem. If identifier is provided, commits that file and its dependencies.
|
|
140
|
+
|
|
141
|
+
**`commitAll(): Promise<void>`**
|
|
142
|
+
Commit all changes to the filesystem.
|
|
143
|
+
|
|
144
|
+
### Operations Comparison
|
|
145
|
+
|
|
146
|
+
| Operation | Commits Changes | Removes from Memory | Deletes File |
|
|
147
|
+
|-----------|----------------|---------------------|--------------|
|
|
148
|
+
| `commit()` | ✅ | ❌ | ❌ |
|
|
149
|
+
| `release()` | ✅ | ✅ | ❌ |
|
|
150
|
+
| `drop()` | ❌ | ✅ | ✅ |
|
|
151
|
+
|
|
152
|
+
**Example:**
|
|
153
|
+
```typescript
|
|
154
|
+
// Commit only - save changes but keep in memory
|
|
155
|
+
await storage.commit('config.json');
|
|
156
|
+
|
|
157
|
+
// Release - save and unload from memory (file persists)
|
|
158
|
+
await storage.release('config.json');
|
|
159
|
+
|
|
160
|
+
// Drop - delete file and unload (changes lost)
|
|
161
|
+
await storage.drop('temp.json');
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**`subStorage(identifier: string): IACSubStorage`**
|
|
165
|
+
Create a sub-storage scoped to a specific directory prefix.
|
|
166
|
+
|
|
167
|
+
**`addAccessEvent(customId: string, event: AccessorEvent): void`**
|
|
168
|
+
Register a custom accessor type for handling custom file formats.
|
|
169
|
+
|
|
170
|
+
**`addListener(event: 'access' | 'destroy', listener: Function): void`**
|
|
171
|
+
Add event listeners for file access and destroy events.
|
|
172
|
+
|
package/REPORT.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
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를 적용하려면 다시 수정 필요.
|
|
@@ -0,0 +1,81 @@
|
|
|
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의 동시성 문제 해결로 인해 제거된 테스트들은 더 이상 필요하지 않습니다.
|