@chen-rmag/core-infra 1.0.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.
Files changed (71) hide show
  1. package/README.md +46 -0
  2. package/dist/ProjectContextManager.d.ts +30 -0
  3. package/dist/ProjectContextManager.js +41 -0
  4. package/dist/directory-validator.d.ts +28 -0
  5. package/dist/directory-validator.js +90 -0
  6. package/dist/index.d.ts +14 -0
  7. package/dist/index.js +44 -0
  8. package/dist/mcp/file-mcp-manager.d.ts +13 -0
  9. package/dist/mcp/file-mcp-manager.js +45 -0
  10. package/dist/mcp/index.d.ts +20 -0
  11. package/dist/mcp/index.js +16 -0
  12. package/dist/mcp/mcp-client.d.ts +127 -0
  13. package/dist/mcp/mcp-client.js +165 -0
  14. package/dist/mcp/mcp-manager.d.ts +20 -0
  15. package/dist/mcp/mcp-manager.js +98 -0
  16. package/dist/mcp/playwright-mcp-manager.d.ts +18 -0
  17. package/dist/mcp/playwright-mcp-manager.js +115 -0
  18. package/dist/model.d.ts +10 -0
  19. package/dist/model.js +207 -0
  20. package/dist/repositories/BaseRepository.d.ts +68 -0
  21. package/dist/repositories/BaseRepository.js +212 -0
  22. package/dist/repositories/DirectoryRepository.d.ts +69 -0
  23. package/dist/repositories/DirectoryRepository.js +335 -0
  24. package/dist/repositories/ExplorationRepository.d.ts +33 -0
  25. package/dist/repositories/ExplorationRepository.js +53 -0
  26. package/dist/repositories/FileRepository.d.ts +55 -0
  27. package/dist/repositories/FileRepository.js +131 -0
  28. package/dist/repositories/ModelConfigRepository.d.ts +33 -0
  29. package/dist/repositories/ModelConfigRepository.js +51 -0
  30. package/dist/repositories/ProjectRepository.d.ts +31 -0
  31. package/dist/repositories/ProjectRepository.js +66 -0
  32. package/dist/repositories/SettingsRepository.d.ts +18 -0
  33. package/dist/repositories/SettingsRepository.js +71 -0
  34. package/dist/repositories/TableDataRepository.d.ts +21 -0
  35. package/dist/repositories/TableDataRepository.js +32 -0
  36. package/dist/repositories/TestCaseRepository.d.ts +120 -0
  37. package/dist/repositories/TestCaseRepository.js +463 -0
  38. package/dist/repositories/TestPlanRepository.d.ts +34 -0
  39. package/dist/repositories/TestPlanRepository.js +79 -0
  40. package/dist/repositories/TestResultRepository.d.ts +29 -0
  41. package/dist/repositories/TestResultRepository.js +53 -0
  42. package/dist/repositories/index.d.ts +16 -0
  43. package/dist/repositories/index.js +30 -0
  44. package/dist/storageService.d.ts +129 -0
  45. package/dist/storageService.js +297 -0
  46. package/dist/types.d.ts +217 -0
  47. package/dist/types.js +2 -0
  48. package/package.json +32 -0
  49. package/src/directory-validator.ts +98 -0
  50. package/src/index.ts +26 -0
  51. package/src/mcp/file-mcp-manager.ts +50 -0
  52. package/src/mcp/index.ts +35 -0
  53. package/src/mcp/mcp-client.ts +209 -0
  54. package/src/mcp/mcp-manager.ts +118 -0
  55. package/src/mcp/playwright-mcp-manager.ts +127 -0
  56. package/src/model.ts +234 -0
  57. package/src/repositories/BaseRepository.ts +193 -0
  58. package/src/repositories/DirectoryRepository.ts +393 -0
  59. package/src/repositories/ExplorationRepository.ts +57 -0
  60. package/src/repositories/FileRepository.ts +153 -0
  61. package/src/repositories/ModelConfigRepository.ts +55 -0
  62. package/src/repositories/ProjectRepository.ts +70 -0
  63. package/src/repositories/SettingsRepository.ts +38 -0
  64. package/src/repositories/TableDataRepository.ts +33 -0
  65. package/src/repositories/TestCaseRepository.ts +521 -0
  66. package/src/repositories/TestPlanRepository.ts +89 -0
  67. package/src/repositories/TestResultRepository.ts +56 -0
  68. package/src/repositories/index.ts +17 -0
  69. package/src/storageService.ts +404 -0
  70. package/src/types.ts +246 -0
  71. package/tsconfig.json +19 -0
