fix(git-sync): a malformed non-UUID gitmost_id no longer wedges a space's sync (C9-D1)

A vault file with a broken/hand-edited `gitmost_id` frontmatter (e.g.
`gitmost_id: [unclosed` or a non-uuid token) fed that value into a Postgres
`uuid` predicate (page update/delete), throwing 22P02 "invalid input syntax for
type uuid". The push apply recorded it as a per-cycle failure that never cleared —
refs never advance when failures>0, so the WHOLE space's sync looped on the same
failure indefinitely and no further legitimate change synced (found via web-test).

Wrap the id-scoped write ops (import/delete/move/rename/restore) at the bind()
seam: swallow exactly the 22P02 as an inert no-op so the cycle succeeds and the
rest of the space keeps syncing; re-throw anything else. pageId is the only
user-influenced uuid in these ops, so a 22P02 there unambiguously means it.

Verified on the stand: pushing a non-UUID gitmost_id now logs a skip warn and the
space stays at 0 failures (was 1 failure/cycle forever); a concurrent legit edit
to another page still syncs. Unit tests: import/delete swallow 22P02, non-22P02
re-throws. Full server suite green (2145).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-03 05:33:36 +03:00
parent 67dca8c10e
commit f36a2def73
2 changed files with 95 additions and 5 deletions
@@ -418,6 +418,45 @@ describe('GitmostDataSourceService', () => {
});
});
// Bug C9-D1: a vault file with a malformed (non-UUID) `gitmost_id` frontmatter
// makes the id reach a Postgres `uuid` predicate, which throws error code
// '22P02'. Left unhandled the push apply records it as a per-cycle failure that
// never clears -> the whole space's sync loops forever. The bind() seam wraps the
// id-scoped writes so exactly that error is swallowed as an inert no-op.
describe('malformed-id guard (bug C9-D1: non-UUID gitmost_id must not wedge sync)', () => {
const pgInvalidUuid = Object.assign(
new Error('invalid input syntax for type uuid: "not-a-uuid"'),
{ code: '22P02' },
);
it('importPageMarkdown swallows a 22P02 and does NOT write the body', async () => {
const { service, mocks } = build();
mocks.pageRepo.findById.mockRejectedValue(pgInvalidUuid);
const res = await service
.bind(CTX)
.importPageMarkdown('not-a-uuid', '# x');
expect(res).toEqual({}); // inert no-op, no throw
expect(mocks.collabGateway.writePageBody).not.toHaveBeenCalled();
});
it('deletePage swallows the 22P02 thrown by the uuid predicate (no wedge)', async () => {
const { service, mocks } = build();
// The malformed id reaches removePage's `uuid` predicate, which throws 22P02.
mocks.pageService.removePage.mockRejectedValue(pgInvalidUuid);
await expect(
service.bind(CTX).deletePage('not-a-uuid'),
).resolves.toBeUndefined();
});
it('re-throws a NON-22P02 error (does not mask real failures)', async () => {
const { service, mocks } = build();
mocks.pageRepo.findById.mockRejectedValue(new Error('db down'));
await expect(
service.bind(CTX).importPageMarkdown('not-a-uuid', '# x'),
).rejects.toThrow('db down');
});
});
describe('createPage', () => {
it('creates the shell with git-sync provenance, writes body, returns id', async () => {
const { service, mocks } = build();