confpub-cli 1.7.2__tar.gz → 1.7.3__tar.gz

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.
Files changed (55) hide show
  1. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/PKG-INFO +1 -1
  2. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/__init__.py +1 -1
  3. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/cli.py +0 -2
  4. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/puller.py +77 -25
  5. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_puller.py +106 -15
  6. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/.github/copilot-instructions.md +0 -0
  7. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/.github/workflows/publish.yml +0 -0
  8. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/.gitignore +0 -0
  9. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/CLAUDE.md +0 -0
  10. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/LICENSE +0 -0
  11. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/PRD.md +0 -0
  12. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/README.md +0 -0
  13. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/applier.py +0 -0
  14. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/assets.py +0 -0
  15. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/config.py +0 -0
  16. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/confluence.py +0 -0
  17. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/converter.py +0 -0
  18. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/envelope.py +0 -0
  19. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/errors.py +0 -0
  20. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/front_matter.py +0 -0
  21. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/guide.py +0 -0
  22. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/lockfile.py +0 -0
  23. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/macro_plugin.py +0 -0
  24. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/manifest.py +0 -0
  25. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/output.py +0 -0
  26. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/planner.py +0 -0
  27. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/publish.py +0 -0
  28. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/py.typed +0 -0
  29. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/reverse_converter.py +0 -0
  30. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/validator.py +0 -0
  31. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/verifier.py +0 -0
  32. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub.lock +0 -0
  33. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/pyproject.toml +0 -0
  34. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/__init__.py +0 -0
  35. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/conftest.py +0 -0
  36. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_applier.py +0 -0
  37. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_assets.py +0 -0
  38. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_config.py +0 -0
  39. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_confluence.py +0 -0
  40. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_converter.py +0 -0
  41. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_envelope.py +0 -0
  42. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_errors.py +0 -0
  43. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_front_matter.py +0 -0
  44. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_guide.py +0 -0
  45. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_integration.py +0 -0
  46. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_lockfile.py +0 -0
  47. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_macro_plugin.py +0 -0
  48. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_manifest.py +0 -0
  49. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_output.py +0 -0
  50. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_planner.py +0 -0
  51. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_publish.py +0 -0
  52. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_reverse_converter.py +0 -0
  53. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_validator.py +0 -0
  54. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_verifier.py +0 -0
  55. {confpub_cli-1.7.2 → confpub_cli-1.7.3}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: confpub-cli
3
- Version: 1.7.2
3
+ Version: 1.7.3
4
4
  Summary: Agent-first CLI to publish Markdown to Confluence
5
5
  Project-URL: Homepage, https://github.com/ThomasRohde/confpub-cli
6
6
  Project-URL: Repository, https://github.com/ThomasRohde/confpub-cli.git
@@ -1,3 +1,3 @@
1
1
  """confpub — Agent-first CLI to publish Markdown to Confluence."""
2
2
 
3
- __version__ = "1.7.2"
3
+ __version__ = "1.7.3"
@@ -357,7 +357,6 @@ def page_pull(
357
357
  force: bool = typer.Option(False, "--force", help="Overwrite existing files"),
358
358
  layout: str = typer.Option("flat", "--layout", help="Output layout: flat or nested"),
359
359
  no_attachments: bool = typer.Option(False, "--no-attachments", help="Skip downloading attachments"),
360
- manifest: bool = typer.Option(False, "--manifest", help="Generate confpub.yaml manifest"),
361
360
  ) -> None:
362
361
  """Pull Confluence pages to local Markdown files."""
363
362
  with command_context("page.pull", target={"space": space, "title": title, "page_id": page_id}) as ctx:
@@ -378,7 +377,6 @@ def page_pull(
378
377
  force=force,
379
378
  layout=layout,
380
379
  include_attachments=not no_attachments,
381
- generate_manifest=manifest,
382
380
  )
383
381
  ctx.warnings.extend(result.pop("warnings", []))
384
382
  ctx.result = result
@@ -11,6 +11,8 @@ import re
11
11
  from pathlib import Path
12
12
  from typing import Any
13
13
 
14
+ import yaml
15
+
14
16
  from confpub.confluence import ConfluenceClient, build_client
15
17
  from confpub.output import emit_progress
