mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 13:51:33 +00:00
* refactor(config): consolidate media model lists * refactor(config): unify memory configuration * refactor(config): consolidate TTS ownership * refactor(config): move typing policy to agents * refactor(config): retire product-level config surfaces * refactor(config): share scoped tool policy type * chore(config): refresh generated baselines * fix(config): honor agent typing overrides * fix(config): migrate sibling config consumers * refactor(infra): keep base64url decoder private * fix(config): strip invalid legacy TTS values * chore(config): refresh rebased baseline hash * fix(doctor): route legacy messages.tts.realtime voice to talk during tts move * refactor(config): polish final layout names * refactor(config): freeze retired tuning defaults * feat(config): add fast mode default symmetry * refactor(config): key agent entries by id * docs(config): update final layout reference * test(config): cover final layout migrations * chore(config): refresh final layout baselines * fix(config): align final layout runtime readers * fix(config): align remaining readers * fix(config): stabilize final layout migrations * fix(config): finalize config projection proof * fix(config): address final layout review * docs(release): preserve historical config names * fix(config): complete keyed agent migration * fix(config): close final migration gaps * fix(config): finish full-branch review * fix(config): complete runtime secret detection * fix(config): close final review findings * fix(config): finish canonical docs and heartbeat migration * fix(config): integrate latest main after rebase * refactor(env): isolate test-only controls * refactor(env): isolate build and development controls * refactor(env): collapse process identity indirection * refactor(env): remove duplicate config and temp aliases * docs(env): define the operator-facing allowlist * ci(env): ratchet production variable count * fix(env): remove stale provider helper import * fix(env): make ratchet sorting explicit * test(env): keep test seam in dead-code audit * test(env): cover ratchet growth and boundary; document surface budgets * docs(config): document tier-eval consolidations * docs(config): clarify speech preference ownership * test(memory): align retired tuning fixtures * refactor(memory): freeze engine heuristics * refactor(config): apply tier-eval tranche * refactor(tts): move persona shaping to providers * refactor(compaction): move prompt policy to providers * test(config): align hookified prompt fixtures * chore(deadcode): classify test-only exports * chore(github): remove unused spawn helper * chore(deadcode): classify queue diagnostics * chore(deadcode): remove unused lane snapshot export * chore(plugin-sdk): ratchet consolidated surface * fix(config): integrate latest main after rebase
390 lines
12 KiB
TypeScript
390 lines
12 KiB
TypeScript
// Config CLI integration tests cover end-to-end config command reads and writes.
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import JSON5 from "json5";
|
|
import { beforeAll, describe, expect, it } from "vitest";
|
|
import { clearConfigCache, clearRuntimeConfigSnapshot } from "../config/config.js";
|
|
import { captureEnv, deleteTestEnvValue, setTestEnvValue } from "../test-utils/env.js";
|
|
import { runConfigSet } from "./config-cli.js";
|
|
|
|
function createTestRuntime() {
|
|
const logs: string[] = [];
|
|
const errors: string[] = [];
|
|
return {
|
|
logs,
|
|
errors,
|
|
runtime: {
|
|
log: (...args: unknown[]) => logs.push(args.map((arg) => String(arg)).join(" ")),
|
|
error: (...args: unknown[]) => errors.push(args.map((arg) => String(arg)).join(" ")),
|
|
exit: (code: number) => {
|
|
throw new Error(`__exit__:${code}`);
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function createExecDryRunBatch(params: { markerPath: string }) {
|
|
const response = JSON.stringify({
|
|
protocolVersion: 1,
|
|
values: {
|
|
dryrun_id: "ok",
|
|
},
|
|
});
|
|
const script = [
|
|
`#!${process.execPath}`,
|
|
'const fs = require("node:fs");',
|
|
`fs.writeFileSync(${JSON.stringify(params.markerPath)}, "dryrun\\n", "utf8");`,
|
|
`process.stdout.write(${JSON.stringify(response)});`,
|
|
].join("\n");
|
|
const scriptPath = path.join(path.dirname(params.markerPath), "exec-provider.cjs");
|
|
fs.writeFileSync(scriptPath, script, { mode: 0o700 });
|
|
return [
|
|
{
|
|
path: "secrets.providers.runner",
|
|
provider: {
|
|
source: "exec",
|
|
command: scriptPath,
|
|
trustedDirs: [path.dirname(scriptPath)],
|
|
timeoutMs: 60_000,
|
|
noOutputTimeoutMs: 60_000,
|
|
},
|
|
},
|
|
{
|
|
path: "channels.discord.token",
|
|
ref: {
|
|
source: "exec",
|
|
provider: "runner",
|
|
id: "dryrun_id",
|
|
},
|
|
},
|
|
];
|
|
}
|
|
|
|
async function withExecDryRunConfigHarness(
|
|
prefix: string,
|
|
run: (params: {
|
|
batchPath: string;
|
|
configPath: string;
|
|
markerPath: string;
|
|
runtime: ReturnType<typeof createTestRuntime>;
|
|
}) => Promise<void>,
|
|
) {
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
const configPath = path.join(tempDir, "openclaw.json");
|
|
const batchPath = path.join(tempDir, "batch.json");
|
|
const markerPath = path.join(tempDir, "marker.txt");
|
|
const envSnapshot = captureEnv(["OPENCLAW_CONFIG_PATH", "OPENCLAW_TEST_FAST"]);
|
|
try {
|
|
fs.writeFileSync(
|
|
configPath,
|
|
`${JSON.stringify(
|
|
{
|
|
gateway: { port: 18789 },
|
|
},
|
|
null,
|
|
2,
|
|
)}\n`,
|
|
"utf8",
|
|
);
|
|
fs.writeFileSync(
|
|
batchPath,
|
|
`${JSON.stringify(createExecDryRunBatch({ markerPath }), null, 2)}\n`,
|
|
"utf8",
|
|
);
|
|
|
|
setTestEnvValue("OPENCLAW_TEST_FAST", "1");
|
|
setTestEnvValue("OPENCLAW_CONFIG_PATH", configPath);
|
|
clearConfigCache();
|
|
clearRuntimeConfigSnapshot();
|
|
|
|
await run({
|
|
batchPath,
|
|
configPath,
|
|
markerPath,
|
|
runtime: createTestRuntime(),
|
|
});
|
|
} finally {
|
|
envSnapshot.restore();
|
|
clearConfigCache();
|
|
clearRuntimeConfigSnapshot();
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
describe("config cli integration", () => {
|
|
beforeAll(async () => {
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-config-cli-warmup-"));
|
|
const configPath = path.join(tempDir, "openclaw.json");
|
|
const envSnapshot = captureEnv(["OPENCLAW_CONFIG_PATH", "OPENCLAW_TEST_FAST"]);
|
|
try {
|
|
fs.writeFileSync(configPath, `${JSON.stringify({ gateway: { port: 18789 } }, null, 2)}\n`);
|
|
setTestEnvValue("OPENCLAW_TEST_FAST", "1");
|
|
setTestEnvValue("OPENCLAW_CONFIG_PATH", configPath);
|
|
clearConfigCache();
|
|
clearRuntimeConfigSnapshot();
|
|
await runConfigSet({
|
|
path: "gateway.port",
|
|
value: "18790",
|
|
cliOptions: {},
|
|
runtime: createTestRuntime().runtime,
|
|
});
|
|
} finally {
|
|
envSnapshot.restore();
|
|
clearConfigCache();
|
|
clearRuntimeConfigSnapshot();
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("accepts plugin hook conversation-access policy via config set", async () => {
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-config-cli-plugin-hooks-"));
|
|
const configPath = path.join(tempDir, "openclaw.json");
|
|
const envSnapshot = captureEnv(["OPENCLAW_CONFIG_PATH", "OPENCLAW_TEST_FAST"]);
|
|
try {
|
|
fs.writeFileSync(
|
|
configPath,
|
|
`${JSON.stringify(
|
|
{
|
|
gateway: { port: 18789 },
|
|
},
|
|
null,
|
|
2,
|
|
)}\n`,
|
|
"utf8",
|
|
);
|
|
|
|
setTestEnvValue("OPENCLAW_TEST_FAST", "1");
|
|
setTestEnvValue("OPENCLAW_CONFIG_PATH", configPath);
|
|
clearConfigCache();
|
|
clearRuntimeConfigSnapshot();
|
|
|
|
const runtime = createTestRuntime();
|
|
await runConfigSet({
|
|
path: "plugins.entries.openclaw-mem0.hooks.allowConversationAccess",
|
|
value: "true",
|
|
cliOptions: {},
|
|
runtime: runtime.runtime,
|
|
});
|
|
|
|
expect(runtime.errors).toStrictEqual([]);
|
|
const afterWrite = JSON5.parse(fs.readFileSync(configPath, "utf8"));
|
|
expect(afterWrite.plugins?.entries?.["openclaw-mem0"]?.hooks).toEqual({
|
|
allowConversationAccess: true,
|
|
});
|
|
} finally {
|
|
envSnapshot.restore();
|
|
clearConfigCache();
|
|
clearRuntimeConfigSnapshot();
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("supports batch-file dry-run and then writes real config changes", async () => {
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-config-cli-int-"));
|
|
const configPath = path.join(tempDir, "openclaw.json");
|
|
const batchPath = path.join(tempDir, "batch.json");
|
|
const envSnapshot = captureEnv([
|
|
"OPENCLAW_CONFIG_PATH",
|
|
"OPENCLAW_TEST_FAST",
|
|
"DISCORD_BOT_TOKEN",
|
|
]);
|
|
try {
|
|
fs.writeFileSync(
|
|
configPath,
|
|
`${JSON.stringify(
|
|
{
|
|
gateway: { port: 18789 },
|
|
},
|
|
null,
|
|
2,
|
|
)}\n`,
|
|
"utf8",
|
|
);
|
|
fs.writeFileSync(
|
|
batchPath,
|
|
`${JSON.stringify(
|
|
[
|
|
{
|
|
path: "secrets.providers.default",
|
|
provider: { source: "env" },
|
|
},
|
|
{
|
|
path: "channels.discord.token",
|
|
ref: {
|
|
source: "env",
|
|
provider: "default",
|
|
id: "DISCORD_BOT_TOKEN",
|
|
},
|
|
},
|
|
],
|
|
null,
|
|
2,
|
|
)}\n`,
|
|
"utf8",
|
|
);
|
|
|
|
setTestEnvValue("OPENCLAW_TEST_FAST", "1");
|
|
setTestEnvValue("OPENCLAW_CONFIG_PATH", configPath);
|
|
setTestEnvValue("DISCORD_BOT_TOKEN", "test-token");
|
|
clearConfigCache();
|
|
clearRuntimeConfigSnapshot();
|
|
|
|
const runtime = createTestRuntime();
|
|
const before = fs.readFileSync(configPath, "utf8");
|
|
await runConfigSet({
|
|
cliOptions: {
|
|
batchFile: batchPath,
|
|
dryRun: true,
|
|
},
|
|
runtime: runtime.runtime,
|
|
});
|
|
const afterDryRun = fs.readFileSync(configPath, "utf8");
|
|
expect(afterDryRun).toBe(before);
|
|
expect(runtime.errors).toStrictEqual([]);
|
|
expect(runtime.logs.some((line) => line.includes("Dry run successful: 2 update(s)"))).toBe(
|
|
true,
|
|
);
|
|
|
|
await runConfigSet({
|
|
cliOptions: {
|
|
batchFile: batchPath,
|
|
},
|
|
runtime: runtime.runtime,
|
|
});
|
|
const afterWrite = JSON5.parse(fs.readFileSync(configPath, "utf8"));
|
|
expect(afterWrite.secrets?.providers?.default).toEqual({
|
|
source: "env",
|
|
});
|
|
expect(afterWrite.channels?.discord?.token).toEqual({
|
|
source: "env",
|
|
provider: "default",
|
|
id: "DISCORD_BOT_TOKEN",
|
|
});
|
|
} finally {
|
|
envSnapshot.restore();
|
|
clearConfigCache();
|
|
clearRuntimeConfigSnapshot();
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("keeps file unchanged when real-file dry-run fails and reports JSON error payload", async () => {
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-config-cli-int-fail-"));
|
|
const configPath = path.join(tempDir, "openclaw.json");
|
|
const envSnapshot = captureEnv([
|
|
"OPENCLAW_CONFIG_PATH",
|
|
"OPENCLAW_TEST_FAST",
|
|
"MISSING_TEST_SECRET",
|
|
]);
|
|
try {
|
|
fs.writeFileSync(
|
|
configPath,
|
|
`${JSON.stringify(
|
|
{
|
|
gateway: { port: 18789 },
|
|
secrets: {
|
|
providers: {
|
|
default: { source: "env" },
|
|
},
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
)}\n`,
|
|
"utf8",
|
|
);
|
|
|
|
setTestEnvValue("OPENCLAW_TEST_FAST", "1");
|
|
setTestEnvValue("OPENCLAW_CONFIG_PATH", configPath);
|
|
deleteTestEnvValue("MISSING_TEST_SECRET");
|
|
clearConfigCache();
|
|
clearRuntimeConfigSnapshot();
|
|
|
|
const runtime = createTestRuntime();
|
|
const before = fs.readFileSync(configPath, "utf8");
|
|
await expect(
|
|
runConfigSet({
|
|
path: "channels.discord.token",
|
|
cliOptions: {
|
|
refProvider: "default",
|
|
refSource: "env",
|
|
refId: "MISSING_TEST_SECRET",
|
|
dryRun: true,
|
|
json: true,
|
|
},
|
|
runtime: runtime.runtime,
|
|
}),
|
|
).rejects.toThrow("__exit__:1");
|
|
const after = fs.readFileSync(configPath, "utf8");
|
|
expect(after).toBe(before);
|
|
expect(runtime.errors).toStrictEqual([]);
|
|
const raw = runtime.logs.at(-1);
|
|
if (raw === undefined) {
|
|
throw new Error("expected config check JSON log");
|
|
}
|
|
const payload = JSON.parse(raw) as {
|
|
ok?: boolean;
|
|
checks?: { schema?: boolean; resolvability?: boolean };
|
|
errors?: Array<{ kind?: string; ref?: string }>;
|
|
};
|
|
expect(payload.ok).toBe(false);
|
|
expect(payload.checks?.resolvability).toBe(true);
|
|
expect(payload.errors?.some((entry) => entry.kind === "resolvability")).toBe(true);
|
|
expect(
|
|
payload.errors?.some((entry) => (entry.ref ?? "").includes("MISSING_TEST_SECRET")),
|
|
).toBe(true);
|
|
} finally {
|
|
envSnapshot.restore();
|
|
clearConfigCache();
|
|
clearRuntimeConfigSnapshot();
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("skips exec provider execution during dry-run by default", async () => {
|
|
await withExecDryRunConfigHarness("openclaw-config-cli-int-exec-skip-", async (params) => {
|
|
const before = fs.readFileSync(params.configPath, "utf8");
|
|
await runConfigSet({
|
|
cliOptions: {
|
|
batchFile: params.batchPath,
|
|
dryRun: true,
|
|
},
|
|
runtime: params.runtime.runtime,
|
|
});
|
|
const after = fs.readFileSync(params.configPath, "utf8");
|
|
|
|
expect(after).toBe(before);
|
|
expect(fs.existsSync(params.markerPath)).toBe(false);
|
|
expect(
|
|
params.runtime.logs.some((line) =>
|
|
line.includes("Dry run note: skipped 1 exec SecretRef resolvability check(s)."),
|
|
),
|
|
).toBe(true);
|
|
});
|
|
});
|
|
|
|
it("executes exec providers during dry-run when --allow-exec is set", async () => {
|
|
await withExecDryRunConfigHarness("openclaw-config-cli-int-exec-allow-", async (params) => {
|
|
const before = fs.readFileSync(params.configPath, "utf8");
|
|
await runConfigSet({
|
|
cliOptions: {
|
|
batchFile: params.batchPath,
|
|
dryRun: true,
|
|
allowExec: true,
|
|
},
|
|
runtime: params.runtime.runtime,
|
|
});
|
|
const after = fs.readFileSync(params.configPath, "utf8");
|
|
|
|
expect(after).toBe(before);
|
|
expect(fs.existsSync(params.markerPath)).toBe(true);
|
|
expect(
|
|
params.runtime.logs.some((line) =>
|
|
line.includes("Dry run note: skipped 1 exec SecretRef resolvability check(s)."),
|
|
),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
});
|