Add open-notebook skill: self-hosted NotebookLM alternative (issue #56)

Implements the open-notebook skill as a comprehensive integration for the
open-source, self-hosted alternative to Google NotebookLM. Addresses the
gap created by Google not providing a public NotebookLM API.

Developed using TDD with 44 tests covering skill structure, SKILL.md
frontmatter/content, reference documentation, example scripts, API
endpoint coverage, and marketplace.json registration.

Includes:
- SKILL.md with full documentation, code examples, and provider matrix
- references/api_reference.md covering all 20+ REST API endpoint groups
- references/examples.md with complete research workflow examples
- references/configuration.md with Docker, env vars, and security setup
- references/architecture.md with system design and data flow diagrams
- scripts/ with 3 example scripts (notebook, source, chat) + test suite
- marketplace.json updated to register the new skill

Closes #56

https://claude.ai/code/session_015CqcNWNYmDF9sqxKxziXcz
This commit is contained in:
Claude
2026-02-23 00:18:19 +00:00
parent f7585b7624
commit 259e01f7fd
10 changed files with 2599 additions and 0 deletions

View File

@@ -0,0 +1,423 @@
"""
Test-Driven Development tests for the Open-Notebook skill.
These tests validate the structure, content completeness, and correctness
of the open-notebook skill implementation for the claude-scientific-skills repository.
Run with: python -m pytest test_open_notebook_skill.py -v
Or: python -m unittest test_open_notebook_skill.py -v
"""
import json
import os
import re
import unittest
# Resolve paths relative to this test file
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
SKILL_DIR = os.path.dirname(SCRIPT_DIR)
REPO_ROOT = os.path.dirname(os.path.dirname(SKILL_DIR))
REFERENCES_DIR = os.path.join(SKILL_DIR, "references")
SCRIPTS_DIR = SCRIPT_DIR
SKILL_MD = os.path.join(SKILL_DIR, "SKILL.md")
MARKETPLACE_JSON = os.path.join(REPO_ROOT, ".claude-plugin", "marketplace.json")
class TestSkillDirectoryStructure(unittest.TestCase):
"""Tests that the skill directory has the required structure."""
def test_skill_directory_exists(self):
"""The open-notebook skill directory must exist."""
self.assertTrue(
os.path.isdir(SKILL_DIR),
f"Skill directory does not exist: {SKILL_DIR}",
)
def test_skill_md_exists(self):
"""SKILL.md must exist in the skill directory."""
self.assertTrue(
os.path.isfile(SKILL_MD),
f"SKILL.md does not exist: {SKILL_MD}",
)
def test_references_directory_exists(self):
"""A references/ directory must exist."""
self.assertTrue(
os.path.isdir(REFERENCES_DIR),
f"References directory does not exist: {REFERENCES_DIR}",
)
def test_scripts_directory_exists(self):
"""A scripts/ directory must exist."""
self.assertTrue(
os.path.isdir(SCRIPTS_DIR),
f"Scripts directory does not exist: {SCRIPTS_DIR}",
)
class TestSkillMdFrontmatter(unittest.TestCase):
"""Tests that SKILL.md has correct YAML frontmatter."""
@classmethod
def setUpClass(cls):
with open(SKILL_MD, "r") as f:
cls.content = f.read()
# Extract frontmatter between --- delimiters
match = re.match(r"^---\n(.*?)\n---", cls.content, re.DOTALL)
cls.frontmatter = match.group(1) if match else ""
def test_has_yaml_frontmatter(self):
"""SKILL.md must start with YAML frontmatter delimiters."""
self.assertTrue(
self.content.startswith("---\n"),
"SKILL.md must start with '---' YAML frontmatter delimiter",
)
self.assertIn(
"\n---\n",
self.content[4:],
"SKILL.md must have a closing '---' YAML frontmatter delimiter",
)
def test_frontmatter_has_name(self):
"""Frontmatter must include a 'name' field set to 'open-notebook'."""
self.assertIn("name:", self.frontmatter)
self.assertRegex(self.frontmatter, r"name:\s*open-notebook")
def test_frontmatter_has_description(self):
"""Frontmatter must include a 'description' field."""
self.assertIn("description:", self.frontmatter)
# Description should be substantive (at least 50 characters)
desc_match = re.search(r"description:\s*(.+)", self.frontmatter)
self.assertIsNotNone(desc_match, "description field must have content")
description = desc_match.group(1).strip()
self.assertGreater(
len(description),
50,
"description must be substantive (>50 chars)",
)
def test_frontmatter_has_license(self):
"""Frontmatter must include a 'license' field."""
self.assertIn("license:", self.frontmatter)
self.assertRegex(self.frontmatter, r"license:\s*MIT")
def test_frontmatter_has_metadata_author(self):
"""Frontmatter must include metadata with skill-author."""
self.assertIn("metadata:", self.frontmatter)
self.assertIn("skill-author:", self.frontmatter)
self.assertRegex(self.frontmatter, r"skill-author:\s*K-Dense Inc\.")
class TestSkillMdContent(unittest.TestCase):
"""Tests that SKILL.md has required content sections."""
@classmethod
def setUpClass(cls):
with open(SKILL_MD, "r") as f:
cls.content = f.read()
def test_has_title_heading(self):
"""SKILL.md must have an H1 title heading."""
self.assertIsNotNone(
re.search(r"^# .+", self.content, flags=re.MULTILINE),
"SKILL.md must have an H1 title heading",
)
def test_has_overview_section(self):
"""SKILL.md must have an Overview section."""
self.assertRegex(
self.content,
r"## Overview",
"Must include an Overview section",
)
def test_has_quick_start_section(self):
"""SKILL.md must have a Quick Start section."""
self.assertRegex(
self.content,
r"## Quick Start",
"Must include a Quick Start section",
)
def test_has_docker_setup(self):
"""SKILL.md must include Docker setup instructions."""
self.assertIn("docker", self.content.lower())
self.assertIn("docker-compose", self.content.lower())
def test_has_api_base_url(self):
"""SKILL.md must mention the API base URL."""
self.assertIn("localhost:5055", self.content)
def test_mentions_notebooklm_alternative(self):
"""SKILL.md must explain open-notebook as a NotebookLM alternative."""
content_lower = self.content.lower()
self.assertTrue(
"notebooklm" in content_lower or "notebook lm" in content_lower,
"Must mention NotebookLM as context for why open-notebook exists",
)
def test_mentions_self_hosted(self):
"""SKILL.md must highlight the self-hosted/privacy aspect."""
content_lower = self.content.lower()
self.assertTrue(
"self-hosted" in content_lower or "privacy" in content_lower,
"Must highlight self-hosted/privacy benefits",
)
def test_mentions_multiple_ai_providers(self):
"""SKILL.md must mention support for multiple AI providers."""
content_lower = self.content.lower()
providers_mentioned = sum(
1
for p in ["openai", "anthropic", "google", "ollama", "groq", "mistral"]
if p in content_lower
)
self.assertGreaterEqual(
providers_mentioned,
4,
"Must mention at least 4 AI providers",
)
def test_has_core_features_section(self):
"""SKILL.md must describe core features."""
content_lower = self.content.lower()
features = ["notebook", "source", "note", "chat", "podcast", "search"]
features_found = sum(1 for f in features if f in content_lower)
self.assertGreaterEqual(
features_found,
5,
"Must describe at least 5 core features (notebook, source, note, chat, podcast, search)",
)
def test_has_api_reference_section(self):
"""SKILL.md must have an API section or reference to API docs."""
content_lower = self.content.lower()
self.assertTrue(
"api" in content_lower and "reference" in content_lower,
"Must include API reference information",
)
def test_has_python_code_examples(self):
"""SKILL.md must contain Python code examples."""
self.assertIn("```python", self.content)
def test_has_bash_code_examples(self):
"""SKILL.md must contain bash/shell code examples."""
self.assertTrue(
"```bash" in self.content or "```shell" in self.content,
"Must contain bash or shell code blocks",
)
def test_has_installation_instructions(self):
"""SKILL.md must include installation instructions."""
content_lower = self.content.lower()
self.assertTrue(
"install" in content_lower or "setup" in content_lower,
"Must include installation/setup instructions",
)
def test_has_environment_variable_info(self):
"""SKILL.md must mention the encryption key environment variable."""
self.assertIn(
"OPEN_NOTEBOOK_ENCRYPTION_KEY",
self.content,
"Must mention the required encryption key env var",
)
def test_has_kdense_suggestion(self):
"""SKILL.md must include the K-Dense Web suggestion section."""
self.assertIn(
"K-Dense Web",
self.content,
"Must include K-Dense Web suggestion",
)
def test_content_length_sufficient(self):
"""SKILL.md must be substantive (at least 5000 characters)."""
self.assertGreater(
len(self.content),
5000,
"SKILL.md must be at least 5000 characters for a comprehensive skill",
)
class TestReferenceFiles(unittest.TestCase):
"""Tests that reference documentation files exist and have sufficient content."""
def _read_reference(self, filename):
path = os.path.join(REFERENCES_DIR, filename)
self.assertTrue(
os.path.isfile(path),
f"Reference file must exist: {filename}",
)
with open(path, "r") as f:
content = f.read()
return content
def test_api_reference_exists_and_comprehensive(self):
"""references/api_reference.md must exist and cover key API endpoints."""
content = self._read_reference("api_reference.md")
self.assertGreater(len(content), 3000, "API reference must be comprehensive")
# Must cover core endpoint groups
for endpoint_group in ["notebooks", "sources", "notes", "chat", "search"]:
self.assertIn(
endpoint_group,
content.lower(),
f"API reference must cover {endpoint_group} endpoints",
)
def test_api_reference_has_http_methods(self):
"""API reference must document HTTP methods."""
content = self._read_reference("api_reference.md")
for method in ["GET", "POST", "PUT", "DELETE"]:
self.assertIn(
method,
content,
f"API reference must document {method} method",
)
def test_examples_reference_exists(self):
"""references/examples.md must exist with practical code examples."""
content = self._read_reference("examples.md")
self.assertGreater(len(content), 2000, "Examples must be substantive")
self.assertIn("```python", content, "Examples must include Python code")
def test_configuration_reference_exists(self):
"""references/configuration.md must exist with setup details."""
content = self._read_reference("configuration.md")
self.assertGreater(len(content), 1500, "Configuration guide must be substantive")
content_lower = content.lower()
self.assertTrue(
"docker" in content_lower,
"Configuration must cover Docker setup",
)
self.assertTrue(
"environment" in content_lower or "env" in content_lower,
"Configuration must cover environment variables",
)
def test_architecture_reference_exists(self):
"""references/architecture.md must exist explaining the system."""
content = self._read_reference("architecture.md")
self.assertGreater(len(content), 1000, "Architecture doc must be substantive")
content_lower = content.lower()
for component in ["fastapi", "surrealdb", "langchain"]:
self.assertIn(
component,
content_lower,
f"Architecture must mention {component}",
)
class TestExampleScripts(unittest.TestCase):
"""Tests that example scripts exist and are valid Python."""
def _check_script(self, filename):
path = os.path.join(SCRIPTS_DIR, filename)
self.assertTrue(
os.path.isfile(path),
f"Script must exist: {filename}",
)
with open(path, "r") as f:
content = f.read()
# Verify it's valid Python syntax
try:
compile(content, filename, "exec")
except SyntaxError as e:
self.fail(f"Script {filename} has invalid Python syntax: {e}")
return content
def test_notebook_management_script_exists(self):
"""A notebook management example script must exist."""
content = self._check_script("notebook_management.py")
self.assertIn("notebook", content.lower())
self.assertIn("requests", content.lower())
def test_source_ingestion_script_exists(self):
"""A source ingestion example script must exist."""
content = self._check_script("source_ingestion.py")
self.assertIn("source", content.lower())
def test_chat_interaction_script_exists(self):
"""A chat interaction example script must exist."""
content = self._check_script("chat_interaction.py")
self.assertIn("chat", content.lower())
class TestMarketplaceJson(unittest.TestCase):
"""Tests that marketplace.json includes the open-notebook skill."""
@classmethod
def setUpClass(cls):
with open(MARKETPLACE_JSON, "r") as f:
cls.marketplace = json.load(f)
def test_marketplace_has_open_notebook_skill(self):
"""marketplace.json must list the open-notebook skill."""
skills = self.marketplace["plugins"][0]["skills"]
skill_path = "./scientific-skills/open-notebook"
self.assertIn(
skill_path,
skills,
f"marketplace.json must include '{skill_path}' in the skills list",
)
def test_marketplace_valid_json(self):
"""marketplace.json must be valid JSON with expected structure."""
self.assertIn("plugins", self.marketplace)
self.assertIsInstance(self.marketplace["plugins"], list)
self.assertGreater(len(self.marketplace["plugins"]), 0)
self.assertIn("skills", self.marketplace["plugins"][0])
class TestSkillMdApiEndpointCoverage(unittest.TestCase):
"""Tests that SKILL.md or reference docs cover key API endpoint categories."""
@classmethod
def setUpClass(cls):
with open(SKILL_MD, "r") as f:
cls.skill_content = f.read()
api_ref_path = os.path.join(REFERENCES_DIR, "api_reference.md")
with open(api_ref_path, "r") as f:
cls.api_content = f.read()
cls.combined = cls.skill_content + cls.api_content
def test_covers_notebook_endpoints(self):
"""Must document notebook management endpoints."""
self.assertIn("/notebooks", self.api_content)
def test_covers_source_endpoints(self):
"""Must document source management endpoints."""
self.assertIn("/sources", self.api_content)
def test_covers_note_endpoints(self):
"""Must document note management endpoints."""
self.assertIn("/notes", self.api_content)
def test_covers_chat_endpoints(self):
"""Must document chat endpoints."""
self.assertIn("/chat", self.api_content)
def test_covers_search_endpoints(self):
"""Must document search endpoints."""
self.assertIn("/search", self.api_content)
def test_covers_podcast_endpoints(self):
"""Must document podcast endpoints."""
self.assertIn("/podcasts", self.api_content)
def test_covers_transformation_endpoints(self):
"""Must document transformation endpoints."""
self.assertIn("/transformations", self.api_content)
def test_covers_model_management(self):
"""Must document model management endpoints."""
self.assertIn("/models", self.api_content)
def test_covers_credential_management(self):
"""Must document credential management endpoints."""
self.assertIn("/credentials", self.api_content)
if __name__ == "__main__":
unittest.main()