Files
gbrain/test/sync-resolveSourceId-unconditional-regression.test.ts
triton6564685 e5d94f63d9 feat(v0.18.0 Step 5): thread source_id through write path
Closes the in-code Step 5 TODO at postgres-engine.ts:131 + pglite-engine.ts:127.

- types.ts: PageInput.source_id?: string
- {postgres,pglite}-engine.ts putPage: accept source_id, INSERT explicit col when set
- {postgres,pglite}-engine.ts getTags/addTag/removeTag/upsertChunks/deleteChunks/
  createVersion: optional sourceId param scopes slug->page_id lookup (avoids
  subquery uniqueness violations on multi-source same-slug)
- engine.ts interface: matching optional sourceId params
- import-file.ts importFromContent/importFromFile: opts.sourceId, source-aware
  idempotency check, threads through entire transaction
- import.ts runImport: opts.sourceId
- sync.ts: thread opts.sourceId through 3 importFile call sites + unconditional
  resolveSourceId with pre-v0.17 backward-compat safety net (drop literal
  'default' to undefined when no explicit/env signal)
- operations.ts put_page handler: resolveSourceId chain, source_id param schema

Tests:
- test/multi-source-write-path.test.ts (new): putPage explicit/implicit, ON
  CONFLICT upsert, cross-source same-slug isolation, importFromContent
  threading, content_hash idempotency source-aware
- test/sync-resolveSourceId-unconditional-regression.test.ts (new, CRITICAL
  REGRESSION): pre-v0.17 brain backwards-compat, dotfile/cwd-prefix branches
  fire, sync.ts safety-net rule

bun test: 2182 pass / 0 fail / 250 skip (E2E DATABASE_URL gated). Baseline
preserved.

Strategy: full fork-local (no upstream PR sent), per /plan-eng-review T1
outside-voice tension reconsidered post-impl. Engine-method source-aware
expansion was discovered mid-impl when cross-source same-slug tests hit
SQL state 21000 (subquery uniqueness violation) on slug-only methods.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:10:44 +08:00

180 lines
6.9 KiB
TypeScript

