feat(sync): FS->Docmost push #3 — move/rename apply (§5 path-as-truth)
Complete the push action coverage (create/update/delete/move/rename/noop). - push.ts classifyRenameMoves (pure): the file PATH is the source of truth for tree position (§5) — new parent resolved from the enclosing folder's <dir>.md page, not the stale meta.parentPageId. Emit move iff parent changed, rename iff meta.title changed; a pure path-only rename is a NOOP (no Docmost call — the path is local, identity is pageId) - applyPushActions: move (move_page, reparent) THEN rename (rename_page); noop records and calls nothing; per-page isolation + refs-only-on-success preserved - resolveParentPageId reads <dir>.md meta via readFile (current) / git.showFileAtRef(last-pushed) (prev), matching buildVaultLayout - review fixes: prefetch wrapped in per-page try/catch so a tree-read throw isolates one page (§12), not the batch; failures.kind attributes the op that actually threw (rename-after-move -> "rename") - tests (+13): classifier (move/rename/both/noop/to-root), apply (calls/no-calls, ordering, isolation); 724 -> 737 green (x2 stable); corpus STABLE Deferred (final increment): live main() daemon, FS-watcher/debounce (§7.1), git-remote push (§7.2), pull-side bodyHash/updatedAt consumption, fractional-index position, escalate-on-divergent-docmost.
This commit is contained in:
@@ -33,6 +33,18 @@ function makeClient(opts?: { createId?: string }) {
|
||||
}),
|
||||
),
|
||||
deletePage: vi.fn(async (_pageId: string) => ({ success: true })),
|
||||
movePage: vi.fn(
|
||||
async (
|
||||
_pageId: string,
|
||||
_parentPageId: string | null,
|
||||
_position?: string,
|
||||
) => ({ success: true }),
|
||||
),
|
||||
renamePage: vi.fn(async (pageId: string, title: string) => ({
|
||||
success: true,
|
||||
pageId,
|
||||
title,
|
||||
})),
|
||||
};
|
||||
return client;
|
||||
}
|
||||
@@ -42,9 +54,14 @@ function makeClient(opts?: { createId?: string }) {
|
||||
* (advance the `docmost` mirror, the loop-close). `ffResult` configures what the
|
||||
* ff returns (default a successful advance).
|
||||
*/
|
||||
function makeGit(opts?: { ffResult?: { ok: boolean; reason?: string } }) {
|
||||
function makeGit(opts?: {
|
||||
ffResult?: { ok: boolean; reason?: string };
|
||||
/** Pre-image tree at `refs/docmost/last-pushed` (path -> text). */
|
||||
prevTree?: Record<string, string>;
|
||||
}) {
|
||||
const updateRefCalls: { ref: string; target: string }[] = [];
|
||||
const ffCalls: { branch: string; toCommit: string }[] = [];
|
||||
const prevTree = opts?.prevTree ?? {};
|
||||
const git = {
|
||||
updateRef: vi.fn(async (ref: string, target: string) => {
|
||||
updateRefCalls.push({ ref, target });
|
||||
@@ -53,6 +70,11 @@ function makeGit(opts?: { ffResult?: { ok: boolean; reason?: string } }) {
|
||||
ffCalls.push({ branch, toCommit });
|
||||
return opts?.ffResult ?? { ok: true };
|
||||
}),
|
||||
// The move/rename classifier reads the PREVIOUS parent folder's `.md` at
|
||||
// refs/docmost/last-pushed via this; `null` when absent there (SPEC §5).
|
||||
showFileAtRef: vi.fn(async (_ref: string, path: string) =>
|
||||
path in prevTree ? prevTree[path] : null,
|
||||
),
|
||||
};
|
||||
return { git, updateRefCalls, ffCalls };
|
||||
}
|
||||
@@ -189,23 +211,237 @@ describe('applyPushActions — delete (soft-delete to Trash, SPEC §8)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyPushActions — rename/move is DEFERRED (NEXT increment)', () => {
|
||||
it('returns renames/moves as `deferred` with NO client call', async () => {
|
||||
const client = makeClient();
|
||||
const { git } = makeGit();
|
||||
const fs = makeFs();
|
||||
// FS→Docmost push #3 (SPEC §5/§6/§16): the move/rename APPLY. The classifier
|
||||
// resolves the parent from the FILE PATH (the enclosing folder's `.md`), not
|
||||
// stale `meta.parentPageId`, then `applyPushActions` calls move_page / rename_page
|
||||
// (both for a reparent+retitle) or records a path-only NO-OP with NO client call.
|
||||
|
||||
/**
|
||||
* Helper: a self-contained file with the given pageId + title in its meta. Used
|
||||
* both to seed the working tree (fs) and the prev tree (git.showFileAtRef).
|
||||
*/
|
||||
function fileWith(meta: { pageId: string; title?: string }): string {
|
||||
return serializeDocmostMarkdownBody(
|
||||
{ version: 1, pageId: meta.pageId, ...(meta.title ? { title: meta.title } : {}) },
|
||||
'body',
|
||||
);
|
||||
}
|
||||
|
||||
describe('applyPushActions — move (parent changed, title same; SPEC §5/§16)', () => {
|
||||
it('calls movePage(pageId, newParent) and NOT renamePage', async () => {
|
||||
// The page moved from the space root (Doc.md) under a folder (Parent/Doc.md).
|
||||
// The new parent page's file is `Parent.md`; its meta carries the parent id.
|
||||
const client = makeClient();
|
||||
const { git } = makeGit({
|
||||
// Prev pre-image: the file used to sit at the root (parent ROOT).
|
||||
prevTree: { 'Doc.md': fileWith({ pageId: 'p-mv', title: 'Doc' }) },
|
||||
});
|
||||
const fs = makeFs({
|
||||
// Current tree: the moved file + its new parent folder's `.md`.
|
||||
'Parent/Doc.md': fileWith({ pageId: 'p-mv', title: 'Doc' }),
|
||||
'Parent.md': fileWith({ pageId: 'parent-id', title: 'Parent' }),
|
||||
});
|
||||
|
||||
const rm = { pageId: 'p-mv', oldPath: 'Old.md', newPath: 'New.md' };
|
||||
const res = await applyPushActions(
|
||||
deps(client, git, fs),
|
||||
actions({ renamesMoves: [rm] }),
|
||||
actions({
|
||||
renamesMoves: [
|
||||
{ pageId: 'p-mv', oldPath: 'Doc.md', newPath: 'Parent/Doc.md' },
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.deferred).toEqual([rm]);
|
||||
// NOTHING was pushed for the move this increment.
|
||||
expect(client.importPageMarkdown).not.toHaveBeenCalled();
|
||||
expect(client.createPage).not.toHaveBeenCalled();
|
||||
expect(client.deletePage).not.toHaveBeenCalled();
|
||||
expect(res.moved).toBe(1);
|
||||
expect(res.renamed).toBe(0);
|
||||
expect(client.movePage).toHaveBeenCalledTimes(1);
|
||||
// Reparented under `parent-id`; position left UNDEFINED (client default).
|
||||
expect(client.movePage).toHaveBeenCalledWith('p-mv', 'parent-id');
|
||||
expect(client.renamePage).not.toHaveBeenCalled();
|
||||
expect(res.noops).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyPushActions — move-to-root (newParent null; SPEC §16)', () => {
|
||||
it('calls movePage(pageId, null) when the file lands at the space root', async () => {
|
||||
const client = makeClient();
|
||||
const { git } = makeGit({
|
||||
// Prev: the file used to live under `Parent/`, so its old parent is the
|
||||
// page whose file is `Parent.md` (parent-id).
|
||||
prevTree: {
|
||||
'Parent/Doc.md': fileWith({ pageId: 'p-mv', title: 'Doc' }),
|
||||
'Parent.md': fileWith({ pageId: 'parent-id', title: 'Parent' }),
|
||||
},
|
||||
});
|
||||
// Current: the file is now at the root -> no enclosing folder -> parent ROOT.
|
||||
const fs = makeFs({ 'Doc.md': fileWith({ pageId: 'p-mv', title: 'Doc' }) });
|
||||
|
||||
const res = await applyPushActions(
|
||||
deps(client, git, fs),
|
||||
actions({
|
||||
renamesMoves: [
|
||||
{ pageId: 'p-mv', oldPath: 'Parent/Doc.md', newPath: 'Doc.md' },
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.moved).toBe(1);
|
||||
expect(client.movePage).toHaveBeenCalledWith('p-mv', null);
|
||||
expect(client.renamePage).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyPushActions — rename (same parent, title changed; SPEC §5/§6)', () => {
|
||||
it('calls renamePage(pageId, title) and NOT movePage', async () => {
|
||||
// Same enclosing folder on both sides (parent unchanged), only the title
|
||||
// changed in meta -> a pure rename.
|
||||
const client = makeClient();
|
||||
const { git } = makeGit({
|
||||
prevTree: {
|
||||
'Folder/Old.md': fileWith({ pageId: 'p-rn', title: 'Old Title' }),
|
||||
'Folder.md': fileWith({ pageId: 'folder-id', title: 'Folder' }),
|
||||
},
|
||||
});
|
||||
const fs = makeFs({
|
||||
'Folder/New.md': fileWith({ pageId: 'p-rn', title: 'New Title' }),
|
||||
'Folder.md': fileWith({ pageId: 'folder-id', title: 'Folder' }),
|
||||
});
|
||||
|
||||
const res = await applyPushActions(
|
||||
deps(client, git, fs),
|
||||
actions({
|
||||
renamesMoves: [
|
||||
{ pageId: 'p-rn', oldPath: 'Folder/Old.md', newPath: 'Folder/New.md' },
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.renamed).toBe(1);
|
||||
expect(res.moved).toBe(0);
|
||||
expect(client.renamePage).toHaveBeenCalledTimes(1);
|
||||
expect(client.renamePage).toHaveBeenCalledWith('p-rn', 'New Title');
|
||||
expect(client.movePage).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyPushActions — both (reparent + retitle; move THEN rename)', () => {
|
||||
it('calls movePage first, then renamePage', async () => {
|
||||
const callOrder: string[] = [];
|
||||
const client = makeClient();
|
||||
client.movePage.mockImplementation(async () => {
|
||||
callOrder.push('move');
|
||||
return { success: true };
|
||||
});
|
||||
client.renamePage.mockImplementation(async (pageId: string, title: string) => {
|
||||
callOrder.push('rename');
|
||||
return { success: true, pageId, title };
|
||||
});
|
||||
const { git } = makeGit({
|
||||
// Prev: at root (parent ROOT) with the old title.
|
||||
prevTree: { 'Old.md': fileWith({ pageId: 'p-x', title: 'Old' }) },
|
||||
});
|
||||
const fs = makeFs({
|
||||
// Current: under a new folder AND retitled.
|
||||
'NewParent/New.md': fileWith({ pageId: 'p-x', title: 'New' }),
|
||||
'NewParent.md': fileWith({ pageId: 'np-id', title: 'NewParent' }),
|
||||
});
|
||||
|
||||
const res = await applyPushActions(
|
||||
deps(client, git, fs),
|
||||
actions({
|
||||
renamesMoves: [
|
||||
{ pageId: 'p-x', oldPath: 'Old.md', newPath: 'NewParent/New.md' },
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.moved).toBe(1);
|
||||
expect(res.renamed).toBe(1);
|
||||
expect(client.movePage).toHaveBeenCalledWith('p-x', 'np-id');
|
||||
expect(client.renamePage).toHaveBeenCalledWith('p-x', 'New');
|
||||
// Order matters: reparent FIRST, then retitle.
|
||||
expect(callOrder).toEqual(['move', 'rename']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyPushActions — noop (path-only rename; NO Docmost call; SPEC §5)', () => {
|
||||
it('calls NEITHER movePage NOR renamePage and records the noop', async () => {
|
||||
// Same enclosing folder AND same title on both sides: a purely LOCAL file
|
||||
// rename. The page is its pageId; the path is cosmetic -> Docmost untouched.
|
||||
const client = makeClient();
|
||||
const { git } = makeGit({
|
||||
prevTree: {
|
||||
'Folder/A.md': fileWith({ pageId: 'p-noop', title: 'Same' }),
|
||||
'Folder.md': fileWith({ pageId: 'folder-id', title: 'Folder' }),
|
||||
},
|
||||
});
|
||||
const fs = makeFs({
|
||||
'Folder/B.md': fileWith({ pageId: 'p-noop', title: 'Same' }),
|
||||
'Folder.md': fileWith({ pageId: 'folder-id', title: 'Folder' }),
|
||||
});
|
||||
|
||||
const res = await applyPushActions(
|
||||
deps(client, git, fs),
|
||||
actions({
|
||||
renamesMoves: [
|
||||
{ pageId: 'p-noop', oldPath: 'Folder/A.md', newPath: 'Folder/B.md' },
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.moved).toBe(0);
|
||||
expect(res.renamed).toBe(0);
|
||||
// ZERO Docmost calls for a cosmetic rename.
|
||||
expect(client.movePage).not.toHaveBeenCalled();
|
||||
expect(client.renamePage).not.toHaveBeenCalled();
|
||||
expect(res.noops).toEqual([
|
||||
{
|
||||
pageId: 'p-noop',
|
||||
oldPath: 'Folder/A.md',
|
||||
newPath: 'Folder/B.md',
|
||||
reason: 'path-only-rename',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyPushActions — move whose client call throws (SPEC §12 isolation)', () => {
|
||||
it('isolates the failure into `failures` and does NOT advance the refs', async () => {
|
||||
const client = makeClient();
|
||||
client.movePage.mockImplementation(async () => {
|
||||
throw new Error('move boom');
|
||||
});
|
||||
const { git, updateRefCalls, ffCalls } = makeGit({
|
||||
prevTree: { 'Doc.md': fileWith({ pageId: 'p-mv', title: 'Doc' }) },
|
||||
});
|
||||
const fs = makeFs({
|
||||
'Parent/Doc.md': fileWith({ pageId: 'p-mv', title: 'Doc' }),
|
||||
'Parent.md': fileWith({ pageId: 'parent-id', title: 'Parent' }),
|
||||
});
|
||||
|
||||
const res = await applyPushActions(
|
||||
deps(client, git, fs),
|
||||
actions({
|
||||
renamesMoves: [
|
||||
{ pageId: 'p-mv', oldPath: 'Doc.md', newPath: 'Parent/Doc.md' },
|
||||
],
|
||||
}),
|
||||
'sha-move-fail',
|
||||
);
|
||||
|
||||
expect(res.moved).toBe(0);
|
||||
expect(res.failures).toEqual([
|
||||
{
|
||||
kind: 'move',
|
||||
pageId: 'p-mv',
|
||||
path: 'Parent/Doc.md',
|
||||
error: 'move boom',
|
||||
},
|
||||
]);
|
||||
// A failure means the refs are NOT advanced — a re-run retries cleanly (§12).
|
||||
expect(res.lastPushedAdvanced).toBe(false);
|
||||
expect(updateRefCalls).toEqual([]);
|
||||
expect(ffCalls).toEqual([]);
|
||||
expect(git.updateRef).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
263
test/classify-rename-moves.test.ts
Normal file
263
test/classify-rename-moves.test.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { classifyRenameMoves } from '../src/push.js';
|
||||
import type {
|
||||
ClassifyRenameMovesDeps,
|
||||
MetaSide,
|
||||
RenameMoveAction,
|
||||
} from '../src/push.js';
|
||||
import type { DocmostMdMeta } from '../packages/docmost-client/src/lib/markdown-document.js';
|
||||
|
||||
// FS→Docmost push #3 (SPEC §5/§6/§16). `classifyRenameMoves` is the PURE half of
|
||||
// the move/rename apply: it resolves each `{pageId, oldPath, newPath}` into the
|
||||
// Docmost op(s) it needs, with NO IO (both resolvers are injected). The key
|
||||
// design (SPEC §5) is that the file PATH is the source of truth for tree
|
||||
// position — the NEW parent comes from the new path, the OLD parent from the old
|
||||
// path — and the title comes from the meta. An op is emitted ONLY when something
|
||||
// really changed; a path-only rename (same parent + same title) is a noop and
|
||||
// NEVER calls Docmost.
|
||||
|
||||
/** Build `metaAt` from a `path|side -> meta` table. */
|
||||
function metaTable(
|
||||
table: Record<string, DocmostMdMeta | null>,
|
||||
): (path: string, side: MetaSide) => DocmostMdMeta | null {
|
||||
return (path, side) => {
|
||||
const key = `${path}|${side}`;
|
||||
return key in table ? table[key] : null;
|
||||
};
|
||||
}
|
||||
|
||||
/** Build `resolveParentPageId` from a `path|side -> parentPageId|null` table. */
|
||||
function parentTable(
|
||||
table: Record<string, string | null>,
|
||||
): (path: string, side: MetaSide) => string | null {
|
||||
return (path, side) => {
|
||||
const key = `${path}|${side}`;
|
||||
return key in table ? table[key] : null;
|
||||
};
|
||||
}
|
||||
|
||||
function deps(
|
||||
metas: Record<string, DocmostMdMeta | null>,
|
||||
parents: Record<string, string | null>,
|
||||
): ClassifyRenameMovesDeps {
|
||||
return {
|
||||
metaAt: metaTable(metas),
|
||||
resolveParentPageId: parentTable(parents),
|
||||
};
|
||||
}
|
||||
|
||||
function meta(partial: Partial<DocmostMdMeta>): DocmostMdMeta {
|
||||
return { version: 1, ...partial };
|
||||
}
|
||||
|
||||
describe('classifyRenameMoves — move-only (parent changed, title same)', () => {
|
||||
it('emits move (new parent) and NO rename', () => {
|
||||
const rms: RenameMoveAction[] = [
|
||||
{ pageId: 'p1', oldPath: 'Doc.md', newPath: 'Parent/Doc.md' },
|
||||
];
|
||||
const out = classifyRenameMoves(
|
||||
rms,
|
||||
deps(
|
||||
{
|
||||
// Same title on both sides.
|
||||
'Parent/Doc.md|current': meta({ title: 'Doc' }),
|
||||
'Doc.md|prev': meta({ title: 'Doc' }),
|
||||
},
|
||||
{
|
||||
// Parent changed: root (null) -> 'parent-id'.
|
||||
'Parent/Doc.md|current': 'parent-id',
|
||||
'Doc.md|prev': null,
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(out).toEqual([
|
||||
{
|
||||
pageId: 'p1',
|
||||
oldPath: 'Doc.md',
|
||||
newPath: 'Parent/Doc.md',
|
||||
move: { parentPageId: 'parent-id' },
|
||||
},
|
||||
]);
|
||||
expect(out[0].rename).toBeUndefined();
|
||||
expect(out[0].noop).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('classifyRenameMoves — rename-only (same parent, title changed)', () => {
|
||||
it('emits rename (new title) and NO move', () => {
|
||||
const rms: RenameMoveAction[] = [
|
||||
{ pageId: 'p2', oldPath: 'Folder/Old.md', newPath: 'Folder/New.md' },
|
||||
];
|
||||
const out = classifyRenameMoves(
|
||||
rms,
|
||||
deps(
|
||||
{
|
||||
'Folder/New.md|current': meta({ title: 'New Title' }),
|
||||
'Folder/Old.md|prev': meta({ title: 'Old Title' }),
|
||||
},
|
||||
{
|
||||
// Same parent on both sides.
|
||||
'Folder/New.md|current': 'folder-id',
|
||||
'Folder/Old.md|prev': 'folder-id',
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(out).toEqual([
|
||||
{
|
||||
pageId: 'p2',
|
||||
oldPath: 'Folder/Old.md',
|
||||
newPath: 'Folder/New.md',
|
||||
rename: { title: 'New Title' },
|
||||
},
|
||||
]);
|
||||
expect(out[0].move).toBeUndefined();
|
||||
expect(out[0].noop).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('classifyRenameMoves — both (parent AND title changed)', () => {
|
||||
it('emits BOTH move and rename', () => {
|
||||
const rms: RenameMoveAction[] = [
|
||||
{ pageId: 'p3', oldPath: 'Old.md', newPath: 'NewParent/New.md' },
|
||||
];
|
||||
const out = classifyRenameMoves(
|
||||
rms,
|
||||
deps(
|
||||
{
|
||||
'NewParent/New.md|current': meta({ title: 'New' }),
|
||||
'Old.md|prev': meta({ title: 'Old' }),
|
||||
},
|
||||
{
|
||||
'NewParent/New.md|current': 'np-id',
|
||||
'Old.md|prev': null,
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(out).toEqual([
|
||||
{
|
||||
pageId: 'p3',
|
||||
oldPath: 'Old.md',
|
||||
newPath: 'NewParent/New.md',
|
||||
move: { parentPageId: 'np-id' },
|
||||
rename: { title: 'New' },
|
||||
},
|
||||
]);
|
||||
expect(out[0].noop).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('classifyRenameMoves — noop (path-only rename, same parent + title)', () => {
|
||||
it('emits noop and NEITHER move NOR rename (SPEC §5: page is its pageId)', () => {
|
||||
const rms: RenameMoveAction[] = [
|
||||
{ pageId: 'p4', oldPath: 'Folder/A.md', newPath: 'Folder/B.md' },
|
||||
];
|
||||
const out = classifyRenameMoves(
|
||||
rms,
|
||||
deps(
|
||||
{
|
||||
'Folder/B.md|current': meta({ title: 'Same' }),
|
||||
'Folder/A.md|prev': meta({ title: 'Same' }),
|
||||
},
|
||||
{
|
||||
'Folder/B.md|current': 'folder-id',
|
||||
'Folder/A.md|prev': 'folder-id',
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(out).toEqual([
|
||||
{
|
||||
pageId: 'p4',
|
||||
oldPath: 'Folder/A.md',
|
||||
newPath: 'Folder/B.md',
|
||||
noop: true,
|
||||
},
|
||||
]);
|
||||
expect(out[0].move).toBeUndefined();
|
||||
expect(out[0].rename).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('classifyRenameMoves — move-to-root (newParent null)', () => {
|
||||
it('emits move with parentPageId null when the file lands at the space root', () => {
|
||||
const rms: RenameMoveAction[] = [
|
||||
{ pageId: 'p5', oldPath: 'Parent/Doc.md', newPath: 'Doc.md' },
|
||||
];
|
||||
const out = classifyRenameMoves(
|
||||
rms,
|
||||
deps(
|
||||
{
|
||||
'Doc.md|current': meta({ title: 'Doc' }),
|
||||
'Parent/Doc.md|prev': meta({ title: 'Doc' }),
|
||||
},
|
||||
{
|
||||
// New parent is ROOT (null), old parent was 'parent-id'.
|
||||
'Doc.md|current': null,
|
||||
'Parent/Doc.md|prev': 'parent-id',
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(out).toEqual([
|
||||
{
|
||||
pageId: 'p5',
|
||||
oldPath: 'Parent/Doc.md',
|
||||
newPath: 'Doc.md',
|
||||
move: { parentPageId: null },
|
||||
},
|
||||
]);
|
||||
expect(out[0].rename).toBeUndefined();
|
||||
expect(out[0].noop).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('classifyRenameMoves — title guards', () => {
|
||||
it('an EMPTY new title is NOT a rename (even if it differs from old)', () => {
|
||||
const rms: RenameMoveAction[] = [
|
||||
{ pageId: 'p6', oldPath: 'Folder/A.md', newPath: 'Folder/B.md' },
|
||||
];
|
||||
const out = classifyRenameMoves(
|
||||
rms,
|
||||
deps(
|
||||
{
|
||||
// New title is empty -> never a rename; same parent -> overall noop.
|
||||
'Folder/B.md|current': meta({ title: '' }),
|
||||
'Folder/A.md|prev': meta({ title: 'Had A Title' }),
|
||||
},
|
||||
{
|
||||
'Folder/B.md|current': 'folder-id',
|
||||
'Folder/A.md|prev': 'folder-id',
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(out[0].rename).toBeUndefined();
|
||||
expect(out[0].move).toBeUndefined();
|
||||
expect(out[0].noop).toBe(true);
|
||||
});
|
||||
|
||||
it('a missing new meta is NOT a rename; a parent change still yields a move', () => {
|
||||
const rms: RenameMoveAction[] = [
|
||||
{ pageId: 'p7', oldPath: 'Doc.md', newPath: 'Parent/Doc.md' },
|
||||
];
|
||||
const out = classifyRenameMoves(
|
||||
rms,
|
||||
deps(
|
||||
{
|
||||
// No current meta entry at all (resolver returns null).
|
||||
'Doc.md|prev': meta({ title: 'Doc' }),
|
||||
},
|
||||
{
|
||||
'Parent/Doc.md|current': 'parent-id',
|
||||
'Doc.md|prev': null,
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(out[0].move).toEqual({ parentPageId: 'parent-id' });
|
||||
expect(out[0].rename).toBeUndefined();
|
||||
expect(out[0].noop).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('classifyRenameMoves — empty input', () => {
|
||||
it('returns an empty array for no rename/move entries', () => {
|
||||
expect(classifyRenameMoves([], deps({}, {}))).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user