fix: resolve final review findings (wiki cache tenant isolation, meeting search pagination, ambient test)

This commit is contained in:
Lucas Winther
2026-09-07 03:12:55 +02:00
parent e0aa91e388
commit 7097d1c174
8 changed files with 353 additions and 21 deletions
+7
View File
@@ -145,6 +145,13 @@ export class OpenProjectClient {
this.authHeader = `Basic ${btoa(`apikey:${this.apiKey}`)}`;
}
/**
* Returns a unique cache key scoped to both baseUrl and apiKey for multi-tenant cache isolation.
*/
getCacheKey(): string {
return `${this.baseUrl}#${this.apiKey}`;
}
/**
* Performs an HTTP GET request to OpenProject API v3.
*/
+35 -12
View File
@@ -378,21 +378,44 @@ export async function searchMeetings(
const opClient = resolveClient(client);
const needle = params.query.toLowerCase().trim();
// Fetch candidate meetings scoped to project if specified
const candidateBatchSize = Math.max(params.pageSize ?? 50, 50);
const meetingsResult = await listMeetings(
{
projectId: params.projectId,
offset: 1,
pageSize: candidateBatchSize,
},
opClient
);
// Fetch candidate meetings scoped to project if specified, across multiple pages up to cap
const maxCandidates = 250;
const candidateBatchSize = Math.min(maxCandidates, Math.max(params.pageSize ?? 50, 50));
const candidateMeetings: MeetingSummary[] = [];
let candidateOffset = 1;
const candidateMeetings = meetingsResult.elements;
while (candidateMeetings.length < maxCandidates) {
const meetingsResult = await listMeetings(
{
projectId: params.projectId,
offset: candidateOffset,
pageSize: candidateBatchSize,
},
opClient
);
const elements = meetingsResult.elements;
if (elements.length === 0) {
break;
}
candidateMeetings.push(...elements);
// Stop if all meetings fetched or fewer elements returned than requested page size
if (
candidateMeetings.length >= meetingsResult.total ||
elements.length < candidateBatchSize
) {
break;
}
candidateOffset += 1;
}
const scopedCandidates = candidateMeetings.slice(0, maxCandidates);
// Process candidate meetings concurrently
const matchPromises = candidateMeetings.map(async (meeting) => {
const matchPromises = scopedCandidates.map(async (meeting) => {
const titleMatches = meeting.title.toLowerCase().includes(needle);
const locationMatches = Boolean(
meeting.location && meeting.location.toLowerCase().includes(needle)
+1 -1
View File
@@ -76,7 +76,7 @@ export async function getOpenApiSpec(
client?: OpenProjectClient
): Promise<unknown> {
const activeClient = resolveClient(client);
const cacheKey = activeClient.baseUrl;
const cacheKey = activeClient.getCacheKey?.() ?? activeClient.baseUrl;
if (options?.refresh) {
openApiCache.delete(cacheKey);
+8 -8
View File
@@ -63,16 +63,16 @@ export interface SearchWikiPagesParams {
refreshCache?: boolean;
}
// In-memory cache for discovered wiki pages keyed by client baseUrl
// In-memory cache for discovered wiki pages keyed by client cache key
const wikiPagesCache = new Map<string, Map<number, WikiPageDetail>>();
const discoveredBaseUrls = new Set<string>();
const discoveredCacheKeys = new Set<string>();
/**
* Clears the in-memory wiki discovery cache.
*/
export function clearWikiCache(): void {
wikiPagesCache.clear();
discoveredBaseUrls.clear();
discoveredCacheKeys.clear();
}
/**
@@ -209,8 +209,8 @@ export async function getWikiPage(
const detail = normalizeWikiPageDetail(pageResource, attachments);
// Update in-memory cache for this client baseUrl if initialized
const cacheKey = opClient.baseUrl || "default";
// Update in-memory cache for this client cache key if initialized
const cacheKey = opClient.getCacheKey?.() ?? (opClient.baseUrl || "default");
let cacheMap = wikiPagesCache.get(cacheKey);
if (!cacheMap) {
cacheMap = new Map<number, WikiPageDetail>();
@@ -259,10 +259,10 @@ export async function searchWikiPages(
client?: OpenProjectClient
): Promise<WikiPageSummary[]> {
const opClient = resolveClient(client);
const cacheKey = opClient.baseUrl || "default";
const cacheKey = opClient.getCacheKey?.() ?? (opClient.baseUrl || "default");
let cacheMap = wikiPagesCache.get(cacheKey);
const needsDiscovery = !cacheMap || !discoveredBaseUrls.has(cacheKey) || params?.refreshCache;
const needsDiscovery = !cacheMap || !discoveredCacheKeys.has(cacheKey) || params?.refreshCache;
if (params?.refreshCache && cacheMap) {
cacheMap.clear();
@@ -355,7 +355,7 @@ export async function searchWikiPages(
}
}
discoveredBaseUrls.add(cacheKey);
discoveredCacheKeys.add(cacheKey);
}
// 3. Resolve target project ID if provided