@archznn/crewloop-skills 0.6.0 → 0.7.0
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/README.md +4 -16
- package/package.json +1 -2
- package/packages/cli/dist/agents.js +1 -1
- package/packages/cli/dist/agents.js.map +1 -1
- package/packages/cli/dist/cli.d.ts.map +1 -1
- package/packages/cli/dist/cli.js +2 -30
- package/packages/cli/dist/cli.js.map +1 -1
- package/packages/cli/dist/hooks.d.ts +6 -4
- package/packages/cli/dist/hooks.d.ts.map +1 -1
- package/packages/cli/dist/hooks.js +250 -98
- package/packages/cli/dist/hooks.js.map +1 -1
- package/packages/cli/dist/tests/hooks.test.js +245 -33
- package/packages/cli/dist/tests/hooks.test.js.map +1 -1
- package/references/conventions.md +1 -10
- package/references/workflow.md +1 -1
- package/servers/dashboard/README.md +55 -1
- package/servers/dashboard/dist/adapters/agy.d.ts +19 -0
- package/servers/dashboard/dist/adapters/agy.d.ts.map +1 -0
- package/servers/dashboard/dist/adapters/agy.js +108 -0
- package/servers/dashboard/dist/adapters/agy.js.map +1 -0
- package/servers/dashboard/dist/adapters/codex.d.ts.map +1 -1
- package/servers/dashboard/dist/adapters/codex.js +2 -0
- package/servers/dashboard/dist/adapters/codex.js.map +1 -1
- package/servers/dashboard/dist/adapters/kimi.d.ts +1 -1
- package/servers/dashboard/dist/adapters/kimi.d.ts.map +1 -1
- package/servers/dashboard/dist/adapters/kimi.js +9 -0
- package/servers/dashboard/dist/adapters/kimi.js.map +1 -1
- package/servers/dashboard/dist/adapters/shim.d.ts +1 -1
- package/servers/dashboard/dist/adapters/shim.d.ts.map +1 -1
- package/servers/dashboard/dist/adapters/shim.js +32 -11
- package/servers/dashboard/dist/adapters/shim.js.map +1 -1
- package/servers/dashboard/dist/adapters/shim.test.js +46 -4
- package/servers/dashboard/dist/adapters/shim.test.js.map +1 -1
- package/servers/dashboard/dist/lib/constants.d.ts +5 -0
- package/servers/dashboard/dist/lib/constants.d.ts.map +1 -0
- package/servers/dashboard/dist/lib/constants.js +46 -0
- package/servers/dashboard/dist/lib/constants.js.map +1 -0
- package/servers/dashboard/dist/lib/format.d.ts +6 -0
- package/servers/dashboard/dist/lib/format.d.ts.map +1 -0
- package/servers/dashboard/dist/lib/format.js +52 -0
- package/servers/dashboard/dist/lib/format.js.map +1 -0
- package/servers/dashboard/dist/lib/graph.d.ts +22 -0
- package/servers/dashboard/dist/lib/graph.d.ts.map +1 -0
- package/servers/dashboard/dist/lib/graph.js +45 -0
- package/servers/dashboard/dist/lib/graph.js.map +1 -0
- package/servers/dashboard/dist/lib/invocations.d.ts +32 -0
- package/servers/dashboard/dist/lib/invocations.d.ts.map +1 -0
- package/servers/dashboard/dist/lib/invocations.js +135 -0
- package/servers/dashboard/dist/lib/invocations.js.map +1 -0
- package/servers/dashboard/dist/lib/invocations.test.d.ts +2 -0
- package/servers/dashboard/dist/lib/invocations.test.d.ts.map +1 -0
- package/servers/dashboard/dist/lib/invocations.test.js +68 -0
- package/servers/dashboard/dist/lib/invocations.test.js.map +1 -0
- package/servers/dashboard/dist/lib/paths.d.ts +2 -0
- package/servers/dashboard/dist/lib/paths.d.ts.map +1 -0
- package/servers/dashboard/dist/lib/paths.js +40 -0
- package/servers/dashboard/dist/lib/paths.js.map +1 -0
- package/servers/dashboard/dist/presenter.d.ts.map +1 -1
- package/servers/dashboard/dist/presenter.js +2 -0
- package/servers/dashboard/dist/presenter.js.map +1 -1
- package/servers/dashboard/dist/public/assets/index-DjmMKbPN.css +1 -0
- package/servers/dashboard/dist/public/assets/index-DzOqMleZ.js +5323 -0
- package/servers/dashboard/dist/public/assets/index-DzOqMleZ.js.map +1 -0
- package/servers/dashboard/dist/public/index.html +16 -0
- package/servers/dashboard/dist/server.d.ts.map +1 -1
- package/servers/dashboard/dist/server.js +5 -1
- package/servers/dashboard/dist/server.js.map +1 -1
- package/servers/dashboard/dist/skills/infer.d.ts.map +1 -1
- package/servers/dashboard/dist/skills/infer.js +0 -6
- package/servers/dashboard/dist/skills/infer.js.map +1 -1
- package/servers/dashboard/dist/skills/infer.test.js +10 -3
- package/servers/dashboard/dist/skills/infer.test.js.map +1 -1
- package/servers/dashboard/dist/skills/mapping.d.ts +0 -3
- package/servers/dashboard/dist/skills/mapping.d.ts.map +1 -1
- package/servers/dashboard/dist/skills/mapping.js +0 -18
- package/servers/dashboard/dist/skills/mapping.js.map +1 -1
- package/servers/dashboard/dist/skills/registry.d.ts.map +1 -1
- package/servers/dashboard/dist/skills/registry.js +0 -1
- package/servers/dashboard/dist/skills/registry.js.map +1 -1
- package/servers/dashboard/dist/tests/adapters.test.d.ts +2 -0
- package/servers/dashboard/dist/tests/adapters.test.d.ts.map +1 -0
- package/servers/dashboard/dist/tests/adapters.test.js +180 -0
- package/servers/dashboard/dist/tests/adapters.test.js.map +1 -0
- package/servers/dashboard/dist/tests/lib-helpers.test.d.ts +2 -0
- package/servers/dashboard/dist/tests/lib-helpers.test.d.ts.map +1 -0
- package/servers/dashboard/dist/tests/lib-helpers.test.js +123 -0
- package/servers/dashboard/dist/tests/lib-helpers.test.js.map +1 -0
- package/servers/dashboard/dist/tests/shim.test.js +88 -2
- package/servers/dashboard/dist/tests/shim.test.js.map +1 -1
- package/servers/dashboard/dist/types.d.ts +5 -2
- package/servers/dashboard/dist/types.d.ts.map +1 -1
- package/servers/dashboard/package.json +22 -5
- package/servers/dashboard/src/adapters/agy.ts +136 -0
- package/servers/dashboard/src/adapters/codex.ts +2 -0
- package/servers/dashboard/src/adapters/kimi.ts +11 -1
- package/servers/dashboard/src/adapters/shim.test.ts +57 -4
- package/servers/dashboard/src/adapters/shim.ts +31 -11
- package/servers/dashboard/src/lib/constants.ts +44 -0
- package/servers/dashboard/src/lib/format.ts +44 -0
- package/servers/dashboard/src/lib/graph.ts +69 -0
- package/servers/dashboard/src/lib/invocations.test.ts +70 -0
- package/servers/dashboard/src/lib/invocations.ts +172 -0
- package/servers/dashboard/src/lib/paths.ts +35 -0
- package/servers/dashboard/src/presenter.ts +2 -0
- package/servers/dashboard/src/server.ts +5 -1
- package/servers/dashboard/src/skills/infer.test.ts +11 -3
- package/servers/dashboard/src/skills/infer.ts +1 -8
- package/servers/dashboard/src/skills/mapping.ts +0 -20
- package/servers/dashboard/src/skills/registry.ts +0 -1
- package/servers/dashboard/src/tests/adapters.test.ts +198 -0
- package/servers/dashboard/src/tests/lib-helpers.test.ts +133 -0
- package/servers/dashboard/src/tests/shim.test.ts +110 -2
- package/servers/dashboard/src/types.ts +5 -3
- package/servers/dashboard/ui/index.html +15 -0
- package/servers/dashboard/ui/postcss.config.js +6 -0
- package/servers/dashboard/ui/src/App.tsx +360 -0
- package/servers/dashboard/ui/src/components/ActiveSkillPanel.tsx +69 -0
- package/servers/dashboard/ui/src/components/ActivityGraph.tsx +74 -0
- package/servers/dashboard/ui/src/components/CommandPalette.tsx +200 -0
- package/servers/dashboard/ui/src/components/FileActivity.tsx +20 -0
- package/servers/dashboard/ui/src/components/FileDiff.tsx +68 -0
- package/servers/dashboard/ui/src/components/FileList.tsx +64 -0
- package/servers/dashboard/ui/src/components/FilterBar.tsx +208 -0
- package/servers/dashboard/ui/src/components/Network3D.tsx +178 -0
- package/servers/dashboard/ui/src/components/SessionSelector.tsx +95 -0
- package/servers/dashboard/ui/src/components/Sidebar.tsx +110 -0
- package/servers/dashboard/ui/src/components/TelemetryPanel.tsx +57 -0
- package/servers/dashboard/ui/src/components/Timeline.tsx +57 -0
- package/servers/dashboard/ui/src/components/TimelineRow.tsx +112 -0
- package/servers/dashboard/ui/src/components/TopBar.tsx +116 -0
- package/servers/dashboard/ui/src/components/ViewHeader.tsx +19 -0
- package/servers/dashboard/ui/src/components/ui/Icon.tsx +105 -0
- package/servers/dashboard/ui/src/components/ui/StatusBadge.tsx +19 -0
- package/servers/dashboard/ui/src/components/views/FilesView.tsx +23 -0
- package/servers/dashboard/ui/src/components/views/NetworkView.tsx +20 -0
- package/servers/dashboard/ui/src/components/views/Overview.tsx +135 -0
- package/servers/dashboard/ui/src/components/views/SessionsView.tsx +84 -0
- package/servers/dashboard/ui/src/components/views/SettingsView.tsx +138 -0
- package/servers/dashboard/ui/src/components/views/SkillsView.tsx +92 -0
- package/servers/dashboard/ui/src/components/views/TimelineView.tsx +46 -0
- package/servers/dashboard/ui/src/contexts/FilterContext.tsx +41 -0
- package/servers/dashboard/ui/src/contexts/PinnedSessionsContext.tsx +80 -0
- package/servers/dashboard/ui/src/contexts/SettingsContext.tsx +60 -0
- package/servers/dashboard/ui/src/hooks/useCommandPalette.ts +36 -0
- package/servers/dashboard/ui/src/hooks/useKeyboardShortcut.ts +38 -0
- package/servers/dashboard/ui/src/hooks/useNow.ts +12 -0
- package/servers/dashboard/ui/src/hooks/useReducedMotion.ts +15 -0
- package/servers/dashboard/ui/src/hooks/useSessions.ts +64 -0
- package/servers/dashboard/ui/src/hooks/useTheme.ts +30 -0
- package/servers/dashboard/ui/src/hooks/useViewport.ts +19 -0
- package/servers/dashboard/ui/src/hooks/useWebSocket.ts +118 -0
- package/servers/dashboard/ui/src/lib/export.test.ts +33 -0
- package/servers/dashboard/ui/src/lib/export.ts +39 -0
- package/servers/dashboard/ui/src/lib/filter.test.ts +95 -0
- package/servers/dashboard/ui/src/lib/filter.ts +178 -0
- package/servers/dashboard/ui/src/lib/format.test.ts +25 -0
- package/servers/dashboard/ui/src/lib/search.test.ts +52 -0
- package/servers/dashboard/ui/src/lib/search.ts +60 -0
- package/servers/dashboard/ui/src/lib/settings.test.ts +50 -0
- package/servers/dashboard/ui/src/lib/settings.ts +56 -0
- package/servers/dashboard/ui/src/lib/types.ts +124 -0
- package/servers/dashboard/ui/src/main.tsx +19 -0
- package/servers/dashboard/ui/src/styles/index.css +155 -0
- package/servers/dashboard/ui/tailwind.config.js +45 -0
- package/servers/dashboard/ui/tsconfig.json +33 -0
- package/servers/dashboard/ui/tsconfig.node.json +10 -0
- package/servers/dashboard/ui/vite.config.ts +37 -0
- package/servers/dashboard/ui/vitest.config.ts +8 -0
- package/skills/accessibility-auditor/SKILL.md +0 -20
- package/skills/architect/SKILL.md +0 -45
- package/skills/designer/SKILL.md +0 -30
- package/skills/docs-writer/SKILL.md +0 -13
- package/skills/engineer/SKILL.md +0 -30
- package/skills/maintainer/SKILL.md +0 -20
- package/skills/orchestrator/SKILL.md +0 -13
- package/skills/product-manager/SKILL.md +0 -20
- package/skills/researcher/SKILL.md +0 -20
- package/skills/reviewer/SKILL.md +0 -30
- package/skills/security-guard/SKILL.md +0 -20
- package/skills/shipper/SKILL.md +0 -33
- package/skills/tester/SKILL.md +0 -20
- package/packages/cli/dist/mcp.d.ts +0 -28
- package/packages/cli/dist/mcp.d.ts.map +0 -1
- package/packages/cli/dist/mcp.js +0 -148
- package/packages/cli/dist/mcp.js.map +0 -1
- package/packages/cli/dist/tests/mcp.test.d.ts +0 -2
- package/packages/cli/dist/tests/mcp.test.d.ts.map +0 -1
- package/packages/cli/dist/tests/mcp.test.js +0 -232
- package/packages/cli/dist/tests/mcp.test.js.map +0 -1
- package/references/obsidian-mcp-usage.md +0 -190
- package/servers/dashboard/public/app.js +0 -516
- package/servers/dashboard/public/index.html +0 -96
- package/servers/dashboard/public/styles.css +0 -819
- package/servers/obsidian-mcp/README.md +0 -82
- package/servers/obsidian-mcp/pyproject.toml +0 -32
- package/servers/obsidian-mcp/src/obsidian_mcp/__init__.py +0 -0
- package/servers/obsidian-mcp/src/obsidian_mcp/config.py +0 -47
- package/servers/obsidian-mcp/src/obsidian_mcp/indexer/__init__.py +0 -0
- package/servers/obsidian-mcp/src/obsidian_mcp/indexer/embeddings.py +0 -105
- package/servers/obsidian-mcp/src/obsidian_mcp/indexer/indexer.py +0 -79
- package/servers/obsidian-mcp/src/obsidian_mcp/indexer/store.py +0 -141
- package/servers/obsidian-mcp/src/obsidian_mcp/indexer/sync.py +0 -37
- package/servers/obsidian-mcp/src/obsidian_mcp/learning/__init__.py +0 -0
- package/servers/obsidian-mcp/src/obsidian_mcp/learning/detector.py +0 -66
- package/servers/obsidian-mcp/src/obsidian_mcp/learning/note_generator.py +0 -40
- package/servers/obsidian-mcp/src/obsidian_mcp/main.py +0 -4
- package/servers/obsidian-mcp/src/obsidian_mcp/models.py +0 -42
- package/servers/obsidian-mcp/src/obsidian_mcp/privacy/__init__.py +0 -0
- package/servers/obsidian-mcp/src/obsidian_mcp/privacy/filter.py +0 -68
- package/servers/obsidian-mcp/src/obsidian_mcp/rag/__init__.py +0 -0
- package/servers/obsidian-mcp/src/obsidian_mcp/rag/engine.py +0 -50
- package/servers/obsidian-mcp/src/obsidian_mcp/rag/graph_search.py +0 -55
- package/servers/obsidian-mcp/src/obsidian_mcp/rag/text_search.py +0 -37
- package/servers/obsidian-mcp/src/obsidian_mcp/rag/vector_search.py +0 -118
- package/servers/obsidian-mcp/src/obsidian_mcp/server.py +0 -61
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/__init__.py +0 -0
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/create.py +0 -43
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/delete.py +0 -16
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/learn.py +0 -42
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/list.py +0 -16
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/read.py +0 -15
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/registry.py +0 -130
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/related.py +0 -20
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/search.py +0 -26
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/sync.py +0 -22
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/update.py +0 -34
- package/servers/obsidian-mcp/src/obsidian_mcp/vault/__init__.py +0 -0
- package/servers/obsidian-mcp/src/obsidian_mcp/vault/parser.py +0 -82
- package/servers/obsidian-mcp/src/obsidian_mcp/vault/repository.py +0 -68
- package/servers/obsidian-mcp/src/obsidian_mcp/vault/writer.py +0 -61
- package/servers/obsidian-mcp/tests/conftest.py +0 -39
- package/servers/obsidian-mcp/tests/test_async_tools.py +0 -87
- package/servers/obsidian-mcp/tests/test_edge_cases.py +0 -59
- package/servers/obsidian-mcp/tests/test_indexer.py +0 -27
- package/servers/obsidian-mcp/tests/test_integration.py +0 -90
- package/servers/obsidian-mcp/tests/test_learning.py +0 -34
- package/servers/obsidian-mcp/tests/test_privacy.py +0 -31
- package/servers/obsidian-mcp/tests/test_privacy_config.py +0 -44
- package/servers/obsidian-mcp/tests/test_rag.py +0 -64
- package/servers/obsidian-mcp/tests/test_read_raw.py +0 -37
- package/servers/obsidian-mcp/tests/test_tfidf_fallback.py +0 -54
- package/servers/obsidian-mcp/tests/test_tools.py +0 -108
- package/servers/obsidian-mcp/tests/test_vault.py +0 -103
- package/servers/obsidian-mcp/tests/test_writer.py +0 -139
- package/skills/obsidian-second-brain/SKILL.md +0 -298
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
|
|
3
|
-
from obsidian_mcp.config import Config
|
|
4
|
-
from obsidian_mcp.privacy.filter import PrivacyFilter
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def test_blocks_api_key():
|
|
8
|
-
f = PrivacyFilter()
|
|
9
|
-
assert not f.is_safe("API_KEY=sk-123")
|
|
10
|
-
with pytest.raises(ValueError):
|
|
11
|
-
f.validate("API_KEY=sk-123")
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def test_allows_safe_text():
|
|
15
|
-
f = PrivacyFilter()
|
|
16
|
-
assert f.is_safe("This is a normal note about MCP.")
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def test_blocks_email():
|
|
20
|
-
f = PrivacyFilter()
|
|
21
|
-
assert not f.is_safe("Contact me at user@example.com")
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def test_blocks_secret_key_prefix():
|
|
25
|
-
f = PrivacyFilter()
|
|
26
|
-
assert not f.is_safe("Key: sk-abc123def456ghi789")
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def test_allows_tokenization_word():
|
|
30
|
-
f = PrivacyFilter()
|
|
31
|
-
assert f.is_safe("This note discusses tokenization of text.")
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
|
|
3
|
-
from obsidian_mcp.config import Config, PrivacyConfig
|
|
4
|
-
from obsidian_mcp.privacy.filter import PrivacyFilter
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def test_privacy_filter_disabled_via_config():
|
|
8
|
-
config = Config(privacy={"enabled": False})
|
|
9
|
-
f = PrivacyFilter(config)
|
|
10
|
-
assert f.is_safe("API_KEY=secret")
|
|
11
|
-
f.validate("API_KEY=secret") # no exception
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def test_privacy_filter_allow_list_bypass():
|
|
15
|
-
config = Config(privacy={"allowed_strings": ["user@example.com"]})
|
|
16
|
-
f = PrivacyFilter(config)
|
|
17
|
-
assert f.is_safe("Contact user@example.com please")
|
|
18
|
-
f.validate("Contact user@example.com please")
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def test_privacy_filter_toggle_emails():
|
|
22
|
-
config = Config(privacy={"block_emails": False})
|
|
23
|
-
f = PrivacyFilter(config)
|
|
24
|
-
assert f.is_safe("user@example.com")
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def test_privacy_filter_toggle_credit_cards():
|
|
28
|
-
config = Config(privacy={"block_credit_cards": False})
|
|
29
|
-
f = PrivacyFilter(config)
|
|
30
|
-
assert f.is_safe("1234 5678 9012 3456")
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def test_privacy_filter_default_still_blocks():
|
|
34
|
-
f = PrivacyFilter()
|
|
35
|
-
assert not f.is_safe("API_KEY=secret")
|
|
36
|
-
assert not f.is_safe("user@example.com")
|
|
37
|
-
with pytest.raises(ValueError):
|
|
38
|
-
f.validate("API_KEY=secret")
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def test_privacy_config_object():
|
|
42
|
-
config = Config(privacy=PrivacyConfig(enabled=False))
|
|
43
|
-
f = PrivacyFilter(config)
|
|
44
|
-
assert f.is_safe("anything")
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
from obsidian_mcp.indexer.indexer import Indexer
|
|
2
|
-
from obsidian_mcp.models import Note
|
|
3
|
-
from obsidian_mcp.rag.engine import RAGEngine
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def test_text_search(indexer, vault, config, store):
|
|
7
|
-
note = Note(path="search.md", title="Search", content="Obsidian vault local second brain.")
|
|
8
|
-
vault.save(note)
|
|
9
|
-
indexer.index_note(note)
|
|
10
|
-
engine = RAGEngine(config, store)
|
|
11
|
-
results = engine.search("Obsidian", mode="text")
|
|
12
|
-
assert any(r.note_path == "search.md" for r in results)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def test_related_notes(indexer, vault, config, store):
|
|
16
|
-
vault.save(Note(path="a.md", title="A", content="See [[b]]."))
|
|
17
|
-
vault.save(Note(path="b.md", title="B", content="Content."))
|
|
18
|
-
indexer.index_note(vault.read("a.md"))
|
|
19
|
-
indexer.index_note(vault.read("b.md"))
|
|
20
|
-
indexer.compute_backlinks()
|
|
21
|
-
engine = RAGEngine(config, store)
|
|
22
|
-
results = engine.related("a.md")
|
|
23
|
-
assert any(r.note_path == "b.md" for r in results)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def test_graph_search(indexer, vault, config, store):
|
|
27
|
-
vault.save(Note(path="a.md", title="A", content="See [[b]]."))
|
|
28
|
-
vault.save(Note(path="b.md", title="B", content="Content."))
|
|
29
|
-
indexer.index_note(vault.read("a.md"))
|
|
30
|
-
indexer.index_note(vault.read("b.md"))
|
|
31
|
-
indexer.compute_backlinks()
|
|
32
|
-
engine = RAGEngine(config, store)
|
|
33
|
-
results = engine.search("b", mode="graph")
|
|
34
|
-
assert any(r.note_path == "b.md" for r in results)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def test_vector_search_fallback(config, vault, store):
|
|
38
|
-
vault.save(Note(path="v.md", title="V", content="MCP local second brain"))
|
|
39
|
-
indexer = Indexer(config, vault, store)
|
|
40
|
-
indexer.index_all()
|
|
41
|
-
engine = RAGEngine(config, store)
|
|
42
|
-
results = engine.search("MCP", mode="vector")
|
|
43
|
-
assert any(r.note_path == "v.md" for r in results)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def test_vector_search_fallback_ranks_matching_note(config, vault, store):
|
|
47
|
-
vault.save(Note(path="a.md", title="A", content="MCP local second brain"))
|
|
48
|
-
vault.save(Note(path="b.md", title="B", content="Another note about servers and embedding"))
|
|
49
|
-
indexer = Indexer(config, vault, store)
|
|
50
|
-
indexer.index_all()
|
|
51
|
-
engine = RAGEngine(config, store)
|
|
52
|
-
results = engine.search("MCP server", mode="vector")
|
|
53
|
-
assert results[0].note_path == "a.md"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def test_hybrid_search_normalizes_scores(config, vault, store):
|
|
57
|
-
vault.save(Note(path="a.md", title="A", content="MCP local second brain"))
|
|
58
|
-
vault.save(Note(path="b.md", title="B", content="Another note about servers and embedding"))
|
|
59
|
-
indexer = Indexer(config, vault, store)
|
|
60
|
-
indexer.index_all()
|
|
61
|
-
engine = RAGEngine(config, store)
|
|
62
|
-
results = engine.search("MCP", mode="hybrid")
|
|
63
|
-
assert results
|
|
64
|
-
assert all(0.0 <= r.score <= 1.0 for r in results)
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
|
|
3
|
-
from obsidian_mcp.tools.registry import dispatch
|
|
4
|
-
from obsidian_mcp.vault.repository import VaultRepository
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def test_read_note_returns_exact_file_content(config, temp_vault):
|
|
8
|
-
VaultRepository(config)
|
|
9
|
-
raw = "---\ntitle: Exact Note\ntags:\n - alpha\n - beta\n---\n\n# Exact Note\n\nKeep the frontmatter exactly as written."
|
|
10
|
-
(temp_vault / "exact.md").write_text(raw, encoding="utf-8")
|
|
11
|
-
result = dispatch("read_note", {"path": "exact.md"}, config)
|
|
12
|
-
assert result == raw
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def test_read_note_does_not_inject_title_or_tags(config, temp_vault):
|
|
16
|
-
VaultRepository(config)
|
|
17
|
-
raw = "Plain body without title or tags."
|
|
18
|
-
(temp_vault / "plain.md").write_text(raw, encoding="utf-8")
|
|
19
|
-
result = dispatch("read_note", {"path": "plain.md"}, config)
|
|
20
|
-
assert result == raw
|
|
21
|
-
assert "# " not in result
|
|
22
|
-
assert "**Tags:**" not in result
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def test_read_note_non_utf8_replacement(config, vault, temp_vault):
|
|
26
|
-
VaultRepository(config)
|
|
27
|
-
path = temp_vault / "binary.md"
|
|
28
|
-
path.write_bytes(b"\xff\xfeHello\xfa world")
|
|
29
|
-
result = dispatch("read_note", {"path": "binary.md"}, config)
|
|
30
|
-
assert "Hello" in result
|
|
31
|
-
assert "world" in result
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def test_read_note_missing_raises(config):
|
|
35
|
-
VaultRepository(config)
|
|
36
|
-
with pytest.raises(FileNotFoundError):
|
|
37
|
-
dispatch("read_note", {"path": "missing.md"}, config)
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
from obsidian_mcp.indexer.indexer import Indexer
|
|
2
|
-
from obsidian_mcp.models import Chunk, Note
|
|
3
|
-
from obsidian_mcp.rag.engine import RAGEngine
|
|
4
|
-
from obsidian_mcp.rag.vector_search import TfidfIndex
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def test_tfidf_index_fits_on_corpus():
|
|
8
|
-
index = TfidfIndex()
|
|
9
|
-
chunks = [
|
|
10
|
-
Chunk(id="c1", note_path="a.md", text="MCP local second brain"),
|
|
11
|
-
Chunk(id="c2", note_path="b.md", text="Another note about servers"),
|
|
12
|
-
]
|
|
13
|
-
index.fit(chunks)
|
|
14
|
-
results = index.query("MCP", top_k=10)
|
|
15
|
-
assert any(doc_id == "c1" for doc_id, _ in results)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def test_tfidf_index_query_empty_corpus():
|
|
19
|
-
index = TfidfIndex()
|
|
20
|
-
index.fit([])
|
|
21
|
-
assert index.query("anything") == []
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def test_vector_search_builds_corpus_fitted_index(config, vault, store):
|
|
25
|
-
vault.save(Note(path="a.md", title="A", content="MCP local second brain"))
|
|
26
|
-
indexer = Indexer(config, vault, store)
|
|
27
|
-
indexer.index_all()
|
|
28
|
-
engine = RAGEngine(config, store)
|
|
29
|
-
assert engine.vector_search._tfidf_index is None
|
|
30
|
-
results = engine.search("MCP", mode="vector")
|
|
31
|
-
assert results
|
|
32
|
-
assert engine.vector_search._tfidf_index is not None
|
|
33
|
-
assert engine.vector_search._tfidf_chunk_count > 0
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def test_vector_search_reuses_fitted_index(config, vault, store):
|
|
37
|
-
vault.save(Note(path="a.md", title="A", content="MCP local second brain"))
|
|
38
|
-
indexer = Indexer(config, vault, store)
|
|
39
|
-
indexer.index_all()
|
|
40
|
-
engine = RAGEngine(config, store)
|
|
41
|
-
engine.search("MCP", mode="vector")
|
|
42
|
-
first_index = engine.vector_search._tfidf_index
|
|
43
|
-
engine.search("local", mode="vector")
|
|
44
|
-
assert engine.vector_search._tfidf_index is first_index
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def test_vector_search_ranks_matching_note_highest(config, vault, store):
|
|
48
|
-
vault.save(Note(path="a.md", title="A", content="MCP local second brain"))
|
|
49
|
-
vault.save(Note(path="b.md", title="B", content="Another note about servers and embedding"))
|
|
50
|
-
indexer = Indexer(config, vault, store)
|
|
51
|
-
indexer.index_all()
|
|
52
|
-
engine = RAGEngine(config, store)
|
|
53
|
-
results = engine.search("MCP server", mode="vector")
|
|
54
|
-
assert results[0].note_path == "a.md"
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
|
|
3
|
-
from obsidian_mcp.indexer.indexer import Indexer
|
|
4
|
-
from obsidian_mcp.indexer.store import IndexStore
|
|
5
|
-
from obsidian_mcp.models import Note
|
|
6
|
-
from obsidian_mcp.tools.registry import dispatch
|
|
7
|
-
from obsidian_mcp.vault.repository import VaultRepository
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def test_tool_create_and_read(config):
|
|
11
|
-
VaultRepository(config)
|
|
12
|
-
result = dispatch("create_note", {"path": "t.md", "content": "Hello"}, config)
|
|
13
|
-
assert result["status"] == "created"
|
|
14
|
-
text = dispatch("read_note", {"path": "t.md"}, config)
|
|
15
|
-
assert "Hello" in text
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def test_tool_create_note_adds_extension(config):
|
|
19
|
-
VaultRepository(config)
|
|
20
|
-
result = dispatch("create_note", {"path": "no-extension", "content": "body"}, config)
|
|
21
|
-
assert result["path"] == "no-extension.md"
|
|
22
|
-
text = dispatch("read_note", {"path": "no-extension.md"}, config)
|
|
23
|
-
assert "body" in text
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def test_tool_create_note_title_from_path(config):
|
|
27
|
-
VaultRepository(config)
|
|
28
|
-
dispatch("create_note", {"path": "my-great-note.md", "content": "body"}, config)
|
|
29
|
-
text = dispatch("read_note", {"path": "my-great-note.md"}, config)
|
|
30
|
-
assert "My Great Note" in text
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def test_tool_read_note_returns_raw_content(config, temp_vault):
|
|
34
|
-
VaultRepository(config)
|
|
35
|
-
dispatch(
|
|
36
|
-
"create_note",
|
|
37
|
-
{"path": "fm.md", "title": "FM", "content": "body", "tags": ["a", "b"]},
|
|
38
|
-
config,
|
|
39
|
-
)
|
|
40
|
-
raw = (temp_vault / "fm.md").read_text(encoding="utf-8")
|
|
41
|
-
text = dispatch("read_note", {"path": "fm.md"}, config)
|
|
42
|
-
assert text == raw
|
|
43
|
-
assert "body" in text
|
|
44
|
-
assert "FM" in text
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def test_tool_search(config):
|
|
48
|
-
vault = VaultRepository(config)
|
|
49
|
-
vault.save(Note(path="s.md", title="S", content="MCP server local"))
|
|
50
|
-
indexer = Indexer(config, vault, IndexStore(config.index_dir / "index.db"))
|
|
51
|
-
indexer.index_all()
|
|
52
|
-
text = dispatch("search_notes", {"query": "MCP", "mode": "text"}, config)
|
|
53
|
-
assert "s.md" in text
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def test_tool_privacy_blocks(config):
|
|
57
|
-
with pytest.raises(ValueError):
|
|
58
|
-
dispatch("create_note", {"path": "bad.md", "content": "API_KEY=secret"}, config)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def test_tool_update_and_delete(config):
|
|
62
|
-
VaultRepository(config)
|
|
63
|
-
dispatch("create_note", {"path": "u.md", "content": "Original", "tags": ["a"]}, config)
|
|
64
|
-
dispatch("update_note", {"path": "u.md", "append": "More", "tags": ["b"]}, config)
|
|
65
|
-
text = dispatch("read_note", {"path": "u.md"}, config)
|
|
66
|
-
assert "Original" in text
|
|
67
|
-
assert "More" in text
|
|
68
|
-
assert "b" in text
|
|
69
|
-
dispatch("delete_note", {"path": "u.md"}, config)
|
|
70
|
-
with pytest.raises(FileNotFoundError):
|
|
71
|
-
dispatch("read_note", {"path": "u.md"}, config)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def test_tool_list_and_related(config):
|
|
75
|
-
vault = VaultRepository(config)
|
|
76
|
-
vault.save(Note(path="x.md", title="X", content="Link to [[y]]."))
|
|
77
|
-
vault.save(Note(path="y.md", title="Y", content="Content."))
|
|
78
|
-
indexer = Indexer(config, vault, IndexStore(config.index_dir / "index.db"))
|
|
79
|
-
indexer.index_all()
|
|
80
|
-
indexer.compute_backlinks()
|
|
81
|
-
listed = dispatch("list_notes", {}, config)
|
|
82
|
-
assert "x.md" in listed
|
|
83
|
-
related = dispatch("get_related_notes", {"path": "x.md"}, config)
|
|
84
|
-
assert "y.md" in related
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def test_tool_sync_from_bundle(config, temp_vault):
|
|
88
|
-
(temp_vault / "skills" / "orchestrator").mkdir(parents=True)
|
|
89
|
-
(temp_vault / "skills" / "orchestrator" / "SKILL.md").write_text("# Orchestrator\n")
|
|
90
|
-
(temp_vault / "README.md").write_text("# Bundle\n")
|
|
91
|
-
result = dispatch("sync_from_bundle", {"force": True}, config)
|
|
92
|
-
assert result["status"] == "synced"
|
|
93
|
-
assert result["indexed_bundle_files"] > 0
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def test_tool_path_traversal_blocked(config):
|
|
97
|
-
with pytest.raises(ValueError):
|
|
98
|
-
dispatch("create_note", {"path": "../outside.md", "content": "x"}, config)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def test_tool_learn_from_text(config):
|
|
102
|
-
result = dispatch(
|
|
103
|
-
"learn_from_text",
|
|
104
|
-
{"text": "Novo conceito: Observability no projeto LEA."},
|
|
105
|
-
config,
|
|
106
|
-
)
|
|
107
|
-
assert result["status"] == "learned"
|
|
108
|
-
assert any("observability" in p.lower() for p in result["created_notes"])
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
|
|
3
|
-
from obsidian_mcp.models import Note
|
|
4
|
-
from obsidian_mcp.vault.parser import parse_note
|
|
5
|
-
from obsidian_mcp.vault.repository import VaultRepository
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def test_parse_note_with_frontmatter(temp_vault):
|
|
9
|
-
path = temp_vault / "test.md"
|
|
10
|
-
path.write_text("---\ntitle: Hello\ntags: [a, b]\n---\n\nBody here with [[link]].")
|
|
11
|
-
note = parse_note("test.md", path)
|
|
12
|
-
assert note.title == "Hello"
|
|
13
|
-
assert note.frontmatter["tags"] == ["a", "b"]
|
|
14
|
-
assert "Body here" in note.content
|
|
15
|
-
assert note.links == ["link.md"]
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def test_parse_note_without_frontmatter(temp_vault):
|
|
19
|
-
path = temp_vault / "no-front.md"
|
|
20
|
-
path.write_text("# My Title\n\nContent.")
|
|
21
|
-
note = parse_note("no-front.md", path)
|
|
22
|
-
assert note.title == "My Title"
|
|
23
|
-
assert note.content == "Content."
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def test_parse_note_with_invalid_frontmatter(temp_vault):
|
|
27
|
-
path = temp_vault / "bad-front.md"
|
|
28
|
-
path.write_text("---\ntitle: [unclosed\n---\n\nBody.")
|
|
29
|
-
note = parse_note("bad-front.md", path)
|
|
30
|
-
assert note.frontmatter == {}
|
|
31
|
-
assert "Body." in note.content
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def test_vault_crud(config):
|
|
35
|
-
repo = VaultRepository(config)
|
|
36
|
-
note = Note(path="foo.md", title="Foo", content="bar")
|
|
37
|
-
repo.save(note)
|
|
38
|
-
assert repo.exists("foo.md")
|
|
39
|
-
read = repo.read("foo.md")
|
|
40
|
-
assert read.title == "Foo"
|
|
41
|
-
repo.delete("foo.md")
|
|
42
|
-
assert not repo.exists("foo.md")
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def test_vault_path_escape(config):
|
|
46
|
-
repo = VaultRepository(config)
|
|
47
|
-
with pytest.raises(ValueError):
|
|
48
|
-
repo._resolve("../outside.md")
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def test_writer_preserves_created_and_tags(config):
|
|
52
|
-
repo = VaultRepository(config)
|
|
53
|
-
note = Note(path="w.md", title="W", content="body", tags=["tag1"])
|
|
54
|
-
repo.save(note)
|
|
55
|
-
first = repo.read("w.md")
|
|
56
|
-
created = first.frontmatter["created"]
|
|
57
|
-
repo.save(first)
|
|
58
|
-
second = repo.read("w.md")
|
|
59
|
-
assert second.frontmatter["created"] == created
|
|
60
|
-
assert "tag1" in second.frontmatter["tags"]
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def test_vault_ignores_hidden_dirs(config):
|
|
64
|
-
repo = VaultRepository(config)
|
|
65
|
-
hidden = repo.root / ".hidden"
|
|
66
|
-
hidden.mkdir()
|
|
67
|
-
(hidden / "secret.md").write_text("# secret")
|
|
68
|
-
(repo.root / "visible.md").write_text("# visible")
|
|
69
|
-
notes = repo.list_notes()
|
|
70
|
-
assert "visible.md" in notes
|
|
71
|
-
assert "secret.md" not in notes
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def test_vault_blocks_symlink_escape(config, temp_vault):
|
|
75
|
-
repo = VaultRepository(config)
|
|
76
|
-
outside = temp_vault.parent / "outside.md"
|
|
77
|
-
outside.write_text("# outside")
|
|
78
|
-
link = repo.root / "escape.md"
|
|
79
|
-
link.symlink_to(outside)
|
|
80
|
-
assert "escape.md" not in repo.list_notes()
|
|
81
|
-
with pytest.raises(ValueError):
|
|
82
|
-
repo.read("escape.md")
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def test_vault_read_raw_returns_exact_content(config):
|
|
86
|
-
repo = VaultRepository(config)
|
|
87
|
-
raw = "---\ntitle: Raw\ntags:\n - a\n---\n\nbody"
|
|
88
|
-
(repo.root / "raw.md").write_text(raw, encoding="utf-8")
|
|
89
|
-
assert repo.read_raw("raw.md") == raw
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def test_vault_read_raw_missing_raises(config):
|
|
93
|
-
repo = VaultRepository(config)
|
|
94
|
-
with pytest.raises(FileNotFoundError):
|
|
95
|
-
repo.read_raw("missing.md")
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def test_vault_read_raw_replaces_non_utf8(config, temp_vault):
|
|
99
|
-
repo = VaultRepository(config)
|
|
100
|
-
(repo.root / "binary.md").write_bytes(b"\xff\xfeHello\xfaworld")
|
|
101
|
-
text = repo.read_raw("binary.md")
|
|
102
|
-
assert "Hello" in text
|
|
103
|
-
assert "world" in text
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
from datetime import datetime
|
|
2
|
-
|
|
3
|
-
import pytest
|
|
4
|
-
import yaml
|
|
5
|
-
|
|
6
|
-
from obsidian_mcp.models import Note
|
|
7
|
-
from obsidian_mcp.vault.writer import write_note
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def _parse_frontmatter(text: str) -> dict:
|
|
11
|
-
assert text.startswith("---")
|
|
12
|
-
end = text.find("\n---", 3)
|
|
13
|
-
assert end != -1
|
|
14
|
-
return yaml.safe_load(text[3:end])
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def _get_body(text: str) -> str:
|
|
18
|
-
end = text.find("\n---", 3)
|
|
19
|
-
return text[end + 5 :].lstrip("\n")
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def test_write_note_without_frontmatter(temp_vault):
|
|
23
|
-
path = temp_vault / "clean.md"
|
|
24
|
-
note = Note(path="clean.md", title="Clean Note", content="# Clean Note\n\nBody.")
|
|
25
|
-
write_note(path, note)
|
|
26
|
-
|
|
27
|
-
written = path.read_text(encoding="utf-8")
|
|
28
|
-
frontmatter = _parse_frontmatter(written)
|
|
29
|
-
body = _get_body(written)
|
|
30
|
-
|
|
31
|
-
assert frontmatter["title"] == "Clean Note"
|
|
32
|
-
assert "created" in frontmatter
|
|
33
|
-
assert "updated" in frontmatter
|
|
34
|
-
assert body == "# Clean Note\n\nBody."
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def test_write_note_with_embedded_frontmatter(temp_vault):
|
|
38
|
-
path = temp_vault / "embedded.md"
|
|
39
|
-
content = (
|
|
40
|
-
"---\n"
|
|
41
|
-
"type: journal\n"
|
|
42
|
-
"title: Embedded Title\n"
|
|
43
|
-
"tags: [a, b]\n"
|
|
44
|
-
"updated: 2026-06-16T17:35:00Z\n"
|
|
45
|
-
"---\n"
|
|
46
|
-
"\n"
|
|
47
|
-
"# Embedded Title\n"
|
|
48
|
-
"\n"
|
|
49
|
-
"Body text."
|
|
50
|
-
)
|
|
51
|
-
note = Note(path="embedded.md", title="Server Title", content=content, tags=["c"])
|
|
52
|
-
write_note(path, note)
|
|
53
|
-
|
|
54
|
-
written = path.read_text(encoding="utf-8")
|
|
55
|
-
frontmatter = _parse_frontmatter(written)
|
|
56
|
-
body = _get_body(written)
|
|
57
|
-
|
|
58
|
-
assert frontmatter["title"] == "Server Title"
|
|
59
|
-
assert frontmatter["type"] == "journal"
|
|
60
|
-
assert frontmatter["tags"] == ["a", "b", "c"]
|
|
61
|
-
assert "created" in frontmatter
|
|
62
|
-
assert "updated" in frontmatter
|
|
63
|
-
assert body == "# Embedded Title\n\nBody text."
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def test_write_note_with_malformed_frontmatter(temp_vault, caplog):
|
|
67
|
-
path = temp_vault / "bad.md"
|
|
68
|
-
content = "---\ntitle: [unclosed\n---\n\nBody."
|
|
69
|
-
note = Note(path="bad.md", title="Bad Note", content=content)
|
|
70
|
-
|
|
71
|
-
with caplog.at_level("WARNING"):
|
|
72
|
-
write_note(path, note)
|
|
73
|
-
|
|
74
|
-
written = path.read_text(encoding="utf-8")
|
|
75
|
-
frontmatter = _parse_frontmatter(written)
|
|
76
|
-
body = _get_body(written)
|
|
77
|
-
|
|
78
|
-
assert "failed to parse frontmatter" in caplog.text.lower()
|
|
79
|
-
assert frontmatter["title"] == "Bad Note"
|
|
80
|
-
assert body == "Body."
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def test_write_note_merges_tags(temp_vault):
|
|
84
|
-
path = temp_vault / "tags.md"
|
|
85
|
-
content = "---\ntags: [z, a, a]\n---\n\nBody."
|
|
86
|
-
note = Note(path="tags.md", title="Tags Note", content=content, tags=["b", "a"])
|
|
87
|
-
write_note(path, note)
|
|
88
|
-
|
|
89
|
-
written = path.read_text(encoding="utf-8")
|
|
90
|
-
frontmatter = _parse_frontmatter(written)
|
|
91
|
-
|
|
92
|
-
assert frontmatter["tags"] == ["a", "b", "z"]
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def test_write_note_preserves_created_on_update(temp_vault):
|
|
96
|
-
path = temp_vault / "preserve.md"
|
|
97
|
-
note = Note(path="preserve.md", title="Preserve", content="Body.")
|
|
98
|
-
write_note(path, note)
|
|
99
|
-
|
|
100
|
-
first = path.read_text(encoding="utf-8")
|
|
101
|
-
first_created = _parse_frontmatter(first)["created"]
|
|
102
|
-
|
|
103
|
-
note2 = Note(
|
|
104
|
-
path="preserve.md",
|
|
105
|
-
title="Preserve",
|
|
106
|
-
content="Updated body.",
|
|
107
|
-
frontmatter={"created": first_created},
|
|
108
|
-
)
|
|
109
|
-
write_note(path, note2)
|
|
110
|
-
|
|
111
|
-
second = path.read_text(encoding="utf-8")
|
|
112
|
-
second_frontmatter = _parse_frontmatter(second)
|
|
113
|
-
|
|
114
|
-
assert second_frontmatter["created"] == first_created
|
|
115
|
-
assert second_frontmatter["updated"] != first_created
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def test_write_note_title_precedence(temp_vault):
|
|
119
|
-
path = temp_vault / "title.md"
|
|
120
|
-
content = "---\ntitle: Embedded Title\n---\n\nBody."
|
|
121
|
-
note = Note(path="title.md", title="Server Title", content=content)
|
|
122
|
-
write_note(path, note)
|
|
123
|
-
|
|
124
|
-
written = path.read_text(encoding="utf-8")
|
|
125
|
-
frontmatter = _parse_frontmatter(written)
|
|
126
|
-
|
|
127
|
-
assert frontmatter["title"] == "Server Title"
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def test_write_note_tags_as_string(temp_vault):
|
|
131
|
-
path = temp_vault / "string-tags.md"
|
|
132
|
-
content = "---\ntags: x, y, z\n---\n\nBody."
|
|
133
|
-
note = Note(path="string-tags.md", title="String Tags", content=content)
|
|
134
|
-
write_note(path, note)
|
|
135
|
-
|
|
136
|
-
written = path.read_text(encoding="utf-8")
|
|
137
|
-
frontmatter = _parse_frontmatter(written)
|
|
138
|
-
|
|
139
|
-
assert frontmatter["tags"] == ["x", "y", "z"]
|