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


Quelle  gateway-child.test.ts

  Sprache: JAVA
 

Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

import { EventEmitter } from "node:events";
import { lstat, mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
  __testing,
  buildQaRuntimeEnv,
  resolveQaControlUiRoot,
  startQaGatewayChild,
} from "./gateway-child.js";

const fetchWithSsrFGuardMock = vi.hoisted(() => vi.fn());
const resolveQaNodeExecPathMock = vi.hoisted(() => vi.fn(async () => process.execPath));
const qaTempPathState = vi.hoisted(() => ({
  preferredTmpDir: process.env.TMPDIR || "/tmp",
}));

vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
  fetchWithSsrFGuard: fetchWithSsrFGuardMock,
}));

vi.mock("openclaw/plugin-sdk/temp-path", () => ({
  resolvePreferredOpenClawTmpDir: () => qaTempPathState.preferredTmpDir,
}));

vi.mock("./node-exec.js", () => ({
  resolveQaNodeExecPath: resolveQaNodeExecPathMock,
}));

const cleanups: Array<() => Promise<void>> = [];

afterEach(async () => {
  fetchWithSsrFGuardMock.mockReset();
  resolveQaNodeExecPathMock.mockReset();
  qaTempPathState.preferredTmpDir = process.env.TMPDIR || "/tmp";
  while (cleanups.length > 0) {
    await cleanups.pop()?.();
  }
});

function createParams(baseEnv?: NodeJS.ProcessEnv) {
  return {
    configPath: "/tmp/openclaw-qa/openclaw.json",
    gatewayToken: "qa-token",
    homeDir: "/tmp/openclaw-qa/home",
    stateDir: "/tmp/openclaw-qa/state",
    xdgConfigHome: "/tmp/openclaw-qa/xdg-config",
    xdgDataHome: "/tmp/openclaw-qa/xdg-data",
    xdgCacheHome: "/tmp/openclaw-qa/xdg-cache",
    bundledPluginsDir: "/tmp/openclaw-qa/bundled-plugins",
    compatibilityHostVersion: "2026.4.8",
    baseEnv,
  };
}

