import { expect, test } from "vitest" import type { LogEmitter, LogEmitterItem } from "#Source/log/index.ts" import { Logger, getGlobalLogger } from "#Source/log/index.ts" const internalMakeMemoryEmitter = ( sink: string[], logType: LogEmitterItem["logType"] = "info", ): LogEmitterItem => { class InternalMemoryEmitter implements LogEmitter { readonly tags: string[] readonly messages: unknown[] readonly formattedMessages: string[] constructor(options: { tags?: string[] | undefined; messages?: unknown[] | undefined }) { this.tags = options.tags ?? [] this.messages = options.messages ?? [] this.formattedMessages = [ this.tags.map((tag) => `[${tag}]`).join(""), this.messages.map((message) => String(message)).join(" "), ] } emit(): void { sink.push(`${logType}:${this.formattedMessages.join(" ")}`) } } return { logType, LogEmitter: InternalMemoryEmitter, } } const internalMakeThrowingEmitter = ( logType: LogEmitterItem["logType"], error: Error, ): LogEmitterItem => { class InternalThrowingEmitter implements LogEmitter { readonly tags: string[] readonly messages: unknown[] readonly formattedMessages: string[] constructor(options: { tags?: string[] | undefined; messages?: unknown[] | undefined }) { this.tags = options.tags ?? [] this.messages = options.messages ?? [] this.formattedMessages = [] } emit(): void { throw error } } return { logType, LogEmitter: InternalThrowingEmitter, } } const internalMakeAllMemoryEmitters = (sink: string[]): LogEmitterItem[] => { return [ internalMakeMemoryEmitter(sink, "log"), internalMakeMemoryEmitter(sink, "info"), internalMakeMemoryEmitter(sink, "warn"), internalMakeMemoryEmitter(sink, "error"), internalMakeMemoryEmitter(sink, "debug"), ] } test("Logger.fromOptions returns provided logger and otherwise derives from global logger", () => { const explicit = new Logger({ name: "Explicit" }) const resolvedExplicit = Logger.fromOptions({ logger: explicit }) const resolvedDerived = Logger.fromOptions({}).setDefaultName("Derived") expect(resolvedExplicit).toBe(explicit) expect(resolvedDerived.hasParent()).toBe(true) expect(resolvedDerived.getNameTags()).toEqual([getGlobalLogger().getName(), "Derived"]) }) test("Logger.derive creates child logger with inherited configs", () => { const parent = new Logger({ name: "Parent" }) const child = Logger.derive(parent).setDefaultName("Child") const instanceChild = parent.derive().setDefaultName("InstanceChild") expect(child.hasParent()).toBe(true) expect(child.getNameTags()).toEqual(["Parent", "Child"]) expect(instanceChild.hasParent()).toBe(true) expect(instanceChild.getNameTags()).toEqual(["Parent", "InstanceChild"]) }) test("Logger name APIs prefer explicit name and fall back to defaultName", () => { const logger = new Logger({}) expect(logger.getName()).toBe("Unnamed") logger.setDefaultName("DefaultLogger") expect(logger.getName()).toBe("DefaultLogger") logger.setName("ExplicitLogger") expect(logger.getName()).toBe("ExplicitLogger") logger.setName(undefined) expect(logger.getName()).toBe("DefaultLogger") }) test("Logger parent APIs maintain parent-child relationships", () => { const parentA = new Logger({ name: "ParentA" }) const parentB = new Logger({ name: "ParentB" }) const child = new Logger({ name: "Child" }) parentA.addChild(child) expect(child.hasParent()).toBe(true) child.setParent(parentB) parentA.delChild(child) expect(child.hasParent()).toBe(true) child.unsetParent() expect(child.hasParent()).toBe(false) parentA.addChild(child) parentA.delChild(child) expect(child.hasParent()).toBe(false) }) test("Logger config APIs merge, refresh and apply parent configs", () => { const parent = new Logger({ name: "Parent", configs: { enabled: true, autoSend: true, filter: (): boolean => true, emitters: [], }, }) const child = new Logger({ name: "Child", parent, configs: { enabled: false, }, }) expect(child.getConfigs().enabled).toBe(true) child.setConfigs({ enabled: false }) expect(child.getConfigs().enabled).toBe(false) parent.setConfigs({ autoSend: false }) expect(child.getConfigs().autoSend).toBe(true) const internalUpdatedFilter = (): boolean => false parent.setConfigs({ filter: internalUpdatedFilter }) child.updateParentConfigs() expect(child.getConfigs().filter).not.toBe(internalUpdatedFilter) child.useParentConfigs() expect(child.getConfigs().autoSend).toBe(false) expect(child.getConfigs().filter).toBe(internalUpdatedFilter) expect(child.getConfigs().enabled).toBe(true) parent.setConfigs({ enabled: false }) child.useParentConfigs() expect(child.getConfigs().enabled).toBe(false) }) test("Logger emitter APIs add, deduplicate, invoke and remove emitters", () => { const sink: string[] = [] const emitter = internalMakeMemoryEmitter(sink) const logger = new Logger({ name: "EmitterLogger", configs: { emitters: [], enabled: true, autoSend: true, filter: (): boolean => true, }, }) logger.addLogEmitters([emitter, emitter]) expect(logger.getAllLogEmitters()).toHaveLength(1) expect(logger.getLogEmitters("info")).toHaveLength(1) logger.invokeLogEmitter({ type: "info", tags: ["manual"], messages: ["hello"], }) expect(sink).toEqual(["info:[manual] hello"]) logger.removeLogEmitters([emitter]) expect(logger.getAllLogEmitters()).toHaveLength(0) }) test("Logger name and instance tag APIs compose logger context", () => { const parent = new Logger({ name: "Parent" }) const logger = Logger.derive(parent).setDefaultName("Child") expect(logger.getNameTags()).toEqual(["Parent", "Child"]) logger.addInstanceTags(["instance-a", "instance-b"]) expect(logger.getInstanceTags()).toEqual(["instance-a", "instance-b"]) logger.removeInstanceTags(["instance-a"]) expect(logger.getInstanceTags()).toEqual(["instance-b"]) logger.clearInstanceTags() expect(logger.getInstanceTags()).toEqual([]) }) test("Logger session tag APIs manage session state", () => { const logger = new Logger({ name: "SessionLogger" }) logger.addSessionTags(["session-a", "session-b"]) expect(logger.getSessionTags()).toEqual(["session-a", "session-b"]) logger.removeSessionTags(["session-a"]) expect(logger.getSessionTags()).toEqual(["session-b"]) logger.clearSessionTags() expect(logger.getSessionTags()).toEqual([]) }) test("Logger.autoTag adds temporary tags during disposable scope", () => { const sink: string[] = [] const logger = new Logger({ name: "AutoTagLogger", configs: { emitters: [internalMakeMemoryEmitter(sink)], enabled: true, autoSend: true, filter: (): boolean => true, }, }) const disposable = logger.autoTag(["scoped"]) logger.info("within") disposable[Symbol.dispose]() logger.info("outside") expect(sink[0]).toContain("[scoped]") expect(sink[1]).not.toContain("[scoped]") }) test("Logger.tagStart and Logger.tagEnd manage explicit session scopes", () => { const logger = new Logger({ name: "TagLogger" }) logger.tagStart(["scoped"]) expect(logger.getSessionTags()).toEqual(["scoped"]) logger.tagEnd() expect(logger.getSessionTags()).toEqual([]) }) test("Logger once tag APIs expose and clear queued once tags", () => { const logger = new Logger({ name: "OnceLogger" }) logger.addOnceTags(["once"]) expect(logger.getOnceTags()).toEqual(["once"]) logger.clearOnceTags() expect(logger.getOnceTags()).toEqual([]) }) test("Logger logging APIs emit in order and respect enabled and filter configs", () => { const sink: string[] = [] const logger = new Logger({ name: "LoggingLogger", configs: { emitters: internalMakeAllMemoryEmitters(sink), enabled: true, autoSend: true, filter: (): boolean => true, }, }) logger.log("a").info("b").warn("c").error("d").debug("e") expect(sink).toEqual([ "log:[🟢][LoggingLogger] a", "info:[🔵][LoggingLogger] b", "warn:[🟡][LoggingLogger] c", "error:[🔴][LoggingLogger] d", "debug:[🟤][LoggingLogger] e", ]) logger.setConfigs({ enabled: false }) logger.info("disabled") expect(sink).toHaveLength(5) logger.setConfigs({ enabled: true, filter: () => false }) logger.info("blocked") expect(sink).toHaveLength(5) logger.setConfigs({ filter: () => true }) logger.info("allowed") expect(sink).toHaveLength(6) }) test("Logger.batch creates manual child logger configured for queued sending", () => { const sink: string[] = [] const logger = new Logger({ name: "ParentLogger", configs: { emitters: [internalMakeMemoryEmitter(sink)], enabled: true, autoSend: true, filter: (): boolean => true, }, }) const child = logger.batch("batch-id") expect(child.hasParent()).toBe(true) expect(child.getInstanceTags()).toContain("batch-id") child.info("queued") expect(sink).toHaveLength(0) }) test("Logger.send flushes queued logs in insertion order and propagates emitter failures", () => { const sink: string[] = [] const logger = new Logger({ name: "SendLogger", configs: { emitters: [internalMakeMemoryEmitter(sink)], enabled: true, autoSend: false, filter: (): boolean => true, }, }) logger.info("first") logger.info("second") expect(sink).toEqual([]) logger.send() expect(sink).toEqual(["info:[🔵][SendLogger] first", "info:[🔵][SendLogger] second"]) const internalEmitError = new Error("emit failed") const failingLogger = new Logger({ name: "FailingLogger", configs: { emitters: [internalMakeThrowingEmitter("info", internalEmitError)], enabled: true, autoSend: false, filter: (): boolean => true, }, }) failingLogger.info("boom") expect(() => failingLogger.send()).toThrow(internalEmitError) }) test("Logger Symbol.dispose flushes queued logs", () => { const sink: string[] = [] const logger = new Logger({ name: "DisposableLogger", configs: { emitters: [internalMakeMemoryEmitter(sink)], enabled: true, autoSend: false, filter: (): boolean => true, }, }) logger.info("queued") expect(sink).toEqual([]) logger[Symbol.dispose]() expect(sink).toEqual(["info:[🔵][DisposableLogger] queued"]) }) test("getGlobalLogger returns singleton instance", () => { const logger1 = getGlobalLogger() const logger2 = getGlobalLogger() expect(logger1).toBe(logger2) })