feat(opencode-go): update on-complete hook for provider display
This commit is contained in:
@@ -1,26 +1,41 @@
|
|||||||
import { describe, it, expect, spyOn, beforeEach, afterEach } from "bun:test"
|
import { describe, it, expect, spyOn, beforeEach, afterEach } from "bun:test"
|
||||||
import * as spawnWithWindowsHideModule from "../../shared/spawn-with-windows-hide"
|
import * as spawnWithWindowsHideModule from "../../shared/spawn-with-windows-hide"
|
||||||
|
import * as loggerModule from "../../shared/logger"
|
||||||
import { executeOnCompleteHook } from "./on-complete-hook"
|
import { executeOnCompleteHook } from "./on-complete-hook"
|
||||||
|
|
||||||
describe("executeOnCompleteHook", () => {
|
describe("executeOnCompleteHook", () => {
|
||||||
function createProc(exitCode: number) {
|
function createStream(text: string): ReadableStream<Uint8Array> | undefined {
|
||||||
|
if (text.length === 0) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const encoder = new TextEncoder()
|
||||||
|
return new ReadableStream<Uint8Array>({
|
||||||
|
start(controller) {
|
||||||
|
controller.enqueue(encoder.encode(text))
|
||||||
|
controller.close()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProc(exitCode: number, output?: { stdout?: string; stderr?: string }) {
|
||||||
return {
|
return {
|
||||||
exited: Promise.resolve(exitCode),
|
exited: Promise.resolve(exitCode),
|
||||||
exitCode,
|
exitCode,
|
||||||
stdout: undefined,
|
stdout: createStream(output?.stdout ?? ""),
|
||||||
stderr: undefined,
|
stderr: createStream(output?.stderr ?? ""),
|
||||||
kill: () => {},
|
kill: () => {},
|
||||||
} satisfies ReturnType<typeof spawnWithWindowsHideModule.spawnWithWindowsHide>
|
} satisfies ReturnType<typeof spawnWithWindowsHideModule.spawnWithWindowsHide>
|
||||||
}
|
}
|
||||||
|
|
||||||
let consoleErrorSpy: ReturnType<typeof spyOn<typeof console, "error">>
|
let logSpy: ReturnType<typeof spyOn<typeof loggerModule, "log">>
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
consoleErrorSpy = spyOn(console, "error").mockImplementation(() => {})
|
logSpy = spyOn(loggerModule, "log").mockImplementation(() => {})
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
consoleErrorSpy.mockRestore()
|
logSpy.mockRestore()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("executes command with correct env vars", async () => {
|
it("executes command with correct env vars", async () => {
|
||||||
@@ -46,8 +61,8 @@ describe("executeOnCompleteHook", () => {
|
|||||||
expect(options?.env?.EXIT_CODE).toBe("0")
|
expect(options?.env?.EXIT_CODE).toBe("0")
|
||||||
expect(options?.env?.DURATION_MS).toBe("5000")
|
expect(options?.env?.DURATION_MS).toBe("5000")
|
||||||
expect(options?.env?.MESSAGE_COUNT).toBe("10")
|
expect(options?.env?.MESSAGE_COUNT).toBe("10")
|
||||||
expect(options?.stdout).toBe("inherit")
|
expect(options?.stdout).toBe("pipe")
|
||||||
expect(options?.stderr).toBe("inherit")
|
expect(options?.stderr).toBe("pipe")
|
||||||
} finally {
|
} finally {
|
||||||
spawnSpy.mockRestore()
|
spawnSpy.mockRestore()
|
||||||
}
|
}
|
||||||
@@ -140,9 +155,8 @@ describe("executeOnCompleteHook", () => {
|
|||||||
).resolves.toBeUndefined()
|
).resolves.toBeUndefined()
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(consoleErrorSpy).toHaveBeenCalled()
|
const warningCall = logSpy.mock.calls.find(
|
||||||
const warningCall = consoleErrorSpy.mock.calls.find(
|
(call) => call[0] === "On-complete hook exited with non-zero code"
|
||||||
(call) => typeof call[0] === "string" && call[0].includes("Warning: on-complete hook exited with code 1")
|
|
||||||
)
|
)
|
||||||
expect(warningCall).toBeDefined()
|
expect(warningCall).toBeDefined()
|
||||||
} finally {
|
} finally {
|
||||||
@@ -170,12 +184,41 @@ describe("executeOnCompleteHook", () => {
|
|||||||
).resolves.toBeUndefined()
|
).resolves.toBeUndefined()
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(consoleErrorSpy).toHaveBeenCalled()
|
const errorCall = logSpy.mock.calls.find(
|
||||||
const errorCalls = consoleErrorSpy.mock.calls.filter((call) => {
|
(call) => call[0] === "Failed to execute on-complete hook"
|
||||||
const firstArg = call[0]
|
)
|
||||||
return typeof firstArg === "string" && (firstArg.includes("Warning") || firstArg.toLowerCase().includes("error"))
|
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 {
|
} finally {
|
||||||
spawnSpy.mockRestore()
|
spawnSpy.mockRestore()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,24 @@
|
|||||||
import pc from "picocolors"
|
|
||||||
import { spawnWithWindowsHide } from "../../shared/spawn-with-windows-hide"
|
import { spawnWithWindowsHide } from "../../shared/spawn-with-windows-hide"
|
||||||
|
import { log } from "../../shared"
|
||||||
|
|
||||||
|
async function readOutput(
|
||||||
|
stream: ReadableStream<Uint8Array> | undefined,
|
||||||
|
streamName: "stdout" | "stderr"
|
||||||
|
): Promise<string> {
|
||||||
|
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: {
|
export async function executeOnCompleteHook(options: {
|
||||||
command: string
|
command: string
|
||||||
@@ -15,7 +34,7 @@ export async function executeOnCompleteHook(options: {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(pc.dim(`Running on-complete hook: ${trimmedCommand}`))
|
log("Running on-complete hook", { command: trimmedCommand })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const proc = spawnWithWindowsHide(["sh", "-c", trimmedCommand], {
|
const proc = spawnWithWindowsHide(["sh", "-c", trimmedCommand], {
|
||||||
@@ -26,18 +45,34 @@ export async function executeOnCompleteHook(options: {
|
|||||||
DURATION_MS: String(durationMs),
|
DURATION_MS: String(durationMs),
|
||||||
MESSAGE_COUNT: String(messageCount),
|
MESSAGE_COUNT: String(messageCount),
|
||||||
},
|
},
|
||||||
stdout: "inherit",
|
stdout: "pipe",
|
||||||
stderr: "inherit",
|
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) {
|
if (hookExitCode !== 0) {
|
||||||
console.error(
|
log("On-complete hook exited with non-zero code", {
|
||||||
pc.yellow(`Warning: on-complete hook exited with code ${hookExitCode}`)
|
command: trimmedCommand,
|
||||||
)
|
exitCode: hookExitCode,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user