Files
gstack/test/brain-cache-spec.test.ts
T
garrytan 6dc624eda6 test: delete test-infrastructure dead code (G)
- exit-propagation drives the runner's real strict verdict
  (BunTestOutputClassifier + strictTestExitCode); delete the unused
  shardRunLooksTruncated predicate.
- delete skill-coverage-matrix registry + its gate (nothing reads it; the
  floor already iterates skillCensus()).
- delete touchfiles-facade export-parity tests (Bun fails missing imports
  at link time) and the duplicated E2E_TIERS tier-value test.
- delete brain-cache-spec TRANSPORT_DEFAULT_POLICY, SKILL_RUN_RETENTION_DAYS
  and the now-unused BrainTrustPolicy type with their literal tests.
  AUTOPLAN_PREFLIGHT_BUDGET_BYTES stays: skill-preflight-budget enforces it
  against real resolver output.
- delete audit-compliance's JSDoc-comment grep.
2026-09-29 04:35:13 +00:00

211 lines
8.9 KiB
TypeScript

/**
* Brain cache spec internal-consistency invariants (T14 / D2).
*
* Asserts that scripts/brain-cache-spec.ts is self-consistent:
* - Every skill's subset only references entities that exist.
* - Per-skill budget cap is achievable given per-entity caps.
* - Cross-project entities are clearly distinguished from per-project.
* - Invalidation graph has no dangling skill references.
* - Helper functions throw on unknown names (defensive).
*
* Gate-tier, free, pure import + assertion. Runs in <100ms.
*/
import { describe, test, expect, afterAll } from 'bun:test';
import { mkdtempSync, writeFileSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import {
BRAIN_CACHE_ENTITIES,
SKILL_DIGEST_SUBSETS,
SKILL_PREFLIGHT_BUDGET_BYTES,
AUTOPLAN_PREFLIGHT_BUDGET_BYTES,
SALIENCE_DEFAULT_ALLOWLIST,
SKILL_CALIBRATION_WEIGHTS,
USER_SLUG_RESOLUTION_ORDER,
GSTACK_SCHEMA_PACK_NAME,
GSTACK_SCHEMA_PACK_VERSION,
CACHE_REFRESH_LOCK_TIMEOUT_MS,
getCacheFile,
getSkillSubset,
getSkillBudget,
getInvalidationTargets,
getPreflightSkills,
getMaxSubsetBytes,
} from '../scripts/brain-cache-spec';
describe('brain-cache-spec internal consistency', () => {
test('every skill subset references only known entities', () => {
const entityNames = new Set(Object.keys(BRAIN_CACHE_ENTITIES));
for (const [skill, subset] of Object.entries(SKILL_DIGEST_SUBSETS)) {
for (const name of subset) {
expect(entityNames.has(name)).toBe(true);
}
}
});
test('every skill with a subset has a budget', () => {
for (const skill of Object.keys(SKILL_DIGEST_SUBSETS)) {
expect(SKILL_PREFLIGHT_BUDGET_BYTES[skill]).toBeGreaterThan(0);
}
});
test('per-skill budget is achievable given per-entity budgets', () => {
// Per-entity budgets are hard ceilings on each digest's own file size.
// Per-skill budget is enforced by the compressor on the SUM injected into
// the skill's preflight context — the same entity may be sampled (top-N)
// rather than verbatim. So sum may legitimately exceed skill budget; the
// compressor trims at write time. We allow up to 3x as a sanity ceiling
// (caught test/skill-preflight-budget.test.ts enforces the real cap).
for (const skill of Object.keys(SKILL_DIGEST_SUBSETS)) {
const maxBytes = getMaxSubsetBytes(skill);
const skillBudget = getSkillBudget(skill);
expect(maxBytes).toBeLessThanOrEqual(skillBudget * 3);
}
});
test('autoplan total budget covers the 4 plan-* skills (excluding office-hours)', () => {
const autoplanSkills = ['plan-ceo-review', 'plan-eng-review', 'plan-design-review', 'plan-devex-review'];
const sum = autoplanSkills.reduce((acc, s) => acc + getSkillBudget(s), 0);
expect(sum).toBeLessThanOrEqual(AUTOPLAN_PREFLIGHT_BUDGET_BYTES);
});
test('every entity has a positive TTL and a positive budget', () => {
for (const [name, entity] of Object.entries(BRAIN_CACHE_ENTITIES)) {
expect(entity.ttl_ms).toBeGreaterThan(0);
expect(entity.budget_bytes).toBeGreaterThan(0);
expect(entity.file).toMatch(/\.md$/);
expect(['cross-project', 'per-project']).toContain(entity.scope);
}
});
test('user-profile is the only cross-project entity', () => {
const crossProject = Object.entries(BRAIN_CACHE_ENTITIES)
.filter(([_, e]) => e.scope === 'cross-project')
.map(([n]) => n);
expect(crossProject).toEqual(['user-profile']);
});
test('salience entity has shortest TTL (changes hourly)', () => {
const ttls = Object.values(BRAIN_CACHE_ENTITIES).map((e) => e.ttl_ms);
expect(BRAIN_CACHE_ENTITIES.salience.ttl_ms).toBe(Math.min(...ttls));
});
test('salience allowlist has sane defaults (no personal/family/therapy)', () => {
const blocked = ['personal/', 'family/', 'therapy/', 'reflection'];
for (const prefix of blocked) {
expect(SALIENCE_DEFAULT_ALLOWLIST.some((p) => p.startsWith(prefix))).toBe(false);
}
// Must contain at least projects/ + gstack/ (work-flow surfaces)
expect(SALIENCE_DEFAULT_ALLOWLIST).toContain('projects/');
expect(SALIENCE_DEFAULT_ALLOWLIST).toContain('gstack/');
});
test('calibration weights are bounded 0-1 and present for all preflight skills', () => {
for (const skill of getPreflightSkills()) {
const weight = SKILL_CALIBRATION_WEIGHTS[skill];
expect(weight).toBeGreaterThan(0);
expect(weight).toBeLessThanOrEqual(1);
}
});
test('user-slug resolution chain has 4 deterministic fallbacks ending in non-empty', () => {
expect(USER_SLUG_RESOLUTION_ORDER.length).toBe(4);
expect(USER_SLUG_RESOLUTION_ORDER[USER_SLUG_RESOLUTION_ORDER.length - 1]).toBe('anonymous_hostname_sha8');
});
test('schema pack identity is stable strings', () => {
expect(GSTACK_SCHEMA_PACK_NAME).toBe('gstack-core');
expect(GSTACK_SCHEMA_PACK_VERSION).toMatch(/^\d+\.\d+\.\d+$/);
});
test('refresh lock timeout matches /sync-gbrain convention (5 min)', () => {
expect(CACHE_REFRESH_LOCK_TIMEOUT_MS).toBe(5 * 60_000);
});
test('invalidation graph: every "skill-run-write" target also depends on it', () => {
// recent-decisions invalidates on skill-run-write — verify the contract holds
const targets = getInvalidationTargets('skill-run-write');
expect(targets).toContain('recent-decisions');
});
test('invalidation graph: /plan-ceo-review invalidates product + goals + recent-decisions chain', () => {
const targets = getInvalidationTargets('/plan-ceo-review');
expect(targets).toContain('product');
expect(targets).toContain('goals');
});
test('helpers throw on unknown names (defensive)', () => {
expect(() => getCacheFile('nonsense-entity')).toThrow();
expect(() => getSkillSubset('not-a-skill')).toThrow();
expect(() => getSkillBudget('not-a-skill')).toThrow();
});
test('helpers return correct values for known names', () => {
expect(getCacheFile('product')).toBe('product.md');
expect(getSkillSubset('plan-eng-review')).toEqual(['product', 'recent-decisions']);
expect(getSkillBudget('office-hours')).toBe(5120);
});
test('all 5 preflight skills are real planning-skill names', () => {
const expected = ['office-hours', 'plan-ceo-review', 'plan-eng-review', 'plan-design-review', 'plan-devex-review'];
expect(getPreflightSkills().sort()).toEqual(expected.sort());
});
});
describe('brain-cache MCP scope precedence (C15 pin)', () => {
// Claude Code resolves a same-name MCP conflict in favor of the
// PROJECT-LOCAL entry (.projects[cwd].mcpServers) over the user-scope
// entry (.mcpServers). Verified empirically against claude 2.1.233 with a
// hermetic fake $HOME: `claude mcp get gbrain` reported "Scope: Local
// config" and the project-local URL when both scopes defined gbrain.
// detectEndpointHash must hash the endpoint the project actually talks
// to, or a brain switch would never invalidate the cache.
const TMP = mkdtempSync(join(tmpdir(), 'brain-cache-precedence-'));
afterAll(() => rmSync(TMP, { recursive: true, force: true }));
const cache = () => import('../bin/gstack-brain-cache');
const writeFixture = (name: string, cfg: object): string => {
const p = join(TMP, name);
writeFileSync(p, JSON.stringify(cfg));
return p;
};
const USER_URL = { type: 'http', url: 'https://user.example/mcp' };
const PROJ_URL = { type: 'http', url: 'https://proj.example/mcp' };
test('project-local gbrain entry beats user scope for a cwd inside the project', async () => {
const mod = await cache();
const conflict = writeFixture('claude-conflict.json', {
mcpServers: { gbrain: USER_URL },
projects: { '/w/repo': { mcpServers: { gbrain: PROJ_URL } } },
});
const conflictHash = mod.detectEndpointHash(conflict, '/w/repo/src');
// Same hash as the project entry alone → the project-local entry won.
const projOnly = writeFixture('claude-proj-only.json', {
projects: { '/w/repo': { mcpServers: { gbrain: PROJ_URL } } },
});
expect(conflictHash).toBe(mod.detectEndpointHash(projOnly, '/w/repo/src'));
// And NOT the user entry's hash.
const userOnly = writeFixture('claude-user-only.json', {
mcpServers: { gbrain: USER_URL },
});
expect(conflictHash).not.toBe(mod.detectEndpointHash(userOnly, '/w/repo/src'));
});
test('user scope still resolves when the cwd has no project-local entry', async () => {
const mod = await cache();
const cj = writeFixture('claude-user-fallback.json', {
mcpServers: { gbrain: USER_URL },
projects: { '/other/repo': { mcpServers: { gbrain: PROJ_URL } } },
});
const hash = mod.detectEndpointHash(cj, '/w/unrelated');
expect(hash).toHaveLength(8);
// Matches the user-only hash — the OTHER project's entry is invisible
// outside its own tree.
const userOnly = writeFixture('claude-user-only-2.json', {
mcpServers: { gbrain: USER_URL },
});
expect(hash).toBe(mod.detectEndpointHash(userOnly, '/w/unrelated'));
});
});