loggily 0.3.0 → 0.4.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/README.md +67 -22
- package/package.json +24 -11
- package/src/context.ts +26 -11
- package/src/core.ts +118 -72
- package/src/file-writer.ts +12 -6
- package/src/index.browser.ts +9 -1
- package/src/index.ts +9 -1
- package/src/tracing.ts +11 -3
- package/src/worker.ts +119 -132
- package/.github/workflows/docs.yml +0 -58
- package/.github/workflows/release.yml +0 -31
- package/.github/workflows/test.yml +0 -20
- package/CHANGELOG.md +0 -45
- package/CLAUDE.md +0 -299
- package/CONTRIBUTING.md +0 -58
- package/benchmarks/overhead.ts +0 -267
- package/bun.lock +0 -479
- package/docs/api-reference.md +0 -400
- package/docs/benchmarks.md +0 -106
- package/docs/comparison.md +0 -315
- package/docs/conditional-logging-research.md +0 -159
- package/docs/guide.md +0 -205
- package/docs/migration-from-debug.md +0 -310
- package/docs/migration-from-pino.md +0 -178
- package/docs/migration-from-winston.md +0 -179
- package/docs/site/.vitepress/config.ts +0 -67
- package/docs/site/api/configuration.md +0 -94
- package/docs/site/api/index.md +0 -61
- package/docs/site/api/logger.md +0 -99
- package/docs/site/api/worker.md +0 -120
- package/docs/site/api/writers.md +0 -69
- package/docs/site/guide/getting-started.md +0 -143
- package/docs/site/guide/journey.md +0 -203
- package/docs/site/guide/migration-from-debug.md +0 -24
- package/docs/site/guide/spans.md +0 -139
- package/docs/site/guide/why.md +0 -55
- package/docs/site/guide/workers.md +0 -113
- package/docs/site/guide/zero-overhead.md +0 -87
- package/docs/site/index.md +0 -54
- package/tests/features.test.ts +0 -552
- package/tests/logger.test.ts +0 -944
- package/tests/tracing.test.ts +0 -618
- package/tests/universal.test.ts +0 -107
- package/tests/worker.test.ts +0 -590
- package/tsconfig.json +0 -20
- package/vitest.config.ts +0 -10
package/tests/features.test.ts
DELETED
|
@@ -1,552 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for new logger features:
|
|
3
|
-
* 1. Lazy string interpolation
|
|
4
|
-
* 2. Child loggers with context
|
|
5
|
-
* 3. Structured logging (LOG_FORMAT=json)
|
|
6
|
-
* 4. Async file writer
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"
|
|
10
|
-
import { existsSync, readFileSync, unlinkSync } from "fs"
|
|
11
|
-
import { join } from "path"
|
|
12
|
-
import { tmpdir } from "os"
|
|
13
|
-
import {
|
|
14
|
-
createLogger,
|
|
15
|
-
setLogLevel,
|
|
16
|
-
setLogFormat,
|
|
17
|
-
getLogFormat,
|
|
18
|
-
setOutputMode,
|
|
19
|
-
resetIds,
|
|
20
|
-
disableSpans,
|
|
21
|
-
enableSpans,
|
|
22
|
-
setTraceFilter,
|
|
23
|
-
setDebugFilter,
|
|
24
|
-
createFileWriter,
|
|
25
|
-
addWriter,
|
|
26
|
-
type FileWriter,
|
|
27
|
-
} from "../src/index.ts"
|
|
28
|
-
|
|
29
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
-
const parseJSON = (s: string): Record<string, any> => JSON.parse(s)
|
|
31
|
-
|
|
32
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
-
// Test Helpers
|
|
34
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
interface CapturedLog {
|
|
37
|
-
level: string
|
|
38
|
-
message: string
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Create a mock console that captures output */
|
|
42
|
-
function createConsoleMock() {
|
|
43
|
-
const output: CapturedLog[] = []
|
|
44
|
-
const capture =
|
|
45
|
-
(level: string) =>
|
|
46
|
-
(msg: unknown): void => {
|
|
47
|
-
output.push({ level, message: String(msg) })
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
vi.spyOn(console, "debug").mockImplementation(capture("debug"))
|
|
51
|
-
vi.spyOn(console, "info").mockImplementation(capture("info"))
|
|
52
|
-
vi.spyOn(console, "warn").mockImplementation(capture("warn"))
|
|
53
|
-
vi.spyOn(console, "error").mockImplementation(capture("error"))
|
|
54
|
-
|
|
55
|
-
vi.spyOn(process.stderr, "write").mockImplementation(((chunk: string | Uint8Array) => {
|
|
56
|
-
output.push({ level: "stderr", message: String(chunk) })
|
|
57
|
-
return true
|
|
58
|
-
}) as typeof process.stderr.write)
|
|
59
|
-
|
|
60
|
-
return { output }
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
let consoleMock: ReturnType<typeof createConsoleMock>
|
|
64
|
-
|
|
65
|
-
beforeEach(() => {
|
|
66
|
-
resetIds()
|
|
67
|
-
setLogLevel("trace")
|
|
68
|
-
disableSpans()
|
|
69
|
-
setTraceFilter(null)
|
|
70
|
-
setDebugFilter(null)
|
|
71
|
-
setOutputMode("console")
|
|
72
|
-
setLogFormat("console")
|
|
73
|
-
consoleMock = createConsoleMock()
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
afterEach(() => {
|
|
77
|
-
vi.restoreAllMocks()
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
81
|
-
// 1. Lazy String Interpolation
|
|
82
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
83
|
-
|
|
84
|
-
describe("lazy string interpolation", () => {
|
|
85
|
-
test("accepts a function that returns a string", () => {
|
|
86
|
-
const log = createLogger("test")
|
|
87
|
-
log.info?.(() => "lazy message")
|
|
88
|
-
|
|
89
|
-
expect(consoleMock.output).toHaveLength(1)
|
|
90
|
-
expect(consoleMock.output[0]!.message).toContain("lazy message")
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
test("function is called when level is enabled", () => {
|
|
94
|
-
const fn = vi.fn(() => "computed value")
|
|
95
|
-
const log = createLogger("test")
|
|
96
|
-
log.info?.(fn)
|
|
97
|
-
|
|
98
|
-
expect(fn).toHaveBeenCalledTimes(1)
|
|
99
|
-
expect(consoleMock.output[0]!.message).toContain("computed value")
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
test("function is NOT called when level is disabled", () => {
|
|
103
|
-
setLogLevel("error")
|
|
104
|
-
const fn = vi.fn(() => "expensive computation")
|
|
105
|
-
const log = createLogger("test")
|
|
106
|
-
|
|
107
|
-
// debug is disabled at error level, so fn should never be called
|
|
108
|
-
log.debug?.(fn)
|
|
109
|
-
|
|
110
|
-
expect(fn).not.toHaveBeenCalled()
|
|
111
|
-
expect(consoleMock.output).toHaveLength(0)
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
test("function is NOT called when namespace is filtered out", () => {
|
|
115
|
-
setDebugFilter(["allowed"])
|
|
116
|
-
const fn = vi.fn(() => "expensive computation")
|
|
117
|
-
const log = createLogger("blocked")
|
|
118
|
-
|
|
119
|
-
log.info?.(fn)
|
|
120
|
-
|
|
121
|
-
expect(fn).not.toHaveBeenCalled()
|
|
122
|
-
expect(consoleMock.output).toHaveLength(0)
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
test("string messages still work unchanged", () => {
|
|
126
|
-
const log = createLogger("test")
|
|
127
|
-
log.info?.("plain string")
|
|
128
|
-
|
|
129
|
-
expect(consoleMock.output).toHaveLength(1)
|
|
130
|
-
expect(consoleMock.output[0]!.message).toContain("plain string")
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
test("lazy messages work with data parameter", () => {
|
|
134
|
-
const log = createLogger("test")
|
|
135
|
-
log.info?.(() => "lazy with data", { key: "value" })
|
|
136
|
-
|
|
137
|
-
expect(consoleMock.output).toHaveLength(1)
|
|
138
|
-
expect(consoleMock.output[0]!.message).toContain("lazy with data")
|
|
139
|
-
expect(consoleMock.output[0]!.message).toContain("key")
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
test("lazy messages work with all log levels", () => {
|
|
143
|
-
const log = createLogger("test")
|
|
144
|
-
|
|
145
|
-
log.trace?.(() => "trace lazy")
|
|
146
|
-
log.debug?.(() => "debug lazy")
|
|
147
|
-
log.info?.(() => "info lazy")
|
|
148
|
-
log.warn?.(() => "warn lazy")
|
|
149
|
-
log.error?.(() => "error lazy")
|
|
150
|
-
|
|
151
|
-
expect(consoleMock.output).toHaveLength(5)
|
|
152
|
-
expect(consoleMock.output[0]!.message).toContain("trace lazy")
|
|
153
|
-
expect(consoleMock.output[4]!.message).toContain("error lazy")
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
test("lazy messages work in JSON format", () => {
|
|
157
|
-
setLogFormat("json")
|
|
158
|
-
const log = createLogger("test")
|
|
159
|
-
log.info?.(() => "json lazy")
|
|
160
|
-
|
|
161
|
-
const parsed = parseJSON(consoleMock.output[0]!.message)
|
|
162
|
-
expect(parsed.msg).toBe("json lazy")
|
|
163
|
-
})
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
167
|
-
// 2. Child Loggers with Context
|
|
168
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
169
|
-
|
|
170
|
-
describe("child loggers with context", () => {
|
|
171
|
-
test("child({...}) creates logger with context fields", () => {
|
|
172
|
-
const log = createLogger("app")
|
|
173
|
-
const child = log.child({ requestId: "abc-123" })
|
|
174
|
-
|
|
175
|
-
child.info?.("handling request")
|
|
176
|
-
|
|
177
|
-
expect(consoleMock.output).toHaveLength(1)
|
|
178
|
-
expect(consoleMock.output[0]!.message).toContain("requestId")
|
|
179
|
-
expect(consoleMock.output[0]!.message).toContain("abc-123")
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
test("child keeps parent namespace", () => {
|
|
183
|
-
const log = createLogger("app")
|
|
184
|
-
const child = log.child({ requestId: "abc" })
|
|
185
|
-
|
|
186
|
-
expect(child.name).toBe("app")
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
test("child inherits parent props", () => {
|
|
190
|
-
const log = createLogger("app", { version: "1.0" })
|
|
191
|
-
const child = log.child({ requestId: "abc" })
|
|
192
|
-
|
|
193
|
-
expect(child.props).toEqual({ version: "1.0", requestId: "abc" })
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
test("child context is included in every log message", () => {
|
|
197
|
-
setLogFormat("json")
|
|
198
|
-
const log = createLogger("app")
|
|
199
|
-
const child = log.child({ requestId: "abc" })
|
|
200
|
-
|
|
201
|
-
child.info?.("first")
|
|
202
|
-
child.warn?.("second")
|
|
203
|
-
|
|
204
|
-
const first = parseJSON(consoleMock.output[0]!.message)
|
|
205
|
-
const second = parseJSON(consoleMock.output[1]!.message)
|
|
206
|
-
expect(first.requestId).toBe("abc")
|
|
207
|
-
expect(second.requestId).toBe("abc")
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
test("child context merges with per-call data", () => {
|
|
211
|
-
setLogFormat("json")
|
|
212
|
-
const log = createLogger("app")
|
|
213
|
-
const child = log.child({ requestId: "abc" })
|
|
214
|
-
|
|
215
|
-
child.info?.("msg", { extra: "data" })
|
|
216
|
-
|
|
217
|
-
const parsed = parseJSON(consoleMock.output[0]!.message)
|
|
218
|
-
expect(parsed.requestId).toBe("abc")
|
|
219
|
-
expect(parsed.extra).toBe("data")
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
test("nested children accumulate context", () => {
|
|
223
|
-
setLogFormat("json")
|
|
224
|
-
const log = createLogger("app")
|
|
225
|
-
const child1 = log.child({ requestId: "abc" })
|
|
226
|
-
const child2 = child1.child({ userId: "user-1" })
|
|
227
|
-
|
|
228
|
-
child2.info?.("nested context")
|
|
229
|
-
|
|
230
|
-
const parsed = parseJSON(consoleMock.output[0]!.message)
|
|
231
|
-
expect(parsed.requestId).toBe("abc")
|
|
232
|
-
expect(parsed.userId).toBe("user-1")
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
test("child context overrides parent props on conflict", () => {
|
|
236
|
-
setLogFormat("json")
|
|
237
|
-
const log = createLogger("app", { env: "prod" })
|
|
238
|
-
const child = log.child({ env: "test" })
|
|
239
|
-
|
|
240
|
-
child.info?.("override")
|
|
241
|
-
|
|
242
|
-
const parsed = parseJSON(consoleMock.output[0]!.message)
|
|
243
|
-
expect(parsed.env).toBe("test")
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
test("deprecated string child still works", () => {
|
|
247
|
-
const log = createLogger("app")
|
|
248
|
-
const child = log.child("import")
|
|
249
|
-
|
|
250
|
-
expect(child.name).toBe("app:import")
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
test("child can create spans", () => {
|
|
254
|
-
enableSpans()
|
|
255
|
-
const log = createLogger("app")
|
|
256
|
-
const child = log.child({ requestId: "abc" })
|
|
257
|
-
|
|
258
|
-
{
|
|
259
|
-
using span = child.span("work")
|
|
260
|
-
span.info?.("working")
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Check span output includes the context
|
|
264
|
-
const spanOutput = consoleMock.output.find((o) => o.message.includes("SPAN"))
|
|
265
|
-
expect(spanOutput).toBeDefined()
|
|
266
|
-
expect(spanOutput!.message).toContain("requestId")
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
test("child can create further children via .logger()", () => {
|
|
270
|
-
const log = createLogger("app")
|
|
271
|
-
const child = log.child({ requestId: "abc" })
|
|
272
|
-
const subLogger = child.logger("db")
|
|
273
|
-
|
|
274
|
-
expect(subLogger.name).toBe("app:db")
|
|
275
|
-
expect(subLogger.props).toEqual({ requestId: "abc" })
|
|
276
|
-
})
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
280
|
-
// 3. Structured Logging (LOG_FORMAT=json)
|
|
281
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
282
|
-
|
|
283
|
-
describe("LOG_FORMAT configuration", () => {
|
|
284
|
-
test("setLogFormat('json') produces JSON output", () => {
|
|
285
|
-
setLogFormat("json")
|
|
286
|
-
const log = createLogger("test")
|
|
287
|
-
|
|
288
|
-
log.info?.("json message", { key: "value" })
|
|
289
|
-
|
|
290
|
-
const parsed = parseJSON(consoleMock.output[0]!.message)
|
|
291
|
-
expect(parsed.level).toBe("info")
|
|
292
|
-
expect(parsed.name).toBe("test")
|
|
293
|
-
expect(parsed.msg).toBe("json message")
|
|
294
|
-
expect(parsed.key).toBe("value")
|
|
295
|
-
expect(parsed.time).toBeDefined()
|
|
296
|
-
})
|
|
297
|
-
|
|
298
|
-
test("setLogFormat('console') produces human-readable output", () => {
|
|
299
|
-
setLogFormat("console")
|
|
300
|
-
const log = createLogger("test")
|
|
301
|
-
|
|
302
|
-
log.info?.("console message")
|
|
303
|
-
|
|
304
|
-
const output = consoleMock.output[0]!.message
|
|
305
|
-
expect(output).toContain("INFO")
|
|
306
|
-
expect(output).toContain("test")
|
|
307
|
-
expect(output).toContain("console message")
|
|
308
|
-
// Should not be valid JSON
|
|
309
|
-
expect(() => parseJSON(output)).toThrow()
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
test("getLogFormat returns current format", () => {
|
|
313
|
-
expect(getLogFormat()).toBe("console")
|
|
314
|
-
|
|
315
|
-
setLogFormat("json")
|
|
316
|
-
expect(getLogFormat()).toBe("json")
|
|
317
|
-
|
|
318
|
-
setLogFormat("console")
|
|
319
|
-
expect(getLogFormat()).toBe("console")
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
test("JSON format includes all props", () => {
|
|
323
|
-
setLogFormat("json")
|
|
324
|
-
const log = createLogger("test", { app: "myapp", version: "1.0" })
|
|
325
|
-
|
|
326
|
-
log.info?.("message")
|
|
327
|
-
|
|
328
|
-
const parsed = parseJSON(consoleMock.output[0]!.message)
|
|
329
|
-
expect(parsed.app).toBe("myapp")
|
|
330
|
-
expect(parsed.version).toBe("1.0")
|
|
331
|
-
})
|
|
332
|
-
|
|
333
|
-
test("JSON format handles errors", () => {
|
|
334
|
-
setLogFormat("json")
|
|
335
|
-
const log = createLogger("test")
|
|
336
|
-
const err = new Error("json error")
|
|
337
|
-
|
|
338
|
-
log.error?.(err)
|
|
339
|
-
|
|
340
|
-
const parsed = parseJSON(consoleMock.output[0]!.message)
|
|
341
|
-
expect(parsed.msg).toBe("json error")
|
|
342
|
-
expect(parsed.error_type).toBe("Error")
|
|
343
|
-
})
|
|
344
|
-
|
|
345
|
-
test("JSON format works with spans", () => {
|
|
346
|
-
setLogFormat("json")
|
|
347
|
-
enableSpans()
|
|
348
|
-
const log = createLogger("test")
|
|
349
|
-
|
|
350
|
-
{
|
|
351
|
-
using span = log.span("work")
|
|
352
|
-
span.spanData.items = 5
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
const spanOutput = consoleMock.output.find((o) => {
|
|
356
|
-
try {
|
|
357
|
-
const parsed = parseJSON(o.message)
|
|
358
|
-
return parsed.level === "span"
|
|
359
|
-
} catch {
|
|
360
|
-
return false
|
|
361
|
-
}
|
|
362
|
-
})
|
|
363
|
-
expect(spanOutput).toBeDefined()
|
|
364
|
-
|
|
365
|
-
const parsed = parseJSON(spanOutput!.message)
|
|
366
|
-
expect(parsed.level).toBe("span")
|
|
367
|
-
expect(parsed.items).toBe(5)
|
|
368
|
-
})
|
|
369
|
-
|
|
370
|
-
test("JSON output has standard fields: time, level, name, msg", () => {
|
|
371
|
-
setLogFormat("json")
|
|
372
|
-
const log = createLogger("myapp")
|
|
373
|
-
|
|
374
|
-
log.info?.("request handled")
|
|
375
|
-
|
|
376
|
-
const parsed = parseJSON(consoleMock.output[0]!.message)
|
|
377
|
-
expect(parsed).toHaveProperty("time")
|
|
378
|
-
expect(parsed).toHaveProperty("level", "info")
|
|
379
|
-
expect(parsed).toHaveProperty("name", "myapp")
|
|
380
|
-
expect(parsed).toHaveProperty("msg", "request handled")
|
|
381
|
-
// time should be ISO format
|
|
382
|
-
expect(new Date(parsed.time).toISOString()).toBe(parsed.time)
|
|
383
|
-
})
|
|
384
|
-
|
|
385
|
-
describe("LOG_FORMAT env var", () => {
|
|
386
|
-
let originalLogFormat: string | undefined
|
|
387
|
-
|
|
388
|
-
beforeEach(() => {
|
|
389
|
-
originalLogFormat = process.env.LOG_FORMAT
|
|
390
|
-
})
|
|
391
|
-
|
|
392
|
-
afterEach(() => {
|
|
393
|
-
if (originalLogFormat === undefined) {
|
|
394
|
-
delete process.env.LOG_FORMAT
|
|
395
|
-
} else {
|
|
396
|
-
process.env.LOG_FORMAT = originalLogFormat
|
|
397
|
-
}
|
|
398
|
-
})
|
|
399
|
-
|
|
400
|
-
test("LOG_FORMAT=json is respected at init time (tested via setLogFormat)", () => {
|
|
401
|
-
// The env var is read at module load time, so we test the API directly
|
|
402
|
-
setLogFormat("json")
|
|
403
|
-
const log = createLogger("test")
|
|
404
|
-
|
|
405
|
-
log.info?.("env json")
|
|
406
|
-
|
|
407
|
-
const parsed = parseJSON(consoleMock.output[0]!.message)
|
|
408
|
-
expect(parsed.msg).toBe("env json")
|
|
409
|
-
})
|
|
410
|
-
})
|
|
411
|
-
})
|
|
412
|
-
|
|
413
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
414
|
-
// 4. Async File Writer
|
|
415
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
416
|
-
|
|
417
|
-
describe("createFileWriter", () => {
|
|
418
|
-
let testFile: string
|
|
419
|
-
let writer: FileWriter | null = null
|
|
420
|
-
|
|
421
|
-
beforeEach(() => {
|
|
422
|
-
testFile = join(tmpdir(), `logger-test-${Date.now()}-${Math.random().toString(36).slice(2)}.log`)
|
|
423
|
-
})
|
|
424
|
-
|
|
425
|
-
afterEach(() => {
|
|
426
|
-
writer?.close()
|
|
427
|
-
writer = null
|
|
428
|
-
if (existsSync(testFile)) {
|
|
429
|
-
unlinkSync(testFile)
|
|
430
|
-
}
|
|
431
|
-
})
|
|
432
|
-
|
|
433
|
-
test("writes lines to file", () => {
|
|
434
|
-
writer = createFileWriter(testFile, { bufferSize: 1 }) // tiny buffer = immediate flush
|
|
435
|
-
writer.write("line one")
|
|
436
|
-
writer.write("line two")
|
|
437
|
-
writer.flush()
|
|
438
|
-
|
|
439
|
-
const content = readFileSync(testFile, "utf-8")
|
|
440
|
-
expect(content).toContain("line one\n")
|
|
441
|
-
expect(content).toContain("line two\n")
|
|
442
|
-
})
|
|
443
|
-
|
|
444
|
-
test("flushes on buffer size threshold", () => {
|
|
445
|
-
writer = createFileWriter(testFile, { bufferSize: 10, flushInterval: 60000 })
|
|
446
|
-
|
|
447
|
-
// Write enough to exceed 10 bytes
|
|
448
|
-
writer.write("hello world this is a long line")
|
|
449
|
-
|
|
450
|
-
// Should have flushed automatically
|
|
451
|
-
const content = readFileSync(testFile, "utf-8")
|
|
452
|
-
expect(content).toContain("hello world")
|
|
453
|
-
})
|
|
454
|
-
|
|
455
|
-
test("flush() writes buffer to disk", () => {
|
|
456
|
-
writer = createFileWriter(testFile, { bufferSize: 999999, flushInterval: 60000 })
|
|
457
|
-
|
|
458
|
-
writer.write("buffered line")
|
|
459
|
-
// Not yet flushed (buffer is large, interval is long)
|
|
460
|
-
const before = existsSync(testFile) ? readFileSync(testFile, "utf-8") : ""
|
|
461
|
-
|
|
462
|
-
writer.flush()
|
|
463
|
-
const after = readFileSync(testFile, "utf-8")
|
|
464
|
-
expect(after).toContain("buffered line\n")
|
|
465
|
-
})
|
|
466
|
-
|
|
467
|
-
test("close() flushes remaining buffer and closes fd", () => {
|
|
468
|
-
writer = createFileWriter(testFile, { bufferSize: 999999, flushInterval: 60000 })
|
|
469
|
-
|
|
470
|
-
writer.write("final line")
|
|
471
|
-
writer.close()
|
|
472
|
-
writer = null // prevent double close in afterEach
|
|
473
|
-
|
|
474
|
-
const content = readFileSync(testFile, "utf-8")
|
|
475
|
-
expect(content).toContain("final line\n")
|
|
476
|
-
})
|
|
477
|
-
|
|
478
|
-
test("writes are ignored after close", () => {
|
|
479
|
-
writer = createFileWriter(testFile, { bufferSize: 1 })
|
|
480
|
-
writer.write("before close")
|
|
481
|
-
writer.close()
|
|
482
|
-
|
|
483
|
-
// This should not throw or write
|
|
484
|
-
writer.write("after close")
|
|
485
|
-
writer = null
|
|
486
|
-
|
|
487
|
-
const content = readFileSync(testFile, "utf-8")
|
|
488
|
-
expect(content).toContain("before close")
|
|
489
|
-
expect(content).not.toContain("after close")
|
|
490
|
-
})
|
|
491
|
-
|
|
492
|
-
test("integrates with addWriter", () => {
|
|
493
|
-
writer = createFileWriter(testFile, { bufferSize: 1 })
|
|
494
|
-
const unsubscribe = addWriter((formatted) => writer!.write(formatted))
|
|
495
|
-
|
|
496
|
-
const log = createLogger("test")
|
|
497
|
-
log.info?.("writer integration")
|
|
498
|
-
|
|
499
|
-
writer.flush()
|
|
500
|
-
unsubscribe()
|
|
501
|
-
|
|
502
|
-
const content = readFileSync(testFile, "utf-8")
|
|
503
|
-
expect(content).toContain("writer integration")
|
|
504
|
-
})
|
|
505
|
-
|
|
506
|
-
test("flushes on interval", async () => {
|
|
507
|
-
writer = createFileWriter(testFile, { bufferSize: 999999, flushInterval: 50 })
|
|
508
|
-
|
|
509
|
-
writer.write("interval line")
|
|
510
|
-
|
|
511
|
-
// Wait for the flush interval to fire
|
|
512
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
513
|
-
|
|
514
|
-
const content = readFileSync(testFile, "utf-8")
|
|
515
|
-
expect(content).toContain("interval line\n")
|
|
516
|
-
})
|
|
517
|
-
|
|
518
|
-
test("multiple close calls are safe", () => {
|
|
519
|
-
writer = createFileWriter(testFile, { bufferSize: 1 })
|
|
520
|
-
writer.write("data")
|
|
521
|
-
writer.close()
|
|
522
|
-
// Should not throw
|
|
523
|
-
writer.close()
|
|
524
|
-
writer = null
|
|
525
|
-
})
|
|
526
|
-
|
|
527
|
-
test("creates file if it does not exist", () => {
|
|
528
|
-
expect(existsSync(testFile)).toBe(false)
|
|
529
|
-
writer = createFileWriter(testFile)
|
|
530
|
-
writer.write("new file")
|
|
531
|
-
writer.flush()
|
|
532
|
-
|
|
533
|
-
expect(existsSync(testFile)).toBe(true)
|
|
534
|
-
expect(readFileSync(testFile, "utf-8")).toContain("new file")
|
|
535
|
-
})
|
|
536
|
-
|
|
537
|
-
test("appends to existing file", () => {
|
|
538
|
-
// Create file with initial content
|
|
539
|
-
const w1 = createFileWriter(testFile, { bufferSize: 1 })
|
|
540
|
-
w1.write("first")
|
|
541
|
-
w1.close()
|
|
542
|
-
|
|
543
|
-
// Open again and append
|
|
544
|
-
writer = createFileWriter(testFile, { bufferSize: 1 })
|
|
545
|
-
writer.write("second")
|
|
546
|
-
writer.flush()
|
|
547
|
-
|
|
548
|
-
const content = readFileSync(testFile, "utf-8")
|
|
549
|
-
expect(content).toContain("first\n")
|
|
550
|
-
expect(content).toContain("second\n")
|
|
551
|
-
})
|
|
552
|
-
})
|