@nahisaho/katashiro-workspace 0.4.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.
@@ -0,0 +1,283 @@
1
+ /**
2
+ * LocalWorkspace テスト
3
+ *
4
+ * @requirement REQ-011-02
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
8
+ import * as fs from 'node:fs/promises';
9
+ import * as path from 'node:path';
10
+ import * as os from 'node:os';
11
+ import { LocalWorkspace } from '../src/local-workspace.js';
12
+ import { WorkspaceError } from '../src/types.js';
13
+
14
+ describe('LocalWorkspace', () => {
15
+ let tempDir: string;
16
+ let workspace: LocalWorkspace;
17
+
18
+ beforeEach(async () => {
19
+ // テスト用一時ディレクトリを作成
20
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'katashiro-workspace-test-'));
21
+ workspace = new LocalWorkspace({
22
+ type: 'local',
23
+ workingDir: tempDir,
24
+ });
25
+ });
26
+
27
+ afterEach(async () => {
28
+ // 一時ディレクトリを削除
29
+ await fs.rm(tempDir, { recursive: true, force: true });
30
+ });
31
+
32
+ describe('constructor', () => {
33
+ it('should initialize with correct type', () => {
34
+ expect(workspace.type).toBe('local');
35
+ expect(workspace.workingDir).toBe(tempDir);
36
+ expect(workspace.readOnly).toBe(false);
37
+ });
38
+
39
+ it('should support read-only mode', () => {
40
+ const roWorkspace = new LocalWorkspace({
41
+ type: 'local',
42
+ workingDir: tempDir,
43
+ readOnly: true,
44
+ });
45
+ expect(roWorkspace.readOnly).toBe(true);
46
+ });
47
+ });
48
+
49
+ describe('read/write', () => {
50
+ it('should write and read a file', async () => {
51
+ const content = 'Hello, World!';
52
+ await workspace.write('test.txt', content);
53
+
54
+ const result = await workspace.read('test.txt');
55
+ expect(result).toBe(content);
56
+ });
57
+
58
+ it('should write and read UTF-8 content', async () => {
59
+ const content = 'こんにちは世界 🌍';
60
+ await workspace.write('unicode.txt', content);
61
+
62
+ const result = await workspace.read('unicode.txt');
63
+ expect(result).toBe(content);
64
+ });
65
+
66
+ it('should create nested directories automatically', async () => {
67
+ await workspace.write('deep/nested/file.txt', 'nested content');
68
+
69
+ const result = await workspace.read('deep/nested/file.txt');
70
+ expect(result).toBe('nested content');
71
+ });
72
+
73
+ it('should throw NOT_FOUND for non-existent file', async () => {
74
+ await expect(workspace.read('nonexistent.txt')).rejects.toThrow(WorkspaceError);
75
+ try {
76
+ await workspace.read('nonexistent.txt');
77
+ } catch (e) {
78
+ expect((e as WorkspaceError).code).toBe('NOT_FOUND');
79
+ }
80
+ });
81
+
82
+ it('should throw READ_ONLY when writing in read-only mode', async () => {
83
+ const roWorkspace = new LocalWorkspace({
84
+ type: 'local',
85
+ workingDir: tempDir,
86
+ readOnly: true,
87
+ });
88
+
89
+ await expect(roWorkspace.write('test.txt', 'content')).rejects.toThrow(WorkspaceError);
90
+ try {
91
+ await roWorkspace.write('test.txt', 'content');
92
+ } catch (e) {
93
+ expect((e as WorkspaceError).code).toBe('READ_ONLY');
94
+ }
95
+ });
96
+ });
97
+
98
+ describe('readBuffer/writeBuffer', () => {
99
+ it('should write and read binary data', async () => {
100
+ const buffer = Buffer.from([0x00, 0x01, 0x02, 0xff]);
101
+ await workspace.writeBuffer('binary.bin', buffer);
102
+
103
+ const result = await workspace.readBuffer('binary.bin');
104
+ expect(result).toEqual(buffer);
105
+ });
106
+ });
107
+
108
+ describe('list', () => {
109
+ beforeEach(async () => {
110
+ await workspace.write('file1.txt', 'content1');
111
+ await workspace.write('file2.txt', 'content2');
112
+ await workspace.mkdir('subdir');
113
+ await workspace.write('subdir/file3.txt', 'content3');
114
+ });
115
+
116
+ it('should list directory contents', async () => {
117
+ const files = await workspace.list('.');
118
+
119
+ expect(files).toHaveLength(3);
120
+ const names = files.map(f => f.name);
121
+ expect(names).toContain('file1.txt');
122
+ expect(names).toContain('file2.txt');
123
+ expect(names).toContain('subdir');
124
+ });
125
+
126
+ it('should include file info', async () => {
127
+ const files = await workspace.list('.');
128
+ const file1 = files.find(f => f.name === 'file1.txt');
129
+
130
+ expect(file1).toBeDefined();
131
+ expect(file1!.isDirectory).toBe(false);
132
+ expect(file1!.size).toBe(8); // 'content1'.length
133
+ });
134
+
135
+ it('should identify directories', async () => {
136
+ const files = await workspace.list('.');
137
+ const subdir = files.find(f => f.name === 'subdir');
138
+
139
+ expect(subdir).toBeDefined();
140
+ expect(subdir!.isDirectory).toBe(true);
141
+ });
142
+ });
143
+
144
+ describe('listEntries', () => {
145
+ beforeEach(async () => {
146
+ await workspace.write('file.txt', 'content');
147
+ await workspace.mkdir('dir');
148
+ });
149
+
150
+ it('should return simple directory entries', async () => {
151
+ const entries = await workspace.listEntries('.');
152
+
153
+ expect(entries).toHaveLength(2);
154
+ expect(entries.find(e => e.name === 'file.txt')?.isDirectory).toBe(false);
155
+ expect(entries.find(e => e.name === 'dir')?.isDirectory).toBe(true);
156
+ });
157
+ });
158
+
159
+ describe('search', () => {
160
+ beforeEach(async () => {
161
+ await workspace.write('src/index.ts', 'export {};');
162
+ await workspace.write('src/utils.ts', 'export {};');
163
+ await workspace.write('tests/index.test.ts', 'test();');
164
+ await workspace.write('README.md', '# README');
165
+ });
166
+
167
+ it('should find files matching glob pattern', async () => {
168
+ const files = await workspace.search('**/*.ts');
169
+
170
+ expect(files).toHaveLength(3);
171
+ expect(files).toContain('src/index.ts');
172
+ expect(files).toContain('src/utils.ts');
173
+ expect(files).toContain('tests/index.test.ts');
174
+ });
175
+
176
+ it('should find files with specific extension', async () => {
177
+ const files = await workspace.search('**/*.md');
178
+
179
+ expect(files).toHaveLength(1);
180
+ expect(files).toContain('README.md');
181
+ });
182
+ });
183
+
184
+ describe('exists', () => {
185
+ beforeEach(async () => {
186
+ await workspace.write('exists.txt', 'content');
187
+ });
188
+
189
+ it('should return true for existing file', async () => {
190
+ const result = await workspace.exists('exists.txt');
191
+ expect(result).toBe(true);
192
+ });
193
+
194
+ it('should return false for non-existing file', async () => {
195
+ const result = await workspace.exists('notexists.txt');
196
+ expect(result).toBe(false);
197
+ });
198
+ });
199
+
200
+ describe('delete', () => {
201
+ it('should delete a file', async () => {
202
+ await workspace.write('todelete.txt', 'content');
203
+ expect(await workspace.exists('todelete.txt')).toBe(true);
204
+
205
+ await workspace.delete('todelete.txt');
206
+ expect(await workspace.exists('todelete.txt')).toBe(false);
207
+ });
208
+
209
+ it('should delete a directory recursively', async () => {
210
+ await workspace.write('dir/subdir/file.txt', 'content');
211
+ expect(await workspace.exists('dir/subdir/file.txt')).toBe(true);
212
+
213
+ await workspace.delete('dir');
214
+ expect(await workspace.exists('dir')).toBe(false);
215
+ });
216
+ });
217
+
218
+ describe('mkdir', () => {
219
+ it('should create directory', async () => {
220
+ await workspace.mkdir('newdir');
221
+ expect(await workspace.exists('newdir')).toBe(true);
222
+ });
223
+
224
+ it('should create nested directories', async () => {
225
+ await workspace.mkdir('a/b/c');
226
+ expect(await workspace.exists('a/b/c')).toBe(true);
227
+ });
228
+ });
229
+
230
+ describe('stat', () => {
231
+ beforeEach(async () => {
232
+ await workspace.write('statfile.txt', 'test content');
233
+ });
234
+
235
+ it('should return file info', async () => {
236
+ const info = await workspace.stat('statfile.txt');
237
+
238
+ expect(info.name).toBe('statfile.txt');
239
+ expect(info.size).toBe(12);
240
+ expect(info.isDirectory).toBe(false);
241
+ expect(info.modifiedAt).toBeInstanceOf(Date);
242
+ });
243
+ });
244
+
245
+ describe('copy', () => {
246
+ it('should copy a file', async () => {
247
+ await workspace.write('original.txt', 'content');
248
+ await workspace.copy('original.txt', 'copied.txt');
249
+
250
+ const content = await workspace.read('copied.txt');
251
+ expect(content).toBe('content');
252
+ });
253
+
254
+ it('should copy a directory', async () => {
255
+ await workspace.write('dir/file.txt', 'content');
256
+ await workspace.copy('dir', 'copied-dir');
257
+
258
+ const content = await workspace.read('copied-dir/file.txt');
259
+ expect(content).toBe('content');
260
+ });
261
+ });
262
+
263
+ describe('move', () => {
264
+ it('should move a file', async () => {
265
+ await workspace.write('tomove.txt', 'content');
266
+ await workspace.move('tomove.txt', 'moved.txt');
267
+
268
+ expect(await workspace.exists('tomove.txt')).toBe(false);
269
+ expect(await workspace.read('moved.txt')).toBe('content');
270
+ });
271
+ });
272
+
273
+ describe('path security', () => {
274
+ it('should prevent path traversal', async () => {
275
+ await expect(workspace.read('../../../etc/passwd')).rejects.toThrow(WorkspaceError);
276
+ try {
277
+ await workspace.read('../../../etc/passwd');
278
+ } catch (e) {
279
+ expect((e as WorkspaceError).code).toBe('PERMISSION_DENIED');
280
+ }
281
+ });
282
+ });
283
+ });
@@ -0,0 +1,120 @@
1
+ /**
2
+ * WorkspaceFactory テスト
3
+ *
4
+ * @requirement REQ-011-05
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
8
+ import * as fs from 'node:fs/promises';
9
+ import * as path from 'node:path';
10
+ import * as os from 'node:os';
11
+ import { WorkspaceFactory, createWorkspace, readFile, writeFile } from '../src/workspace-factory.js';
12
+ import { LocalWorkspace } from '../src/local-workspace.js';
13
+ import { WorkspaceError } from '../src/types.js';
14
+
15
+ describe('WorkspaceFactory', () => {
16
+ let tempDir: string;
17
+
18
+ beforeEach(async () => {
19
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'katashiro-workspace-factory-test-'));
20
+ });
21
+
22
+ afterEach(async () => {
23
+ await fs.rm(tempDir, { recursive: true, force: true });
24
+ });
25
+
26
+ describe('create', () => {
27
+ it('should create LocalWorkspace', () => {
28
+ const workspace = WorkspaceFactory.create({
29
+ type: 'local',
30
+ workingDir: tempDir,
31
+ });
32
+
33
+ expect(workspace).toBeInstanceOf(LocalWorkspace);
34
+ expect(workspace.type).toBe('local');
35
+ });
36
+
37
+ it('should throw for remote workspace (not implemented)', () => {
38
+ expect(() =>
39
+ WorkspaceFactory.create({
40
+ type: 'remote',
41
+ workingDir: '/app',
42
+ apiUrl: 'http://localhost:3000',
43
+ })
44
+ ).toThrow(WorkspaceError);
45
+ });
46
+ });
47
+
48
+ describe('createLocal', () => {
49
+ it('should create LocalWorkspace with options', () => {
50
+ const workspace = WorkspaceFactory.createLocal(tempDir, { readOnly: true });
51
+
52
+ expect(workspace.type).toBe('local');
53
+ expect(workspace.readOnly).toBe(true);
54
+ });
55
+ });
56
+
57
+ describe('createWorkspace', () => {
58
+ it('should create and initialize workspace', async () => {
59
+ const workspace = await createWorkspace({
60
+ type: 'local',
61
+ workingDir: tempDir,
62
+ });
63
+
64
+ expect(workspace.type).toBe('local');
65
+ });
66
+ });
67
+
68
+ describe('readFile', () => {
69
+ it('should read file using config', async () => {
70
+ await fs.writeFile(path.join(tempDir, 'test.txt'), 'hello');
71
+
72
+ const content = await readFile(
73
+ { type: 'local', workingDir: tempDir },
74
+ 'test.txt'
75
+ );
76
+
77
+ expect(content).toBe('hello');
78
+ });
79
+ });
80
+
81
+ describe('writeFile', () => {
82
+ it('should write file using config', async () => {
83
+ await writeFile(
84
+ { type: 'local', workingDir: tempDir },
85
+ 'output.txt',
86
+ 'written content'
87
+ );
88
+
89
+ const content = await fs.readFile(path.join(tempDir, 'output.txt'), 'utf-8');
90
+ expect(content).toBe('written content');
91
+ });
92
+ });
93
+
94
+ describe('unified interface (REQ-011-05)', () => {
95
+ it('should allow tools to work identically regardless of workspace type', async () => {
96
+ // 統一インターフェースでツールを定義
97
+ async function readConfigTool(workspace: ReturnType<typeof WorkspaceFactory.create>): Promise<string> {
98
+ return workspace.read('config.json');
99
+ }
100
+
101
+ async function writeResultTool(
102
+ workspace: ReturnType<typeof WorkspaceFactory.create>,
103
+ result: string
104
+ ): Promise<void> {
105
+ await workspace.write('result.json', result);
106
+ }
107
+
108
+ // LocalWorkspaceでテスト
109
+ const localWorkspace = WorkspaceFactory.createLocal(tempDir);
110
+ await localWorkspace.write('config.json', '{"key": "value"}');
111
+
112
+ const config = await readConfigTool(localWorkspace);
113
+ expect(config).toBe('{"key": "value"}');
114
+
115
+ await writeResultTool(localWorkspace, '{"status": "ok"}');
116
+ const result = await localWorkspace.read('result.json');
117
+ expect(result).toBe('{"status": "ok"}');
118
+ });
119
+ });
120
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "outDir": "./dist",
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "esModuleInterop": true
9
+ },
10
+ "include": ["src/**/*"],
11
+ "exclude": ["node_modules", "dist", "tests"]
12
+ }