embedsync 0.4.0__tar.gz → 0.5.0__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.
- {embedsync-0.4.0 → embedsync-0.5.0}/CHANGELOG.md +5 -0
- {embedsync-0.4.0 → embedsync-0.5.0}/PKG-INFO +3 -2
- {embedsync-0.4.0 → embedsync-0.5.0}/README.md +2 -1
- {embedsync-0.4.0 → embedsync-0.5.0}/pyproject.toml +1 -1
- {embedsync-0.4.0 → embedsync-0.5.0}/src/embedsync/__init__.py +1 -1
- {embedsync-0.4.0 → embedsync-0.5.0}/src/embedsync/cli.py +6 -2
- {embedsync-0.4.0 → embedsync-0.5.0}/src/embedsync/sync/engine.py +24 -7
- {embedsync-0.4.0 → embedsync-0.5.0}/tests/test_embedders.py +3 -3
- {embedsync-0.4.0 → embedsync-0.5.0}/tests/test_engine.py +13 -0
- embedsync-0.5.0/tests/test_scenarios_v05.py +196 -0
- {embedsync-0.4.0 → embedsync-0.5.0}/.env.example +0 -0
- {embedsync-0.4.0 → embedsync-0.5.0}/.github/workflows/ci.yml +0 -0
- {embedsync-0.4.0 → embedsync-0.5.0}/.gitignore +0 -0
- {embedsync-0.4.0 → embedsync-0.5.0}/CONTRIBUTING.md +0 -0
- {embedsync-0.4.0 → embedsync-0.5.0}/LICENSE +0 -0
- {embedsync-0.4.0 → embedsync-0.5.0}/SECURITY.md +0 -0
- {embedsync-0.4.0 → embedsync-0.5.0}/compose.yaml +0 -0
- {embedsync-0.4.0 → embedsync-0.5.0}/docker/Dockerfile +0 -0
- {embedsync-0.4.0 → embedsync-0.5.0}/examples/docs/api.md +0 -0
- {embedsync-0.4.0 → embedsync-0.5.0}/examples/docs/getting-started.md +0 -0
- {embedsync-0.4.0 → embedsync-0.5.0}/plans/README.md +0 -0
- {embedsync-0.4.0 → embedsync-0.5.0}/plans/v0.1-completed-foundation.md +0 -0
- {embedsync-0.4.0 → embedsync-0.5.0}/plans/v0.2-next-phase-roadmap.md +0 -0
- {embedsync-0.4.0 → embedsync-0.5.0}/src/embedsync/chunking.py +0 -0
- {embedsync-0.4.0 → embedsync-0.5.0}/src/embedsync/config.py +0 -0
- {embedsync-0.4.0 → embedsync-0.5.0}/src/embedsync/destinations/__init__.py +0 -0
- {embedsync-0.4.0 → embedsync-0.5.0}/src/embedsync/destinations/jsonl.py +0 -0
- {embedsync-0.4.0 → embedsync-0.5.0}/src/embedsync/destinations/memory.py +0 -0
- {embedsync-0.4.0 → embedsync-0.5.0}/src/embedsync/embedders.py +0 -0
- {embedsync-0.4.0 → embedsync-0.5.0}/src/embedsync/sources/__init__.py +0 -0
- {embedsync-0.4.0 → embedsync-0.5.0}/src/embedsync/sources/local.py +0 -0
- {embedsync-0.4.0 → embedsync-0.5.0}/src/embedsync/state/__init__.py +0 -0
- {embedsync-0.4.0 → embedsync-0.5.0}/src/embedsync/state/store.py +0 -0
- {embedsync-0.4.0 → embedsync-0.5.0}/src/embedsync/sync/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: embedsync
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Incremental synchronization between source documents and vector indexes
|
|
5
5
|
Project-URL: Homepage, https://github.com/yashshah9/embedsync
|
|
6
6
|
Project-URL: Repository, https://github.com/yashshah9/embedsync
|
|
@@ -29,7 +29,7 @@ Incremental synchronization between **source documents** and **vector indexes**
|
|
|
29
29
|
[](https://www.python.org/downloads/)
|
|
30
30
|
[](https://github.com/yashshah9/embedsync/actions/workflows/ci.yml)
|
|
31
31
|
|
|
32
|
-
> **Status:** v0.
|
|
32
|
+
> **Status:** v0.5 — hash + Ollama embedders, paragraph chunks, JSONL destination, chunk-level re-embed, `--full-reindex`.
|
|
33
33
|
|
|
34
34
|
## 60-second try
|
|
35
35
|
|
|
@@ -84,6 +84,7 @@ embedsync health
|
|
|
84
84
|
embedsync plan examples/docs --state-db /tmp/embedsync-demo.db
|
|
85
85
|
embedsync run examples/docs --dry-run --state-db /tmp/embedsync-demo.db
|
|
86
86
|
embedsync run examples/docs --embedder hash --destination memory --state-db /tmp/embedsync-demo.db
|
|
87
|
+
embedsync run examples/docs --full-reindex --embedder hash --destination jsonl:/tmp/index.jsonl
|
|
87
88
|
embedsync run examples/docs --embedder hash --destination jsonl:/tmp/index.jsonl
|
|
88
89
|
# Requires a running Ollama with an embedding model:
|
|
89
90
|
embedsync run examples/docs --embedder ollama --destination jsonl:/tmp/index.jsonl
|
|
@@ -6,7 +6,7 @@ Incremental synchronization between **source documents** and **vector indexes**
|
|
|
6
6
|
[](https://www.python.org/downloads/)
|
|
7
7
|
[](https://github.com/yashshah9/embedsync/actions/workflows/ci.yml)
|
|
8
8
|
|
|
9
|
-
> **Status:** v0.
|
|
9
|
+
> **Status:** v0.5 — hash + Ollama embedders, paragraph chunks, JSONL destination, chunk-level re-embed, `--full-reindex`.
|
|
10
10
|
|
|
11
11
|
## 60-second try
|
|
12
12
|
|
|
@@ -61,6 +61,7 @@ embedsync health
|
|
|
61
61
|
embedsync plan examples/docs --state-db /tmp/embedsync-demo.db
|
|
62
62
|
embedsync run examples/docs --dry-run --state-db /tmp/embedsync-demo.db
|
|
63
63
|
embedsync run examples/docs --embedder hash --destination memory --state-db /tmp/embedsync-demo.db
|
|
64
|
+
embedsync run examples/docs --full-reindex --embedder hash --destination jsonl:/tmp/index.jsonl
|
|
64
65
|
embedsync run examples/docs --embedder hash --destination jsonl:/tmp/index.jsonl
|
|
65
66
|
# Requires a running Ollama with an embedding model:
|
|
66
67
|
embedsync run examples/docs --embedder ollama --destination jsonl:/tmp/index.jsonl
|
|
@@ -46,11 +46,12 @@ def _destination(dest_spec: str) -> MemoryDestination | JsonlDestination:
|
|
|
46
46
|
@main.command("plan")
|
|
47
47
|
@click.argument("source_dir", type=click.Path(exists=True, file_okay=False, path_type=Path))
|
|
48
48
|
@click.option("--state-db", default=None, help="SQLite state database path")
|
|
49
|
-
|
|
49
|
+
@click.option("--full-reindex", is_flag=True, help="Treat all current docs as updates")
|
|
50
|
+
def plan_cmd(source_dir: Path, state_db: str | None, full_reindex: bool) -> None:
|
|
50
51
|
store, path = _store(state_db)
|
|
51
52
|
try:
|
|
52
53
|
source = LocalFileSource(source_dir)
|
|
53
|
-
sync_plan = plan_sync(source, store)
|
|
54
|
+
sync_plan = plan_sync(source, store, full_reindex=full_reindex)
|
|
54
55
|
except ValueError as exc:
|
|
55
56
|
console.print(f"[red]Error:[/red] {exc}")
|
|
56
57
|
raise SystemExit(2) from exc
|
|
@@ -76,12 +77,14 @@ def plan_cmd(source_dir: Path, state_db: str | None) -> None:
|
|
|
76
77
|
@click.option("--state-db", default=None)
|
|
77
78
|
@click.option("--embedder", default="hash")
|
|
78
79
|
@click.option("--destination", "dest_spec", default="memory", help="memory | jsonl:/path")
|
|
80
|
+
@click.option("--full-reindex", is_flag=True, help="Force re-embed all current docs")
|
|
79
81
|
def run_cmd(
|
|
80
82
|
source_dir: Path,
|
|
81
83
|
dry_run: bool,
|
|
82
84
|
state_db: str | None,
|
|
83
85
|
embedder: str,
|
|
84
86
|
dest_spec: str,
|
|
87
|
+
full_reindex: bool,
|
|
85
88
|
) -> None:
|
|
86
89
|
store, path = _store(state_db)
|
|
87
90
|
try:
|
|
@@ -92,6 +95,7 @@ def run_cmd(
|
|
|
92
95
|
destination=dest,
|
|
93
96
|
dry_run=dry_run,
|
|
94
97
|
embedder=resolve_embedder(embedder),
|
|
98
|
+
full_reindex=full_reindex,
|
|
95
99
|
)
|
|
96
100
|
except (ValueError, click.UsageError) as exc:
|
|
97
101
|
store.close()
|
|
@@ -5,7 +5,12 @@ from dataclasses import dataclass, field
|
|
|
5
5
|
import structlog
|
|
6
6
|
|
|
7
7
|
from embedsync.chunking import chunk_document
|
|
8
|
-
from embedsync.destinations.memory import
|
|
8
|
+
from embedsync.destinations.memory import (
|
|
9
|
+
Destination,
|
|
10
|
+
DestinationReport,
|
|
11
|
+
MemoryDestination,
|
|
12
|
+
SyncAction,
|
|
13
|
+
)
|
|
9
14
|
from embedsync.embedders import Embedder, HashEmbedder
|
|
10
15
|
from embedsync.sources.local import LocalFileSource
|
|
11
16
|
from embedsync.state.store import DocumentState, StateStore, content_hash
|
|
@@ -24,7 +29,11 @@ class SyncPlan:
|
|
|
24
29
|
return len(self.adds) + len(self.updates) + len(self.deletes)
|
|
25
30
|
|
|
26
31
|
|
|
27
|
-
def plan_sync(
|
|
32
|
+
def plan_sync(
|
|
33
|
+
source: LocalFileSource,
|
|
34
|
+
store: StateStore,
|
|
35
|
+
full_reindex: bool = False,
|
|
36
|
+
) -> SyncPlan:
|
|
28
37
|
"""Compute add/update/delete plan without touching the destination."""
|
|
29
38
|
plan = SyncPlan()
|
|
30
39
|
current_ids: set[str] = set()
|
|
@@ -43,7 +52,7 @@ def plan_sync(source: LocalFileSource, store: StateStore) -> SyncPlan:
|
|
|
43
52
|
)
|
|
44
53
|
if existing is None:
|
|
45
54
|
plan.adds.append(action)
|
|
46
|
-
elif existing.content_hash != digest:
|
|
55
|
+
elif full_reindex or existing.content_hash != digest:
|
|
47
56
|
action.action = "update"
|
|
48
57
|
plan.updates.append(action)
|
|
49
58
|
|
|
@@ -60,19 +69,27 @@ def execute_sync(
|
|
|
60
69
|
destination: Destination | None = None,
|
|
61
70
|
dry_run: bool = False,
|
|
62
71
|
embedder: Embedder | None = None,
|
|
72
|
+
full_reindex: bool = False,
|
|
63
73
|
) -> DestinationReport:
|
|
64
74
|
dest: Destination = destination or MemoryDestination()
|
|
65
75
|
encoder = embedder or HashEmbedder()
|
|
66
|
-
plan = plan_sync(source, store)
|
|
76
|
+
plan = plan_sync(source, store, full_reindex=full_reindex)
|
|
67
77
|
report = DestinationReport()
|
|
68
78
|
docs = {d.doc_id: d for d in source.list_documents()}
|
|
69
79
|
|
|
70
80
|
for action in plan.adds + plan.updates:
|
|
71
81
|
old = store.chunks_for(action.doc_id)
|
|
72
82
|
new_hashes = {chunk.chunk_id: content_hash(chunk.content) for chunk in action.chunks}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
83
|
+
if full_reindex and action.action == "update":
|
|
84
|
+
# Force DELETE old chunks + ADD fresh embeddings, ignore content-hash equality.
|
|
85
|
+
write_chunks = action.chunks
|
|
86
|
+
removed = list(old.keys())
|
|
87
|
+
else:
|
|
88
|
+
changed = [
|
|
89
|
+
chunk for chunk in action.chunks if old.get(chunk.chunk_id) != new_hashes[chunk.chunk_id]
|
|
90
|
+
]
|
|
91
|
+
removed = [chunk_id for chunk_id in old if chunk_id not in new_hashes]
|
|
92
|
+
write_chunks = action.chunks if action.action == "add" else changed
|
|
76
93
|
texts = [c.content for c in write_chunks]
|
|
77
94
|
vectors = encoder.embed(texts) if texts else []
|
|
78
95
|
dest_action = SyncAction(
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Tests for embedders."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
5
4
|
import threading
|
|
5
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
6
6
|
|
|
7
7
|
from embedsync.embedders import HashEmbedder, OllamaEmbedder, resolve_embedder
|
|
8
8
|
|
|
@@ -23,7 +23,7 @@ def test_resolve_ollama_model_name() -> None:
|
|
|
23
23
|
|
|
24
24
|
def test_ollama_embedder_against_mock_server() -> None:
|
|
25
25
|
class Handler(BaseHTTPRequestHandler):
|
|
26
|
-
def do_POST(self) -> None:
|
|
26
|
+
def do_POST(self) -> None:
|
|
27
27
|
length = int(self.headers.get("Content-Length", "0"))
|
|
28
28
|
body = json.loads(self.rfile.read(length))
|
|
29
29
|
assert body["model"] == "nomic-embed-text"
|
|
@@ -34,7 +34,7 @@ def test_ollama_embedder_against_mock_server() -> None:
|
|
|
34
34
|
self.end_headers()
|
|
35
35
|
self.wfile.write(payload)
|
|
36
36
|
|
|
37
|
-
def log_message(self, format: str, *args: object) -> None:
|
|
37
|
+
def log_message(self, format: str, *args: object) -> None:
|
|
38
38
|
return
|
|
39
39
|
|
|
40
40
|
httpd = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
|
@@ -30,6 +30,19 @@ def test_run_indexes_documents(tmp_path: Path) -> None:
|
|
|
30
30
|
store.close()
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
def test_full_reindex_rewrites_unchanged_content(tmp_path: Path) -> None:
|
|
34
|
+
store = StateStore(str(tmp_path / "state.db"))
|
|
35
|
+
source = LocalFileSource(DOCS)
|
|
36
|
+
dest = MemoryDestination()
|
|
37
|
+
first = execute_sync(source, store, dest)
|
|
38
|
+
assert first.embeddings_written > 0
|
|
39
|
+
second = execute_sync(source, store, dest)
|
|
40
|
+
assert second.embeddings_written == 0
|
|
41
|
+
third = execute_sync(source, store, dest, full_reindex=True)
|
|
42
|
+
assert third.embeddings_written == first.embeddings_written
|
|
43
|
+
store.close()
|
|
44
|
+
|
|
45
|
+
|
|
33
46
|
def test_update_reembeds_changed_chunks_only(tmp_path: Path) -> None:
|
|
34
47
|
docs = tmp_path / "docs"
|
|
35
48
|
docs.mkdir()
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Scenario tests for embedsync 0.5 behavior."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
import pytest
|
|
10
|
+
from click.testing import CliRunner
|
|
11
|
+
|
|
12
|
+
from embedsync.cli import _destination, main
|
|
13
|
+
from embedsync.destinations.jsonl import JsonlDestination
|
|
14
|
+
from embedsync.destinations.memory import MemoryDestination
|
|
15
|
+
from embedsync.embedders import HashEmbedder, OllamaEmbedder, resolve_embedder
|
|
16
|
+
from embedsync.sources.local import LocalFileSource
|
|
17
|
+
from embedsync.state.store import StateStore
|
|
18
|
+
from embedsync.sync.engine import execute_sync, plan_sync
|
|
19
|
+
|
|
20
|
+
DOCS = Path(__file__).parent.parent / "examples" / "docs"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _docs(tmp_path: Path, files: dict[str, str]) -> Path:
|
|
24
|
+
root = tmp_path / "docs"
|
|
25
|
+
root.mkdir()
|
|
26
|
+
for name, text in files.items():
|
|
27
|
+
path = root / name
|
|
28
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
path.write_text(text, encoding="utf-8")
|
|
30
|
+
return root
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_full_reindex_forces_rewrite_when_unchanged(tmp_path: Path) -> None:
|
|
34
|
+
store = StateStore(str(tmp_path / "state.db"))
|
|
35
|
+
source = LocalFileSource(DOCS)
|
|
36
|
+
dest = MemoryDestination()
|
|
37
|
+
first = execute_sync(source, store, dest)
|
|
38
|
+
assert first.embeddings_written > 0
|
|
39
|
+
assert execute_sync(source, store, dest).embeddings_written == 0
|
|
40
|
+
third = execute_sync(source, store, dest, full_reindex=True)
|
|
41
|
+
assert third.embeddings_written == first.embeddings_written
|
|
42
|
+
store.close()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_plan_adds_new_documents(tmp_path: Path) -> None:
|
|
46
|
+
docs = _docs(tmp_path, {"a.md": "alpha content here"})
|
|
47
|
+
store = StateStore(str(tmp_path / "state.db"))
|
|
48
|
+
plan = plan_sync(LocalFileSource(docs), store)
|
|
49
|
+
assert len(plan.adds) == 1
|
|
50
|
+
assert plan.adds[0].doc_id == "a.md"
|
|
51
|
+
assert plan.updates == []
|
|
52
|
+
assert plan.deletes == []
|
|
53
|
+
store.close()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_plan_updates_changed_documents(tmp_path: Path) -> None:
|
|
57
|
+
docs = _docs(tmp_path, {"a.md": "original text"})
|
|
58
|
+
store = StateStore(str(tmp_path / "state.db"))
|
|
59
|
+
execute_sync(LocalFileSource(docs), store, MemoryDestination())
|
|
60
|
+
(docs / "a.md").write_text("changed text", encoding="utf-8")
|
|
61
|
+
plan = plan_sync(LocalFileSource(docs), store)
|
|
62
|
+
assert plan.adds == []
|
|
63
|
+
assert len(plan.updates) == 1
|
|
64
|
+
assert plan.updates[0].doc_id == "a.md"
|
|
65
|
+
store.close()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_plan_deletes_removed_documents(tmp_path: Path) -> None:
|
|
69
|
+
docs = _docs(tmp_path, {"keep.md": "keep", "gone.md": "gone"})
|
|
70
|
+
store = StateStore(str(tmp_path / "state.db"))
|
|
71
|
+
execute_sync(LocalFileSource(docs), store, MemoryDestination())
|
|
72
|
+
(docs / "gone.md").unlink()
|
|
73
|
+
plan = plan_sync(LocalFileSource(docs), store)
|
|
74
|
+
assert len(plan.deletes) == 1
|
|
75
|
+
assert plan.deletes[0].doc_id == "gone.md"
|
|
76
|
+
store.close()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_plan_full_reindex_marks_existing_as_updates(tmp_path: Path) -> None:
|
|
80
|
+
docs = _docs(tmp_path, {"a.md": "stable"})
|
|
81
|
+
store = StateStore(str(tmp_path / "state.db"))
|
|
82
|
+
execute_sync(LocalFileSource(docs), store, MemoryDestination())
|
|
83
|
+
plan = plan_sync(LocalFileSource(docs), store, full_reindex=True)
|
|
84
|
+
assert plan.adds == []
|
|
85
|
+
assert len(plan.updates) == 1
|
|
86
|
+
store.close()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_idempotent_run_writes_zero_on_second_pass(tmp_path: Path) -> None:
|
|
90
|
+
store = StateStore(str(tmp_path / "state.db"))
|
|
91
|
+
source = LocalFileSource(DOCS)
|
|
92
|
+
dest = MemoryDestination()
|
|
93
|
+
first = execute_sync(source, store, dest)
|
|
94
|
+
second = execute_sync(source, store, dest)
|
|
95
|
+
assert first.embeddings_written > 0
|
|
96
|
+
assert second.embeddings_written == 0
|
|
97
|
+
assert second.actions == []
|
|
98
|
+
store.close()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_hash_embedder_dimension() -> None:
|
|
102
|
+
emb = HashEmbedder(dimension=16)
|
|
103
|
+
vectors = emb.embed(["hello", "world"])
|
|
104
|
+
assert emb.dimension == 16
|
|
105
|
+
assert len(vectors) == 2
|
|
106
|
+
assert all(len(v) == 16 for v in vectors)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_hash_embedder_stability() -> None:
|
|
110
|
+
emb = HashEmbedder(dimension=8)
|
|
111
|
+
a = emb.embed(["stable text"])[0]
|
|
112
|
+
b = emb.embed(["stable text"])[0]
|
|
113
|
+
c = emb.embed(["different"])[0]
|
|
114
|
+
assert a == b
|
|
115
|
+
assert a != c
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_resolve_ollama_default_model() -> None:
|
|
119
|
+
emb = resolve_embedder("ollama")
|
|
120
|
+
assert isinstance(emb, OllamaEmbedder)
|
|
121
|
+
assert emb.model == "nomic-embed-text"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_resolve_ollama_colon_model() -> None:
|
|
125
|
+
emb = resolve_embedder("ollama:mxbai-embed-large")
|
|
126
|
+
assert isinstance(emb, OllamaEmbedder)
|
|
127
|
+
assert emb.model == "mxbai-embed-large"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_bad_destination_raises() -> None:
|
|
131
|
+
with pytest.raises(click.UsageError, match="memory.*jsonl"):
|
|
132
|
+
_destination("pgvector://localhost")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_empty_source_dir_cli_error(tmp_path: Path) -> None:
|
|
136
|
+
empty = tmp_path / "empty"
|
|
137
|
+
empty.mkdir()
|
|
138
|
+
runner = CliRunner()
|
|
139
|
+
# Empty dir is valid for click Path, but plan reports zero actions (no docs).
|
|
140
|
+
result = runner.invoke(
|
|
141
|
+
main,
|
|
142
|
+
["plan", str(empty), "--state-db", str(tmp_path / "state.db")],
|
|
143
|
+
)
|
|
144
|
+
assert result.exit_code == 0
|
|
145
|
+
assert "Total: 0 action(s)" in result.output
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_missing_source_dir_cli_error(tmp_path: Path) -> None:
|
|
149
|
+
runner = CliRunner()
|
|
150
|
+
result = runner.invoke(
|
|
151
|
+
main,
|
|
152
|
+
["plan", str(tmp_path / "does-not-exist"), "--state-db", str(tmp_path / "state.db")],
|
|
153
|
+
)
|
|
154
|
+
assert result.exit_code != 0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_bad_destination_cli_error(tmp_path: Path) -> None:
|
|
158
|
+
docs = _docs(tmp_path, {"a.md": "hi"})
|
|
159
|
+
runner = CliRunner()
|
|
160
|
+
result = runner.invoke(
|
|
161
|
+
main,
|
|
162
|
+
[
|
|
163
|
+
"run",
|
|
164
|
+
str(docs),
|
|
165
|
+
"--state-db",
|
|
166
|
+
str(tmp_path / "state.db"),
|
|
167
|
+
"--destination",
|
|
168
|
+
"bogus",
|
|
169
|
+
],
|
|
170
|
+
)
|
|
171
|
+
assert result.exit_code != 0
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_jsonl_destination_write_roundtrip(tmp_path: Path) -> None:
|
|
175
|
+
docs = _docs(tmp_path, {"note.md": "jsonl roundtrip content"})
|
|
176
|
+
out = tmp_path / "out" / "embeddings.jsonl"
|
|
177
|
+
store = StateStore(str(tmp_path / "state.db"))
|
|
178
|
+
dest = JsonlDestination(out)
|
|
179
|
+
report = execute_sync(LocalFileSource(docs), store, dest)
|
|
180
|
+
assert report.embeddings_written > 0
|
|
181
|
+
assert out.exists()
|
|
182
|
+
rows = [json.loads(line) for line in out.read_text(encoding="utf-8").splitlines() if line.strip()]
|
|
183
|
+
assert len(rows) == report.embeddings_written
|
|
184
|
+
assert all("embedding" in r and "chunk_id" in r and r["doc_id"] == "note.md" for r in rows)
|
|
185
|
+
store.close()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_dry_run_writes_zero_embeddings(tmp_path: Path) -> None:
|
|
189
|
+
store = StateStore(str(tmp_path / "state.db"))
|
|
190
|
+
dest = MemoryDestination()
|
|
191
|
+
report = execute_sync(LocalFileSource(DOCS), store, dest, dry_run=True)
|
|
192
|
+
assert report.embeddings_written == 0
|
|
193
|
+
assert dest.indexed == {}
|
|
194
|
+
assert dest.vectors == {}
|
|
195
|
+
assert store.all_ids() == set()
|
|
196
|
+
store.close()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|