Files
gbrain/test/cache-scope-key.test.ts
Garry TanandClaude Fable 5 3cfd0c4b42 fix(search): keep semantic cache alive for the query op (expandFn folded into knobs hash, not unsafe)
isSemanticCacheRequestSafe listed expandFn as cache-unsafe, but
operations.ts passes expandFn on every default `query` op call
(expand = p.expand !== false), so the semantic cache was silently
disabled for the flagship op (permanent 'disabled' status, 0% hit
rate).

expandFn's result-shaping effect is exactly the expansion arm, so fold
EFFECTIVE expansion into the cache-key resolver instead:
hybridSearchCached now threads `expandFn ? expansion : false` into
resolvedForCache.perCall (the inner search can only expand when
expansionAllowed AND expandFn is wired), and expandFn leaves the
unsafe list. No-expandFn callers under an expansion-on bundle key as
expansion=false, matching what actually ran — the cross-serve gap the
unsafe listing was defending against stays closed.

Pinned by a query-op-shaped miss→hit roundtrip in
test/search/hybrid-reranker-integration.serial.test.ts (verified
failing with 'disabled' before this commit) and a unit case in
test/cache-scope-key.test.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 11:32:50 -07:00

80 lines
3.3 KiB
TypeScript

/**
* Query-cache scope key (federation hardening).
*
* A federated search reads a different graph than a single-source one, so
* the semantic cache must key them apart. `cacheScopeKey` produces an
* order-independent key for federated scopes and keeps unscoped all-source
* reads distinct from every scalar source key.
*/
import { describe, test, expect } from 'bun:test';
import { cacheScopeKey, isSemanticCacheRequestSafe } from '../src/core/search/hybrid.ts';
import type { HybridSearchOpts } from '../src/core/search/hybrid.ts';
describe('cacheScopeKey', () => {
test('unscoped uses a typed key and never collides with scalar default', () => {
expect(cacheScopeKey(undefined)).toBe('["all"]');
expect(cacheScopeKey({})).toBe('["all"]');
expect(cacheScopeKey({})).not.toBe(cacheScopeKey({ sourceId: 'default' }));
});
test('scalar sourceId uses a typed key', () => {
expect(cacheScopeKey({ sourceId: 'host' })).toBe('["scalar","host"]');
});
test('federated sourceIds → order-independent set key', () => {
const k1 = cacheScopeKey({ sourceIds: ['team-b', 'team-a', 'host'] });
const k2 = cacheScopeKey({ sourceIds: ['host', 'team-a', 'team-b'] });
expect(k1).toBe(k2); // order does not matter
expect(k1).toBe('["set","host","team-a","team-b"]');
});
test('different source-sets do NOT share a key', () => {
const a = cacheScopeKey({ sourceIds: ['host', 'team-a'] });
const b = cacheScopeKey({ sourceIds: ['host', 'team-b'] });
expect(a).not.toBe(b);
});
test('federated set key is distinct from any single scalar key', () => {
const set = cacheScopeKey({ sourceIds: ['host'] });
const scalar = cacheScopeKey({ sourceId: 'host' });
expect(set).not.toBe(scalar); // a 1-element set still cannot serve a scalar read
});
test('rejects forged scalar/set encodings and invalid federated ids', () => {
expect(() => cacheScopeKey({ sourceId: '__set__:a,b' })).toThrow('Invalid source_id');
expect(() => cacheScopeKey({ sourceIds: ['a', '__all__'] })).toThrow('Invalid source_id');
});
});
describe('isSemanticCacheRequestSafe', () => {
test('accepts the standard cache-safe request shape', () => {
expect(isSemanticCacheRequestSafe({ limit: 20, sourceId: 'default' })).toBe(true);
});
test('accepts the production query-op shape (expandFn is folded into the knobs hash, not unsafe)', () => {
// operations.ts always passes expandFn on the default `query` op path;
// listing it unsafe would permanently disable the semantic cache for the
// flagship op. Its effect is keyed via the expansion bit instead
// (hybridSearchCached folds `expandFn ? expansion : false` into the hash).
expect(isSemanticCacheRequestSafe({
limit: 20,
offset: 0,
expansion: true,
expandFn: async (q: string) => [q],
sourceId: 'default',
})).toBe(true);
});
const unsafeRequests: HybridSearchOpts[] = [
{ offset: 10 }, { detail: 'high' }, { language: 'typescript' },
{ symbolKind: 'function' }, { types: ['note'] }, { since: '7d' },
{ until: '2026-07-18' }, { salience: 'on' }, { recency: 'strong' },
{ crossModal: 'both' }, { exclude_slugs: ['private/page'] },
];
test.each(unsafeRequests)('rejects unsupported result-shaping request %#', (opts) => {
expect(isSemanticCacheRequestSafe(opts)).toBe(false);
});
});