@grunnverk/kilde 0.1.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/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +31 -0
- package/.github/pull_request_template.md +48 -0
- package/.github/workflows/deploy-docs.yml +59 -0
- package/.github/workflows/npm-publish.yml +48 -0
- package/.github/workflows/test.yml +48 -0
- package/CHANGELOG.md +92 -0
- package/CONTRIBUTING.md +438 -0
- package/LICENSE +190 -0
- package/PROJECT_SUMMARY.md +318 -0
- package/README.md +444 -0
- package/RELEASE_CHECKLIST.md +182 -0
- package/dist/application.js +166 -0
- package/dist/application.js.map +1 -0
- package/dist/commands/release.js +326 -0
- package/dist/commands/release.js.map +1 -0
- package/dist/constants.js +122 -0
- package/dist/constants.js.map +1 -0
- package/dist/logging.js +176 -0
- package/dist/logging.js.map +1 -0
- package/dist/main.js +24 -0
- package/dist/main.js.map +1 -0
- package/dist/mcp-server.js +17467 -0
- package/dist/mcp-server.js.map +7 -0
- package/dist/utils/config.js +89 -0
- package/dist/utils/config.js.map +1 -0
- package/docs/AI_GUIDE.md +618 -0
- package/eslint.config.mjs +85 -0
- package/guide/architecture.md +776 -0
- package/guide/commands.md +580 -0
- package/guide/configuration.md +779 -0
- package/guide/mcp-integration.md +708 -0
- package/guide/overview.md +225 -0
- package/package.json +91 -0
- package/scripts/build-mcp.js +115 -0
- package/scripts/test-mcp-compliance.js +254 -0
- package/src/application.ts +246 -0
- package/src/commands/release.ts +450 -0
- package/src/constants.ts +162 -0
- package/src/logging.ts +210 -0
- package/src/main.ts +25 -0
- package/src/mcp/prompts/index.ts +98 -0
- package/src/mcp/resources.ts +121 -0
- package/src/mcp/server.ts +195 -0
- package/src/mcp/tools.ts +219 -0
- package/src/types.ts +131 -0
- package/src/utils/config.ts +181 -0
- package/tests/application.test.ts +114 -0
- package/tests/commands/commit.test.ts +248 -0
- package/tests/commands/release.test.ts +325 -0
- package/tests/constants.test.ts +118 -0
- package/tests/logging.test.ts +142 -0
- package/tests/mcp/prompts/index.test.ts +202 -0
- package/tests/mcp/resources.test.ts +166 -0
- package/tests/mcp/tools.test.ts +211 -0
- package/tests/utils/config.test.ts +212 -0
- package/tsconfig.json +32 -0
- package/vite.config.ts +107 -0
- package/vitest.config.ts +40 -0
- package/website/index.html +14 -0
- package/website/src/App.css +142 -0
- package/website/src/App.tsx +34 -0
- package/website/src/components/Commands.tsx +182 -0
- package/website/src/components/Configuration.tsx +214 -0
- package/website/src/components/Examples.tsx +234 -0
- package/website/src/components/Footer.css +99 -0
- package/website/src/components/Footer.tsx +93 -0
- package/website/src/components/GettingStarted.tsx +94 -0
- package/website/src/components/Hero.css +95 -0
- package/website/src/components/Hero.tsx +50 -0
- package/website/src/components/Navigation.css +102 -0
- package/website/src/components/Navigation.tsx +57 -0
- package/website/src/index.css +36 -0
- package/website/src/main.tsx +10 -0
- package/website/vite.config.ts +12 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { execute } from '../../src/commands/release';
|
|
3
|
+
import { Config, Log, Diff } from '@grunnverk/core';
|
|
4
|
+
import * as GitTools from '@grunnverk/git-tools';
|
|
5
|
+
|
|
6
|
+
// Mock dependencies
|
|
7
|
+
vi.mock('@grunnverk/git-tools', () => ({
|
|
8
|
+
getDefaultFromRef: vi.fn(),
|
|
9
|
+
getCurrentBranch: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock('@grunnverk/core', async () => {
|
|
13
|
+
const actual = await vi.importActual('@grunnverk/core');
|
|
14
|
+
return {
|
|
15
|
+
...actual,
|
|
16
|
+
Log: {
|
|
17
|
+
create: vi.fn(),
|
|
18
|
+
},
|
|
19
|
+
Diff: {
|
|
20
|
+
create: vi.fn(),
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
vi.mock('@grunnverk/ai-service', () => ({
|
|
26
|
+
runAgenticRelease: vi.fn(),
|
|
27
|
+
getDryRunLogger: vi.fn((isDryRun) => ({
|
|
28
|
+
info: vi.fn(),
|
|
29
|
+
error: vi.fn(),
|
|
30
|
+
debug: vi.fn(),
|
|
31
|
+
warn: vi.fn(),
|
|
32
|
+
})),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
vi.mock('@grunnverk/shared', () => ({
|
|
36
|
+
createStorage: vi.fn(() => ({
|
|
37
|
+
writeFile: vi.fn(),
|
|
38
|
+
readFile: vi.fn(),
|
|
39
|
+
exists: vi.fn(),
|
|
40
|
+
})),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
describe('release command integration', () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
vi.clearAllMocks();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('basic release functionality', () => {
|
|
49
|
+
it('should generate release notes with default config', async () => {
|
|
50
|
+
const mockConfig: Config = {
|
|
51
|
+
configDirectory: '/test/.kilde',
|
|
52
|
+
discoveredConfigDirs: [],
|
|
53
|
+
resolvedConfigDirs: [],
|
|
54
|
+
release: {},
|
|
55
|
+
} as Config;
|
|
56
|
+
|
|
57
|
+
vi.mocked(GitTools.getCurrentBranch).mockResolvedValue('main');
|
|
58
|
+
vi.mocked(GitTools.getDefaultFromRef).mockResolvedValue('v1.0.0');
|
|
59
|
+
|
|
60
|
+
vi.mocked(Log.create).mockResolvedValue({
|
|
61
|
+
toString: () => 'commit log content',
|
|
62
|
+
commits: [],
|
|
63
|
+
} as any);
|
|
64
|
+
|
|
65
|
+
vi.mocked(Diff.create).mockResolvedValue({
|
|
66
|
+
toString: () => 'diff content',
|
|
67
|
+
files: [],
|
|
68
|
+
} as any);
|
|
69
|
+
|
|
70
|
+
const { runAgenticRelease } = await import('@grunnverk/ai-service');
|
|
71
|
+
vi.mocked(runAgenticRelease).mockResolvedValue({
|
|
72
|
+
title: 'Release v2.0.0',
|
|
73
|
+
body: 'Release notes content',
|
|
74
|
+
} as any);
|
|
75
|
+
|
|
76
|
+
const result = await execute(mockConfig);
|
|
77
|
+
|
|
78
|
+
expect(result).toEqual({
|
|
79
|
+
title: 'Release v2.0.0',
|
|
80
|
+
body: 'Release notes content',
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should use specified tag range', async () => {
|
|
85
|
+
const mockConfig: Config = {
|
|
86
|
+
configDirectory: '/test/.kilde',
|
|
87
|
+
discoveredConfigDirs: [],
|
|
88
|
+
resolvedConfigDirs: [],
|
|
89
|
+
release: {
|
|
90
|
+
from: 'v1.0.0',
|
|
91
|
+
to: 'v2.0.0',
|
|
92
|
+
},
|
|
93
|
+
} as Config;
|
|
94
|
+
|
|
95
|
+
vi.mocked(GitTools.getCurrentBranch).mockResolvedValue('main');
|
|
96
|
+
|
|
97
|
+
vi.mocked(Log.create).mockResolvedValue({
|
|
98
|
+
toString: () => 'log between tags',
|
|
99
|
+
commits: [],
|
|
100
|
+
} as any);
|
|
101
|
+
|
|
102
|
+
vi.mocked(Diff.create).mockResolvedValue({
|
|
103
|
+
toString: () => 'diff between tags',
|
|
104
|
+
files: [],
|
|
105
|
+
} as any);
|
|
106
|
+
|
|
107
|
+
const { runAgenticRelease } = await import('@grunnverk/ai-service');
|
|
108
|
+
vi.mocked(runAgenticRelease).mockResolvedValue({
|
|
109
|
+
title: 'Release v2.0.0',
|
|
110
|
+
body: 'Changes from v1.0.0 to v2.0.0',
|
|
111
|
+
} as any);
|
|
112
|
+
|
|
113
|
+
const result = await execute(mockConfig);
|
|
114
|
+
|
|
115
|
+
expect(Log.create).toHaveBeenCalledWith(
|
|
116
|
+
expect.objectContaining({
|
|
117
|
+
from: 'v1.0.0',
|
|
118
|
+
to: 'v2.0.0',
|
|
119
|
+
})
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('release with version', () => {
|
|
125
|
+
it('should include version in release notes', async () => {
|
|
126
|
+
const mockConfig: Config = {
|
|
127
|
+
configDirectory: '/test/.kilde',
|
|
128
|
+
discoveredConfigDirs: [],
|
|
129
|
+
resolvedConfigDirs: [],
|
|
130
|
+
release: {
|
|
131
|
+
from: 'v1.0.0',
|
|
132
|
+
} as any,
|
|
133
|
+
} as Config;
|
|
134
|
+
|
|
135
|
+
(mockConfig.release as any).version = 'v2.0.0';
|
|
136
|
+
|
|
137
|
+
vi.mocked(GitTools.getCurrentBranch).mockResolvedValue('main');
|
|
138
|
+
vi.mocked(GitTools.getDefaultFromRef).mockResolvedValue('v1.0.0');
|
|
139
|
+
|
|
140
|
+
vi.mocked(Log.create).mockResolvedValue({
|
|
141
|
+
toString: () => 'commit log',
|
|
142
|
+
commits: [],
|
|
143
|
+
} as any);
|
|
144
|
+
|
|
145
|
+
vi.mocked(Diff.create).mockResolvedValue({
|
|
146
|
+
toString: () => 'diff content',
|
|
147
|
+
files: [],
|
|
148
|
+
} as any);
|
|
149
|
+
|
|
150
|
+
const { runAgenticRelease } = await import('@grunnverk/ai-service');
|
|
151
|
+
vi.mocked(runAgenticRelease).mockResolvedValue({
|
|
152
|
+
title: 'Release v2.0.0',
|
|
153
|
+
body: 'Version 2.0.0 release notes',
|
|
154
|
+
} as any);
|
|
155
|
+
|
|
156
|
+
await execute(mockConfig);
|
|
157
|
+
|
|
158
|
+
expect(runAgenticRelease).toHaveBeenCalledWith(
|
|
159
|
+
expect.objectContaining({
|
|
160
|
+
targetVersion: 'v2.0.0',
|
|
161
|
+
})
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('release with focus', () => {
|
|
167
|
+
it('should use focus parameter for targeted release notes', async () => {
|
|
168
|
+
const mockConfig: Config = {
|
|
169
|
+
configDirectory: '/test/.kilde',
|
|
170
|
+
discoveredConfigDirs: [],
|
|
171
|
+
resolvedConfigDirs: [],
|
|
172
|
+
release: {
|
|
173
|
+
focus: 'breaking changes',
|
|
174
|
+
},
|
|
175
|
+
} as Config;
|
|
176
|
+
|
|
177
|
+
vi.mocked(GitTools.getCurrentBranch).mockResolvedValue('main');
|
|
178
|
+
vi.mocked(GitTools.getDefaultFromRef).mockResolvedValue('v1.0.0');
|
|
179
|
+
|
|
180
|
+
vi.mocked(Log.create).mockResolvedValue({
|
|
181
|
+
toString: () => 'commit log',
|
|
182
|
+
commits: [],
|
|
183
|
+
} as any);
|
|
184
|
+
|
|
185
|
+
vi.mocked(Diff.create).mockResolvedValue({
|
|
186
|
+
toString: () => 'diff content',
|
|
187
|
+
files: [],
|
|
188
|
+
} as any);
|
|
189
|
+
|
|
190
|
+
const { runAgenticRelease } = await import('@grunnverk/ai-service');
|
|
191
|
+
vi.mocked(runAgenticRelease).mockResolvedValue({
|
|
192
|
+
title: 'Release Notes',
|
|
193
|
+
body: 'Breaking changes highlighted',
|
|
194
|
+
} as any);
|
|
195
|
+
|
|
196
|
+
await execute(mockConfig);
|
|
197
|
+
|
|
198
|
+
expect(runAgenticRelease).toHaveBeenCalledWith(
|
|
199
|
+
expect.objectContaining({
|
|
200
|
+
releaseFocus: 'breaking changes',
|
|
201
|
+
})
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('release with context', () => {
|
|
207
|
+
it('should include user context in generation', async () => {
|
|
208
|
+
const mockConfig: Config = {
|
|
209
|
+
configDirectory: '/test/.kilde',
|
|
210
|
+
discoveredConfigDirs: [],
|
|
211
|
+
resolvedConfigDirs: [],
|
|
212
|
+
release: {
|
|
213
|
+
context: 'Major refactoring release',
|
|
214
|
+
},
|
|
215
|
+
} as Config;
|
|
216
|
+
|
|
217
|
+
vi.mocked(GitTools.getCurrentBranch).mockResolvedValue('main');
|
|
218
|
+
vi.mocked(GitTools.getDefaultFromRef).mockResolvedValue('v1.0.0');
|
|
219
|
+
|
|
220
|
+
vi.mocked(Log.create).mockResolvedValue({
|
|
221
|
+
toString: () => 'commit log',
|
|
222
|
+
commits: [],
|
|
223
|
+
} as any);
|
|
224
|
+
|
|
225
|
+
vi.mocked(Diff.create).mockResolvedValue({
|
|
226
|
+
toString: () => 'diff content',
|
|
227
|
+
files: [],
|
|
228
|
+
} as any);
|
|
229
|
+
|
|
230
|
+
const { runAgenticRelease } = await import('@grunnverk/ai-service');
|
|
231
|
+
vi.mocked(runAgenticRelease).mockResolvedValue({
|
|
232
|
+
title: 'Release Notes',
|
|
233
|
+
body: 'Notes with context',
|
|
234
|
+
} as any);
|
|
235
|
+
|
|
236
|
+
await execute(mockConfig);
|
|
237
|
+
|
|
238
|
+
expect(runAgenticRelease).toHaveBeenCalledWith(
|
|
239
|
+
expect.objectContaining({
|
|
240
|
+
userContext: 'Major refactoring release',
|
|
241
|
+
})
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('dry-run mode', () => {
|
|
247
|
+
it('should preview release notes without saving', async () => {
|
|
248
|
+
const mockConfig: Config = {
|
|
249
|
+
configDirectory: '/test/.kilde',
|
|
250
|
+
discoveredConfigDirs: [],
|
|
251
|
+
resolvedConfigDirs: [],
|
|
252
|
+
dryRun: true,
|
|
253
|
+
release: {},
|
|
254
|
+
} as Config;
|
|
255
|
+
|
|
256
|
+
vi.mocked(GitTools.getCurrentBranch).mockResolvedValue('main');
|
|
257
|
+
vi.mocked(GitTools.getDefaultFromRef).mockResolvedValue('v1.0.0');
|
|
258
|
+
|
|
259
|
+
vi.mocked(Log.create).mockResolvedValue({
|
|
260
|
+
toString: () => 'commit log',
|
|
261
|
+
commits: [],
|
|
262
|
+
} as any);
|
|
263
|
+
|
|
264
|
+
vi.mocked(Diff.create).mockResolvedValue({
|
|
265
|
+
toString: () => 'diff content',
|
|
266
|
+
files: [],
|
|
267
|
+
} as any);
|
|
268
|
+
|
|
269
|
+
const { runAgenticRelease } = await import('@grunnverk/ai-service');
|
|
270
|
+
vi.mocked(runAgenticRelease).mockResolvedValue({
|
|
271
|
+
title: 'Preview Release',
|
|
272
|
+
body: 'Preview content',
|
|
273
|
+
} as any);
|
|
274
|
+
|
|
275
|
+
const result = await execute(mockConfig);
|
|
276
|
+
|
|
277
|
+
expect(result.title).toBe('Preview Release');
|
|
278
|
+
// In dry-run, files should not be written
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe('error handling', () => {
|
|
283
|
+
it('should handle git log errors', async () => {
|
|
284
|
+
const mockConfig: Config = {
|
|
285
|
+
configDirectory: '/test/.kilde',
|
|
286
|
+
discoveredConfigDirs: [],
|
|
287
|
+
resolvedConfigDirs: [],
|
|
288
|
+
release: {},
|
|
289
|
+
} as Config;
|
|
290
|
+
|
|
291
|
+
vi.mocked(GitTools.getCurrentBranch).mockResolvedValue('main');
|
|
292
|
+
vi.mocked(GitTools.getDefaultFromRef).mockResolvedValue('v1.0.0');
|
|
293
|
+
vi.mocked(Log.create).mockRejectedValue(new Error('Git log failed'));
|
|
294
|
+
|
|
295
|
+
await expect(execute(mockConfig)).rejects.toThrow();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should handle AI service errors', async () => {
|
|
299
|
+
const mockConfig: Config = {
|
|
300
|
+
configDirectory: '/test/.kilde',
|
|
301
|
+
discoveredConfigDirs: [],
|
|
302
|
+
resolvedConfigDirs: [],
|
|
303
|
+
release: {},
|
|
304
|
+
} as Config;
|
|
305
|
+
|
|
306
|
+
vi.mocked(GitTools.getCurrentBranch).mockResolvedValue('main');
|
|
307
|
+
vi.mocked(GitTools.getDefaultFromRef).mockResolvedValue('v1.0.0');
|
|
308
|
+
|
|
309
|
+
vi.mocked(Log.create).mockResolvedValue({
|
|
310
|
+
toString: () => 'log',
|
|
311
|
+
commits: [],
|
|
312
|
+
} as any);
|
|
313
|
+
|
|
314
|
+
vi.mocked(Diff.create).mockResolvedValue({
|
|
315
|
+
toString: () => 'diff',
|
|
316
|
+
files: [],
|
|
317
|
+
} as any);
|
|
318
|
+
|
|
319
|
+
const { runAgenticRelease } = await import('@grunnverk/ai-service');
|
|
320
|
+
vi.mocked(runAgenticRelease).mockRejectedValue(new Error('AI service error'));
|
|
321
|
+
|
|
322
|
+
await expect(execute(mockConfig)).rejects.toThrow();
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
PROGRAM_NAME,
|
|
4
|
+
VERSION,
|
|
5
|
+
BUILD_HOSTNAME,
|
|
6
|
+
BUILD_TIMESTAMP,
|
|
7
|
+
COMMAND_COMMIT,
|
|
8
|
+
COMMAND_RELEASE,
|
|
9
|
+
DEFAULT_CONFIG_DIR,
|
|
10
|
+
DEFAULT_EXCLUDED_PATTERNS,
|
|
11
|
+
KILDE_DEFAULTS,
|
|
12
|
+
} from '../src/constants';
|
|
13
|
+
|
|
14
|
+
describe('constants', () => {
|
|
15
|
+
describe('program metadata', () => {
|
|
16
|
+
it('should have PROGRAM_NAME defined', () => {
|
|
17
|
+
expect(PROGRAM_NAME).toBeDefined();
|
|
18
|
+
expect(typeof PROGRAM_NAME).toBe('string');
|
|
19
|
+
expect(PROGRAM_NAME).toBe('kilde');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should have VERSION defined', () => {
|
|
23
|
+
expect(VERSION).toBeDefined();
|
|
24
|
+
expect(typeof VERSION).toBe('string');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should have BUILD_HOSTNAME defined', () => {
|
|
28
|
+
expect(BUILD_HOSTNAME).toBeDefined();
|
|
29
|
+
expect(typeof BUILD_HOSTNAME).toBe('string');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should have BUILD_TIMESTAMP defined', () => {
|
|
33
|
+
expect(BUILD_TIMESTAMP).toBeDefined();
|
|
34
|
+
expect(typeof BUILD_TIMESTAMP).toBe('string');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('commands', () => {
|
|
39
|
+
it('should have COMMAND_COMMIT defined', () => {
|
|
40
|
+
expect(COMMAND_COMMIT).toBe('commit');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should have COMMAND_RELEASE defined', () => {
|
|
44
|
+
expect(COMMAND_RELEASE).toBe('release');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('configuration', () => {
|
|
49
|
+
it('should have DEFAULT_CONFIG_DIR defined', () => {
|
|
50
|
+
expect(DEFAULT_CONFIG_DIR).toBeDefined();
|
|
51
|
+
expect(typeof DEFAULT_CONFIG_DIR).toBe('string');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should have DEFAULT_EXCLUDED_PATTERNS defined', () => {
|
|
55
|
+
expect(DEFAULT_EXCLUDED_PATTERNS).toBeDefined();
|
|
56
|
+
expect(Array.isArray(DEFAULT_EXCLUDED_PATTERNS)).toBe(true);
|
|
57
|
+
expect(DEFAULT_EXCLUDED_PATTERNS.length).toBeGreaterThan(0);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should exclude common patterns', () => {
|
|
61
|
+
expect(DEFAULT_EXCLUDED_PATTERNS).toContain('node_modules');
|
|
62
|
+
expect(DEFAULT_EXCLUDED_PATTERNS).toContain('.git');
|
|
63
|
+
expect(DEFAULT_EXCLUDED_PATTERNS).toContain('dist');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('KILDE_DEFAULTS', () => {
|
|
68
|
+
it('should have all required config fields', () => {
|
|
69
|
+
expect(KILDE_DEFAULTS.configDirectory).toBeDefined();
|
|
70
|
+
expect(KILDE_DEFAULTS.discoveredConfigDirs).toBeDefined();
|
|
71
|
+
expect(KILDE_DEFAULTS.resolvedConfigDirs).toBeDefined();
|
|
72
|
+
expect(KILDE_DEFAULTS.excludedPatterns).toBeDefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should have boolean flags with default values', () => {
|
|
76
|
+
expect(typeof KILDE_DEFAULTS.verbose).toBe('boolean');
|
|
77
|
+
expect(typeof KILDE_DEFAULTS.debug).toBe('boolean');
|
|
78
|
+
expect(typeof KILDE_DEFAULTS.dryRun).toBe('boolean');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should have model configuration', () => {
|
|
82
|
+
expect(KILDE_DEFAULTS.model).toBeDefined();
|
|
83
|
+
expect(typeof KILDE_DEFAULTS.model).toBe('string');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should have commit configuration', () => {
|
|
87
|
+
expect(KILDE_DEFAULTS.commit).toBeDefined();
|
|
88
|
+
expect(typeof KILDE_DEFAULTS.commit).toBe('object');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should have release configuration', () => {
|
|
92
|
+
expect(KILDE_DEFAULTS.release).toBeDefined();
|
|
93
|
+
expect(typeof KILDE_DEFAULTS.release).toBe('object');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should have sensible commit defaults', () => {
|
|
97
|
+
expect(KILDE_DEFAULTS.commit?.sendit).toBe(false);
|
|
98
|
+
expect(KILDE_DEFAULTS.commit?.interactive).toBe(false);
|
|
99
|
+
expect(KILDE_DEFAULTS.commit?.add).toBe(false);
|
|
100
|
+
expect(KILDE_DEFAULTS.commit?.cached).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should have sensible release defaults', () => {
|
|
104
|
+
expect(KILDE_DEFAULTS.release?.interactive).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should use excluded patterns from DEFAULT_EXCLUDED_PATTERNS', () => {
|
|
108
|
+
expect(KILDE_DEFAULTS.excludedPatterns).toEqual(DEFAULT_EXCLUDED_PATTERNS);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should have empty arrays for directory lists', () => {
|
|
112
|
+
expect(Array.isArray(KILDE_DEFAULTS.discoveredConfigDirs)).toBe(true);
|
|
113
|
+
expect(Array.isArray(KILDE_DEFAULTS.resolvedConfigDirs)).toBe(true);
|
|
114
|
+
expect(KILDE_DEFAULTS.discoveredConfigDirs.length).toBe(0);
|
|
115
|
+
expect(KILDE_DEFAULTS.resolvedConfigDirs.length).toBe(0);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import winston from 'winston';
|
|
3
|
+
import { getLogger, setLogLevel, getDryRunLogger } from '../src/logging';
|
|
4
|
+
|
|
5
|
+
describe('logging', () => {
|
|
6
|
+
let originalEnv: NodeJS.ProcessEnv;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
originalEnv = { ...process.env };
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
process.env = originalEnv;
|
|
14
|
+
// Reset log level
|
|
15
|
+
setLogLevel('info');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('getLogger', () => {
|
|
19
|
+
it('should return a winston logger instance', () => {
|
|
20
|
+
const logger = getLogger();
|
|
21
|
+
expect(logger).toBeDefined();
|
|
22
|
+
expect(typeof logger.info).toBe('function');
|
|
23
|
+
expect(typeof logger.error).toBe('function');
|
|
24
|
+
expect(typeof logger.debug).toBe('function');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should return the same logger instance on multiple calls', () => {
|
|
28
|
+
const logger1 = getLogger();
|
|
29
|
+
const logger2 = getLogger();
|
|
30
|
+
expect(logger1).toBe(logger2);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should have console transport in non-MCP mode', () => {
|
|
34
|
+
delete process.env.KILDE_MCP_SERVER;
|
|
35
|
+
const logger = getLogger() as winston.Logger;
|
|
36
|
+
const transports = (logger as any).transports || [];
|
|
37
|
+
const hasConsole = transports.some((t: any) => t instanceof winston.transports.Console);
|
|
38
|
+
expect(hasConsole).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('setLogLevel', () => {
|
|
43
|
+
it('should change logger level to verbose', () => {
|
|
44
|
+
const logger = getLogger() as winston.Logger;
|
|
45
|
+
setLogLevel('verbose');
|
|
46
|
+
expect(logger.level).toBe('verbose');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should change logger level to debug', () => {
|
|
50
|
+
const logger = getLogger() as winston.Logger;
|
|
51
|
+
setLogLevel('debug');
|
|
52
|
+
expect(logger.level).toBe('debug');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should change logger level to info', () => {
|
|
56
|
+
const logger = getLogger() as winston.Logger;
|
|
57
|
+
setLogLevel('info');
|
|
58
|
+
expect(logger.level).toBe('info');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('getDryRunLogger', () => {
|
|
63
|
+
it('should return a logger with dry-run prefix when isDryRun is true', () => {
|
|
64
|
+
const logger = getDryRunLogger(true);
|
|
65
|
+
expect(logger).toBeDefined();
|
|
66
|
+
|
|
67
|
+
// Test that it has the expected methods
|
|
68
|
+
expect(typeof logger.info).toBe('function');
|
|
69
|
+
expect(typeof logger.error).toBe('function');
|
|
70
|
+
expect(typeof logger.debug).toBe('function');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should return the original logger when isDryRun is false', () => {
|
|
74
|
+
const originalLogger = getLogger();
|
|
75
|
+
const logger = getDryRunLogger(false);
|
|
76
|
+
expect(logger).toBe(originalLogger);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should prefix messages with [DRY-RUN] when isDryRun is true', () => {
|
|
80
|
+
const logger = getDryRunLogger(true);
|
|
81
|
+
const mockLog = vi.fn();
|
|
82
|
+
|
|
83
|
+
// Replace the underlying logger's info method
|
|
84
|
+
const originalLogger = getLogger();
|
|
85
|
+
const originalInfo = originalLogger.info;
|
|
86
|
+
originalLogger.info = mockLog as any;
|
|
87
|
+
|
|
88
|
+
logger.info('test message');
|
|
89
|
+
|
|
90
|
+
// Verify the message was passed with dryRun metadata
|
|
91
|
+
expect(mockLog).toHaveBeenCalledWith('test message', { dryRun: true });
|
|
92
|
+
|
|
93
|
+
// Restore original
|
|
94
|
+
originalLogger.info = originalInfo;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should handle all log methods with dry-run metadata', () => {
|
|
98
|
+
const logger = getDryRunLogger(true);
|
|
99
|
+
const originalLogger = getLogger();
|
|
100
|
+
|
|
101
|
+
// Test warn
|
|
102
|
+
const mockWarn = vi.fn();
|
|
103
|
+
const originalWarn = originalLogger.warn;
|
|
104
|
+
originalLogger.warn = mockWarn as any;
|
|
105
|
+
logger.warn('warning');
|
|
106
|
+
expect(mockWarn).toHaveBeenCalledWith('warning', { dryRun: true });
|
|
107
|
+
originalLogger.warn = originalWarn;
|
|
108
|
+
|
|
109
|
+
// Test error
|
|
110
|
+
const mockError = vi.fn();
|
|
111
|
+
const originalError = originalLogger.error;
|
|
112
|
+
originalLogger.error = mockError as any;
|
|
113
|
+
logger.error('error');
|
|
114
|
+
expect(mockError).toHaveBeenCalledWith('error', { dryRun: true });
|
|
115
|
+
originalLogger.error = originalError;
|
|
116
|
+
|
|
117
|
+
// Test debug
|
|
118
|
+
const mockDebug = vi.fn();
|
|
119
|
+
const originalDebug = originalLogger.debug;
|
|
120
|
+
originalLogger.debug = mockDebug as any;
|
|
121
|
+
logger.debug('debug');
|
|
122
|
+
expect(mockDebug).toHaveBeenCalledWith('debug', { dryRun: true });
|
|
123
|
+
originalLogger.debug = originalDebug;
|
|
124
|
+
|
|
125
|
+
// Test verbose
|
|
126
|
+
const mockVerbose = vi.fn();
|
|
127
|
+
const originalVerbose = originalLogger.verbose;
|
|
128
|
+
originalLogger.verbose = mockVerbose as any;
|
|
129
|
+
logger.verbose('verbose');
|
|
130
|
+
expect(mockVerbose).toHaveBeenCalledWith('verbose', { dryRun: true });
|
|
131
|
+
originalLogger.verbose = originalVerbose;
|
|
132
|
+
|
|
133
|
+
// Test silly
|
|
134
|
+
const mockSilly = vi.fn();
|
|
135
|
+
const originalSilly = originalLogger.silly;
|
|
136
|
+
originalLogger.silly = mockSilly as any;
|
|
137
|
+
logger.silly('silly');
|
|
138
|
+
expect(mockSilly).toHaveBeenCalledWith('silly', { dryRun: true });
|
|
139
|
+
originalLogger.silly = originalSilly;
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|