@@ -0,0 +1,393 @@
1
+ import { BaseRepository } from './BaseRepository';
2
+ import { DirectoryInfo, TestDirectory, TestCase } from '../types';
3
+ import { join, dirname } from 'path';
4
+ import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync, readdirSync, renameSync, statSync } from 'fs';
5
+ import { validateDirectoryName, validateDirectoryPath } from '../directory-validator';
6
+
7
+ /**
8
+ * DirectoryRepository handles all storage operations for test case directories
9
+ * Manages the hierarchical organization of test cases (up to 3 levels)
10
+ * Each directory can have an info.json file with metadata (description, createdAt, etc.)
11
+ */
12
+ export class DirectoryRepository extends BaseRepository {
13
+ constructor(projectId: string) {
14
+ super(join("projects", projectId, 'tests'));
15
+ }
16
+
17
+ /**
18
+ * Create a new directory at the specified path
19
+ * @param path - Directory path like 'folder1' or 'folder1/subfolder' (max 3 levels)
20
+ * @param info - Directory metadata
21
+ */
22
+ createDirectory(path: string, info: Omit<DirectoryInfo, 'id' | 'path'>): DirectoryInfo {
23
+ // Validate path
24
+ const validation = validateDirectoryPath(path);
25
+ if (!validation.valid) {
26
+ throw new Error(validation.error);
27
+ }
28
+
29
+ const fullPath = join(this.getBaseDir(), path);
30
+
31
+ // Check if directory already exists
32
+ if (existsSync(fullPath)) {
33
+ throw new Error(`Directory already exists: ${path}`);
34
+ }
35
+
36
+ // Create directory
37
+ mkdirSync(fullPath, { recursive: true });
38
+
39
+ // Create directory info
40
+ const directoryInfo: DirectoryInfo = {
41
+ id: `dir_${Date.now()}`,
42
+ name: path.split('/').pop() || path,
43
+ description: info.description,
44
+ createdAt: Date.now(),
45
+ updatedAt: Date.now(),
46
+ };
47
+
48
+ // Save info.json
49
+ this.saveDirectoryInfo(path, directoryInfo);
50
+
51
+ return directoryInfo;
52
+ }
53
+
54
+ /**
55
+ * Get directory info from info.json
56
+ */
57
+ getDirectoryInfo(path: string): DirectoryInfo | null {
58
+ const infoPath = join(this.getBaseDir(), path, 'info.json');
59
+ if (!existsSync(infoPath)) {
60
+ return null;
61
+ }
62
+
63
+ try {
64
+ const content = readFileSync(infoPath, 'utf-8');
65
+ return JSON.parse(content) as DirectoryInfo;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Update directory metadata
73
+ */
74
+ updateDirectoryInfo(path: string, updates: Partial<Omit<DirectoryInfo, 'id' | 'path'>>): DirectoryInfo {
75
+ const existing = this.getDirectoryInfo(path);
76
+ if (!existing) {
77
+ throw new Error(`Directory does not exist: ${path}`);
78
+ }
79
+
80
+ const updated: DirectoryInfo = {
81
+ ...existing,
82
+ ...updates,
83
+ updatedAt: Date.now(),
84
+ };
85
+
86
+ this.saveDirectoryInfo(path, updated);
87
+ return updated;
88
+ }
89
+
90
+ /**
91
+ * Delete a directory (must be empty)
92
+ */
93
+ deleteDirectory(path: string): void {
94
+ const fullPath = join(this.getBaseDir(), path);
95
+
96
+ if (!existsSync(fullPath)) {
97
+ throw new Error(`Directory does not exist: ${path}`);
98
+ }
99
+
100
+ // Delete the directory
101
+ rmSync(fullPath, { recursive: true, force: true });
102
+ }
103
+
104
+ /**
105
+ * Rename a directory
106
+ */
107
+ renameDirectory(oldPath: string, newName: string): DirectoryInfo {
108
+ // Validate new name
109
+ const nameValidation = validateDirectoryName(newName);
110
+ if (!nameValidation.valid) {
111
+ throw new Error(nameValidation.error);
112
+ }
113
+
114
+ const oldFullPath = join(this.getBaseDir(), oldPath);
115
+ const parentPath = oldPath.split('/').slice(0, -1).join('/');
116
+ const newPath = parentPath ? `${parentPath}/${newName}` : newName;
117
+ const newFullPath = join(this.getBaseDir(), newPath);
118
+
119
+ if (!existsSync(oldFullPath)) {
120
+ throw new Error(`Directory does not exist: ${oldPath}`);
121
+ }
122
+
123
+ if (existsSync(newFullPath)) {
124
+ throw new Error(`Directory already exists: ${newPath}`);
125
+ }
126
+
127
+ // Rename directory
128
+ renameSync(oldFullPath, newFullPath);
129
+
130
+ // Update all test cases in the renamed directory and its subdirectories
131
+ this.updateTestPathsRecursively(newFullPath, oldPath, newPath);
132
+
133
+ // Update info.json
134
+ const dirInfo = this.getDirectoryInfo(newPath);
135
+ if (dirInfo) {
136
+ dirInfo.name = newName;
137
+ dirInfo.updatedAt = Date.now();
138
+ this.saveDirectoryInfo(newPath, dirInfo);
139
+ }
140
+
141
+ return dirInfo || { id: '', name: newName, createdAt: Date.now(), updatedAt: Date.now() };
142
+ }
143
+
144
+ /**
145
+ * Build the complete directory tree with tests
146
+ * This is the main method to get the hierarchical structure
147
+ */
148
+ buildDirectoryTree(basePath: string = ''): TestDirectory {
149
+ const fullPath = join(this.getBaseDir(), basePath);
150
+
151
+ // Ensure directory exists
152
+ if (!existsSync(fullPath)) {
153
+ mkdirSync(fullPath, { recursive: true });
154
+ }
155
+
156
+ const pathParts = basePath ? basePath.split('/') : [];
157
+ const name = pathParts.length > 0 ? pathParts[pathParts.length - 1] : 'root';
158
+
159
+ const infoData = basePath ? this.getDirectoryInfo(basePath) : null;
160
+ const info = infoData || undefined;
161
+
162
+ const tree: TestDirectory = {
163
+ path: basePath,
164
+ name,
165
+ info,
166
+ children: [],
167
+ tests: [],
168
+ };
169
+
170
+ try {
171
+ const items = readdirSync(fullPath);
172
+
173
+ for (const item of items) {
174
+ const itemPath = join(fullPath, item);
175
+ const stat = statSync(itemPath);
176
+
177
+ if (stat.isDirectory() && item !== 'info.json') {
178
+ const subPath = basePath ? `${basePath}/${item}` : item;
179
+
180
+ // Check depth (max 3 levels)
181
+ if (subPath.split('/').length <= 3) {
182
+ tree.children.push(this.buildDirectoryTree(subPath));
183
+ }
184
+ }
185
+ }
186
+
187
+ // Load test files (only .json files that are test cases)
188
+ for (const item of items) {
189
+ if (item.endsWith('.json') && item !== 'info.json') {
190
+ const testPath = join(fullPath, item);
191
+ try {
192
+ const content = readFileSync(testPath, 'utf-8');
193
+ const testCase = JSON.parse(content) as TestCase;
194
+ // Only add tests that belong to this directory (no path or matching path)
195
+ if (testCase.path === basePath) {
196
+ tree.tests.push(testCase);
197
+ }
198
+ } catch {
199
+ // Skip invalid test files
200
+ }
201
+ }
202
+ }
203
+
204
+ // Sort children by name
205
+ tree.children.sort((a, b) => a.name.localeCompare(b.name));
206
+ // Sort tests by creation date (newest first)
207
+ tree.tests.sort((a, b) => b.createdAt - a.createdAt);
208
+ } catch {
209
+ // Return empty tree if unable to read directory
210
+ }
211
+
212
+ return tree;
213
+ }
214
+
215
+ /**
216
+ * Get all directories at a specific level (flat list)
217
+ */
218
+ getDirectoriesAtPath(path: string = ''): DirectoryInfo[] {
219
+ const fullPath = join(this.getBaseDir(), path);
220
+
221
+ if (!existsSync(fullPath)) {
222
+ return [];
223
+ }
224
+
225
+ const directories: DirectoryInfo[] = [];
226
+
227
+ try {
228
+ const items = readdirSync(fullPath);
229
+
230
+ for (const item of items) {
231
+ const itemPath = join(fullPath, item);
232
+ const stat = statSync(itemPath);
233
+
234
+ if (stat.isDirectory() && item !== 'info.json') {
235
+ const subPath = path ? `${path}/${item}` : item;
236
+ const info = this.getDirectoryInfo(subPath);
237
+
238
+ if (info) {
239
+ directories.push(info);
240
+ }
241
+ }
242
+ }
243
+ } catch {
244
+ // Return empty array if unable to read directory
245
+ }
246
+
247
+ return directories.sort((a, b) => a.name.localeCompare(b.name));
248
+ }
249
+
250
+ /**
251
+ * Check if a path is valid (exists and is a directory)
252
+ */
253
+ isValidDirectory(path: string): boolean {
254
+ const fullPath = join(this.getBaseDir(), path);
255
+ return existsSync(fullPath) && statSync(fullPath).isDirectory();
256
+ }
257
+
258
+ /**
259
+ * Move a directory to a new parent directory
260
+ * @param sourcePath - The current directory path
261
+ * @param targetParentPath - The target parent directory path (or empty string for root)
262
+ */
263
+ moveDirectory(sourcePath: string, targetParentPath: string): DirectoryInfo {
264
+ const sourceFullPath = join(this.getBaseDir(), sourcePath);
265
+ const directoryName = sourcePath.split('/').pop() || sourcePath;
266
+
267
+ // Validate paths
268
+ if (!existsSync(sourceFullPath)) {
269
+ throw new Error(`Source directory does not exist: ${sourcePath}`);
270
+ }
271
+
272
+ const targetPath = targetParentPath ? `${targetParentPath}/${directoryName}` : directoryName;
273
+ const targetFullPath = join(this.getBaseDir(), targetPath);
274
+
275
+ // Check if target already exists
276
+ if (existsSync(targetFullPath)) {
277
+ throw new Error(`Target path already exists: ${targetPath}`);
278
+ }
279
+
280
+ // Validate target path
281
+ const pathValidation = validateDirectoryPath(targetPath);
282
+ if (!pathValidation.valid) {
283
+ throw new Error(pathValidation.error);
284
+ }
285
+
286
+ // Ensure target parent directory exists
287
+ const targetParentFullPath = dirname(targetFullPath);
288
+ if (!existsSync(targetParentFullPath)) {
289
+ mkdirSync(targetParentFullPath, { recursive: true });
290
+ }
291
+
292
+ // Move the directory
293
+ renameSync(sourceFullPath, targetFullPath);
294
+
295
+ // Update all test cases in the moved directory and its subdirectories
296
+ this.updateTestPathsRecursively(targetFullPath, sourcePath, targetPath);
297
+
298
+ // Get the directory info
299
+ const dirInfo = this.getDirectoryInfo(targetPath);
300
+ return dirInfo || { id: '', name: directoryName, createdAt: Date.now(), updatedAt: Date.now() };
301
+ }
302
+
303
+ /**
304
+ * Recursively update the path field in all test case JSON files
305
+ * @param dirFullPath - Full filesystem path to the directory
306
+ * @param oldBasePath - Old logical path (e.g., 'folder1/subfolder')
307
+ * @param newBasePath - New logical path (e.g., 'folder2/subfolder')
308
+ */
309
+ private updateTestPathsRecursively(dirFullPath: string, oldBasePath: string, newBasePath: string): void {
310
+ if (!existsSync(dirFullPath)) {
311
+ return;
312
+ }
313
+
314
+ try {
315
+ const items = readdirSync(dirFullPath);
316
+
317
+ for (const item of items) {
318
+ const itemPath = join(dirFullPath, item);
319
+ const stat = statSync(itemPath);
320
+
321
+ if (stat.isDirectory() && item !== 'info.json') {
322
+ // Recursively update subdirectories
323
+ this.updateTestPathsRecursively(itemPath, oldBasePath, newBasePath);
324
+ } else if (item.endsWith('.json') && item !== 'info.json') {
325
+ // Update test case file
326
+ try {
327
+ const content = readFileSync(itemPath, 'utf-8');
328
+ const testCase = JSON.parse(content) as TestCase;
329
+
330
+ // Update the path field if it starts with the old base path
331
+ if (testCase.path && testCase.path.startsWith(oldBasePath)) {
332
+ const relativePath = testCase.path.substring(oldBasePath.length);
333
+ testCase.path = newBasePath + relativePath;
334
+
335
+ // Save the updated test case
336
+ writeFileSync(itemPath, JSON.stringify(testCase, null, 2), 'utf-8');
337
+ }
338
+ } catch (error) {
339
+ console.error(`Failed to update test case ${item}:`, error);
340
+ }
341
+ }
342
+ }
343
+ } catch (error) {
344
+ console.error(`Failed to update test paths in ${dirFullPath}:`, error);
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Move a test case to a different directory
350
+ * @param testId - The test case ID
351
+ * @param sourceDir - The current directory path
352
+ * @param targetDir - The target directory path
353
+ */
354
+ moveTestCase(testId: string, sourceDir: string, targetDir: string): boolean {
355
+ const sourceFile = join(this.getBaseDir(), sourceDir, `${testId}.json`);
356
+ const targetFile = join(this.getBaseDir(), targetDir, `${testId}.json`);
357
+
358
+ // Check if source file exists
359
+ if (!existsSync(sourceFile)) {
360
+ throw new Error(`Test case file does not exist: ${sourceFile}`);
361
+ }
362
+
363
+ // Check if target directory exists
364
+ const targetDirFullPath = join(this.getBaseDir(), targetDir);
365
+ if (!existsSync(targetDirFullPath)) {
366
+ throw new Error(`Target directory does not exist: ${targetDir}`);
367
+ }
368
+
369
+ // Check if file already exists in target
370
+ if (existsSync(targetFile)) {
371
+ throw new Error(`Test case already exists in target directory`);
372
+ }
373
+
374
+ // Move the file
375
+ renameSync(sourceFile, targetFile);
376
+ return true;
377
+ }
378
+
379
+ /**
380
+ * Save directory info to info.json
381
+ */
382
+ private saveDirectoryInfo(path: string, info: DirectoryInfo): void {
383
+ const infoPath = join(this.getBaseDir(), path, 'info.json');
384
+
385
+ // Ensure parent directory exists
386
+ const dir = dirname(infoPath);
387
+ if (!existsSync(dir)) {
388
+ mkdirSync(dir, { recursive: true });
389
+ }
390
+
391
+ writeFileSync(infoPath, JSON.stringify(info, null, 2), 'utf-8');
392
+ }
393
+ }
@@ -0,0 +1,57 @@
1
+ import { join } from 'path';
2
+ import { BaseRepository } from './BaseRepository';
3
+ import { ExplorationRecord } from '../types';
4
+
5
+ /**
6
+ * ExplorationRepository handles all storage operations for exploration records
7
+ * Responsible for managing web exploration and test generation history
8
+ */
9
+ export class ExplorationRepository extends BaseRepository {
10
+ constructor(projectId: string) {
11
+ super(join("projects", projectId, 'explorations'));
12
+ }
13
+
14
+ /**
15
+ * Save an exploration record
16
+ */
17
+ async saveExplorationRecord(record: ExplorationRecord): Promise<void> {
18
+ await this.save(record.id, record, 'json');
19
+ }
20
+
21
+ /**
22
+ * Load an exploration record by ID
23
+ */
24
+ async loadExplorationRecord(recordId: string): Promise<ExplorationRecord | null> {
25
+ return this.load<ExplorationRecord>(recordId, 'json');
26
+ }
27
+
28
+ /**
29
+ * List all exploration records sorted by start time (newest first)
30
+ */
31
+ async listExplorationRecords(): Promise<ExplorationRecord[]> {
32
+ const records = await this.listAll<ExplorationRecord>();
33
+ return records.sort((a, b) => b.startedAt - a.startedAt);
34
+ }
35
+
36
+ /**
37
+ * Update an exploration record
38
+ */
39
+ async updateExplorationRecord(record: ExplorationRecord): Promise<void> {
40
+ await this.saveExplorationRecord(record);
41
+ }
42
+
43
+ /**
44
+ * Delete an exploration record
45
+ */
46
+ async deleteExplorationRecord(recordId: string): Promise<void> {
47
+ await this.delete(recordId, 'json');
48
+ }
49
+
50
+ /**
51
+ * Get exploration records for a specific URL
52
+ */
53
+ async getRecordsByUrl(url: string): Promise<ExplorationRecord[]> {
54
+ const records = await this.listExplorationRecords();
55
+ return records.filter(r => r.url === url);
56
+ }
57
+ }
@@ -0,0 +1,153 @@
1
+ import { existsSync, mkdirSync, readdirSync, unlinkSync, readFileSync, writeFileSync, statSync, rmSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+
5
+ const STORAGE_DIR = join(homedir(), '.test_agent');
6
+
7
+ export interface FileInfo {
8
+ name: string;
9
+ size: number;
10
+ uploadedAt: string;
11
+ path: string;
12
+ }
13
+
14
+ /**
15
+ * FileRepository - Handles file storage for test cases
16
+ * Each test case has its own files directory
17
+ */
18
+ export class FileRepository {
19
+ private projectId: string;
20
+ private filesBaseDir: string;
21
+
22
+ constructor(projectId: string) {
23
+ this.projectId = projectId;
24
+ this.filesBaseDir = join(STORAGE_DIR, 'projects', projectId, 'files');
25
+ this.ensureBaseDir();
26
+ }
27
+
28
+ /**
29
+ * Ensure base files directory exists
30
+ */
31
+ private ensureBaseDir(): void {
32
+ if (!existsSync(this.filesBaseDir)) {
33
+ mkdirSync(this.filesBaseDir, { recursive: true });
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Get files directory for a specific test case
39
+ */
40
+ getTestFilesDir(testId: string): string {
41
+ return join(this.filesBaseDir, testId);
42
+ }
43
+
44
+ /**
45
+ * Ensure test files directory exists
46
+ */
47
+ private ensureTestFilesDir(testId: string): void {
48
+ const dir = this.getTestFilesDir(testId);
49
+ if (!existsSync(dir)) {
50
+ mkdirSync(dir, { recursive: true });
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Save a file for a test case
56
+ */
57
+ saveFile(testId: string, fileName: string, content: Buffer): FileInfo {
58
+ this.ensureTestFilesDir(testId);
59
+ const filePath = join(this.getTestFilesDir(testId), fileName);
60
+
61
+ writeFileSync(filePath, content);
62
+
63
+ const stats = statSync(filePath);
64
+ return {
65
+ name: fileName,
66
+ size: stats.size,
67
+ uploadedAt: new Date().toISOString(),
68
+ path: filePath,
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Load a file for a test case
74
+ */
75
+ loadFile(testId: string, fileName: string): Buffer | null {
76
+ const filePath = join(this.getTestFilesDir(testId), fileName);
77
+ if (!existsSync(filePath)) {
78
+ return null;
79
+ }
80
+ return readFileSync(filePath);
81
+ }
82
+
83
+ /**
84
+ * List all files for a test case
85
+ */
86
+ listFiles(testId: string): FileInfo[] {
87
+ const dir = this.getTestFilesDir(testId);
88
+ if (!existsSync(dir)) {
89
+ return [];
90
+ }
91
+
92
+ const files = readdirSync(dir);
93
+ return files.map(fileName => {
94
+ const filePath = join(dir, fileName);
95
+ const stats = statSync(filePath);
96
+ return {
97
+ name: fileName,
98
+ size: stats.size,
99
+ uploadedAt: stats.mtime.toISOString(),
100
+ path: filePath,
101
+ };
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Delete a file for a test case
107
+ */
108
+ deleteFile(testId: string, fileName: string): boolean {
109
+ const filePath = join(this.getTestFilesDir(testId), fileName);
110
+ if (!existsSync(filePath)) {
111
+ return false;
112
+ }
113
+ unlinkSync(filePath);
114
+ return true;
115
+ }
116
+
117
+ /**
118
+ * Delete all files for a test case
119
+ */
120
+ deleteAllFiles(testId: string): void {
121
+ const dir = this.getTestFilesDir(testId);
122
+ if (!existsSync(dir)) {
123
+ return;
124
+ }
125
+ rmSync(dir, { recursive: true });
126
+ }
127
+
128
+ /**
129
+ * Check if a file exists
130
+ */
131
+ fileExists(testId: string, fileName: string): boolean {
132
+ const filePath = join(this.getTestFilesDir(testId), fileName);
133
+ return existsSync(filePath);
134
+ }
135
+
136
+ /**
137
+ * Get file info
138
+ */
139
+ getFileInfo(testId: string, fileName: string): FileInfo | null {
140
+ const filePath = join(this.getTestFilesDir(testId), fileName);
141
+ if (!existsSync(filePath)) {
142
+ return null;
143
+ }
144
+
145
+ const stats = statSync(filePath);
146
+ return {
147
+ name: fileName,
148
+ size: stats.size,
149
+ uploadedAt: stats.mtime.toISOString(),
150
+ path: filePath,
151
+ };
152
+ }
153
+ }
@@ -0,0 +1,55 @@
1
+ import { BaseRepository } from './BaseRepository';
2
+ import { ModelConfig } from '../types';
3
+
4
+ /**
5
+ * ModelConfigRepository handles all storage operations for model configurations
6
+ * Responsible for managing AI model settings and credentials
7
+ */
8
+ export class ModelConfigRepository extends BaseRepository {
9
+ constructor() {
10
+ super('models');
11
+ }
12
+
13
+ /**
14
+ * Save a model configuration
15
+ */
16
+ async saveModelConfig(config: ModelConfig): Promise<void> {
17
+ await this.save(config.id, config, 'json');
18
+ }
19
+
20
+ /**
21
+ * Load a model configuration by ID
22
+ */
23
+ async loadModelConfig(configId: string): Promise<ModelConfig | null> {
24
+ return this.load<ModelConfig>(configId, 'json');
25
+ }
26
+
27
+ /**
28
+ * List all model configurations sorted by update time (newest first)
29
+ */
30
+ async listModelConfigs(): Promise<ModelConfig[]> {
31
+ const configs = await this.listAll<ModelConfig>();
32
+ return configs.sort((a, b) => b.updatedAt - a.updatedAt);
33
+ }
34
+
35
+ /**
36
+ * Update a model configuration
37
+ */
38
+ async updateModelConfig(config: ModelConfig): Promise<void> {
39
+ await this.saveModelConfig(config);
40
+ }
41
+
42
+ /**
43
+ * Delete a model configuration
44
+ */
45
+ async deleteModelConfig(configId: string): Promise<void> {
46
+ await this.delete(configId, 'json');
47
+ }
48
+
49
+ /**
50
+ * Check if a model configuration exists
51
+ */
52
+ async exists(configId: string): Promise<boolean> {
53
+ return this.existsSync(configId, 'json');
54
+ }
55
+ }