@selesai/code 0.10.1 → 0.11.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/CHANGELOG.md +23 -0
- package/dist/config.d.ts +5 -3
- package/dist/config.js +6 -4
- package/dist/core/skills.d.ts +2 -0
- package/dist/core/skills.js +1 -0
- package/dist/defaults/settings.json +0 -3
- package/dist/extensions/auto-session-name.test.ts +157 -0
- package/dist/extensions/auto-session-name.ts +96 -0
- package/dist/extensions/cost-reconcile.test.ts +76 -0
- package/dist/extensions/cost-reconcile.ts +278 -0
- package/dist/extensions/enable-readonly-tools.test.ts +11 -0
- package/dist/extensions/enable-readonly-tools.ts +7 -3
- package/dist/extensions/package.json +3 -0
- package/dist/extensions/pi-tool-display/CHANGELOG.md +329 -0
- package/dist/extensions/pi-tool-display/LICENSE +21 -0
- package/dist/extensions/pi-tool-display/README.md +417 -0
- package/dist/extensions/pi-tool-display/config/config.example.json +40 -0
- package/dist/extensions/pi-tool-display/index.ts +3 -0
- package/dist/extensions/pi-tool-display/package-lock.json +2555 -0
- package/dist/extensions/pi-tool-display/package.json +86 -0
- package/dist/extensions/pi-tool-display/src/agent-dir.ts +35 -0
- package/dist/extensions/pi-tool-display/src/ansi-utils.ts +116 -0
- package/dist/extensions/pi-tool-display/src/bash-display.ts +213 -0
- package/dist/extensions/pi-tool-display/src/capabilities.ts +88 -0
- package/dist/extensions/pi-tool-display/src/config-modal.ts +541 -0
- package/dist/extensions/pi-tool-display/src/config-store.ts +304 -0
- package/dist/extensions/pi-tool-display/src/debug-logger.ts +150 -0
- package/dist/extensions/pi-tool-display/src/diff-presentation.ts +72 -0
- package/dist/extensions/pi-tool-display/src/diff-renderer.ts +2517 -0
- package/dist/extensions/pi-tool-display/src/disposable.ts +34 -0
- package/dist/extensions/pi-tool-display/src/extension-lifecycle.ts +14 -0
- package/dist/extensions/pi-tool-display/src/index.ts +109 -0
- package/dist/extensions/pi-tool-display/src/line-width-safety.ts +93 -0
- package/dist/extensions/pi-tool-display/src/modal-icons.ts +57 -0
- package/dist/extensions/pi-tool-display/src/pending-diff-preview.ts +370 -0
- package/dist/extensions/pi-tool-display/src/presets.ts +117 -0
- package/dist/extensions/pi-tool-display/src/render-utils.ts +186 -0
- package/dist/extensions/pi-tool-display/src/settings-inspector-modal.ts +475 -0
- package/dist/extensions/pi-tool-display/src/thinking-label.ts +343 -0
- package/dist/extensions/pi-tool-display/src/tool-call-box.ts +181 -0
- package/dist/extensions/pi-tool-display/src/tool-metadata.ts +139 -0
- package/dist/extensions/pi-tool-display/src/tool-overrides.ts +2168 -0
- package/dist/extensions/pi-tool-display/src/types.ts +109 -0
- package/dist/extensions/pi-tool-display/src/user-message-box-markdown.ts +64 -0
- package/dist/extensions/pi-tool-display/src/user-message-box-native.ts +66 -0
- package/dist/extensions/pi-tool-display/src/user-message-box-patch.ts +72 -0
- package/dist/extensions/pi-tool-display/src/user-message-box-renderer.ts +419 -0
- package/dist/extensions/pi-tool-display/src/user-message-box-utils.ts +106 -0
- package/dist/extensions/pi-tool-display/src/write-display-utils.ts +31 -0
- package/dist/extensions/pi-tool-display/src/zellij-modal.ts +1027 -0
- package/dist/extensions/pi-tool-display/tests/active-backlog-red.test.ts +65 -0
- package/dist/extensions/pi-tool-display/tests/ansi-utils.test.ts +345 -0
- package/dist/extensions/pi-tool-display/tests/bash-display.test.ts +442 -0
- package/dist/extensions/pi-tool-display/tests/capabilities-edge.test.ts +360 -0
- package/dist/extensions/pi-tool-display/tests/capabilities.test.ts +54 -0
- package/dist/extensions/pi-tool-display/tests/config-modal.test.ts +360 -0
- package/dist/extensions/pi-tool-display/tests/config-store.test.ts +87 -0
- package/dist/extensions/pi-tool-display/tests/custom-tool-overrides.test.ts +415 -0
- package/dist/extensions/pi-tool-display/tests/debug-logger-edge.test.ts +630 -0
- package/dist/extensions/pi-tool-display/tests/debug-logger.test.ts +62 -0
- package/dist/extensions/pi-tool-display/tests/diff-renderer-ansi.test.ts +55 -0
- package/dist/extensions/pi-tool-display/tests/diff-renderer-edge.test.ts +886 -0
- package/dist/extensions/pi-tool-display/tests/diff-renderer-numbering.test.ts +135 -0
- package/dist/extensions/pi-tool-display/tests/diff-renderer-width.test.ts +454 -0
- package/dist/extensions/pi-tool-display/tests/diff-renderer-wrap.test.ts +169 -0
- package/dist/extensions/pi-tool-display/tests/index-integration.test.ts +354 -0
- package/dist/extensions/pi-tool-display/tests/pending-diff-preview.test.ts +79 -0
- package/dist/extensions/pi-tool-display/tests/presets-edge.test.ts +229 -0
- package/dist/extensions/pi-tool-display/tests/presets.test.ts +26 -0
- package/dist/extensions/pi-tool-display/tests/reload-behavior.test.ts +1241 -0
- package/dist/extensions/pi-tool-display/tests/render-utils.test.ts +710 -0
- package/dist/extensions/pi-tool-display/tests/settings-inspector.test.ts +450 -0
- package/dist/extensions/pi-tool-display/tests/thinking-label-edge.test.ts +904 -0
- package/dist/extensions/pi-tool-display/tests/thinking-label.test.ts +72 -0
- package/dist/extensions/pi-tool-display/tests/tool-call-box.test.ts +105 -0
- package/dist/extensions/pi-tool-display/tests/tool-overrides-config.test.ts +613 -0
- package/dist/extensions/pi-tool-display/tests/tool-overrides-mcp.test.ts +499 -0
- package/dist/extensions/pi-tool-display/tests/tool-overrides-registration.test.ts +315 -0
- package/dist/extensions/pi-tool-display/tests/tool-overrides-write-meta.test.ts +68 -0
- package/dist/extensions/pi-tool-display/tests/tool-ui-utils.test.ts +413 -0
- package/dist/extensions/pi-tool-display/tests/user-message-box-performance-red.test.ts +176 -0
- package/dist/extensions/pi-tool-display/tests/user-message-box.test.ts +908 -0
- package/dist/extensions/pi-tool-display/tests/zellij-modal.test.ts +848 -0
- package/dist/extensions/pi-tool-display/tool-display-api-consumer.d.ts +30 -0
- package/dist/extensions/pi-tool-display/tool-display-api-consumer.js +40 -0
- package/dist/extensions/pi-zentui/extensions/zentui/format.ts +46 -1
- package/dist/extensions/pi-zentui/extensions/zentui/index.ts +10 -0
- package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +1 -1
- package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +3 -3
- package/dist/extensions/pi-zentui/test/format.test.ts +50 -2
- package/dist/modes/interactive/components/startup-box.d.ts +38 -0
- package/dist/modes/interactive/components/startup-box.js +90 -0
- package/dist/modes/interactive/interactive-mode.d.ts +3 -0
- package/dist/modes/interactive/interactive-mode.js +85 -122
- package/dist/skills/agent-browser/SKILL.md +1 -0
- package/dist/skills/batch-grill-me/SKILL.md +1 -0
- package/dist/skills/brandkit/SKILL.md +1 -0
- package/dist/skills/design-references/SKILL.md +1 -0
- package/dist/skills/design-taste-frontend/SKILL.md +1 -0
- package/dist/skills/design-taste-frontend-v1/SKILL.md +1 -0
- package/dist/skills/full-output-enforcement/SKILL.md +1 -0
- package/dist/skills/gpt-taste/SKILL.md +1 -0
- package/dist/skills/grill-me/SKILL.md +1 -0
- package/dist/skills/handoff/SKILL.md +1 -0
- package/dist/skills/handoff-text/SKILL.md +1 -0
- package/dist/skills/high-end-visual-design/SKILL.md +1 -0
- package/dist/skills/image-to-code/SKILL.md +1 -0
- package/dist/skills/imagegen-frontend-mobile/SKILL.md +1 -0
- package/dist/skills/imagegen-frontend-web/SKILL.md +1 -0
- package/dist/skills/implanger/SKILL.md +1 -0
- package/dist/skills/improve-codebase/SKILL.md +1 -0
- package/dist/skills/industrial-brutalist-ui/SKILL.md +1 -0
- package/dist/skills/minimalist-ui/SKILL.md +1 -0
- package/dist/skills/pi-subagents/SKILL.md +1 -0
- package/dist/skills/planger/SKILL.md +1 -0
- package/dist/skills/ponytail/SKILL.md +1 -0
- package/dist/skills/ponytail-audit/SKILL.md +1 -0
- package/dist/skills/ponytail-debt/SKILL.md +1 -0
- package/dist/skills/ponytail-gain/SKILL.md +1 -0
- package/dist/skills/ponytail-help/SKILL.md +1 -0
- package/dist/skills/ponytail-review/SKILL.md +1 -0
- package/dist/skills/redesign-existing-projects/SKILL.md +1 -0
- package/dist/skills/selesai-handoff/SKILL.md +1 -0
- package/dist/skills/stitch-design-taste/SKILL.md +1 -0
- package/dist/skills/unlazy/SKILL.md +1 -0
- package/dist/skills/web-design-guidelines/SKILL.md +1 -0
- package/dist/skills/workflow/SKILL.md +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
statSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { appendFile } from "node:fs/promises";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import test from "node:test";
|
|
15
|
+
import {
|
|
16
|
+
createToolDisplayDebugLogger,
|
|
17
|
+
type ToolDisplayDebugLoggerOptions,
|
|
18
|
+
} from "../src/debug-logger.ts";
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Helpers
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
async function withTempRoot(
|
|
25
|
+
run: (root: string) => Promise<void> | void,
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
const root = mkdtempSync(join(tmpdir(), "pi-tool-display-debug-edge-"));
|
|
28
|
+
try {
|
|
29
|
+
await run(root);
|
|
30
|
+
} finally {
|
|
31
|
+
rmSync(root, { recursive: true, force: true });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function loggerOpts(
|
|
36
|
+
root: string,
|
|
37
|
+
overrides: Partial<ToolDisplayDebugLoggerOptions> = {},
|
|
38
|
+
): ToolDisplayDebugLoggerOptions {
|
|
39
|
+
return {
|
|
40
|
+
configFile: join(root, "config.json"),
|
|
41
|
+
debugDir: join(root, "debug"),
|
|
42
|
+
debugLogFile: join(root, "debug", "debug.log"),
|
|
43
|
+
createDate: () => new Date("2026-01-01T00:00:00.000Z"),
|
|
44
|
+
now: () => 1000000,
|
|
45
|
+
...overrides,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function writeConfig(root: string, debug: boolean): void {
|
|
50
|
+
writeFileSync(join(root, "config.json"), JSON.stringify({ debug }), "utf-8");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readLog(root: string): string {
|
|
54
|
+
const logFile = join(root, "debug", "debug.log");
|
|
55
|
+
assert.ok(existsSync(logFile), "debug.log should exist");
|
|
56
|
+
return readFileSync(logFile, "utf-8");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Concurrent Write Queue Behavior
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
test("concurrent writes are queued and flushed in order", async () => {
|
|
64
|
+
await withTempRoot(async (root) => {
|
|
65
|
+
writeConfig(root, true);
|
|
66
|
+
const logger = createToolDisplayDebugLogger(loggerOpts(root));
|
|
67
|
+
|
|
68
|
+
logger.log("first");
|
|
69
|
+
logger.log("second");
|
|
70
|
+
logger.log("third");
|
|
71
|
+
|
|
72
|
+
await logger.flush();
|
|
73
|
+
|
|
74
|
+
const content = readLog(root);
|
|
75
|
+
const lines = content.trim().split("\n");
|
|
76
|
+
assert.equal(lines.length, 3, "all three lines written");
|
|
77
|
+
assert.match(lines[0]!, /first/);
|
|
78
|
+
assert.match(lines[1]!, /second/);
|
|
79
|
+
assert.match(lines[2]!, /third/);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("concurrent writes with simulated failures still complete", async () => {
|
|
84
|
+
await withTempRoot(async (root) => {
|
|
85
|
+
writeConfig(root, true);
|
|
86
|
+
let appendCallCount = 0;
|
|
87
|
+
const failingAppend = async (
|
|
88
|
+
_path: string,
|
|
89
|
+
_content: string,
|
|
90
|
+
_enc: string,
|
|
91
|
+
): Promise<void> => {
|
|
92
|
+
appendCallCount++;
|
|
93
|
+
if (appendCallCount === 2) {
|
|
94
|
+
throw new Error("simulated append failure");
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const logger = createToolDisplayDebugLogger(
|
|
99
|
+
loggerOpts(root, {
|
|
100
|
+
fileSystem: {
|
|
101
|
+
existsSync,
|
|
102
|
+
mkdirSync,
|
|
103
|
+
readFileSync,
|
|
104
|
+
statSync: ((p: string) => {
|
|
105
|
+
try {
|
|
106
|
+
return statSync(p);
|
|
107
|
+
} catch {
|
|
108
|
+
throw new Error("ENOENT");
|
|
109
|
+
}
|
|
110
|
+
}) as unknown as typeof statSync,
|
|
111
|
+
appendFile: failingAppend as unknown as typeof appendFile,
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
logger.log("first ok");
|
|
117
|
+
logger.log("second fails");
|
|
118
|
+
logger.log("third ok");
|
|
119
|
+
|
|
120
|
+
await logger.flush();
|
|
121
|
+
assert.ok(true, "flush completed despite append failures");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("flush waits for all queued writes", async () => {
|
|
126
|
+
await withTempRoot(async (root) => {
|
|
127
|
+
writeConfig(root, true);
|
|
128
|
+
|
|
129
|
+
const order: string[] = [];
|
|
130
|
+
const trackingAppend = async (
|
|
131
|
+
_path: string,
|
|
132
|
+
content: string,
|
|
133
|
+
_enc: string,
|
|
134
|
+
): Promise<void> => {
|
|
135
|
+
if (content.includes("slow")) {
|
|
136
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
137
|
+
}
|
|
138
|
+
order.push(content.trim());
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const logger = createToolDisplayDebugLogger(
|
|
142
|
+
loggerOpts(root, {
|
|
143
|
+
fileSystem: {
|
|
144
|
+
existsSync,
|
|
145
|
+
mkdirSync,
|
|
146
|
+
readFileSync,
|
|
147
|
+
statSync: ((p: string) => {
|
|
148
|
+
try {
|
|
149
|
+
return statSync(p);
|
|
150
|
+
} catch {
|
|
151
|
+
throw new Error("ENOENT");
|
|
152
|
+
}
|
|
153
|
+
}) as unknown as typeof statSync,
|
|
154
|
+
appendFile: trackingAppend as unknown as typeof appendFile,
|
|
155
|
+
},
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
logger.log("fast");
|
|
160
|
+
logger.log("slow");
|
|
161
|
+
logger.log("fast2");
|
|
162
|
+
|
|
163
|
+
await logger.flush();
|
|
164
|
+
|
|
165
|
+
assert.ok(order.some((l) => l.includes("fast")), "fast logged");
|
|
166
|
+
assert.ok(order.some((l) => l.includes("slow")), "slow logged");
|
|
167
|
+
assert.ok(order.some((l) => l.includes("fast2")), "fast2 logged");
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Redaction Patterns
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
test("redaction masks API-key-style tokens in messages", async () => {
|
|
176
|
+
await withTempRoot(async (root) => {
|
|
177
|
+
writeConfig(root, true);
|
|
178
|
+
const logger = createToolDisplayDebugLogger(loggerOpts(root));
|
|
179
|
+
|
|
180
|
+
// The SECRET_VALUE_PATTERN matches sk-[A-Za-z0-9_-]{12,}
|
|
181
|
+
// We need exactly the pattern threshold to trigger redaction.
|
|
182
|
+
logger.log("key sk-abcdefghijklmnop");
|
|
183
|
+
await logger.flush();
|
|
184
|
+
|
|
185
|
+
const content = readLog(root);
|
|
186
|
+
assert.doesNotMatch(content, /sk-abcdefghijklmnop/);
|
|
187
|
+
assert.match(content, /\[REDACTED\]/);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("redaction masks JWT-style token patterns in messages", async () => {
|
|
192
|
+
await withTempRoot(async (root) => {
|
|
193
|
+
writeConfig(root, true);
|
|
194
|
+
const logger = createToolDisplayDebugLogger(loggerOpts(root));
|
|
195
|
+
|
|
196
|
+
// Pattern matches groups of 24+ . 6+ . 12+ alphanum chars
|
|
197
|
+
logger.log("jwt TOKEN_STR=aabbccddeeffgghhiijjkkll.aabbcc.1234567890ab");
|
|
198
|
+
await logger.flush();
|
|
199
|
+
|
|
200
|
+
const content = readLog(root);
|
|
201
|
+
assert.doesNotMatch(
|
|
202
|
+
content,
|
|
203
|
+
/aabbccddeeffgghhiijjkkll\.aabbcc\.1234567890ab/,
|
|
204
|
+
);
|
|
205
|
+
assert.match(content, /\[REDACTED\]/);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("redaction does not match non-secret IDs that lack secret patterns", async () => {
|
|
210
|
+
await withTempRoot(async (root) => {
|
|
211
|
+
writeConfig(root, true);
|
|
212
|
+
const logger = createToolDisplayDebugLogger(loggerOpts(root));
|
|
213
|
+
|
|
214
|
+
// Short sk- pattern (< 12 chars after prefix) should NOT be redacted
|
|
215
|
+
// Hex commit hashes without sk- prefix should NOT be redacted
|
|
216
|
+
// Session IDs should NOT be redacted
|
|
217
|
+
logger.log("session: abc-123 def-456");
|
|
218
|
+
logger.log("hash: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0");
|
|
219
|
+
logger.log("short-key: sk-short");
|
|
220
|
+
await logger.flush();
|
|
221
|
+
|
|
222
|
+
const content = readLog(root);
|
|
223
|
+
assert.match(content, /abc-123 def-456/);
|
|
224
|
+
assert.match(content, /a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0/);
|
|
225
|
+
assert.match(content, /short-key: sk-short/);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("redaction handles tokens with hyphens and underscores in body", async () => {
|
|
230
|
+
await withTempRoot(async (root) => {
|
|
231
|
+
writeConfig(root, true);
|
|
232
|
+
const logger = createToolDisplayDebugLogger(loggerOpts(root));
|
|
233
|
+
|
|
234
|
+
// JWT body with hyphen/underscore should still match [A-Za-z0-9_-]
|
|
235
|
+
logger.log("token: aabbccddeeffgghhiijjkkll.abc-def_ghi.1234567890ab");
|
|
236
|
+
await logger.flush();
|
|
237
|
+
|
|
238
|
+
const content = readLog(root);
|
|
239
|
+
assert.doesNotMatch(content, /abc-def_ghi\.1234567890ab/);
|
|
240
|
+
assert.match(content, /\[REDACTED\]/);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// Config File Fingerprint Caching
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
test("fingerprint cache avoids re-reading config within TTL", async () => {
|
|
249
|
+
await withTempRoot(async (root) => {
|
|
250
|
+
let readCount = 0;
|
|
251
|
+
const trackingReadFileSync = (path: string, enc: string): string => {
|
|
252
|
+
readCount++;
|
|
253
|
+
return readFileSync(path, enc as BufferEncoding);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
writeConfig(root, true);
|
|
257
|
+
|
|
258
|
+
const logger = createToolDisplayDebugLogger(
|
|
259
|
+
loggerOpts(root, {
|
|
260
|
+
now: () => 1000000,
|
|
261
|
+
fileSystem: {
|
|
262
|
+
existsSync,
|
|
263
|
+
mkdirSync,
|
|
264
|
+
readFileSync: trackingReadFileSync as unknown as typeof readFileSync,
|
|
265
|
+
statSync: ((p: string) => {
|
|
266
|
+
try {
|
|
267
|
+
return statSync(p);
|
|
268
|
+
} catch {
|
|
269
|
+
throw new Error("ENOENT");
|
|
270
|
+
}
|
|
271
|
+
}) as unknown as typeof statSync,
|
|
272
|
+
appendFile: async () => {},
|
|
273
|
+
},
|
|
274
|
+
}),
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
logger.log("first");
|
|
278
|
+
const readsAfterFirst = readCount;
|
|
279
|
+
assert.equal(readsAfterFirst >= 1, true, "config read at least once on first log");
|
|
280
|
+
|
|
281
|
+
logger.log("second");
|
|
282
|
+
assert.equal(readCount, readsAfterFirst, "no additional read within TTL");
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("fingerprint cache re-reads config for a newly created logger after file change", async () => {
|
|
287
|
+
await withTempRoot(async (root) => {
|
|
288
|
+
// Use a real-time logger to verify config change detection
|
|
289
|
+
writeConfig(root, false);
|
|
290
|
+
|
|
291
|
+
const logger = createToolDisplayDebugLogger(
|
|
292
|
+
loggerOpts(root, { cacheTtlMs: 30, now: () => Date.now() }),
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
logger.log("first-msg");
|
|
296
|
+
await logger.flush();
|
|
297
|
+
|
|
298
|
+
// No log file should exist while debug is disabled
|
|
299
|
+
assert.equal(
|
|
300
|
+
existsSync(join(root, "debug", "debug.log")),
|
|
301
|
+
false,
|
|
302
|
+
"no log while disabled",
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// Enable debug and wait for TTL to pass
|
|
306
|
+
writeFileSync(
|
|
307
|
+
join(root, "config.json"),
|
|
308
|
+
JSON.stringify({ debug: true }),
|
|
309
|
+
"utf-8",
|
|
310
|
+
);
|
|
311
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
312
|
+
|
|
313
|
+
logger.log("after-enable");
|
|
314
|
+
await logger.flush();
|
|
315
|
+
|
|
316
|
+
assert.ok(
|
|
317
|
+
existsSync(join(root, "debug", "debug.log")),
|
|
318
|
+
"log created after enabling",
|
|
319
|
+
);
|
|
320
|
+
const content = readLog(root);
|
|
321
|
+
assert.match(content, /after-enable/, "after-enable message appears");
|
|
322
|
+
assert.doesNotMatch(content, /first-msg/, "first message not logged when disabled");
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
// Debug Directory Creation Timing
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
|
|
330
|
+
test("debug directory is created only on first enabled write", async () => {
|
|
331
|
+
await withTempRoot(async (root) => {
|
|
332
|
+
writeConfig(root, true);
|
|
333
|
+
|
|
334
|
+
const logger = createToolDisplayDebugLogger(
|
|
335
|
+
loggerOpts(root, { now: () => 1000000 }),
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
assert.equal(
|
|
339
|
+
existsSync(join(root, "debug")),
|
|
340
|
+
false,
|
|
341
|
+
"dir not yet created before first write",
|
|
342
|
+
);
|
|
343
|
+
logger.log("first enabled write");
|
|
344
|
+
await logger.flush();
|
|
345
|
+
|
|
346
|
+
assert.ok(existsSync(join(root, "debug")), "debug dir created on write");
|
|
347
|
+
assert.ok(existsSync(join(root, "debug", "debug.log")), "debug.log created");
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("disabled logger does not create debug directory", async () => {
|
|
352
|
+
await withTempRoot(async (root) => {
|
|
353
|
+
writeConfig(root, false);
|
|
354
|
+
|
|
355
|
+
const logger = createToolDisplayDebugLogger(
|
|
356
|
+
loggerOpts(root, { now: () => 1000000 }),
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
logger.log("disabled message");
|
|
360
|
+
await logger.flush();
|
|
361
|
+
|
|
362
|
+
assert.equal(
|
|
363
|
+
existsSync(join(root, "debug")),
|
|
364
|
+
false,
|
|
365
|
+
"no debug dir when disabled",
|
|
366
|
+
);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
// Swallowed Logging Errors
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
test("logger swallows errors when config file is unreadable", async () => {
|
|
375
|
+
await withTempRoot(async (root) => {
|
|
376
|
+
writeFileSync(join(root, "config.json"), "not-valid-json", "utf-8");
|
|
377
|
+
|
|
378
|
+
const logger = createToolDisplayDebugLogger(
|
|
379
|
+
loggerOpts(root, { now: () => 1000000 }),
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
assert.doesNotThrow(() => logger.log("after bad config"));
|
|
383
|
+
await assert.doesNotReject(() => logger.flush());
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("logger swallows errors when config file is missing", async () => {
|
|
388
|
+
await withTempRoot(async (root) => {
|
|
389
|
+
const logger = createToolDisplayDebugLogger(
|
|
390
|
+
loggerOpts(root, { now: () => 1000000 }),
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
assert.doesNotThrow(() => logger.log("no config file"));
|
|
394
|
+
await assert.doesNotReject(() => logger.flush());
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("logger swallows directory creation errors", async () => {
|
|
399
|
+
await withTempRoot(async (root) => {
|
|
400
|
+
writeConfig(root, true);
|
|
401
|
+
|
|
402
|
+
// Block the debug directory with a regular file
|
|
403
|
+
writeFileSync(join(root, "debug"), "blocking file", "utf-8");
|
|
404
|
+
|
|
405
|
+
const logger = createToolDisplayDebugLogger(
|
|
406
|
+
loggerOpts(root, { now: () => 1000000 }),
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
assert.doesNotThrow(() => logger.log("after blocked mkdir"));
|
|
410
|
+
await assert.doesNotReject(() => logger.flush());
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("logger swallows individual append failures without crashing", async () => {
|
|
415
|
+
await withTempRoot(async (root) => {
|
|
416
|
+
writeConfig(root, true);
|
|
417
|
+
|
|
418
|
+
let failNext = false;
|
|
419
|
+
const flakyAppend = async (
|
|
420
|
+
_path: string,
|
|
421
|
+
_content: string,
|
|
422
|
+
_enc: string,
|
|
423
|
+
): Promise<void> => {
|
|
424
|
+
if (failNext) {
|
|
425
|
+
failNext = false;
|
|
426
|
+
throw new Error("flaky error");
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const logger = createToolDisplayDebugLogger(
|
|
431
|
+
loggerOpts(root, {
|
|
432
|
+
now: () => 1000000,
|
|
433
|
+
fileSystem: {
|
|
434
|
+
existsSync,
|
|
435
|
+
mkdirSync,
|
|
436
|
+
readFileSync,
|
|
437
|
+
statSync: ((p: string) => {
|
|
438
|
+
try {
|
|
439
|
+
return statSync(p);
|
|
440
|
+
} catch {
|
|
441
|
+
throw new Error("ENOENT");
|
|
442
|
+
}
|
|
443
|
+
}) as unknown as typeof statSync,
|
|
444
|
+
appendFile: flakyAppend as unknown as typeof appendFile,
|
|
445
|
+
},
|
|
446
|
+
}),
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
failNext = true;
|
|
450
|
+
logger.log("this will fail");
|
|
451
|
+
await logger.flush();
|
|
452
|
+
|
|
453
|
+
logger.log("this should succeed");
|
|
454
|
+
await logger.flush();
|
|
455
|
+
|
|
456
|
+
assert.ok(true, "logging errors swallowed without crashing logger");
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// ---------------------------------------------------------------------------
|
|
461
|
+
// Log File Append Behavior
|
|
462
|
+
// ---------------------------------------------------------------------------
|
|
463
|
+
|
|
464
|
+
test("multiple writes append to the same log file", async () => {
|
|
465
|
+
await withTempRoot(async (root) => {
|
|
466
|
+
writeConfig(root, true);
|
|
467
|
+
const logger = createToolDisplayDebugLogger(
|
|
468
|
+
loggerOpts(root, { now: () => 1000000 }),
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
logger.log("entry-a");
|
|
472
|
+
logger.log("entry-b");
|
|
473
|
+
logger.log("entry-c");
|
|
474
|
+
await logger.flush();
|
|
475
|
+
|
|
476
|
+
const content = readLog(root);
|
|
477
|
+
const lines = content.trim().split("\n");
|
|
478
|
+
assert.equal(lines.length, 3, "three entries appended");
|
|
479
|
+
assert.match(lines[0]!, /entry-a/);
|
|
480
|
+
assert.match(lines[1]!, /entry-b/);
|
|
481
|
+
assert.match(lines[2]!, /entry-c/);
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
test("log lines include ISO timestamp prefix", async () => {
|
|
486
|
+
await withTempRoot(async (root) => {
|
|
487
|
+
writeConfig(root, true);
|
|
488
|
+
const logger = createToolDisplayDebugLogger(loggerOpts(root));
|
|
489
|
+
|
|
490
|
+
logger.log("timed entry");
|
|
491
|
+
await logger.flush();
|
|
492
|
+
|
|
493
|
+
const content = readLog(root);
|
|
494
|
+
assert.match(
|
|
495
|
+
content,
|
|
496
|
+
/^2026-01-01T00:00:00\.000Z /,
|
|
497
|
+
"line starts with ISO timestamp",
|
|
498
|
+
);
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test("log lines include error details after the message", async () => {
|
|
503
|
+
await withTempRoot(async (root) => {
|
|
504
|
+
writeConfig(root, true);
|
|
505
|
+
const logger = createToolDisplayDebugLogger(loggerOpts(root));
|
|
506
|
+
|
|
507
|
+
logger.log("operation", new Error("something broke"));
|
|
508
|
+
await logger.flush();
|
|
509
|
+
|
|
510
|
+
const content = readLog(root);
|
|
511
|
+
assert.match(content, /operation/);
|
|
512
|
+
assert.match(content, /Error: something broke/);
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// ---------------------------------------------------------------------------
|
|
517
|
+
// Config Change After Logger Creation
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
|
|
520
|
+
test("logger picks up config from disabled to enabled after cache expiry", async () => {
|
|
521
|
+
await withTempRoot(async (root) => {
|
|
522
|
+
writeConfig(root, false);
|
|
523
|
+
|
|
524
|
+
const ttlMs = 40;
|
|
525
|
+
const logger = createToolDisplayDebugLogger(
|
|
526
|
+
loggerOpts(root, {
|
|
527
|
+
cacheTtlMs: ttlMs,
|
|
528
|
+
now: () => Date.now(),
|
|
529
|
+
}),
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
logger.log("disabled-message");
|
|
533
|
+
await logger.flush();
|
|
534
|
+
assert.equal(
|
|
535
|
+
existsSync(join(root, "debug")),
|
|
536
|
+
false,
|
|
537
|
+
"no debug dir while disabled",
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
// Switch to enabled
|
|
541
|
+
writeFileSync(join(root, "config.json"), JSON.stringify({ debug: true }), "utf-8");
|
|
542
|
+
|
|
543
|
+
// Wait for cache TTL and file system settle
|
|
544
|
+
await new Promise((r) => setTimeout(r, ttlMs * 2 + 50));
|
|
545
|
+
|
|
546
|
+
logger.log("enabled-message");
|
|
547
|
+
await logger.flush();
|
|
548
|
+
|
|
549
|
+
assert.ok(
|
|
550
|
+
existsSync(join(root, "debug", "debug.log")),
|
|
551
|
+
"log file created after enabling",
|
|
552
|
+
);
|
|
553
|
+
const content = readLog(root);
|
|
554
|
+
assert.match(content, /enabled-message/, "enabled message appears");
|
|
555
|
+
assert.doesNotMatch(content, /disabled-message/, "disabled message not in log");
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test("logger picks up config from enabled to disabled", async () => {
|
|
560
|
+
await withTempRoot(async (root) => {
|
|
561
|
+
writeConfig(root, true);
|
|
562
|
+
|
|
563
|
+
const ttlMs = 40;
|
|
564
|
+
const logger = createToolDisplayDebugLogger(
|
|
565
|
+
loggerOpts(root, {
|
|
566
|
+
cacheTtlMs: ttlMs,
|
|
567
|
+
now: () => Date.now(),
|
|
568
|
+
}),
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
logger.log("before-disable");
|
|
572
|
+
await logger.flush();
|
|
573
|
+
assert.ok(
|
|
574
|
+
existsSync(join(root, "debug", "debug.log")),
|
|
575
|
+
"log exists while enabled",
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
writeFileSync(join(root, "config.json"), JSON.stringify({ debug: false }), "utf-8");
|
|
579
|
+
|
|
580
|
+
await new Promise((r) => setTimeout(r, ttlMs * 2 + 50));
|
|
581
|
+
|
|
582
|
+
logger.log("after-disable");
|
|
583
|
+
await logger.flush();
|
|
584
|
+
|
|
585
|
+
const content = readLog(root);
|
|
586
|
+
assert.match(content, /before-disable/, "before-disable message present");
|
|
587
|
+
assert.doesNotMatch(
|
|
588
|
+
content,
|
|
589
|
+
/after-disable/,
|
|
590
|
+
"after-disable message not logged",
|
|
591
|
+
);
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// ---------------------------------------------------------------------------
|
|
596
|
+
// Error Redaction in Error Objects
|
|
597
|
+
// ---------------------------------------------------------------------------
|
|
598
|
+
|
|
599
|
+
test("error messages containing secrets are redacted", async () => {
|
|
600
|
+
await withTempRoot(async (root) => {
|
|
601
|
+
writeConfig(root, true);
|
|
602
|
+
const logger = createToolDisplayDebugLogger(loggerOpts(root));
|
|
603
|
+
|
|
604
|
+
// The secret value pattern matches sk-[A-Za-z0-9_-]{12,}
|
|
605
|
+
const secretKey = "sk-" + "a".repeat(14) + "b".repeat(8);
|
|
606
|
+
logger.log("auth failed", new Error("invalid key " + secretKey));
|
|
607
|
+
await logger.flush();
|
|
608
|
+
|
|
609
|
+
const content = readLog(root);
|
|
610
|
+
assert.doesNotMatch(content, new RegExp(secretKey.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")));
|
|
611
|
+
assert.match(content, /\[REDACTED\]/);
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
test("non-Error error arguments are converted to string", async () => {
|
|
616
|
+
await withTempRoot(async (root) => {
|
|
617
|
+
writeConfig(root, true);
|
|
618
|
+
const logger = createToolDisplayDebugLogger(loggerOpts(root));
|
|
619
|
+
|
|
620
|
+
logger.log("string error", "just a string");
|
|
621
|
+
logger.log("number error", 42);
|
|
622
|
+
logger.log("object error", { code: 500 });
|
|
623
|
+
await logger.flush();
|
|
624
|
+
|
|
625
|
+
const content = readLog(root);
|
|
626
|
+
assert.match(content, /just a string/);
|
|
627
|
+
assert.match(content, /42/);
|
|
628
|
+
assert.match(content, /\[object Object\]/);
|
|
629
|
+
});
|
|
630
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { createToolDisplayDebugLogger } from "../src/debug-logger.ts";
|
|
7
|
+
|
|
8
|
+
async function withTempRoot(run: (root: string) => Promise<void> | void): Promise<void> {
|
|
9
|
+
const root = mkdtempSync(join(tmpdir(), "pi-tool-display-debug-"));
|
|
10
|
+
try {
|
|
11
|
+
await run(root);
|
|
12
|
+
} finally {
|
|
13
|
+
rmSync(root, { recursive: true, force: true });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createLogger(root: string) {
|
|
18
|
+
return createToolDisplayDebugLogger({
|
|
19
|
+
configFile: join(root, "config.json"),
|
|
20
|
+
debugDir: join(root, "debug"),
|
|
21
|
+
debugLogFile: join(root, "debug", "debug.log"),
|
|
22
|
+
createDate: () => new Date("2026-01-01T00:00:00.000Z"),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test("disabled debug logger is a no-op and does not create debug artifacts", async () => {
|
|
27
|
+
await withTempRoot(async (root) => {
|
|
28
|
+
writeFileSync(join(root, "config.json"), JSON.stringify({ debug: false }), "utf-8");
|
|
29
|
+
const logger = createLogger(root);
|
|
30
|
+
|
|
31
|
+
assert.equal(logger.log("disabled sk-abcdefghijkl", new Error("hidden sk-bcdefghijklm")), undefined);
|
|
32
|
+
await logger.flush();
|
|
33
|
+
|
|
34
|
+
assert.equal(existsSync(join(root, "debug")), false);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("enabled debug logger writes on flush and redacts secret values", async () => {
|
|
39
|
+
await withTempRoot(async (root) => {
|
|
40
|
+
writeFileSync(join(root, "config.json"), JSON.stringify({ debug: true }), "utf-8");
|
|
41
|
+
const logger = createLogger(root);
|
|
42
|
+
|
|
43
|
+
assert.equal(logger.log("request sk-abcdefghijkl", new Error("failed sk-bcdefghijklm")), undefined);
|
|
44
|
+
await logger.flush();
|
|
45
|
+
|
|
46
|
+
const logContent = readFileSync(join(root, "debug", "debug.log"), "utf-8");
|
|
47
|
+
assert.match(logContent, /^2026-01-01T00:00:00\.000Z request \[REDACTED\] Error: failed \[REDACTED\]/);
|
|
48
|
+
assert.doesNotMatch(logContent, /sk-abcdefghijkl|sk-bcdefghijklm/);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("debug logger swallows append failures", async () => {
|
|
53
|
+
await withTempRoot(async (root) => {
|
|
54
|
+
writeFileSync(join(root, "config.json"), JSON.stringify({ debug: true }), "utf-8");
|
|
55
|
+
mkdirSync(join(root, "debug"), { recursive: true });
|
|
56
|
+
mkdirSync(join(root, "debug", "debug.log"));
|
|
57
|
+
const logger = createLogger(root);
|
|
58
|
+
|
|
59
|
+
assert.doesNotThrow(() => logger.log("write-fails"));
|
|
60
|
+
await assert.doesNotReject(() => logger.flush());
|
|
61
|
+
});
|
|
62
|
+
});
|