evOo-local/packages/logging/src/logger.test.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

150 lines
4.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { createLogger } from "./logger.js";
import { serializeError } from "./serialize-error.js";
import { createTestSink } from "./test-sink.js";
describe("logger", () => {
it("filters levels including off", () => {
const sink = createTestSink();
const logger = createLogger({ service: "rakazo-api", level: "warn", sinks: [sink] });
logger.debug("d");
logger.info("i");
logger.warn("w");
logger.error("e");
expect(sink.events.map((event) => event.message)).toEqual(["w", "e"]);
const silent = createLogger({ service: "rakazo-api", level: "off", sinks: [sink] });
silent.error("never");
expect(sink.events.map((event) => event.message)).toEqual(["w", "e"]);
});
it("merges child bindings under call-site bindings", () => {
const sink = createTestSink();
const logger = createLogger({
service: "rakazo-api",
sinks: [sink],
bindings: { "service.role": "api" },
});
logger.child({ "job.type": "run.continue" }).info("hello", { "job.type": "override" });
expect(sink.events[0]).toMatchObject({
message: "hello",
"service.name": "rakazo-api",
"service.role": "api",
"job.type": "override",
});
});
it("keeps the configured service name when bindings try to overwrite it", () => {
const sink = createTestSink();
const logger = createLogger({ service: "rakazo-api", sinks: [sink] });
logger.info("svc", { "service.name": "spoofed" });
expect(sink.events[0]?.["service.name"]).toBe("rakazo-api");
});
it("prefers explicit bindings over async context", () => {
const sink = createTestSink();
const logger = createLogger({ service: "rakazo-api", sinks: [sink] });
logger.withContext({ "request.id": "als" }, () => {
logger.info("ctx", { "request.id": "call" });
});
expect(sink.events[0]?.["request.id"]).toBe("call");
});
it("isolates concurrent async contexts", async () => {
const sink = createTestSink();
const logger = createLogger({ service: "rakazo-api", sinks: [sink] });
let releaseFirst!: () => void;
const firstHold = new Promise<void>((resolve) => {
releaseFirst = resolve;
});
const first = logger.withContext({ "request.id": "a" }, async () => {
await firstHold;
logger.info("first");
});
const second = logger.withContext({ "request.id": "b" }, async () => {
logger.info("second");
releaseFirst();
});
await Promise.all([first, second]);
expect(sink.events).toEqual(
expect.arrayContaining([
expect.objectContaining({ message: "first", "request.id": "a" }),
expect.objectContaining({ message: "second", "request.id": "b" }),
]),
);
});
it("keeps logging when a sink throws", () => {
const sink = createTestSink();
const logger = createLogger({
service: "rakazo-api",
sinks: [
{
write() {
throw new Error("sink down");
},
},
sink,
],
});
expect(() => logger.info("still works")).not.toThrow();
expect(sink.events).toHaveLength(1);
});
it("flushes sinks and respects a timeout", async () => {
let flushed = false;
const logger = createLogger({
service: "rakazo-api",
sinks: [
{
write() {},
async flush() {
flushed = true;
},
},
],
});
await logger.flush({ timeoutMs: 50 });
expect(flushed).toBe(true);
const hanging = createLogger({
service: "rakazo-api",
sinks: [
{
write() {},
flush() {
return new Promise(() => undefined);
},
},
],
});
await expect(hanging.flush({ timeoutMs: 20 })).resolves.toBeUndefined();
});
});
describe("error serialization", () => {
it("records cause chains", () => {
const error = new Error("outer", { cause: new Error("inner", { cause: new Error("root") }) });
expect(serializeError(error)).toMatchObject({
message: "outer",
cause: { message: "inner", cause: { message: "root" } },
});
});
it("redacts secrets in error messages and stacks", () => {
const error = new Error("unauthorized Bearer supersecret for person@example.com");
const serialized = serializeError(error);
expect(JSON.stringify(serialized)).not.toContain("supersecret");
expect(JSON.stringify(serialized)).not.toContain("person@example.com");
expect(serialized.message).toContain("[Redacted]");
});
it("redacts secrets interpolated into the log message", () => {
const sink = createTestSink();
const logger = createLogger({ service: "rakazo-api", sinks: [sink] });
logger.error("render_plot failed: Bearer supersecret for person@example.com");
expect(JSON.stringify(sink.events[0])).not.toContain("supersecret");
expect(JSON.stringify(sink.events[0])).not.toContain("person@example.com");
});
});