mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 08:53:22 +00:00
put_page: report push state honestly instead of implying it via committed (#3936)
Wave-assembled from PR #3936 by @dovstern. Conflict resolution: kept master's resolveRepoRoot() block AND the PR's exported currentBranch in src/core/brain-repo-durability.ts. Adaptation: the new serial test now writes the simulated push log under $GBRAIN_HOME/.gbrain (CX2-8 parent-dir semantics landed on master after the PR's base). Co-Authored-By: Dov Stern <dovstern@users.noreply.github.com>
This commit is contained in:
committed by
Sina Matian
co-authored by
Dov Stern
parent
45bd04ff9f
commit
44eea64084
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* put_page's write-through response on a durability-hardened repo reports
|
||||
* `committed: true` for both a successful and a FAILED background push — the
|
||||
* commit lands locally either way, but the push runs detached in the
|
||||
* post-commit hook, so the caller had no field to distinguish "pushed fine"
|
||||
* from "still local-only". This pins the honest contract: `committed` is
|
||||
* commit-only, `pushed: 'pending'` says the push outcome isn't known yet, and
|
||||
* `lastPushStatus` surfaces the hook's own log so a caller (or health
|
||||
* tooling) can see whether pushes for this branch are currently landing.
|
||||
*/
|
||||
|
||||
import { describe, test, expect, beforeAll, beforeEach, afterEach, afterAll } from 'bun:test';
|
||||
import { mkdtempSync, writeFileSync, rmSync, mkdirSync, chmodSync, appendFileSync } from 'fs';
|
||||
import { execSync, execFileSync } from 'child_process';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
||||
import { resetPgliteState } from './helpers/reset-pglite.ts';
|
||||
import { operations } from '../src/core/operations.ts';
|
||||
import type { OperationContext } from '../src/core/operations.ts';
|
||||
import { configureGateway, resetGateway, __setEmbedTransportForTests } from '../src/core/ai/gateway.ts';
|
||||
|
||||
const putPageOp = operations.find((o) => o.name === 'put_page')!;
|
||||
|
||||
let engine: PGLiteEngine;
|
||||
let repo: string;
|
||||
let gbrainHome: string;
|
||||
let oldGbrainHome: string | undefined;
|
||||
|
||||
function git(cwd: string, ...args: string[]): string {
|
||||
return execFileSync('git', ['-C', cwd, ...args], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf-8',
|
||||
}).trim();
|
||||
}
|
||||
|
||||
/** Same fixture as write-through-commit.serial.test.ts: a hook file carrying
|
||||
* the gbrain banner (the only thing `isDurabilityHardened` checks) with a
|
||||
* no-op body so tests never attempt a real network push. */
|
||||
function installFakeDurabilityHook(repoPath: string): void {
|
||||
const hooksDir = join(repoPath, '.git', 'hooks');
|
||||
mkdirSync(hooksDir, { recursive: true });
|
||||
const hookPath = join(hooksDir, 'post-commit');
|
||||
writeFileSync(hookPath, [
|
||||
'#!/usr/bin/env bash',
|
||||
'# gbrain brain-durability post-commit hook (v0.42.44+)',
|
||||
'exit 0',
|
||||
'',
|
||||
].join('\n'));
|
||||
chmodSync(hookPath, 0o755);
|
||||
}
|
||||
|
||||
function makeCtx(opts: Partial<OperationContext> = {}): OperationContext {
|
||||
return {
|
||||
engine,
|
||||
config: { engine: 'pglite' as const },
|
||||
logger: { info: () => {}, warn: () => {}, error: () => {} },
|
||||
dryRun: false,
|
||||
remote: false,
|
||||
sourceId: 'default',
|
||||
...opts,
|
||||
};
|
||||
}
|
||||
|
||||
describe('put_page write-through — commit/push reporting on a hardened repo', () => {
|
||||
beforeAll(async () => {
|
||||
configureGateway({
|
||||
embedding_model: 'openai:text-embedding-3-large',
|
||||
embedding_dimensions: 1536,
|
||||
env: { ...process.env, OPENAI_API_KEY: process.env.OPENAI_API_KEY || 'sk-test-stub' },
|
||||
});
|
||||
__setEmbedTransportForTests(async ({ values }: any) => ({
|
||||
embeddings: values.map(() => new Array(1536).fill(0)),
|
||||
usage: { tokens: 0 },
|
||||
}) as any);
|
||||
|
||||
engine = new PGLiteEngine();
|
||||
await engine.connect({});
|
||||
await engine.initSchema();
|
||||
}, 60_000);
|
||||
|
||||
afterAll(async () => {
|
||||
await engine.disconnect();
|
||||
__setEmbedTransportForTests(null);
|
||||
resetGateway();
|
||||
}, 60_000);
|
||||
|
||||
beforeEach(async () => {
|
||||
await resetPgliteState(engine);
|
||||
repo = mkdtempSync(join(tmpdir(), 'gbrain-ppr-'));
|
||||
execSync('git init -q -b main', { cwd: repo, stdio: 'pipe' });
|
||||
execSync('git config user.email "t@t.t"', { cwd: repo, stdio: 'pipe' });
|
||||
execSync('git config user.name "T"', { cwd: repo, stdio: 'pipe' });
|
||||
writeFileSync(join(repo, 'seed.md'), 'seed\n');
|
||||
execSync('git add -A && git commit -qm init', { cwd: repo, stdio: 'pipe' });
|
||||
installFakeDurabilityHook(repo);
|
||||
await engine.setConfig('sync.repo_path', repo);
|
||||
|
||||
gbrainHome = mkdtempSync(join(tmpdir(), 'gbrain-home-'));
|
||||
oldGbrainHome = process.env.GBRAIN_HOME;
|
||||
process.env.GBRAIN_HOME = gbrainHome;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (oldGbrainHome === undefined) delete process.env.GBRAIN_HOME; else process.env.GBRAIN_HOME = oldGbrainHome;
|
||||
if (repo) rmSync(repo, { recursive: true, force: true });
|
||||
if (gbrainHome) rmSync(gbrainHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('reports committed + pending push, not an implied success, with no push history yet', async () => {
|
||||
const res: any = await putPageOp.handler(makeCtx(), {
|
||||
slug: 'notes/ppr-fresh',
|
||||
content: '---\ntype: concept\ntitle: PPR Fresh\n---\n\nbody',
|
||||
});
|
||||
|
||||
expect(res.write_through.committed).toBe(true);
|
||||
expect(res.write_through.pushed).toBe('pending');
|
||||
expect(res.write_through.lastPushStatus.status).toBe('unknown');
|
||||
});
|
||||
|
||||
test('surfaces a prior LOCAL-ONLY push failure instead of hiding it behind committed:true', async () => {
|
||||
// Simulate the hook having already logged an unresolved push failure for
|
||||
// this branch (e.g. from an earlier write in the same session).
|
||||
// CX2-8: GBRAIN_HOME is a PARENT dir — the resolved home is
|
||||
// $GBRAIN_HOME/.gbrain, so the push log lives under it.
|
||||
mkdirSync(join(gbrainHome, '.gbrain'), { recursive: true });
|
||||
appendFileSync(
|
||||
join(gbrainHome, '.gbrain', 'brain-push.log'),
|
||||
'2025-01-01T00:00:00Z [push] LOCAL-ONLY, NEEDS ATTENTION: main @ deadbee could not reach origin. Run: gbrain sources pull <id> && git push\n',
|
||||
);
|
||||
|
||||
const res: any = await putPageOp.handler(makeCtx(), {
|
||||
slug: 'notes/ppr-broken-push',
|
||||
content: '---\ntype: concept\ntitle: PPR Broken Push\n---\n\nbody',
|
||||
});
|
||||
|
||||
// The commit itself still succeeds (git commit doesn't touch the network) —
|
||||
// that's the honest part of `committed: true`. What must NOT happen is the
|
||||
// caller reading `committed: true` as "this is durably on the remote".
|
||||
expect(res.write_through.committed).toBe(true);
|
||||
expect(res.write_through.pushed).toBe('pending');
|
||||
expect(res.write_through.lastPushStatus.status).toBe('needs_attention');
|
||||
expect(res.write_through.lastPushStatus.detail).toContain('NEEDS ATTENTION');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user