@dependabit/action 0.1.1
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/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +225 -0
- package/action.yml +85 -0
- package/dist/actions/check.d.ts +33 -0
- package/dist/actions/check.d.ts.map +1 -0
- package/dist/actions/check.js +162 -0
- package/dist/actions/check.js.map +1 -0
- package/dist/actions/generate.d.ts +9 -0
- package/dist/actions/generate.d.ts.map +1 -0
- package/dist/actions/generate.js +152 -0
- package/dist/actions/generate.js.map +1 -0
- package/dist/actions/update.d.ts +9 -0
- package/dist/actions/update.d.ts.map +1 -0
- package/dist/actions/update.js +246 -0
- package/dist/actions/update.js.map +1 -0
- package/dist/actions/validate.d.ts +33 -0
- package/dist/actions/validate.d.ts.map +1 -0
- package/dist/actions/validate.js +226 -0
- package/dist/actions/validate.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +114 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +154 -0
- package/dist/logger.js.map +1 -0
- package/dist/utils/agent-config.d.ts +31 -0
- package/dist/utils/agent-config.d.ts.map +1 -0
- package/dist/utils/agent-config.js +42 -0
- package/dist/utils/agent-config.js.map +1 -0
- package/dist/utils/agent-router.d.ts +33 -0
- package/dist/utils/agent-router.d.ts.map +1 -0
- package/dist/utils/agent-router.js +57 -0
- package/dist/utils/agent-router.js.map +1 -0
- package/dist/utils/errors.d.ts +51 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +219 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/inputs.d.ts +35 -0
- package/dist/utils/inputs.d.ts.map +1 -0
- package/dist/utils/inputs.js +47 -0
- package/dist/utils/inputs.js.map +1 -0
- package/dist/utils/metrics.d.ts +66 -0
- package/dist/utils/metrics.d.ts.map +1 -0
- package/dist/utils/metrics.js +116 -0
- package/dist/utils/metrics.js.map +1 -0
- package/dist/utils/outputs.d.ts +43 -0
- package/dist/utils/outputs.d.ts.map +1 -0
- package/dist/utils/outputs.js +146 -0
- package/dist/utils/outputs.js.map +1 -0
- package/dist/utils/performance.d.ts +100 -0
- package/dist/utils/performance.d.ts.map +1 -0
- package/dist/utils/performance.js +185 -0
- package/dist/utils/performance.js.map +1 -0
- package/dist/utils/reporter.d.ts +43 -0
- package/dist/utils/reporter.d.ts.map +1 -0
- package/dist/utils/reporter.js +122 -0
- package/dist/utils/reporter.js.map +1 -0
- package/dist/utils/secrets.d.ts +45 -0
- package/dist/utils/secrets.d.ts.map +1 -0
- package/dist/utils/secrets.js +94 -0
- package/dist/utils/secrets.js.map +1 -0
- package/package.json +45 -0
- package/src/actions/check.ts +223 -0
- package/src/actions/generate.ts +181 -0
- package/src/actions/update.ts +284 -0
- package/src/actions/validate.ts +292 -0
- package/src/index.ts +43 -0
- package/src/logger.test.ts +200 -0
- package/src/logger.ts +210 -0
- package/src/utils/agent-config.ts +61 -0
- package/src/utils/agent-router.ts +67 -0
- package/src/utils/errors.ts +251 -0
- package/src/utils/inputs.ts +75 -0
- package/src/utils/metrics.ts +169 -0
- package/src/utils/outputs.ts +202 -0
- package/src/utils/performance.ts +248 -0
- package/src/utils/reporter.ts +169 -0
- package/src/utils/secrets.ts +124 -0
- package/test/actions/check.test.ts +216 -0
- package/test/actions/generate.test.ts +82 -0
- package/test/actions/update.test.ts +70 -0
- package/test/actions/validate.test.ts +257 -0
- package/test/utils/agent-config.test.ts +112 -0
- package/test/utils/agent-router.test.ts +129 -0
- package/test/utils/metrics.test.ts +221 -0
- package/test/utils/reporter.test.ts +196 -0
- package/test/utils/secrets.test.ts +217 -0
- package/tsconfig.json +15 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readManifest,
|
|
3
|
+
validateManifest,
|
|
4
|
+
readConfig,
|
|
5
|
+
type DependencyManifest,
|
|
6
|
+
type DependabitConfig
|
|
7
|
+
} from '@dependabit/manifest';
|
|
8
|
+
import * as core from '@actions/core';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validation result
|
|
12
|
+
*/
|
|
13
|
+
export interface ValidationResult {
|
|
14
|
+
valid: boolean;
|
|
15
|
+
errors: string[];
|
|
16
|
+
warnings: string[];
|
|
17
|
+
manifest?: DependencyManifest;
|
|
18
|
+
config: DependabitConfig | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Main entry point for the validate action wrapped for error handling
|
|
23
|
+
*/
|
|
24
|
+
export async function run(): Promise<void> {
|
|
25
|
+
try {
|
|
26
|
+
const manifestPath = core.getInput('manifest_path') || '.dependabit/manifest.json';
|
|
27
|
+
const configPath = core.getInput('config_path') || '';
|
|
28
|
+
|
|
29
|
+
console.log('Starting validation...');
|
|
30
|
+
const result = await validateAction(manifestPath, configPath || undefined);
|
|
31
|
+
|
|
32
|
+
// Output the formatted result
|
|
33
|
+
const formatted = formatValidationErrors(result);
|
|
34
|
+
console.log('\n' + formatted);
|
|
35
|
+
|
|
36
|
+
if (!result.valid) {
|
|
37
|
+
core.setFailed(`Validation failed with ${result.errors.length} errors`);
|
|
38
|
+
} else {
|
|
39
|
+
core.setOutput('valid', 'true');
|
|
40
|
+
core.setOutput('errors', JSON.stringify(result.errors));
|
|
41
|
+
core.setOutput('warnings', JSON.stringify(result.warnings));
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
core.setFailed(error instanceof Error ? error.message : String(error));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Validate manifest file with comprehensive checks
|
|
50
|
+
*
|
|
51
|
+
* Performs:
|
|
52
|
+
* - Schema validation (Zod)
|
|
53
|
+
* - Business rule validation (duplicate IDs, valid URLs, timestamp order)
|
|
54
|
+
* - Optional config validation
|
|
55
|
+
*
|
|
56
|
+
* @param manifestPath Path to manifest.json
|
|
57
|
+
* @param configPath Optional path to config.yml
|
|
58
|
+
* @returns Validation result with errors and warnings
|
|
59
|
+
*/
|
|
60
|
+
export async function validateAction(
|
|
61
|
+
manifestPath: string,
|
|
62
|
+
configPath?: string
|
|
63
|
+
): Promise<ValidationResult> {
|
|
64
|
+
const errors: string[] = [];
|
|
65
|
+
const warnings: string[] = [];
|
|
66
|
+
let manifest: DependencyManifest | undefined;
|
|
67
|
+
let config: DependabitConfig | undefined;
|
|
68
|
+
|
|
69
|
+
console.log(`Validating manifest: ${manifestPath}`);
|
|
70
|
+
|
|
71
|
+
// Step 1: Read and validate manifest schema
|
|
72
|
+
try {
|
|
73
|
+
const rawManifest = await readManifest(manifestPath);
|
|
74
|
+
manifest = validateManifest(rawManifest);
|
|
75
|
+
console.log(`✓ Schema validation passed`);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
78
|
+
errors.push(`Schema validation failed: ${message}`);
|
|
79
|
+
console.error(`✗ Schema validation failed: ${message}`);
|
|
80
|
+
return { valid: false, errors, warnings, config: undefined };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Step 2: Validate business rules
|
|
84
|
+
validateBusinessRules(manifest, errors, warnings);
|
|
85
|
+
|
|
86
|
+
// Step 3: Read and validate config if provided
|
|
87
|
+
if (configPath) {
|
|
88
|
+
try {
|
|
89
|
+
config = await readConfig(configPath);
|
|
90
|
+
console.log(`✓ Config validation passed: ${configPath}`);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
93
|
+
errors.push(`Config validation failed: ${message}`);
|
|
94
|
+
console.error(`✗ Config validation failed: ${message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Step 4: Cross-validate manifest and config
|
|
99
|
+
if (manifest && config) {
|
|
100
|
+
validateManifestConfigCompatibility(manifest, config, errors, warnings);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const valid = errors.length === 0;
|
|
104
|
+
|
|
105
|
+
if (valid) {
|
|
106
|
+
console.log(`✓ Validation passed with ${warnings.length} warnings`);
|
|
107
|
+
} else {
|
|
108
|
+
console.error(`✗ Validation failed with ${errors.length} errors`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { valid, errors, warnings, manifest, config: config || undefined };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Validate business rules
|
|
116
|
+
*/
|
|
117
|
+
function validateBusinessRules(
|
|
118
|
+
manifest: DependencyManifest,
|
|
119
|
+
errors: string[],
|
|
120
|
+
warnings: string[]
|
|
121
|
+
): void {
|
|
122
|
+
// Check for duplicate dependency IDs
|
|
123
|
+
const ids = new Set<string>();
|
|
124
|
+
const duplicateIds: string[] = [];
|
|
125
|
+
|
|
126
|
+
for (const dep of manifest.dependencies) {
|
|
127
|
+
if (ids.has(dep.id)) {
|
|
128
|
+
duplicateIds.push(dep.id);
|
|
129
|
+
}
|
|
130
|
+
ids.add(dep.id);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (duplicateIds.length > 0) {
|
|
134
|
+
errors.push(`Duplicate dependency IDs found: ${duplicateIds.join(', ')}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check for duplicate URLs
|
|
138
|
+
const urls = new Set<string>();
|
|
139
|
+
const duplicateUrls: string[] = [];
|
|
140
|
+
|
|
141
|
+
for (const dep of manifest.dependencies) {
|
|
142
|
+
if (urls.has(dep.url)) {
|
|
143
|
+
duplicateUrls.push(dep.url);
|
|
144
|
+
}
|
|
145
|
+
urls.add(dep.url);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (duplicateUrls.length > 0) {
|
|
149
|
+
warnings.push(`Duplicate URLs found: ${duplicateUrls.join(', ')}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Validate URL formats explicitly
|
|
153
|
+
for (const dep of manifest.dependencies) {
|
|
154
|
+
try {
|
|
155
|
+
new URL(dep.url);
|
|
156
|
+
} catch {
|
|
157
|
+
errors.push(`Dependency ${dep.name}: invalid URL format: ${dep.url}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Validate timestamps order
|
|
162
|
+
for (const dep of manifest.dependencies) {
|
|
163
|
+
const detectedAt = new Date(dep.detectedAt);
|
|
164
|
+
const lastChecked = new Date(dep.lastChecked);
|
|
165
|
+
|
|
166
|
+
if (lastChecked < detectedAt) {
|
|
167
|
+
errors.push(
|
|
168
|
+
`Dependency ${dep.name}: lastChecked (${dep.lastChecked}) is before detectedAt (${dep.detectedAt})`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (dep.lastChanged) {
|
|
173
|
+
const lastChanged = new Date(dep.lastChanged);
|
|
174
|
+
if (lastChanged < detectedAt) {
|
|
175
|
+
errors.push(
|
|
176
|
+
`Dependency ${dep.name}: lastChanged (${dep.lastChanged}) is before detectedAt (${dep.detectedAt})`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Validate statistics consistency
|
|
183
|
+
if (manifest.statistics.totalDependencies !== manifest.dependencies.length) {
|
|
184
|
+
errors.push(
|
|
185
|
+
`Statistics mismatch: totalDependencies (${manifest.statistics.totalDependencies}) != actual count (${manifest.dependencies.length})`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Validate referenced files exist (warning only)
|
|
190
|
+
const missingReferences: string[] = [];
|
|
191
|
+
for (const dep of manifest.dependencies) {
|
|
192
|
+
for (const ref of dep.referencedIn) {
|
|
193
|
+
// This is a warning because files might have been deleted
|
|
194
|
+
// We don't fail validation for this
|
|
195
|
+
if (!ref.file) {
|
|
196
|
+
missingReferences.push(`Dependency ${dep.name} has empty file reference`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (missingReferences.length > 0) {
|
|
202
|
+
warnings.push(...missingReferences);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check for low confidence detections
|
|
206
|
+
const lowConfidence = manifest.dependencies.filter((dep) => dep.detectionConfidence < 0.5);
|
|
207
|
+
|
|
208
|
+
if (lowConfidence.length > 0) {
|
|
209
|
+
warnings.push(`${lowConfidence.length} dependencies have low detection confidence (<0.5)`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Check manifest size
|
|
213
|
+
const manifestSize = JSON.stringify(manifest).length;
|
|
214
|
+
const sizeInMB = manifestSize / (1024 * 1024);
|
|
215
|
+
|
|
216
|
+
if (sizeInMB > 10) {
|
|
217
|
+
errors.push(`Manifest size (${sizeInMB.toFixed(2)}MB) exceeds maximum (10MB)`);
|
|
218
|
+
} else if (sizeInMB > 5) {
|
|
219
|
+
warnings.push(`Manifest size (${sizeInMB.toFixed(2)}MB) is large (target: <1MB, warn: >5MB)`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
console.log(`✓ Business rule validation completed`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Validate manifest and config compatibility
|
|
227
|
+
*/
|
|
228
|
+
function validateManifestConfigCompatibility(
|
|
229
|
+
manifest: DependencyManifest,
|
|
230
|
+
config: DependabitConfig,
|
|
231
|
+
errors: string[],
|
|
232
|
+
warnings: string[]
|
|
233
|
+
): void {
|
|
234
|
+
// Check that dependency overrides reference valid dependencies
|
|
235
|
+
if (config.dependencies) {
|
|
236
|
+
for (const override of config.dependencies) {
|
|
237
|
+
const found = manifest.dependencies.some((dep) => dep.url === override.url);
|
|
238
|
+
if (!found) {
|
|
239
|
+
warnings.push(
|
|
240
|
+
`Config override for ${override.url} does not match any dependency in manifest`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Check that ignored URLs are not in manifest (warning)
|
|
247
|
+
if (config.ignore?.urls) {
|
|
248
|
+
for (const ignoredUrl of config.ignore.urls) {
|
|
249
|
+
const found = manifest.dependencies.some((dep) => dep.url === ignoredUrl);
|
|
250
|
+
if (found) {
|
|
251
|
+
warnings.push(`Dependency ${ignoredUrl} is in manifest but marked as ignored in config`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
console.log(`✓ Cross-validation completed`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Format validation errors for CLI output
|
|
261
|
+
*/
|
|
262
|
+
export function formatValidationErrors(result: ValidationResult): string {
|
|
263
|
+
const lines: string[] = [];
|
|
264
|
+
|
|
265
|
+
if (result.valid) {
|
|
266
|
+
lines.push('✓ Validation passed');
|
|
267
|
+
if (result.warnings.length > 0) {
|
|
268
|
+
lines.push('');
|
|
269
|
+
lines.push(`Warnings (${result.warnings.length}):`);
|
|
270
|
+
for (const warning of result.warnings) {
|
|
271
|
+
lines.push(` ⚠ ${warning}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
lines.push('✗ Validation failed');
|
|
276
|
+
lines.push('');
|
|
277
|
+
lines.push(`Errors (${result.errors.length}):`);
|
|
278
|
+
for (const error of result.errors) {
|
|
279
|
+
lines.push(` ✗ ${error}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (result.warnings.length > 0) {
|
|
283
|
+
lines.push('');
|
|
284
|
+
lines.push(`Warnings (${result.warnings.length}):`);
|
|
285
|
+
for (const warning of result.warnings) {
|
|
286
|
+
lines.push(` ⚠ ${warning}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return lines.join('\n');
|
|
292
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry point for @dependabit/action
|
|
3
|
+
* Routes to the appropriate action based on input
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as core from '@actions/core';
|
|
7
|
+
import { run as runGenerate } from './actions/generate.js';
|
|
8
|
+
import { run as runUpdate } from './actions/update.js';
|
|
9
|
+
import { run as runValidate } from './actions/validate.js';
|
|
10
|
+
|
|
11
|
+
async function main(): Promise<void> {
|
|
12
|
+
const action = core.getInput('action') || 'generate';
|
|
13
|
+
|
|
14
|
+
switch (action) {
|
|
15
|
+
case 'generate':
|
|
16
|
+
await runGenerate();
|
|
17
|
+
break;
|
|
18
|
+
|
|
19
|
+
case 'update':
|
|
20
|
+
await runUpdate();
|
|
21
|
+
break;
|
|
22
|
+
|
|
23
|
+
case 'check':
|
|
24
|
+
core.setFailed('Check action not yet implemented');
|
|
25
|
+
break;
|
|
26
|
+
|
|
27
|
+
case 'validate':
|
|
28
|
+
await runValidate();
|
|
29
|
+
break;
|
|
30
|
+
|
|
31
|
+
default:
|
|
32
|
+
core.setFailed(`Unknown action: ${action}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Run the action
|
|
37
|
+
main().catch((error) => {
|
|
38
|
+
core.setFailed(error instanceof Error ? error.message : String(error));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Export for testing
|
|
42
|
+
export { main };
|
|
43
|
+
export * from './logger.js';
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import * as core from '@actions/core';
|
|
3
|
+
import { Logger, createLogger, withTiming } from '../src/logger.js';
|
|
4
|
+
|
|
5
|
+
// Mock @actions/core
|
|
6
|
+
vi.mock('@actions/core', () => ({
|
|
7
|
+
debug: vi.fn(),
|
|
8
|
+
info: vi.fn(),
|
|
9
|
+
warning: vi.fn(),
|
|
10
|
+
error: vi.fn(),
|
|
11
|
+
startGroup: vi.fn(),
|
|
12
|
+
endGroup: vi.fn()
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
describe('Logger Tests', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.clearAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('Logger', () => {
|
|
21
|
+
it('should create logger with auto-generated correlation ID', () => {
|
|
22
|
+
const logger = new Logger();
|
|
23
|
+
expect(logger.getCorrelationId()).toBeDefined();
|
|
24
|
+
expect(logger.getCorrelationId()).toMatch(/^\d+-[a-z0-9]+$/);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should create logger with custom correlation ID', () => {
|
|
28
|
+
const logger = new Logger({ correlationId: 'test-123' });
|
|
29
|
+
expect(logger.getCorrelationId()).toBe('test-123');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should log info messages with JSON formatting', () => {
|
|
33
|
+
const logger = new Logger({ correlationId: 'test-123' });
|
|
34
|
+
logger.info('Test message', { key: 'value' });
|
|
35
|
+
|
|
36
|
+
expect(core.info).toHaveBeenCalledWith(expect.stringContaining('"level":"info"'));
|
|
37
|
+
expect(core.info).toHaveBeenCalledWith(expect.stringContaining('"message":"Test message"'));
|
|
38
|
+
expect(core.info).toHaveBeenCalledWith(expect.stringContaining('"correlationId":"test-123"'));
|
|
39
|
+
expect(core.info).toHaveBeenCalledWith(expect.stringContaining('"key":"value"'));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should log warning messages', () => {
|
|
43
|
+
const logger = new Logger();
|
|
44
|
+
logger.warning('Warning message');
|
|
45
|
+
|
|
46
|
+
expect(core.warning).toHaveBeenCalledWith(expect.stringContaining('"level":"warning"'));
|
|
47
|
+
expect(core.warning).toHaveBeenCalledWith(
|
|
48
|
+
expect.stringContaining('"message":"Warning message"')
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should log error messages', () => {
|
|
53
|
+
const logger = new Logger();
|
|
54
|
+
logger.error('Error message');
|
|
55
|
+
|
|
56
|
+
expect(core.error).toHaveBeenCalledWith(expect.stringContaining('"level":"error"'));
|
|
57
|
+
expect(core.error).toHaveBeenCalledWith(expect.stringContaining('"message":"Error message"'));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should only log debug when enabled', () => {
|
|
61
|
+
const logger1 = new Logger({ enableDebug: false });
|
|
62
|
+
logger1.debug('Debug message');
|
|
63
|
+
expect(core.debug).not.toHaveBeenCalled();
|
|
64
|
+
|
|
65
|
+
const logger2 = new Logger({ enableDebug: true });
|
|
66
|
+
logger2.debug('Debug message');
|
|
67
|
+
expect(core.debug).toHaveBeenCalledWith(expect.stringContaining('"level":"debug"'));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should start and end log groups', () => {
|
|
71
|
+
const logger = new Logger();
|
|
72
|
+
logger.startGroup('Test Group');
|
|
73
|
+
expect(core.startGroup).toHaveBeenCalledWith('Test Group');
|
|
74
|
+
|
|
75
|
+
logger.endGroup();
|
|
76
|
+
expect(core.endGroup).toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should create child logger with same correlation ID', () => {
|
|
80
|
+
const parent = new Logger({ correlationId: 'parent-123' });
|
|
81
|
+
const child = parent.child();
|
|
82
|
+
|
|
83
|
+
expect(child.getCorrelationId()).toBe('parent-123');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should create child logger with merged context', () => {
|
|
87
|
+
const parent = new Logger({
|
|
88
|
+
correlationId: 'parent-123',
|
|
89
|
+
context: { service: 'api' }
|
|
90
|
+
});
|
|
91
|
+
const child = parent.child({ operation: 'fetch' });
|
|
92
|
+
|
|
93
|
+
child.info('Test message');
|
|
94
|
+
|
|
95
|
+
expect(core.info).toHaveBeenCalledWith(expect.stringContaining('"service":"api"'));
|
|
96
|
+
expect(core.info).toHaveBeenCalledWith(expect.stringContaining('"operation":"fetch"'));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should log LLM interactions', () => {
|
|
100
|
+
const logger = new Logger();
|
|
101
|
+
logger.logLLMInteraction({
|
|
102
|
+
provider: 'github-copilot',
|
|
103
|
+
model: 'gpt-4',
|
|
104
|
+
prompt: 'Test prompt',
|
|
105
|
+
response: 'Test response',
|
|
106
|
+
tokens: 100,
|
|
107
|
+
latencyMs: 500
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(core.info).toHaveBeenCalledWith(expect.stringContaining('"type":"llm_interaction"'));
|
|
111
|
+
expect(core.info).toHaveBeenCalledWith(
|
|
112
|
+
expect.stringContaining('"provider":"github-copilot"')
|
|
113
|
+
);
|
|
114
|
+
expect(core.info).toHaveBeenCalledWith(expect.stringContaining('"tokens":100'));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should log API calls', () => {
|
|
118
|
+
const logger = new Logger();
|
|
119
|
+
logger.logAPICall({
|
|
120
|
+
endpoint: '/repos/owner/repo',
|
|
121
|
+
method: 'GET',
|
|
122
|
+
statusCode: 200,
|
|
123
|
+
latencyMs: 300,
|
|
124
|
+
rateLimit: {
|
|
125
|
+
remaining: 4999,
|
|
126
|
+
limit: 5000,
|
|
127
|
+
reset: 1609459200
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(core.info).toHaveBeenCalledWith(expect.stringContaining('"type":"api_call"'));
|
|
132
|
+
expect(core.info).toHaveBeenCalledWith(
|
|
133
|
+
expect.stringContaining('"endpoint":"/repos/owner/repo"')
|
|
134
|
+
);
|
|
135
|
+
expect(core.info).toHaveBeenCalledWith(expect.stringContaining('"statusCode":200'));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should log operation duration', () => {
|
|
139
|
+
const logger = new Logger();
|
|
140
|
+
logger.logDuration('test-operation', 1234, { result: 'success' });
|
|
141
|
+
|
|
142
|
+
expect(core.info).toHaveBeenCalledWith(
|
|
143
|
+
expect.stringContaining('"type":"operation_duration"')
|
|
144
|
+
);
|
|
145
|
+
expect(core.info).toHaveBeenCalledWith(
|
|
146
|
+
expect.stringContaining('"operation":"test-operation"')
|
|
147
|
+
);
|
|
148
|
+
expect(core.info).toHaveBeenCalledWith(expect.stringContaining('"durationMs":1234'));
|
|
149
|
+
expect(core.info).toHaveBeenCalledWith(expect.stringContaining('"result":"success"'));
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('createLogger', () => {
|
|
154
|
+
it('should create a logger instance', () => {
|
|
155
|
+
const logger = createLogger();
|
|
156
|
+
expect(logger).toBeInstanceOf(Logger);
|
|
157
|
+
expect(logger.getCorrelationId()).toBeDefined();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should accept configuration', () => {
|
|
161
|
+
const logger = createLogger({ correlationId: 'custom-id' });
|
|
162
|
+
expect(logger.getCorrelationId()).toBe('custom-id');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('withTiming', () => {
|
|
167
|
+
it('should measure and log successful operation', async () => {
|
|
168
|
+
const logger = new Logger();
|
|
169
|
+
const fn = vi.fn(async () => {
|
|
170
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
171
|
+
return 'result';
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const result = await withTiming(logger, 'test-operation', fn);
|
|
175
|
+
|
|
176
|
+
expect(result).toBe('result');
|
|
177
|
+
expect(fn).toHaveBeenCalled();
|
|
178
|
+
expect(core.info).toHaveBeenCalledWith(
|
|
179
|
+
expect.stringContaining('"operation":"test-operation"')
|
|
180
|
+
);
|
|
181
|
+
expect(core.info).toHaveBeenCalledWith(expect.stringContaining('"durationMs":'));
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should measure and log failed operation', async () => {
|
|
185
|
+
const logger = new Logger();
|
|
186
|
+
const error = new Error('Test error');
|
|
187
|
+
const fn = vi.fn(async () => {
|
|
188
|
+
throw error;
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await expect(withTiming(logger, 'test-operation', fn)).rejects.toThrow('Test error');
|
|
192
|
+
|
|
193
|
+
expect(fn).toHaveBeenCalled();
|
|
194
|
+
expect(core.info).toHaveBeenCalledWith(
|
|
195
|
+
expect.stringContaining('"operation":"test-operation"')
|
|
196
|
+
);
|
|
197
|
+
expect(core.info).toHaveBeenCalledWith(expect.stringContaining('"error":"Test error"'));
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|