From c106cf20cbfcec8e1fcd953ad8e52bdfbf8c3f9c Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:02:33 +0800 Subject: [PATCH] fix(setup): preserve owned Gemini user settings --- src/application/GeminiClientSettings.ts | 9 ++- src/application/SetupClientConfiguration.ts | 4 +- .../filesystem/geminiPolicySources.test.ts | 77 +++++++++++++++++++ 3 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 tests/boundary/filesystem/geminiPolicySources.test.ts diff --git a/src/application/GeminiClientSettings.ts b/src/application/GeminiClientSettings.ts index ae503ccf57..55a0bb9b4d 100644 --- a/src/application/GeminiClientSettings.ts +++ b/src/application/GeminiClientSettings.ts @@ -110,10 +110,13 @@ const workspaceTrusted = async ( return ok(trusted); }; -/** Check effective Gemini scopes while preserving their authority and source identity. */ +/** + * Check Gemini scopes using the caller's user-file observation (undefined means absent). + * Other settings sources are read here; transaction owners supply fresh user facts. + */ export const readClientPolicyBlock = async ( client: SetupClient, - userDocument?: ClientConfigurationDocument, + userDocument: ClientConfigurationDocument | undefined, desiredRegistration?: unknown, resources?: ArtifactResourceScope, ): Promise> => { @@ -128,7 +131,7 @@ export const readClientPolicyBlock = async ( settings.systemPath, ]) { const parsed = - path === client.configPath && userDocument !== undefined + path === client.configPath ? ok(userDocument) : await readConfigurationFile(path, "json", resources); if (!parsed.ok) return parsed; diff --git a/src/application/SetupClientConfiguration.ts b/src/application/SetupClientConfiguration.ts index 3d4887f2c3..7408f32901 100644 --- a/src/application/SetupClientConfiguration.ts +++ b/src/application/SetupClientConfiguration.ts @@ -138,7 +138,7 @@ const configureClientDocument = async ( return { status: "failed", reason: "readback" }; const policy = await readClientPolicyBlock( client, - undefined, + existing, await clientConfigurationDesired( client, environment, @@ -498,7 +498,7 @@ const inspectClientDocument = async ( }; const policy = await readClientPolicyBlock( client, - undefined, + existing, await clientConfigurationDesired( client, providerEnvironment, diff --git a/tests/boundary/filesystem/geminiPolicySources.test.ts b/tests/boundary/filesystem/geminiPolicySources.test.ts new file mode 100644 index 0000000000..00b215438f --- /dev/null +++ b/tests/boundary/filesystem/geminiPolicySources.test.ts @@ -0,0 +1,77 @@ +import { mkdir, rm, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { expect, it } from "vitest"; + +import { readConfigurationFile } from "../../../src/application/ClientConfigurationFiles.js"; +import { readClientPolicyBlock } from "../../../src/application/GeminiClientSettings.js"; +import type { SetupClient } from "../../../src/application/SupportedClients.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const fixture = async () => { + const home = await createTestTempDirectory("rea-gemini-policy-sources-"); + const configPath = join(home, ".gemini", "settings.json"); + await mkdir(dirname(configPath), { recursive: true }); + const client: SetupClient = { + name: "gemini_cli", + configPath, + format: "json", + geminiSettings: { + defaultsPath: join(home, "defaults.json"), + systemPath: join(home, "system.json"), + workspaceDirectory: home, + trustedFoldersPath: join(home, "trust.json"), + platform: process.platform, + }, + }; + return { client, configPath, systemPath: join(home, "system.json") }; +}; + +it("keeps observed user absence until the caller reads a new configuration", async () => { + const { client, configPath } = await fixture(); + const absent = await readConfigurationFile(configPath, "json"); + expect(absent).toEqual({ ok: true, value: undefined }); + if (!absent.ok) throw absent.error; + + await writeFile(configPath, JSON.stringify({ mcp: { excluded: ["rea"] } })); + expect(await readClientPolicyBlock(client, absent.value)).toEqual({ + ok: true, + value: undefined, + }); + + const current = await readConfigurationFile(configPath, "json"); + if (!current.ok) throw current.error; + expect(await readClientPolicyBlock(client, current.value)).toMatchObject({ + ok: true, + value: expect.stringContaining("mcp.excluded"), + }); +}); + +it("preserves the parsed user policy when its source disappears", async () => { + const { client, configPath } = await fixture(); + await writeFile(configPath, JSON.stringify({ mcp: { excluded: ["rea"] } })); + const observed = await readConfigurationFile(configPath, "json"); + if (!observed.ok) throw observed.error; + await rm(configPath); + + expect(await readClientPolicyBlock(client, observed.value)).toMatchObject({ + ok: true, + value: expect.stringContaining(configPath), + }); +}); + +it("still reads independent system policy when the user file was absent", async () => { + const { client, configPath, systemPath } = await fixture(); + const absent = await readConfigurationFile(configPath, "json"); + if (!absent.ok) throw absent.error; + await writeFile(systemPath, JSON.stringify({ mcp: { excluded: ["rea"] } })); + + expect(await readClientPolicyBlock(client, absent.value)).toMatchObject({ + ok: true, + value: expect.stringContaining(systemPath), + }); + await writeFile(systemPath, "{ invalid settings"); + expect(await readClientPolicyBlock(client, absent.value)).toMatchObject({ + ok: false, + error: { kind: "malformed", path: systemPath }, + }); +});