mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 04:31:10 +00:00
feat: stream Claude CLI JSONL output
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { onAgentEvent, resetAgentEventsForTest } from "../infra/agent-events.js";
|
||||
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
|
||||
import {
|
||||
createManagedRun,
|
||||
@@ -13,6 +14,10 @@ import {
|
||||
supervisorSpawnMock,
|
||||
} from "./cli-runner.test-support.js";
|
||||
|
||||
beforeEach(() => {
|
||||
resetAgentEventsForTest();
|
||||
});
|
||||
|
||||
describe("runCliAgent spawn path", () => {
|
||||
it("does not inject hardcoded 'Tools are disabled' text into CLI arguments", async () => {
|
||||
const runCliAgent = await setupCliRunnerTestModule();
|
||||
@@ -47,6 +52,40 @@ describe("runCliAgent spawn path", () => {
|
||||
expect(allArgs).toContain("You are a helpful assistant.");
|
||||
});
|
||||
|
||||
it("pipes Claude prompts over stdin instead of argv", async () => {
|
||||
const runCliAgent = await setupCliRunnerTestModule();
|
||||
supervisorSpawnMock.mockResolvedValueOnce(
|
||||
createManagedRun({
|
||||
reason: "exit",
|
||||
exitCode: 0,
|
||||
exitSignal: null,
|
||||
durationMs: 50,
|
||||
stdout: "ok",
|
||||
stderr: "",
|
||||
timedOut: false,
|
||||
noOutputTimedOut: false,
|
||||
}),
|
||||
);
|
||||
|
||||
await runCliAgent({
|
||||
sessionId: "s1",
|
||||
sessionFile: "/tmp/session.jsonl",
|
||||
workspaceDir: "/tmp",
|
||||
prompt: "Explain this diff",
|
||||
provider: "claude-cli",
|
||||
model: "sonnet",
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-stdin-claude",
|
||||
});
|
||||
|
||||
const input = supervisorSpawnMock.mock.calls[0]?.[0] as {
|
||||
argv?: string[];
|
||||
input?: string;
|
||||
};
|
||||
expect(input.input).toContain("Explain this diff");
|
||||
expect(input.argv).not.toContain("Explain this diff");
|
||||
});
|
||||
|
||||
it("injects a strict empty MCP config for bundle-MCP-enabled Claude CLI runs", async () => {
|
||||
const runCliAgent = await setupCliRunnerTestModule();
|
||||
supervisorSpawnMock.mockResolvedValueOnce(
|
||||
@@ -142,6 +181,82 @@ describe("runCliAgent spawn path", () => {
|
||||
expect(input.scopeKey).toContain("thread-123");
|
||||
});
|
||||
|
||||
it("streams Claude text deltas from stream-json stdout", async () => {
|
||||
const runCliAgent = await setupCliRunnerTestModule();
|
||||
const agentEvents: Array<{ stream: string; text?: string; delta?: string }> = [];
|
||||
const stop = onAgentEvent((evt) => {
|
||||
agentEvents.push({
|
||||
stream: evt.stream,
|
||||
text: typeof evt.data.text === "string" ? evt.data.text : undefined,
|
||||
delta: typeof evt.data.delta === "string" ? evt.data.delta : undefined,
|
||||
});
|
||||
});
|
||||
supervisorSpawnMock.mockImplementationOnce(async (...args: unknown[]) => {
|
||||
const input = (args[0] ?? {}) as { onStdout?: (chunk: string) => void };
|
||||
input.onStdout?.(
|
||||
[
|
||||
JSON.stringify({ type: "init", session_id: "session-123" }),
|
||||
JSON.stringify({
|
||||
type: "stream_event",
|
||||
event: { type: "content_block_delta", delta: { type: "text_delta", text: "Hello" } },
|
||||
}),
|
||||
].join("\n") + "\n",
|
||||
);
|
||||
input.onStdout?.(
|
||||
JSON.stringify({
|
||||
type: "stream_event",
|
||||
event: { type: "content_block_delta", delta: { type: "text_delta", text: " world" } },
|
||||
}) + "\n",
|
||||
);
|
||||
return createManagedRun({
|
||||
reason: "exit",
|
||||
exitCode: 0,
|
||||
exitSignal: null,
|
||||
durationMs: 50,
|
||||
stdout: [
|
||||
JSON.stringify({ type: "init", session_id: "session-123" }),
|
||||
JSON.stringify({
|
||||
type: "stream_event",
|
||||
event: { type: "content_block_delta", delta: { type: "text_delta", text: "Hello" } },
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: "stream_event",
|
||||
event: { type: "content_block_delta", delta: { type: "text_delta", text: " world" } },
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: "result",
|
||||
session_id: "session-123",
|
||||
result: "Hello world",
|
||||
}),
|
||||
].join("\n"),
|
||||
stderr: "",
|
||||
timedOut: false,
|
||||
noOutputTimedOut: false,
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await runCliAgent({
|
||||
sessionId: "s1",
|
||||
sessionFile: "/tmp/session.jsonl",
|
||||
workspaceDir: "/tmp",
|
||||
prompt: "hi",
|
||||
provider: "claude-cli",
|
||||
model: "sonnet",
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-claude-stream-json",
|
||||
});
|
||||
|
||||
expect(result.payloads?.[0]?.text).toBe("Hello world");
|
||||
expect(agentEvents).toEqual([
|
||||
{ stream: "assistant", text: "Hello", delta: "Hello" },
|
||||
{ stream: "assistant", text: "Hello world", delta: " world" },
|
||||
]);
|
||||
} finally {
|
||||
stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("sanitizes dangerous backend env overrides before spawn", async () => {
|
||||
const runCliAgent = await setupCliRunnerTestModule();
|
||||
mockSuccessfulCliRun();
|
||||
@@ -334,7 +449,9 @@ describe("runCliAgent spawn path", () => {
|
||||
const argv = input.argv ?? [];
|
||||
expect(argv).not.toContain("--image");
|
||||
const promptCarrier = [input.input ?? "", ...argv].join("\n");
|
||||
const appendedPath = argv.find((value) => value.includes("openclaw-cli-images-"));
|
||||
const appendedPath = promptCarrier
|
||||
.split("\n")
|
||||
.find((value) => value.includes("openclaw-cli-images-"));
|
||||
expect(appendedPath).toBeDefined();
|
||||
expect(appendedPath).not.toBe(sourceImage);
|
||||
expect(promptCarrier).toContain(appendedPath ?? "");
|
||||
|
||||
Reference in New Issue
Block a user