mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 00:26:26 +00:00
feat(notion-sync): one-way read-only pipeline view in Notion via MCP (#169)
Adds /notion-sync per the conditions agreed in discussion #166: tool-agnostic sync contract with Notion as the in-tree reference binding, silently optional (covers unconfigured, headless, and unauthenticated states), read-only toward the repo with the gitignored sync-state file as its only local write, write-once page bodies, documents sync as filenames only. Complements /html-report: deep local dashboard vs glanceable anywhere-view.
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
"""Guards for the /notion-sync command spec.
|
||||
|
||||
The command is a markdown spec (the spec IS the implementation), so these
|
||||
tests pin the invariants that would break silently: the header format that
|
||||
lint_skills.py enforces, the gitignore entry that keeps the personal sync
|
||||
state out of version control, and the privacy rule that forbids syncing
|
||||
document content to Notion.
|
||||
"""
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import yaml # noqa: F401 - only probing availability for the lint integration test
|
||||
_HAVE_YAML = True
|
||||
except ImportError:
|
||||
_HAVE_YAML = False
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
COMMAND = REPO / ".claude" / "commands" / "notion-sync.md"
|
||||
GITIGNORE = REPO / ".gitignore"
|
||||
|
||||
|
||||
class NotionSyncCommandSpec(unittest.TestCase):
|
||||
def test_command_file_exists_with_lint_compliant_header(self):
|
||||
self.assertTrue(COMMAND.is_file(), "command spec missing")
|
||||
first_line = COMMAND.read_text(encoding="utf-8").splitlines()[0]
|
||||
self.assertTrue(
|
||||
first_line.startswith("# /notion-sync"),
|
||||
f"header must start with '# /notion-sync' (lint_skills.py enforces it), got: {first_line!r}",
|
||||
)
|
||||
|
||||
def test_command_file_is_substantive(self):
|
||||
text = COMMAND.read_text(encoding="utf-8")
|
||||
for section in ("## Step 0", "## Step 4", "## Important Rules"):
|
||||
self.assertIn(section, text, f"spec lost its {section!r} section")
|
||||
|
||||
def test_personal_sync_state_is_gitignored(self):
|
||||
self.assertIn(
|
||||
"job_scraper/notion_sync.json",
|
||||
GITIGNORE.read_text(encoding="utf-8"),
|
||||
"notion_sync.json is personal state and must never be committable",
|
||||
)
|
||||
|
||||
def test_privacy_rule_documents_never_sync(self):
|
||||
text = COMMAND.read_text(encoding="utf-8")
|
||||
self.assertIn(
|
||||
"never upload, attach, or embed",
|
||||
text,
|
||||
"spec lost the rule that CV/cover-letter content never syncs to Notion",
|
||||
)
|
||||
|
||||
@unittest.skipUnless(
|
||||
_HAVE_YAML,
|
||||
"PyYAML not installed (the CI Python-test job omits it; the lint job runs lint_skills.py directly)",
|
||||
)
|
||||
def test_lint_skills_passes(self):
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(REPO / "tools" / "lint_skills.py")],
|
||||
cwd=REPO,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, f"lint_skills.py failed:\n{result.stdout}{result.stderr}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user