mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-18 15:50:11 +02:00
fix(brain-cache): loadMeta tolerates malformed _meta.json without crashing (#1879)
loadMeta returned the parsed JSON verbatim. A valid JSON file that lacked the last_refresh map made three consumers (isStale, cmdInvalidate, refreshEntity) throw a TypeError dereferencing meta.last_refresh — the sibling last_attempt was already guarded, last_refresh wasn't. Fix in loadMeta: - Shape-guard: JSON.parse can return null/array/string/number; non-object → fresh meta. - Normalize ONLY the dereferenced maps (last_refresh, last_attempt). - Deliberately do NOT default schema_version/endpoint_hash. Leaving them absent makes schemaVersionMismatch()/endpointSwitched() force a rebuild (missing identity = mismatch = safe); defaulting them would suppress cache invalidation and trust a stale file of unknown provenance. Tests: missing last_refresh no longer throws; null/array/primitive treated as cold; missing schema_version forces rebuild instead of a trusted warm hit. Reported by @jbetala7. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -86,6 +86,41 @@ describe('brain-cache meta lifecycle', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('brain-cache malformed _meta.json (#1879)', () => {
|
||||
function seedMeta(content: string): void {
|
||||
const cacheDir = join(TMP_HOME, 'projects', 'helsinki', 'brain-cache');
|
||||
mkdirSync(cacheDir, { recursive: true });
|
||||
writeFileSync(join(cacheDir, '_meta.json'), content);
|
||||
}
|
||||
|
||||
test('cmdInvalidate does not throw when last_refresh is missing', async () => {
|
||||
const mod = await importCache();
|
||||
// Valid JSON object, but no last_refresh map — the original crash.
|
||||
seedMeta(JSON.stringify({ schema_version: '0.0.1', endpoint_hash: 'x' }));
|
||||
expect(() => mod.cmdInvalidate('product', 'helsinki')).not.toThrow();
|
||||
});
|
||||
|
||||
test('cmdGet does not throw on null / array / primitive _meta.json', async () => {
|
||||
const mod = await importCache();
|
||||
for (const bad of ['null', '[]', '"a string"', '42']) {
|
||||
seedMeta(bad);
|
||||
expect(() => mod.cmdGet('product', 'helsinki')).not.toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
test('missing schema_version is treated as a mismatch (forces rebuild, not trust)', async () => {
|
||||
const mod = await importCache();
|
||||
const cacheDir = join(TMP_HOME, 'projects', 'helsinki', 'brain-cache');
|
||||
mkdirSync(cacheDir, { recursive: true });
|
||||
writeFileSync(join(cacheDir, 'product.md'), '# stale-no-schema\n');
|
||||
// No schema_version field — must NOT be trusted as a warm hit.
|
||||
seedMeta(JSON.stringify({ endpoint_hash: mod.detectEndpointHash(), last_refresh: { product: Date.now() } }));
|
||||
const result = mod.cmdGet('product', 'helsinki');
|
||||
// Brain unreachable in test → rebuild path runs; must not be a trusted warm hit.
|
||||
expect(['missing', 'cold-refreshed', 'stale-fallback']).toContain(result.state);
|
||||
});
|
||||
});
|
||||
|
||||
describe('brain-cache endpoint detection', () => {
|
||||
test('detectEndpointHash returns "local" when no ~/.claude.json gbrain MCP', async () => {
|
||||
// We don't write ~/.claude.json in the temp env, so this falls through to local.
|
||||
|
||||
Reference in New Issue
Block a user