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:
claude code agent 227
2026-06-24 01:00:59 +03:00
parent 55a0b60140
commit 2a45426008
6 changed files with 82 additions and 17 deletions
@@ -292,10 +292,13 @@ describe('GitmostDataSourceService', () => {
it('uses the soft-delete path (removePage), not a force delete', async () => {
const { service, mocks } = build();
await service.bind(CTX).deletePage('p1');
// Passes git-sync provenance so the soft-delete stamps
// lastUpdatedSource='git-sync' (loop-guard, PR #119 review).
expect(mocks.pageService.removePage).toHaveBeenCalledWith(
'p1',
'svc-user',
'ws-1',
{ actor: 'git-sync', aiChatId: null },
);
// No forceDelete on the service surface used here.
expect((mocks.pageService as any).forceDelete).toBeUndefined();
@@ -358,7 +361,12 @@ describe('GitmostDataSourceService', () => {
it('restores via the repo restore path scoped to the workspace', async () => {
const { service, mocks } = build();
await service.bind(CTX).restorePage('p1');
expect(mocks.pageRepo.restorePage).toHaveBeenCalledWith('p1', 'ws-1');
// Stamps lastUpdatedSource='git-sync' on restore (loop-guard, PR #119).
expect(mocks.pageRepo.restorePage).toHaveBeenCalledWith(
'p1',
'ws-1',
'git-sync',
);
});
});
});