




















@@ -2694,6 +2694,181 @@ describe("config cli", () => {
26942694unsetPaths: [["channels", "discord", "guilds", "123"]],
26952695});
26962696});
2697+2698+it("dry-runs an unset without writing the config file", async () => {
2699+const resolved: OpenClawConfig = {
2700+agents: { list: [{ id: "main" }] },
2701+gateway: { port: 18789 },
2702+tools: {
2703+profile: "coding",
2704+alsoAllow: ["agents_list"],
2705+},
2706+};
2707+setSnapshot(resolved, resolved);
2708+setSnapshot(resolved, resolved);
2709+2710+await runConfigCommand(["config", "unset", "tools.alsoAllow", "--dry-run"]);
2711+2712+expect(mockWriteConfigFile).not.toHaveBeenCalled();
2713+expectLogIncludes("Dry run successful: 1 update(s) validated against /tmp/openclaw.json.");
2714+expect(mockReadConfigFileSnapshot).toHaveBeenCalledTimes(2);
2715+});
2716+2717+it("prints JSON for config unset dry-run", async () => {
2718+const resolved: OpenClawConfig = {
2719+agents: { list: [{ id: "main" }] },
2720+gateway: { port: 18789 },
2721+tools: {
2722+profile: "coding",
2723+alsoAllow: ["agents_list"],
2724+},
2725+};
2726+setSnapshot(resolved, resolved);
2727+setSnapshot(resolved, resolved);
2728+2729+await runConfigCommand(["config", "unset", "tools.alsoAllow", "--dry-run", "--json"]);
2730+2731+expect(mockWriteConfigFile).not.toHaveBeenCalled();
2732+expect(parseLastLogPayload()).toMatchObject({
2733+ok: true,
2734+operations: 1,
2735+inputModes: ["unset"],
2736+checks: {
2737+schema: true,
2738+resolvability: true,
2739+resolvabilityComplete: true,
2740+},
2741+});
2742+});
2743+2744+it("prints structured JSON when unset dry-run misses a path", async () => {
2745+const resolved: OpenClawConfig = {
2746+gateway: { port: 18789 },
2747+tools: {
2748+profile: "coding",
2749+},
2750+};
2751+setSnapshot(resolved, resolved);
2752+2753+await expect(
2754+runConfigCommand(["config", "unset", "tools.alsoAllow", "--dry-run", "--json"]),
2755+).rejects.toThrow("__exit__:1");
2756+2757+expect(mockWriteConfigFile).not.toHaveBeenCalled();
2758+expect(mockError).not.toHaveBeenCalled();
2759+const payload = parseLastLogPayload() as {
2760+ok: boolean;
2761+inputModes: string[];
2762+checks: { schema: boolean; resolvability: boolean; resolvabilityComplete: boolean };
2763+errors?: Array<{ kind: string; message: string }>;
2764+};
2765+expect(payload.ok).toBe(false);
2766+expect(payload.inputModes).toEqual(["unset"]);
2767+expect(payload.checks).toEqual({
2768+schema: false,
2769+resolvability: false,
2770+resolvabilityComplete: false,
2771+});
2772+expect(payload.errors).toEqual([
2773+{
2774+kind: "missing-path",
2775+message: "Config path not found: tools.alsoAllow. Nothing was changed.",
2776+},
2777+]);
2778+});
2779+2780+it("validates existing refs when unset dry-run removes all secret providers", async () => {
2781+const resolved: OpenClawConfig = {
2782+gateway: { port: 18789 },
2783+secrets: {
2784+providers: {
2785+vaultfile: { source: "file", path: "/tmp/secrets.json", mode: "json" },
2786+},
2787+},
2788+tools: {
2789+web: {
2790+search: {
2791+enabled: true,
2792+apiKey: {
2793+source: "file",
2794+provider: "vaultfile",
2795+id: "/providers/search/apiKey",
2796+},
2797+},
2798+},
2799+} as never,
2800+};
2801+setSnapshot(resolved, resolved);
2802+setSnapshot(resolved, resolved);
2803+mockResolveSecretRefValue.mockRejectedValueOnce(new Error("provider removed"));
2804+2805+await expect(
2806+runConfigCommand(["config", "unset", "secrets.providers", "--dry-run"]),
2807+).rejects.toThrow("__exit__:1");
2808+2809+expect(mockWriteConfigFile).not.toHaveBeenCalled();
2810+const [secretRef] = requireResolveSecretRefCall(0);
2811+const secretRefRecord = requireRecord(secretRef, "existing SecretRef");
2812+expect(secretRefRecord.provider).toBe("vaultfile");
2813+expect(secretRefRecord.id).toBe("/providers/search/apiKey");
2814+expectErrorIncludes("Dry run failed: 1 SecretRef assignment(s) could not be resolved.");
2815+expectErrorIncludes("provider removed");
2816+});
2817+2818+it("validates existing refs when unset dry-run removes secret defaults", async () => {
2819+const resolved: OpenClawConfig = {
2820+gateway: { port: 18789 },
2821+secrets: {
2822+defaults: {
2823+env: "vaultenv",
2824+},
2825+providers: {
2826+default: { source: "env" },
2827+vaultenv: { source: "env" },
2828+},
2829+},
2830+tools: {
2831+web: {
2832+search: {
2833+enabled: true,
2834+apiKey: "${WEB_SEARCH_API_KEY}",
2835+},
2836+},
2837+} as never,
2838+} as OpenClawConfig;
2839+setSnapshot(resolved, resolved);
2840+setSnapshot(resolved, resolved);
2841+2842+await runConfigCommand(["config", "unset", "secrets.defaults", "--dry-run"]);
2843+2844+expect(mockWriteConfigFile).not.toHaveBeenCalled();
2845+const [secretRef] = requireResolveSecretRefCall(0);
2846+const secretRefRecord = requireRecord(secretRef, "defaulted SecretRef");
2847+expect(secretRefRecord).toMatchObject({
2848+source: "env",
2849+provider: "default",
2850+id: "WEB_SEARCH_API_KEY",
2851+});
2852+expectLogIncludes("Dry run successful: 1 update(s) validated against /tmp/openclaw.json.");
2853+});
2854+2855+it("rejects config unset --json without --dry-run", async () => {
2856+await expect(
2857+runConfigCommand(["config", "unset", "tools.alsoAllow", "--json"]),
2858+).rejects.toThrow("__exit__:1");
2859+2860+expect(mockWriteConfigFile).not.toHaveBeenCalled();
2861+expectErrorIncludes("--json can only be used with --dry-run.");
2862+});
2863+2864+it("rejects config unset --allow-exec without --dry-run", async () => {
2865+await expect(
2866+runConfigCommand(["config", "unset", "tools.alsoAllow", "--allow-exec"]),
2867+).rejects.toThrow("__exit__:1");
2868+2869+expect(mockWriteConfigFile).not.toHaveBeenCalled();
2870+expectErrorIncludes("--allow-exec can only be used with --dry-run.");
2871+});
26972872});
2698287326992874describe("config file", () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。