describe("buildQaRuntimeEnv", () => {
  it("cleans up temp QA gateway roots when node path resolution fails before startup", async () => {
    const tempParent = await mkdtemp(path.join(os.tmpdir(), "qa-gateway-node-exec-fail-"));
    cleanups.push(async () => {
      await rm(tempParent, { recursive: true, force: true });
    });
    qaTempPathState.preferredTmpDir = tempParent;
    resolveQaNodeExecPathMock.mockRejectedValueOnce(new Error("node missing"));

    await expect(
      startQaGatewayChild({
        repoRoot: process.cwd(),
        transport: {
          requiredPluginIds: [],
          createGatewayConfig: () => ({}),
        },
        transportBaseUrl: "http://127.0.0.1:43123",
      }),
    ).rejects.toThrow("node missing");

    await expect(readdir(tempParent)).resolves.toEqual([]);
  });

  it("keeps the slow-reply QA opt-out enabled under fast mode", () => {
    const env = buildQaRuntimeEnv({
      ...createParams(),
      providerMode: "mock-openai",
    });

    expect(env.OPENCLAW_TEST_FAST).toBe("1");
    expect(env.OPENCLAW_QA_ALLOW_LOCAL_IMAGE_PROVIDER).toBe("1");
    expect(env.OPENCLAW_ALLOW_SLOW_REPLY_TESTS).toBe("1");
    expect(env.OPENCLAW_BUNDLED_PLUGINS_DIR).toBe("/tmp/openclaw-qa/bundled-plugins");
    expect(env.OPENCLAW_COMPATIBILITY_HOST_VERSION).toBe("2026.4.8");
  });

  it("maps live frontier key aliases into provider env vars", () => {
    const env = buildQaRuntimeEnv({
      ...createParams({
        OPENCLAW_LIVE_OPENAI_KEY: "openai-live",
        OPENCLAW_LIVE_ANTHROPIC_KEY: "anthropic-live",
        OPENCLAW_LIVE_GEMINI_KEY: "gemini-live",
      }),
      providerMode: "live-frontier",
    });

    expect(env.OPENAI_API_KEY).toBe("openai-live");
    expect(env.ANTHROPIC_API_KEY).toBe("anthropic-live");
    expect(env.GEMINI_API_KEY).toBe("gemini-live");
  });

  it("defaults gateway-child provider mode to mock-openai when omitted", () => {
    expect(__testing.resolveQaGatewayChildProviderMode(undefined)).toBe("mock-openai");
    expect(__testing.resolveQaGatewayChildProviderMode("live-frontier")).toBe("live-frontier");
  });

  it("keeps explicit provider env vars over live aliases", () => {
    const env = buildQaRuntimeEnv({
      ...createParams({
        OPENAI_API_KEY: "openai-explicit",
        OPENCLAW_LIVE_OPENAI_KEY: "openai-live",
      }),
      providerMode: "live-frontier",
    });

    expect(env.OPENAI_API_KEY).toBe("openai-explicit");
  });

  it("preserves Codex CLI auth home for live frontier runs while sandboxing OpenClaw home", async () => {
    const hostHome = await mkdtemp(path.join(os.tmpdir(), "qa-host-home-"));
    cleanups.push(async () => {
      await rm(hostHome, { recursive: true, force: true });
    });
    const codexHome = path.join(hostHome, ".codex");
    await mkdir(codexHome);

    const env = buildQaRuntimeEnv({
      ...createParams({
        HOME: hostHome,
      }),
      providerMode: "live-frontier",
    });

    expect(env.HOME).toBe("/tmp/openclaw-qa/home");
    expect(env.OPENCLAW_HOME).toBe("/tmp/openclaw-qa/home");
    expect(env.CODEX_HOME).toBe(codexHome);
  });

  it("forwards host HOME for live Claude CLI runs while keeping OpenClaw home sandboxed", async () => {
    const hostHome = await mkdtemp(path.join(os.tmpdir(), "qa-host-home-"));
    cleanups.push(async () => {
      await rm(hostHome, { recursive: true, force: true });
    });

    const env = buildQaRuntimeEnv({
      ...createParams({
        HOME: hostHome,
      }),
      providerMode: "live-frontier",
      forwardHostHomeForClaudeCli: true,
    });

    expect(env.HOME).toBe(hostHome);
    expect(env.OPENCLAW_HOME).toBe("/tmp/openclaw-qa/home");
    expect(env.OPENCLAW_STATE_DIR).toBe("/tmp/openclaw-qa/state");
  });

  it("can forward host HOME for browser-backed QA runs while keeping OpenClaw home sandboxed", async () => {
    const hostHome = await mkdtemp(path.join(os.tmpdir(), "qa-host-home-"));
    cleanups.push(async () => {
      await rm(hostHome, { recursive: true, force: true });
    });

    const env = buildQaRuntimeEnv({
      ...createParams({
        HOME: hostHome,
      }),
      providerMode: "mock-openai",
      forwardHostHome: true,
    });

    expect(env.HOME).toBe(hostHome);
    expect(env.OPENCLAW_HOME).toBe("/tmp/openclaw-qa/home");
    expect(env.OPENCLAW_STATE_DIR).toBe("/tmp/openclaw-qa/state");
  });

  it("preserves the live Anthropic key for live Claude CLI runs without writing it into config", async () => {
    const hostHome = await mkdtemp(path.join(os.tmpdir(), "qa-host-home-"));
    cleanups.push(async () => {
      await rm(hostHome, { recursive: true, force: true });
    });

    const env = buildQaRuntimeEnv({
      ...createParams({
        HOME: hostHome,
        OPENCLAW_LIVE_ANTHROPIC_KEY: "anthropic-live",
        OPENCLAW_LIVE_CLI_BACKEND_PRESERVE_ENV: '["SAFE_KEEP"]',
      }),
      providerMode: "live-frontier",
      forwardHostHomeForClaudeCli: true,
      claudeCliAuthMode: "api-key",
    });

    expect(env.ANTHROPIC_API_KEY).toBe("anthropic-live");
    expect(env.OPENCLAW_LIVE_CLI_BACKEND_PRESERVE_ENV).toBe('["SAFE_KEEP","ANTHROPIC_API_KEY"]');
    expect(env.OPENCLAW_LIVE_CLI_BACKEND_AUTH_MODE).toBe("api-key");
  });

  it("removes preserved Anthropic keys for live Claude CLI subscription runs", async () => {
    const hostHome = await mkdtemp(path.join(os.tmpdir(), "qa-host-home-"));
    cleanups.push(async () => {
      await rm(hostHome, { recursive: true, force: true });
    });

    const env = buildQaRuntimeEnv({
      ...createParams({
        HOME: hostHome,
        ANTHROPIC_API_KEY: "anthropic-live",
        OPENCLAW_LIVE_CLI_BACKEND_PRESERVE_ENV: '["SAFE_KEEP","ANTHROPIC_API_KEY"]',
      }),
      providerMode: "live-frontier",
      forwardHostHomeForClaudeCli: true,
      claudeCliAuthMode: "subscription",
    });

    expect(env.ANTHROPIC_API_KEY).toBe("anthropic-live");
    expect(env.OPENCLAW_LIVE_CLI_BACKEND_PRESERVE_ENV).toBe('["SAFE_KEEP"]');
    expect(env.OPENCLAW_LIVE_CLI_BACKEND_AUTH_MODE).toBe("subscription");
  });

  it("does not pass QA setup-token values to the gateway child env", () => {
    const env = buildQaRuntimeEnv({
      ...createParams({
        OPENCLAW_LIVE_SETUP_TOKEN_VALUE: `sk-ant-oat01-${"a".repeat(80)}`,
        OPENCLAW_QA_LIVE_ANTHROPIC_SETUP_TOKEN: `sk-ant-oat01-${"b".repeat(80)}`,
      }),
      providerMode: "live-frontier",
    });

    expect(env.OPENCLAW_LIVE_SETUP_TOKEN_VALUE).toBeUndefined();
    expect(env.OPENCLAW_QA_LIVE_ANTHROPIC_SETUP_TOKEN).toBeUndefined();
  });

  it("does not pass Convex credential broker secrets to the gateway child env", () => {
    const env = buildQaRuntimeEnv({
      ...createParams({
        OPENCLAW_QA_CONVEX_SECRET_CI: "convex-ci-secret",
        OPENCLAW_QA_CONVEX_SECRET_MAINTAINER: "convex-maintainer-secret",
      }),
      providerMode: "live-frontier",
    });

    expect(env.OPENCLAW_QA_CONVEX_SECRET_CI).toBeUndefined();
    expect(env.OPENCLAW_QA_CONVEX_SECRET_MAINTAINER).toBeUndefined();
  });

  it("requires an Anthropic key for live Claude CLI API-key mode", async () => {
    const hostHome = await mkdtemp(path.join(os.tmpdir(), "qa-host-home-"));
    cleanups.push(async () => {
      await rm(hostHome, { recursive: true, force: true });
    });

    expect(() =>
      buildQaRuntimeEnv({
        ...createParams({
          HOME: hostHome,
        }),
        providerMode: "live-frontier",
        forwardHostHomeForClaudeCli: true,
        claudeCliAuthMode: "api-key",
      }),
    ).toThrow("Claude CLI API-key QA mode requires ANTHROPIC_API_KEY");
  });

  it("keeps explicit Codex CLI auth home for live frontier runs", () => {
    const env = buildQaRuntimeEnv({
      ...createParams({
        CODEX_HOME: "/custom/codex-home",
        HOME: "/host/home",
      }),
      providerMode: "live-frontier",
    });

    expect(env.CODEX_HOME).toBe("/custom/codex-home");
  });

  it.each(["mock-openai", "aimock"] as const)(
    "scrubs direct and live provider keys in %s mode",
    (providerMode) => {
      const env = buildQaRuntimeEnv({
        ...createParams({
          ANTHROPIC_API_KEY: "anthropic-live",
          ANTHROPIC_OAUTH_TOKEN: "anthropic-oauth",
          GEMINI_API_KEY: "gemini-live",
          GEMINI_API_KEYS: "gemini-a gemini-b",
          GOOGLE_API_KEY: "google-live",
          OPENAI_API_KEY: "openai-live",
          OPENAI_API_KEYS: "openai-a,openai-b",
          CODEX_HOME: "/host/.codex",
          OPENCLAW_LIVE_ANTHROPIC_KEY: "anthropic-live",
          OPENCLAW_LIVE_ANTHROPIC_KEYS: "anthropic-a,anthropic-b",
          OPENCLAW_LIVE_GEMINI_KEY: "gemini-live",
          OPENCLAW_LIVE_OPENAI_KEY: "openai-live",
        }),
        providerMode,
      });

      expect(env.OPENAI_API_KEY).toBeUndefined();
      expect(env.OPENAI_API_KEYS).toBeUndefined();
      expect(env.CODEX_HOME).toBeUndefined();
      expect(env.ANTHROPIC_API_KEY).toBeUndefined();
      expect(env.ANTHROPIC_OAUTH_TOKEN).toBeUndefined();
      expect(env.GEMINI_API_KEY).toBeUndefined();
      expect(env.GEMINI_API_KEYS).toBeUndefined();
      expect(env.GOOGLE_API_KEY).toBeUndefined();
      expect(env.OPENCLAW_LIVE_OPENAI_KEY).toBeUndefined();
      expect(env.OPENCLAW_LIVE_ANTHROPIC_KEY).toBeUndefined();
      expect(env.OPENCLAW_LIVE_ANTHROPIC_KEYS).toBeUndefined();
      expect(env.OPENCLAW_LIVE_GEMINI_KEY).toBeUndefined();
    },
  );

  it("treats restart socket closures as retryable gateway call errors", () => {
    expect(__testing.isRetryableGatewayCallError("gateway closed (1006 abnormal closure)")).toBe(
      true,
    );
    expect(__testing.isRetryableGatewayCallError("gateway closed (1012 service restart)")).toBe(
      true,
    );
    expect(__testing.isRetryableGatewayCallError("service restart in progress")).toBe(true);
    expect(__testing.isRetryableGatewayCallError("permission denied")).toBe(false);
  });

  it("stages a live Anthropic setup-token profile for isolated QA workers", async () => {
    const stateDir = await mkdtemp(path.join(os.tmpdir(), "qa-setup-token-state-"));
    cleanups.push(async () => {
      await rm(stateDir, { recursive: true, force: true });
    });
    const token = `sk-ant-oat01-${"c".repeat(80)}`;

    const cfg = await __testing.stageQaLiveAnthropicSetupToken({
      cfg: {},
      stateDir,
      env: {
        OPENCLAW_LIVE_SETUP_TOKEN_VALUE: token,
      },
    });

    expect(cfg.auth?.profiles?.["anthropic:qa-setup-token"]).toMatchObject({
      provider: "anthropic",
      mode: "token",
    });
    const storeRaw = await readFile(
      path.join(stateDir, "agents", "main", "agent", "auth-profiles.json"),
      "utf8",
    );
    expect(JSON.parse(storeRaw)).toMatchObject({
      profiles: {
        "anthropic:qa-setup-token": {
          type: "token",
          provider: "anthropic",
          token,
        },
      },
    });
  });

  it("stages placeholder mock auth profiles per agent dir so mock-openai runs can resolve credentials", async () => {
    const stateDir = await mkdtemp(path.join(os.tmpdir(), "qa-mock-auth-"));
    cleanups.push(async () => {
      await rm(stateDir, { recursive: true, force: true });
    });

    const cfg = await __testing.stageQaMockAuthProfiles({
      cfg: {},
      stateDir,
    });

    // Config side: both providers should have a profile entry with mode
    // "api_key" so the runtime picks up the staging without any further
    // config mutation.
    expect(cfg.auth?.profiles?.["qa-mock-openai"]).toMatchObject({
      provider: "openai",
      mode: "api_key",
      displayName: "QA mock openai credential",
    });
    expect(cfg.auth?.profiles?.["qa-mock-anthropic"]).toMatchObject({
      provider: "anthropic",
      mode: "api_key",
      displayName: "QA mock anthropic credential",
    });

    // Store side: each agent dir should have its own auth-profiles.json
    // containing the placeholder credential for each staged provider. This
    // is what the scenario runner actually reads when it resolves auth
    // before calling the mock.
    for (const agentId of ["main", "qa"]) {
      const storeRaw = await readFile(
        path.join(stateDir, "agents", agentId, "agent", "auth-profiles.json"),
        "utf8",
      );
      const parsed = JSON.parse(storeRaw) as {
        profiles: Record<string, { type: string; provider: string; key: string }>;
      };
      expect(parsed.profiles["qa-mock-openai"]).toMatchObject({
        type: "api_key",
        provider: "openai",
        key: "qa-mock-not-a-real-key",
      });
      expect(parsed.profiles["qa-mock-anthropic"]).toMatchObject({
        type: "api_key",
        provider: "anthropic",
        key: "qa-mock-not-a-real-key",
      });
    }
  });

  it("stages mock profiles only for the requested agents and providers when callers override the defaults", async () => {
    const stateDir = await mkdtemp(path.join(os.tmpdir(), "qa-mock-auth-override-"));
    cleanups.push(async () => {
      await rm(stateDir, { recursive: true, force: true });
    });

    const cfg = await __testing.stageQaMockAuthProfiles({
      cfg: {},
      stateDir,
      agentIds: ["qa"],
      providers: ["openai"],
    });

    expect(cfg.auth?.profiles?.["qa-mock-openai"]).toMatchObject({
      provider: "openai",
      mode: "api_key",
    });
    // Anthropic should NOT be staged when the caller restricts providers.
    expect(cfg.auth?.profiles?.["qa-mock-anthropic"]).toBeUndefined();

    const qaStore = JSON.parse(
      await readFile(path.join(stateDir, "agents", "qa", "agent", "auth-profiles.json"), "utf8"),
    ) as { profiles: Record<string, unknown> };
    expect(qaStore.profiles["qa-mock-openai"]).toBeDefined();
    expect(qaStore.profiles["qa-mock-anthropic"]).toBeUndefined();

    // main/agent should not exist because it wasn't in the agentIds list.
    await expect(
      readFile(path.join(stateDir, "agents", "main", "agent", "auth-profiles.json"), "utf8"),
    ).rejects.toThrow(/ENOENT/);
  });

  it("allows loopback gateway health probes through the SSRF guard", async () => {
    const release = vi.fn(async () => {});
    fetchWithSsrFGuardMock.mockResolvedValue({
      response: { ok: true },
      release,
    });

    await expect(
      __testing.fetchLocalGatewayHealth({
        baseUrl: "http://127.0.0.1:18789",
        healthPath: "/readyz",
      }),
    ).resolves.toBe(true);

    expect(fetchWithSsrFGuardMock).toHaveBeenCalledWith(
      expect.objectContaining({
        url: "http://127.0.0.1:18789/readyz",
        policy: { allowPrivateNetwork: true },
        auditContext: "qa-lab-gateway-child-health",
      }),
    );
    expect(release).toHaveBeenCalledTimes(1);
  });

  it("force-stops gateway children that ignore the graceful signal", async () => {
    const child = Object.assign(new EventEmitter(), {
      pid: 12345,
      exitCode: null as number | null,
      signalCode: null as string | null,
      kill: vi.fn((signal?: "SIGTERM" | "SIGKILL" | number) => {
        if (signal === "SIGKILL") {
          child.signalCode = "SIGKILL";
          queueMicrotask(() => child.emit("exit"));
        }
        return true;
      }),
    });
    const processKill = vi.spyOn(process, "kill").mockImplementation((_pid, signal) => {
      if (signal === "SIGKILL") {
        child.signalCode = "SIGKILL";
        queueMicrotask(() => child.emit("exit"));
      }
      return true;
    });

    await __testing.stopQaGatewayChildProcessTree(
      child as unknown as Parameters<typeof __testing.stopQaGatewayChildProcessTree>[0],
      {
        gracefulTimeoutMs: 1,
        forceTimeoutMs: 10,
      },
    );

    if (process.platform === "win32") {
      expect(child.kill).toHaveBeenCalledWith("SIGTERM");
      expect(child.kill).toHaveBeenCalledWith("SIGKILL");
    } else {
      expect(processKill).toHaveBeenCalledWith(-12345, "SIGTERM");
      expect(processKill).toHaveBeenCalledWith(-12345, "SIGKILL");
    }
    expect(child.exitCode !== null || child.signalCode !== null).toBe(true);
  });

  it("treats bind collisions as retryable gateway startup errors", () => {
    expect(
      __testing.isRetryableGatewayStartupError(
        "another gateway instance is already listening on ws://127.0.0.1:43124",
      ),
    ).toBe(true);
    expect(
      __testing.isRetryableGatewayStartupError(
        "failed to bind gateway socket on ws://127.0.0.1:43124: Error: listen EADDRINUSE",
      ),
    ).toBe(true);
    expect(__testing.isRetryableGatewayStartupError("gateway failed to become healthy")).toBe(
      false,
    );
  });

  it("treats startup token mismatches as retryable rpc startup errors", () => {
    expect(
      __testing.isRetryableRpcStartupError(
        "unauthorized: gateway token mismatch (set gateway.remote.token to match gateway.auth.token)",
      ),
    ).toBe(true);
    expect(__testing.isRetryableRpcStartupError("permission denied")).toBe(false);
  });

  it("probes gateway health with a one-shot HEAD request through the SSRF guard", async () => {
    const release = vi.fn(async () => {});
    fetchWithSsrFGuardMock.mockResolvedValue({
      response: { ok: true },
      release,
    });

    await expect(
      __testing.fetchLocalGatewayHealth({
        baseUrl: "http://127.0.0.1:43124",
        healthPath: "/readyz",
      }),
    ).resolves.toBe(true);

    expect(fetchWithSsrFGuardMock).toHaveBeenCalledWith(
      expect.objectContaining({
        url: "http://127.0.0.1:43124/readyz",
        init: expect.objectContaining({
          method: "HEAD",
          headers: {
            connection: "close",
          },
          signal: expect.any(AbortSignal),
        }),
        policy: { allowPrivateNetwork: true },
        auditContext: "qa-lab-gateway-child-health",
      }),
    );
    expect(release).toHaveBeenCalledTimes(1);
  });

  it("preserves only sanitized gateway debug artifacts", async () => {
    const tempRoot = await mkdtemp(path.join(os.tmpdir(), "qa-gateway-preserve-src-"));
    const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-gateway-preserve-repo-"));
    cleanups.push(async () => {
      await rm(tempRoot, { recursive: true, force: true });
      await rm(repoRoot, { recursive: true, force: true });
    });

    const stdoutLogPath = path.join(tempRoot, "gateway.stdout.log");
    const stderrLogPath = path.join(tempRoot, "gateway.stderr.log");
    const artifactDir = path.join(repoRoot, ".artifacts", "qa-e2e", "gateway-runtime");
    await mkdir(path.dirname(artifactDir), { recursive: true });
    await writeFile(
      stdoutLogPath,
      [
        "OPENCLAW_GATEWAY_TOKEN=qa-suite-token",
        'OPENAI_API_KEY="openai-live"',
        "OPENCLAW_QA_CONVEX_SECRET_CI=convex-ci-secret",
        "OPENCLAW_QA_CONVEX_SECRET_MAINTAINER=convex-maintainer-secret",
        "botToken=12345:AbCdEfGhIjKl",
        '"driverToken":"12345:driver-secr3t"',
        "sutToken='12345:sut-secr3t'",
        "leaseToken=lease-12345",
        "url=http://127.0.0.1:18789/#token=abc123",
      ].join("\n"),
      "utf8",
    );
    await writeFile(stderrLogPath, "Authorization: Bearer secret+/token=123456", "utf8");
    await mkdir(path.join(tempRoot, "state"), { recursive: true });
    await writeFile(path.join(tempRoot, "state", "secret.txt"), "do-not-copy", "utf8");

    await __testing.preserveQaGatewayDebugArtifacts({
      preserveToDir: artifactDir,
      stdoutLogPath,
      stderrLogPath,
      tempRoot,
      repoRoot,
    });

    expect((await readdir(artifactDir)).toSorted()).toEqual([
      "README.txt",
      "gateway.stderr.log",
      "gateway.stdout.log",
    ]);
    await expect(readFile(path.join(artifactDir, "gateway.stdout.log"), "utf8")).resolves.toBe(
      [
        "OPENCLAW_GATEWAY_TOKEN=<redacted>",
        "OPENAI_API_KEY=<redacted>",
        "OPENCLAW_QA_CONVEX_SECRET_CI=<redacted>",
        "OPENCLAW_QA_CONVEX_SECRET_MAINTAINER=<redacted>",
        "botToken=<redacted>",
        '"driverToken":"<redacted>"',
        "sutToken=<redacted>",
        "leaseToken=<redacted>",
        "url=http://127.0.0.1:18789/#token=<redacted>",
      ].join("\n"),
    );
    await expect(readFile(path.join(artifactDir, "gateway.stderr.log"), "utf8")).resolves.toBe(
      "Authorization: Bearer <redacted>",
    );
    await expect(readFile(path.join(artifactDir, "README.txt"), "utf8")).resolves.toContain(
      "was not copied because it may contain credentials or auth tokens",
    );
  });

  it("rejects preserved gateway artifacts outside the repo root", async () => {
    await expect(
      __testing.assertQaArtifactDirWithinRepo("/tmp/openclaw-repo", "/tmp/outside"),
    ).rejects.toThrow("QA gateway artifact directory must stay within the repo root.");
  });

  it("rejects preserved gateway artifacts that traverse symlinks", async () => {
    const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-gateway-guard-repo-"));
    const outsideRoot = await mkdtemp(path.join(os.tmpdir(), "qa-gateway-guard-outside-"));
    cleanups.push(async () => {
      await rm(repoRoot, { recursive: true, force: true });
      await rm(outsideRoot, { recursive: true, force: true });
    });
    await mkdir(path.join(repoRoot, ".artifacts"), { recursive: true });
    await symlink(outsideRoot, path.join(repoRoot, ".artifacts", "qa-e2e"), "dir");

    await expect(
      __testing.assertQaArtifactDirWithinRepo(
        repoRoot,
        path.join(repoRoot, ".artifacts", "qa-e2e", "gateway-runtime"),
      ),
    ).rejects.toThrow("QA gateway artifact directory must not traverse symlinks.");
  });

  it("cleans startup temp roots when they are not preserved", async () => {
    const tempRoot = await mkdtemp(path.join(os.tmpdir(), "qa-gateway-cleanup-src-"));
    const stagedRoot = await mkdtemp(path.join(os.tmpdir(), "qa-gateway-cleanup-stage-"));
    cleanups.push(async () => {
      await rm(tempRoot, { recursive: true, force: true });
      await rm(stagedRoot, { recursive: true, force: true });
    });

    await writeFile(path.join(tempRoot, "openclaw.json"), "{}", "utf8");
    await writeFile(path.join(stagedRoot, "marker.txt"), "x", "utf8");

    await __testing.cleanupQaGatewayTempRoots({
      tempRoot,
      stagedBundledPluginsRoot: stagedRoot,
    });

    await expect(lstat(tempRoot)).rejects.toMatchObject({ code: "ENOENT" });
    await expect(lstat(stagedRoot)).rejects.toMatchObject({ code: "ENOENT" });
  });
});

