Files
gbrain/test/e2e/cli-source-scoping-pglite.test.ts
T
Garry TanandClaude Opus 4.7 39a1a2e091 fix(v0.34 W0b): flip CLI source-scoping default to truly source-scoped
Codex outside-voice review (finding #7) caught that the v0.20.0
docstring claim "by default we only match the caller's source_id"
contradicted the implementation in code-callers.ts:54 + code-callees.ts:43:

  allSources: allSources || !sourceId

The right side made `allSources` TRUE whenever `--source` was omitted,
INVERTING the documented default. Multi-source brains silently cross-
contaminated structural retrieval; `gbrain code-callers parseMarkdown`
on a brain with two repos returned callers from both even though the
docstring promised per-source scoping.

Fix:
* New canonical helper `resolveDefaultSource(engine)` in sources-ops.ts.
  Contract per eng review D7:
    - exactly 1 source registered → return its id (single-source brains,
      the 80% case; --source flag is unnecessary friction there)
    - 2+ sources → throw SourceResolutionError(multiple_sources_ambiguous)
      with the list of valid ids
    - 0 sources → throw SourceResolutionError(no_sources)
* code-callers.ts + code-callees.ts now resolve to the default source
  when both --source AND --all-sources are absent. To get the pre-v0.34
  cross-source behavior, callers must pass --all-sources explicitly.
* Same hint text on both commands. Pinned by test/e2e/cli-source-scoping-pglite.test.ts.

IRON RULE regression R2: docstring promise now holds. Multi-source brain
running `gbrain code-callers <symbol>` without --source gets a clear
error listing valid source ids instead of silent cross-resolution.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 12:29:23 -07:00

99 lines
3.6 KiB
TypeScript

/**
* v0.34 W0b — CLI source-scoping default regression test.
*
* Pre-v0.34 (Codex finding #7): code-callers.ts:54 + code-callees.ts:43
* set `allSources: allSources || !sourceId`. Default behavior with the
* `--source` flag omitted was GLOBAL cross-source resolution — the
* opposite of what the docstring promised. Multi-source brains silently
* cross-resolved `Admin::UsersController#render` between repos.
*
* IRON RULE regression R2: existing caller without `--source` flag now
* defaults to source-scoped behavior. To get the pre-v0.34 cross-source
* default, the caller must pass `--all-sources` explicitly.
*
* This test bypasses CLI argv parsing and drives the resolveDefaultSource
* helper + the engine.getCallersOf path directly. The CLI handlers thread
* resolveDefaultSource() output to engine.getCallersOf, so this E2E pins
* the same contract end-to-end.
*
* PGLite in-memory, no DATABASE_URL.
*/
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { PGLiteEngine } from '../../src/core/pglite-engine.ts';
import { resolveDefaultSource, SourceResolutionError } from '../../src/core/sources-ops.ts';
import { resetPgliteState } from '../helpers/reset-pglite.ts';
let engine: PGLiteEngine;
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
});
afterAll(async () => {
await engine.disconnect();
});
describe('v0.34 W0b — resolveDefaultSource resolution rule', () => {
test('single-source brain: returns the only source id', async () => {
await resetPgliteState(engine);
// After reset, the default 'default' source from schema bootstrap is
// the only one present. resolveDefaultSource returns it.
const id = await resolveDefaultSource(engine);
expect(id).toBe('default');
});
test('multi-source brain: throws with the list of valid ids', async () => {
await resetPgliteState(engine);
await engine.executeRaw(
`INSERT INTO sources (id, name, local_path, config, created_at)
VALUES ('repo-a', 'repo-a', '/fake/a', '{}'::jsonb, NOW())
ON CONFLICT (id) DO NOTHING`,
[],
);
await engine.executeRaw(
`INSERT INTO sources (id, name, local_path, config, created_at)
VALUES ('repo-b', 'repo-b', '/fake/b', '{}'::jsonb, NOW())
ON CONFLICT (id) DO NOTHING`,
[],
);
let caught: unknown = null;
try {
await resolveDefaultSource(engine);
} catch (e) {
caught = e;
}
expect(caught).toBeInstanceOf(SourceResolutionError);
if (caught instanceof SourceResolutionError) {
expect(caught.code).toBe('multiple_sources_ambiguous');
expect(caught.availableSources).toContain('default');
expect(caught.availableSources).toContain('repo-a');
expect(caught.availableSources).toContain('repo-b');
expect(caught.message).toContain('--source');
}
});
test('zero-sources brain: throws no_sources code', async () => {
await resetPgliteState(engine);
// resetPgliteState preserves the 'default' source from the schema
// bootstrap. Delete it to simulate a brain with no registered sources.
// FK from pages.source_id is ON DELETE CASCADE; resetPgliteState
// truncates pages first so this delete is safe.
await engine.executeRaw(`DELETE FROM sources WHERE id = 'default'`, []);
let caught: unknown = null;
try {
await resolveDefaultSource(engine);
} catch (e) {
caught = e;
}
expect(caught).toBeInstanceOf(SourceResolutionError);
if (caught instanceof SourceResolutionError) {
expect(caught.code).toBe('no_sources');
}
});
});