@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,70 @@
1
+ import { join } from 'path';
2
+ import { BaseRepository } from './BaseRepository';
3
+ import type { Project } from '../types';
4
+ import { readdirSync, rmSync } from 'fs';
5
+
6
+ /**
7
+ * ProjectRepository handles all CRUD operations for projects
8
+ * Projects are stored at ~/.test_agent/projects
9
+ */
10
+ export class ProjectRepository extends BaseRepository {
11
+ private projectJSON: string = "project";
12
+
13
+ constructor() {
14
+ super('projects');
15
+ }
16
+
17
+ /**
18
+ * Save a new project or update an existing one
19
+ */
20
+ saveProject(project: Project): void {
21
+ this.saveSync(join(project.id, this.projectJSON), project);
22
+ }
23
+
24
+ /**
25
+ * Load a single project by ID
26
+ */
27
+ loadProject(projectId: string): Project | null {
28
+ return this.loadSync<Project>(join(projectId, this.projectJSON));
29
+ }
30
+
31
+ /**
32
+ * List all projects
33
+ */
34
+ listProjects(): Project[] {
35
+ const projectDir = this.getBaseDir();
36
+ const projectIds = this.listDirectories(projectDir);
37
+ const projects: Project[] = [];
38
+ for (const projectId of projectIds) {
39
+ const project = this.loadSync<Project>(join(projectId, this.projectJSON));
40
+ if (project) {
41
+ projects.push(project);
42
+ } else {
43
+ projects.push({ id: 'default', 'description': 'Default Project', 'name': 'Default', 'createdAt': 0, isActive: false });
44
+ }
45
+ }
46
+ return projects;
47
+ }
48
+
49
+ private listDirectories(dirPath: string): string[] {
50
+ return readdirSync(dirPath, { withFileTypes: true })
51
+ .filter(dirent => dirent.isDirectory())
52
+ .map(dirent => dirent.name);
53
+ }
54
+
55
+ /**
56
+ * Delete a project by ID
57
+ */
58
+ deleteProject(projectId: string): void {
59
+ const projectDir = this.getBaseDir();
60
+ rmSync(join(projectDir, projectId), { recursive: true });
61
+ }
62
+
63
+ /**
64
+ * Check if a project exists
65
+ */
66
+ async exists(projectId: string): Promise<boolean> {
67
+ const project = this.loadProject(join(projectId, this.projectJSON));
68
+ return project !== null;
69
+ }
70
+ }
@@ -0,0 +1,38 @@
1
+ import { BaseRepository } from './BaseRepository';
2
+ import { AppSettings } from '../types';
3
+
4
+ /**
5
+ * SettingsRepository handles all storage operations for application settings
6
+ * Responsible for managing user preferences and configuration
7
+ */
8
+ export class SettingsRepository extends BaseRepository {
9
+ private readonly SETTINGS_ID = 'settings';
10
+
11
+ constructor() {
12
+ super('.');
13
+ }
14
+
15
+ /**
16
+ * Save application settings
17
+ */
18
+ async saveSettings(settings: AppSettings): Promise<void> {
19
+ // Save at root of storage directory
20
+ const path = `${this.storageDir}/settings.json`;
21
+ const fs = await import('fs/promises');
22
+ await fs.writeFile(path, JSON.stringify(settings, null, 2), 'utf-8');
23
+ }
24
+
25
+ /**
26
+ * Load application settings
27
+ */
28
+ async loadSettings(): Promise<AppSettings | null> {
29
+ const path = `${this.storageDir}/settings.json`;
30
+ try {
31
+ const fs = await import('fs/promises');
32
+ const content = await fs.readFile(path, 'utf-8');
33
+ return JSON.parse(content) as AppSettings;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,33 @@
1
+ import { BaseRepository } from './BaseRepository';
2
+ import type { TableData } from '../types';
3
+
4
+ /**
5
+ * TableDataRepository handles storage operations for test table data
6
+ * Stores table data in data directory as {testId}.json
7
+ */
8
+ export class TableDataRepository extends BaseRepository {
9
+ constructor(projectId: string) {
10
+ super(`projects/${projectId}/data`);
11
+ }
12
+
13
+ /**
14
+ * Save table data for a test
15
+ */
16
+ async saveTableData(tableData: TableData): Promise<void> {
17
+ await this.save(tableData.testId, tableData);
18
+ }
19
+
20
+ /**
21
+ * Load table data for a test
22
+ */
23
+ async loadTableData(testId: string): Promise<TableData | null> {
24
+ return this.load<TableData>(testId);
25
+ }
26
+
27
+ /**
28
+ * Delete table data for a test
29
+ */
30
+ async deleteTableData(testId: string): Promise<void> {
31
+ await this.delete(testId);
32
+ }
33
+ }
@@ -0,0 +1,521 @@
1
+ import { BaseRepository } from './BaseRepository';
2
+ import { TestCase, TestStep, TestDirectory } from '../types';
3
+ import { join, relative } from 'path';
4
+ import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync, readdirSync, statSync } from 'fs';
5
+
6
+ /**
7
+ * TestCaseRepository handles all storage operations for test cases
8
+ * Responsible for managing test case data, steps, and associated scripts
9
+ * Supports hierarchical directory organization (up to 3 levels)
10
+ */
11
+ export class TestCaseRepository extends BaseRepository {
12
+ constructor(projectId: string) {
13
+ super(join("projects", projectId, 'tests'));
14
+ }
15
+
16
+ /**
17
+ * Get scripts directory - project-aware
18
+ * When project is active: ~/.test_agent/projects/{projectId}/scripts
19
+ * When no project: ~/.test_agent/scripts
20
+ */
21
+ private getScriptsDir(): string {
22
+ const testsBaseDir = this.getBaseDir(); // ~/.test_agent/projects/{projectId}/tests or ~/.test_agent/tests
23
+ const parentDir = join(testsBaseDir, '..');
24
+ return join(parentDir, 'scripts');
25
+ }
26
+
27
+ /**
28
+ * Get steps directory - project-aware
29
+ * When project is active: ~/.test_agent/projects/{projectId}/steps
30
+ * When no project: ~/.test_agent/steps
31
+ */
32
+ private getStepsDir(): string {
33
+ const testsBaseDir = this.getBaseDir(); // ~/.test_agent/projects/{projectId}/tests or ~/.test_agent/tests
34
+ const parentDir = join(testsBaseDir, '..');
35
+ return join(parentDir, 'steps');
36
+ }
37
+
38
+ /**
39
+ * Ensure scripts directory exists
40
+ */
41
+ private ensureScriptsDir(): void {
42
+ const scriptsDir = this.getScriptsDir();
43
+ if (!existsSync(scriptsDir)) {
44
+ mkdirSync(scriptsDir, { recursive: true });
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Ensure steps directory exists
50
+ */
51
+ private ensureStepsDir(): void {
52
+ const stepsDir = this.getStepsDir();
53
+ if (!existsSync(stepsDir)) {
54
+ mkdirSync(stepsDir, { recursive: true });
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Save a test case with support for directory paths
60
+ */
61
+ saveTestCase(testCase: TestCase): void {
62
+ const dirPath = join(this.getBaseDir(), testCase.path);
63
+
64
+ // Ensure directory exists
65
+ if (!existsSync(dirPath)) {
66
+ mkdirSync(dirPath, { recursive: true });
67
+ }
68
+
69
+ // Save to file within the directory path
70
+ const filePath = join(dirPath, `${testCase.id}.json`);
71
+ // C:\Users\85847\.test_agent\projects\43af4db4-3249-4135-b279-97b24f27106b\tests\aaa\ccc\ddd
72
+ writeFileSync(filePath, JSON.stringify(testCase, (key, value) => {
73
+ if (key === 'steps' || key === 'playwrightScript' || key === 'table') {
74
+ return undefined; // Return undefined to ignore this field
75
+ }
76
+ return value;
77
+ }, 2), 'utf-8');
78
+ }
79
+
80
+ /**
81
+ * Load a test case with its steps and script
82
+ */
83
+ loadTestCase(testId: string, path?: string): TestCase | null {
84
+ let filePath: string;
85
+
86
+ if (path) {
87
+ filePath = join(this.getBaseDir(), path, `${testId}.json`);
88
+ } else {
89
+ // Try to find test case in the base directory or any subdirectory
90
+ filePath = join(this.getBaseDir(), `${testId}.json`);
91
+ if (!existsSync(filePath)) {
92
+ // Search in all directories
93
+ const found = this.findTestCaseFile(testId, this.getBaseDir());
94
+ if (!found) {
95
+ return null;
96
+ }
97
+ filePath = found;
98
+ }
99
+ }
100
+
101
+ if (!existsSync(filePath)) {
102
+ return null;
103
+ }
104
+
105
+ try {
106
+ const content = readFileSync(filePath, 'utf-8');
107
+ const testCase = JSON.parse(content) as TestCase;
108
+
109
+ // Load associated steps
110
+ const steps = this.loadTestSteps(testId);
111
+ testCase.steps = steps;
112
+
113
+ return testCase;
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Recursively find a test case file by ID
121
+ */
122
+ private findTestCaseFile(testId: string, searchPath: string): string | null {
123
+ try {
124
+ const items = readdirSync(searchPath);
125
+
126
+ for (const item of items) {
127
+ const itemPath = join(searchPath, item);
128
+ const stat = statSync(itemPath);
129
+
130
+ if (stat.isDirectory() && item !== 'info.json') {
131
+ // Skip deep nesting (max 3 levels from tests root)
132
+ const relativePath = relative(this.getBaseDir(), itemPath);
133
+ if (relativePath.split(/[\/\\]/).length > 3) {
134
+ continue;
135
+ }
136
+ const result = this.findTestCaseFile(testId, itemPath);
137
+ if (result) {
138
+ return result;
139
+ }
140
+ } else if (item === `${testId}.json`) {
141
+ return itemPath;
142
+ }
143
+ }
144
+ } catch {
145
+ // Skip if unable to read directory
146
+ }
147
+
148
+ return null;
149
+ }
150
+
151
+ /**
152
+ * List all test cases sorted by creation date (newest first)
153
+ * Returns a hierarchical directory tree
154
+ */
155
+ listTestCases(): TestCase[] {
156
+ return this.listTestCasesInDirectory(this.getBaseDir());
157
+ }
158
+
159
+ /**
160
+ * Build complete directory tree
161
+ */
162
+ buildDirectoryTree(): TestDirectory {
163
+ return this.buildDirectoryTreeRecursive(this.getBaseDir(), '');
164
+ }
165
+
166
+ /**
167
+ * Recursively build directory tree
168
+ */
169
+ private buildDirectoryTreeRecursive(fullPath: string, relativePath: string): TestDirectory {
170
+ const pathParts = relativePath ? relativePath.split('/') : [];
171
+ const name = pathParts.length > 0 ? pathParts[pathParts.length - 1] : 'root';
172
+
173
+ // Load directory info if exists
174
+ let info = undefined;
175
+ const infoPath = join(fullPath, 'info.json');
176
+ if (existsSync(infoPath)) {
177
+ try {
178
+ const content = readFileSync(infoPath, 'utf-8');
179
+ info = JSON.parse(content);
180
+ } catch {
181
+ // Skip invalid info.json
182
+ }
183
+ }
184
+
185
+ const tree: TestDirectory = {
186
+ path: relativePath,
187
+ name,
188
+ info,
189
+ children: [],
190
+ tests: [],
191
+ };
192
+
193
+ try {
194
+ const items = readdirSync(fullPath);
195
+
196
+ for (const item of items) {
197
+ if (item === 'info.json') {
198
+ continue;
199
+ }
200
+
201
+ const itemPath = join(fullPath, item);
202
+ const stat = statSync(itemPath);
203
+
204
+ if (stat.isDirectory()) {
205
+ const subPath = relativePath ? `${relativePath}/${item}` : item;
206
+
207
+ // Check depth (max 3 levels)
208
+ if (subPath.split('/').length <= 3) {
209
+ tree.children.push(this.buildDirectoryTreeRecursive(itemPath, subPath));
210
+ }
211
+ }
212
+ }
213
+
214
+ // Load test files from current directory
215
+ for (const item of items) {
216
+ if (item.endsWith('.json') && item !== 'info.json') {
217
+ try {
218
+ const content = readFileSync(join(fullPath, item), 'utf-8');
219
+ const testCase = JSON.parse(content) as TestCase;
220
+ // Only include tests that belong to this directory
221
+ if (testCase.path === relativePath) {
222
+ tree.tests.push(testCase);
223
+ }
224
+ } catch {
225
+ // Skip invalid test files
226
+ }
227
+ }
228
+ }
229
+
230
+ // Sort children by name and tests by creation date
231
+ tree.children.sort((a, b) => a.name.localeCompare(b.name));
232
+ tree.tests.sort((a, b) => b.createdAt - a.createdAt);
233
+ } catch {
234
+ // Return tree with no children/tests if unable to read
235
+ }
236
+
237
+ return tree;
238
+ }
239
+
240
+ /**
241
+ * List test cases in a specific directory recursively.
242
+ * Loads full test data including steps
243
+ */
244
+ listTestCasesRecursively(dirPath: string, seen: Set<string> = new Set()): TestCase[] {
245
+ const fullPath = join(this.getBaseDir(), dirPath);
246
+ const tests: TestCase[] = [];
247
+
248
+ const items = readdirSync(fullPath);
249
+
250
+ for (const item of items) {
251
+ const itemPath = join(fullPath, item);
252
+ const stat = statSync(itemPath);
253
+
254
+ if (stat.isDirectory()) {
255
+ // Recursively list tests in subdirectories
256
+ const subDirPath = dirPath ? `${dirPath}/${item}` : item;
257
+ tests.push(...this.listTestCasesRecursively(subDirPath, seen));
258
+ } else if (item.endsWith('.json') && item !== 'info.json') {
259
+ // Load test case file
260
+ const testId = item.replace('.json', '');
261
+ if (seen.has(testId)) {
262
+ continue; // Skip duplicates
263
+ }
264
+ seen.add(testId);
265
+ const testCase = this.loadTestCase(testId, dirPath);
266
+ if (testCase) {
267
+ tests.push(testCase);
268
+ }
269
+ }
270
+ }
271
+
272
+ return tests.sort((a, b) => b.createdAt - a.createdAt);
273
+ }
274
+
275
+ /**
276
+ * List test cases in a specific directory
277
+ */
278
+ private listTestCasesInDirectory(dirPath: string): TestCase[] {
279
+ const tests: TestCase[] = [];
280
+
281
+ try {
282
+ const items = readdirSync(dirPath);
283
+
284
+ for (const item of items) {
285
+ if (item.endsWith('.json') && item !== 'info.json') {
286
+ try {
287
+ const content = readFileSync(join(dirPath, item), 'utf-8');
288
+ tests.push(JSON.parse(content) as TestCase);
289
+ } catch {
290
+ // Skip invalid files
291
+ }
292
+ } else {
293
+ const itemPath = join(dirPath, item);
294
+ const stat = statSync(itemPath);
295
+ if (stat.isDirectory()) {
296
+ tests.push(...this.listTestCasesInDirectory(itemPath));
297
+ }
298
+ }
299
+ }
300
+ } catch {
301
+ // Return empty array if unable to read
302
+ }
303
+
304
+ return tests.sort((a, b) => b.createdAt - a.createdAt);
305
+ }
306
+
307
+ /**
308
+ * Delete a test case and its associated data
309
+ */
310
+ deleteTestCase(testId: string): void {
311
+ const filePath = this.findTestCaseFile(testId, this.getBaseDir());
312
+ if (filePath) {
313
+ rmSync(filePath);
314
+ }
315
+ this.deleteTestSteps(testId);
316
+ this.deleteTestScript(testId);
317
+ }
318
+
319
+ /**
320
+ * Save test case steps
321
+ */
322
+ saveTestSteps(testId: string, steps: TestStep[]): void {
323
+ // Ensure steps directory exists
324
+ this.ensureStepsDir();
325
+
326
+ // Reset any running steps
327
+ let runningFound = false;
328
+ steps.forEach(step => {
329
+ if (runningFound || step.status === 'running') {
330
+ runningFound = true;
331
+ step.status = undefined;
332
+ step.error = undefined;
333
+ }
334
+ });
335
+
336
+ const stepsFile = this.getStepsFilePath(testId);
337
+ writeFileSync(stepsFile, JSON.stringify(steps, null, 2), 'utf-8');
338
+ }
339
+
340
+ /**
341
+ * Load test case steps
342
+ */
343
+ loadTestSteps(testId: string): TestStep[] {
344
+ const stepsFile = this.getStepsFilePath(testId);
345
+ if (!existsSync(stepsFile)) {
346
+ return [];
347
+ }
348
+ const content = readFileSync(stepsFile, 'utf-8');
349
+ return JSON.parse(content) as TestStep[];
350
+ }
351
+
352
+ /**
353
+ * Delete all steps for a test case
354
+ */
355
+ private deleteTestSteps(testId: string): void {
356
+ const stepsFile = this.getStepsFilePath(testId);
357
+ if (existsSync(stepsFile)) {
358
+ rmSync(stepsFile);
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Save a single test step or insert a new one
364
+ * @returns true if updated, false if created
365
+ */
366
+ saveTestStep(
367
+ testId: string,
368
+ step: { id: string; description?: string; isBreakpoint?: boolean; isAI?: boolean; reusedTestIds?: string[] },
369
+ afterStepId?: string
370
+ ): boolean {
371
+ const testCase = this.loadTestCase(testId);
372
+ if (!testCase) {
373
+ throw new Error(`Test case ${testId} not found`);
374
+ }
375
+
376
+ testCase.status = 'draft';
377
+
378
+ let updated = false;
379
+ let insertIndex = 0;
380
+ const dbStep = testCase.steps.find(s => s.id === step.id);
381
+
382
+ if (!dbStep) {
383
+ if (afterStepId) {
384
+ insertIndex = testCase.steps.findIndex(s => s.id === afterStepId);
385
+ if (insertIndex !== -1) {
386
+ insertIndex += 1;
387
+ } else {
388
+ insertIndex = testCase.steps.length;
389
+ }
390
+ testCase.steps.splice(insertIndex, 0, step as TestStep);
391
+ } else {
392
+ testCase.steps.push(step as TestStep);
393
+ }
394
+ }
395
+
396
+ if (dbStep) {
397
+ if (step.description) {
398
+ dbStep.description = step.description;
399
+ }
400
+ if (step.isBreakpoint !== undefined && step.isBreakpoint !== null) {
401
+ dbStep.isBreakpoint = step.isBreakpoint;
402
+ }
403
+ if (step.isAI !== undefined && step.isAI !== null) {
404
+ dbStep.isAI = step.isAI;
405
+ }
406
+ if (step.reusedTestIds !== undefined) {
407
+ dbStep.reusedTestIds = step.reusedTestIds;
408
+ }
409
+ dbStep.status = undefined;
410
+ dbStep.error = undefined;
411
+ updated = true;
412
+ }
413
+
414
+ this.saveTestSteps(testId, testCase.steps);
415
+ return updated;
416
+ }
417
+
418
+ /**
419
+ * Insert a new step after a specific step
420
+ */
421
+ insertStep(
422
+ testId: string,
423
+ afterStepId: string | null | undefined,
424
+ newStep: TestStep
425
+ ): void {
426
+ const testCase = this.loadTestCase(testId);
427
+ if (!testCase) {
428
+ throw new Error(`Test case ${testId} not found`);
429
+ }
430
+
431
+ testCase.status = 'draft';
432
+
433
+ let insertIndex = 0;
434
+ if (afterStepId) {
435
+ insertIndex = testCase.steps.findIndex(s => s.id === afterStepId);
436
+ if (insertIndex !== -1) {
437
+ insertIndex += 1;
438
+ } else {
439
+ insertIndex = testCase.steps.length;
440
+ }
441
+ }
442
+
443
+ testCase.steps.splice(insertIndex, 0, newStep);
444
+ this.saveTestSteps(testId, testCase.steps);
445
+ }
446
+
447
+ /**
448
+ * Delete a specific step by ID
449
+ */
450
+ deleteStep(testId: string, stepId: string): boolean {
451
+ let steps = this.loadTestSteps(testId);
452
+ if (steps.length === 0) {
453
+ return false;
454
+ }
455
+
456
+ const index = steps.findIndex(s => s.id === stepId);
457
+ steps = [
458
+ ...steps.slice(0, index),
459
+ ...steps.slice(index + 1),
460
+ ];
461
+ this.saveTestSteps(testId, steps);
462
+
463
+ const testCase = this.loadTestCase(testId);
464
+ if (testCase && testCase.status !== 'draft') {
465
+ testCase.status = 'draft';
466
+ this.saveTestCase(testCase);
467
+ }
468
+
469
+ return true;
470
+ }
471
+
472
+ /**
473
+ * Save Playwright script for a test case
474
+ */
475
+ saveTestScript(testId: string, script: string): void {
476
+ // Ensure scripts directory exists
477
+ this.ensureScriptsDir();
478
+
479
+ const scriptPath = this.getScriptFilePath(testId);
480
+ writeFileSync(scriptPath, script, 'utf-8');
481
+ }
482
+
483
+ /**
484
+ * Load Playwright script for a test case
485
+ */
486
+ loadTestScript(testId: string): string | undefined {
487
+ const scriptPath = this.getScriptFilePath(testId);
488
+ if (!existsSync(scriptPath)) {
489
+ return undefined;
490
+ }
491
+ return readFileSync(scriptPath, 'utf-8');
492
+ }
493
+
494
+ hasTestScript(testId: string): boolean {
495
+ return existsSync(this.getScriptFilePath(testId));
496
+ }
497
+
498
+ /**
499
+ * Delete Playwright script for a test case
500
+ */
501
+ private deleteTestScript(testId: string): void {
502
+ const scriptPath = this.getScriptFilePath(testId);
503
+ if (existsSync(scriptPath)) {
504
+ rmSync(scriptPath);
505
+ }
506
+ }
507
+
508
+ /**
509
+ * Get the file path for a test step file
510
+ */
511
+ private getStepsFilePath(testId: string): string {
512
+ return join(this.getStepsDir(), `${testId}.json`);
513
+ }
514
+
515
+ /**
516
+ * Get the file path for a test script file
517
+ */
518
+ private getScriptFilePath(testId: string): string {
519
+ return join(this.getScriptsDir(), `${testId}.ts`);
520
+ }
521
+ }