feat(git-sync): per-space toggle for conflict-marker handling on push (#13)

Red-team #13 (conflict markers reaching Docmost) is now a per-space policy
exposed as a UI toggle, instead of a hardcoded behavior. New boolean
`gitSync.autoMergeConflicts` (default FALSE), mirroring the existing per-space
`gitSync.enabled` flag end-to-end (jsonb space settings -> update-space DTO ->
space.service -> client types -> space settings form switch):

- OFF (default, safe): a page whose committed body still has unresolved git
  conflict markers is NOT pushed — it is recorded as a per-page push FAILURE
  ("unresolved conflict markers — resolve in git first"). Recording a failure
  (not a soft skip) deliberately HOLDS refs/docmost/last-pushed so the conflict
  commit is never marked pushed and a later pull cannot clobber the user's
  in-progress resolution; the page retries until the conflict is resolved in git.
- ON: the marker lines are stripped and both sides' content is pushed (the prior
  behavior), so the conflict becomes visible/fixable inside Docmost.

The engine Settings carries `autoMergeConflicts`; runPush threads it into the
update AND create paths. The orchestrator's buildSettings reads the per-space
flag from jsonb (strict opt-in like `enabled`, default false).

Tests: redteam-push-cycle #13 rewritten (default -> not pushed + failure + refs
held; ON -> strip-and-push); space.service + edit-space-form + orchestrator
specs extended. git-sync vitest 618, server jest space+git-sync 163, client
edit-space-form 11, server/client tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
claude code agent 227
2026-06-26 01:56:55 +03:00
parent 1a6ec0ff1f
commit 4ce3346cee
11 changed files with 336 additions and 25 deletions

View File

@@ -122,7 +122,19 @@ function build(opts: BuildOptions = {}): Built {
};
const redisService = { getOrThrow: jest.fn(() => redis) };
const db = {};
// Chainable Kysely stub. `buildSettings` reads the space's
// `gitSync.autoMergeConflicts` flag via
// `selectFrom('spaces').select(...).where('id','=',id).executeTakeFirst()`;
// default it to the SAFE off value. `enabledSpaces` uses `.execute()`.
const db = (() => {
const builder: any = {
select: () => builder,
where: () => builder,
executeTakeFirst: async () => ({ autoMergeConflicts: false }),
execute: async () => [],
};
return { selectFrom: () => builder };
})();
// The REAL SpaceLockService, constructed against the mock redis above, so all
// existing lock assertions (lock-held, in-progress, leader lock, release CAS,