Files
gitmost/packages/git-sync/test/redteam-layout-title.test.ts
claude code agent 227 24b903aaf3 build(git-sync): land the @docmost/git-sync package into develop, code-only (#326 step 1 / PR-A)
The git-sync converter + engine source lived only on the #119 branch; develop
had just the dead compiled build/. Bring the whole package (src + ~700 tests)
onto develop under CI, with NO consumer wired — git-sync stays fully inert in
develop (nothing in apps/server imports it), so runtime behavior is unchanged.
This unblocks #293 (extract the shared converter package from the landed source)
and lets #119's functionality land LAST, already writing the canonical format
(per the #326 landing order).

- packages/git-sync: src (lib converter + engine) + test corpus + configs.
- Remove develop's dead committed packages/git-sync/build/; gitignore it
  (built in CI/Docker via pnpm build, never committed — no src/build drift).
- pnpm-lock.yaml: add the @docmost/git-sync importer (a missing workspace
  package in the lock is a CI blocker). `pnpm install --frozen-lockfile` passes.
- NO server integration / loader / Dockerfile runtime changes (those come with
  #119 at step 6).

Verified: tsc clean; vitest 711 passed | 1 expected-fail, 0 failures, 0 type
errors; pnpm --frozen-lockfile EXIT 0; apps/server has no git-sync import.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 06:21:41 +03:00

72 lines
3.2 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { buildVaultLayout, type PageNode } from '../src/engine/layout.js';
import { classifyRenameMoves } from '../src/engine/push.js';
import type {
ClassifyRenameMovesDeps,
MetaSide,
RenameMoveAction,
} from '../src/engine/push.js';
import type { DocmostMdMeta } from '../src/lib/index.js';
// RED-TEAM finding #4 (two facets):
// (a) buildVaultLayout disambiguation is ORDER-DEPENDENT: which of two
// equally-titled root pages keeps the bare stem (and which gets the
// ` ~slugId` suffix) depends purely on input array order. The layout is
// supposed to be a deterministic function of the page SET, so reordering
// the input must not move the suffix onto a different page.
// (b) The page title derived from a DISAMBIGUATED filename ('Report ~a1.md')
// never strips the cosmetic ` ~slugId` suffix, so a pure disambiguation
// file-rename is mis-classified as a real title RENAME that would push the
// suffix ('Report ~a1') back into Docmost as the page's actual title.
describe('redteam #4a — buildVaultLayout is stable under input reorder', () => {
it('keeps the same stem for page A regardless of input order', () => {
const A: PageNode = { id: 'A', title: 'Report', slugId: 'a1', parentPageId: null };
const B: PageNode = { id: 'B', title: 'Report', slugId: 'b2', parentPageId: null };
const l1 = buildVaultLayout([A, B]);
const l2 = buildVaultLayout([B, A]);
// Identity (pageId A) must resolve to the same file stem no matter how the
// flat page list happened to be ordered.
expect(l2.get('A')?.stem).toBe(l1.get('A')?.stem);
});
});
describe('redteam #4b — disambiguation suffix is not a title change', () => {
// Mirror production push.ts `titleFromPath` EXACTLY: the synthetic native meta
// sets `title = baseName(path) without ".md"`. This is the real derivation the
// injected `metaAt` carries in `main`.
function titleFromPath(path: string): string {
const slash = path.lastIndexOf('/');
const base = slash < 0 ? path : path.slice(slash + 1);
return base.endsWith('.md') ? base.slice(0, -3) : base;
}
function deps(): ClassifyRenameMovesDeps {
const metaAt = (path: string, _side: MetaSide): DocmostMdMeta | null => ({
version: 1,
title: titleFromPath(path),
pageId: 'p1',
});
// Same enclosing folder (root) on both sides -> no reparent.
const resolveParentPageId = (_path: string, _side: MetaSide): string | null => null;
return { metaAt, resolveParentPageId };
}
it('does NOT emit a rename when only a ~slugId suffix was appended', () => {
// A sibling collision appeared, so the file 'Report.md' was relocated to the
// disambiguated 'Report ~a1.md'. The page TITLE in Docmost is still 'Report'.
const rms: RenameMoveAction[] = [
{ pageId: 'p1', oldPath: 'Report.md', newPath: 'Report ~a1.md' },
];
const [classified] = classifyRenameMoves(rms, deps());
// Desired behaviour: a pure disambiguation file-rename is cosmetic/local and
// must NOT be pushed as a title change. (If any rename WERE emitted it must
// carry the real title 'Report', never the suffixed 'Report ~a1'.)
expect(classified.rename).toBeUndefined();
});
});