Skip to content

Commit fc837be

Browse files
committed
fix(api): scope agent_cache key_prefix invalidate to cache namespace
bugbot HIGH: applyAgentInvalidate's key_prefix branch built a SCAN pattern as `<filter_value>*` without prepending `<cache.name>:`, unlike the tool/session branches. This let a key_prefix invalidation match keys belonging to other caches or unrelated application data on the same Valkey instance. Scope the pattern to the cache namespace and add a regression test.
1 parent db14ecd commit fc837be

2 files changed

Lines changed: 25 additions & 3 deletions

File tree

apps/api/src/cache-proposals/__tests__/cache-apply.dispatcher.spec.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@ class FakeClient {
2020
return keys.length;
2121
}
2222

23-
async scan(cursor: string, _match: string, _pattern: string, _count: string, _n: number): Promise<[string, string[]]> {
24-
return [cursor === '0' ? '0' : '0', []];
23+
public scanCalls: string[] = [];
24+
public scanResults: string[] = [];
25+
26+
async scan(_cursor: string, _match: string, pattern: string, _count: string, _n: number): Promise<[string, string[]]> {
27+
this.scanCalls.push(pattern);
28+
return ['0', this.scanResults];
2529
}
2630

2731
public ftSearchResponse: unknown = [0];
@@ -153,6 +157,24 @@ describe('CacheApplyDispatcher', () => {
153157
expect(out.actualAffected).toBe(3);
154158
});
155159

160+
it('agent invalidate by key_prefix scopes the SCAN pattern to the cache namespace', async () => {
161+
const client = new FakeClient();
162+
const dispatcher = buildDispatcher(AGENT_CACHE, client);
163+
await dispatcher.dispatch(
164+
proposal({
165+
cache_name: 'ac:prod',
166+
cache_type: 'agent_cache',
167+
proposal_type: 'invalidate',
168+
proposal_payload: {
169+
filter_kind: 'key_prefix',
170+
filter_value: 'memo:',
171+
estimated_affected: 5,
172+
},
173+
}),
174+
);
175+
expect(client.scanCalls).toEqual(['ac:prod:memo:*']);
176+
});
177+
156178
it('fails when cache type changed since proposal creation', async () => {
157179
const client = new FakeClient();
158180
const dispatcher = buildDispatcher(AGENT_CACHE, client);

apps/api/src/cache-proposals/cache-apply.dispatcher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ export class CacheApplyDispatcher {
208208
};
209209
}
210210
if (payload.filter_kind === 'key_prefix') {
211-
const pattern = `${escapeGlob(payload.filter_value)}*`;
211+
const pattern = `${escapeGlob(cache.name)}:${escapeGlob(payload.filter_value)}*`;
212212
const deleted = await scanAndDelete(client, pattern);
213213
return {
214214
actualAffected: deleted,

0 commit comments

Comments
 (0)