@aaronshaf/confluence-cli 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +69 -0
- package/package.json +73 -0
- package/src/cli/commands/attachments.ts +113 -0
- package/src/cli/commands/clone.ts +188 -0
- package/src/cli/commands/comments.ts +56 -0
- package/src/cli/commands/create.ts +58 -0
- package/src/cli/commands/delete.ts +46 -0
- package/src/cli/commands/doctor.ts +161 -0
- package/src/cli/commands/duplicate-check.ts +89 -0
- package/src/cli/commands/file-rename.ts +113 -0
- package/src/cli/commands/folder-hierarchy.ts +241 -0
- package/src/cli/commands/info.ts +56 -0
- package/src/cli/commands/labels.ts +53 -0
- package/src/cli/commands/move.ts +23 -0
- package/src/cli/commands/open.ts +145 -0
- package/src/cli/commands/pull.ts +241 -0
- package/src/cli/commands/push-errors.ts +40 -0
- package/src/cli/commands/push.ts +699 -0
- package/src/cli/commands/search.ts +62 -0
- package/src/cli/commands/setup.ts +124 -0
- package/src/cli/commands/spaces.ts +42 -0
- package/src/cli/commands/status.ts +88 -0
- package/src/cli/commands/tree.ts +190 -0
- package/src/cli/help.ts +425 -0
- package/src/cli/index.ts +413 -0
- package/src/cli/utils/browser.ts +34 -0
- package/src/cli/utils/progress-reporter.ts +49 -0
- package/src/cli.ts +6 -0
- package/src/lib/config.ts +156 -0
- package/src/lib/confluence-client/attachment-operations.ts +221 -0
- package/src/lib/confluence-client/client.ts +653 -0
- package/src/lib/confluence-client/comment-operations.ts +60 -0
- package/src/lib/confluence-client/folder-operations.ts +203 -0
- package/src/lib/confluence-client/index.ts +47 -0
- package/src/lib/confluence-client/label-operations.ts +102 -0
- package/src/lib/confluence-client/page-operations.ts +270 -0
- package/src/lib/confluence-client/search-operations.ts +60 -0
- package/src/lib/confluence-client/types.ts +329 -0
- package/src/lib/confluence-client/user-operations.ts +58 -0
- package/src/lib/dependency-sorter.ts +233 -0
- package/src/lib/errors.ts +237 -0
- package/src/lib/file-scanner.ts +195 -0
- package/src/lib/formatters.ts +314 -0
- package/src/lib/health-check.ts +204 -0
- package/src/lib/markdown/converter.ts +427 -0
- package/src/lib/markdown/frontmatter.ts +116 -0
- package/src/lib/markdown/html-converter.ts +398 -0
- package/src/lib/markdown/index.ts +21 -0
- package/src/lib/markdown/link-converter.ts +189 -0
- package/src/lib/markdown/reference-updater.ts +251 -0
- package/src/lib/markdown/slugify.ts +32 -0
- package/src/lib/page-state.ts +195 -0
- package/src/lib/resolve-page-target.ts +33 -0
- package/src/lib/space-config.ts +264 -0
- package/src/lib/sync/cleanup.ts +50 -0
- package/src/lib/sync/folder-path.ts +61 -0
- package/src/lib/sync/index.ts +2 -0
- package/src/lib/sync/link-resolution-pass.ts +139 -0
- package/src/lib/sync/sync-engine.ts +681 -0
- package/src/lib/sync/sync-specific.ts +221 -0
- package/src/lib/sync/types.ts +42 -0
- package/src/test/attachments.test.ts +68 -0
- package/src/test/clone.test.ts +373 -0
- package/src/test/comments.test.ts +53 -0
- package/src/test/config.test.ts +209 -0
- package/src/test/confluence-client.test.ts +535 -0
- package/src/test/delete.test.ts +39 -0
- package/src/test/dependency-sorter.test.ts +384 -0
- package/src/test/errors.test.ts +199 -0
- package/src/test/file-rename.test.ts +305 -0
- package/src/test/file-scanner.test.ts +331 -0
- package/src/test/folder-hierarchy.test.ts +337 -0
- package/src/test/formatters.test.ts +213 -0
- package/src/test/html-converter.test.ts +399 -0
- package/src/test/info.test.ts +56 -0
- package/src/test/labels.test.ts +70 -0
- package/src/test/link-conversion-integration.test.ts +189 -0
- package/src/test/link-converter.test.ts +413 -0
- package/src/test/link-resolution-pass.test.ts +368 -0
- package/src/test/markdown.test.ts +443 -0
- package/src/test/mocks/handlers.ts +228 -0
- package/src/test/move.test.ts +53 -0
- package/src/test/msw-schema-validation.ts +151 -0
- package/src/test/page-state.test.ts +542 -0
- package/src/test/push.test.ts +551 -0
- package/src/test/reference-updater.test.ts +293 -0
- package/src/test/resolve-page-target.test.ts +55 -0
- package/src/test/search.test.ts +64 -0
- package/src/test/setup-msw.ts +75 -0
- package/src/test/space-config.test.ts +516 -0
- package/src/test/spaces.test.ts +53 -0
- package/src/test/sync-engine.test.ts +486 -0
- package/src/types/turndown-plugin-gfm.d.ts +9 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { http, HttpResponse } from 'msw';
|
|
6
|
+
import { SyncEngine } from '../lib/sync/sync-engine.js';
|
|
7
|
+
import { writeSpaceConfig, type SpaceConfigWithState } from '../lib/space-config.js';
|
|
8
|
+
import { server } from './setup-msw.js';
|
|
9
|
+
import { createValidPage, createValidSpace } from './msw-schema-validation.js';
|
|
10
|
+
import { parseMarkdown } from '../lib/markdown/frontmatter.js';
|
|
11
|
+
|
|
12
|
+
const testConfig = {
|
|
13
|
+
confluenceUrl: 'https://test.atlassian.net',
|
|
14
|
+
email: 'test@example.com',
|
|
15
|
+
apiToken: 'test-token',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe('SyncEngine', () => {
|
|
19
|
+
let testDir: string;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
testDir = join(tmpdir(), `cn-test-${Date.now()}`);
|
|
23
|
+
mkdirSync(testDir, { recursive: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
if (existsSync(testDir)) {
|
|
28
|
+
rmSync(testDir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('initSync', () => {
|
|
33
|
+
test('initializes sync for a space', async () => {
|
|
34
|
+
server.use(
|
|
35
|
+
http.get('*/wiki/api/v2/spaces', ({ request }) => {
|
|
36
|
+
const url = new URL(request.url);
|
|
37
|
+
const keys = url.searchParams.get('keys');
|
|
38
|
+
if (keys === 'TEST') {
|
|
39
|
+
return HttpResponse.json({
|
|
40
|
+
results: [createValidSpace({ id: 'space-123', key: 'TEST', name: 'Test Space' })],
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return HttpResponse.json({ results: [] });
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const engine = new SyncEngine(testConfig);
|
|
48
|
+
const config = await engine.initSync(testDir, 'TEST');
|
|
49
|
+
|
|
50
|
+
expect(config.spaceKey).toBe('TEST');
|
|
51
|
+
expect(config.spaceId).toBe('space-123');
|
|
52
|
+
expect(config.spaceName).toBe('Test Space');
|
|
53
|
+
|
|
54
|
+
// Check that .confluence.json was created
|
|
55
|
+
const configPath = join(testDir, '.confluence.json');
|
|
56
|
+
expect(existsSync(configPath)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('fetchPageTree', () => {
|
|
61
|
+
test('fetches all pages in a space', async () => {
|
|
62
|
+
const engine = new SyncEngine(testConfig);
|
|
63
|
+
const pages = await engine.fetchPageTree('space-123');
|
|
64
|
+
|
|
65
|
+
expect(pages).toBeArray();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('buildPageTree', () => {
|
|
70
|
+
test('builds tree from flat pages', () => {
|
|
71
|
+
const pages = [
|
|
72
|
+
{ id: 'page-1', title: 'Home', spaceId: 'space-123', status: 'current', parentId: null },
|
|
73
|
+
{ id: 'page-2', title: 'Getting Started', spaceId: 'space-123', status: 'current', parentId: 'page-1' },
|
|
74
|
+
{ id: 'page-3', title: 'API Reference', spaceId: 'space-123', status: 'current', parentId: 'page-1' },
|
|
75
|
+
{ id: 'page-4', title: 'Installation', spaceId: 'space-123', status: 'current', parentId: 'page-2' },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
const engine = new SyncEngine(testConfig);
|
|
79
|
+
const tree = engine.buildPageTree(pages);
|
|
80
|
+
|
|
81
|
+
expect(tree).toHaveLength(1);
|
|
82
|
+
expect(tree[0].page.title).toBe('Home');
|
|
83
|
+
expect(tree[0].children).toHaveLength(2);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('handles orphan pages', () => {
|
|
87
|
+
const pages = [
|
|
88
|
+
{ id: 'page-1', title: 'Page 1', spaceId: 'space-123', status: 'current', parentId: 'missing-parent' },
|
|
89
|
+
{ id: 'page-2', title: 'Page 2', spaceId: 'space-123', status: 'current', parentId: null },
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
const engine = new SyncEngine(testConfig);
|
|
93
|
+
const tree = engine.buildPageTree(pages);
|
|
94
|
+
|
|
95
|
+
expect(tree).toHaveLength(2);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('computeDiff', () => {
|
|
100
|
+
test('detects added pages', () => {
|
|
101
|
+
const remotePages = [
|
|
102
|
+
{ id: 'page-1', title: 'Page 1', spaceId: 'space-123', status: 'current', version: { number: 1 } },
|
|
103
|
+
{ id: 'page-2', title: 'Page 2', spaceId: 'space-123', status: 'current', version: { number: 1 } },
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
const localConfig: SpaceConfigWithState = {
|
|
107
|
+
spaceKey: 'TEST',
|
|
108
|
+
spaceId: 'space-123',
|
|
109
|
+
spaceName: 'Test Space',
|
|
110
|
+
pages: {},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const engine = new SyncEngine(testConfig);
|
|
114
|
+
const diff = engine.computeDiff(remotePages, localConfig);
|
|
115
|
+
|
|
116
|
+
expect(diff.added).toHaveLength(2);
|
|
117
|
+
expect(diff.modified).toHaveLength(0);
|
|
118
|
+
expect(diff.deleted).toHaveLength(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('detects modified pages', () => {
|
|
122
|
+
const remotePages = [
|
|
123
|
+
{ id: 'page-1', title: 'Page 1', spaceId: 'space-123', status: 'current', version: { number: 2 } },
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
// Per ADR-0024: pages is now Record<string, string> (pageId -> localPath)
|
|
127
|
+
const localConfig: SpaceConfigWithState = {
|
|
128
|
+
spaceKey: 'TEST',
|
|
129
|
+
spaceId: 'space-123',
|
|
130
|
+
spaceName: 'Test Space',
|
|
131
|
+
pages: {
|
|
132
|
+
'page-1': 'page-1.md',
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const engine = new SyncEngine(testConfig);
|
|
137
|
+
// Without PageStateCache, local version defaults to 0, so remote v2 > local v0 -> modified
|
|
138
|
+
const diff = engine.computeDiff(remotePages, localConfig);
|
|
139
|
+
|
|
140
|
+
expect(diff.added).toHaveLength(0);
|
|
141
|
+
expect(diff.modified).toHaveLength(1);
|
|
142
|
+
expect(diff.deleted).toHaveLength(0);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('detects deleted pages', () => {
|
|
146
|
+
const remotePages: any[] = [];
|
|
147
|
+
|
|
148
|
+
// Per ADR-0024: pages is now Record<string, string> (pageId -> localPath)
|
|
149
|
+
const localConfig: SpaceConfigWithState = {
|
|
150
|
+
spaceKey: 'TEST',
|
|
151
|
+
spaceId: 'space-123',
|
|
152
|
+
spaceName: 'Test Space',
|
|
153
|
+
pages: {
|
|
154
|
+
'page-1': 'page-1.md',
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const engine = new SyncEngine(testConfig);
|
|
159
|
+
const diff = engine.computeDiff(remotePages, localConfig);
|
|
160
|
+
|
|
161
|
+
expect(diff.added).toHaveLength(0);
|
|
162
|
+
expect(diff.modified).toHaveLength(0);
|
|
163
|
+
expect(diff.deleted).toHaveLength(1);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('handles null localConfig', () => {
|
|
167
|
+
const remotePages = [
|
|
168
|
+
{ id: 'page-1', title: 'Page 1', spaceId: 'space-123', status: 'current', version: { number: 1 } },
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
const engine = new SyncEngine(testConfig);
|
|
172
|
+
const diff = engine.computeDiff(remotePages, null);
|
|
173
|
+
|
|
174
|
+
expect(diff.added).toHaveLength(1);
|
|
175
|
+
expect(diff.modified).toHaveLength(0);
|
|
176
|
+
expect(diff.deleted).toHaveLength(0);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('filters out archived pages from remote', () => {
|
|
180
|
+
const remotePages = [
|
|
181
|
+
{ id: 'page-1', title: 'Current Page', spaceId: 'space-123', status: 'current', version: { number: 1 } },
|
|
182
|
+
{ id: 'page-2', title: 'Archived Page', spaceId: 'space-123', status: 'archived', version: { number: 1 } },
|
|
183
|
+
{ id: 'page-3', title: 'Another Current', spaceId: 'space-123', status: 'current', version: { number: 1 } },
|
|
184
|
+
{ id: 'page-4', title: 'Draft Page', spaceId: 'space-123', status: 'draft', version: { number: 1 } },
|
|
185
|
+
{ id: 'page-5', title: 'Trashed Page', spaceId: 'space-123', status: 'trashed', version: { number: 1 } },
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
const localConfig: SpaceConfigWithState = {
|
|
189
|
+
spaceKey: 'TEST',
|
|
190
|
+
spaceId: 'space-123',
|
|
191
|
+
spaceName: 'Test Space',
|
|
192
|
+
pages: {},
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const engine = new SyncEngine(testConfig);
|
|
196
|
+
const diff = engine.computeDiff(remotePages, localConfig);
|
|
197
|
+
|
|
198
|
+
// Only the 2 current pages should be added (filters out archived, draft, and trashed)
|
|
199
|
+
expect(diff.added).toHaveLength(2);
|
|
200
|
+
expect(diff.added[0].pageId).toBe('page-1');
|
|
201
|
+
expect(diff.added[1].pageId).toBe('page-3');
|
|
202
|
+
expect(diff.modified).toHaveLength(0);
|
|
203
|
+
expect(diff.deleted).toHaveLength(0);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('treats locally-synced archived pages as deleted', () => {
|
|
207
|
+
const remotePages = [
|
|
208
|
+
{ id: 'page-1', title: 'Current Page', spaceId: 'space-123', status: 'current', version: { number: 1 } },
|
|
209
|
+
{ id: 'page-2', title: 'Archived Page', spaceId: 'space-123', status: 'archived', version: { number: 1 } },
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
const localConfig: SpaceConfigWithState = {
|
|
213
|
+
spaceKey: 'TEST',
|
|
214
|
+
spaceId: 'space-123',
|
|
215
|
+
spaceName: 'Test Space',
|
|
216
|
+
pages: {
|
|
217
|
+
'page-1': 'page-1.md',
|
|
218
|
+
'page-2': 'page-2.md', // This page is archived remotely
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Provide PageStateCache so page-1 is not seen as modified
|
|
223
|
+
const pageState = {
|
|
224
|
+
pages: new Map([
|
|
225
|
+
['page-1', { pageId: 'page-1', localPath: 'page-1.md', title: 'Current Page', version: 1 }],
|
|
226
|
+
['page-2', { pageId: 'page-2', localPath: 'page-2.md', title: 'Archived Page', version: 1 }],
|
|
227
|
+
]),
|
|
228
|
+
pathToPageId: new Map([
|
|
229
|
+
['page-1.md', 'page-1'],
|
|
230
|
+
['page-2.md', 'page-2'],
|
|
231
|
+
]),
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const engine = new SyncEngine(testConfig);
|
|
235
|
+
const diff = engine.computeDiff(remotePages, localConfig, pageState);
|
|
236
|
+
|
|
237
|
+
expect(diff.added).toHaveLength(0);
|
|
238
|
+
expect(diff.modified).toHaveLength(0);
|
|
239
|
+
// page-2 should be detected as deleted because it's archived
|
|
240
|
+
expect(diff.deleted).toHaveLength(1);
|
|
241
|
+
expect(diff.deleted[0].pageId).toBe('page-2');
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('sync', () => {
|
|
246
|
+
test('fails without space configuration', async () => {
|
|
247
|
+
const engine = new SyncEngine(testConfig);
|
|
248
|
+
const result = await engine.sync(testDir);
|
|
249
|
+
|
|
250
|
+
expect(result.success).toBe(false);
|
|
251
|
+
expect(result.errors).toHaveLength(1);
|
|
252
|
+
expect(result.errors[0]).toContain('No space configuration found');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('performs dry run without changes', async () => {
|
|
256
|
+
// Set up space config
|
|
257
|
+
const spaceConfig: SpaceConfigWithState = {
|
|
258
|
+
spaceKey: 'TEST',
|
|
259
|
+
spaceId: 'space-123',
|
|
260
|
+
spaceName: 'Test Space',
|
|
261
|
+
pages: {},
|
|
262
|
+
};
|
|
263
|
+
writeSpaceConfig(testDir, spaceConfig);
|
|
264
|
+
|
|
265
|
+
const engine = new SyncEngine(testConfig);
|
|
266
|
+
const result = await engine.sync(testDir, { dryRun: true });
|
|
267
|
+
|
|
268
|
+
expect(result.success).toBe(true);
|
|
269
|
+
// In dry run, no files should be created
|
|
270
|
+
const files = existsSync(join(testDir, 'home.md'));
|
|
271
|
+
expect(files).toBe(false);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('syncs new pages', async () => {
|
|
275
|
+
// Set up mocks for pages
|
|
276
|
+
server.use(
|
|
277
|
+
http.get('*/wiki/api/v2/spaces/:spaceId/pages', () => {
|
|
278
|
+
return HttpResponse.json({
|
|
279
|
+
results: [
|
|
280
|
+
createValidPage({
|
|
281
|
+
id: 'page-1',
|
|
282
|
+
title: 'Home',
|
|
283
|
+
spaceId: 'space-123',
|
|
284
|
+
body: '<p>Welcome!</p>',
|
|
285
|
+
}),
|
|
286
|
+
],
|
|
287
|
+
});
|
|
288
|
+
}),
|
|
289
|
+
http.get('*/wiki/api/v2/pages/:pageId', ({ params }) => {
|
|
290
|
+
return HttpResponse.json(
|
|
291
|
+
createValidPage({
|
|
292
|
+
id: params.pageId as string,
|
|
293
|
+
title: 'Home',
|
|
294
|
+
spaceId: 'space-123',
|
|
295
|
+
body: '<p>Welcome!</p>',
|
|
296
|
+
}),
|
|
297
|
+
);
|
|
298
|
+
}),
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
// Set up space config
|
|
302
|
+
const spaceConfig: SpaceConfigWithState = {
|
|
303
|
+
spaceKey: 'TEST',
|
|
304
|
+
spaceId: 'space-123',
|
|
305
|
+
spaceName: 'Test Space',
|
|
306
|
+
pages: {},
|
|
307
|
+
};
|
|
308
|
+
writeSpaceConfig(testDir, spaceConfig);
|
|
309
|
+
|
|
310
|
+
const engine = new SyncEngine(testConfig);
|
|
311
|
+
const result = await engine.sync(testDir);
|
|
312
|
+
|
|
313
|
+
expect(result.success).toBe(true);
|
|
314
|
+
expect(result.changes.added).toHaveLength(1);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test('skips pages with reserved filenames during sync', async () => {
|
|
318
|
+
// Set up mocks for pages - include a page titled "Claude" which would generate claude.md
|
|
319
|
+
server.use(
|
|
320
|
+
http.get('*/wiki/api/v2/spaces/:spaceId/pages', () => {
|
|
321
|
+
return HttpResponse.json({
|
|
322
|
+
results: [
|
|
323
|
+
createValidPage({
|
|
324
|
+
id: 'page-1',
|
|
325
|
+
title: 'Home',
|
|
326
|
+
spaceId: 'space-123',
|
|
327
|
+
body: '<p>Welcome!</p>',
|
|
328
|
+
}),
|
|
329
|
+
createValidPage({
|
|
330
|
+
id: 'page-2',
|
|
331
|
+
title: 'Claude',
|
|
332
|
+
spaceId: 'space-123',
|
|
333
|
+
parentId: 'page-1',
|
|
334
|
+
body: '<p>This should be skipped</p>',
|
|
335
|
+
}),
|
|
336
|
+
createValidPage({
|
|
337
|
+
id: 'page-3',
|
|
338
|
+
title: 'Agents',
|
|
339
|
+
spaceId: 'space-123',
|
|
340
|
+
parentId: 'page-1',
|
|
341
|
+
body: '<p>This should also be skipped</p>',
|
|
342
|
+
}),
|
|
343
|
+
],
|
|
344
|
+
});
|
|
345
|
+
}),
|
|
346
|
+
http.get('*/wiki/api/v2/pages/:pageId', ({ params }) => {
|
|
347
|
+
const pageId = params.pageId as string;
|
|
348
|
+
const titles: Record<string, string> = {
|
|
349
|
+
'page-1': 'Home',
|
|
350
|
+
'page-2': 'Claude',
|
|
351
|
+
'page-3': 'Agents',
|
|
352
|
+
};
|
|
353
|
+
return HttpResponse.json(
|
|
354
|
+
createValidPage({
|
|
355
|
+
id: pageId,
|
|
356
|
+
title: titles[pageId] || 'Unknown',
|
|
357
|
+
spaceId: 'space-123',
|
|
358
|
+
parentId: pageId === 'page-1' ? undefined : 'page-1',
|
|
359
|
+
body: '<p>Content</p>',
|
|
360
|
+
}),
|
|
361
|
+
);
|
|
362
|
+
}),
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
// Set up space config
|
|
366
|
+
const spaceConfig: SpaceConfigWithState = {
|
|
367
|
+
spaceKey: 'TEST',
|
|
368
|
+
spaceId: 'space-123',
|
|
369
|
+
spaceName: 'Test Space',
|
|
370
|
+
pages: {},
|
|
371
|
+
};
|
|
372
|
+
writeSpaceConfig(testDir, spaceConfig);
|
|
373
|
+
|
|
374
|
+
const engine = new SyncEngine(testConfig);
|
|
375
|
+
const result = await engine.sync(testDir);
|
|
376
|
+
|
|
377
|
+
expect(result.success).toBe(true);
|
|
378
|
+
// 3 pages were added to diff, but 2 should be skipped
|
|
379
|
+
expect(result.changes.added).toHaveLength(3);
|
|
380
|
+
// Only README.md (home page) should exist, not claude.md or agents.md
|
|
381
|
+
expect(existsSync(join(testDir, 'README.md'))).toBe(true);
|
|
382
|
+
expect(existsSync(join(testDir, 'claude.md'))).toBe(false);
|
|
383
|
+
expect(existsSync(join(testDir, 'agents.md'))).toBe(false);
|
|
384
|
+
// Should have warnings about skipped pages (check for "reserved filename" in the message)
|
|
385
|
+
expect(result.warnings.some((w) => w.includes('reserved filename') && w.includes('Claude'))).toBe(true);
|
|
386
|
+
expect(result.warnings.some((w) => w.includes('reserved filename') && w.includes('Agents'))).toBe(true);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test('includes child_count in frontmatter for synced pages', async () => {
|
|
390
|
+
// Set up page hierarchy:
|
|
391
|
+
// Root (page-root) - 2 children
|
|
392
|
+
// ├─ Child 1 (page-child1) - 0 children
|
|
393
|
+
// └─ Child 2 (page-child2) - 1 child
|
|
394
|
+
// └─ Grandchild (page-grandchild) - 0 children
|
|
395
|
+
server.use(
|
|
396
|
+
http.get('*/wiki/api/v2/spaces/:spaceId/pages', () => {
|
|
397
|
+
return HttpResponse.json({
|
|
398
|
+
results: [
|
|
399
|
+
createValidPage({
|
|
400
|
+
id: 'page-root',
|
|
401
|
+
title: 'Root Page',
|
|
402
|
+
spaceId: 'space-123',
|
|
403
|
+
body: '<p>Root content</p>',
|
|
404
|
+
}),
|
|
405
|
+
createValidPage({
|
|
406
|
+
id: 'page-child1',
|
|
407
|
+
title: 'Child 1',
|
|
408
|
+
spaceId: 'space-123',
|
|
409
|
+
parentId: 'page-root',
|
|
410
|
+
body: '<p>Child 1 content</p>',
|
|
411
|
+
}),
|
|
412
|
+
createValidPage({
|
|
413
|
+
id: 'page-child2',
|
|
414
|
+
title: 'Child 2',
|
|
415
|
+
spaceId: 'space-123',
|
|
416
|
+
parentId: 'page-root',
|
|
417
|
+
body: '<p>Child 2 content</p>',
|
|
418
|
+
}),
|
|
419
|
+
createValidPage({
|
|
420
|
+
id: 'page-grandchild',
|
|
421
|
+
title: 'Grandchild',
|
|
422
|
+
spaceId: 'space-123',
|
|
423
|
+
parentId: 'page-child2',
|
|
424
|
+
body: '<p>Grandchild content</p>',
|
|
425
|
+
}),
|
|
426
|
+
],
|
|
427
|
+
});
|
|
428
|
+
}),
|
|
429
|
+
http.get('*/wiki/api/v2/pages/:pageId', ({ params }) => {
|
|
430
|
+
const pageId = params.pageId as string;
|
|
431
|
+
const pageData: Record<string, { title: string; parentId?: string }> = {
|
|
432
|
+
'page-root': { title: 'Root Page' },
|
|
433
|
+
'page-child1': { title: 'Child 1', parentId: 'page-root' },
|
|
434
|
+
'page-child2': { title: 'Child 2', parentId: 'page-root' },
|
|
435
|
+
'page-grandchild': { title: 'Grandchild', parentId: 'page-child2' },
|
|
436
|
+
};
|
|
437
|
+
const data = pageData[pageId] || { title: 'Unknown' };
|
|
438
|
+
return HttpResponse.json(
|
|
439
|
+
createValidPage({
|
|
440
|
+
id: pageId,
|
|
441
|
+
title: data.title,
|
|
442
|
+
spaceId: 'space-123',
|
|
443
|
+
parentId: data.parentId,
|
|
444
|
+
body: `<p>${data.title} content</p>`,
|
|
445
|
+
}),
|
|
446
|
+
);
|
|
447
|
+
}),
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
// Set up space config
|
|
451
|
+
const spaceConfig: SpaceConfigWithState = {
|
|
452
|
+
spaceKey: 'TEST',
|
|
453
|
+
spaceId: 'space-123',
|
|
454
|
+
spaceName: 'Test Space',
|
|
455
|
+
pages: {},
|
|
456
|
+
};
|
|
457
|
+
writeSpaceConfig(testDir, spaceConfig);
|
|
458
|
+
|
|
459
|
+
const engine = new SyncEngine(testConfig);
|
|
460
|
+
const result = await engine.sync(testDir);
|
|
461
|
+
|
|
462
|
+
expect(result.success).toBe(true);
|
|
463
|
+
expect(result.changes.added).toHaveLength(4);
|
|
464
|
+
|
|
465
|
+
// Verify child_count in synced files
|
|
466
|
+
const rootContent = readFileSync(join(testDir, 'README.md'), 'utf-8');
|
|
467
|
+
const child1Content = readFileSync(join(testDir, 'child-1.md'), 'utf-8');
|
|
468
|
+
const child2Content = readFileSync(join(testDir, 'child-2/README.md'), 'utf-8');
|
|
469
|
+
const grandchildContent = readFileSync(join(testDir, 'child-2/grandchild.md'), 'utf-8');
|
|
470
|
+
|
|
471
|
+
const rootFrontmatter = parseMarkdown(rootContent).frontmatter;
|
|
472
|
+
const child1Frontmatter = parseMarkdown(child1Content).frontmatter;
|
|
473
|
+
const child2Frontmatter = parseMarkdown(child2Content).frontmatter;
|
|
474
|
+
const grandchildFrontmatter = parseMarkdown(grandchildContent).frontmatter;
|
|
475
|
+
|
|
476
|
+
// Root has 2 direct children
|
|
477
|
+
expect(rootFrontmatter.child_count).toBe(2);
|
|
478
|
+
// Child 1 has 0 children (leaf page)
|
|
479
|
+
expect(child1Frontmatter.child_count).toBe(0);
|
|
480
|
+
// Child 2 has 1 child
|
|
481
|
+
expect(child2Frontmatter.child_count).toBe(1);
|
|
482
|
+
// Grandchild has 0 children (leaf page)
|
|
483
|
+
expect(grandchildFrontmatter.child_count).toBe(0);
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
declare module 'turndown-plugin-gfm' {
|
|
2
|
+
import type TurndownService from 'turndown';
|
|
3
|
+
|
|
4
|
+
export function gfm(turndownService: TurndownService): void;
|
|
5
|
+
export function tables(turndownService: TurndownService): void;
|
|
6
|
+
export function strikethrough(turndownService: TurndownService): void;
|
|
7
|
+
export function taskListItems(turndownService: TurndownService): void;
|
|
8
|
+
export function highlightedCodeBlock(turndownService: TurndownService): void;
|
|
9
|
+
}
|