/**
* v0.18.0 Step 5 — REGRESSION test for sync.ts:549 unconditional resolveSourceId.
*
* IRON-RULE regression coverage. Pre-Step-5, sync.ts only invoked
* resolveSourceId when --source or GBRAIN_SOURCE was set. The dotfile and
* cwd-prefix branches of resolveSourceId were therefore dead in practice
* for `gbrain sync` (alive only for direct `gbrain put` and similar).
*
* Step 5 lifts that guard so dotfile + cwd-prefix fire for plain
* `gbrain sync`. The risk: pre-v0.17 brains (no sources.default config,
* no .gbrain-source dotfile, no flag, no env) MUST still flow through the
* legacy global-config sync path with `sourceId = undefined`. If we naively
* pass the resolver's literal 'default' fallback through, the per-source
* anchor on the 'default' row gets read instead of the legacy
* sync.repo_path/last_commit config — which is NULL for never-migrated
* brains and breaks sync silently.
*
* The safety net in sync.ts:
*
* let sourceId = await resolveSourceId(engine, explicitSource);
* if (!explicitSource && !process.env.GBRAIN_SOURCE && sourceId === 'default') {
* sourceId = undefined;
* }
*
* This file verifies (a) the resolver chain returns the expected id under
* each input scenario, and (b) the safety-net rule preserves backward compat.
*/
import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import { resolveSourceId } from '../src/core/source-resolver.ts';
let engine: PGLiteEngine;
let tmpRoot: string;
let originalEnv: string | undefined;
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({ type: 'pglite' } as never);
await engine.initSchema();
tmpRoot = mkdtempSync(join(tmpdir(), 'gbrain-step5-regr-'));
// Register two sources with concrete local_paths so cwd-prefix matches
// are testable. memory-dashboard owns ${tmpRoot}/proj-mem; stock-dashboard
// owns ${tmpRoot}/proj-stock.
mkdirSync(join(tmpRoot, 'proj-mem'), { recursive: true });
mkdirSync(join(tmpRoot, 'proj-stock'), { recursive: true });
await engine.executeRaw(
`INSERT INTO sources (id, name, local_path, config) VALUES
('memory-dashboard', 'memory-dashboard', $1, '{"federated": true}'::jsonb),
('stock-dashboard', 'stock-dashboard', $2, '{"federated": true}'::jsonb)
ON CONFLICT (id) DO UPDATE SET local_path = EXCLUDED.local_path`,
[join(tmpRoot, 'proj-mem'), join(tmpRoot, 'proj-stock')],
);
});
afterAll(async () => {
await engine.disconnect();
rmSync(tmpRoot, { recursive: true, force: true });
});
beforeEach(() => {
originalEnv = process.env.GBRAIN_SOURCE;
delete process.env.GBRAIN_SOURCE;
});
afterAll(() => {
if (originalEnv !== undefined) process.env.GBRAIN_SOURCE = originalEnv;
});
describe('Resolver chain — unconditional invocation outcomes', () => {
test('pre-v0.17 brain shape: no flag, no env, no dotfile, no cwd-prefix → returns literal default', async () => {
// CWD outside any registered source's local_path; no dotfile in tree.
const isolatedCwd = mkdtempSync(join(tmpdir(), 'gbrain-isolated-'));
try {
const result = await resolveSourceId(engine, null, isolatedCwd);
expect(result).toBe('default');
} finally {
rmSync(isolatedCwd, { recursive: true, force: true });
}
});
test('cwd inside registered source local_path → resolves to that source', async () => {
const cwd = join(tmpRoot, 'proj-mem', 'subdir');
mkdirSync(cwd, { recursive: true });
const result = await resolveSourceId(engine, null, cwd);
expect(result).toBe('memory-dashboard');
});
test('.gbrain-source dotfile pinned to source → resolves to that source even outside local_path', async () => {
const cwd = mkdtempSync(join(tmpdir(), 'gbrain-dotfile-'));
try {
writeFileSync(join(cwd, '.gbrain-source'), 'stock-dashboard\n');
const result = await resolveSourceId(engine, null, cwd);
expect(result).toBe('stock-dashboard');
} finally {
rmSync(cwd, { recursive: true, force: true });
}
});
test('explicit flag wins over cwd-prefix that would have matched', async () => {
const cwd = join(tmpRoot, 'proj-mem');
const result = await resolveSourceId(engine, 'stock-dashboard', cwd);
expect(result).toBe('stock-dashboard');
});
test('GBRAIN_SOURCE env var wins over cwd-prefix that would have matched', async () => {
process.env.GBRAIN_SOURCE = 'stock-dashboard';
try {
const cwd = join(tmpRoot, 'proj-mem');
const result = await resolveSourceId(engine, null, cwd);
expect(result).toBe('stock-dashboard');
} finally {
delete process.env.GBRAIN_SOURCE;
}
});
});
describe('sync.ts safety net — drop literal default to undefined when no signal', () => {
/**
* Reproduces the exact sync.ts:549-560 logic so the safety-net invariant
* is locked into a unit test. If anyone removes the "drop to undefined"
* branch in a future refactor, this test fails immediately rather than
* silently breaking pre-v0.17 sync.
*/
async function syncResolveCwd(
explicitSource: string | null,
envSource: string | null,
cwd: string,
): Promise<string | undefined> {
if (envSource !== null) process.env.GBRAIN_SOURCE = envSource;
else delete process.env.GBRAIN_SOURCE;
let sourceId: string | undefined = await resolveSourceId(engine, explicitSource, cwd);
if (!explicitSource && !process.env.GBRAIN_SOURCE && sourceId === 'default') {
sourceId = undefined;
}
return sourceId;
}
test('REGRESSION: pre-v0.17 brain pattern (no signal, no match) → sourceId = undefined', async () => {
const isolatedCwd = mkdtempSync(join(tmpdir(), 'gbrain-isolated-2-'));
try {
const result = await syncResolveCwd(null, null, isolatedCwd);
expect(result).toBeUndefined();
} finally {
rmSync(isolatedCwd, { recursive: true, force: true });
}
});
test('cwd-prefix match returns named source (not undefined)', async () => {
const result = await syncResolveCwd(null, null, join(tmpRoot, 'proj-mem'));
expect(result).toBe('memory-dashboard');
});
test('explicit --source default still returns default (signal present, do not drop)', async () => {
const isolatedCwd = mkdtempSync(join(tmpdir(), 'gbrain-isolated-3-'));
try {
const result = await syncResolveCwd('default', null, isolatedCwd);
expect(result).toBe('default');
} finally {
rmSync(isolatedCwd, { recursive: true, force: true });
}
});
test('GBRAIN_SOURCE=default still returns default (signal present, do not drop)', async () => {
const isolatedCwd = mkdtempSync(join(tmpdir(), 'gbrain-isolated-4-'));
try {
const result = await syncResolveCwd(null, 'default', isolatedCwd);
expect(result).toBe('default');
} finally {
rmSync(isolatedCwd, { recursive: true, force: true });
}
});
});