Skip to content

Commit 0b7cd10

Browse files
authored
refactor(code-index): use immutable configuration snapshots (Zoo-Code-Org#1815)
1 parent bf3bc78 commit 0b7cd10

4 files changed

Lines changed: 492 additions & 395 deletions

File tree

‎src/services/code-index/__tests__/config-manager.spec.ts‎

Lines changed: 179 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ describe("CodeIndexConfigManager", () => {
6060
expect(configManager.currentEmbedderProvider).toBe("openai")
6161
})
6262

63+
it("returns the original context proxy", () => {
64+
expect(configManager.getContextProxy()).toBe(mockContextProxy)
65+
})
66+
6367
it("loads Bedrock as the embedder provider with its optional profile", () => {
6468
mockContextProxy.getGlobalState.mockReturnValue({
6569
codebaseIndexEnabled: true,
@@ -81,6 +85,34 @@ describe("CodeIndexConfigManager", () => {
8185
})
8286
})
8387

88+
describe("model dimension parsing", () => {
89+
it.each([
90+
{ raw: undefined, expected: undefined, warns: false },
91+
{ raw: null, expected: undefined, warns: false },
92+
{ raw: 1536, expected: 1536, warns: false },
93+
{ raw: "768", expected: 768, warns: false },
94+
{ raw: 0, expected: undefined, warns: true },
95+
{ raw: -1, expected: undefined, warns: true },
96+
{ raw: "invalid", expected: undefined, warns: true },
97+
{ raw: NaN, expected: undefined, warns: true },
98+
])("parses $raw with warning=$warns", async ({ raw, expected, warns }) => {
99+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
100+
try {
101+
mockContextProxy.getGlobalState.mockReturnValue({ codebaseIndexEmbedderModelDimension: raw })
102+
const { currentConfig } = await configManager.loadConfiguration()
103+
expect(currentConfig.modelDimension).toBe(expected)
104+
expect(warn).toHaveBeenCalledTimes(warns ? 1 : 0)
105+
if (warns) {
106+
expect(warn).toHaveBeenCalledWith(
107+
`Invalid codebaseIndexEmbedderModelDimension value: ${raw}. Must be a positive number.`,
108+
)
109+
}
110+
} finally {
111+
warn.mockRestore()
112+
}
113+
})
114+
})
115+
84116
describe("isFeatureEnabled", () => {
85117
it("should return false when codebaseIndexEnabled is false", async () => {
86118
mockContextProxy.getGlobalState.mockReturnValue({
@@ -192,6 +224,7 @@ describe("CodeIndexConfigManager", () => {
192224
qdrantUrl: "http://localhost:6333",
193225
qdrantApiKey: "",
194226
searchMinScore: 0.4,
227+
searchMaxResults: 50,
195228
})
196229
expect(result.requiresRestart).toBe(false)
197230
})
@@ -1553,6 +1586,59 @@ describe("CodeIndexConfigManager", () => {
15531586
})
15541587

15551588
describe("doesConfigChangeRequireRestart", () => {
1589+
it.each([
1590+
{ missing: "enabled", expected: true },
1591+
{ missing: "configured", expected: true },
1592+
{ missing: "embedderProvider", expected: false },
1593+
] as const)(
1594+
"defaults missing previous $missing when comparing a ready configuration",
1595+
async ({ missing, expected }) => {
1596+
mockContextProxy.getGlobalState.mockReturnValue({
1597+
codebaseIndexEnabled: true,
1598+
codebaseIndexEmbedderProvider: providerIdentifiers.openai,
1599+
codebaseIndexQdrantUrl: "http://localhost:6333",
1600+
})
1601+
setupSecretMocks({ codeIndexOpenAiKey: "test-key" })
1602+
configManager = new CodeIndexConfigManager(mockContextProxy)
1603+
const { configSnapshot } = await configManager.loadConfiguration()
1604+
const previous = { ...configSnapshot }
1605+
// Required in the type, but the comparison deliberately tolerates incomplete runtime snapshots.
1606+
Reflect.deleteProperty(previous, missing)
1607+
1608+
expect(configManager.doesConfigChangeRequireRestart(previous)).toBe(expected)
1609+
},
1610+
)
1611+
1612+
it("normalizes absent connection values on either side of a defensive comparison", () => {
1613+
const previous: PreviousConfigSnapshot = {
1614+
enabled: true,
1615+
configured: true,
1616+
embedderProvider: providerIdentifiers.openai,
1617+
}
1618+
// Normal loads fill every connection value; exercise the helper's defensive defaults directly.
1619+
expect(configManager["_hasConnectionSettingsChanged"](previous, { ...previous, openAiKey: "" })).toBe(false)
1620+
expect(configManager["_hasConnectionSettingsChanged"]({ ...previous, openAiKey: "" }, previous)).toBe(false)
1621+
expect(
1622+
configManager["_hasConnectionSettingsChanged"](previous, { ...previous, qdrantApiKey: "new-key" }),
1623+
).toBe(true)
1624+
})
1625+
1626+
it("normalizes missing optional snapshot credentials in the previous load result", async () => {
1627+
// The snapshot type permits absent credentials even though normal readers initialize them.
1628+
configManager["config"] = Object.freeze({
1629+
...configManager["config"],
1630+
openAiOptions: undefined,
1631+
qdrantApiKey: undefined,
1632+
})
1633+
1634+
const { configSnapshot, requiresRestart } = await configManager.loadConfiguration()
1635+
1636+
expect(configSnapshot.openAiKey).toBe("")
1637+
expect(configSnapshot.qdrantApiKey).toBe("")
1638+
expect(configSnapshot.configured).toBe(false)
1639+
expect(requiresRestart).toBe(false)
1640+
})
1641+
15561642
it("should return true when enabling the feature", async () => {
15571643
// Initial state: disabled
15581644
mockContextProxy.getGlobalState.mockReturnValue({
@@ -1773,6 +1859,69 @@ describe("CodeIndexConfigManager", () => {
17731859
})
17741860

17751861
describe("getConfig", () => {
1862+
it("publishes frozen configuration and options for every provider", async () => {
1863+
mockContextProxy.getGlobalState.mockReturnValue({
1864+
codebaseIndexEnabled: true,
1865+
codebaseIndexEmbedderProvider: providerIdentifiers.openai,
1866+
codebaseIndexQdrantUrl: "http://localhost:6333",
1867+
codebaseIndexOpenAiCompatibleBaseUrl: "https://example.com/v1",
1868+
codebaseIndexSearchMaxResults: 23,
1869+
})
1870+
mockContextProxy.getSecret.mockReturnValue("test-key")
1871+
1872+
const { currentConfig } = await configManager.loadConfiguration()
1873+
expect(currentConfig).toEqual(configManager.getConfig())
1874+
expect(currentConfig.searchMaxResults).toBe(23)
1875+
expect(Object.isFrozen(currentConfig)).toBe(true)
1876+
expect(Reflect.set(currentConfig, "modelId", "changed")).toBe(false)
1877+
for (const options of [
1878+
currentConfig.openAiOptions,
1879+
currentConfig.ollamaOptions,
1880+
currentConfig.openAiCompatibleOptions,
1881+
currentConfig.geminiOptions,
1882+
currentConfig.mistralOptions,
1883+
currentConfig.vercelAiGatewayOptions,
1884+
currentConfig.bedrockOptions,
1885+
currentConfig.openRouterOptions,
1886+
]) {
1887+
expect(options).toBeDefined()
1888+
if (!options) throw new Error("Expected provider options")
1889+
expect(Object.isFrozen(options)).toBe(true)
1890+
for (const key of Object.keys(options)) {
1891+
expect(Reflect.set(options, key, "changed")).toBe(false)
1892+
}
1893+
}
1894+
expect(configManager.getConfig()).toEqual(currentConfig)
1895+
})
1896+
1897+
it("preserves previously returned snapshots across reloads", async () => {
1898+
mockContextProxy.getSecret.mockReturnValue("old-key")
1899+
const { currentConfig: previous } = await configManager.loadConfiguration()
1900+
mockContextProxy.getSecret.mockReturnValue("new-key")
1901+
const { currentConfig: current, configSnapshot } = await configManager.loadConfiguration()
1902+
1903+
expect(previous.openAiOptions?.openAiNativeApiKey).toBe("old-key")
1904+
expect(current.openAiOptions?.openAiNativeApiKey).toBe("new-key")
1905+
expect(current.openAiOptions).not.toBe(previous.openAiOptions)
1906+
expect(configSnapshot.openAiKey).toBe("old-key")
1907+
})
1908+
1909+
it("keeps the current snapshot when building the next snapshot fails", async () => {
1910+
const previous = configManager.getConfig()
1911+
mockContextProxy.getGlobalState.mockReturnValue({
1912+
codebaseIndexEnabled: true,
1913+
codebaseIndexQdrantUrl: "http://changed:6333",
1914+
get codebaseIndexEmbedderModelDimension(): number {
1915+
throw new Error("Invalid stored dimension")
1916+
},
1917+
})
1918+
1919+
await expect(configManager.loadConfiguration()).rejects.toThrow("Invalid stored dimension")
1920+
expect(configManager.getConfig()).toEqual(previous)
1921+
expect(configManager.isFeatureEnabled).toBe(false)
1922+
expect(configManager.getConfig().openAiOptions).toBe(previous.openAiOptions)
1923+
})
1924+
17761925
it("should return the current configuration", () => {
17771926
mockContextProxy.getGlobalState.mockReturnValue({
17781927
codebaseIndexEnabled: true,
@@ -2240,6 +2389,32 @@ describe("CodeIndexConfigManager", () => {
22402389
expect(result.currentConfig.isConfigured).toBe(true)
22412390
})
22422391

2392+
it("keeps an explicitly empty Bedrock region unconfigured and restarts when restored", async () => {
2393+
const settings = {
2394+
codebaseIndexEnabled: true,
2395+
codebaseIndexQdrantUrl: "http://qdrant.local",
2396+
codebaseIndexEmbedderProvider: providerIdentifiers.bedrock,
2397+
codebaseIndexBedrockRegion: "",
2398+
codebaseIndexBedrockProfile: "test-profile",
2399+
}
2400+
mockContextProxy.getGlobalState.mockReturnValue(settings)
2401+
configManager = new CodeIndexConfigManager(mockContextProxy)
2402+
2403+
const unchanged = await configManager.loadConfiguration()
2404+
expect(unchanged.currentConfig.bedrockOptions).toBeUndefined()
2405+
expect(unchanged.currentConfig.isConfigured).toBe(false)
2406+
expect(unchanged.configSnapshot.bedrockRegion).toBe("")
2407+
expect(unchanged.configSnapshot.bedrockProfile).toBe("")
2408+
expect(unchanged.requiresRestart).toBe(false)
2409+
2410+
mockContextProxy.getGlobalState.mockReturnValue({ ...settings, codebaseIndexBedrockRegion: "eu-west-1" })
2411+
const restored = await configManager.loadConfiguration()
2412+
expect(restored.currentConfig.bedrockOptions).toEqual({ region: "eu-west-1", profile: "test-profile" })
2413+
expect(restored.currentConfig.isConfigured).toBe(true)
2414+
expect(restored.requiresRestart).toBe(true)
2415+
expect(unchanged.currentConfig.bedrockOptions).toBeUndefined()
2416+
})
2417+
22432418
it("should return false from isConfigured for Bedrock when the Qdrant URL is missing", () => {
22442419
mockContextProxy.getGlobalState.mockReturnValue({
22452420
codebaseIndexEnabled: true,
@@ -2288,17 +2463,14 @@ describe("CodeIndexConfigManager", () => {
22882463
// private field to a value outside the union.
22892464
mockContextProxy.getGlobalState.mockReturnValue({
22902465
codebaseIndexEnabled: true,
2466+
codebaseIndexQdrantUrl: "http://localhost:6333",
22912467
})
22922468
mockContextProxy.getSecret.mockReturnValue(undefined)
22932469

22942470
configManager = new CodeIndexConfigManager(mockContextProxy)
2295-
// The defensive `return false` is unreachable through the public API because
2296-
// EmbedderProvider is a closed union, so exercise it by forcing the private field
2297-
// to a value outside the union. `embedderProvider` is TypeScript `private`, not
2298-
// `#`-private, so a runtime property write reaches it. The double assertion is a
2299-
// last resort: the private field is not part of the public type surface, and
2300-
// `as any` is avoided to keep the file's no-explicit-any suppression budget flat.
2301-
;(configManager as unknown as Record<string, unknown>)["embedderProvider"] = "not-a-provider"
2471+
// Replace the snapshot with deliberately invalid runtime data to exercise
2472+
// the defensive branch without mutating the frozen production snapshot.
2473+
Reflect.set(configManager, "config", { ...configManager["config"], embedderProvider: "not-a-provider" })
23022474
expect(configManager.isConfigured()).toBe(false)
23032475
})
23042476
})

0 commit comments

Comments
 (0)