From a9fde452ac05ee1dacf3849ce2d71dacb2047d31 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 12 Mar 2026 17:31:20 +0900 Subject: [PATCH] feat(opencode-go): update on-complete hook for provider display --- src/cli/run/on-complete-hook.test.ts | 75 ++++++++++++++++++++++------ src/cli/run/on-complete-hook.ts | 53 ++++++++++++++++---- 2 files changed, 103 insertions(+), 25 deletions(-) diff --git a/src/cli/run/on-complete-hook.test.ts b/src/cli/run/on-complete-hook.test.ts index 930651a2d..7bd7d2cd0 100644 --- a/src/cli/run/on-complete-hook.test.ts +++ b/src/cli/run/on-complete-hook.test.ts @@ -1,26 +1,41 @@ import { describe, it, expect, spyOn, beforeEach, afterEach } from "bun:test" import * as spawnWithWindowsHideModule from "../../shared/spawn-with-windows-hide" +import * as loggerModule from "../../shared/logger" import { executeOnCompleteHook } from "./on-complete-hook" describe("executeOnCompleteHook", () => { - function createProc(exitCode: number) { + function createStream(text: string): ReadableStream | undefined { + if (text.length === 0) { + return undefined + } + + const encoder = new TextEncoder() + return new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode(text)) + controller.close() + }, + }) + } + + function createProc(exitCode: number, output?: { stdout?: string; stderr?: string }) { return { exited: Promise.resolve(exitCode), exitCode, - stdout: undefined, - stderr: undefined, + stdout: createStream(output?.stdout ?? ""), + stderr: createStream(output?.stderr ?? ""), kill: () => {}, } satisfies ReturnType } - let consoleErrorSpy: ReturnType> + let logSpy: ReturnType> beforeEach(() => { - consoleErrorSpy = spyOn(console, "error").mockImplementation(() => {}) + logSpy = spyOn(loggerModule, "log").mockImplementation(() => {}) }) afterEach(() => { - consoleErrorSpy.mockRestore() + logSpy.mockRestore() }) it("executes command with correct env vars", async () => { @@ -46,8 +61,8 @@ describe("executeOnCompleteHook", () => { expect(options?.env?.EXIT_CODE).toBe("0") expect(options?.env?.DURATION_MS).toBe("5000") expect(options?.env?.MESSAGE_COUNT).toBe("10") - expect(options?.stdout).toBe("inherit") - expect(options?.stderr).toBe("inherit") + expect(options?.stdout).toBe("pipe") + expect(options?.stderr).toBe("pipe") } finally { spawnSpy.mockRestore() } @@ -140,9 +155,8 @@ describe("executeOnCompleteHook", () => { ).resolves.toBeUndefined() // then - expect(consoleErrorSpy).toHaveBeenCalled() - const warningCall = consoleErrorSpy.mock.calls.find( - (call) => typeof call[0] === "string" && call[0].includes("Warning: on-complete hook exited with code 1") + const warningCall = logSpy.mock.calls.find( + (call) => call[0] === "On-complete hook exited with non-zero code" ) expect(warningCall).toBeDefined() } finally { @@ -170,12 +184,41 @@ describe("executeOnCompleteHook", () => { ).resolves.toBeUndefined() // then - expect(consoleErrorSpy).toHaveBeenCalled() - const errorCalls = consoleErrorSpy.mock.calls.filter((call) => { - const firstArg = call[0] - return typeof firstArg === "string" && (firstArg.includes("Warning") || firstArg.toLowerCase().includes("error")) + const errorCall = logSpy.mock.calls.find( + (call) => call[0] === "Failed to execute on-complete hook" + ) + expect(errorCall).toBeDefined() + } finally { + spawnSpy.mockRestore() + } + }) + + it("hook stdout and stderr are logged to file logger", async () => { + // given + const spawnSpy = spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockReturnValue( + createProc(0, { stdout: "hook output\n", stderr: "hook warning\n" }) + ) + + try { + // when + await executeOnCompleteHook({ + command: "echo test", + sessionId: "session-123", + exitCode: 0, + durationMs: 5000, + messageCount: 10, }) - expect(errorCalls.length).toBeGreaterThan(0) + + // then + const stdoutCall = logSpy.mock.calls.find( + (call) => call[0] === "On-complete hook stdout" + ) + const stderrCall = logSpy.mock.calls.find( + (call) => call[0] === "On-complete hook stderr" + ) + + expect(stdoutCall?.[1]).toEqual({ command: "echo test", stdout: "hook output" }) + expect(stderrCall?.[1]).toEqual({ command: "echo test", stderr: "hook warning" }) } finally { spawnSpy.mockRestore() } diff --git a/src/cli/run/on-complete-hook.ts b/src/cli/run/on-complete-hook.ts index b266ca887..1685a2133 100644 --- a/src/cli/run/on-complete-hook.ts +++ b/src/cli/run/on-complete-hook.ts @@ -1,5 +1,24 @@ -import pc from "picocolors" import { spawnWithWindowsHide } from "../../shared/spawn-with-windows-hide" +import { log } from "../../shared" + +async function readOutput( + stream: ReadableStream | undefined, + streamName: "stdout" | "stderr" +): Promise { + if (!stream) { + return "" + } + + try { + return await new Response(stream).text() + } catch (error) { + log("Failed to read on-complete hook output", { + stream: streamName, + error: error instanceof Error ? error.message : String(error), + }) + return "" + } +} export async function executeOnCompleteHook(options: { command: string @@ -15,7 +34,7 @@ export async function executeOnCompleteHook(options: { return } - console.error(pc.dim(`Running on-complete hook: ${trimmedCommand}`)) + log("Running on-complete hook", { command: trimmedCommand }) try { const proc = spawnWithWindowsHide(["sh", "-c", trimmedCommand], { @@ -26,18 +45,34 @@ export async function executeOnCompleteHook(options: { DURATION_MS: String(durationMs), MESSAGE_COUNT: String(messageCount), }, - stdout: "inherit", - stderr: "inherit", + stdout: "pipe", + stderr: "pipe", }) - const hookExitCode = await proc.exited + const [hookExitCode, stdout, stderr] = await Promise.all([ + proc.exited, + readOutput(proc.stdout, "stdout"), + readOutput(proc.stderr, "stderr"), + ]) + + if (stdout.trim()) { + log("On-complete hook stdout", { command: trimmedCommand, stdout: stdout.trim() }) + } + + if (stderr.trim()) { + log("On-complete hook stderr", { command: trimmedCommand, stderr: stderr.trim() }) + } if (hookExitCode !== 0) { - console.error( - pc.yellow(`Warning: on-complete hook exited with code ${hookExitCode}`) - ) + log("On-complete hook exited with non-zero code", { + command: trimmedCommand, + exitCode: hookExitCode, + }) } } catch (error) { - console.error(pc.yellow(`Warning: Failed to execute on-complete hook: ${error instanceof Error ? error.message : String(error)}`)) + log("Failed to execute on-complete hook", { + command: trimmedCommand, + error: error instanceof Error ? error.message : String(error), + }) } }