





















@@ -13,6 +13,45 @@ const writeConfigFileMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined
1313const replaceConfigFileMock = vi.hoisted(() =>
1414vi.fn(async (params: { nextConfig: unknown }) => await writeConfigFileMock(params.nextConfig)),
1515);
16+const transformConfigWithPendingPluginInstallsMock = vi.hoisted(() =>
17+vi.fn(
18+async (params: {
19+transform: (
20+config: Record<string, unknown>,
21+context: {
22+snapshot: Record<string, unknown>;
23+previousHash: string | null;
24+attempt: number;
25+},
26+) =>
27+| Promise<{ nextConfig: unknown; result?: unknown }>
28+| { nextConfig: unknown; result?: unknown };
29+}) => {
30+const snapshot = (await readConfigFileSnapshotMock()) as {
31+path?: string;
32+hash?: string;
33+config?: Record<string, unknown>;
34+sourceConfig?: Record<string, unknown>;
35+};
36+const transformed = await params.transform(snapshot.sourceConfig ?? snapshot.config ?? {}, {
37+ snapshot,
38+previousHash: snapshot.hash ?? null,
39+attempt: 0,
40+});
41+await writeConfigFileMock(transformed.nextConfig);
42+return {
43+path: snapshot.path ?? "/tmp/openclaw.json",
44+previousHash: snapshot.hash ?? null,
45+ snapshot,
46+nextConfig: transformed.nextConfig,
47+result: transformed.result,
48+attempts: 1,
49+afterWrite: { mode: "auto" },
50+followUp: { mode: "auto", requiresRestart: false },
51+};
52+},
53+),
54+);
16551756const wizardMocks = vi.hoisted(() => ({
1857createClackPrompter: vi.fn(),
@@ -25,6 +64,13 @@ vi.mock("../config/config.js", async () => ({
2564replaceConfigFile: replaceConfigFileMock,
2665}));
276667+vi.mock("../cli/plugins-install-record-commit.js", async () => ({
68+ ...(await vi.importActual<typeof import("../cli/plugins-install-record-commit.js")>(
69+"../cli/plugins-install-record-commit.js",
70+)),
71+transformConfigWithPendingPluginInstalls: transformConfigWithPendingPluginInstallsMock,
72+}));
73+2874vi.mock("../wizard/clack-prompter.js", () => ({
2975createClackPrompter: wizardMocks.createClackPrompter,
3076}));
@@ -44,6 +90,7 @@ describe("agents add command", () => {
4490readConfigFileSnapshotMock.mockClear();
4591writeConfigFileMock.mockClear();
4692replaceConfigFileMock.mockClear();
93+transformConfigWithPendingPluginInstallsMock.mockClear();
4794wizardMocks.createClackPrompter.mockClear();
4895runtime.log.mockClear();
4996runtime.error.mockClear();
@@ -227,4 +274,98 @@ describe("agents add command", () => {
227274}),
228275).toBe('OAuth profiles stay shared from "main" unless this agent signs in separately.');
229276});
277+278+describe("non-interactive config mutation", () => {
279+it("rebases agent creation on the latest config snapshot", async () => {
280+readConfigFileSnapshotMock
281+.mockResolvedValueOnce({
282+ ...baseConfigSnapshot,
283+hash: "hash-1",
284+config: { agents: { list: [] } },
285+sourceConfig: { agents: { list: [] } },
286+})
287+.mockResolvedValueOnce({
288+ ...baseConfigSnapshot,
289+hash: "hash-2",
290+config: { agents: { list: [{ id: "other-agent" }] } },
291+sourceConfig: { agents: { list: [{ id: "other-agent" }] } },
292+});
293+294+await agentsAddCommand({ name: "Work", workspace: "/tmp/work" }, runtime, {
295+hasFlags: true,
296+});
297+298+expect(transformConfigWithPendingPluginInstallsMock).toHaveBeenCalledOnce();
299+expect(writeConfigFileMock).toHaveBeenCalledWith(
300+expect.objectContaining({
301+agents: {
302+list: [
303+{ id: "other-agent" },
304+expect.objectContaining({ id: "work", workspace: "/tmp/work" }),
305+],
306+},
307+}),
308+);
309+expect(runtime.exit).not.toHaveBeenCalled();
310+expect(runtime.error).not.toHaveBeenCalled();
311+});
312+313+it("fails instead of overwriting when the same agent appears before commit", async () => {
314+readConfigFileSnapshotMock
315+.mockResolvedValueOnce({
316+ ...baseConfigSnapshot,
317+hash: "hash-1",
318+config: { agents: { list: [] } },
319+sourceConfig: { agents: { list: [] } },
320+})
321+.mockResolvedValueOnce({
322+ ...baseConfigSnapshot,
323+hash: "hash-2",
324+config: { agents: { list: [{ id: "work", workspace: "/tmp/other" }] } },
325+sourceConfig: { agents: { list: [{ id: "work", workspace: "/tmp/other" }] } },
326+});
327+328+await agentsAddCommand({ name: "Work", workspace: "/tmp/work" }, runtime, {
329+hasFlags: true,
330+});
331+332+expect(writeConfigFileMock).not.toHaveBeenCalled();
333+expect(runtime.error).toHaveBeenCalledWith('Agent "work" already exists.');
334+expect(runtime.exit).toHaveBeenCalledWith(1);
335+});
336+337+it("reports binding conflicts from the committed mutation", async () => {
338+readConfigFileSnapshotMock
339+.mockResolvedValueOnce({
340+ ...baseConfigSnapshot,
341+hash: "hash-1",
342+config: { agents: { list: [] } },
343+sourceConfig: { agents: { list: [] } },
344+})
345+.mockResolvedValueOnce({
346+ ...baseConfigSnapshot,
347+hash: "hash-2",
348+config: {
349+agents: { list: [{ id: "other-agent" }] },
350+bindings: [{ type: "route", agentId: "other-agent", match: { channel: "telegram" } }],
351+},
352+sourceConfig: {
353+agents: { list: [{ id: "other-agent" }] },
354+bindings: [{ type: "route", agentId: "other-agent", match: { channel: "telegram" } }],
355+},
356+});
357+358+await agentsAddCommand(
359+{ name: "Work", workspace: "/tmp/work", bind: ["telegram"], json: true },
360+runtime,
361+{ hasFlags: true },
362+);
363+364+const payload = JSON.parse(String(runtime.log.mock.calls.at(-1)?.[0])) as {
365+bindings: { added: string[]; conflicts: string[] };
366+};
367+expect(payload.bindings.added).toEqual([]);
368+expect(payload.bindings.conflicts).toEqual(["telegram (agent=other-agent)"]);
369+});
370+});
230371});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。