Skip to content

Commit 51b9e23

Browse files
committed
fix(config): derive ENV_LLM_KEYS from ENV_LLM_MAP to prevent drift
applyEnvProvenance hardcoded a second literal list of the four CODEGRAPH_LLM_* env var names to decide whether to attribute provenance.llm = 'env'. If a new override were ever added to ENV_LLM_MAP without updating this list, the override would work but `codegraph config --explain` would silently fail to attribute it. Move ENV_LLM_MAP above the provenance helpers and derive the key list via Object.keys(ENV_LLM_MAP) so the two can never drift apart.
1 parent bf8e969 commit 51b9e23

2 files changed

Lines changed: 42 additions & 15 deletions

File tree

src/infrastructure/config.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,19 @@ export function clearConfigCache(): void {
821821
_globalConfigCache.clear();
822822
}
823823

824+
/**
825+
* Single source of truth for CODEGRAPH_LLM_* env var overrides, mapping each
826+
* env var name to the `config.llm` field it overrides. Consumed both by
827+
* applyEnvOverrides (actual override behavior) and applyEnvProvenance (which
828+
* derives its key list via Object.keys) so the two can never drift.
829+
*/
830+
const ENV_LLM_MAP: Record<string, string> = {
831+
CODEGRAPH_LLM_PROVIDER: 'provider',
832+
CODEGRAPH_LLM_API_KEY: 'apiKey',
833+
CODEGRAPH_LLM_MODEL: 'model',
834+
CODEGRAPH_LLM_BASE_URL: 'baseUrl',
835+
};
836+
824837
/**
825838
* Layer 0 provenance: every top-level key starts attributed to 'default'.
826839
*/
@@ -866,16 +879,11 @@ function applyProjectProvenance(provenance: Record<string, ConfigSource>, cwd: s
866879

867880
/**
868881
* Layer 3+ provenance: mark `llm` as env-sourced if any CODEGRAPH_LLM_*
869-
* override variable is set.
882+
* override variable is set. Keys are derived from ENV_LLM_MAP so this can
883+
* never drift from the env vars applyEnvOverrides actually honors.
870884
*/
871885
function applyEnvProvenance(provenance: Record<string, ConfigSource>): void {
872-
const ENV_LLM_KEYS = [
873-
'CODEGRAPH_LLM_PROVIDER',
874-
'CODEGRAPH_LLM_API_KEY',
875-
'CODEGRAPH_LLM_MODEL',
876-
'CODEGRAPH_LLM_BASE_URL',
877-
];
878-
if (ENV_LLM_KEYS.some((k) => process.env[k] !== undefined)) {
886+
if (Object.keys(ENV_LLM_MAP).some((k) => process.env[k] !== undefined)) {
879887
provenance.llm = 'env';
880888
}
881889
}
@@ -913,13 +921,6 @@ export function loadConfigWithProvenance(
913921
return { config, provenance, appliedGlobalPath: applied ? globalPath : null, consentDecision };
914922
}
915923

916-
const ENV_LLM_MAP: Record<string, string> = {
917-
CODEGRAPH_LLM_PROVIDER: 'provider',
918-
CODEGRAPH_LLM_API_KEY: 'apiKey',
919-
CODEGRAPH_LLM_MODEL: 'model',
920-
CODEGRAPH_LLM_BASE_URL: 'baseUrl',
921-
};
922-
923924
export function applyEnvOverrides(config: CodegraphConfig): CodegraphConfig {
924925
for (const [envKey, field] of Object.entries(ENV_LLM_MAP)) {
925926
if (process.env[envKey as keyof NodeJS.ProcessEnv] !== undefined) {

tests/unit/config-user.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,32 @@ describe('loadConfigWithProvenance', () => {
333333
const { provenance } = loadConfigWithProvenance(dir, opts);
334334
expect(provenance.exclude).toBe('user');
335335
});
336+
337+
// Regression test for issue #1857: the provenance key list must be derived
338+
// from the same source as the actual override map, so every env var that
339+
// triggers an override is also attributed as 'env'-sourced. A hardcoded
340+
// second list would silently miss any var added to one but not the other.
341+
describe('marks "llm" as "env" source for every CODEGRAPH_LLM_* override var', () => {
342+
const envVars = [
343+
'CODEGRAPH_LLM_PROVIDER',
344+
'CODEGRAPH_LLM_API_KEY',
345+
'CODEGRAPH_LLM_MODEL',
346+
'CODEGRAPH_LLM_BASE_URL',
347+
];
348+
349+
afterEach(() => {
350+
for (const key of envVars) delete process.env[key];
351+
});
352+
353+
for (const key of envVars) {
354+
it(key, () => {
355+
const dir = fs.mkdtempSync(path.join(tmpDir, 'prov-env-'));
356+
process.env[key] = 'test-value';
357+
const { provenance } = loadConfigWithProvenance(dir);
358+
expect(provenance.llm).toBe('env');
359+
});
360+
}
361+
});
336362
});
337363

338364
// ── setUserConfigOverride integration ────────────────────────────────

0 commit comments

Comments
 (0)