1import { consoleLogFloor, makeConsoleLogger } from "@/Cli/GlobalLog.ts";
2import { makePlainConsoleSink } from "@/Util/ConsoleSink.ts";
3import { describe, expect, it } from "alchemy-test";
4import * as Console from "effect/Console";
5import * as Effect from "effect/Effect";
6import * as Logger from "effect/Logger";
7import { MinimumLogLevel } from "effect/References";
8
9/**
10 * The console sink keeps an Info floor by default so `~/.alchemy/logs` stays
11 * the place full Debug detail lives. `--log-level debug` has to lower that
12 * floor for the run, or the flag produces zero terminal output (#1231).
13 */
14describe("consoleLogFloor", { tags: ["unit", "local"] }, () => {
15 it("defaults to Info", () => {
16 expect(consoleLogFloor(["profile", "list"])).toBe("Info");
17 });
18
19 it("reads --log-level in both spellings", () => {
20 expect(consoleLogFloor(["--log-level", "debug", "profile"])).toBe("Debug");
21 expect(consoleLogFloor(["profile", "--log-level=trace"])).toBe("Trace");
22 expect(consoleLogFloor(["deploy", "--log-level", "WARNING"])).toBe("Warn");
23 });
24
25 it("ignores the flag after a -- separator and unknown values", () => {
26 expect(consoleLogFloor(["--", "--log-level", "debug"])).toBe("Info");
27 expect(consoleLogFloor(["--log-level", "loud"])).toBe("Info");
28 expect(consoleLogFloor(["--log-level"])).toBe("Info");
29 });
30});
31
32describe("makeConsoleLogger", { tags: ["unit", "local"] }, () => {
33 it.effect("forwards only records at or above the floor", () =>
34 Effect.gen(function* () {
35 const seen: Array<{ level: string; message: unknown }> = [];
36 const probe = Logger.make<unknown, void>((options) => {
37 seen.push({ level: options.logLevel, message: options.message });
38 });
39 const emit = Effect.gen(function* () {
40 yield* Effect.logDebug("debug");
41 yield* Effect.logInfo("info");
42 }).pipe(Effect.provideService(MinimumLogLevel, "Debug"));
43
44 yield* emit.pipe(
45 Effect.provide(Logger.layer([makeConsoleLogger("Info", probe)])),
46 );
47 expect(seen.map((r) => r.level)).toEqual(["Info"]);
48
49 seen.length = 0;
50 yield* emit.pipe(
51 Effect.provide(Logger.layer([makeConsoleLogger("Debug", probe)])),
52 );
53 expect(seen.map((r) => r.level)).toEqual(["Debug", "Info"]);
54 }),
55 );
56
57 it.effect("keeps Effect prefixes without tinting the whole message", () => {
58 const lines: string[] = [];
59 const console = Object.assign(Object.create(globalThis.console), {
60 log: (...args: unknown[]) => lines.push(args.join(" ")),
61 });
62 return Effect.logInfo("plain message").pipe(
63 Effect.provide(Logger.layer([makeConsoleLogger("Info")])),
64 Effect.provideService(Console.Console, console),
65 Effect.andThen(
66 Effect.sync(() => {
67 expect(lines).toHaveLength(1);
68 expect(lines[0]).toMatch(
69 /^\[\d{2}:\d{2}:\d{2}\.\d{3}\] INFO \(#\d+\): plain message$/,
70 );
71 expect(lines[0]).not.toContain("\x1b[");
72 }),
73 ),
74 );
75 });
76
77 it.effect("colors the Effect prefix but resets before the message", () => {
78 const lines: string[] = [];
79 const console = Object.assign(Object.create(globalThis.console), {
80 log: (...args: unknown[]) => lines.push(args.join(" ")),
81 });
82 return Effect.logInfo("plain message").pipe(
83 Effect.provide(
84 Logger.layer([makeConsoleLogger("Info", makePlainConsoleSink(true))]),
85 ),
86 Effect.provideService(Console.Console, console),
87 Effect.andThen(
88 Effect.sync(() => {
89 expect(lines).toHaveLength(1);
90 expect(lines[0]).toContain("\x1b[32mINFO\x1b[0m");
91 expect(lines[0]).toContain("\x1b[0mplain message");
92 expect(lines[0]).not.toContain("\x1b[36mplain message");
93 }),
94 ),
95 );
96 });
97});