fix: provider-scoped snapshot refresh with self-healing invalidation

This commit is contained in:
Mohamed Boudra
2026-04-21 13:14:59 +07:00
parent b01e274cce
commit 3832053736
6 changed files with 236 additions and 31 deletions
@@ -588,6 +588,50 @@ describe("ProviderSnapshotManager", () => {
manager.destroy();
});
test("settings refresh invalidation self-heals workspace snapshots through the next read without force", async () => {
const fetchModels = vi
.fn<(cwd: string, force: boolean) => Promise<AgentModelDefinition[]>>()
.mockImplementation(async (cwd) => [createModel("codex", cwd)]);
const { registry } = createRegistry([
createMockProvider({
provider: "codex",
fetchModels: async (cwd, force) => fetchModels(cwd, force),
fetchModes: async () => [createMode("auto")],
}),
]);
const manager = new ProviderSnapshotManager(registry, createTestLogger());
manager.getSnapshot(projectCwd);
await vi.waitFor(() => {
expect(getProviderEntry(manager.getSnapshot(projectCwd), "codex")?.status).toBe("ready");
});
await manager.refreshSettingsSnapshot({ providers: ["codex"] });
expect(fetchModels.mock.calls.map(([cwd]) => cwd)).toEqual([projectCwd, homedir()]);
expect(fetchModels.mock.calls.map(([, force]) => force)).toEqual([false, true]);
const invalidatedSnapshot = manager.getSnapshot(projectCwd);
expect(getProviderEntry(invalidatedSnapshot, "codex")).toMatchObject({
provider: "codex",
status: "loading",
});
await vi.waitFor(() => {
expect(fetchModels).toHaveBeenCalledTimes(3);
});
expect(fetchModels.mock.calls[2]).toEqual([projectCwd, false]);
await vi.waitFor(() => {
expect(getProviderEntry(manager.getSnapshot(projectCwd), "codex")?.status).toBe("ready");
});
manager.destroy();
});
test("refresh marks a slow provider as error after the timeout", async () => {
const fetchModels = deferred<AgentModelDefinition[]>();
const { registry } = createRegistry([
@@ -63,6 +63,13 @@ export class ProviderSnapshotManager {
this.resetSnapshotToLoading(resolvedCwd, missingProviders);
void this.warmUp(resolvedCwd, missingProviders);
}
const providerLoads = this.providerLoads.get(resolvedCwd);
const loadingProviders = Array.from(entries.values())
.filter((entry) => entry.status === "loading" && !providerLoads?.has(entry.provider))
.map((entry) => entry.provider);
if (loadingProviders.length > 0) {
void this.warmUp(resolvedCwd, loadingProviders);
}
if (this.shouldRevalidate(resolvedCwd)) {
void this.warmUp(resolvedCwd);
}
@@ -392,10 +399,10 @@ export class ProviderSnapshotManager {
}
if (!options.providers) {
this.snapshots.delete(cwd);
this.resetSnapshotToLoading(cwd);
this.lastCheckedAts.delete(cwd);
this.providerLoads.delete(cwd);
this.events.emit("change", [], cwd);
this.emitChange(cwd);
continue;
}
@@ -405,9 +412,10 @@ export class ProviderSnapshotManager {
}
let changed = false;
for (const provider of options.providers) {
changed = snapshot.delete(provider) || changed;
changed = snapshot.has(provider) || changed;
this.providerLoads.get(cwd)?.delete(provider);
}
this.resetSnapshotToLoading(cwd, options.providers);
if (this.providerLoads.get(cwd)?.size === 0) {
this.providerLoads.delete(cwd);
}