16
18
  from confpub.errors import (
@@ -147,6 +149,7 @@ def _download_page_attachments(
147
149
  output_dir: str,
148
150
  layout: str,
149
151
  warnings: list[str],
152
+ file_path: str | None = None,
150
153
  ) -> dict[str, str]:
151
154
  """Download attachments for a page. Returns {attachment_name: local_path}.
152
155
 
@@ -158,7 +161,10 @@ def _download_page_attachments(
158
161
  if not attachments:
159
162
  return attachment_map
160
163
 
161
- if layout == "nested":
164
+ if layout == "nested" and file_path:
165
+ # Place assets next to the markdown file (e.g. .../page-slug/assets/)
166
+ assets_dir = os.path.join(os.path.dirname(file_path), "assets")
167
+ elif layout == "nested":
162
168
  assets_dir = os.path.join(output_dir, slug, "assets")
163
169
  else:
164
170
  assets_dir = os.path.join(output_dir, "assets", slug)
@@ -224,6 +230,27 @@ def _build_page_tree(
224
230
  return [root_entry]
225
231
 
226
232
 
233
+ def _build_front_matter(
234
+ title: str,
235
+ page_id: str,
236
+ space: str,
237
+ parent: str | None = None,
238
+ labels: list[str] | None = None,
239
+ ) -> str:
240
+ """Build a YAML front matter block for a pulled markdown file."""
241
+ data: dict[str, Any] = {
242
+ "title": title,
243
+ "page_id": page_id,
244
+ "space": space,
245
+ }
246
+ if parent:
247
+ data["parent"] = parent
248
+ if labels:
249
+ data["labels"] = labels
250
+ yaml_str = yaml.dump(data, default_flow_style=False, allow_unicode=True, sort_keys=False)
251
+ return f"---\n{yaml_str}---\n\n"
252
+
253
+
227
254
  def pull_pages(
228
255
  *,
229
256
  space: str | None = None,
@@ -234,7 +261,6 @@ def pull_pages(
234
261
  force: bool = False,
235
262
  layout: str = "flat",
236
263
  include_attachments: bool = True,
237
- generate_manifest: bool = False,
238
264
  ) -> dict[str, Any]:
239
265
  """Pull pages from Confluence to local Markdown files.
240
266
 
@@ -272,6 +298,19 @@ def pull_pages(
272
298
  # Check for conflicts
273
299
  _check_conflicts(file_paths, force)
274
300
 
301
+ # Build lookup maps for front matter parent resolution
302
+ id_to_title: dict[str, str] = {}
303
+ id_to_parent: dict[str, str | None] = {}
304
+ for entry in all_pages:
305
+ page = entry["page"]
306
+ pid = str(page["id"])
307
+ id_to_title[pid] = page.get("title", "")
308
+ id_to_parent[pid] = str(entry["parent_id"]) if entry["parent_id"] else None
309
+
310
+ # Get the root page's parent in Confluence (for front matter + manifest)
311
+ ancestors = client.get_page_ancestors(root_id)
312
+ root_parent_title = ancestors[-1].get("title", "") if ancestors else None
313
+
275
314
  # Process each page
276
315
  files_result: list[dict[str, Any]] = []
277
316
  total_attachments = 0
@@ -288,27 +327,45 @@ def pull_pages(
288
327
  # Download attachments
289
328
  attachment_map: dict[str, str] = {}
290
329
  attachments_downloaded = 0
330
+ out_path = file_paths[pid]
291
331
  if include_attachments:
292
332
  attachment_map = _download_page_attachments(
293
333
  client, pid, slug, output_dir, layout, pull_warnings,
334
+ file_path=out_path,
294
335
  )
295
336
  attachments_downloaded = len(attachment_map)
296
337
  total_attachments += attachments_downloaded
297
338
 
298
339
  # Convert storage format to markdown
299
340
  body_storage = page.get("body", {}).get("storage", {}).get("value", "")
300
- result = convert_storage_to_markdown(
341
+ conv_result = convert_storage_to_markdown(
301
342
  body_storage, attachment_map=attachment_map,
302
343
  )
303
344
 
304
- # Write markdown file
305
- out_path = file_paths[pid]
306
- os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
307
- Path(out_path).write_text(result.markdown, encoding="utf-8")
308
-
309
345
  # Fetch labels
310
346
  page_labels = [lbl["name"] for lbl in client.get_labels(pid)]
311
347
 
348
+ # Determine parent title for front matter
349
+ if pid == root_id:
350
+ parent_title = root_parent_title
351
+ else:
352
+ par_id = id_to_parent.get(pid)
353
+ parent_title = id_to_title.get(par_id) if par_id else None
354
+
355
+ # Build and prepend front matter
356
+ front_matter = _build_front_matter(
357
+ title=page_title,
358
+ page_id=pid,
359
+ space=root_space,
360
+ parent=parent_title,
361
+ labels=page_labels,
362
+ )
363
+ markdown_content = front_matter + conv_result.markdown
364
+
365
+ # Write markdown file
366
+ os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
367
+ Path(out_path).write_text(markdown_content, encoding="utf-8")
368
+
312
369
  files_result.append({
313
370
  "page_id": pid,
314
371
  "title": page_title,
@@ -318,22 +375,17 @@ def pull_pages(
318
375
  "labels": page_labels,
319
376
  })
320
377
 
321
- # Generate manifest if explicitly requested
322
- manifest_file: str | None = None
323
- if generate_manifest:
324
- root_title = root_page.get("title", "")
325
- # Determine the actual parent of the root page
326
- ancestors = client.get_page_ancestors(root_id)
327
- manifest_parent = ancestors[-1].get("title", root_title) if ancestors else root_title
328
- # Collect labels by page ID for manifest generation
329
- pulled_labels: dict[str, list[str]] = {
330
- f["page_id"]: f.get("labels", []) for f in files_result
331
- }
332
- page_tree = _build_page_tree(all_pages, file_paths, root_id, output_dir, page_labels=pulled_labels)
333
- manifest_yaml = generate_manifest_yaml(root_space, manifest_parent, page_tree)
334
- manifest_path = os.path.join(output_dir, "confpub.yaml")
335
- Path(manifest_path).write_text(manifest_yaml, encoding="utf-8")
336
- manifest_file = manifest_path
378
+ # Always generate manifest
379
+ root_title = root_page.get("title", "")
380
+ manifest_parent = ancestors[-1].get("title", root_title) if ancestors else root_title
381
+ pulled_labels: dict[str, list[str]] = {
382
+ f["page_id"]: f.get("labels", []) for f in files_result
383
+ }
384
+ page_tree = _build_page_tree(all_pages, file_paths, root_id, output_dir, page_labels=pulled_labels)
385
+ manifest_yaml = generate_manifest_yaml(root_space, manifest_parent, page_tree)
386
+ manifest_path = os.path.join(output_dir, "confpub.yaml")
387
+ Path(manifest_path).write_text(manifest_yaml, encoding="utf-8")
388
+ manifest_file: str = manifest_path
337
389
 
338
390
  # Update lockfile
339
391
  lockfile_path = os.path.join(output_dir, "confpub.lock")
@@ -356,6 +408,6 @@ def pull_pages(
356
408
  "summary": {
357
409
  "pages_pulled": len(files_result),
358
410
  "attachments_downloaded": total_attachments,
359
- "manifest_generated": manifest_file is not None,
411
+ "manifest_generated": True,
360
412
  },
361
413
  }
@@ -7,8 +7,10 @@ from unittest.mock import MagicMock, patch
7
7
 
8
8
  import pytest
9
9
 
10
+ import yaml
11
+
10
12
  from confpub.errors import ERR_CONFLICT_FILE_EXISTS, ERR_VALIDATION_REQUIRED, ConfpubError
11
- from confpub.puller import _slugify, pull_pages
13
+ from confpub.puller import _build_front_matter, _slugify, pull_pages
12
14
 
13
15
 
14
16
  # ---------------------------------------------------------------------------
@@ -61,12 +63,16 @@ def _mock_client(pages: dict[str, dict], children: dict[str, list] | None = None
61
63
  def get_page_ancestors(pid):
62
64
  return []
63
65
 
66
+ def get_labels(pid):
67
+ return []
68
+
64
69
  client.get_page_by_id = get_page_by_id
65
70
  client.get_page = get_page
66
71
  client.get_page_children_deep = get_page_children_deep
67
72
  client.get_attachments = get_attachments
68
73
  client.download_attachment = download_attachment
69
74
  client.get_page_ancestors = get_page_ancestors
75
+ client.get_labels = get_labels
70
76
  return client
71
77
 
72
78
 
@@ -113,11 +119,14 @@ class TestSinglePagePull:
113
119
  assert result["files"][0]["page_id"] == "123"
114
120
  assert result["files"][0]["title"] == "Overview"
115
121
 
116
- # File should exist
122
+ # File should exist with front matter
117
123
  md_file = tmp_path / "overview.md"
118
124
  assert md_file.exists()
119
125
  content = md_file.read_text()
126
+ assert content.startswith("---\n")
120
127
  assert "Hello world" in content
128
+ assert "page_id: '123'" in content
129
+ assert "title: Overview" in content
121
130
 
122
131
  def test_pull_single_page_by_space_title(self, tmp_path):
123
132
  page = _make_page("456", "Getting Started", "<h1>Welcome</h1>")
@@ -180,7 +189,6 @@ class TestRecursivePull:
180
189
  page_id="1",
181
190
  output_dir=str(tmp_path),
182
191
  recursive=True,
183
- generate_manifest=True,
184
192
  )
185
193
 
186
194
  assert result["summary"]["manifest_generated"] is True
@@ -410,7 +418,6 @@ class TestDataCenterCompat:
410
418
  result = pull_pages(
411
419
  space="PROJ", title="Root",
412
420
  output_dir=str(tmp_path), recursive=True,
413
- generate_manifest=True,
414
421
  )
415
422
 
416
423
  assert result["summary"]["pages_pulled"] == 2
@@ -482,6 +489,7 @@ class TestMultiLevelRecursivePull:
482
489
  assert (tmp_path / "child.md").exists()
483
490
  assert (tmp_path / "grandchild.md").exists()
484
491
  gc_content = (tmp_path / "grandchild.md").read_text()
492
+ assert gc_content.startswith("---\n")
485
493
  assert "Deep content" in gc_content
486
494
 
487
495
  def test_page_id_recursive_combination(self, tmp_path):
@@ -527,7 +535,6 @@ class TestManifestPathSeparators:
527
535
  output_dir=str(tmp_path),
528
536
  recursive=True,
529
537
  layout="nested",
530
- generate_manifest=True,
531
538
  )
532
539
 
533
540
  manifest_content = Path(result["manifest_file"]).read_text()
@@ -537,10 +544,10 @@ class TestManifestPathSeparators:
537
544
  assert "\\" not in line, f"Backslash found in manifest path: {line}"
538
545
 
539
546
 
540
- class TestNestedLayoutNoAutoManifest:
541
- """Bug 4: --layout nested without --manifest should NOT create confpub.yaml."""
547
+ class TestManifestAlwaysGenerated:
548
+ """Manifest is always generated regardless of layout."""
542
549
 
543
- def test_nested_without_manifest_flag(self, tmp_path):
550
+ def test_nested_always_generates_manifest(self, tmp_path):
544
551
  root = _make_page("1", "Root")
545
552
  child = _make_page("2", "Child")
546
553
  pages = {"1": root, "2": child}
@@ -553,17 +560,16 @@ class TestNestedLayoutNoAutoManifest:
553
560
  output_dir=str(tmp_path),
554
561
  recursive=True,
555
562
  layout="nested",
556
- generate_manifest=False,
557
563
  )
558
564
 
559
- assert result["summary"]["manifest_generated"] is False
560
- assert result["manifest_file"] is None
561
- assert not (tmp_path / "confpub.yaml").exists()
565
+ assert result["summary"]["manifest_generated"] is True
566
+ assert result["manifest_file"] is not None
567
+ assert (tmp_path / "confpub.yaml").exists()
562
568
 
563
569
 
564
570
  class TestManifestFlag:
565
- def test_single_page_with_manifest_flag(self, tmp_path):
566
- """--manifest generates confpub.yaml even for a single page."""
571
+ def test_single_page_generates_manifest(self, tmp_path):
572
+ """Manifest is always generated, even for a single page."""
567
573
  page = _make_page("1", "Solo Page")
568
574
  client = _mock_client({"1": page})
569
575
 
@@ -571,7 +577,6 @@ class TestManifestFlag:
571
577
  result = pull_pages(
572
578
  page_id="1",
573
579
  output_dir=str(tmp_path),
574
- generate_manifest=True,
575
580
  )
576
581
 
577
582
  assert result["summary"]["manifest_generated"] is True
@@ -580,3 +585,89 @@ class TestManifestFlag:
580
585
  assert manifest.exists()
581
586
  content = manifest.read_text()
582
587
  assert "Solo Page" in content
588
+
589
+
590
+ # ---------------------------------------------------------------------------
591
+ # Front matter tests
592
+ # ---------------------------------------------------------------------------
593
+
594
+
595
+ class TestBuildFrontMatter:
596
+ def test_basic_fields(self):
597
+ fm = _build_front_matter("My Page", "123", "SD")
598
+ assert fm.startswith("---\n")
599
+ assert fm.endswith("---\n\n")
600
+ parsed = yaml.safe_load(fm.strip("- \n"))
601
+ assert parsed["title"] == "My Page"
602
+ assert parsed["page_id"] == "123"
603
+ assert parsed["space"] == "SD"
604
+ assert "parent" not in parsed
605
+ assert "labels" not in parsed
606
+
607
+ def test_with_parent_and_labels(self):
608
+ fm = _build_front_matter("Child", "456", "SD", parent="Parent Page", labels=["a", "b"])
609
+ parsed = yaml.safe_load(fm.strip("- \n"))
610
+ assert parsed["parent"] == "Parent Page"
611
+ assert parsed["labels"] == ["a", "b"]
612
+
613
+ def test_empty_labels_omitted(self):
614
+ fm = _build_front_matter("Page", "1", "SD", labels=[])
615
+ parsed = yaml.safe_load(fm.strip("- \n"))
616
+ assert "labels" not in parsed
617
+
618
+
619
+ class TestFrontMatterInPulledFiles:
620
+ def test_front_matter_fields_correct(self, tmp_path):
621
+ """Pulled files contain correct front matter metadata."""
622
+ root = _make_page("1", "Root", space_key="SD")
623
+ child = _make_page("2", "Child Page", "<p>Body</p>", space_key="SD")
624
+ pages = {"1": root, "2": child}
625
+ children = {"1": [child]}
626
+ client = _mock_client(pages, children)
627
+ client.get_labels = lambda pid: [{"name": "my-label"}] if pid == "2" else []
628
+
629
+ with patch("confpub.puller.build_client", return_value=client):
630
+ pull_pages(
631
+ page_id="1",
632
+ output_dir=str(tmp_path),
633
+ recursive=True,
634
+ )
635
+
636
+ # Check child file front matter
637
+ child_content = (tmp_path / "child-page.md").read_text()
638
+ assert child_content.startswith("---\n")
639
+ # Extract YAML block
640
+ parts = child_content.split("---\n", 2)
641
+ fm = yaml.safe_load(parts[1])
642
+ assert fm["title"] == "Child Page"
643
+ assert fm["page_id"] == "2"
644
+ assert fm["space"] == "SD"
645
+ assert fm["parent"] == "Root"
646
+ assert fm["labels"] == ["my-label"]
647
+
648
+ def test_root_page_has_no_parent_when_no_ancestors(self, tmp_path):
649
+ """Root page omits parent when Confluence has no ancestors."""
650
+ page = _make_page("1", "Root")
651
+ client = _mock_client({"1": page})
652
+
653
+ with patch("confpub.puller.build_client", return_value=client):
654
+ pull_pages(page_id="1", output_dir=str(tmp_path))
655
+
656
+ content = (tmp_path / "root.md").read_text()
657
+ parts = content.split("---\n", 2)
658
+ fm = yaml.safe_load(parts[1])
659
+ assert "parent" not in fm
660
+
661
+ def test_root_page_has_parent_from_ancestors(self, tmp_path):
662
+ """Root page gets parent from Confluence ancestors."""
663
+ page = _make_page("1", "Root")
664
+ client = _mock_client({"1": page})
665
+ client.get_page_ancestors = lambda pid: [{"title": "Space Home"}]
666
+
667
+ with patch("confpub.puller.build_client", return_value=client):
668
+ pull_pages(page_id="1", output_dir=str(tmp_path))
669
+
670
+ content = (tmp_path / "root.md").read_text()
671
+ parts = content.split("---\n", 2)
672
+ fm = yaml.safe_load(parts[1])
673
+ assert fm["parent"] == "Space Home"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes