@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.
- package/README.md +46 -0
- package/dist/ProjectContextManager.d.ts +30 -0
- package/dist/ProjectContextManager.js +41 -0
- package/dist/directory-validator.d.ts +28 -0
- package/dist/directory-validator.js +90 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +44 -0
- package/dist/mcp/file-mcp-manager.d.ts +13 -0
- package/dist/mcp/file-mcp-manager.js +45 -0
- package/dist/mcp/index.d.ts +20 -0
- package/dist/mcp/index.js +16 -0
- package/dist/mcp/mcp-client.d.ts +127 -0
- package/dist/mcp/mcp-client.js +165 -0
- package/dist/mcp/mcp-manager.d.ts +20 -0
- package/dist/mcp/mcp-manager.js +98 -0
- package/dist/mcp/playwright-mcp-manager.d.ts +18 -0
- package/dist/mcp/playwright-mcp-manager.js +115 -0
- package/dist/model.d.ts +10 -0
- package/dist/model.js +207 -0
- package/dist/repositories/BaseRepository.d.ts +68 -0
- package/dist/repositories/BaseRepository.js +212 -0
- package/dist/repositories/DirectoryRepository.d.ts +69 -0
- package/dist/repositories/DirectoryRepository.js +335 -0
- package/dist/repositories/ExplorationRepository.d.ts +33 -0
- package/dist/repositories/ExplorationRepository.js +53 -0
- package/dist/repositories/FileRepository.d.ts +55 -0
- package/dist/repositories/FileRepository.js +131 -0
- package/dist/repositories/ModelConfigRepository.d.ts +33 -0
- package/dist/repositories/ModelConfigRepository.js +51 -0
- package/dist/repositories/ProjectRepository.d.ts +31 -0
- package/dist/repositories/ProjectRepository.js +66 -0
- package/dist/repositories/SettingsRepository.d.ts +18 -0
- package/dist/repositories/SettingsRepository.js +71 -0
- package/dist/repositories/TableDataRepository.d.ts +21 -0
- package/dist/repositories/TableDataRepository.js +32 -0
- package/dist/repositories/TestCaseRepository.d.ts +120 -0
- package/dist/repositories/TestCaseRepository.js +463 -0
- package/dist/repositories/TestPlanRepository.d.ts +34 -0
- package/dist/repositories/TestPlanRepository.js +79 -0
- package/dist/repositories/TestResultRepository.d.ts +29 -0
- package/dist/repositories/TestResultRepository.js +53 -0
- package/dist/repositories/index.d.ts +16 -0
- package/dist/repositories/index.js +30 -0
- package/dist/storageService.d.ts +129 -0
- package/dist/storageService.js +297 -0
- package/dist/types.d.ts +217 -0
- package/dist/types.js +2 -0
- package/package.json +32 -0
- package/src/directory-validator.ts +98 -0
- package/src/index.ts +26 -0
- package/src/mcp/file-mcp-manager.ts +50 -0
- package/src/mcp/index.ts +35 -0
- package/src/mcp/mcp-client.ts +209 -0
- package/src/mcp/mcp-manager.ts +118 -0
- package/src/mcp/playwright-mcp-manager.ts +127 -0
- package/src/model.ts +234 -0
- package/src/repositories/BaseRepository.ts +193 -0
- package/src/repositories/DirectoryRepository.ts +393 -0
- package/src/repositories/ExplorationRepository.ts +57 -0
- package/src/repositories/FileRepository.ts +153 -0
- package/src/repositories/ModelConfigRepository.ts +55 -0
- package/src/repositories/ProjectRepository.ts +70 -0
- package/src/repositories/SettingsRepository.ts +38 -0
- package/src/repositories/TableDataRepository.ts +33 -0
- package/src/repositories/TestCaseRepository.ts +521 -0
- package/src/repositories/TestPlanRepository.ts +89 -0
- package/src/repositories/TestResultRepository.ts +56 -0
- package/src/repositories/index.ts +17 -0
- package/src/storageService.ts +404 -0
- package/src/types.ts +246 -0
- 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
|
+
}
|