mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 08:53:22 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
208bd36dc7 | ||
|
|
bf493d36a0 | ||
|
|
7b3e4357ee |
@@ -2761,12 +2761,18 @@ const sync_brain: Operation = {
|
||||
localOnly: true,
|
||||
handler: async (ctx, p) => {
|
||||
const { performSync } = await import('../commands/sync.ts');
|
||||
// #2830: thread ctx.sourceId (D7 pattern, same as revert_version /
|
||||
// put_page) so a no-`repo` call resolves the CALLER's sync anchor.
|
||||
// Without it, performSync read the default source's repo_path/last_commit
|
||||
// and silently synced against the wrong repo on multi-source brains.
|
||||
const sourceOpts = ctx.sourceId ? { sourceId: ctx.sourceId } : {};
|
||||
return performSync(ctx.engine, {
|
||||
repoPath: p.repo as string | undefined,
|
||||
dryRun: ctx.dryRun || (p.dry_run as boolean) || false,
|
||||
noEmbed: (p.no_embed as boolean) || false,
|
||||
noPull: (p.no_pull as boolean) || false,
|
||||
full: (p.full as boolean) || false,
|
||||
...sourceOpts,
|
||||
});
|
||||
},
|
||||
cliHints: { name: 'sync', hidden: true },
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* #2830 — the `sync_brain` MCP op must thread ctx.sourceId into performSync,
|
||||
* mirroring the D7 pattern already applied to revert_version / put_page.
|
||||
*
|
||||
* Pre-fix: the handler called performSync with no sourceId, so a call with
|
||||
* no explicit `repo` argument (the normal MCP usage) resolved the sync
|
||||
* anchor of the DEFAULT source instead of the caller's own source — on a
|
||||
* multi-source brain that silently syncs (or reports "up to date" against)
|
||||
* the wrong repo's history.
|
||||
*
|
||||
* Behavioral pin: with a source whose local_path is a committed git repo,
|
||||
* calling sync_brain with ctx.sourceId = that source and NO repo param must
|
||||
* import that repo's pages into that source. On master the anchor lookup
|
||||
* runs against `default` (no local_path) and the sync errors out — zero
|
||||
* pages land in the source.
|
||||
*/
|
||||
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
||||
import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'fs';
|
||||
import { execSync } from 'child_process';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
||||
import { runSources } from '../src/commands/sources.ts';
|
||||
import { operationsByName, type OperationContext } from '../src/core/operations.ts';
|
||||
|
||||
const SOURCE = 'srcb-2830';
|
||||
|
||||
let engine: PGLiteEngine;
|
||||
let repoPath: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
engine = new PGLiteEngine();
|
||||
await engine.connect({});
|
||||
await engine.initSchema();
|
||||
|
||||
repoPath = mkdtempSync(join(tmpdir(), 'gbrain-syncop-'));
|
||||
execSync('git init', { cwd: repoPath, stdio: 'pipe' });
|
||||
execSync('git config user.email "test@test.com"', { cwd: repoPath, stdio: 'pipe' });
|
||||
execSync('git config user.name "Test"', { cwd: repoPath, stdio: 'pipe' });
|
||||
mkdirSync(join(repoPath, 'topics'), { recursive: true });
|
||||
writeFileSync(join(repoPath, 'topics/anchor-check.md'), [
|
||||
'---',
|
||||
'type: concept',
|
||||
'title: Anchor Check',
|
||||
'---',
|
||||
'',
|
||||
'Body long enough to import cleanly for the sync-op source test.',
|
||||
'',
|
||||
].join('\n'));
|
||||
execSync('git add -A && git commit -m seed', { cwd: repoPath, stdio: 'pipe' });
|
||||
|
||||
await runSources(engine, ['add', SOURCE, '--path', repoPath, '--no-federated']);
|
||||
}, 60_000);
|
||||
|
||||
afterAll(async () => {
|
||||
if (engine) await engine.disconnect();
|
||||
if (repoPath) rmSync(repoPath, { recursive: true, force: true });
|
||||
}, 60_000);
|
||||
|
||||
describe('sync_brain op threads ctx.sourceId (#2830)', () => {
|
||||
test('no-repo call syncs the caller source own anchor, not default', async () => {
|
||||
const op = operationsByName['sync_brain']!;
|
||||
const ctx = {
|
||||
engine,
|
||||
config: {},
|
||||
logger: { info() {}, warn() {}, error() {} },
|
||||
dryRun: false,
|
||||
remote: false,
|
||||
sourceId: SOURCE,
|
||||
} as unknown as OperationContext;
|
||||
|
||||
// No `repo` param — the anchor must resolve from ctx.sourceId.
|
||||
let result: unknown;
|
||||
let error: unknown;
|
||||
try {
|
||||
result = await op.handler(ctx, { no_embed: true, no_pull: true });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
const rows = await engine.executeRaw<{ n: number }>(
|
||||
`SELECT COUNT(*)::int AS n FROM pages WHERE source_id = $1`,
|
||||
[SOURCE],
|
||||
);
|
||||
// Pre-fix: performSync resolved the DEFAULT anchor (no local_path) and
|
||||
// errored — nothing landed in the source. Post-fix: the repo imports
|
||||
// into the caller's source.
|
||||
expect({ imported: rows[0]!.n > 0, error: error ? String(error) : null })
|
||||
.toEqual({ imported: true, error: null });
|
||||
expect(result).toBeDefined();
|
||||
}, 60_000);
|
||||
});
|
||||
Reference in New Issue
Block a user