Skip to content

Commit 846fdcd

Browse files
committed
Document catalog credential-removal convergence as rebuild-only
Every catalog rebuild derives rows from the current settings file plus the live credential stores, so removal converges on the next rebuild and manual rows are never deleted. A disabled flag has no home and no removal event to drive it.
1 parent bf79cc0 commit 846fdcd

2 files changed

Lines changed: 65 additions & 0 deletions

File tree

src/config.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2061,6 +2061,58 @@ describe("refreshLiveProviderCatalog", () => {
20612061
});
20622062
});
20632063

2064+
describe("catalog credential-removal convergence", () => {
2065+
const resolved: ResolvedProvider = {
2066+
providerName: "fp",
2067+
baseURL: "https://fp/v1",
2068+
apiKey: "fp-key",
2069+
model: "fp-large",
2070+
};
2071+
const credentialed: Settings = {
2072+
providers: {
2073+
fp: { baseURL: "https://fp/v1", apiKey: "fp-key", models: ["fp-large"] },
2074+
},
2075+
};
2076+
const credentialRemoved: Settings = {
2077+
providers: {
2078+
fp: { baseURL: "https://fp/v1", models: ["fp-large"] },
2079+
},
2080+
};
2081+
2082+
test("rebuild preserves the manual row across removal and restore", async () => {
2083+
expect(
2084+
buildProviderCatalog(credentialed, resolved).find((c) => c.name === "fp")
2085+
?.apiKey,
2086+
).toBe("fp-key");
2087+
2088+
const converged = await refreshLiveProviderCatalog(
2089+
credentialRemoved,
2090+
resolved,
2091+
);
2092+
const row = converged.find((c) => c.name === "fp");
2093+
expect(row?.models).toEqual(["fp-large"]);
2094+
expect(row?.baseURL).toBe("https://fp/v1");
2095+
expect(row?.apiKey).toBeUndefined();
2096+
2097+
const restored = await refreshLiveProviderCatalog(credentialed, resolved);
2098+
expect(restored.find((c) => c.name === "fp")?.apiKey).toBe("fp-key");
2099+
});
2100+
2101+
test("persisting the converged catalog keeps the manual row", async () => {
2102+
const converged = await refreshLiveProviderCatalog(
2103+
credentialRemoved,
2104+
resolved,
2105+
);
2106+
const persisted = providerCatalogToSettings(
2107+
converged,
2108+
undefined,
2109+
credentialRemoved,
2110+
);
2111+
expect(persisted.providers.fp?.models).toEqual(["fp-large"]);
2112+
expect(persisted.providers.fp?.baseURL).toBe("https://fp/v1");
2113+
});
2114+
});
2115+
20642116
describe("mergeProviderIntoSettings", () => {
20652117
test("preserves plugins and non-provider fields when upserting a provider", () => {
20662118
const existing: Settings = {

src/config/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1132,6 +1132,19 @@ export async function loadConfig(
11321132
// provider connect (mid-session, no restart) can rebuild the picker's
11331133
// catalog after writing new credentials, instead of only taking effect on
11341134
// the next process start.
1135+
//
1136+
// Credential-removal convergence (CL-5446) is rebuild-only: every rebuild
1137+
// derives rows from the current settings file plus the live Codex/xAI
1138+
// stores, so a provider without a credential is rebuilt without one and
1139+
// re-auth restores it on the next rebuild. Manual rows are never deleted —
1140+
// settings-file providers are re-projected verbatim — which is the
1141+
// disable-not-delete rationale: nothing the operator wrote is lost, and
1142+
// nothing stale survives past the next rebuild. A dedicated disabled flag
1143+
// was rejected: no such state exists on the catalog entry, the picker
1144+
// option, or the host boundary, and no removal event drives a refresh
1145+
// (there is no logout/disconnect surface or auth-store watcher; refresh
1146+
// runs on connect, prefetch, and startup), so removal takes effect on the
1147+
// next rebuild, not live.
11351148
function mergeOAuthCatalog(
11361149
settings: Settings | null,
11371150
resolved: ResolvedProvider,

0 commit comments

Comments
 (0)