From 197a60e5e649e7028b21effebb5dfec0b8e5ab6a Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 6 Mar 2026 01:14:42 -0500 Subject: [PATCH] test(config): cover operator policy overlay --- src/config/io.write-config.test.ts | 87 ++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/src/config/io.write-config.test.ts b/src/config/io.write-config.test.ts index 6b73b9fbd30..c292435e60d 100644 --- a/src/config/io.write-config.test.ts +++ b/src/config/io.write-config.test.ts @@ -47,6 +47,13 @@ describe("config io write", () => { return { configPath, io, snapshot }; } + async function writeOperatorPolicy(params: { home: string; policy: Record }) { + const policyPath = path.join(params.home, ".openclaw", "operator-policy.json5"); + await fs.mkdir(path.dirname(policyPath), { recursive: true }); + await fs.writeFile(policyPath, JSON.stringify(params.policy, null, 2), "utf-8"); + return policyPath; + } + async function writeTokenAuthAndReadConfig(params: { io: { writeConfigFile: (config: Record) => Promise }; snapshot: { config: Record }; @@ -142,6 +149,86 @@ describe("config io write", () => { }); }); + it("applies immutable operator policy to the effective config snapshot", async () => { + await withSuiteHome(async (home) => { + await writeOperatorPolicy({ + home, + policy: { + tools: { profile: "messaging" }, + approvals: { exec: { enabled: false } }, + }, + }); + const { io, snapshot } = await writeConfigAndCreateIo({ + home, + initialConfig: { gateway: { mode: "local" } }, + }); + + const reloaded = await io.readConfigFileSnapshot(); + expect(reloaded.valid).toBe(true); + expect(reloaded.config.gateway?.mode).toBe("local"); + expect(reloaded.config.tools?.profile).toBe("messaging"); + expect(reloaded.config.approvals?.exec?.enabled).toBe(false); + expect(reloaded.resolved.tools?.profile).toBeUndefined(); + expect(reloaded.policy?.exists).toBe(true); + expect(reloaded.policy?.valid).toBe(true); + expect(reloaded.policy?.lockedPaths).toEqual( + expect.arrayContaining(["tools.profile", "approvals.exec.enabled"]), + ); + expect(snapshot.policy?.lockedPaths).toEqual(reloaded.policy?.lockedPaths); + }); + }); + + it("rejects writes that conflict with locked operator policy paths", async () => { + await withSuiteHome(async (home) => { + await writeOperatorPolicy({ + home, + policy: { + tools: { profile: "messaging" }, + }, + }); + const { io, snapshot } = await writeConfigAndCreateIo({ + home, + initialConfig: { gateway: { mode: "local" } }, + }); + + const next = structuredClone(snapshot.config); + next.tools = { + ...next.tools, + profile: "full", + }; + + await expect(io.writeConfigFile(next)).rejects.toThrow( + "Config path locked by operator policy: tools.profile", + ); + }); + }); + + it("keeps locked operator policy paths out of the mutable config file", async () => { + await withSuiteHome(async (home) => { + await writeOperatorPolicy({ + home, + policy: { + tools: { profile: "messaging" }, + }, + }); + const { configPath, io, snapshot } = await writeConfigAndCreateIo({ + home, + initialConfig: { gateway: { mode: "local" } }, + }); + + const next = structuredClone(snapshot.config); + next.gateway = { mode: "remote" }; + await io.writeConfigFile(next); + + const persisted = JSON.parse(await fs.readFile(configPath, "utf-8")) as { + gateway?: { mode?: string }; + tools?: { profile?: string }; + }; + expect(persisted.gateway?.mode).toBe("remote"); + expect(persisted.tools?.profile).toBeUndefined(); + }); + }); + it('shows actionable guidance for dmPolicy="open" without wildcard allowFrom', async () => { await withSuiteHome(async (home) => { const io = createConfigIO({