Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  cli-runner.spawn.test.ts

  Sprache: JAVA
 

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
  __testing as replyRunTesting,
  createReplyOperation,
  replyRunRegistry,
} from "../auto-reply/reply/reply-run-registry.js";
import { onAgentEvent, resetAgentEventsForTest } from "../infra/agent-events.js";
import type { getProcessSupervisor } from "../process/supervisor/index.js";
import {
  makeBootstrapWarn as realMakeBootstrapWarn,
  resolveBootstrapContextForRun as realResolveBootstrapContextForRun,
} from "./bootstrap-files.js";
import { buildRunClaudeCliAgentParams } from "./cli-runner.js";
import {
  createManagedRun,
  mockSuccessfulCliRun,
  restoreCliRunnerPrepareTestDeps,
  supervisorSpawnMock,
} from "./cli-runner.test-support.js";
import {
  buildClaudeLiveArgs,
  resetClaudeLiveSessionsForTest,
  runClaudeLiveSessionTurn,
} from "./cli-runner/claude-live-session.js";
import { buildCliEnvAuthLog, executePreparedCliRun } from "./cli-runner/execute.js";
import { buildSystemPrompt } from "./cli-runner/helpers.js";
import { setCliRunnerPrepareTestDeps } from "./cli-runner/prepare.js";
import type { PreparedCliRunContext } from "./cli-runner/types.js";
import { createClaudeApiErrorFixture } from "./test-helpers/claude-api-error-fixture.js";

type ProcessSupervisor = ReturnType<typeof getProcessSupervisor>;
type SupervisorSpawnFn = ProcessSupervisor["spawn"];

beforeEach(() => {
  resetAgentEventsForTest();
  resetClaudeLiveSessionsForTest();
  replyRunTesting.resetReplyRunRegistry();
  restoreCliRunnerPrepareTestDeps();
  supervisorSpawnMock.mockClear();
});

afterEach(() => {
  resetClaudeLiveSessionsForTest();
  replyRunTesting.resetReplyRunRegistry();
});

function buildPreparedCliRunContext(params: {
  provider: "claude-cli" | "codex-cli";
  model: string;
  runId: string;
  prompt?: string;
  sessionId?: string;
  sessionKey?: string;
  backend?: Partial<PreparedCliRunContext["preparedBackend"]["backend"]>;
  config?: PreparedCliRunContext["params"]["config"];
  mcpConfigHash?: string;
  skillsSnapshot?: PreparedCliRunContext["params"]["skillsSnapshot"];
  workspaceDir?: string;
}): PreparedCliRunContext {
  const workspaceDir = params.workspaceDir ?? "/tmp";
  const baseBackend =
    params.provider === "claude-cli"
      ? {
          command: "claude",
          args: ["-p""--output-format""stream-json"],
          output: "jsonl" as const,
          input: "stdin" as const,
          modelArg: "--model",
          sessionArg: "--session-id",
          sessionMode: "always" as const,
          systemPromptArg: "--append-system-prompt",
          systemPromptWhen: "first" as const,
          serialize: true,
        }
      : {
          command: "codex",
          args: ["exec""--json"],
          resumeArgs: ["exec""resume""{sessionId}""--skip-git-repo-check"],
          output: "text" as const,
          input: "arg" as const,
          modelArg: "--model",
          sessionMode: "existing" as const,
          systemPromptFileConfigArg: "-c",
          systemPromptFileConfigKey: "model_instructions_file",
          systemPromptWhen: "first" as const,
          serialize: true,
        };
  const backend = { ...baseBackend, ...params.backend };
  return {
    params: {
      sessionId: params.sessionId ?? "s1",
      sessionKey: params.sessionKey,
      sessionFile: "/tmp/session.jsonl",
      workspaceDir,
      config: params.config,
      prompt: params.prompt ?? "hi",
      provider: params.provider,
      model: params.model,
      timeoutMs: 1_000,
      runId: params.runId,
      skillsSnapshot: params.skillsSnapshot,
    },
    started: Date.now(),
    workspaceDir,
    backendResolved: {
      id: params.provider,
      config: backend,
      bundleMcp: params.provider === "claude-cli",
      pluginId: params.provider === "claude-cli" ? "anthropic" : "openai",
    },
    preparedBackend: {
      backend,
      env: {},
      ...(params.mcpConfigHash ? { mcpConfigHash: params.mcpConfigHash } : {}),
    },
    reusableCliSession: {},
    modelId: params.model,
    normalizedModel: params.model,
    systemPrompt: "You are a helpful assistant.",
    systemPromptReport: {} as PreparedCliRunContext["systemPromptReport"],
    bootstrapPromptWarningLines: [],
    authEpochVersion: 2,
  };
}

