claudekit-cli 1.1.0 → 1.2.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/workflows/release.yml +44 -0
- package/CHANGELOG.md +16 -0
- package/CLAUDE.md +3 -9
- package/LICENSE +21 -0
- package/README.md +53 -1
- package/dist/index.js +11473 -10945
- package/package.json +1 -1
- package/src/commands/new.ts +41 -9
- package/src/commands/update.ts +59 -13
- package/src/index.ts +42 -1
- package/src/lib/download.ts +231 -1
- package/src/lib/github.ts +56 -0
- package/src/lib/prompts.ts +4 -3
- package/src/types.ts +4 -2
- package/src/utils/file-scanner.ts +134 -0
- package/src/utils/logger.ts +108 -21
- package/src/utils/safe-prompts.ts +54 -0
- package/tests/commands/version.test.ts +2 -2
- package/tests/lib/github-download-priority.test.ts +301 -0
- package/tests/lib/github.test.ts +2 -2
- package/tests/lib/merge.test.ts +77 -0
- package/tests/types.test.ts +4 -0
- package/tests/utils/file-scanner.test.ts +202 -0
- package/tests/utils/logger.test.ts +115 -0
package/tests/lib/merge.test.ts
CHANGED
|
@@ -135,4 +135,81 @@ describe("FileMerger", () => {
|
|
|
135
135
|
expect(existsSync(join(testDestDir, "custom-ignore.txt"))).toBe(false);
|
|
136
136
|
});
|
|
137
137
|
});
|
|
138
|
+
|
|
139
|
+
describe("custom .claude file preservation", () => {
|
|
140
|
+
test("should preserve custom .claude files when patterns are added", async () => {
|
|
141
|
+
// Create .claude directories
|
|
142
|
+
const sourceClaudeDir = join(testSourceDir, ".claude");
|
|
143
|
+
const destClaudeDir = join(testDestDir, ".claude");
|
|
144
|
+
await mkdir(sourceClaudeDir, { recursive: true });
|
|
145
|
+
await mkdir(destClaudeDir, { recursive: true });
|
|
146
|
+
|
|
147
|
+
// Create files in source (from release package)
|
|
148
|
+
await writeFile(join(sourceClaudeDir, "standard.md"), "standard content");
|
|
149
|
+
|
|
150
|
+
// Create files in destination (existing + custom)
|
|
151
|
+
await writeFile(join(destClaudeDir, "standard.md"), "old standard content");
|
|
152
|
+
await writeFile(join(destClaudeDir, "custom.md"), "custom content");
|
|
153
|
+
|
|
154
|
+
// Add custom file to ignore patterns (this would be done by update.ts)
|
|
155
|
+
merger.addIgnorePatterns([".claude/custom.md"]);
|
|
156
|
+
|
|
157
|
+
await merger.merge(testSourceDir, testDestDir, true);
|
|
158
|
+
|
|
159
|
+
// Standard file should be overwritten
|
|
160
|
+
const standardContent = await Bun.file(join(destClaudeDir, "standard.md")).text();
|
|
161
|
+
expect(standardContent).toBe("standard content");
|
|
162
|
+
|
|
163
|
+
// Custom file should be preserved
|
|
164
|
+
expect(existsSync(join(destClaudeDir, "custom.md"))).toBe(true);
|
|
165
|
+
const customContent = await Bun.file(join(destClaudeDir, "custom.md")).text();
|
|
166
|
+
expect(customContent).toBe("custom content");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("should preserve nested custom .claude files", async () => {
|
|
170
|
+
// Create nested .claude structure
|
|
171
|
+
const sourceCommandsDir = join(testSourceDir, ".claude", "commands");
|
|
172
|
+
const destCommandsDir = join(testDestDir, ".claude", "commands");
|
|
173
|
+
await mkdir(sourceCommandsDir, { recursive: true });
|
|
174
|
+
await mkdir(destCommandsDir, { recursive: true });
|
|
175
|
+
|
|
176
|
+
// Create standard file in source
|
|
177
|
+
await writeFile(join(sourceCommandsDir, "standard-cmd.md"), "standard command");
|
|
178
|
+
|
|
179
|
+
// Create custom file in destination
|
|
180
|
+
await writeFile(join(destCommandsDir, "custom-cmd.md"), "custom command");
|
|
181
|
+
|
|
182
|
+
// Add custom file to ignore patterns
|
|
183
|
+
merger.addIgnorePatterns([".claude/commands/custom-cmd.md"]);
|
|
184
|
+
|
|
185
|
+
await merger.merge(testSourceDir, testDestDir, true);
|
|
186
|
+
|
|
187
|
+
// Custom file should be preserved
|
|
188
|
+
expect(existsSync(join(destCommandsDir, "custom-cmd.md"))).toBe(true);
|
|
189
|
+
const customContent = await Bun.file(join(destCommandsDir, "custom-cmd.md")).text();
|
|
190
|
+
expect(customContent).toBe("custom command");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("should preserve multiple custom .claude files", async () => {
|
|
194
|
+
const sourceClaudeDir = join(testSourceDir, ".claude");
|
|
195
|
+
const destClaudeDir = join(testDestDir, ".claude");
|
|
196
|
+
await mkdir(sourceClaudeDir, { recursive: true });
|
|
197
|
+
await mkdir(destClaudeDir, { recursive: true });
|
|
198
|
+
|
|
199
|
+
// Create multiple custom files in destination
|
|
200
|
+
await writeFile(join(destClaudeDir, "custom1.md"), "custom1");
|
|
201
|
+
await writeFile(join(destClaudeDir, "custom2.md"), "custom2");
|
|
202
|
+
await writeFile(join(destClaudeDir, "custom3.md"), "custom3");
|
|
203
|
+
|
|
204
|
+
// Add all custom files to ignore patterns
|
|
205
|
+
merger.addIgnorePatterns([".claude/custom1.md", ".claude/custom2.md", ".claude/custom3.md"]);
|
|
206
|
+
|
|
207
|
+
await merger.merge(testSourceDir, testDestDir, true);
|
|
208
|
+
|
|
209
|
+
// All custom files should be preserved
|
|
210
|
+
expect(existsSync(join(destClaudeDir, "custom1.md"))).toBe(true);
|
|
211
|
+
expect(existsSync(join(destClaudeDir, "custom2.md"))).toBe(true);
|
|
212
|
+
expect(existsSync(join(destClaudeDir, "custom3.md"))).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
138
215
|
});
|
package/tests/types.test.ts
CHANGED
|
@@ -156,6 +156,8 @@ describe("Types and Schemas", () => {
|
|
|
156
156
|
},
|
|
157
157
|
],
|
|
158
158
|
published_at: "2024-01-01T00:00:00Z",
|
|
159
|
+
tarball_url: "https://api.github.com/repos/test/test-repo/tarball/v1.0.0",
|
|
160
|
+
zipball_url: "https://api.github.com/repos/test/test-repo/zipball/v1.0.0",
|
|
159
161
|
};
|
|
160
162
|
const result = GitHubReleaseSchema.parse(release);
|
|
161
163
|
expect(result.id).toBe(1);
|
|
@@ -171,6 +173,8 @@ describe("Types and Schemas", () => {
|
|
|
171
173
|
draft: false,
|
|
172
174
|
prerelease: false,
|
|
173
175
|
assets: [],
|
|
176
|
+
tarball_url: "https://api.github.com/repos/test/test-repo/tarball/v1.0.0",
|
|
177
|
+
zipball_url: "https://api.github.com/repos/test/test-repo/zipball/v1.0.0",
|
|
174
178
|
};
|
|
175
179
|
const result = GitHubReleaseSchema.parse(release);
|
|
176
180
|
expect(result.published_at).toBeUndefined();
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { mkdir, remove, writeFile } from "fs-extra";
|
|
4
|
+
import { FileScanner } from "../../src/utils/file-scanner.js";
|
|
5
|
+
|
|
6
|
+
describe("FileScanner", () => {
|
|
7
|
+
const testDir = join(__dirname, "..", "..", "temp-test-file-scanner");
|
|
8
|
+
const destDir = join(testDir, "dest");
|
|
9
|
+
const sourceDir = join(testDir, "source");
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
// Clean up and create test directories
|
|
13
|
+
await remove(testDir);
|
|
14
|
+
await mkdir(testDir, { recursive: true });
|
|
15
|
+
await mkdir(destDir, { recursive: true });
|
|
16
|
+
await mkdir(sourceDir, { recursive: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
// Clean up test directories
|
|
21
|
+
await remove(testDir);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("getFiles", () => {
|
|
25
|
+
test("should return empty array for non-existent directory", async () => {
|
|
26
|
+
const files = await FileScanner.getFiles(join(testDir, "non-existent"));
|
|
27
|
+
expect(files).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("should return files from directory", async () => {
|
|
31
|
+
// Create test files
|
|
32
|
+
await writeFile(join(destDir, "file1.txt"), "content1");
|
|
33
|
+
await writeFile(join(destDir, "file2.txt"), "content2");
|
|
34
|
+
|
|
35
|
+
const files = await FileScanner.getFiles(destDir);
|
|
36
|
+
|
|
37
|
+
expect(files).toHaveLength(2);
|
|
38
|
+
expect(files).toContain("file1.txt");
|
|
39
|
+
expect(files).toContain("file2.txt");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("should recursively scan subdirectories", async () => {
|
|
43
|
+
// Create nested structure
|
|
44
|
+
await mkdir(join(destDir, "subdir"), { recursive: true });
|
|
45
|
+
await writeFile(join(destDir, "file1.txt"), "content1");
|
|
46
|
+
await writeFile(join(destDir, "subdir", "file2.txt"), "content2");
|
|
47
|
+
|
|
48
|
+
const files = await FileScanner.getFiles(destDir);
|
|
49
|
+
|
|
50
|
+
expect(files).toHaveLength(2);
|
|
51
|
+
expect(files).toContain("file1.txt");
|
|
52
|
+
expect(files).toContain("subdir/file2.txt");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("should handle empty directory", async () => {
|
|
56
|
+
const files = await FileScanner.getFiles(destDir);
|
|
57
|
+
expect(files).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("should handle deeply nested directories", async () => {
|
|
61
|
+
// Create deeply nested structure
|
|
62
|
+
const deepPath = join(destDir, "a", "b", "c", "d");
|
|
63
|
+
await mkdir(deepPath, { recursive: true });
|
|
64
|
+
await writeFile(join(deepPath, "deep.txt"), "deep content");
|
|
65
|
+
|
|
66
|
+
const files = await FileScanner.getFiles(destDir);
|
|
67
|
+
|
|
68
|
+
expect(files).toHaveLength(1);
|
|
69
|
+
expect(files).toContain("a/b/c/d/deep.txt");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("should return relative paths", async () => {
|
|
73
|
+
await mkdir(join(destDir, "subdir"), { recursive: true });
|
|
74
|
+
await writeFile(join(destDir, "subdir", "file.txt"), "content");
|
|
75
|
+
|
|
76
|
+
const files = await FileScanner.getFiles(destDir);
|
|
77
|
+
|
|
78
|
+
// Should return relative path, not absolute
|
|
79
|
+
expect(files[0]).toBe("subdir/file.txt");
|
|
80
|
+
expect(files[0]).not.toContain(destDir);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("findCustomFiles", () => {
|
|
85
|
+
test("should identify files in dest but not in source", async () => {
|
|
86
|
+
// Create .claude directories
|
|
87
|
+
const destClaudeDir = join(destDir, ".claude");
|
|
88
|
+
const sourceClaudeDir = join(sourceDir, ".claude");
|
|
89
|
+
await mkdir(destClaudeDir, { recursive: true });
|
|
90
|
+
await mkdir(sourceClaudeDir, { recursive: true });
|
|
91
|
+
|
|
92
|
+
// Create files
|
|
93
|
+
await writeFile(join(destClaudeDir, "custom.md"), "custom content");
|
|
94
|
+
await writeFile(join(destClaudeDir, "standard.md"), "standard content");
|
|
95
|
+
await writeFile(join(sourceClaudeDir, "standard.md"), "standard content");
|
|
96
|
+
|
|
97
|
+
const customFiles = await FileScanner.findCustomFiles(destDir, sourceDir, ".claude");
|
|
98
|
+
|
|
99
|
+
expect(customFiles).toHaveLength(1);
|
|
100
|
+
expect(customFiles).toContain(".claude/custom.md");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("should return empty array when no custom files exist", async () => {
|
|
104
|
+
// Create .claude directories
|
|
105
|
+
const destClaudeDir = join(destDir, ".claude");
|
|
106
|
+
const sourceClaudeDir = join(sourceDir, ".claude");
|
|
107
|
+
await mkdir(destClaudeDir, { recursive: true });
|
|
108
|
+
await mkdir(sourceClaudeDir, { recursive: true });
|
|
109
|
+
|
|
110
|
+
// Create same files in both
|
|
111
|
+
await writeFile(join(destClaudeDir, "file1.md"), "content1");
|
|
112
|
+
await writeFile(join(sourceClaudeDir, "file1.md"), "content1");
|
|
113
|
+
|
|
114
|
+
const customFiles = await FileScanner.findCustomFiles(destDir, sourceDir, ".claude");
|
|
115
|
+
|
|
116
|
+
expect(customFiles).toEqual([]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("should handle missing .claude in destination", async () => {
|
|
120
|
+
// Only create source .claude directory
|
|
121
|
+
const sourceClaudeDir = join(sourceDir, ".claude");
|
|
122
|
+
await mkdir(sourceClaudeDir, { recursive: true });
|
|
123
|
+
await writeFile(join(sourceClaudeDir, "file1.md"), "content1");
|
|
124
|
+
|
|
125
|
+
const customFiles = await FileScanner.findCustomFiles(destDir, sourceDir, ".claude");
|
|
126
|
+
|
|
127
|
+
expect(customFiles).toEqual([]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("should handle missing .claude in source", async () => {
|
|
131
|
+
// Only create dest .claude directory
|
|
132
|
+
const destClaudeDir = join(destDir, ".claude");
|
|
133
|
+
await mkdir(destClaudeDir, { recursive: true });
|
|
134
|
+
await writeFile(join(destClaudeDir, "custom.md"), "custom content");
|
|
135
|
+
|
|
136
|
+
const customFiles = await FileScanner.findCustomFiles(destDir, sourceDir, ".claude");
|
|
137
|
+
|
|
138
|
+
expect(customFiles).toHaveLength(1);
|
|
139
|
+
expect(customFiles).toContain(".claude/custom.md");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("should handle nested subdirectories", async () => {
|
|
143
|
+
// Create nested structure
|
|
144
|
+
const destNestedDir = join(destDir, ".claude", "commands");
|
|
145
|
+
const sourceNestedDir = join(sourceDir, ".claude", "commands");
|
|
146
|
+
await mkdir(destNestedDir, { recursive: true });
|
|
147
|
+
await mkdir(sourceNestedDir, { recursive: true });
|
|
148
|
+
|
|
149
|
+
// Create custom file in nested dir
|
|
150
|
+
await writeFile(join(destNestedDir, "custom-cmd.md"), "custom command");
|
|
151
|
+
await writeFile(join(destNestedDir, "standard-cmd.md"), "standard command");
|
|
152
|
+
await writeFile(join(sourceNestedDir, "standard-cmd.md"), "standard command");
|
|
153
|
+
|
|
154
|
+
const customFiles = await FileScanner.findCustomFiles(destDir, sourceDir, ".claude");
|
|
155
|
+
|
|
156
|
+
expect(customFiles).toHaveLength(1);
|
|
157
|
+
expect(customFiles).toContain(".claude/commands/custom-cmd.md");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("should handle multiple custom files", async () => {
|
|
161
|
+
// Create .claude directories
|
|
162
|
+
const destClaudeDir = join(destDir, ".claude");
|
|
163
|
+
const sourceClaudeDir = join(sourceDir, ".claude");
|
|
164
|
+
await mkdir(destClaudeDir, { recursive: true });
|
|
165
|
+
await mkdir(sourceClaudeDir, { recursive: true });
|
|
166
|
+
|
|
167
|
+
// Create multiple custom files
|
|
168
|
+
await writeFile(join(destClaudeDir, "custom1.md"), "custom1");
|
|
169
|
+
await writeFile(join(destClaudeDir, "custom2.md"), "custom2");
|
|
170
|
+
await writeFile(join(destClaudeDir, "custom3.md"), "custom3");
|
|
171
|
+
await writeFile(join(destClaudeDir, "standard.md"), "standard");
|
|
172
|
+
await writeFile(join(sourceClaudeDir, "standard.md"), "standard");
|
|
173
|
+
|
|
174
|
+
const customFiles = await FileScanner.findCustomFiles(destDir, sourceDir, ".claude");
|
|
175
|
+
|
|
176
|
+
expect(customFiles).toHaveLength(3);
|
|
177
|
+
expect(customFiles).toContain(".claude/custom1.md");
|
|
178
|
+
expect(customFiles).toContain(".claude/custom2.md");
|
|
179
|
+
expect(customFiles).toContain(".claude/custom3.md");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("should handle files with special characters in names", async () => {
|
|
183
|
+
// Create .claude directories
|
|
184
|
+
const destClaudeDir = join(destDir, ".claude");
|
|
185
|
+
const sourceClaudeDir = join(sourceDir, ".claude");
|
|
186
|
+
await mkdir(destClaudeDir, { recursive: true });
|
|
187
|
+
await mkdir(sourceClaudeDir, { recursive: true });
|
|
188
|
+
|
|
189
|
+
// Create files with special characters
|
|
190
|
+
await writeFile(join(destClaudeDir, "file-with-dash.md"), "content");
|
|
191
|
+
await writeFile(join(destClaudeDir, "file_with_underscore.md"), "content");
|
|
192
|
+
await writeFile(join(destClaudeDir, "file.multiple.dots.md"), "content");
|
|
193
|
+
|
|
194
|
+
const customFiles = await FileScanner.findCustomFiles(destDir, sourceDir, ".claude");
|
|
195
|
+
|
|
196
|
+
expect(customFiles).toHaveLength(3);
|
|
197
|
+
expect(customFiles).toContain(".claude/file-with-dash.md");
|
|
198
|
+
expect(customFiles).toContain(".claude/file_with_underscore.md");
|
|
199
|
+
expect(customFiles).toContain(".claude/file.multiple.dots.md");
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -7,6 +7,10 @@ describe("Logger Utilities", () => {
|
|
|
7
7
|
const originalDebug = process.env.DEBUG;
|
|
8
8
|
|
|
9
9
|
beforeEach(() => {
|
|
10
|
+
// Reset logger state
|
|
11
|
+
logger.setVerbose(false);
|
|
12
|
+
logger.setLogFile(undefined);
|
|
13
|
+
|
|
10
14
|
consoleLogSpy = mock(() => {});
|
|
11
15
|
consoleErrorSpy = mock(() => {});
|
|
12
16
|
console.log = consoleLogSpy;
|
|
@@ -101,6 +105,18 @@ describe("Logger Utilities", () => {
|
|
|
101
105
|
expect(sanitized).toBe("Token: ghr_***");
|
|
102
106
|
});
|
|
103
107
|
|
|
108
|
+
test("should sanitize Bearer tokens", () => {
|
|
109
|
+
const text = "Authorization: Bearer abc123xyz-token_value";
|
|
110
|
+
const sanitized = logger.sanitize(text);
|
|
111
|
+
expect(sanitized).toBe("Authorization: Bearer ***");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("should sanitize query string tokens", () => {
|
|
115
|
+
const text = "https://api.example.com?token=secret123";
|
|
116
|
+
const sanitized = logger.sanitize(text);
|
|
117
|
+
expect(sanitized).toBe("https://api.example.com?token=***");
|
|
118
|
+
});
|
|
119
|
+
|
|
104
120
|
test("should sanitize multiple tokens", () => {
|
|
105
121
|
const ghpToken = "123456789012345678901234567890123456";
|
|
106
122
|
const patToken =
|
|
@@ -121,4 +137,103 @@ describe("Logger Utilities", () => {
|
|
|
121
137
|
expect(sanitized).toBe("");
|
|
122
138
|
});
|
|
123
139
|
});
|
|
140
|
+
|
|
141
|
+
describe("verbose", () => {
|
|
142
|
+
beforeEach(() => {
|
|
143
|
+
logger.setVerbose(false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("should not log when verbose is disabled", () => {
|
|
147
|
+
logger.verbose("Test verbose message");
|
|
148
|
+
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("should log to stderr when verbose is enabled", () => {
|
|
152
|
+
logger.setVerbose(true);
|
|
153
|
+
logger.verbose("Test verbose message");
|
|
154
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
155
|
+
const call = consoleErrorSpy.mock.calls[1][0];
|
|
156
|
+
expect(call).toContain("[VERBOSE]");
|
|
157
|
+
expect(call).toContain("Test verbose message");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("should include timestamp in verbose logs", () => {
|
|
161
|
+
logger.setVerbose(true);
|
|
162
|
+
logger.verbose("Test message");
|
|
163
|
+
const call = consoleErrorSpy.mock.calls[1][0];
|
|
164
|
+
expect(call).toMatch(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("should sanitize sensitive data in verbose logs", () => {
|
|
168
|
+
logger.setVerbose(true);
|
|
169
|
+
logger.verbose("Token: ghp_123456789012345678901234567890123456");
|
|
170
|
+
const call = consoleErrorSpy.mock.calls[1][0];
|
|
171
|
+
expect(call).toContain("ghp_***");
|
|
172
|
+
expect(call).not.toContain("ghp_123456789012345678901234567890123456");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("should include context in verbose logs", () => {
|
|
176
|
+
logger.setVerbose(true);
|
|
177
|
+
logger.verbose("Test message", { key: "value", num: 123 });
|
|
178
|
+
const call = consoleErrorSpy.mock.calls[1][0];
|
|
179
|
+
expect(call).toContain('"key": "value"');
|
|
180
|
+
expect(call).toContain('"num": 123');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("should sanitize context strings", () => {
|
|
184
|
+
logger.setVerbose(true);
|
|
185
|
+
logger.verbose("Test message", {
|
|
186
|
+
token: "ghp_123456789012345678901234567890123456",
|
|
187
|
+
});
|
|
188
|
+
const call = consoleErrorSpy.mock.calls[1][0];
|
|
189
|
+
expect(call).toContain("ghp_***");
|
|
190
|
+
expect(call).not.toContain("ghp_123456789012345678901234567890123456");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("should handle nested objects in context", () => {
|
|
194
|
+
logger.setVerbose(true);
|
|
195
|
+
logger.verbose("Test message", {
|
|
196
|
+
nested: { key: "value" },
|
|
197
|
+
});
|
|
198
|
+
const call = consoleErrorSpy.mock.calls[1][0];
|
|
199
|
+
expect(call).toContain('"nested"');
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("setVerbose", () => {
|
|
204
|
+
test("should enable verbose logging", () => {
|
|
205
|
+
logger.setVerbose(true);
|
|
206
|
+
expect(logger.isVerbose()).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("should disable verbose logging", () => {
|
|
210
|
+
logger.setVerbose(true);
|
|
211
|
+
logger.setVerbose(false);
|
|
212
|
+
expect(logger.isVerbose()).toBe(false);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("should log when enabling verbose", () => {
|
|
216
|
+
logger.setVerbose(true);
|
|
217
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
218
|
+
const call = consoleErrorSpy.mock.calls[0][0];
|
|
219
|
+
expect(call).toContain("Verbose logging enabled");
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("isVerbose", () => {
|
|
224
|
+
test("should return false by default", () => {
|
|
225
|
+
expect(logger.isVerbose()).toBe(false);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("should return true when enabled", () => {
|
|
229
|
+
logger.setVerbose(true);
|
|
230
|
+
expect(logger.isVerbose()).toBe(true);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("should return false when disabled", () => {
|
|
234
|
+
logger.setVerbose(true);
|
|
235
|
+
logger.setVerbose(false);
|
|
236
|
+
expect(logger.isVerbose()).toBe(false);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
124
239
|
});
|