fix(git-sync): address PR #119 review #4 — symlink guard, dead-code cull, changelog + warnings/suggestions
Blocking (review id 2514): - [security] Forbid symlinks in vaults. ensureServable now sets core.symlinks=false in each vault's local git config (a pushed symlink is checked out as a plain file, never a real link), and the engine cycle wraps every read/write/mkdir in an lstat/realpath guard (new path-guard.ts) that refuses a path that is — or traverses — a symlink, or whose realpath escapes the vault root. Prevents a writer from publishing /etc/passwd or the server .env, or writing outside the vault. Adds unit tests (path-guard.test.ts) + a read-guard integration test (cycle.test.ts) + real lstat/realpath in the roundtrip integration test. - [simplification] Delete dead lib/diff.ts + test/diff.test.ts and drop the now-unused @fellow/prosemirror-recreate-transform dependency. - [documentation] Add a CHANGELOG [Unreleased] → Added entry for git-sync. Warnings: - [test-coverage] Cover the CREATE-branch conflict-markers guard (a new .md with markers and no gitmost_id is recorded as a create failure, never created). Suggestions: - [stability] Bound each `git config` in ensureServable with a timeout. - [authz] Trigger endpoint resolves spaceId workspace-scoped and 404s a foreign space before any vault directory is created. - [stability] Attribute git-initiated moves to the service account (lastUpdatedById), via an optional actor param on PageService.movePage. - [documentation] Document the per-space autoMergeConflicts toggle in AGENTS.md. - [test-coverage] Cover the unterminated `:::` callout fence fallback. - [simplification] Move test-only roundtrip-helpers.ts out of src/ into test/. Architecture: - Move the Yjs/ProseMirror merge primitives (yjs-body-merge, three-way-merge, lcs + specs) into collaboration/merge/, breaking the collaboration → integrations/git-sync dependency cycle this PR introduced. - Port the schema-surface drift gate to packages/mcp (the mcp schema mirror had none); pins 52 entries. Deferred (with rationale in the review thread): the incremental-pull perf warning (correctness-neutral; needs a high-water-mark design + its own tests on the data-loss-critical path) and the redis-sync rolling-deploy mixed-version edge (the deficient behavior is in already-released old-instance code; the new code is correct on both sides; impact is a transient rollout-window artifact). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { runPush, LAST_PUSHED_REF, DOCMOST_BRANCH } from '../src/engine/push';
|
||||
import {
|
||||
runPush,
|
||||
LAST_PUSHED_REF,
|
||||
DOCMOST_BRANCH,
|
||||
CONFLICT_MARKERS_FAILURE_REASON,
|
||||
} from '../src/engine/push';
|
||||
import type { PushDeps } from '../src/engine/push';
|
||||
import type { Settings } from '../src/engine/settings';
|
||||
import { runCycle, type RunCycleDeps } from '../src/engine/cycle';
|
||||
@@ -139,6 +144,59 @@ describe('#13 conflict markers reach Docmost', () => {
|
||||
expect(pushedBody).toContain('my line');
|
||||
expect(pushedBody).toContain('their line');
|
||||
});
|
||||
|
||||
it('CREATE branch (autoMergeConflicts off): does NOT create a page from a conflicted NEW file; records a create failure', async () => {
|
||||
// The conflict-markers guard is DUPLICATED on the CREATE path (a brand-new
|
||||
// .md with NO gitmost_id, status 'A') and was previously untested — only the
|
||||
// UPDATE branch had coverage. Without this, a regression would SILENTLY push
|
||||
// `<<<<<<<`/`>>>>>>>` into a freshly-created page. Assert the create path
|
||||
// isolates it exactly like update: no createPage, a kind:'create' failure
|
||||
// with the conflict reason, and the refs held.
|
||||
const { git, calls } = makePushGit({
|
||||
changes: [{ status: 'A', path: 'New.md' }],
|
||||
});
|
||||
const createPage = vi.fn(async () => ({ data: { id: 'new-1' } }));
|
||||
const client = {
|
||||
listSpaceTree: vi.fn(async () => ({ pages: [], complete: true })),
|
||||
importPageMarkdown: vi.fn(),
|
||||
createPage,
|
||||
deletePage: vi.fn(),
|
||||
movePage: vi.fn(),
|
||||
renamePage: vi.fn(),
|
||||
};
|
||||
const deps: PushDeps = {
|
||||
// makeSettings() leaves autoMergeConflicts undefined -> the SAFE default.
|
||||
settings: makeSettings(),
|
||||
git,
|
||||
makeClient: () => client as any,
|
||||
// Raw conflict body with NO gitmost_id frontmatter -> classified as CREATE.
|
||||
readFile: vi.fn(async (path: string) => {
|
||||
if (path === 'New.md') return conflictBody;
|
||||
throw new Error(`no such file: ${path}`);
|
||||
}),
|
||||
writeFile: vi.fn(async () => {}),
|
||||
log: () => {},
|
||||
};
|
||||
|
||||
const res = await runPush(deps, { dryRun: false });
|
||||
expect(res.mode).toBe('apply');
|
||||
|
||||
// No page was created from the conflicted content.
|
||||
expect(createPage).not.toHaveBeenCalled();
|
||||
|
||||
// Recorded as a CREATE failure with the conflict-markers reason.
|
||||
expect(res.applied?.failures).toEqual([
|
||||
expect.objectContaining({
|
||||
kind: 'create',
|
||||
path: 'New.md',
|
||||
error: CONFLICT_MARKERS_FAILURE_REASON,
|
||||
}),
|
||||
]);
|
||||
|
||||
// A failure prevents advancing the last-pushed ref.
|
||||
expect(res.applied?.lastPushedAdvanced).toBe(false);
|
||||
expect(calls.updateRef).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user