describe("runCliAgent spawn path", () => {
  it("does not inject hardcoded 'Tools are disabled' text into CLI arguments", async () => {
    supervisorSpawnMock.mockResolvedValueOnce(
      createManagedRun({
        reason: "exit",
        exitCode: 0,
        exitSignal: null,
        durationMs: 50,
        stdout: "ok",
        stderr: "",
        timedOut: false,
        noOutputTimedOut: false,
      }),
    );

    const backendConfig = {
      command: "claude",
      args: ["-p""--output-format""stream-json"],
      output: "jsonl" as const,
      input: "stdin" as const,
      modelArg: "--model",
      sessionArg: "--session-id",
      systemPromptArg: "--append-system-prompt",
      systemPromptWhen: "first" as const,
      serialize: true,
    };
    const context: PreparedCliRunContext = {
      params: {
        sessionId: "s1",
        sessionFile: "/tmp/session.jsonl",
        workspaceDir: "/tmp",
        prompt: "Run: node script.mjs",
        provider: "claude-cli",
        model: "sonnet",
        timeoutMs: 1_000,
        runId: "run-no-tools-disabled",
        extraSystemPrompt: "You are a helpful assistant.",
      },
      started: Date.now(),
      workspaceDir: "/tmp",
      backendResolved: {
        id: "claude-cli",
        config: backendConfig,
        bundleMcp: true,
        pluginId: "anthropic",
      },
      preparedBackend: {
        backend: backendConfig,
        env: {},
      },
      reusableCliSession: {},
      modelId: "sonnet",
      normalizedModel: "sonnet",
      systemPrompt: "You are a helpful assistant.",
      systemPromptReport: {} as PreparedCliRunContext["systemPromptReport"],
      bootstrapPromptWarningLines: [],
      authEpochVersion: 2,
    };
    await executePreparedCliRun(context);

    const input = supervisorSpawnMock.mock.calls[0]?.[0] as { argv?: string[] };
    const allArgs = (input.argv ?? []).join("\n");
    expect(allArgs).not.toContain("Tools are disabled in this session");
    expect(allArgs).toContain("You are a helpful assistant.");
  });

  it("includes the OpenClaw skills prompt in CLI system prompts", () => {
    const systemPrompt = buildSystemPrompt({
      workspaceDir: "/tmp",
      modelDisplay: "claude-cli/sonnet",
      tools: [],
      skillsPrompt: [
        "<available_skills>",
        "  <skill>",
        "    <name>weather</name>",
        "    <description>Use weather tools.</description>",
        "    <location>/tmp/skills/weather/SKILL.md</location>",
        "  </skill>",
        "</available_skills>",
      ].join("\n"),
    });

    expect(systemPrompt).toContain("## Skills (mandatory)");
    expect(systemPrompt).toContain("<name>weather</name>");
    expect(systemPrompt).toContain("/tmp/skills/weather/SKILL.md");
  });

  it("pipes Claude prompts over stdin instead of argv", async () => {
    supervisorSpawnMock.mockResolvedValueOnce(
      createManagedRun({
        reason: "exit",
        exitCode: 0,
        exitSignal: null,
        durationMs: 50,
        stdout: "ok",
        stderr: "",
        timedOut: false,
        noOutputTimedOut: false,
      }),
    );

    await executePreparedCliRun(
      buildPreparedCliRunContext({
        provider: "claude-cli",
        model: "sonnet",
        runId: "run-stdin-claude",
        prompt: "Explain this diff",
      }),
    );

    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("passes --session-id for new Claude sessions", async () => {
    mockSuccessfulCliRun();

    await executePreparedCliRun(
      buildPreparedCliRunContext({
        provider: "claude-cli",
        model: "sonnet",
        runId: "run-claude-session-id",
      }),
    );

    const input = supervisorSpawnMock.mock.calls[0]?.[0] as {
      argv?: string[];
      input?: string;
      mode?: string;
    };
    expect(input.mode).toBe("child");
    expect(input.argv).toContain("claude");
    const sessionArgIndex = input.argv?.indexOf("--session-id") ?? -1;
    expect(sessionArgIndex).toBeGreaterThanOrEqual(0);
    expect(input.argv?.[sessionArgIndex + 1]?.trim()).toBeTruthy();
    expect(input.input).toContain("hi");
    expect(input.argv).not.toContain("hi");
  });

  it("passes OpenClaw skills to Claude as a session plugin", async () => {
    const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cli-skills-"));
    const skillDir = path.join(workspaceDir, "skills""weather");
    await fs.mkdir(skillDir, { recursive: true });
    await fs.writeFile(
      path.join(skillDir, "SKILL.md"),
      [
        "---",
        "name: weather",
        "description: Use weather tools for forecasts.",
        "---",
        "",
        "Read forecast data before replying.",
      ].join("\n"),
      "utf-8",
    );

    let pluginDir = "";
    supervisorSpawnMock.mockImplementationOnce(async (...args: unknown[]) => {
      const input = (args[0] ?? {}) as { argv?: string[] };
      const pluginArgIndex = input.argv?.indexOf("--plugin-dir") ?? -1;
      expect(pluginArgIndex).toBeGreaterThanOrEqual(0);
      pluginDir = input.argv?.[pluginArgIndex + 1] ?? "";
      const manifest = JSON.parse(
        await fs.readFile(path.join(pluginDir, ".claude-plugin""plugin.json"), "utf-8"),
      ) as { name?: string; skills?: string };
      expect(manifest).toMatchObject({
        name: "openclaw-skills",
        skills: "./skills",
      });
      await expect(
        fs.readFile(path.join(pluginDir, "skills""weather""SKILL.md"), "utf-8"),
      ).resolves.toContain("Read forecast data before replying.");
      return createManagedRun({
        reason: "exit",
        exitCode: 0,
        exitSignal: null,
        durationMs: 50,
        stdout: "ok",
        stderr: "",
        timedOut: false,
        noOutputTimedOut: false,
      });
    });

    try {
      await executePreparedCliRun(
        buildPreparedCliRunContext({
          provider: "claude-cli",
          model: "sonnet",
          runId: "run-claude-skills-plugin",
          workspaceDir,
          skillsSnapshot: {
            prompt: "",
            skills: [{ name: "weather" }],
            resolvedSkills: [
              {
                name: "weather",
                description: "Use weather tools for forecasts.",
                filePath: path.join(skillDir, "SKILL.md"),
                baseDir: skillDir,
                source: "test",
                sourceInfo: {
                  path: skillDir,
                  source: "test",
                  scope: "project",
                  origin: "top-level",
                  baseDir: skillDir,
                },
                disableModelInvocation: false,
              },
            ],
          },
        }),
      );
      await expect(fs.access(pluginDir)).rejects.toThrow();
    } finally {
      await fs.rm(workspaceDir, { recursive: true, force: true });
    }
  });

  it("injects skill env overrides into CLI child env and restores host env", async () => {
    const previousEnvValue = process.env.CLI_SKILL_API_KEY;
    delete process.env.CLI_SKILL_API_KEY;
    supervisorSpawnMock.mockImplementationOnce(async (...args: unknown[]) => {
      const input = (args[0] ?? {}) as { env?: Record<string, string> };
      expect(input.env?.CLI_SKILL_API_KEY).toBe("skill-secret");
      return createManagedRun({
        reason: "exit",
        exitCode: 0,
        exitSignal: null,
        durationMs: 50,
        stdout: "ok",
        stderr: "",
        timedOut: false,
        noOutputTimedOut: false,
      });
    });

    try {
      await executePreparedCliRun(
        buildPreparedCliRunContext({
          provider: "claude-cli",
          model: "sonnet",
          runId: "run-claude-skill-env",
          config: {
            skills: {
              entries: {
                envskill: { apiKey: "skill-secret" }, // pragma: allowlist secret
              },
            },
          },
          skillsSnapshot: {
            prompt: "",
            skills: [{ name: "envskill", primaryEnv: "CLI_SKILL_API_KEY" }],
          },
        }),
      );
      expect(process.env.CLI_SKILL_API_KEY).toBeUndefined();
    } finally {
      if (previousEnvValue === undefined) {
        delete process.env.CLI_SKILL_API_KEY;
      } else {
        process.env.CLI_SKILL_API_KEY = previousEnvValue;
      }
    }
  });

  it("ignores legacy claudeSessionId on the compat wrapper", () => {
    const params = buildRunClaudeCliAgentParams({
      sessionId: "openclaw-session",
      sessionFile: "/tmp/session.jsonl",
      workspaceDir: "/tmp",
      prompt: "hi",
      model: "opus",
      timeoutMs: 1_000,
      runId: "run-claude-legacy-wrapper",
      claudeSessionId: "c9d7b831-1c31-4d22-80b9-1e50ca207d4b",
    });

    expect(params.provider).toBe("claude-cli");
    expect(params.prompt).toBe("hi");
    expect(params).not.toHaveProperty("cliSessionId");
    expect(JSON.stringify(params)).not.toContain("c9d7b831-1c31-4d22-80b9-1e50ca207d4b");
  });

  it("forwards senderIsOwner through the compat wrapper", () => {
    const params = buildRunClaudeCliAgentParams({
      sessionId: "openclaw-session",
      sessionKey: "agent:main:matrix:room:123",
      sessionFile: "/tmp/session.jsonl",
      workspaceDir: "/tmp",
      prompt: "hi",
      model: "opus",
      timeoutMs: 1_000,
      runId: "run-claude-owner-wrapper",
      senderIsOwner: false,
    });

    expect(params.senderIsOwner).toBe(false);
  });

  it("forwards channel context through the compat wrapper", () => {
    const params = buildRunClaudeCliAgentParams({
      sessionId: "openclaw-session",
      sessionFile: "/tmp/session.jsonl",
      workspaceDir: "/tmp",
      prompt: "hi",
      timeoutMs: 1_000,
      runId: "run-claude-channel-wrapper",
      messageChannel: "telegram",
      messageProvider: "acp",
    });

    expect(params.messageChannel).toBe("telegram");
    expect(params.messageProvider).toBe("acp");
  });

  it("forwards static extra system prompt through the compat wrapper", () => {
    const params = buildRunClaudeCliAgentParams({
      sessionId: "openclaw-session",
      sessionFile: "/tmp/session.jsonl",
      workspaceDir: "/tmp",
      prompt: "hi",
      timeoutMs: 1_000,
      runId: "run-claude-static-prompt-wrapper",
      extraSystemPrompt: "dynamic\n\nstatic",
      extraSystemPromptStatic: "static",
    });

    expect(params.extraSystemPrompt).toBe("dynamic\n\nstatic");
    expect(params.extraSystemPromptStatic).toBe("static");
  });

  it("runs CLI through supervisor and returns payload", async () => {
    supervisorSpawnMock.mockResolvedValueOnce(
      createManagedRun({
        reason: "exit",
        exitCode: 0,
        exitSignal: null,
        durationMs: 50,
        stdout: "ok",
        stderr: "",
        timedOut: false,
        noOutputTimedOut: false,
      }),
    );

    const context = buildPreparedCliRunContext({
      provider: "codex-cli",
      model: "gpt-5.4",
      runId: "run-1",
    });
    context.reusableCliSession = { sessionId: "thread-123" };

    const result = await executePreparedCliRun(context, "thread-123");

    expect(result.text).toBe("ok");
    const input = supervisorSpawnMock.mock.calls[0]?.[0] as {
      argv?: string[];
      mode?: string;
      timeoutMs?: number;
      noOutputTimeoutMs?: number;
      replaceExistingScope?: boolean;
      scopeKey?: string;
    };
    expect(input.mode).toBe("child");
    expect(input.argv).toEqual([
      "codex",
      "exec",
      "resume",
      "thread-123",
      "--skip-git-repo-check",
      "--model",
      "gpt-5.4",
      "hi",
    ]);
    expect(input.timeoutMs).toBe(1_000);
    expect(input.noOutputTimeoutMs).toBeGreaterThanOrEqual(1_000);
    expect(input.replaceExistingScope).toBe(true);
    expect(input.scopeKey).toContain("thread-123");
  });

  it("passes Codex system prompts through model_instructions_file", async () => {
    let promptFileText = "";
    supervisorSpawnMock.mockImplementationOnce(async (...args: unknown[]) => {
      const input = (args[0] ?? {}) as { argv?: string[] };
      const configArgIndex = input.argv?.indexOf("-c") ?? -1;
      expect(configArgIndex).toBeGreaterThanOrEqual(0);
      const configArg = input.argv?.[configArgIndex + 1] ?? "";
      const match = /^model_instructions_file="(.+)"$/.exec(configArg);
      expect(match?.[1]).toBeTruthy();
      promptFileText = await fs.readFile(match?.[1] ?? """utf-8");
      return createManagedRun({
        reason: "exit",
        exitCode: 0,
        exitSignal: null,
        durationMs: 50,
        stdout: "ok",
        stderr: "",
        timedOut: false,
        noOutputTimedOut: false,
      });
    });

    await executePreparedCliRun(
      buildPreparedCliRunContext({
        provider: "codex-cli",
        model: "gpt-5.4",
        runId: "run-codex-system-prompt-file",
      }),
    );

    expect(promptFileText).toBe("You are a helpful assistant.");
  });

  it("cancels the managed CLI run when the abort signal fires", async () => {
    const abortController = new AbortController();
    let resolveWait!: (value: {
      reason:
        | "manual-cancel"
        | "overall-timeout"
        | "no-output-timeout"
        | "spawn-error"
        | "signal"
        | "exit";
      exitCode: number | null;
      exitSignal: NodeJS.Signals | number | null;
      durationMs: number;
      stdout: string;
      stderr: string;
      timedOut: boolean;
      noOutputTimedOut: boolean;
    }) => void;
    const cancel = vi.fn((reason?: string) => {
      resolveWait({
        reason: reason === "manual-cancel" ? "manual-cancel" : "signal",
        exitCode: null,
        exitSignal: null,
        durationMs: 50,
        stdout: "",
        stderr: "",
        timedOut: false,
        noOutputTimedOut: false,
      });
    });
    supervisorSpawnMock.mockResolvedValueOnce({
      runId: "run-supervisor",
      pid: 1234,
      startedAtMs: Date.now(),
      stdin: undefined,
      wait: vi.fn(
        async () =>
          await new Promise((resolve) => {
            resolveWait = resolve;
          }),
      ),
      cancel,
    });

    const context = buildPreparedCliRunContext({
      provider: "codex-cli",
      model: "gpt-5.4",
      runId: "run-abort",
    });
    context.params.abortSignal = abortController.signal;

    const runPromise = executePreparedCliRun(context);

    await vi.waitFor(() => {
      expect(supervisorSpawnMock).toHaveBeenCalledTimes(1);
    });
    abortController.abort();

    await expect(runPromise).rejects.toMatchObject({ name: "AbortError" });
    expect(cancel).toHaveBeenCalledWith("manual-cancel");
  });

  it("streams Claude text deltas from stream-json stdout", async () => {
    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 executePreparedCliRun(
        buildPreparedCliRunContext({
          provider: "claude-cli",
          model: "sonnet",
          runId: "run-claude-stream-json",
        }),
      );

      expect(result.text).toBe("Hello world");
      expect(agentEvents).toEqual([
        { stream: "assistant", text: "Hello", delta: "Hello" },
        { stream: "assistant", text: "Hello world", delta: " world" },
      ]);
    } finally {
      stop();
    }
  });

  it("reuses a Claude live session process across turns", async () => {
    const agentEvents: unknown[] = [];
    const stop = onAgentEvent((evt) => {
      if (evt.stream === "assistant") {
        agentEvents.push(evt.data);
      }
    });
    const writes: string[] = [];
    let stdoutListener: ((chunk: string) => void) | undefined;
    const stdin = {
      write: vi.fn((data: string, cb?: (err?: Error | null) => void) => {
        writes.push(data);
        const prompt = (JSON.parse(data) as { message: { content: string } }).message.content;
        const text = prompt === "first" ? "one" : "two";
        stdoutListener?.(
          [
            JSON.stringify({ type: "system", subtype: "init", session_id: "live-session-1" }),
            JSON.stringify({
              type: "stream_event",
              event: {
                type: "content_block_delta",
                delta: { type: "text_delta", text },
              },
            }),
            JSON.stringify({
              type: "result",
              session_id: "live-session-1",
              result: text,
            }),
          ].join("\n") + "\n",
        );
        cb?.();
      }),
      end: vi.fn(),
    };
    supervisorSpawnMock.mockImplementation(async (...args: unknown[]) => {
      const input = (args[0] ?? {}) as { onStdout?: (chunk: string) => void };
      stdoutListener = input.onStdout;
      return {
        runId: "live-run",
        pid: 2345,
        startedAtMs: Date.now(),
        stdin,
        wait: vi.fn(() => new Promise(() => {})),
        cancel: vi.fn(),
      };
    });

    try {
      const first = await executePreparedCliRun(
        buildPreparedCliRunContext({
          provider: "claude-cli",
          model: "sonnet",
          runId: "run-live-1",
          prompt: "first",
          backend: {
            args: [
              "-p",
              "--output-format",
              "stream-json",
              "--strict-mcp-config",
              "--mcp-config",
              "/tmp/mcp-one.json",
            ],
            liveSession: "claude-stdio",
          },
          mcpConfigHash: "same-mcp-config",
        }),
      );
      const second = await executePreparedCliRun(
        buildPreparedCliRunContext({
          provider: "claude-cli",
          model: "sonnet",
          runId: "run-live-2",
          prompt: "second",
          backend: {
            args: [
              "-p",
              "--output-format",
              "stream-json",
              "--strict-mcp-config",
              "--mcp-config",
              "/tmp/mcp-two.json",
            ],
            liveSession: "claude-stdio",
          },
          mcpConfigHash: "same-mcp-config",
        }),
      );

      const spawnInput = supervisorSpawnMock.mock.calls[0]?.[0] as {
        argv?: string[];
        stdinMode?: string;
      };
      expect(first.text).toBe("one");
      expect(second.text).toBe("two");
      expect(supervisorSpawnMock).toHaveBeenCalledOnce();
      expect(spawnInput.stdinMode).toBe("pipe-open");
      expect(spawnInput.argv).toContain("--input-format");
      expect(spawnInput.argv).toContain("stream-json");
      expect(spawnInput.argv).toContain("--replay-user-messages");
      expect(spawnInput.argv).not.toContain("--session-id");
      expect(spawnInput.argv).toContain("/tmp/mcp-one.json");
      expect(
        writes.map(
          (entry) => (JSON.parse(entry) as { message: { content: string } }).message.content,
        ),
      ).toEqual(["first""second"]);
      expect(agentEvents).toEqual([
        { text: "one", delta: "one" },
        { text: "two", delta: "two" },
      ]);
    } finally {
      stop();
    }
  });

  it("reports Claude live session reply backends as streaming until the turn finishes", async () => {
    let stdoutListener: ((chunk: string) => void) | undefined;
    let markWriteReady: (() => void) | undefined;
    const writeReady = new Promise<void>((resolve) => {
      markWriteReady = resolve;
    });
    const stdin = {
      write: vi.fn((_data: string, cb?: (err?: Error | null) => void) => {
        markWriteReady?.();
        cb?.();
      }),
      end: vi.fn(),
    };
    supervisorSpawnMock.mockImplementation(async (...args: unknown[]) => {
      const input = (args[0] ?? {}) as { onStdout?: (chunk: string) => void };
      stdoutListener = input.onStdout;
      return {
        runId: "live-run",
        pid: 2345,
        startedAtMs: Date.now(),
        stdin,
        wait: vi.fn(() => new Promise(() => {})),
        cancel: vi.fn(),
      };
    });
    const operation = createReplyOperation({
      sessionKey: "agent:main:main",
      sessionId: "live-session-reply",
      resetTriggered: false,
    });
    operation.setPhase("running");
    const context = buildPreparedCliRunContext({
      provider: "claude-cli",
      model: "sonnet",
      runId: "run-live-reply-streaming",
      sessionId: "live-session-reply",
      sessionKey: "agent:main:main",
      prompt: "hello",
      backend: {
        liveSession: "claude-stdio",
      },
    });

    const run = executePreparedCliRun({
      ...context,
      params: {
        ...context.params,
        replyOperation: operation,
      },
    });

    await writeReady;
    expect(replyRunRegistry.isStreaming("agent:main:main")).toBe(true);

    stdoutListener?.(
      [
        JSON.stringify({ type: "system", subtype: "init", session_id: "live-session-reply" }),
        JSON.stringify({
          type: "result",
          session_id: "live-session-reply",
          result: "done",
        }),
      ].join("\n") + "\n",
    );

    await expect(run).resolves.toMatchObject({ text: "done" });
    expect(replyRunRegistry.isStreaming("agent:main:main")).toBe(false);
    operation.complete();
  });

  it("reuses a Claude live session when resumed turns omit the system prompt arg", async () => {
    let stdoutListener: ((chunk: string) => void) | undefined;
    let turn = 0;
    const stdin = {
      write: vi.fn((_data: string, cb?: (err?: Error | null) => void) => {
        turn += 1;
        stdoutListener?.(
          [
            JSON.stringify({ type: "system", subtype: "init", session_id: "live-system" }),
            JSON.stringify({
              type: "result",
              session_id: "live-system",
              result: turn === 1 ? "one" : "two",
            }),
          ].join("\n") + "\n",
        );
        cb?.();
      }),
      end: vi.fn(),
    };
    supervisorSpawnMock.mockImplementation(async (...args: unknown[]) => {
      const input = (args[0] ?? {}) as { onStdout?: (chunk: string) => void };
      stdoutListener = input.onStdout;
      return {
        runId: "live-run",
        pid: 2345,
        startedAtMs: Date.now(),
        stdin,
        wait: vi.fn(() => new Promise(() => {})),
        cancel: vi.fn(),
      };
    });

    const backend = {
      resumeArgs: ["-p""--output-format""stream-json""--resume={sessionId}"],
      liveSession: "claude-stdio" as const,
    };
    const first = await executePreparedCliRun(
      buildPreparedCliRunContext({
        provider: "claude-cli",
        model: "sonnet",
        runId: "run-live-system-1",
        prompt: "first",
        backend,
      }),
    );
    const second = await executePreparedCliRun(
      buildPreparedCliRunContext({
        provider: "claude-cli",
        model: "sonnet",
        runId: "run-live-system-2",
        prompt: "second",
        backend,
      }),
      "live-system",
    );

    expect(first.text).toBe("one");
    expect(second.text).toBe("two");
    expect(supervisorSpawnMock).toHaveBeenCalledOnce();
  });

  it("serializes concurrent Claude live session creation for the same key", async () => {
    let stdoutListener: ((chunk: string) => void) | undefined;
    let releaseSpawn: (() => void) | undefined;
    let turn = 0;
    const spawnReady = new Promise<void>((resolve) => {
      releaseSpawn = resolve;
    });
    const stdin = {
      write: vi.fn((_data: string, cb?: (err?: Error | null) => void) => {
        turn += 1;
        stdoutListener?.(
          [
            JSON.stringify({ type: "system", subtype: "init", session_id: "live-concurrent" }),
            JSON.stringify({
              type: "result",
              session_id: "live-concurrent",
              result: turn === 1 ? "one" : "two",
            }),
          ].join("\n") + "\n",
        );
        cb?.();
      }),
      end: vi.fn(),
    };
    supervisorSpawnMock.mockImplementation(async (...args: unknown[]) => {
      const input = (args[0] ?? {}) as { onStdout?: (chunk: string) => void };
      stdoutListener = input.onStdout;
      await spawnReady;
      return {
        runId: "live-run",
        pid: 2345,
        startedAtMs: Date.now(),
        stdin,
        wait: vi.fn(() => new Promise(() => {})),
        cancel: vi.fn(),
      };
    });

    const backend = {
      liveSession: "claude-stdio" as const,
    };
    const first = executePreparedCliRun(
      buildPreparedCliRunContext({
        provider: "claude-cli",
        model: "sonnet",
        runId: "run-live-concurrent-1",
        prompt: "first",
        backend,
      }),
    );
    const second = executePreparedCliRun(
      buildPreparedCliRunContext({
        provider: "claude-cli",
        model: "sonnet",
        runId: "run-live-concurrent-2",
        prompt: "second",
        backend,
      }),
    );
    await vi.waitFor(() => expect(supervisorSpawnMock).toHaveBeenCalledOnce());
    releaseSpawn?.();

    await expect(Promise.all([first, second])).resolves.toEqual([
      expect.objectContaining({ text: "one" }),
      expect.objectContaining({ text: "two" }),
    ]);
    expect(supervisorSpawnMock).toHaveBeenCalledOnce();
  });

  it("counts pending Claude live session creates against the session cap", async () => {
    let releaseSpawn: (() => void) | undefined;
    const spawnReady = new Promise<void>((resolve) => {
      releaseSpawn = resolve;
    });
    supervisorSpawnMock.mockImplementation(async (...args: unknown[]) => {
      const input = (args[0] ?? {}) as { onStdout?: (chunk: string) => void };
      const spawnIndex = supervisorSpawnMock.mock.calls.length;
      await spawnReady;
      const stdin = {
        write: vi.fn((_data: string, cb?: (err?: Error | null) => void) => {
          input.onStdout?.(
            [
              JSON.stringify({
                type: "system",
                subtype: "init",
                session_id: `live-cap-${spawnIndex}`,
              }),
              JSON.stringify({
                type: "result",
                session_id: `live-cap-${spawnIndex}`,
                result: `ok-${spawnIndex}`,
              }),
            ].join("\n") + "\n",
          );
          cb?.();
        }),
        end: vi.fn(),
      };
      return {
        runId: `live-run-${spawnIndex}`,
        pid: 2300 + spawnIndex,
        startedAtMs: Date.now(),
        stdin,
        wait: vi.fn(() => new Promise(() => {})),
        cancel: vi.fn(),
      };
    });

    const backend = {
      liveSession: "claude-stdio" as const,
    };
    const runs = Array.from({ length: 17 }, (_, index) =>
      (() => {
        const context = buildPreparedCliRunContext({
          provider: "claude-cli",
          model: "sonnet",
          runId: `run-live-cap-${index}`,
          prompt: `prompt ${index}`,
          sessionId: `session-${index}`,
          backend,
        });
        return runClaudeLiveSessionTurn({
          context,
          args: context.preparedBackend.backend.args ?? [],
          env: {},
          prompt: `prompt ${index}`,
          useResume: false,
          noOutputTimeoutMs: 1_000,
          getProcessSupervisor: () => ({
            spawn: (params: Parameters<SupervisorSpawnFn>[0]) =>
              supervisorSpawnMock(params) as ReturnType<SupervisorSpawnFn>,
            cancel: vi.fn(),
            cancelScope: vi.fn(),
            reconcileOrphans: vi.fn(),
            getRecord: vi.fn(),
          }),
          onAssistantDelta: () => {},
          cleanup: async () => {},
        });
      })(),
    );

    await vi.waitFor(() => expect(supervisorSpawnMock).toHaveBeenCalledTimes(16));
    const rejectedRun = runs[16];
    expect(rejectedRun).toBeDefined();
    await expect(rejectedRun).rejects.toThrow("Too many Claude CLI live sessions are active.");
    releaseSpawn?.();
    await expect(Promise.all(runs.slice(016))).resolves.toHaveLength(16);
    expect(supervisorSpawnMock).toHaveBeenCalledTimes(16);
  });

  it("preserves Claude resume args when building live session argv", () => {
    const backend: PreparedCliRunContext["preparedBackend"]["backend"] = {
      command: "claude",
      args: ["-p""--output-format""stream-json"],
      output: "jsonl",
      input: "stdin",
      sessionArg: "--session-id",
      systemPromptArg: "--append-system-prompt",
    };

    const args = buildClaudeLiveArgs({
      args: [
        "-p",
        "--output-format",
        "stream-json",
        "--resume",
        "claude-session",
        "--session-id",
        "openclaw-session",
        "--append-system-prompt",
        "old prompt",
      ],
      backend,
      systemPrompt: "current prompt",
      useResume: true,
    });

    expect(args).toContain("--resume");
    expect(args).toContain("claude-session");
    expect(args).not.toContain("--session-id");
    expect(args).not.toContain("openclaw-session");
    expect(args).not.toContain("--append-system-prompt");
    expect(args).not.toContain("old prompt");
    expect(args).not.toContain("current prompt");
  });

  it("restarts Claude live sessions for env changes and fresh retries", async () => {
    const cancels: Array<ReturnType<typeof vi.fn>> = [];
    const turnResults = ["first-ok""resume-ok""env-ok""fresh-ok"];
    let turnIndex = 0;
    supervisorSpawnMock.mockImplementation(async (...args: unknown[]) => {
      const spawnIndex = supervisorSpawnMock.mock.calls.length;
      const input = (args[0] ?? {}) as { onStdout?: (chunk: string) => void };
      const cancel = vi.fn();
      cancels.push(cancel);
      return {
        runId: `live-run-${spawnIndex}`,
        pid: 2345 + spawnIndex,
        startedAtMs: Date.now(),
        stdin: {
          write: vi.fn((_data: string, cb?: (err?: Error | null) => void) => {
            const result = turnResults[turnIndex] ?? "ok";
            turnIndex += 1;
            input.onStdout?.(
              [
                JSON.stringify({ type: "system", subtype: "init", session_id: "live-session" }),
                JSON.stringify({
                  type: "result",
                  session_id: "live-session",
                  result,
                }),
              ].join("\n") + "\n",
            );
            cb?.();
          }),
          end: vi.fn(),
        },
        wait: vi.fn(() => new Promise(() => {})),
        cancel,
      };
    });
    const runTurn = async (runId: string, args: string[], env: Record<string, string>) => {
      const context = buildPreparedCliRunContext({
        provider: "claude-cli",
        model: "sonnet",
        runId,
        backend: {
          liveSession: "claude-stdio",
          resumeArgs: ["-p""--output-format""stream-json""--resume""{sessionId}"],
        },
      });
      const result = await runClaudeLiveSessionTurn({
        context,
        args,
        env,
        prompt: "hi",
        useResume: args.some((entry) => entry.startsWith("--resume")),
        noOutputTimeoutMs: 1_000,
        getProcessSupervisor: () => ({
          spawn: (params: Parameters<SupervisorSpawnFn>[0]) =>
            supervisorSpawnMock(params) as ReturnType<SupervisorSpawnFn>,
          cancel: vi.fn(),
          cancelScope: vi.fn(),
          reconcileOrphans: vi.fn(),
          getRecord: vi.fn(),
        }),
        onAssistantDelta: () => {},
        cleanup: async () => {},
      });
      return result.output.text;
    };
    const freshArgs = ["-p""--output-format""stream-json"];
    const resumeArgs = ["-p""--output-format""stream-json""--resume""live-session"];

    await expect(
      runTurn("run-live-fresh", freshArgs, { ANTHROPIC_BASE_URL: "https://one.example" }),
    ).resolves.toBe("first-ok");
    await expect(
      runTurn("run-live-resume", resumeArgs, { ANTHROPIC_BASE_URL: "https://one.example" }),
    ).resolves.toBe("resume-ok");
    expect(supervisorSpawnMock).toHaveBeenCalledTimes(1);
    expect(cancels[0]).not.toHaveBeenCalled();

    await expect(
      runTurn("run-live-env-change", resumeArgs, { ANTHROPIC_BASE_URL: "https://two.example" }),
    ).resolves.toBe("env-ok");
    expect(supervisorSpawnMock).toHaveBeenCalledTimes(2);
    expect(cancels[0]).toHaveBeenCalledWith("manual-cancel");

    await expect(
      runTurn("run-live-fresh-retry", freshArgs, {
        ANTHROPIC_BASE_URL: "https://two.example",
      }),
    ).resolves.toBe("fresh-ok");

    expect(supervisorSpawnMock).toHaveBeenCalledTimes(3);
    expect(cancels[1]).toHaveBeenCalledWith("manual-cancel");
    expect(cancels[2]).not.toHaveBeenCalled();
  });

  it("ignores non-JSON stdout lines from Claude live sessions", async () => {
    let stdoutListener: ((chunk: string) => void) | undefined;
    const stdin = {
      write: vi.fn((_data: string, cb?: (err?: Error | null) => void) => {
        stdoutListener?.(
          [
            "Claude CLI warning",
            JSON.stringify({ type: "system", subtype: "init", session_id: "live-mixed" }),
            JSON.stringify({
              type: "result",
              session_id: "live-mixed",
              result: "mixed-ok",
            }),
          ].join("\n") + "\n",
        );
        cb?.();
      }),
      end: vi.fn(),
    };
    supervisorSpawnMock.mockImplementationOnce(async (...args: unknown[]) => {
      const input = (args[0] ?? {}) as { onStdout?: (chunk: string) => void };
      stdoutListener = input.onStdout;
      return {
        runId: "live-run",
        pid: 2345,
        startedAtMs: Date.now(),
        stdin,
        wait: vi.fn(() => new Promise(() => {})),
        cancel: vi.fn(),
      };
    });

    const result = await executePreparedCliRun(
      buildPreparedCliRunContext({
        provider: "claude-cli",
        model: "sonnet",
        runId: "run-live-mixed",
        backend: {
          liveSession: "claude-stdio",
        },
      }),
    );

    expect(result.text).toBe("mixed-ok");
  });

  it("fails Claude live turns on is_error results", async () => {
    let stdoutListener: ((chunk: string) => void) | undefined;
    const stdin = {
      write: vi.fn((_data: string, cb?: (err?: Error | null) => void) => {
        stdoutListener?.(
          [
            JSON.stringify({ type: "system", subtype: "init", session_id: "live-error" }),
            JSON.stringify({
              type: "result",
              session_id: "live-error",
              is_error: true,
              result: "Credit balance is too low",
            }),
          ].join("\n") + "\n",
        );
        cb?.();
      }),
      end: vi.fn(),
    };
    supervisorSpawnMock.mockImplementationOnce(async (...args: unknown[]) => {
      const input = (args[0] ?? {}) as { onStdout?: (chunk: string) => void };
      stdoutListener = input.onStdout;
      return {
        runId: "live-run",
        pid: 2345,
        startedAtMs: Date.now(),
        stdin,
        wait: vi.fn(() => new Promise(() => {})),
        cancel: vi.fn(),
      };
    });

    await expect(
      executePreparedCliRun(
        buildPreparedCliRunContext({
          provider: "claude-cli",
          model: "sonnet",
          runId: "run-live-error",
          backend: {
            liveSession: "claude-stdio",
          },
        }),
      ),
    ).rejects.toMatchObject({
      name: "FailoverError",
      message: "Credit balance is too low",
    });
  });

  it("fails when Claude exits before a live turn starts", async () => {
    supervisorSpawnMock.mockImplementationOnce(async () => ({
      runId: "live-run",
      pid: 2345,
      startedAtMs: Date.now(),
      stdin: {
        write: vi.fn(),
        end: vi.fn(),
      },
      wait: vi.fn(async () => ({
        reason: "exit",
        exitCode: 1,
        exitSignal: null,
        durationMs: 1,
        stdout: "",
        stderr: "startup failed",
        timedOut: false,
        noOutputTimedOut: false,
      })),
      cancel: vi.fn(),
    }));

    await expect(
      executePreparedCliRun(
        buildPreparedCliRunContext({
          provider: "claude-cli",
          model: "sonnet",
          runId: "run-live-startup-exit",
          backend: {
            liveSession: "claude-stdio",
          },
        }),
      ),
    ).rejects.toThrow("Claude CLI live session closed before handling the turn");
  });

  it("restarts the Claude live process after request abort", async () => {
    const abortController = new AbortController();
    let stdoutListener: ((chunk: string) => void) | undefined;
    const cancels: Array<ReturnType<typeof vi.fn>> = [];
    supervisorSpawnMock.mockImplementation(async (...args: unknown[]) => {
      const input = (args[0] ?? {}) as { onStdout?: (chunk: string) => void };
      stdoutListener = input.onStdout;
      const spawnIndex = supervisorSpawnMock.mock.calls.length;
      const cancel = vi.fn();
      cancels.push(cancel);
      const stdin = {
        write: vi.fn((_data: string, cb?: (err?: Error | null) => void) => {
          if (spawnIndex === 2) {
            stdoutListener?.(
              [
                JSON.stringify({ type: "system", subtype: "init", session_id: "live-abort-2" }),
                JSON.stringify({
                  type: "result",
                  session_id: "live-abort-2",
                  result: "second-ok",
                }),
              ].join("\n") + "\n",
            );
          }
          cb?.();
        }),
        end: vi.fn(),
      };
      return {
        runId: `live-run-${spawnIndex}`,
        pid: 2345 + spawnIndex,
        startedAtMs: Date.now(),
        stdin,
        wait: vi.fn(
          () =>
            new Promise((resolve) => {
              if (spawnIndex === 1) {
                cancel.mockImplementationOnce(() => {
                  resolve({
                    reason: "manual-cancel",
                    exitCode: null,
                    exitSignal: null,
                    durationMs: 50,
                    stdout: "",
                    stderr: "",
                    timedOut: false,
                    noOutputTimedOut: false,
                  });
                });
              }
            }),
        ),
        cancel,
      };
    });

    const firstContext = buildPreparedCliRunContext({
      provider: "claude-cli",
      model: "sonnet",
      runId: "run-live-abort-1",
      backend: {
        liveSession: "claude-stdio",
      },
    });
    firstContext.params.abortSignal = abortController.signal;
    const first = executePreparedCliRun(firstContext);

    await vi.waitFor(() => {
      expect(supervisorSpawnMock).toHaveBeenCalledTimes(1);
    });
    abortController.abort();

    await expect(first).rejects.toMatchObject({ name: "AbortError" });
    expect(cancels[0]).toHaveBeenCalledWith("manual-cancel");
    stdoutListener?.(
      [
        JSON.stringify({ type: "system", subtype: "init", session_id: "live-abort" }),
        JSON.stringify({
          type: "result",
          session_id: "live-abort",
          result: "discarded",
        }),
      ].join("\n") + "\n",
    );

    const second = await executePreparedCliRun(
      buildPreparedCliRunContext({
        provider: "claude-cli",
        model: "sonnet",
        runId: "run-live-abort-2",
        backend: {
          liveSession: "claude-stdio",
        },
      }),
    );

    expect(second.text).toBe("second-ok");
    expect(supervisorSpawnMock).toHaveBeenCalledTimes(2);
  });

  it("restarts Claude live sessions when selected skills change", async () => {
    const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-live-skills-"));
    const weatherDir = path.join(workspaceDir, "skills""weather");
    const gitDir = path.join(workspaceDir, "skills""git");
    await fs.mkdir(weatherDir, { recursive: true });
    await fs.mkdir(gitDir, { recursive: true });
    await fs.writeFile(path.join(weatherDir, "SKILL.md"), "weather instructions\n""utf-8");
    await fs.writeFile(path.join(gitDir, "SKILL.md"), "git instructions\n""utf-8");

    const cancels: Array<ReturnType<typeof vi.fn>> = [];
    supervisorSpawnMock.mockImplementation(async (...args: unknown[]) => {
      const spawnIndex = supervisorSpawnMock.mock.calls.length;
      const input = (args[0] ?? {}) as { onStdout?: (chunk: string) => void };
      const cancel = vi.fn();
      cancels.push(cancel);
      const stdin = {
        write: vi.fn((_data: string, cb?: (err?: Error | null) => void) => {
          const text = spawnIndex === 1 ? "weather-ok" : "git-ok";
          input.onStdout?.(
            [
              JSON.stringify({ type: "system", subtype: "init", session_id: `live-${spawnIndex}` }),
              JSON.stringify({
                type: "result",
                session_id: `live-${spawnIndex}`,
                result: text,
              }),
            ].join("\n") + "\n",
          );
          cb?.();
        }),
        end: vi.fn(),
      };
      return {
        runId: `live-run-${spawnIndex}`,
        pid: 2345 + spawnIndex,
        startedAtMs: Date.now(),
        stdin,
        wait: vi.fn(() => new Promise(() => {})),
        cancel,
      };
    });

    try {
      const first = await executePreparedCliRun(
        buildPreparedCliRunContext({
          provider: "claude-cli",
          model: "sonnet",
          runId: "run-live-skills-1",
          prompt: "first",
          workspaceDir,
          backend: {
            liveSession: "claude-stdio",
          },
          skillsSnapshot: {
            prompt: "weather",
            skills: [{ name: "weather" }],
            resolvedSkills: [
              {
                name: "weather",
                description: "Weather instructions.",
                filePath: path.join(weatherDir, "SKILL.md"),
                baseDir: weatherDir,
                source: "test",
                sourceInfo: {
                  path: weatherDir,
                  source: "test",
                  scope: "project",
                  origin: "top-level",
                  baseDir: weatherDir,
                },
                disableModelInvocation: false,
              },
            ],
          },
        }),
      );
      const second = await executePreparedCliRun(
        buildPreparedCliRunContext({
          provider: "claude-cli",
          model: "sonnet",
          runId: "run-live-skills-2",
          prompt: "second",
          workspaceDir,
          backend: {
            liveSession: "claude-stdio",
          },
          skillsSnapshot: {
            prompt: "git",
            skills: [{ name: "git" }],
            resolvedSkills: [
              {
                name: "git",
                description: "Git instructions.",
                filePath: path.join(gitDir, "SKILL.md"),
                baseDir: gitDir,
                source: "test",
                sourceInfo: {
                  path: gitDir,
                  source: "test",
                  scope: "project",
                  origin: "top-level",
                  baseDir: gitDir,
                },
                disableModelInvocation: false,
              },
            ],
          },
        }),
      );

      expect(first.text).toBe("weather-ok");
      expect(second.text).toBe("git-ok");
      expect(supervisorSpawnMock).toHaveBeenCalledTimes(2);
      expect(cancels[0]).toHaveBeenCalledWith("manual-cancel");
      expect(cancels[1]).not.toHaveBeenCalled();
    } finally {
      await fs.rm(workspaceDir, { recursive: true, force: true });
    }
  });

  it("closes idle Claude live sessions after ten minutes", async () => {
    vi.useFakeTimers();
    const writes: string[] = [];
    let stdoutListener: ((chunk: string) => void) | undefined;
    const cancel = vi.fn();
    const stdin = {
      write: vi.fn((data: string, cb?: (err?: Error | null) => void) => {
        writes.push(data);
        stdoutListener?.(
          [
            JSON.stringify({ type: "system", subtype: "init", session_id: "live-session-idle" }),
            JSON.stringify({
              type: "result",
              session_id: "live-session-idle",
              result: "idle-ok",
            }),
          ].join("\n") + "\n",
        );
        cb?.();
      }),
      end: vi.fn(),
    };
    supervisorSpawnMock.mockImplementation(async (...args: unknown[]) => {
      const input = (args[0] ?? {}) as { onStdout?: (chunk: string) => void };
      stdoutListener = input.onStdout;
      return {
        runId: "live-run",
        pid: 2345,
        startedAtMs: Date.now(),
        stdin,
        wait: vi.fn(() => new Promise(() => {})),
        cancel,
      };
    });

    try {
      const result = await executePreparedCliRun(
        buildPreparedCliRunContext({
          provider: "claude-cli",
          model: "sonnet",
          runId: "run-live-idle",
          prompt: "idle",
          backend: {
            liveSession: "claude-stdio",
          },
        }),
      );

      expect(result.text).toBe("idle-ok");
      expect(cancel).not.toHaveBeenCalled();
      await vi.advanceTimersByTimeAsync(10 * 60 * 1_000 - 1);
      expect(cancel).not.toHaveBeenCalled();
      await vi.advanceTimersByTimeAsync(1);
      expect(cancel).toHaveBeenCalledWith("manual-cancel");
      expect(
        writes.map(
          (entry) => (JSON.parse(entry) as { message: { content: string } }).message.content,
        ),
      ).toEqual(["idle"]);
    } finally {
      vi.useRealTimers();
    }
  });

  it("does not surface stale stderr after a later Claude live exit", async () => {
    let stdoutListener: ((chunk: string) => void) | undefined;
    let stderrListener: ((chunk: string) => void) | undefined;
    let resolveExit!: (value: {
      reason: "exit";
      exitCode: number;
      exitSignal: null;
      durationMs: number;
      stdout: string;
      stderr: string;
      timedOut: false;
      noOutputTimedOut: false;
    }) => void;
    const wait = new Promise<{
      reason: "exit";
      exitCode: number;
      exitSignal: null;
      durationMs: number;
      stdout: string;
      stderr: string;
      timedOut: false;
      noOutputTimedOut: false;
    }>((resolve) => {
      resolveExit = resolve;
    });
    let writeCount = 0;
    const stdin = {
      write: vi.fn((_data: string, cb?: (err?: Error | null) => void) => {
        writeCount += 1;
        if (writeCount === 1) {
          stderrListener?.("stale stderr from first turn");
          stdoutListener?.(
            [
              JSON.stringify({ type: "system", subtype: "init", session_id: "live-stderr" }),
              JSON.stringify({
                type: "result",
                session_id: "live-stderr",
                result: "first-ok",
              }),
            ].join("\n") + "\n",
          );
          cb?.();
          return;
        }
        cb?.();
        resolveExit({
          reason: "exit",
          exitCode: 1,
          exitSignal: null,
          durationMs: 50,
          stdout: "",
          stderr: "",
          timedOut: false,
          noOutputTimedOut: false,
        });
      }),
      end: vi.fn(),
    };
    supervisorSpawnMock.mockImplementationOnce(async (...args: unknown[]) => {
      const input = (args[0] ?? {}) as {
        onStdout?: (chunk: string) => void;
        onStderr?: (chunk: string) => void;
      };
      stdoutListener = input.onStdout;
      stderrListener = input.onStderr;
      return {
        runId: "live-run",
        pid: 2345,
        startedAtMs: Date.now(),
        stdin,
        wait: vi.fn(() => wait),
        cancel: vi.fn(),
      };
    });

    const first = await executePreparedCliRun(
      buildPreparedCliRunContext({
        provider: "claude-cli",
        model: "sonnet",
        runId: "run-live-stderr-1",
        prompt: "first",
        backend: {
          liveSession: "claude-stdio",
        },
      }),
    );
    const second = executePreparedCliRun(
      buildPreparedCliRunContext({
        provider: "claude-cli",
        model: "sonnet",
        runId: "run-live-stderr-2",
        prompt: "second",
        backend: {
          liveSession: "claude-stdio",
        },
      }),
    );

    expect(first.text).toBe("first-ok");
    await expect(second).rejects.toMatchObject({
      name: "FailoverError",
      message: "Claude CLI failed.",
    });
  });

  it("surfaces nested Claude stream-json API errors instead of raw event output", async () => {
    const { message, jsonl } = createClaudeApiErrorFixture();

    supervisorSpawnMock.mockResolvedValueOnce(
      createManagedRun({
        reason: "exit",
        exitCode: 1,
        exitSignal: null,
        durationMs: 50,
        stdout: jsonl,
        stderr: "",
        timedOut: false,
        noOutputTimedOut: false,
      }),
    );

    const run = executePreparedCliRun(
      buildPreparedCliRunContext({
        provider: "claude-cli",
        model: "sonnet",
        runId: "run-claude-api-error",
      }),
    );

    await expect(run).rejects.toMatchObject({
      name: "FailoverError",
      message,
      reason: "billing",
      status: 402,
    });
  });

  it("sanitizes dangerous backend env overrides before spawn", async () => {
    mockSuccessfulCliRun();
    await executePreparedCliRun(
      buildPreparedCliRunContext({
        provider: "codex-cli",
        model: "gpt-5.4",
        runId: "run-env-sanitized",
        backend: {
          env: {
            NODE_OPTIONS: "--require ./malicious.js",
            LD_PRELOAD: "/tmp/pwn.so",
            PATH: "/tmp/evil",
            HOME: "/tmp/evil-home",
            SAFE_KEY: "ok",
          },
        },
      }),
      "thread-123",
    );

    const input = supervisorSpawnMock.mock.calls[0]?.[0] as {
      env?: Record<string, string | undefined>;
    };
    expect(input.env?.SAFE_KEY).toBe("ok");
    expect(input.env?.PATH).toBe(process.env.PATH);
    expect(input.env?.HOME).toBe(process.env.HOME);
    expect(input.env?.NODE_OPTIONS).toBeUndefined();
    expect(input.env?.LD_PRELOAD).toBeUndefined();
  });

  it("applies clearEnv after sanitizing backend env overrides", async () => {
    process.env.SAFE_CLEAR = "from-base";
    mockSuccessfulCliRun();
    await executePreparedCliRun(
      buildPreparedCliRunContext({
        provider: "codex-cli",
        model: "gpt-5.4",
        runId: "run-clear-env",
        backend: {
          env: {
            SAFE_KEEP: "keep-me",
          },
          clearEnv: ["SAFE_CLEAR"],
        },
      }),
      "thread-123",
    );

    const input = supervisorSpawnMock.mock.calls[0]?.[0] as {
      env?: Record<string, string | undefined>;
    };
    expect(input.env?.SAFE_KEEP).toBe("keep-me");
    expect(input.env?.SAFE_CLEAR).toBeUndefined();
  });

  it("can preserve selected clearEnv keys for live CLI backend probes", async () => {
    try {
      process.env.OPENCLAW_LIVE_CLI_BACKEND_PRESERVE_ENV = '["SAFE_CLEAR"]';
      process.env.SAFE_CLEAR = "from-base";
      mockSuccessfulCliRun();
      await executePreparedCliRun(
        buildPreparedCliRunContext({
          provider: "codex-cli",
          model: "gpt-5.4",
          runId: "run-clear-env-preserve",
          backend: {
            clearEnv: ["SAFE_CLEAR""SAFE_DROP"],
          },
        }),
        "thread-123",
      );

      const input = supervisorSpawnMock.mock.calls[0]?.[0] as {
        env?: Record<string, string | undefined>;
      };
      expect(input.env?.SAFE_CLEAR).toBe("from-base");
      expect(input.env?.SAFE_DROP).toBeUndefined();
    } finally {
      delete process.env.OPENCLAW_LIVE_CLI_BACKEND_PRESERVE_ENV;
      delete process.env.SAFE_CLEAR;
    }
  });

  it("keeps explicit backend env overrides even when clearEnv drops inherited values", async () => {
    process.env.SAFE_OVERRIDE = "from-base";
    mockSuccessfulCliRun();
    await executePreparedCliRun(
      buildPreparedCliRunContext({
        provider: "codex-cli",
        model: "gpt-5.4",
        runId: "run-clear-env-override",
        backend: {
          env: {
            SAFE_OVERRIDE: "from-override",
          },
          clearEnv: ["SAFE_OVERRIDE"],
        },
      }),
      "thread-123",
    );

    const input = supervisorSpawnMock.mock.calls[0]?.[0] as {
      env?: Record<string, string | undefined>;
    };
    expect(input.env?.SAFE_OVERRIDE).toBe("from-override");
  });

  it("clears claude-cli provider-routing, auth, telemetry, and host-managed env", async () => {
    vi.stubEnv("ANTHROPIC_BASE_URL""https://proxy.example.com/v1");
    vi.stubEnv("ANTHROPIC_API_TOKEN""env-api-token");
    vi.stubEnv("ANTHROPIC_CUSTOM_HEADERS""x-test-header: env");
    vi.stubEnv("ANTHROPIC_OAUTH_TOKEN""env-oauth-token");
    vi.stubEnv("CLAUDE_CODE_USE_BEDROCK""1");
    vi.stubEnv("ANTHROPIC_AUTH_TOKEN""env-auth-token");
    vi.stubEnv("CLAUDE_CODE_OAUTH_TOKEN""env-oauth-token");
    vi.stubEnv("CLAUDE_CODE_REMOTE""1");
    vi.stubEnv("ANTHROPIC_UNIX_SOCKET""/tmp/anthropic.sock");
    vi.stubEnv("OTEL_LOGS_EXPORTER""none");
    vi.stubEnv("OTEL_METRICS_EXPORTER""none");
    vi.stubEnv("OTEL_TRACES_EXPORTER""none");
    vi.stubEnv("OTEL_EXPORTER_OTLP_PROTOCOL""none");
    vi.stubEnv("OTEL_SDK_DISABLED""true");
    vi.stubEnv("CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST""1");
    mockSuccessfulCliRun();

    await executePreparedCliRun(
      buildPreparedCliRunContext({
        provider: "claude-cli",
        model: "claude-sonnet-4-6",
        runId: "run-claude-env-hardened",
        backend: {
          env: {
            SAFE_KEEP: "ok",
            ANTHROPIC_BASE_URL: "https://override.example.com/v1",
            CLAUDE_CODE_OAUTH_TOKEN: "override-oauth-token",
            CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST: "1",
          },
          clearEnv: [
            "ANTHROPIC_BASE_URL",
            "ANTHROPIC_API_TOKEN",
            "ANTHROPIC_CUSTOM_HEADERS",
            "ANTHROPIC_OAUTH_TOKEN",
            "CLAUDE_CODE_USE_BEDROCK",
            "ANTHROPIC_AUTH_TOKEN",
            "CLAUDE_CODE_OAUTH_TOKEN",
            "CLAUDE_CODE_REMOTE",
            "ANTHROPIC_UNIX_SOCKET",
            "OTEL_LOGS_EXPORTER",
            "OTEL_METRICS_EXPORTER",
            "OTEL_TRACES_EXPORTER",
            "OTEL_EXPORTER_OTLP_PROTOCOL",
            "OTEL_SDK_DISABLED",
          ],
        },
      }),
    );

    const input = supervisorSpawnMock.mock.calls[0]?.[0] as {
      env?: Record<string, string | undefined>;
    };
    expect(input.env?.SAFE_KEEP).toBe("ok");
    expect(input.env?.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST).toBeUndefined();
    expect(input.env?.ANTHROPIC_BASE_URL).toBe("https://override.example.com/v1");
    expect(input.env?.ANTHROPIC_API_TOKEN).toBeUndefined();
    expect(input.env?.ANTHROPIC_CUSTOM_HEADERS).toBeUndefined();
    expect(input.env?.ANTHROPIC_OAUTH_TOKEN).toBeUndefined();
    expect(input.env?.CLAUDE_CODE_USE_BEDROCK).toBeUndefined();
    expect(input.env?.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
    expect(input.env?.CLAUDE_CODE_OAUTH_TOKEN).toBe("override-oauth-token");
    expect(input.env?.CLAUDE_CODE_REMOTE).toBeUndefined();
    expect(input.env?.ANTHROPIC_UNIX_SOCKET).toBeUndefined();
    expect(input.env?.OTEL_LOGS_EXPORTER).toBeUndefined();
    expect(input.env?.OTEL_METRICS_EXPORTER).toBeUndefined();
    expect(input.env?.OTEL_TRACES_EXPORTER).toBeUndefined();
    expect(input.env?.OTEL_EXPORTER_OTLP_PROTOCOL).toBeUndefined();
    expect(input.env?.OTEL_SDK_DISABLED).toBeUndefined();
  });

  it("formats CLI auth env diagnostics as key names without secret values", () => {
    vi.stubEnv("ANTHROPIC_API_KEY""sk-ant-host");
    vi.stubEnv("ANTHROPIC_API_TOKEN""token-host");
    vi.stubEnv("OPENAI_API_KEY""sk-openai-host");

    const log = buildCliEnvAuthLog({
      ANTHROPIC_API_TOKEN: "token-child",
      CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST: "1",
      OPENAI_API_KEY: "sk-openai-child",
    });

    expect(log).toMatch(/host=.*ANTHROPIC_API_KEY/);
    expect(log).toMatch(/host=.*ANTHROPIC_API_TOKEN/);
    expect(log).toMatch(/host=.*OPENAI_API_KEY/);
    expect(log).toMatch(/child=.*ANTHROPIC_API_TOKEN/);
    expect(log).toMatch(/child=.*CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST/);
    expect(log).toMatch(/child=.*OPENAI_API_KEY/);
    expect(log).toMatch(/cleared=.*ANTHROPIC_API_KEY/);
    expect(log).not.toContain("sk-ant-host");
    expect(log).not.toContain("token-child");
    expect(log).not.toContain("sk-openai-child");
  });

  it("prepends bootstrap warnings to the CLI prompt body", async () => {
    supervisorSpawnMock.mockResolvedValueOnce(
      createManagedRun({
        reason: "exit",
        exitCode: 0,
        exitSignal: null,
        durationMs: 50,
        stdout: "ok",
        stderr: "",
        timedOut: false,
        noOutputTimedOut: false,
      }),
    );
    const context = buildPreparedCliRunContext({
      provider: "codex-cli",
      model: "gpt-5.4",
      runId: "run-warning",
    });
    context.reusableCliSession = { sessionId: "thread-123" };
    context.bootstrapPromptWarningLines = [
      "[Bootstrap truncation warning]",
      "- AGENTS.md: 200 raw -> 20 injected",
    ];

    await executePreparedCliRun(context, "thread-123");

    const input = supervisorSpawnMock.mock.calls[0]?.[0] as {
      argv?: string[];
      input?: string;
    };
    const promptCarrier = [input.input ?? "", ...(input.argv ?? [])].join("\n");

    expect(promptCarrier).toContain("[Bootstrap truncation warning]");
    expect(promptCarrier).toContain("- AGENTS.md: 200 raw -> 20 injected");
    expect(promptCarrier).toContain("hi");
  });

  it("loads workspace bootstrap files into the Claude CLI system prompt", async () => {
    const workspaceDir = await fs.mkdtemp(
      path.join(os.tmpdir(), "openclaw-cli-bootstrap-context-"),
    );

    await fs.writeFile(
      path.join(workspaceDir, "AGENTS.md"),
      [
        "# AGENTS.md",
        "",
        "Read SOUL.md and IDENTITY.md before replying.",
        "Use the injected workspace bootstrap files as standing instructions.",
      ].join("\n"),
      "utf-8",
    );
    await fs.writeFile(path.join(workspaceDir, "SOUL.md"), "SOUL-SECRET\n""utf-8");
    await fs.writeFile(path.join(workspaceDir, "IDENTITY.md"), "IDENTITY-SECRET\n""utf-8");
    await fs.writeFile(path.join(workspaceDir, "USER.md"), "USER-SECRET\n""utf-8");

    setCliRunnerPrepareTestDeps({
      makeBootstrapWarn: realMakeBootstrapWarn,
      resolveBootstrapContextForRun: realResolveBootstrapContextForRun,
    });

    try {
      const { contextFiles } = await realResolveBootstrapContextForRun({
        workspaceDir,
      });
      const allArgs = buildSystemPrompt({
        workspaceDir,
        modelDisplay: "claude-cli/sonnet",
        contextFiles,
        tools: [],
      });
      const agentsPath = path.join(workspaceDir, "AGENTS.md");
      const soulPath = path.join(workspaceDir, "SOUL.md");
      const identityPath = path.join(workspaceDir, "IDENTITY.md");
      const userPath = path.join(workspaceDir, "USER.md");
      expect(allArgs).toContain("# Project Context");
      expect(allArgs).toContain(`## ${agentsPath}`);
      expect(allArgs).toContain("Read SOUL.md and IDENTITY.md before replying.");
      expect(allArgs).toContain(`## ${soulPath}`);
      expect(allArgs).toContain("SOUL-SECRET");
      expect(allArgs).toContain(
        "If SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it.",
      );
      expect(allArgs).toContain(`## ${identityPath}`);
      expect(allArgs).toContain("IDENTITY-SECRET");
      expect(allArgs).toContain(`## ${userPath}`);
      expect(allArgs).toContain("USER-SECRET");
    } finally {
      await fs.rm(workspaceDir, { recursive: true, force: true });
      restoreCliRunnerPrepareTestDeps();
    }
  });
});

Messung V0.5 in Prozent
C=99 H=97 G=97

¤ Dauer der Verarbeitung: 0.36 Sekunden  (vorverarbeitet am  2026-05-26) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge