Skip to content

Commit 2c37861

Browse files
committed
Fix refreshIndexList() to respect fixed mode allowlist
In fixed mode (when indexNames are specified via CLI), calling list_indexes would trigger refreshIndexList() which overwrote this.indexNames with ALL indexes from the store, bypassing the original allowlist. Changes: - Add readonly originalIndexNames field to MultiIndexRunner to store the original allowlist in fixed mode - In constructor, save the original indexNames if provided - In refreshIndexList(), filter results to only include indexes in the original allowlist when in fixed mode This ensures that in fixed mode (-i pytorch -i react), list_indexes only returns the originally specified indexes, even if other indexes exist in the store. In discovery mode (--discovery), all indexes are returned as before. Tests added to verify: - Discovery mode includes all indexes from store - Fixed mode respects the original allowlist - Fixed mode handles missing indexes gracefully Agent-Id: agent-cf0760a8-47e4-4742-a697-7de4bf2367e6
1 parent ba57527 commit 2c37861

File tree

2 files changed

+150
-11
lines changed

2 files changed

+150
-11
lines changed

src/clients/multi-index-runner.test.ts

Lines changed: 132 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
/**
2-
* Tests for createSourceFromState
2+
* Tests for MultiIndexRunner
33
*
4-
* These tests verify that createSourceFromState correctly uses resolvedRef
4+
* Tests for createSourceFromState verify that it correctly uses resolvedRef
55
* from state metadata when creating source instances.
66
*
7-
* We mock GitHub and Website sources to capture what config gets passed
8-
* to the constructors, without needing API credentials.
9-
*
10-
* Since all VCS sources (GitHub, GitLab, BitBucket) use the same getRef() logic,
11-
* we only test GitHub as the representative case.
7+
* Tests for MultiIndexRunner.refreshIndexList verify that it respects
8+
* the fixed mode allowlist when refreshing the index list.
129
*/
1310

1411
import { describe, it, expect, vi, beforeEach } from "vitest";
15-
import type { IndexStateSearchOnly, SourceMetadata } from "../core/types.js";
12+
import type { IndexStateSearchOnly, SourceMetadata, IndexState } from "../core/types.js";
13+
import type { IndexStoreReader } from "../stores/types.js";
14+
import { MultiIndexRunner } from "./multi-index-runner.js";
1615

1716
// Mock only the sources we actually test
1817
vi.mock("../sources/github.js", () => ({
@@ -113,3 +112,128 @@ describe("createSourceFromState", () => {
113112
);
114113
});
115114
});
115+
116+
describe("MultiIndexRunner.refreshIndexList", () => {
117+
// Helper to create a mock store with multiple indexes
118+
const createMockStoreWithIndexes = (indexNames: string[]): IndexStoreReader => {
119+
const mockState = (name: string): IndexState => ({
120+
version: 1,
121+
contextState: { version: 1 } as any,
122+
source: {
123+
type: "github",
124+
config: { owner: "test", repo: name },
125+
syncedAt: new Date().toISOString(),
126+
},
127+
});
128+
129+
return {
130+
loadState: vi.fn(),
131+
loadSearch: vi.fn().mockImplementation((name: string) => {
132+
if (indexNames.includes(name)) {
133+
return Promise.resolve(mockState(name));
134+
}
135+
return Promise.resolve(null);
136+
}),
137+
list: vi.fn().mockResolvedValue(indexNames),
138+
};
139+
};
140+
141+
it("in discovery mode, refreshIndexList includes all indexes from store", async () => {
142+
const store = createMockStoreWithIndexes(["pytorch", "react", "docs"]);
143+
144+
const runner = await MultiIndexRunner.create({
145+
store,
146+
// No indexNames = discovery mode
147+
});
148+
149+
expect(runner.indexNames).toEqual(["pytorch", "react", "docs"]);
150+
151+
// Simulate store gaining a new index
152+
(store.list as any).mockResolvedValue(["pytorch", "react", "docs", "vue"]);
153+
(store.loadSearch as any).mockImplementation((name: string) => {
154+
if (["pytorch", "react", "docs", "vue"].includes(name)) {
155+
return Promise.resolve({
156+
version: 1,
157+
contextState: { version: 1 } as any,
158+
source: {
159+
type: "github",
160+
config: { owner: "test", repo: name },
161+
syncedAt: new Date().toISOString(),
162+
},
163+
});
164+
}
165+
return Promise.resolve(null);
166+
});
167+
168+
await runner.refreshIndexList();
169+
expect(runner.indexNames).toEqual(["pytorch", "react", "docs", "vue"]);
170+
});
171+
172+
it("in fixed mode, refreshIndexList respects the original allowlist", async () => {
173+
const store = createMockStoreWithIndexes(["pytorch", "react", "docs"]);
174+
175+
// Create runner in fixed mode with only pytorch and react
176+
const runner = await MultiIndexRunner.create({
177+
store,
178+
indexNames: ["pytorch", "react"],
179+
});
180+
181+
expect(runner.indexNames).toEqual(["pytorch", "react"]);
182+
183+
// Simulate store gaining a new index (docs is already there, vue is new)
184+
(store.list as any).mockResolvedValue(["pytorch", "react", "docs", "vue"]);
185+
(store.loadSearch as any).mockImplementation((name: string) => {
186+
if (["pytorch", "react", "docs", "vue"].includes(name)) {
187+
return Promise.resolve({
188+
version: 1,
189+
contextState: { version: 1 } as any,
190+
source: {
191+
type: "github",
192+
config: { owner: "test", repo: name },
193+
syncedAt: new Date().toISOString(),
194+
},
195+
});
196+
}
197+
return Promise.resolve(null);
198+
});
199+
200+
await runner.refreshIndexList();
201+
202+
// Should still only include pytorch and react, not docs or vue
203+
expect(runner.indexNames).toEqual(["pytorch", "react"]);
204+
});
205+
206+
it("in fixed mode, refreshIndexList handles missing indexes gracefully", async () => {
207+
const store = createMockStoreWithIndexes(["pytorch", "react"]);
208+
209+
// Create runner in fixed mode with pytorch, react, and a missing index
210+
const runner = await MultiIndexRunner.create({
211+
store,
212+
indexNames: ["pytorch", "react"],
213+
});
214+
215+
expect(runner.indexNames).toEqual(["pytorch", "react"]);
216+
217+
// Simulate pytorch being deleted from store
218+
(store.list as any).mockResolvedValue(["react"]);
219+
(store.loadSearch as any).mockImplementation((name: string) => {
220+
if (name === "react") {
221+
return Promise.resolve({
222+
version: 1,
223+
contextState: { version: 1 } as any,
224+
source: {
225+
type: "github",
226+
config: { owner: "test", repo: name },
227+
syncedAt: new Date().toISOString(),
228+
},
229+
});
230+
}
231+
return Promise.resolve(null);
232+
});
233+
234+
await runner.refreshIndexList();
235+
236+
// Should only include react (pytorch is gone from store)
237+
expect(runner.indexNames).toEqual(["react"]);
238+
});
239+
});

src/clients/multi-index-runner.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export class MultiIndexRunner {
9292
private readonly searchOnly: boolean;
9393
private clientUserAgent?: string;
9494
private readonly clientCache = new Map<string, SearchClient>();
95+
private readonly originalIndexNames: string[] | undefined;
9596

9697
/** Available index names */
9798
indexNames: string[];
@@ -104,13 +105,15 @@ export class MultiIndexRunner {
104105
indexNames: string[],
105106
indexes: IndexInfo[],
106107
searchOnly: boolean,
107-
clientUserAgent?: string
108+
clientUserAgent?: string,
109+
originalIndexNames?: string[]
108110
) {
109111
this.store = store;
110112
this.indexNames = indexNames;
111113
this.indexes = indexes;
112114
this.searchOnly = searchOnly;
113115
this.clientUserAgent = clientUserAgent;
116+
this.originalIndexNames = originalIndexNames;
114117
}
115118

116119
/**
@@ -124,6 +127,9 @@ export class MultiIndexRunner {
124127
const allIndexNames = await store.list();
125128
const indexNames = config.indexNames ?? allIndexNames;
126129

130+
// In fixed mode, save the original allowlist for later filtering
131+
const originalIndexNames = config.indexNames ? [...config.indexNames] : undefined;
132+
127133
// Validate requested indexes exist
128134
const missingIndexes = indexNames.filter((n) => !allIndexNames.includes(n));
129135
if (missingIndexes.length > 0) {
@@ -153,7 +159,7 @@ export class MultiIndexRunner {
153159
}
154160

155161
// Allow empty - server can start with no indexes and user can add via CLI
156-
return new MultiIndexRunner(store, validIndexNames, indexes, searchOnly, config.clientUserAgent);
162+
return new MultiIndexRunner(store, validIndexNames, indexes, searchOnly, config.clientUserAgent, originalIndexNames);
157163
}
158164

159165
/**
@@ -200,13 +206,22 @@ export class MultiIndexRunner {
200206
/**
201207
* Refresh the list of available indexes from the store.
202208
* Call after adding or removing indexes.
209+
*
210+
* In fixed mode (when originalIndexNames is set), only includes indexes
211+
* from the original allowlist, even if other indexes exist in the store.
203212
*/
204213
async refreshIndexList(): Promise<void> {
205214
const allIndexNames = await this.store.list();
215+
216+
// In fixed mode, filter to only the original allowlist
217+
const indexNamesToLoad = this.originalIndexNames
218+
? allIndexNames.filter(name => this.originalIndexNames!.includes(name))
219+
: allIndexNames;
220+
206221
const newIndexes: IndexInfo[] = [];
207222
const newIndexNames: string[] = [];
208223

209-
for (const name of allIndexNames) {
224+
for (const name of indexNamesToLoad) {
210225
try {
211226
const state = await this.store.loadSearch(name);
212227
if (state) {

0 commit comments

Comments
 (0)