@optimizely/ocp-cli 1.2.13 → 1.2.14
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/dist/commands/app/Init.js +1 -1
- package/dist/commands/app/Init.js.map +1 -1
- package/dist/oo-cli.manifest.json +1 -1
- package/package.json +10 -6
- package/src/commands/app/Init.ts +1 -1
- package/src/test/e2e/__tests__/accounts/accounts.test.ts +120 -0
- package/src/test/e2e/__tests__/availability/availability.test.ts +156 -0
- package/src/test/e2e/__tests__/directory/directory.test.ts +668 -0
- package/src/test/e2e/__tests__/jobs/jobs.test.ts +487 -0
- package/src/test/e2e/__tests__/review/review.test.ts +355 -0
- package/src/test/e2e/config/fixture-loader.ts +130 -0
- package/src/test/e2e/config/setup.ts +29 -0
- package/src/test/e2e/config/test-data-config.ts +27 -0
- package/src/test/e2e/config/test-data-helpers.ts +23 -0
- package/src/test/e2e/fixtures/baselines/accounts/whoami.txt +11 -0
- package/src/test/e2e/fixtures/baselines/accounts/whois.txt +4 -0
- package/src/test/e2e/fixtures/baselines/directory/info.txt +7 -0
- package/src/test/e2e/fixtures/baselines/directory/list.txt +4 -0
- package/src/test/e2e/fixtures/baselines/jobs/list.txt +4 -0
- package/src/test/e2e/fixtures/baselines/review/list.txt +4 -0
- package/src/test/e2e/lib/base-test.ts +150 -0
- package/src/test/e2e/lib/command-discovery.ts +324 -0
- package/src/test/e2e/utils/baseline-normalizer.ts +79 -0
- package/src/test/e2e/utils/cli-executor.ts +349 -0
- package/src/test/e2e/utils/command-registry.ts +99 -0
- package/src/test/e2e/utils/output-validator.ts +661 -0
- package/src/test/setup.ts +3 -1
- package/src/test/tsconfig.json +17 -0
- package/dist/test/setup.d.ts +0 -0
- package/dist/test/setup.js +0 -4
- package/dist/test/setup.js.map +0 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { CLIExecutor, CLIExecutionResult, CLIExecutionOptions } from '../utils/cli-executor';
|
|
2
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { BaselineNormalizer } from '../utils/baseline-normalizer';
|
|
5
|
+
|
|
6
|
+
export abstract class BaseE2ETest {
|
|
7
|
+
protected cliExecutor: CLIExecutor;
|
|
8
|
+
protected testConfig: any;
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
this.cliExecutor = new CLIExecutor();
|
|
12
|
+
this.testConfig = (global as any).TEST_CONFIG;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Cleanup method to be called after tests
|
|
17
|
+
*/
|
|
18
|
+
async cleanup(): Promise<void> {
|
|
19
|
+
if (this.cliExecutor) {
|
|
20
|
+
await this.cliExecutor.destroy();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Execute a CLI command and return the result
|
|
26
|
+
*/
|
|
27
|
+
protected async executeCommand(command: string, options: CLIExecutionOptions = {}): Promise<CLIExecutionResult> {
|
|
28
|
+
return this.cliExecutor.executeCommand(command, options);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Execute CLI command with arguments array
|
|
33
|
+
*/
|
|
34
|
+
protected async execute(args: string[], options: CLIExecutionOptions = {}): Promise<CLIExecutionResult> {
|
|
35
|
+
return this.cliExecutor.execute(args, options);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Load test fixture data
|
|
40
|
+
*/
|
|
41
|
+
protected loadFixture(fixturePath: string): string {
|
|
42
|
+
const fullPath = join(this.testConfig.FIXTURES_PATH, fixturePath);
|
|
43
|
+
if (!existsSync(fullPath)) {
|
|
44
|
+
throw new Error(`Fixture not found: ${fullPath}`);
|
|
45
|
+
}
|
|
46
|
+
return readFileSync(fullPath, 'utf-8');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Load JSON fixture data
|
|
51
|
+
*/
|
|
52
|
+
protected loadJsonFixture<T = any>(fixturePath: string): T {
|
|
53
|
+
const content = this.loadFixture(fixturePath);
|
|
54
|
+
return JSON.parse(content);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Load baseline data for comparison
|
|
59
|
+
*/
|
|
60
|
+
protected loadBaseline(baselinePath: string): string {
|
|
61
|
+
const fullPath = join(this.testConfig.BASELINES_PATH, baselinePath);
|
|
62
|
+
if (!existsSync(fullPath)) {
|
|
63
|
+
throw new Error(`Baseline not found: ${fullPath}`);
|
|
64
|
+
}
|
|
65
|
+
return readFileSync(fullPath, 'utf-8');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Save baseline data (for baseline generation/updates)
|
|
70
|
+
*/
|
|
71
|
+
protected saveBaseline(baselinePath: string, content: string): void {
|
|
72
|
+
const fullPath = join(this.testConfig.BASELINES_PATH, baselinePath);
|
|
73
|
+
const dir = dirname(fullPath);
|
|
74
|
+
|
|
75
|
+
if (!existsSync(dir)) {
|
|
76
|
+
mkdirSync(dir, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
writeFileSync(fullPath, content, 'utf-8');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Assert command executed successfully
|
|
84
|
+
*/
|
|
85
|
+
protected assertSuccess(result: CLIExecutionResult, message?: string): void {
|
|
86
|
+
expect(result.exitCode).toBe(0);
|
|
87
|
+
if (message) {
|
|
88
|
+
expect(result.stderr).toBe('');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Assert command failed with expected exit code
|
|
94
|
+
*/
|
|
95
|
+
protected assertFailure(result: CLIExecutionResult, expectedExitCode?: number): void {
|
|
96
|
+
if (expectedExitCode !== undefined) {
|
|
97
|
+
expect(result.exitCode).toBe(expectedExitCode);
|
|
98
|
+
} else {
|
|
99
|
+
expect(result.exitCode).not.toBe(0);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Assert output contains expected text
|
|
105
|
+
*/
|
|
106
|
+
protected assertOutputContains(result: CLIExecutionResult, expectedText: string): void {
|
|
107
|
+
expect(result.stdout).toContain(expectedText);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Assert output matches baseline with normalization
|
|
112
|
+
*/
|
|
113
|
+
protected assertOutputMatchesBaseline(result: CLIExecutionResult, baselinePath: string, maxLines?: number): void {
|
|
114
|
+
const baseline = this.loadBaseline(baselinePath);
|
|
115
|
+
let actualOutput = result.stdout;
|
|
116
|
+
|
|
117
|
+
// If maxLines is specified, truncate both actual and baseline to first N lines
|
|
118
|
+
if (maxLines) {
|
|
119
|
+
actualOutput = this.truncateToLines(actualOutput, maxLines);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const normalizedActual = BaselineNormalizer.normalize(actualOutput);
|
|
123
|
+
const normalizedBaseline = BaselineNormalizer.normalize(baseline);
|
|
124
|
+
|
|
125
|
+
if (normalizedActual !== normalizedBaseline) {
|
|
126
|
+
console.log('Expected (baseline):');
|
|
127
|
+
console.log(normalizedBaseline);
|
|
128
|
+
console.log('\nActual:');
|
|
129
|
+
console.log(normalizedActual);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
expect(normalizedActual).toBe(normalizedBaseline);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Truncate output to first N lines for baseline comparison
|
|
137
|
+
*/
|
|
138
|
+
private truncateToLines(output: string, maxLines: number): string {
|
|
139
|
+
const lines = output.split('\n');
|
|
140
|
+
return lines.slice(0, maxLines).join('\n');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Generate or update a baseline file (for development/maintenance)
|
|
145
|
+
*/
|
|
146
|
+
protected updateBaseline(result: CLIExecutionResult, baselinePath: string): void {
|
|
147
|
+
const normalizedOutput = BaselineNormalizer.normalize(result.stdout);
|
|
148
|
+
this.saveBaseline(baselinePath, normalizedOutput);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { readdirSync, statSync, readFileSync } from 'fs';
|
|
2
|
+
import { join, extname } from 'path';
|
|
3
|
+
|
|
4
|
+
export interface CommandParameter {
|
|
5
|
+
name: string;
|
|
6
|
+
type: 'param' | 'option' | 'flag';
|
|
7
|
+
required: boolean;
|
|
8
|
+
help?: string;
|
|
9
|
+
shortFlag?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CommandMetadata {
|
|
13
|
+
namespace: string;
|
|
14
|
+
commandName: string;
|
|
15
|
+
methodName: string;
|
|
16
|
+
help?: string;
|
|
17
|
+
parameters: CommandParameter[];
|
|
18
|
+
filePath: string;
|
|
19
|
+
className: string;
|
|
20
|
+
isReadOnly: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CommandRegistry {
|
|
24
|
+
commands: CommandMetadata[];
|
|
25
|
+
namespaces: string[];
|
|
26
|
+
readOnlyCommands: CommandMetadata[];
|
|
27
|
+
stateChangingCommands: CommandMetadata[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class CommandDiscoveryService {
|
|
31
|
+
private readonly commandsPath: string;
|
|
32
|
+
private readonly readOnlyPatterns = [
|
|
33
|
+
// Method name patterns that indicate read-only operations
|
|
34
|
+
/^(list|get|show|info|status|whoami|whois|search|find|view|display)$/i,
|
|
35
|
+
// Help-related patterns
|
|
36
|
+
/help/i,
|
|
37
|
+
// Status/info patterns
|
|
38
|
+
/^(runtime)?status$/i
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
private readonly stateChangingPatterns = [
|
|
42
|
+
// Method name patterns that indicate state-changing operations
|
|
43
|
+
/^(create|init|add|update|edit|modify|delete|remove|destroy|terminate|trigger|install|uninstall|publish|unpublish|register|abandon|prepare|package|upgrade|set)$/i
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
constructor(commandsPath: string = 'src/commands') {
|
|
47
|
+
this.commandsPath = commandsPath;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Discover all commands in the commands directory
|
|
52
|
+
*/
|
|
53
|
+
public discoverCommands(): CommandRegistry {
|
|
54
|
+
const commands: CommandMetadata[] = [];
|
|
55
|
+
|
|
56
|
+
this.scanDirectory(this.commandsPath, commands);
|
|
57
|
+
|
|
58
|
+
const namespaces = [...new Set(commands.map(cmd => cmd.namespace))].sort();
|
|
59
|
+
const readOnlyCommands = commands.filter(cmd => cmd.isReadOnly);
|
|
60
|
+
const stateChangingCommands = commands.filter(cmd => !cmd.isReadOnly);
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
commands,
|
|
64
|
+
namespaces,
|
|
65
|
+
readOnlyCommands,
|
|
66
|
+
stateChangingCommands
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get commands for a specific namespace
|
|
72
|
+
*/
|
|
73
|
+
public getCommandsForNamespace(namespace: string): CommandMetadata[] {
|
|
74
|
+
const registry = this.discoverCommands();
|
|
75
|
+
return registry.commands.filter(cmd => cmd.namespace === namespace);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get command by namespace and command name
|
|
80
|
+
*/
|
|
81
|
+
public getCommand(namespace: string, commandName: string): CommandMetadata | undefined {
|
|
82
|
+
const registry = this.discoverCommands();
|
|
83
|
+
return registry.commands.find(cmd =>
|
|
84
|
+
cmd.namespace === namespace && cmd.commandName === commandName
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Recursively scan directory for TypeScript command files
|
|
90
|
+
*/
|
|
91
|
+
private scanDirectory(dirPath: string, commands: CommandMetadata[]): void {
|
|
92
|
+
try {
|
|
93
|
+
const entries = readdirSync(dirPath);
|
|
94
|
+
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
const fullPath = join(dirPath, entry);
|
|
97
|
+
const stat = statSync(fullPath);
|
|
98
|
+
|
|
99
|
+
if (stat.isDirectory()) {
|
|
100
|
+
this.scanDirectory(fullPath, commands);
|
|
101
|
+
} else if (stat.isFile() && extname(entry) === '.ts') {
|
|
102
|
+
const commandMetadata = this.parseCommandFile(fullPath);
|
|
103
|
+
if (commandMetadata) {
|
|
104
|
+
commands.push(commandMetadata);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.warn(`Warning: Could not scan directory ${dirPath}:`, error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Parse a TypeScript file to extract command metadata
|
|
115
|
+
*/
|
|
116
|
+
private parseCommandFile(filePath: string): CommandMetadata | null {
|
|
117
|
+
try {
|
|
118
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
119
|
+
|
|
120
|
+
// Extract namespace from @namespace decorator
|
|
121
|
+
const namespaceMatch = content.match(/@namespace\(['"`]([^'"`]+)['"`]\)/);
|
|
122
|
+
if (!namespaceMatch) {
|
|
123
|
+
return null; // Not a command file if no namespace
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const namespace = namespaceMatch[1];
|
|
127
|
+
|
|
128
|
+
// Extract class name
|
|
129
|
+
const classMatch = content.match(/export\s+class\s+(\w+)/);
|
|
130
|
+
if (!classMatch) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const className = classMatch[1];
|
|
135
|
+
|
|
136
|
+
// Find method with @command decorator
|
|
137
|
+
const commandMethodMatch = content.match(/@command[\s\S]*?public\s+async\s+(\w+)\s*\(/);
|
|
138
|
+
if (!commandMethodMatch) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const methodName = commandMethodMatch[1];
|
|
143
|
+
const commandName = methodName;
|
|
144
|
+
|
|
145
|
+
// Extract help text for the command method (look for @help before the @command method)
|
|
146
|
+
const commandMethodStart = content.indexOf(`public async ${methodName}`);
|
|
147
|
+
const commandSection = content.substring(0, commandMethodStart);
|
|
148
|
+
const lastHelpMatch = commandSection.match(/@help\(['"`]([^'"`]+)['"`]\)(?![\s\S]*@help)/);
|
|
149
|
+
const help = lastHelpMatch ? lastHelpMatch[1] : undefined;
|
|
150
|
+
|
|
151
|
+
// Extract parameters
|
|
152
|
+
const parameters = this.extractParameters(content);
|
|
153
|
+
|
|
154
|
+
// Determine if command is read-only
|
|
155
|
+
const isReadOnly = this.isReadOnlyCommand(methodName, help, content);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
namespace,
|
|
159
|
+
commandName,
|
|
160
|
+
methodName,
|
|
161
|
+
help,
|
|
162
|
+
parameters,
|
|
163
|
+
filePath,
|
|
164
|
+
className,
|
|
165
|
+
isReadOnly
|
|
166
|
+
};
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.warn(`Warning: Could not parse command file ${filePath}:`, error);
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Extract parameter information from command file content
|
|
175
|
+
*/
|
|
176
|
+
private extractParameters(content: string): CommandParameter[] {
|
|
177
|
+
const parameters: CommandParameter[] = [];
|
|
178
|
+
|
|
179
|
+
// Find property declarations with their decorators
|
|
180
|
+
// Look for patterns like:
|
|
181
|
+
// @param
|
|
182
|
+
// @optional
|
|
183
|
+
// @help('...')
|
|
184
|
+
// private propertyName: type
|
|
185
|
+
|
|
186
|
+
const propertyBlocks = content.split(/(?=@(?:param|option|flag)(?:\(['"`][^'"`]*['"`]\))?(?:\s|$))/);
|
|
187
|
+
|
|
188
|
+
for (const block of propertyBlocks) {
|
|
189
|
+
if (!block.trim()) continue;
|
|
190
|
+
|
|
191
|
+
// Extract decorator type - look for the first occurrence at the start of the block
|
|
192
|
+
const decoratorMatch = block.match(/^@(param|option|flag)(?:\(['"`]([^'"`]*)['"`]\))?/);
|
|
193
|
+
if (!decoratorMatch) continue;
|
|
194
|
+
|
|
195
|
+
const [, decoratorType, shortFlag] = decoratorMatch;
|
|
196
|
+
|
|
197
|
+
// Check for @optional
|
|
198
|
+
const isOptional = block.includes('@optional');
|
|
199
|
+
|
|
200
|
+
// Extract help text
|
|
201
|
+
const helpMatch = block.match(/@help\(['"`]([^'"`]+)['"`]\)/);
|
|
202
|
+
const help = helpMatch ? helpMatch[1] : undefined;
|
|
203
|
+
|
|
204
|
+
// Extract property name
|
|
205
|
+
const propMatch = block.match(/(private|public)\s+(\w+)(?:\?)?(?:!)?:\s*/);
|
|
206
|
+
if (!propMatch) continue;
|
|
207
|
+
|
|
208
|
+
const [, , propertyName] = propMatch;
|
|
209
|
+
|
|
210
|
+
parameters.push({
|
|
211
|
+
name: propertyName,
|
|
212
|
+
type: decoratorType as 'param' | 'option' | 'flag',
|
|
213
|
+
required: !isOptional && decoratorType !== 'flag',
|
|
214
|
+
help,
|
|
215
|
+
shortFlag
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return parameters;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Determine if a command is read-only based on method name, help text, and content analysis
|
|
224
|
+
*/
|
|
225
|
+
private isReadOnlyCommand(methodName: string, help?: string, content?: string): boolean {
|
|
226
|
+
// Check method name against read-only patterns
|
|
227
|
+
if (this.readOnlyPatterns.some(pattern => pattern.test(methodName))) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Check method name against state-changing patterns
|
|
232
|
+
if (this.stateChangingPatterns.some(pattern => pattern.test(methodName))) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check help text for read-only indicators
|
|
237
|
+
if (help) {
|
|
238
|
+
const readOnlyHelpPatterns = [
|
|
239
|
+
/^(list|show|display|get|view|info|status)/i,
|
|
240
|
+
/information/i,
|
|
241
|
+
/details/i
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
const stateChangingHelpPatterns = [
|
|
245
|
+
/^(create|add|update|delete|remove|set|install|publish)/i,
|
|
246
|
+
/modify/i,
|
|
247
|
+
/change/i
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
if (readOnlyHelpPatterns.some(pattern => pattern.test(help))) {
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (stateChangingHelpPatterns.some(pattern => pattern.test(help))) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Analyze content for API calls that suggest read-only vs state-changing
|
|
260
|
+
if (content) {
|
|
261
|
+
// Look for HTTP methods or API calls that suggest read-only operations
|
|
262
|
+
const readOnlyContentPatterns = [
|
|
263
|
+
/\.get\(/i,
|
|
264
|
+
/\.search\(/i,
|
|
265
|
+
/\.whoami\(/i,
|
|
266
|
+
/\.list\(/i,
|
|
267
|
+
/\.info\(/i,
|
|
268
|
+
/\.status\(/i
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
const stateChangingContentPatterns = [
|
|
272
|
+
/\.post\(/i,
|
|
273
|
+
/\.put\(/i,
|
|
274
|
+
/\.patch\(/i,
|
|
275
|
+
/\.delete\(/i,
|
|
276
|
+
/\.create\(/i,
|
|
277
|
+
/\.update\(/i,
|
|
278
|
+
/\.remove\(/i,
|
|
279
|
+
/\.install\(/i,
|
|
280
|
+
/\.publish\(/i
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
if (readOnlyContentPatterns.some(pattern => pattern.test(content))) {
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (stateChangingContentPatterns.some(pattern => pattern.test(content))) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Default to read-only if uncertain (safer for testing)
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Generate a full command signature for CLI execution
|
|
298
|
+
*/
|
|
299
|
+
public generateCommandSignature(command: CommandMetadata): string {
|
|
300
|
+
let signature = `${command.namespace} ${command.commandName}`;
|
|
301
|
+
|
|
302
|
+
// Add required parameters
|
|
303
|
+
const requiredParams = command.parameters.filter(p => p.required && p.type === 'param');
|
|
304
|
+
for (const param of requiredParams) {
|
|
305
|
+
signature += ` <${param.name}>`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Add optional parameters
|
|
309
|
+
const optionalParams = command.parameters.filter(p => !p.required && p.type === 'param');
|
|
310
|
+
for (const param of optionalParams) {
|
|
311
|
+
signature += ` [${param.name}]`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Add options and flags
|
|
315
|
+
const options = command.parameters.filter(p => p.type === 'option');
|
|
316
|
+
const flags = command.parameters.filter(p => p.type === 'flag');
|
|
317
|
+
|
|
318
|
+
if (options.length > 0 || flags.length > 0) {
|
|
319
|
+
signature += ' [OPTIONS]';
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return signature;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple baseline normalization utility
|
|
3
|
+
* Replaces dynamic values with placeholders for consistent baseline comparison
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class BaselineNormalizer {
|
|
7
|
+
/**
|
|
8
|
+
* Normalize CLI output by replacing dynamic values with placeholders
|
|
9
|
+
*/
|
|
10
|
+
static normalize(output: string): string {
|
|
11
|
+
let result = output;
|
|
12
|
+
|
|
13
|
+
// Replace deprecation warnings first
|
|
14
|
+
result = result.replace(/\(node:\d+\) \[DEP\d+\] DeprecationWarning: [^\n]+\n/g, '');
|
|
15
|
+
result = result.replace(/\(Use `node --trace-deprecation[^\n]+\n/g, '');
|
|
16
|
+
|
|
17
|
+
// Replace environment
|
|
18
|
+
result = result.replace(/Active environment: \w+/g, 'Active environment: <ENVIRONMENT>');
|
|
19
|
+
|
|
20
|
+
// Replace user-specific data
|
|
21
|
+
result = result.replace(/id: [a-f0-9-]{36}/g, 'id: <USER_ID>');
|
|
22
|
+
result = result.replace(/email: [^\s]+@[^\s]+/g, 'email: <USER_EMAIL>');
|
|
23
|
+
result = result.replace(/role: [^\n]+/g, 'role: <ROLE>');
|
|
24
|
+
result = result.replace(/githubUsername: [^\s]+/g, 'githubUsername: <GITHUB_USERNAME>');
|
|
25
|
+
result = result.replace(/createdAt: (?:'[^']+'|null)/g, "createdAt: <CREATED_DATE>");
|
|
26
|
+
result = result.replace(/vendor: [^\n]+/g, 'vendor: <VENDOR>');
|
|
27
|
+
|
|
28
|
+
// Replace vendor_apps - keep structure but normalize to single entry
|
|
29
|
+
result = result.replace(/vendor_apps:\s*\n(?: - id: [^\n]+\n?)+/g, 'vendor_apps:\n - id: <APP_ID>');
|
|
30
|
+
|
|
31
|
+
// Replace timestamps first (before other patterns)
|
|
32
|
+
result = result.replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z/g, '<TIMESTAMP>');
|
|
33
|
+
|
|
34
|
+
// Replace job IDs (UUIDs)
|
|
35
|
+
result = result.replace(/[a-f0-9-]{36}/g, '<JOB_ID>');
|
|
36
|
+
|
|
37
|
+
// Replace version numbers (semantic versions including dev/beta/alpha)
|
|
38
|
+
result = result.replace(/\d+\.\d+\.\d+(?:-(?:dev|beta|alpha)\.\d+)?/g, '<VERSION>');
|
|
39
|
+
|
|
40
|
+
// Replace whois table data (normalize account info in table format)
|
|
41
|
+
// Match lines that start with digits followed by spaces (account ID)
|
|
42
|
+
result = result.replace(/^(\d+)(\s+)([A-Za-z0-9_-]+)(\s+)(.+?)(\s+)(\w+)$/gm,
|
|
43
|
+
'<ACCOUNT_ID>$2<TRACKER_ID>$4<ACCOUNT_NAME>$6<TARGET_PRODUCT>');
|
|
44
|
+
|
|
45
|
+
// Replace directory info fields
|
|
46
|
+
result = result.replace(/name\s+(.+)/g, 'name\t<APP_NAME>');
|
|
47
|
+
result = result.replace(/id\s+([a-zA-Z0-9_-]+)/g, 'id\t\t<APP_ID>');
|
|
48
|
+
result = result.replace(/created\s+<TIMESTAMP>/g, 'created\t<TIMESTAMP>');
|
|
49
|
+
|
|
50
|
+
// Replace directory list table data (preserve table structure)
|
|
51
|
+
// Only replace app IDs at start of lines that look like directory entries
|
|
52
|
+
result = result.replace(/^([a-zA-Z0-9_-]+)(\s+<VERSION>\s+)/gm, '<APP_ID>$2');
|
|
53
|
+
|
|
54
|
+
// Replace states and products
|
|
55
|
+
result = result.replace(/(PUBLISHED|UNPUBLISHED|BUILT|BUILD_FAILED|BUILDING)/g, '<STATE>');
|
|
56
|
+
result = result.replace(/(ODP|OCP|ZAIUS)(?=\s|$)/g, '<TARGET_PRODUCT>');
|
|
57
|
+
result = result.replace(/(NOT_REQUIRED|REQUIRED)/g, '<REQUIREMENT>');
|
|
58
|
+
|
|
59
|
+
// Replace any remaining tracker IDs that look like Base64
|
|
60
|
+
result = result.replace(/[A-Za-z0-9_-]{22}/g, '<TRACKER_ID>');
|
|
61
|
+
|
|
62
|
+
// Replace job statuses and review statuses
|
|
63
|
+
result = result.replace(/(COMPLETE|RUNNING|FAILED|PENDING|SUCCESS|ERROR|IN_REVIEW|APPROVED|REJECTED)/g, '<STATUS>');
|
|
64
|
+
|
|
65
|
+
// Replace function names (words before tracker IDs in job tables)
|
|
66
|
+
result = result.replace(/\w+(?=\s+<TRACKER_ID>)/g, '<FUNCTION>');
|
|
67
|
+
|
|
68
|
+
// Replace durations
|
|
69
|
+
result = result.replace(/\d+ms/g, '<DURATION>');
|
|
70
|
+
result = result.replace(/\d+s/g, '<DURATION>');
|
|
71
|
+
result = result.replace(/Duration: \d+/g, 'Duration: <DURATION>');
|
|
72
|
+
|
|
73
|
+
// Normalize whitespace
|
|
74
|
+
result = result.replace(/\s+$/gm, ''); // Remove trailing whitespace
|
|
75
|
+
result = result.replace(/\n{3,}/g, '\n\n'); // Normalize multiple newlines
|
|
76
|
+
|
|
77
|
+
return result.trim();
|
|
78
|
+
}
|
|
79
|
+
}
|