mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
d5e89de906
* fix(synology-chat): host outbound attachments * fix(synology-chat): isolate hosted media routes * fix(synology-chat): harden hosted media limits * fix(synology-chat): inspect hosted media content * fix(synology-chat): reject ambiguous media routes * fix(synology-chat): retain indeterminate media capabilities * fix(synology-chat): mask public callback URLs * fix(synology-chat): align callback sensitivity metadata * fix(synology-chat): reserve media capability query keys * fix(synology-chat): release rejected staged media * fix(synology-chat): preserve ambiguous media handoffs * fix(synology-chat): scan complete active media preamble * fix(synology-chat): report validated attachment readiness * fix(synology-chat): bound public media capability probes * fix(synology-chat): bound hosted media responses * test(synology-chat): model response headers read-only * fix(synology-chat): bound hosted media reads * fix(synology-chat): bound hosted media delivery * fix(synology-chat): retain media through active serves * fix(plugin-sdk): lease hosted media readers atomically * fix(synology-chat): close hosted media review gaps * test(synology-chat): clean up hosted media state * fix(system-agent): canonicalize sensitive config paths * fix(system-agent): honor runtime config sensitivity hints * fix(config): inherit sensitive metadata * fix(synology-chat): close latest review findings * test(system-agent): keep config recovery checks lint-clean * fix(system-agent): redact structured config secrets * fix(synology-chat): harden active-content sniffing * fix(security): close hosted media review gaps * fix(security): close remaining hosted media review findings * fix(system-agent): narrow dynamic owner ids safely * fix(system-agent): narrow dynamic channel ids safely * test(channels): isolate hosted media proofs * fix(security): close config and hosted media review gaps * test(plugin-sdk): split outbound media retention coverage * test(plugin-sdk): isolate capacity store fixtures * test(system-agent): assert config secrecy invariant
496 lines
16 KiB
TypeScript
496 lines
16 KiB
TypeScript
// OpenClaw tests cover main rescue and audit command behavior.
|
|
import { beforeAll, describe, expect, it, vi } from "vitest";
|
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
|
import { SystemAgentInferenceUnavailableError } from "./inference-error.js";
|
|
import type { SystemAgentCommandDeps } from "./operations.js";
|
|
import type { SystemAgentOverview } from "./overview.js";
|
|
import { runSystemAgent, type RunSystemAgentOptions } from "./system-agent.js";
|
|
import { createSystemAgentTestRuntime } from "./system-agent.runtime.test-support.js";
|
|
import { createSystemAgentVerifiedInferenceTestFixture } from "./system-agent.test-helpers.js";
|
|
import type { SystemAgentVerifiedInferenceBinding } from "./verified-inference.js";
|
|
|
|
const inferenceMocks = vi.hoisted(() => ({
|
|
sharedVerifiedInference: undefined as SystemAgentVerifiedInferenceBinding | undefined,
|
|
}));
|
|
|
|
vi.mock("../plugins/providers.js", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("../plugins/providers.js")>()),
|
|
resolveOwningPluginIdsForModelRefs: vi.fn(() => []),
|
|
resolveOwningPluginIdsForProviderRef: vi.fn(() => []),
|
|
}));
|
|
|
|
vi.mock("./verified-inference.js", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("./verified-inference.js")>();
|
|
return {
|
|
...actual,
|
|
resolveSystemAgentVerifiedInferenceRoute: (
|
|
...args: Parameters<typeof actual.resolveSystemAgentVerifiedInferenceRoute>
|
|
) => {
|
|
// Ordinary runner cases share one immutable verified fixture. Distinct
|
|
// bindings still use the real resolver for route and apply-boundary drift.
|
|
if (args[0] === inferenceMocks.sharedVerifiedInference) {
|
|
return Promise.resolve(args[0].execution);
|
|
}
|
|
return actual.resolveSystemAgentVerifiedInferenceRoute(...args);
|
|
},
|
|
};
|
|
});
|
|
|
|
const overview: SystemAgentOverview = {
|
|
defaultAgentId: "main",
|
|
defaultModel: "openai/gpt-5.5",
|
|
agents: [{ id: "main", isDefault: true, model: "openai/gpt-5.5" }],
|
|
config: { path: "/tmp/openclaw.json", exists: true, valid: true, issues: [], hash: null },
|
|
tools: {
|
|
codex: { command: "codex", found: false, error: "not found" },
|
|
claude: { command: "claude", found: false, error: "not found" },
|
|
gemini: { command: "gemini", found: false, error: "not found" },
|
|
apiKeys: { openai: true, anthropic: false },
|
|
},
|
|
gateway: {
|
|
url: "ws://127.0.0.1:18789",
|
|
source: "local loopback",
|
|
reachable: false,
|
|
error: "offline",
|
|
},
|
|
references: {
|
|
docsUrl: "https://docs.openclaw.ai",
|
|
sourceUrl: "https://github.com/openclaw/openclaw",
|
|
},
|
|
};
|
|
|
|
const systemAgentOverviewDeps = {
|
|
formatOverview: () => "Default model: openai/gpt-5.5",
|
|
loadOverview: async () => overview,
|
|
};
|
|
|
|
const verifiedConfig = {
|
|
agents: { defaults: { model: "openai/gpt-5.5" } },
|
|
models: {
|
|
providers: {
|
|
openai: {
|
|
baseUrl: "https://api.openai.com/v1",
|
|
apiKey: "test-key",
|
|
auth: "api-key",
|
|
models: [],
|
|
},
|
|
},
|
|
},
|
|
} satisfies OpenClawConfig;
|
|
|
|
function configSnapshot(config: OpenClawConfig) {
|
|
return {
|
|
exists: true,
|
|
valid: true,
|
|
path: "/tmp/openclaw.json",
|
|
hash: "h",
|
|
config,
|
|
runtimeConfig: config,
|
|
sourceConfig: config,
|
|
issues: [],
|
|
};
|
|
}
|
|
|
|
let sharedVerifiedFixture: Awaited<
|
|
ReturnType<typeof createSystemAgentVerifiedInferenceTestFixture>
|
|
>;
|
|
|
|
beforeAll(async () => {
|
|
sharedVerifiedFixture = await createSystemAgentVerifiedInferenceTestFixture(verifiedConfig);
|
|
inferenceMocks.sharedVerifiedInference = sharedVerifiedFixture.binding;
|
|
});
|
|
|
|
function createVerifiedRunOptions(
|
|
deps: SystemAgentCommandDeps = {},
|
|
options: { revalidate?: boolean } = {},
|
|
) {
|
|
const fixture = sharedVerifiedFixture;
|
|
return {
|
|
verifiedInference: options.revalidate ? { ...fixture.binding } : fixture.binding,
|
|
deps: {
|
|
...fixture.deps,
|
|
readConfigFileSnapshot: vi.fn(async () => configSnapshot(verifiedConfig)) as never,
|
|
...deps,
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("runSystemAgent", () => {
|
|
it("rejects a missing inference binding before any runner side effect", async () => {
|
|
const { runtime } = createSystemAgentTestRuntime();
|
|
const loadOverview = vi.fn(async () => overview);
|
|
const planWithAssistant = vi.fn(async () => ({ command: "restart gateway" }));
|
|
const runGatewayRestart = vi.fn(async () => {});
|
|
const runInteractiveTui = vi.fn(async () => {});
|
|
const common = {
|
|
deps: { loadOverview, runGatewayRestart },
|
|
planWithAssistant,
|
|
runInteractiveTui,
|
|
input: { isTTY: true } as unknown as NodeJS.ReadableStream,
|
|
output: { isTTY: true } as unknown as NodeJS.WritableStream,
|
|
};
|
|
const withoutBinding = (opts: Omit<RunSystemAgentOptions, "verifiedInference">) =>
|
|
opts as RunSystemAgentOptions;
|
|
|
|
await expect(
|
|
runSystemAgent(withoutBinding({ ...common, json: true }), runtime),
|
|
).rejects.toBeInstanceOf(SystemAgentInferenceUnavailableError);
|
|
await expect(
|
|
runSystemAgent(withoutBinding({ ...common, message: "please make things nicer" }), runtime),
|
|
).rejects.toBeInstanceOf(SystemAgentInferenceUnavailableError);
|
|
await expect(
|
|
runSystemAgent(withoutBinding({ ...common, message: "restart gateway", yes: true }), runtime),
|
|
).rejects.toBeInstanceOf(SystemAgentInferenceUnavailableError);
|
|
await expect(runSystemAgent(withoutBinding(common), runtime)).rejects.toBeInstanceOf(
|
|
SystemAgentInferenceUnavailableError,
|
|
);
|
|
|
|
expect(loadOverview).not.toHaveBeenCalled();
|
|
expect(planWithAssistant).not.toHaveBeenCalled();
|
|
expect(runGatewayRestart).not.toHaveBeenCalled();
|
|
expect(runInteractiveTui).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("uses the assistant planner only to choose typed operations", async () => {
|
|
const { runtime, lines } = createSystemAgentTestRuntime();
|
|
let runGatewayRestartCalls = 0;
|
|
let onReadyCalls = 0;
|
|
const verified = createVerifiedRunOptions({
|
|
runGatewayRestart: async () => {
|
|
runGatewayRestartCalls += 1;
|
|
},
|
|
});
|
|
|
|
await runSystemAgent(
|
|
{
|
|
...verified,
|
|
message: "the local bridge looks sleepy, poke it",
|
|
onReady: () => {
|
|
onReadyCalls += 1;
|
|
},
|
|
planWithAssistant: async () => ({
|
|
reply: "I can queue a Gateway restart.",
|
|
command: "restart gateway",
|
|
modelLabel: "openai/gpt-5.5",
|
|
}),
|
|
...systemAgentOverviewDeps,
|
|
},
|
|
runtime,
|
|
);
|
|
|
|
expect(runGatewayRestartCalls).toBe(0);
|
|
expect(onReadyCalls).toBe(0);
|
|
expect(lines.join("\n")).toContain("[openclaw] planner: openai/gpt-5.5");
|
|
expect(lines.join("\n")).toContain("[openclaw] interpreted: restart gateway");
|
|
expect(lines.join("\n")).toContain("Plan: restart the Gateway. Say yes to apply.");
|
|
expect(lines.indexOf("Default model: openai/gpt-5.5")).toBeLessThan(
|
|
lines.findIndex((line) => line.includes("[openclaw] planner:")),
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
"config set gateway.auth.token=very-secret",
|
|
"config set gateway.auth.token=very-secret please",
|
|
String.raw`config set gateway.auth.token\ very-secret please`,
|
|
"config set gateway.auth.tokenabcDEF123 please",
|
|
"config set gateway.auth.token_abcDEF123 please",
|
|
"config set gateway.auth.token$abcDEF123 please",
|
|
"config set-ref gateway.auth.tokenabcDEF123 env GATEWAY_TOKEN",
|
|
'config set gateway.auth["token:very-secret"] please',
|
|
])(
|
|
"keeps malformed config write %s away from the one-shot assistant planner",
|
|
async (message) => {
|
|
const { runtime, lines } = createSystemAgentTestRuntime();
|
|
const planWithAssistant = vi.fn(async () => ({ command: "restart gateway" }));
|
|
|
|
await runSystemAgent(
|
|
{
|
|
...createVerifiedRunOptions(),
|
|
message,
|
|
planWithAssistant,
|
|
...systemAgentOverviewDeps,
|
|
},
|
|
runtime,
|
|
);
|
|
|
|
expect(planWithAssistant).not.toHaveBeenCalled();
|
|
expect(lines.join("\n")).toContain("Invalid config path");
|
|
expect(lines.join("\n")).not.toContain("very-secret");
|
|
expect(lines.join("\n")).not.toContain("abcDEF123");
|
|
},
|
|
);
|
|
|
|
it("does not apply a one-shot plan after the verified route changes", async () => {
|
|
const { runtime } = createSystemAgentTestRuntime();
|
|
const changedConfig = {
|
|
agents: { defaults: { model: "anthropic/claude-opus-4-8" } },
|
|
} satisfies OpenClawConfig;
|
|
const readConfigFileSnapshot = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(configSnapshot(verifiedConfig))
|
|
.mockResolvedValue(configSnapshot(changedConfig));
|
|
const runGatewayRestart = vi.fn(async () => {});
|
|
const verified = createVerifiedRunOptions(
|
|
{
|
|
readConfigFileSnapshot: readConfigFileSnapshot as never,
|
|
runGatewayRestart,
|
|
},
|
|
{ revalidate: true },
|
|
);
|
|
|
|
await expect(
|
|
runSystemAgent(
|
|
{
|
|
...verified,
|
|
message: "the bridge looks sleepy, restart it",
|
|
yes: true,
|
|
planWithAssistant: async () => ({ command: "restart gateway" }),
|
|
...systemAgentOverviewDeps,
|
|
},
|
|
runtime,
|
|
),
|
|
).rejects.toBeInstanceOf(SystemAgentInferenceUnavailableError);
|
|
expect(runGatewayRestart).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("rechecks one-shot authority at the persistent apply boundary", async () => {
|
|
const { runtime } = createSystemAgentTestRuntime();
|
|
const changedConfig = {
|
|
agents: { defaults: { model: "anthropic/claude-opus-4-8" } },
|
|
} satisfies OpenClawConfig;
|
|
const readConfigFileSnapshot = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(configSnapshot(verifiedConfig))
|
|
.mockResolvedValueOnce(configSnapshot(verifiedConfig))
|
|
.mockResolvedValueOnce(configSnapshot(verifiedConfig))
|
|
.mockResolvedValue(configSnapshot(changedConfig));
|
|
const runGatewayRestart = vi.fn(async () => {});
|
|
const verified = createVerifiedRunOptions(
|
|
{
|
|
readConfigFileSnapshot: readConfigFileSnapshot as never,
|
|
runGatewayRestart,
|
|
},
|
|
{ revalidate: true },
|
|
);
|
|
|
|
await expect(
|
|
runSystemAgent(
|
|
{
|
|
...verified,
|
|
message: "the bridge looks sleepy, restart it",
|
|
yes: true,
|
|
planWithAssistant: async () => ({ command: "restart gateway" }),
|
|
...systemAgentOverviewDeps,
|
|
},
|
|
runtime,
|
|
),
|
|
).rejects.toBeInstanceOf(SystemAgentInferenceUnavailableError);
|
|
|
|
expect(readConfigFileSnapshot).toHaveBeenCalledTimes(4);
|
|
expect(runGatewayRestart).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("keeps exact one-shot parsing ahead of the assistant planner", async () => {
|
|
const { runtime, lines } = createSystemAgentTestRuntime();
|
|
let plannerCalls = 0;
|
|
let onReadyCalls = 0;
|
|
const verified = createVerifiedRunOptions();
|
|
|
|
await runSystemAgent(
|
|
{
|
|
...verified,
|
|
message: "models",
|
|
planWithAssistant: async () => {
|
|
plannerCalls += 1;
|
|
return { command: "restart gateway" };
|
|
},
|
|
onReady: () => {
|
|
onReadyCalls += 1;
|
|
},
|
|
...systemAgentOverviewDeps,
|
|
},
|
|
runtime,
|
|
);
|
|
|
|
expect(plannerCalls).toBe(0);
|
|
expect(onReadyCalls).toBe(0);
|
|
expect(lines.join("\n")).toContain("Default model:");
|
|
});
|
|
|
|
it("prints an explicit one-shot overview exactly once", async () => {
|
|
const { runtime, lines } = createSystemAgentTestRuntime();
|
|
const verified = createVerifiedRunOptions();
|
|
|
|
await runSystemAgent(
|
|
{
|
|
...verified,
|
|
message: "overview",
|
|
formatOverview: () => "formatted overview",
|
|
loadOverview: async () => overview,
|
|
},
|
|
runtime,
|
|
);
|
|
|
|
expect(lines).toEqual(["formatted overview"]);
|
|
});
|
|
|
|
it.each([
|
|
{ name: "no plan", plan: null },
|
|
{ name: "invalid command", plan: { command: "invent a new operation" } },
|
|
])("fails a fuzzy one-shot when inference returns $name", async ({ plan }) => {
|
|
const { runtime } = createSystemAgentTestRuntime();
|
|
const verified = createVerifiedRunOptions();
|
|
|
|
await expect(
|
|
runSystemAgent(
|
|
{
|
|
...verified,
|
|
message: "please make things nicer",
|
|
planWithAssistant: async () => plan,
|
|
...systemAgentOverviewDeps,
|
|
},
|
|
runtime,
|
|
),
|
|
).rejects.toBeInstanceOf(SystemAgentInferenceUnavailableError);
|
|
});
|
|
|
|
it("prints a valid reply-only one-shot plan", async () => {
|
|
const { runtime, lines } = createSystemAgentTestRuntime();
|
|
const verified = createVerifiedRunOptions();
|
|
|
|
await runSystemAgent(
|
|
{
|
|
...verified,
|
|
message: "explain the current setup",
|
|
planWithAssistant: async () => ({ reply: "The current setup is healthy." }),
|
|
...systemAgentOverviewDeps,
|
|
},
|
|
runtime,
|
|
);
|
|
|
|
expect(lines).toEqual(["Default model: openai/gpt-5.5", "", "The current setup is healthy."]);
|
|
});
|
|
|
|
it("does not print a reply-only plan after its inference owner drifts", async () => {
|
|
const { runtime, lines } = createSystemAgentTestRuntime();
|
|
const changedConfig = {
|
|
agents: { defaults: { model: "anthropic/claude-opus-4-8" } },
|
|
} satisfies OpenClawConfig;
|
|
let currentConfig: OpenClawConfig = verifiedConfig;
|
|
const verified = createVerifiedRunOptions(
|
|
{
|
|
readConfigFileSnapshot: vi.fn(async () => configSnapshot(currentConfig)) as never,
|
|
},
|
|
{ revalidate: true },
|
|
);
|
|
|
|
await expect(
|
|
runSystemAgent(
|
|
{
|
|
...verified,
|
|
message: "explain the current setup",
|
|
planWithAssistant: async () => {
|
|
currentConfig = changedConfig;
|
|
return { reply: "stale reply" };
|
|
},
|
|
...systemAgentOverviewDeps,
|
|
},
|
|
runtime,
|
|
),
|
|
).rejects.toBeInstanceOf(SystemAgentInferenceUnavailableError);
|
|
expect(lines).not.toContain("stale reply");
|
|
});
|
|
|
|
it("starts interactive OpenClaw in the TUI shell", async () => {
|
|
const { runtime, lines } = createSystemAgentTestRuntime();
|
|
let runInteractiveTuiCalls = 0;
|
|
let onReadyCalls = 0;
|
|
const verified = createVerifiedRunOptions();
|
|
|
|
await runSystemAgent(
|
|
{
|
|
...verified,
|
|
input: { isTTY: true } as unknown as NodeJS.ReadableStream,
|
|
output: { isTTY: true } as unknown as NodeJS.WritableStream,
|
|
runInteractiveTui: async () => {
|
|
runInteractiveTuiCalls += 1;
|
|
},
|
|
onReady: () => {
|
|
onReadyCalls += 1;
|
|
},
|
|
},
|
|
runtime,
|
|
);
|
|
|
|
expect(runInteractiveTuiCalls).toBe(1);
|
|
expect(onReadyCalls).toBe(1);
|
|
expect(lines.join("\n")).not.toContain("Say: status");
|
|
});
|
|
|
|
it("prints the formatted overview exactly once when interactive mode is disabled", async () => {
|
|
const { runtime, lines } = createSystemAgentTestRuntime();
|
|
let loadOverviewCalls = 0;
|
|
let runInteractiveTuiCalls = 0;
|
|
const verified = createVerifiedRunOptions();
|
|
|
|
await runSystemAgent(
|
|
{
|
|
...verified,
|
|
interactive: false,
|
|
loadOverview: async () => {
|
|
loadOverviewCalls += 1;
|
|
return overview;
|
|
},
|
|
formatOverview: () => "formatted overview",
|
|
runInteractiveTui: async () => {
|
|
runInteractiveTuiCalls += 1;
|
|
},
|
|
},
|
|
runtime,
|
|
);
|
|
|
|
expect(loadOverviewCalls).toBe(1);
|
|
expect(runInteractiveTuiCalls).toBe(0);
|
|
expect(lines).toEqual(["formatted overview"]);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "stdin is not a TTY",
|
|
input: { isTTY: false } as unknown as NodeJS.ReadableStream,
|
|
output: { isTTY: true } as unknown as NodeJS.WritableStream,
|
|
interactive: true,
|
|
},
|
|
{
|
|
name: "stdout is not a TTY",
|
|
input: { isTTY: true } as unknown as NodeJS.ReadableStream,
|
|
output: { isTTY: false } as unknown as NodeJS.WritableStream,
|
|
interactive: true,
|
|
},
|
|
])("exits non-zero when $name", async ({ input, output, interactive }) => {
|
|
const { runtime, lines } = createSystemAgentTestRuntime();
|
|
let runInteractiveTuiCalls = 0;
|
|
const verified = createVerifiedRunOptions();
|
|
|
|
await expect(
|
|
runSystemAgent(
|
|
{
|
|
...verified,
|
|
input,
|
|
output,
|
|
interactive,
|
|
runInteractiveTui: async () => {
|
|
runInteractiveTuiCalls += 1;
|
|
},
|
|
},
|
|
runtime,
|
|
),
|
|
).rejects.toThrow("exit 1");
|
|
|
|
expect(runInteractiveTuiCalls).toBe(0);
|
|
expect(lines.join("\n")).toContain(
|
|
"OpenClaw needs an interactive TTY. Use --message for one command.",
|
|
);
|
|
});
|
|
});
|