describe("resolveQaControlUiRoot", () => {
  it("returns the built control ui root when repo assets exist", async () => {
    const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-control-ui-root-"));
    cleanups.push(async () => {
      await rm(repoRoot, { recursive: true, force: true });
    });
    const controlUiRoot = path.join(repoRoot, "dist", "control-ui");
    await mkdir(controlUiRoot, { recursive: true });
    await writeFile(path.join(controlUiRoot, "index.html"), "<html></html>", "utf8");

    expect(resolveQaControlUiRoot({ repoRoot })).toBe(controlUiRoot);
  });

  it("returns undefined when control ui is disabled or not built", async () => {
    const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-control-ui-root-missing-"));
    cleanups.push(async () => {
      await rm(repoRoot, { recursive: true, force: true });
    });

    expect(resolveQaControlUiRoot({ repoRoot })).toBeUndefined();
    expect(resolveQaControlUiRoot({ repoRoot, controlUiEnabled: false })).toBeUndefined();
  });
});

describe("qa bundled plugin dir", () => {
  it("prefers a built bundled plugin when present", async () => {
    const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-root-"));
    cleanups.push(async () => {
      await rm(repoRoot, { recursive: true, force: true });
    });
    await mkdir(path.join(repoRoot, "dist", "extensions", "qa-channel"), {
      recursive: true,
    });
    await writeFile(
      path.join(repoRoot, "dist", "extensions", "qa-channel", "package.json"),
      "{}",
      "utf8",
    );
    await mkdir(path.join(repoRoot, "dist-runtime", "extensions", "qa-channel"), {
      recursive: true,
    });
    await writeFile(
      path.join(repoRoot, "dist-runtime", "extensions", "qa-channel", "package.json"),
      "{}",
      "utf8",
    );
    await mkdir(path.join(repoRoot, "extensions", "qa-channel"), { recursive: true });
    await writeFile(path.join(repoRoot, "extensions", "qa-channel", "package.json"), "{}", "utf8");

    expect(
      __testing.resolveQaBundledPluginSourceDir({
        repoRoot,
        pluginId: "qa-channel",
      }),
    ).toBe(path.join(repoRoot, "dist", "extensions", "qa-channel"));
  });

  it("falls back to the source bundled plugin when no built copy exists", async () => {
    const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-source-root-"));
    cleanups.push(async () => {
      await rm(repoRoot, { recursive: true, force: true });
    });
    await mkdir(path.join(repoRoot, "extensions", "qa-channel"), { recursive: true });
    await writeFile(path.join(repoRoot, "extensions", "qa-channel", "package.json"), "{}", "utf8");

    expect(
      __testing.resolveQaBundledPluginSourceDir({
        repoRoot,
        pluginId: "qa-channel",
      }),
    ).toBe(path.join(repoRoot, "extensions", "qa-channel"));
  });

  it("uses a source bundled plugin when the built copy is missing CLI metadata", async () => {
    const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-cli-metadata-root-"));
    cleanups.push(async () => {
      await rm(repoRoot, { recursive: true, force: true });
    });
    await mkdir(path.join(repoRoot, "dist", "extensions", "memory-core"), { recursive: true });
    await writeFile(
      path.join(repoRoot, "dist", "extensions", "memory-core", "package.json"),
      "{}",
      "utf8",
    );
    await writeFile(
      path.join(repoRoot, "dist", "extensions", "memory-core", "openclaw.plugin.json"),
      JSON.stringify({ id: "memory-core", kind: "memory" }),
      "utf8",
    );
    await mkdir(path.join(repoRoot, "extensions", "memory-core"), { recursive: true });
    await writeFile(path.join(repoRoot, "extensions", "memory-core", "package.json"), "{}", "utf8");
    await writeFile(
      path.join(repoRoot, "extensions", "memory-core", "openclaw.plugin.json"),
      JSON.stringify({ id: "memory-core", kind: "memory" }),
      "utf8",
    );
    await writeFile(
      path.join(repoRoot, "extensions", "memory-core", "cli-metadata.ts"),
      "export default { id: 'memory-core' };\n",
      "utf8",
    );

    expect(
      __testing.resolveQaBundledPluginSourceDir({
        repoRoot,
        pluginId: "memory-core",
      }),
    ).toBe(path.join(repoRoot, "extensions", "memory-core"));
  });

  it("creates a scoped bundled plugin tree for allowed plugins plus always-allowed runtime facades", async () => {
    const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-scope-"));
    cleanups.push(async () => {
      await rm(repoRoot, { recursive: true, force: true });
    });
    await writeFile(
      path.join(repoRoot, "package.json"),
      JSON.stringify(
        {
          name: "openclaw",
          type: "module",
          exports: {
            "./plugin-sdk/account-id": {
              default: "./dist/plugin-sdk/account-id.js",
            },
          },
        },
        null,
        2,
      ),
      "utf8",
    );
    await mkdir(path.join(repoRoot, "dist", "extensions", "qa-channel"), { recursive: true });
    await mkdir(path.join(repoRoot, "dist", "extensions", "memory-core"), { recursive: true });
    await mkdir(path.join(repoRoot, "dist", "extensions", "speech-core"), { recursive: true });
    await mkdir(path.join(repoRoot, "dist", "extensions", "unused-plugin"), { recursive: true });
    await mkdir(path.join(repoRoot, "dist", "plugin-sdk"), { recursive: true });
    await writeFile(
      path.join(repoRoot, "dist", "plugin-sdk", "account-id.js"),
      "export const normalizeAccountId = (value) => value.toLowerCase();\n",
      "utf8",
    );
    await writeFile(
      path.join(repoRoot, "dist", "extensions", "qa-channel", "package.json"),
      JSON.stringify({ name: "@openclaw/qa-channel", type: "module" }, null, 2),
      "utf8",
    );
    await writeFile(
      path.join(repoRoot, "dist", "extensions", "qa-channel", "index.js"),
      [
        'import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";',
        'export const accountId = normalizeAccountId("QA");',
        "",
      ].join("\n"),
      "utf8",
    );
    await writeFile(path.join(repoRoot, "dist", "shared-chunk-abc123.js"), "export {};\n", "utf8");
    const tempRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-target-"));
    cleanups.push(async () => {
      await rm(tempRoot, { recursive: true, force: true });
    });

    const { bundledPluginsDir, stagedRoot } = await __testing.createQaBundledPluginsDir({
      repoRoot,
      tempRoot,
      allowedPluginIds: ["qa-channel", "memory-core"],
    });

    expect((await readdir(bundledPluginsDir)).toSorted()).toEqual([
      "memory-core",
      "qa-channel",
      "speech-core",
    ]);
    expect(bundledPluginsDir).toBe(
      path.join(
        repoRoot,
        ".artifacts",
        "qa-runtime",
        path.basename(tempRoot),
        "dist",
        "extensions",
      ),
    );
    expect(stagedRoot).toBe(
      path.join(repoRoot, ".artifacts", "qa-runtime", path.basename(tempRoot)),
    );
    expect(stagedRoot).not.toBeNull();
    if (!stagedRoot) {
      throw new Error("expected staged runtime root");
    }
    await expect(readFile(path.join(stagedRoot, "package.json"), "utf8")).resolves.toContain(
      '"name": "openclaw"',
    );
    await expect(
      import(
        `${pathToFileURL(path.join(bundledPluginsDir, "qa-channel", "index.js")).href}?t=${Date.now()}`
      ),
    ).resolves.toMatchObject({
      accountId: "qa",
    });
    expect((await lstat(path.join(bundledPluginsDir, "qa-channel"))).isDirectory()).toBe(true);
    expect((await lstat(path.join(bundledPluginsDir, "memory-core"))).isDirectory()).toBe(true);
    expect((await lstat(path.join(bundledPluginsDir, "speech-core"))).isDirectory()).toBe(true);
    await expect(
      lstat(
        path.join(
          repoRoot,
          ".artifacts",
          "qa-runtime",
          path.basename(tempRoot),
          "dist",
          "shared-chunk-abc123.js",
        ),
      ),
    ).resolves.toBeTruthy();
  });

  it("skips transient runtime dependency artifacts while staging built bundled plugins", async () => {
    const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-runtime-deps-"));
    cleanups.push(async () => {
      await rm(repoRoot, { recursive: true, force: true });
    });
    await writeFile(
      path.join(repoRoot, "package.json"),
      JSON.stringify({ name: "openclaw", type: "module" }, null, 2),
      "utf8",
    );
    const pluginDir = path.join(repoRoot, "dist", "extensions", "qa-channel");
    await mkdir(path.join(pluginDir, ".openclaw-runtime-deps-copy-active", "node_modules"), {
      recursive: true,
    });
    await writeFile(
      path.join(pluginDir, "package.json"),
      JSON.stringify({ name: "@openclaw/qa-channel", type: "module" }, null, 2),
      "utf8",
    );
    await writeFile(path.join(pluginDir, "index.js"), "export const ok = true;\n", "utf8");
    await writeFile(path.join(pluginDir, ".openclaw-runtime-deps.json"), "{}\n", "utf8");
    await writeFile(path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"), "{}\n", "utf8");
    await writeFile(
      path.join(pluginDir, ".openclaw-runtime-deps-copy-active", "node_modules", "transient.js"),
      "export {};\n",
      "utf8",
    );
    const tempRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-runtime-deps-target-"));
    cleanups.push(async () => {
      await rm(tempRoot, { recursive: true, force: true });
    });

    const { bundledPluginsDir } = await __testing.createQaBundledPluginsDir({
      repoRoot,
      tempRoot,
      allowedPluginIds: ["qa-channel"],
    });

    const stagedPluginDir = path.join(bundledPluginsDir, "qa-channel");
    await expect(readFile(path.join(stagedPluginDir, "index.js"), "utf8")).resolves.toContain("ok");
    await expect(lstat(path.join(stagedPluginDir, ".openclaw-runtime-deps.json"))).rejects.toThrow(
      /ENOENT/u,
    );
    await expect(
      lstat(path.join(stagedPluginDir, ".openclaw-runtime-deps-stamp.json")),
    ).rejects.toThrow(/ENOENT/u);
    await expect(
      lstat(path.join(stagedPluginDir, ".openclaw-runtime-deps-copy-active")),
    ).rejects.toThrow(/ENOENT/u);
  });

  it("preserves dist-runtime-only root chunks when dist also exists", async () => {
    const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-mixed-runtime-"));
    cleanups.push(async () => {
      await rm(repoRoot, { recursive: true, force: true });
    });
    await writeFile(
      path.join(repoRoot, "package.json"),
      JSON.stringify({ name: "openclaw", type: "module" }, null, 2),
      "utf8",
    );
    await mkdir(path.join(repoRoot, "dist"), { recursive: true });
    await writeFile(
      path.join(repoRoot, "dist", "shared-dist.js"),
      'export const dist = "dist";\n',
      "utf8",
    );
    await mkdir(path.join(repoRoot, "dist-runtime", "extensions", "runtime-only"), {
      recursive: true,
    });
    await writeFile(
      path.join(repoRoot, "dist-runtime", "runtime-chunk.js"),
      'export const marker = "runtime";\n',
      "utf8",
    );
    await writeFile(
      path.join(repoRoot, "dist-runtime", "extensions", "runtime-only", "package.json"),
      JSON.stringify({ name: "@openclaw/runtime-only", type: "module" }, null, 2),
      "utf8",
    );
    await writeFile(
      path.join(repoRoot, "dist-runtime", "extensions", "runtime-only", "index.js"),
      ['import { marker } from "../../runtime-chunk.js";', "export { marker };", ""].join("\n"),
      "utf8",
    );
    const tempRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-mixed-target-"));
    cleanups.push(async () => {
      await rm(tempRoot, { recursive: true, force: true });
    });

    const { bundledPluginsDir } = await __testing.createQaBundledPluginsDir({
      repoRoot,
      tempRoot,
      allowedPluginIds: ["runtime-only"],
    });

    expect(bundledPluginsDir).toBe(
      path.join(
        repoRoot,
        ".artifacts",
        "qa-runtime",
        path.basename(tempRoot),
        "dist",
        "extensions",
      ),
    );
    await expect(
      import(
        `${pathToFileURL(path.join(bundledPluginsDir, "runtime-only", "index.js")).href}?t=${Date.now()}`
      ),
    ).resolves.toMatchObject({
      marker: "runtime",
    });
    await expect(
      lstat(
        path.join(
          repoRoot,
          ".artifacts",
          "qa-runtime",
          path.basename(tempRoot),
          "dist",
          "runtime-chunk.js",
        ),
      ),
    ).resolves.toBeTruthy();
  });

  it("rejects invalid bundled plugin ids before staging paths are built", async () => {
    const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-invalid-id-"));
    cleanups.push(async () => {
      await rm(repoRoot, { recursive: true, force: true });
    });
    await writeFile(
      path.join(repoRoot, "package.json"),
      JSON.stringify({ name: "openclaw", type: "module" }, null, 2),
      "utf8",
    );
    const tempRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-invalid-target-"));
    cleanups.push(async () => {
      await rm(tempRoot, { recursive: true, force: true });
    });

    await expect(
      __testing.createQaBundledPluginsDir({
        repoRoot,
        tempRoot,
        allowedPluginIds: ["../escape"],
      }),
    ).rejects.toThrow("invalid QA bundled plugin id: ../escape");
  });

  it("stages source-only bundled plugins into a repo-like runtime root with node_modules", async () => {
    const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-source-stage-"));
    cleanups.push(async () => {
      await rm(repoRoot, { recursive: true, force: true });
    });
    const fakeDepStoreRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-source-store-"));
    cleanups.push(async () => {
      await rm(fakeDepStoreRoot, { recursive: true, force: true });
    });
    await writeFile(
      path.join(repoRoot, "package.json"),
      JSON.stringify(
        {
          name: "openclaw",
          type: "module",
          exports: {
            "./plugin-sdk/account-id": {
              default: "./dist/plugin-sdk/account-id.js",
            },
          },
        },
        null,
        2,
      ),
      "utf8",
    );
    await mkdir(path.join(repoRoot, "dist", "plugin-sdk"), { recursive: true });
    await writeFile(
      path.join(repoRoot, "dist", "plugin-sdk", "account-id.js"),
      "export const normalizeAccountId = (value) => value.toLowerCase();\n",
      "utf8",
    );
    await mkdir(path.join(repoRoot, "extensions", "qa-channel"), { recursive: true });
    await writeFile(
      path.join(repoRoot, "extensions", "qa-channel", "package.json"),
      JSON.stringify({ name: "@openclaw/qa-channel", type: "module" }, null, 2),
      "utf8",
    );
    await writeFile(
      path.join(repoRoot, "extensions", "qa-channel", "index.ts"),
      [
        'import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";',
        'import { marker } from "fake-dep";',
        'export const accountId = `${normalizeAccountId("QA")}:${marker}`;',
        "",
      ].join("\n"),
      "utf8",
    );
    const fakeDepPackageDir = path.join(fakeDepStoreRoot, "fake-dep");
    await mkdir(fakeDepPackageDir, { recursive: true });
    await writeFile(
      path.join(fakeDepPackageDir, "package.json"),
      JSON.stringify({ name: "fake-dep", type: "module" }, null, 2),
      "utf8",
    );
    await writeFile(
      path.join(fakeDepPackageDir, "index.js"),
      'export const marker = "ok";\n',
      "utf8",
    );
    await mkdir(path.join(repoRoot, "node_modules"), { recursive: true });
    await symlink(fakeDepPackageDir, path.join(repoRoot, "node_modules", "fake-dep"), "dir");
    const tempRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-source-target-"));
    cleanups.push(async () => {
      await rm(tempRoot, { recursive: true, force: true });
    });

    const { bundledPluginsDir, stagedRoot } = await __testing.createQaBundledPluginsDir({
      repoRoot,
      tempRoot,
      allowedPluginIds: ["qa-channel"],
    });

    expect(bundledPluginsDir).toBe(
      path.join(
        repoRoot,
        ".artifacts",
        "qa-runtime",
        path.basename(tempRoot),
        "dist",
        "extensions",
      ),
    );
    if (!stagedRoot) {
      throw new Error("expected staged runtime root");
    }
    await expect(
      import(
        `${pathToFileURL(path.join(bundledPluginsDir, "qa-channel", "index.ts")).href}?t=${Date.now()}`
      ),
    ).resolves.toMatchObject({
      accountId: "qa:ok",
    });
    await expect(
      lstat(path.join(stagedRoot, "node_modules", "fake-dep")).then((stats) =>
        stats.isSymbolicLink(),
      ),
    ).resolves.toBe(true);
    await expect(
      readFile(path.join(stagedRoot, "node_modules", "fake-dep", "index.js"), "utf8"),
    ).resolves.toContain('marker = "ok"');
  });

  it("maps cli backend provider ids to their owning bundled plugin ids", async () => {
    const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-plugin-owner-"));
    cleanups.push(async () => {
      await rm(repoRoot, { recursive: true, force: true });
    });
    await mkdir(path.join(repoRoot, "dist", "extensions", "openai"), { recursive: true });
    await writeFile(
      path.join(repoRoot, "dist", "extensions", "openai", "openclaw.plugin.json"),
      JSON.stringify({
        id: "openai",
        providers: ["openai", "openai-codex"],
        cliBackends: ["codex-cli"],
      }),
      "utf8",
    );

    await expect(
      __testing.resolveQaOwnerPluginIdsForProviderIds({
        repoRoot,
        providerIds: ["codex-cli"],
      }),
    ).resolves.toEqual(["openai"]);
  });

  it("maps configured OpenAI Responses provider aliases to the OpenAI plugin", async () => {
    const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-plugin-owner-"));
    cleanups.push(async () => {
      await rm(repoRoot, { recursive: true, force: true });
    });
    await mkdir(path.join(repoRoot, "dist", "extensions", "openai"), { recursive: true });
    await writeFile(
      path.join(repoRoot, "dist", "extensions", "openai", "openclaw.plugin.json"),
      JSON.stringify({
        id: "openai",
        providers: ["openai"],
        cliBackends: ["codex-cli"],
      }),
      "utf8",
    );

    await expect(
      __testing.resolveQaOwnerPluginIdsForProviderIds({
        repoRoot,
        providerIds: ["custom-openai"],
        providerConfigs: {
          "custom-openai": {
            baseUrl: "https://api.example.test/v1",
            api: "openai-responses",
            models: [
              {
                id: "model-a",
                name: "model-a",
                api: "openai-responses",
                reasoning: true,
                input: ["text"],
                cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
                contextWindow: 128_000,
                maxTokens: 4096,
              },
            ],
          },
        },
      }),
    ).resolves.toEqual(["openai"]);
  });

  it("copies selected live provider configs from the host config", async () => {
    const configPath = path.join(
      await mkdtemp(path.join(os.tmpdir(), "qa-provider-config-")),
      "openclaw.json",
    );
    cleanups.push(async () => {
      await rm(path.dirname(configPath), { recursive: true, force: true });
    });
    await writeFile(
      configPath,
      JSON.stringify({
        models: {
          providers: {
            "custom-openai": {
              baseUrl: "https://api.example.test/v1",
              api: "openai-responses",
              models: [
                {
                  id: "model-a",
                  name: "model-a",
                  api: "openai-responses",
                  reasoning: true,
                  input: ["text"],
                  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
                  contextWindow: 128_000,
                  maxTokens: 4096,
                },
              ],
            },
            ignored: {
              baseUrl: "https://ignored.example.test/v1",
              api: "openai-responses",
              models: [],
            },
          },
        },
      }),
      "utf8",
    );

    await expect(
      __testing.readQaLiveProviderConfigOverrides({
        providerIds: ["custom-openai"],
        env: { OPENCLAW_QA_LIVE_PROVIDER_CONFIG_PATH: configPath },
      }),
    ).resolves.toEqual({
      "custom-openai": expect.objectContaining({
        baseUrl: "https://api.example.test/v1",
        api: "openai-responses",
      }),
    });
  });

  it("raises the QA runtime host version to the highest allowed plugin floor", async () => {
    const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-runtime-version-"));
    cleanups.push(async () => {
      await rm(repoRoot, { recursive: true, force: true });
    });
    await writeFile(
      path.join(repoRoot, "package.json"),
      JSON.stringify({ version: "2026.4.7-1" }),
      "utf8",
    );
    const bundledRoot = path.join(repoRoot, "extensions");
    await mkdir(path.join(bundledRoot, "qa-channel"), { recursive: true });
    await writeFile(
      path.join(bundledRoot, "qa-channel", "package.json"),
      JSON.stringify({ openclaw: { install: { minHostVersion: ">=2026.4.8" } } }),
      "utf8",
    );

    await mkdir(path.join(bundledRoot, "memory-core"), { recursive: true });
    await writeFile(
      path.join(bundledRoot, "memory-core", "package.json"),
      JSON.stringify({ openclaw: { install: { minHostVersion: ">=2026.4.7" } } }),
      "utf8",
    );

    await expect(
      __testing.resolveQaRuntimeHostVersion({
        repoRoot,
        allowedPluginIds: ["memory-core", "qa-channel"],
      }),
    ).resolves.toBe("2026.4.8");
  });

  it("includes always-allowed runtime facade plugins when raising the QA runtime host version", async () => {
    const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-runtime-version-runtime-facade-"));
    cleanups.push(async () => {
      await rm(repoRoot, { recursive: true, force: true });
    });
    await writeFile(
      path.join(repoRoot, "package.json"),
      JSON.stringify({ version: "2026.4.7-1" }),
      "utf8",
    );
    const bundledRoot = path.join(repoRoot, "extensions");
    await mkdir(path.join(bundledRoot, "qa-channel"), { recursive: true });
    await writeFile(
      path.join(bundledRoot, "qa-channel", "package.json"),
      JSON.stringify({ openclaw: { install: { minHostVersion: ">=2026.4.8" } } }),
      "utf8",
    );
    await mkdir(path.join(bundledRoot, "speech-core"), { recursive: true });
    await writeFile(
      path.join(bundledRoot, "speech-core", "package.json"),
      JSON.stringify({ openclaw: { install: { minHostVersion: ">=2026.4.9" } } }),
      "utf8",
    );

    await expect(
      __testing.resolveQaRuntimeHostVersion({
        repoRoot,
        allowedPluginIds: ["qa-channel"],
      }),
    ).resolves.toBe("2026.4.9");
  });
});

¤ Dauer der Verarbeitung: 0.28 Sekunden  (vorverarbeitet am  2026-04-27) ¤

*© 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