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


Quelle  io.write-prepare.test.ts

  Sprache: JAVA
 

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

import { describe, expect, it } from "vitest";
import {
  collectChangedPaths,
  formatConfigValidationFailure,
  restoreEnvRefsFromMap,
  resolvePersistCandidateForWrite,
  resolveWriteEnvSnapshotForPath,
  unsetPathForWrite,
} from "./io.write-prepare.js";
import type { OpenClawConfig } from "./types.js";

describe("config io write prepare", () => {
  it("persists caller changes onto resolved config without leaking runtime defaults", () => {
    const persisted = resolvePersistCandidateForWrite({
      runtimeConfig: {
        gateway: { port: 18789 },
        agents: { defaults: { cliBackend: "codex" } },
        messages: { ackReaction: "eyes" },
        sessions: { persistence: true },
      },
      sourceConfig: {
        gateway: { port: 18789 },
      },
      nextConfig: {
        gateway: {
          port: 18789,
          auth: { mode: "token" },
        },
      },
    }) as Record<string, unknown>;

    expect(persisted.gateway).toEqual({
      port: 18789,
      auth: { mode: "token" },
    });
    expect(persisted).not.toHaveProperty("agents.defaults");
    expect(persisted).not.toHaveProperty("messages.ackReaction");
    expect(persisted).not.toHaveProperty("sessions.persistence");
  });

  it("preserves authored source-only nested fields during partial writes", () => {
    const persisted = resolvePersistCandidateForWrite({
      runtimeConfig: {
        plugins: {
          entries: {},
        },
      },
      sourceConfig: {
        plugins: {
          entries: {},
          installs: {
            "openclaw-web-search": {
              source: "npm",
              spec: "@ollama/openclaw-web-search",
              installPath: "/tmp/openclaw-web-search",
              resolvedName: "@ollama/openclaw-web-search",
              resolvedVersion: "0.2.2",
            },
          },
        },
      },
      nextConfig: {
        plugins: {
          entries: {},
          installs: {
            "openclaw-web-search": {
              source: "npm",
              spec: "@ollama/openclaw-web-search@0.2.2",
              installPath: "/tmp/openclaw-web-search",
              resolvedName: "@ollama/openclaw-web-search",
              resolvedVersion: "0.2.2",
            },
          },
        },
      },
    }) as {
      plugins?: {
        installs?: Record<string, Record<string, unknown>>;
      };
    };

    expect(persisted.plugins?.installs?.["openclaw-web-search"]).toEqual({
      source: "npm",
      spec: "@ollama/openclaw-web-search@0.2.2",
      installPath: "/tmp/openclaw-web-search",
      resolvedName: "@ollama/openclaw-web-search",
      resolvedVersion: "0.2.2",
    });
  });

  it("preserves untouched include-owned subtrees during unrelated writes", () => {
    const persisted = resolvePersistCandidateForWrite({
      runtimeConfig: {
        agents: {
          defaults: { model: "openai/gpt-5.4" },
        },
        gateway: { mode: "local" },
      },
      sourceConfig: {
        agents: {
          defaults: { model: "openai/gpt-5.4" },
        },
        gateway: { mode: "local" },
      },
      rootAuthoredConfig: {
        agents: { $include: "./config/agents.json" },
        gateway: { mode: "local" },
      },
      nextConfig: {
        agents: {
          defaults: { model: "openai/gpt-5.4" },
        },
        gateway: { mode: "local", port: 18789 },
      },
    }) as Record<string, unknown>;

    expect(persisted.agents).toEqual({ $include: "./config/agents.json" });
    expect(persisted.gateway).toEqual({ mode: "local", port: 18789 });
  });

  it("rejects writes that would flatten include-owned subtrees", () => {
    expect(() =>
      resolvePersistCandidateForWrite({
        runtimeConfig: {
          agents: {
            defaults: { model: "openai/gpt-5.4" },
          },
        },
        sourceConfig: {
          agents: {
            defaults: { model: "openai/gpt-5.4" },
          },
        },
        rootAuthoredConfig: {
          agents: { $include: "./config/agents.json" },
        },
        nextConfig: {
          agents: {
            defaults: { model: "anthropic/sonnet-4.5" },
          },
        },
      }),
    ).toThrow("Config write would flatten $include-owned config at agents");
  });

  it('formats actionable guidance for dmPolicy="open" without wildcard allowFrom', () => {
    const message = formatConfigValidationFailure(
      "channels.telegram.allowFrom",
      'channels.telegram.dmPolicy = "open" requires channels.telegram.allowFrom to include "*"',
    );

    expect(message).toContain("openclaw config set channels.telegram.allowFrom '[\"*\"]'");
    expect(message).toContain('openclaw config set channels.telegram.dmPolicy "pairing"');
  });

  it("unsets explicit paths when runtime defaults would otherwise reappear", () => {
    const next = unsetPathForWrite(
      {
        gateway: { auth: { mode: "none" } },
        commands: { ownerDisplay: "hash" },
      },
      ["commands", "ownerDisplay"],
    );

    expect(next.changed).toBe(true);
    expect(next.next.commands ?? {}).not.toHaveProperty("ownerDisplay");
  });

  it("does not mutate caller config when unsetting existing config objects", () => {
    const input: OpenClawConfig = {
      gateway: { mode: "local" },
      commands: { ownerDisplay: "hash" },
    } satisfies OpenClawConfig;

    const next = unsetPathForWrite(input, ["commands", "ownerDisplay"]);

    expect(input).toEqual({
      gateway: { mode: "local" },
      commands: { ownerDisplay: "hash" },
    });
    expect(next.next.commands ?? {}).not.toHaveProperty("ownerDisplay");
  });

  it("keeps caller arrays immutable when unsetting array entries", () => {
    const input: OpenClawConfig = {
      gateway: { mode: "local" },
      tools: { alsoAllow: ["exec", "fetch", "read"] },
    } satisfies OpenClawConfig;

    const next = unsetPathForWrite(input, ["tools", "alsoAllow", "1"]);

    expect(input.tools!.alsoAllow).toEqual(["exec", "fetch", "read"]);
    expect((next.next.tools as { alsoAllow?: string[] } | undefined)?.alsoAllow).toEqual([
      "exec",
      "read",
    ]);
  });

  it("treats missing unset paths as no-op without mutating caller config", () => {
    const input: OpenClawConfig = {
      gateway: { mode: "local" },
      commands: { ownerDisplay: "hash" },
    } satisfies OpenClawConfig;

    const next = unsetPathForWrite(input, ["commands", "missingKey"]);

    expect(next.changed).toBe(false);
    expect(next.next).toBe(input);
    expect(input).toEqual({
      gateway: { mode: "local" },
      commands: { ownerDisplay: "hash" },
    });
  });

  it("ignores blocked prototype-key unset path segments", () => {
    const input: OpenClawConfig = {
      gateway: { mode: "local" },
      commands: { ownerDisplay: "hash" },
    } satisfies OpenClawConfig;

    const blocked = [
      ["commands", "__proto__"],
      ["commands", "constructor"],
      ["commands", "prototype"],
    ].map((segments) => unsetPathForWrite(input, segments));

    for (const result of blocked) {
      expect(result.changed).toBe(false);
      expect(result.next).toBe(input);
    }
    expect(input).toEqual({
      gateway: { mode: "local" },
      commands: { ownerDisplay: "hash" },
    });
  });

  it("preserves env refs on unchanged paths while keeping changed paths resolved", () => {
    const changedPaths = new Set<string>();
    collectChangedPaths(
      {
        agents: {
          defaults: {
            cliBackends: {
              codex: {
                env: { OPENAI_API_KEY: "sk-secret" },
              },
            },
          },
        },
        gateway: { port: 18789 },
      },
      {
        agents: {
          defaults: {
            cliBackends: {
              codex: {
                env: { OPENAI_API_KEY: "sk-secret" },
              },
            },
          },
        },
        gateway: {
          port: 18789,
          auth: { mode: "token" },
        },
      },
      "",
      changedPaths,
    );

    const restored = restoreEnvRefsFromMap(
      {
        agents: {
          defaults: {
            cliBackends: {
              codex: {
                env: { OPENAI_API_KEY: "sk-secret" },
              },
            },
          },
        },
        gateway: {
          port: 18789,
          auth: { mode: "token" },
        },
      },
      "",
      new Map([["agents.defaults.cliBackends.codex.env.OPENAI_API_KEY", "${OPENAI_API_KEY}"]]),
      changedPaths,
    ) as {
      agents: { defaults: { cliBackends: { codex: { env: { OPENAI_API_KEY: string } } } } };
      gateway: { port: number; auth: { mode: string } };
    };

    expect(restored.agents.defaults.cliBackends.codex.env.OPENAI_API_KEY).toBe("${OPENAI_API_KEY}");
    expect(restored.gateway).toEqual({
      port: 18789,
      auth: { mode: "token" },
    });
  });

  it("preserves env refs in arrays while keeping appended entries resolved", () => {
    const changedPaths = new Set<string>();
    collectChangedPaths(
      {
        agents: {
          defaults: {
            cliBackends: {
              codex: {
                args: ["${DISCORD_USER_ID}", "123"],
              },
            },
          },
        },
      },
      {
        agents: {
          defaults: {
            cliBackends: {
              codex: {
                args: ["${DISCORD_USER_ID}", "123", "456"],
              },
            },
          },
        },
      },
      "",
      changedPaths,
    );

    const restored = restoreEnvRefsFromMap(
      {
        agents: {
          defaults: {
            cliBackends: {
              codex: {
                args: ["999", "123", "456"],
              },
            },
          },
        },
      },
      "",
      new Map([["agents.defaults.cliBackends.codex.args[0]", "${DISCORD_USER_ID}"]]),
      changedPaths,
    ) as {
      agents: { defaults: { cliBackends: { codex: { args: string[] } } } };
    };

    expect(restored.agents.defaults.cliBackends.codex.args).toEqual([
      "${DISCORD_USER_ID}",
      "123",
      "456",
    ]);
  });

  it("keeps the read-time env snapshot when writing the same config path", () => {
    const snapshot = { OPENAI_API_KEY: "sk-secret" };
    expect(
      resolveWriteEnvSnapshotForPath({
        actualConfigPath: "/tmp/openclaw.json",
        expectedConfigPath: "/tmp/openclaw.json",
        envSnapshotForRestore: snapshot,
      }),
    ).toBe(snapshot);
  });

  it("drops the read-time env snapshot when writing a different config path", () => {
    expect(
      resolveWriteEnvSnapshotForPath({
        actualConfigPath: "/tmp/openclaw.json",
        expectedConfigPath: "/tmp/other.json",
        envSnapshotForRestore: { OPENAI_API_KEY: "sk-secret" },
      }),
    ).toBeUndefined();
  });

  it("keeps plugin AJV defaults out of the persisted candidate", () => {
    const sourceConfig = {
      gateway: { port: 18789 },
      channels: {
        bluebubbles: {
          serverUrl: "http://localhost:1234",
          password: "test-password",
        },
      },
    } satisfies OpenClawConfig;

    const runtimeConfig: OpenClawConfig = {
      gateway: { port: 18789 },
      channels: {
        bluebubbles: {
          serverUrl: "http://localhost:1234",
          password: "test-password",
          enrichGroupParticipantsFromContacts: true,
        },
      },
    } satisfies OpenClawConfig;

    const nextConfig: OpenClawConfig = structuredClone(runtimeConfig);
    nextConfig.gateway = {
      ...nextConfig.gateway,
      auth: { mode: "token" },
    };

    const persisted = resolvePersistCandidateForWrite({
      runtimeConfig,
      sourceConfig,
      nextConfig,
    }) as Record<string, unknown>;

    expect(persisted.gateway).toEqual({
      port: 18789,
      auth: { mode: "token" },
    });
    const channels = persisted.channels as Record<string, Record<string, unknown>> | undefined;
    expect(channels?.bluebubbles).toBeDefined();
    expect(channels?.bluebubbles).not.toHaveProperty("enrichGroupParticipantsFromContacts");
    expect(channels?.bluebubbles?.serverUrl).toBe("http://localhost:1234");
    expect(channels?.bluebubbles?.password).toBe("test-password");
  });

  it("does not reintroduce legacy nested dm.policy defaults in the persisted candidate", () => {
    const sourceConfig: OpenClawConfig = {
      channels: {
        discord: {
          dmPolicy: "pairing",
          dm: { enabled: true, policy: "pairing" },
        },
        slack: {
          dmPolicy: "pairing",
          dm: { enabled: true, policy: "pairing" },
        },
      },
      gateway: { port: 18789 },
    } satisfies OpenClawConfig;

    const nextConfig = structuredClone(sourceConfig);
    delete (nextConfig.channels?.discord?.dm as { enabled?: boolean; policy?: string } | undefined)
      ?.policy;
    delete (nextConfig.channels?.slack?.dm as { enabled?: boolean; policy?: string } | undefined)
      ?.policy;

    const persisted = resolvePersistCandidateForWrite({
      runtimeConfig: sourceConfig,
      sourceConfig,
      nextConfig,
    }) as {
      channels?: {
        discord?: { dm?: Record<string, unknown>; dmPolicy?: unknown };
        slack?: { dm?: Record<string, unknown>; dmPolicy?: unknown };
      };
    };

    expect(persisted.channels?.discord?.dmPolicy).toBe("pairing");
    expect(persisted.channels?.discord?.dm).toEqual({ enabled: true });
    expect(persisted.channels?.slack?.dmPolicy).toBe("pairing");
    expect(persisted.channels?.slack?.dm).toEqual({ enabled: true });
  });

  it("preserves normalized nested channel enabled keys during unrelated writes", () => {
    const sourceConfig = {
      channels: {
        slack: {
          channels: {
            ops: {
              enabled: false,
            },
          },
        },
        googlechat: {
          groups: {
            "spaces/aaa": {
              enabled: true,
            },
          },
        },
        discord: {
          guilds: {
            "100": {
              channels: {
                general: {
                  enabled: false,
                },
              },
            },
          },
        },
      },
    } satisfies OpenClawConfig;

    const nextConfig: OpenClawConfig = {
      ...structuredClone(sourceConfig),
      gateway: {
        auth: { mode: "token" },
      },
    };

    const persisted = resolvePersistCandidateForWrite({
      runtimeConfig: sourceConfig,
      sourceConfig,
      nextConfig,
    }) as {
      channels?: {
        slack?: { channels?: Record<string, Record<string, unknown>> };
        googlechat?: { groups?: Record<string, Record<string, unknown>> };
        discord?: {
          guilds?: Record<string, { channels?: Record<string, Record<string, unknown>> }>;
        };
      };
      gateway?: Record<string, unknown>;
    };

    expect(persisted.gateway).toEqual({
      auth: { mode: "token" },
    });
    expect(persisted.channels?.slack?.channels?.ops).toEqual({ enabled: false });
    expect(persisted.channels?.googlechat?.groups?.["spaces/aaa"]).toEqual({ enabled: true });
    expect(persisted.channels?.discord?.guilds?.["100"]?.channels?.general).toEqual({
      enabled: false,
    });
  });

  it("preserves root $schema during unrelated partial writes", () => {
    const sourceConfig: OpenClawConfig = {
      $schema: "https://openclaw.ai/config.json",
      gateway: { mode: "local" },
    } satisfies OpenClawConfig;

    const persisted = resolvePersistCandidateForWrite({
      runtimeConfig: sourceConfig,
      sourceConfig,
      nextConfig: {
        gateway: { mode: "local", port: 18789 },
      } satisfies OpenClawConfig,
    }) as OpenClawConfig;

    expect(persisted.$schema).toBe("https://openclaw.ai/config.json");
    expect(persisted.gateway).toEqual({ mode: "local", port: 18789 });
  });

  it("rejects writes that would flatten a root include", () => {
    const sourceConfig = {
      $schema: "https://openclaw.ai/config-from-include.json",
      gateway: { mode: "local" },
    };

    expect(() =>
      resolvePersistCandidateForWrite({
        runtimeConfig: sourceConfig,
        sourceConfig,
        rootAuthoredConfig: {
          $include: "./extra.json5",
          gateway: { mode: "local" },
        },
        nextConfig: {
          gateway: { mode: "local", port: 18789 },
        },
      }),
    ).toThrow("Config write would flatten $include-owned config at <root>");
  });

  it("does not restore root $schema when the next config explicitly clears it", () => {
    const sourceConfig = {
      $schema: "https://openclaw.ai/config.json",
      gateway: { mode: "local" },
    };

    const persisted = resolvePersistCandidateForWrite({
      runtimeConfig: sourceConfig,
      sourceConfig,
      nextConfig: {
        $schema: null,
        gateway: { mode: "local", port: 18789 },
      },
    }) as Record<string, unknown>;

    expect(persisted).not.toHaveProperty("$schema");
    expect(persisted.gateway).toEqual({ mode: "local", port: 18789 });
  });

  it("does not restore root $schema when the next config sets an invalid value", () => {
    const sourceConfig = {
      $schema: "https://openclaw.ai/config.json",
      gateway: { mode: "local" },
    };

    const persisted = resolvePersistCandidateForWrite({
      runtimeConfig: sourceConfig,
      sourceConfig,
      nextConfig: {
        $schema: 123,
        gateway: { mode: "local", port: 18789 },
      },
    }) as Record<string, unknown>;

    expect(persisted.$schema).toBe(123);
    expect(persisted.gateway).toEqual({ mode: "local", port: 18789 });
  });
});

¤ Dauer der Verarbeitung: 0.18 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