fix(git-sync): hold refs on suppressed deletes + stamp delete/restore provenance (PR #119 review)
Two stability warnings from the #119 review: 1. delete-cap no longer drops deletions forever. When planned deletes exceed GIT_SYNC_MAX_DELETES_PER_CYCLE the apply client's deletePage now THROWS instead of resolving to a no-op. A throw is recorded by the engine as a per-page failure, so `refs/docmost/last-pushed` is NOT advanced past the commit that dropped the files — the next cycle re-diffs from the un-advanced ref and re-plans the same deletes (a transient over-cap is retried, not silently dropped and then recreated by the next pull). Previously a resolving no-op let the engine count `deleted++` with no failure, advance the ref, and never replay the deletions. 2. git-sync soft-delete and restore now stamp provenance. deletePage routes GIT_SYNC_PROVENANCE through pageService.removePage, and restorePage stamps lastUpdatedSource='git-sync' on the restore update — so the page-change listener's loop-guard (skip when lastUpdatedSource==='git-sync') recognizes both as its own writes instead of scheduling a wasted echo cycle. Done via a backward-compatible optional `lastUpdatedSource` param on pageRepo.removePage/restorePage (omitted for ordinary user deletes/restores). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -303,7 +303,7 @@ describe('GitSyncOrchestrator', () => {
|
||||
});
|
||||
|
||||
describe('delete cap (anti-data-loss)', () => {
|
||||
it('neutralizes deletePage on the apply client when planned deletes exceed the cap', async () => {
|
||||
it('suppresses deletePage on the apply client (by throwing) when planned deletes exceed the cap', async () => {
|
||||
jest.spyOn(Logger.prototype, 'warn').mockImplementation(() => undefined);
|
||||
const built = build({ maxDeletes: 5 });
|
||||
// Dry-run plans 9 deletes (over the cap of 5); apply still runs.
|
||||
@@ -315,20 +315,22 @@ describe('GitSyncOrchestrator', () => {
|
||||
expect(res.ran).toBe(true);
|
||||
expect(runPushMock).toHaveBeenCalledTimes(2);
|
||||
|
||||
// The second runPush (real apply, dryRun:false) got a neutralized client.
|
||||
// The second runPush (real apply, dryRun:false) got a suppressed client.
|
||||
const [applyDeps, applyOpts] = runPushMock.mock.calls[1];
|
||||
expect(applyOpts).toEqual({ dryRun: false });
|
||||
const applyClient = applyDeps.makeClient();
|
||||
// deletePage is still a function (the engine may call it)...
|
||||
// deletePage is still a function (the engine calls it)...
|
||||
expect(typeof applyClient.deletePage).toBe('function');
|
||||
await applyClient.deletePage('p1');
|
||||
// ...but it is a NO-OP: the underlying real deletePage was NOT invoked.
|
||||
// ...but it THROWS, so the engine records a per-page failure and holds
|
||||
// `last-pushed` (a resolving no-op would advance past the dropped deletes
|
||||
// and never replay them — PR #119 review). The real deletePage is NOT hit.
|
||||
await expect(applyClient.deletePage('p1')).rejects.toThrow(/suppress/i);
|
||||
expect(built.client.deletePage).not.toHaveBeenCalled();
|
||||
// Creates/updates pass through to the real client.
|
||||
expect(applyClient.createPage).toBe(built.client.createPage);
|
||||
});
|
||||
|
||||
it('fails safe: a throwing dry-run still suppresses deletes and does not throw', async () => {
|
||||
it('fails safe: a throwing dry-run still suppresses deletes and does not throw the cycle', async () => {
|
||||
jest.spyOn(Logger.prototype, 'warn').mockImplementation(() => undefined);
|
||||
const built = build({ maxDeletes: 5 });
|
||||
runPushMock
|
||||
@@ -340,7 +342,8 @@ describe('GitSyncOrchestrator', () => {
|
||||
expect(res.ran).toBe(true);
|
||||
const [applyDeps] = runPushMock.mock.calls[1];
|
||||
const applyClient = applyDeps.makeClient();
|
||||
await applyClient.deletePage('p1');
|
||||
// Suppressed via throw (same fail-safe path as the over-cap case).
|
||||
await expect(applyClient.deletePage('p1')).rejects.toThrow(/suppress/i);
|
||||
expect(built.client.deletePage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user