diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 6ae5530..1d106b1 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -25,5 +25,7 @@ export function info(message: string): void { export function warn(message: string): void { if (quiet) return; - console.log(chalk.yellow(message)); + // Warnings go to stderr so they never corrupt machine-readable stdout + // (e.g. `--json` output or a piped value); stdout is reserved for results. + console.error(chalk.yellow(message)); } diff --git a/tests/platform/resolve.test.ts b/tests/platform/resolve.test.ts index f82b190..893c77b 100644 --- a/tests/platform/resolve.test.ts +++ b/tests/platform/resolve.test.ts @@ -93,7 +93,7 @@ describe("resolveProjectPlatform", () => { }) ); - const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const logSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const { resolveProjectPlatform } = await import("../../src/platform/resolve.js"); const platform = await resolveProjectPlatform(testDir); @@ -113,7 +113,7 @@ describe("resolveProjectPlatform", () => { }) ); - const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const logSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const { resolveProjectPlatform } = await import("../../src/platform/resolve.js"); const platform = await resolveProjectPlatform(testDir); @@ -128,7 +128,7 @@ describe("resolveProjectPlatform", () => { await mkdir(join(testDir, "bmalph"), { recursive: true }); await writeFile(join(testDir, "bmalph/config.json"), "not valid json{{{"); - const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const logSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const { resolveProjectPlatform } = await import("../../src/platform/resolve.js"); const platform = await resolveProjectPlatform(testDir); diff --git a/tests/utils/config.test.ts b/tests/utils/config.test.ts index 04d82b3..ecd369a 100644 --- a/tests/utils/config.test.ts +++ b/tests/utils/config.test.ts @@ -58,7 +58,7 @@ describe("config", () => { it("returns null and warns when config file has invalid structure", async () => { await writeFile(join(testDir, "bmalph/config.json"), JSON.stringify({ garbage: true })); - const warnSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const warnSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const result = await readConfig(testDir); @@ -123,7 +123,7 @@ platform: claude-code it("returns null and warns for malformed YAML", async () => { await writeFile(join(testDir, "_bmad/config.yaml"), "planning_artifacts: [invalid"); - const warnSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const warnSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const result = await readBmadConfig(testDir); expect(result).toBeNull(); expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Error reading BMAD config")); @@ -132,7 +132,7 @@ platform: claude-code it("returns null and warns for non-string planning_artifacts field", async () => { await writeFile(join(testDir, "_bmad/config.yaml"), "planning_artifacts: 123\n"); - const warnSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const warnSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const result = await readBmadConfig(testDir); expect(result).toBeNull(); expect(warnSpy).toHaveBeenCalledWith( @@ -143,7 +143,7 @@ platform: claude-code it("returns null and warns for boolean planning_artifacts field", async () => { await writeFile(join(testDir, "_bmad/config.yaml"), "planning_artifacts: true\n"); - const warnSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const warnSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const result = await readBmadConfig(testDir); expect(result).toBeNull(); expect(warnSpy).toHaveBeenCalledWith( @@ -154,7 +154,7 @@ platform: claude-code it("returns null and warns for object planning_artifacts field", async () => { await writeFile(join(testDir, "_bmad/config.yaml"), "planning_artifacts:\n foo: bar\n"); - const warnSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const warnSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const result = await readBmadConfig(testDir); expect(result).toBeNull(); expect(warnSpy).toHaveBeenCalledWith( @@ -165,7 +165,7 @@ platform: claude-code it("returns null and warns for empty YAML file", async () => { await writeFile(join(testDir, "_bmad/config.yaml"), ""); - const warnSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const warnSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const result = await readBmadConfig(testDir); expect(result).toBeNull(); expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Error reading BMAD config")); @@ -174,7 +174,7 @@ platform: claude-code it("returns null and warns when YAML parses to a scalar", async () => { await writeFile(join(testDir, "_bmad/config.yaml"), "just a string\n"); - const warnSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const warnSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const result = await readBmadConfig(testDir); expect(result).toBeNull(); expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Error reading BMAD config")); @@ -183,7 +183,7 @@ platform: claude-code it("returns null and warns when YAML parses to an array", async () => { await writeFile(join(testDir, "_bmad/config.yaml"), "- item1\n- item2\n"); - const warnSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const warnSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const result = await readBmadConfig(testDir); expect(result).toBeNull(); expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Error reading BMAD config")); @@ -192,7 +192,7 @@ platform: claude-code it("returns null and warns for non-string platform field", async () => { await writeFile(join(testDir, "_bmad/config.yaml"), "platform: 123\n"); - const warnSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const warnSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const result = await readBmadConfig(testDir); expect(result).toBeNull(); expect(warnSpy).toHaveBeenCalledWith( @@ -203,7 +203,7 @@ platform: claude-code it("returns null and warns for non-string project_name field", async () => { await writeFile(join(testDir, "_bmad/config.yaml"), "project_name: true\n"); - const warnSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const warnSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const result = await readBmadConfig(testDir); expect(result).toBeNull(); expect(warnSpy).toHaveBeenCalledWith( @@ -214,7 +214,7 @@ platform: claude-code it("returns null and warns for non-string output_folder field", async () => { await writeFile(join(testDir, "_bmad/config.yaml"), "output_folder: 42\n"); - const warnSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const warnSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const result = await readBmadConfig(testDir); expect(result).toBeNull(); expect(warnSpy).toHaveBeenCalledWith( diff --git a/tests/utils/logger.test.ts b/tests/utils/logger.test.ts index bbd28dc..a5cad57 100644 --- a/tests/utils/logger.test.ts +++ b/tests/utils/logger.test.ts @@ -12,14 +12,17 @@ vi.mock("chalk", () => ({ describe("logger", () => { let consoleSpy: ReturnType; + let consoleErrorSpy: ReturnType; beforeEach(() => { vi.resetModules(); consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); }); afterEach(() => { consoleSpy.mockRestore(); + consoleErrorSpy.mockRestore(); vi.restoreAllMocks(); }); @@ -100,6 +103,7 @@ describe("logger", () => { warn("warning should not appear"); expect(consoleSpy).not.toHaveBeenCalled(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); }); it("resumes normal output when quiet is disabled", async () => { @@ -127,13 +131,21 @@ describe("logger", () => { }); describe("warn", () => { - it("outputs message with yellow color", async () => { + it("outputs message with yellow color to stderr", async () => { const { warn } = await import("../../src/utils/logger.js"); warn("warning message"); - expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("[yellow]")); - expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("warning message")); + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("[yellow]")); + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("warning message")); + }); + + it("does not write warnings to stdout", async () => { + const { warn } = await import("../../src/utils/logger.js"); + + warn("warning message"); + + expect(consoleSpy).not.toHaveBeenCalled(); }); }); }); diff --git a/tests/utils/state.test.ts b/tests/utils/state.test.ts index d0b81ef..e4cfb6d 100644 --- a/tests/utils/state.test.ts +++ b/tests/utils/state.test.ts @@ -58,7 +58,7 @@ describe("state", () => { JSON.stringify({ garbage: true }) ); - const warnSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const warnSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const result = await readState(testDir); @@ -230,7 +230,7 @@ describe("state", () => { await mkdir(join(testDir, ".ralph"), { recursive: true }); await writeFile(join(testDir, ".ralph/status.json"), '"just a string"'); - const warnSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const warnSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const result = await readRalphStatus(testDir); @@ -250,7 +250,7 @@ describe("state", () => { await mkdir(join(testDir, ".ralph"), { recursive: true }); await writeFile(join(testDir, ".ralph/status.json"), '{"loopCount": "not-a-number"}'); - const warnSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const warnSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const result = await readRalphStatus(testDir); @@ -278,7 +278,7 @@ describe("state", () => { }) ); - const warnSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const warnSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const result = await readRalphStatus(testDir); @@ -305,7 +305,7 @@ describe("state", () => { }) ); - const warnSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const warnSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const result = await readRalphStatus(testDir); @@ -332,7 +332,7 @@ describe("state", () => { }) ); - const warnSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const warnSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const result = await readRalphStatus(testDir); @@ -435,7 +435,7 @@ describe("state", () => { }) ); - const warnSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const warnSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const result = await readRalphStatus(testDir);