返回 last30days-skill
test_corpus_source.py
根目录 / tests / test_corpus_source.py
1 from __future__ import annotations
2
3 import hashlib
4 import io
5 import json
6 import os
7 import sqlite3
8 import sys
9 from contextlib import redirect_stderr, redirect_stdout
10 from datetime import datetime, timezone
11 from pathlib import Path
12 from unittest import mock
13
14 import last30days as cli
15 import store
16 from lib import corpus, env, health, html_render, library, library_index, pipeline, render, schema
17
18
19 def _set_mtime(path: Path, value: str) -> None:
20 timestamp = datetime.fromisoformat(value).replace(tzinfo=timezone.utc).timestamp()
21 os.utime(path, (timestamp, timestamp))
22
23
24 def _scan(tmp_path: Path, *, all_time: bool = False, limit: int = 12):
25 return corpus.search(
26 "MCP servers",
27 [tmp_path],
28 from_date="2026-06-10",
29 to_date="2026-07-10",
30 all_time=all_time,
31 limit=limit,
32 cache_dir=tmp_path / "cache",
33 )
34
35
36 def test_scans_matching_text_and_markdown_with_path_titles(tmp_path):
37 note = tmp_path / "mcp-server-notes.md"
38 note.write_text("MCP servers expose tools to local coding agents.", encoding="utf-8")
39 other = tmp_path / "groceries.txt"
40 other.write_text("milk eggs bread", encoding="utf-8")
41 _set_mtime(note, "2026-07-05T12:00:00")
42 _set_mtime(other, "2026-07-05T12:00:00")
43
44 result = _scan(tmp_path)
45
46 assert [item.title for item in result.items] == ["mcp server notes"]
47 assert result.items[0].source == "corpus"
48 assert result.items[0].published_at == "2026-07-05"
49 assert result.items[0].metadata["local_only"] is True
50 assert result.items[0].url.startswith("corpus://")
51 assert str(note) not in result.items[0].url
52
53
54 def test_multilingual_matching_reuses_shared_cjk_tokenizer(tmp_path):
55 note = tmp_path / "模型记录.md"
56 note.write_text("国产大模型的最新测评和部署记录", encoding="utf-8")
57 _set_mtime(note, "2026-07-05T12:00:00")
58
59 result = corpus.search(
60 "国产大模型 测评",
61 [tmp_path],
62 from_date="2026-06-10",
63 to_date="2026-07-10",
64 cache_dir=tmp_path / "cache",
65 )
66
67 assert [item.title for item in result.items] == ["模型记录"]
68
69
70 def test_recency_window_and_all_time_override(tmp_path):
71 old = tmp_path / "old-mcp-plan.md"
72 old.write_text("MCP servers and local tool protocols", encoding="utf-8")
73 _set_mtime(old, "2025-01-01T00:00:00")
74
75 assert _scan(tmp_path).items == []
76 assert [item.title for item in _scan(tmp_path, all_time=True).items] == ["old mcp plan"]
77
78
79 def test_hidden_git_and_node_modules_directories_are_ignored(tmp_path):
80 visible = tmp_path / "visible.md"
81 visible.write_text("MCP servers are visible", encoding="utf-8")
82 _set_mtime(visible, "2026-07-05T00:00:00")
83 for directory in (tmp_path / ".git", tmp_path / ".hidden", tmp_path / "node_modules"):
84 directory.mkdir()
85 path = directory / "private.md"
86 path.write_text("MCP servers hidden secret", encoding="utf-8")
87 _set_mtime(path, "2026-07-05T00:00:00")
88
89 result = _scan(tmp_path)
90
91 assert [item.metadata["relative_path"] for item in result.items] == ["visible.md"]
92
93
94 def test_pdf_is_skipped_with_one_note_when_pdftotext_is_absent(tmp_path, monkeypatch):
95 pdf = tmp_path / "mcp.pdf"
96 pdf.write_bytes(b"not a real pdf")
97 _set_mtime(pdf, "2026-07-05T00:00:00")
98 monkeypatch.setattr(corpus, "which", lambda _name: None)
99
100 result = _scan(tmp_path)
101
102 assert result.items == []
103 assert result.notes == ["Skipped PDF files because pdftotext is not on PATH"]
104
105
106 def test_mtime_cache_reuses_text_and_is_private(tmp_path, monkeypatch):
107 note = tmp_path / "mcp.md"
108 note.write_text("MCP servers cache this local note", encoding="utf-8")
109 _set_mtime(note, "2026-07-05T00:00:00")
110 first = _scan(tmp_path)
111 assert first.cache_hits == 0
112
113 monkeypatch.setattr(corpus, "_extract_text", mock.Mock(side_effect=AssertionError("cache miss")))
114 second = _scan(tmp_path)
115
116 assert second.cache_hits == 1
117 cache_path = tmp_path / "cache" / corpus.CACHE_FILENAME
118 assert cache_path.stat().st_mode & 0o777 == 0o600
119 assert cache_path.parent.stat().st_mode & 0o777 == 0o700
120
121
122 def test_cache_for_500_large_documents_is_bounded_by_total_bytes(tmp_path, monkeypatch):
123 for index in range(500):
124 (tmp_path / f"document-{index:03d}.txt").touch()
125
126 monkeypatch.setattr(corpus, "MAX_CACHE_BYTES", 1_000_000)
127 monkeypatch.setattr(corpus, "MAX_CACHE_TEXT_CHARS", 100_000)
128 monkeypatch.setattr(corpus, "_extract_text", lambda *_args, **_kwargs: "x" * 1_000_000)
129 monkeypatch.setattr(corpus, "_match_score", lambda *_args, **_kwargs: 0.0)
130
131 result = corpus.search(
132 "anything",
133 [tmp_path],
134 from_date="2026-06-10",
135 to_date="2026-07-10",
136 all_time=True,
137 limit=0,
138 cache_dir=tmp_path / "cache",
139 )
140
141 cache_path = tmp_path / "cache" / corpus.CACHE_FILENAME
142 payload = json.loads(cache_path.read_text(encoding="utf-8"))
143 assert result.files_scanned == 500
144 assert cache_path.stat().st_size <= corpus.MAX_CACHE_BYTES
145 assert len(payload["entries"]) < 500
146 assert all(
147 len(entry["text"]) <= corpus.MAX_CACHE_TEXT_CHARS
148 for entry in payload["entries"].values()
149 )
150
151
152 def test_result_budget_caps_one_corpus_stream(tmp_path):
153 for index in range(10):
154 path = tmp_path / f"mcp-{index}.md"
155 path.write_text(f"MCP servers local note {index}", encoding="utf-8")
156 _set_mtime(path, f"2026-07-{index + 1:02d}T00:00:00")
157
158 assert len(_scan(tmp_path, limit=3).items) == 3
159
160
161 def test_file_cap_is_shared_fairly_across_corpus_roots(tmp_path, monkeypatch):
162 archive = tmp_path / "archive"
163 relevant = tmp_path / "relevant"
164 archive.mkdir()
165 relevant.mkdir()
166 for index in range(4):
167 (archive / f"mcp-archive-{index}.md").write_text(
168 f"MCP servers archived note {index}", encoding="utf-8"
169 )
170 later = relevant / "mcp-current.md"
171 later.write_text("MCP servers relevant current note", encoding="utf-8")
172 monkeypatch.setattr(corpus, "MAX_FILES", 4)
173
174 result = corpus.search(
175 "MCP servers",
176 [archive, relevant],
177 from_date="2026-06-10",
178 to_date="2026-07-10",
179 all_time=True,
180 cache_dir=tmp_path / "cache",
181 )
182
183 assert result.files_scanned <= corpus.MAX_FILES
184 assert any(item.metadata["path"].startswith(str(archive)) for item in result.items)
185 assert any(item.metadata["path"] == str(later) for item in result.items)
186
187
188 def test_pipeline_runs_corpus_outside_network_executor(tmp_path, monkeypatch):
189 note = tmp_path / "mcp-private.md"
190 note.write_text("MCP servers use a secret local transport", encoding="utf-8")
191 _set_mtime(note, "2026-07-05T00:00:00")
192 retrieve = mock.Mock(return_value=([], {}))
193 monkeypatch.setattr(pipeline, "_retrieve_stream", retrieve)
194
195 report = pipeline.run(
196 topic="MCP servers",
197 config={"_CORPUS_DIRS": [str(tmp_path)], "EXCLUDE_SOURCES": ""},
198 depth="quick",
199 requested_sources=["corpus"],
200 mock=True,
201 as_of_date="2026-07-10",
202 external_plan={
203 "intent": "concept",
204 "freshness_mode": "balanced_recent",
205 "cluster_mode": "none",
206 "source_weights": {"corpus": 1.0},
207 "subqueries": [{
208 "label": "primary",
209 "search_query": "MCP servers",
210 "ranking_query": "MCP servers",
211 "sources": ["corpus"],
212 }],
213 },
214 )
215
216 assert report.source_status["corpus"].state == health.OK
217 assert len(report.items_by_source["corpus"]) == 1
218 assert len(report.items_by_source["corpus"]) <= pipeline.DEPTH_SETTINGS["quick"]["per_stream_limit"]
219 assert all(call.kwargs["source"] != "corpus" for call in retrieve.call_args_list)
220
221
222 def test_corpus_never_enters_remote_rerank_or_fun_prompts(tmp_path, monkeypatch):
223 note = tmp_path / "private.md"
224 note.write_text("Model context protocol servers PRIVATE-RERANK-SENTINEL", encoding="utf-8")
225 _set_mtime(note, "2026-07-05T00:00:00")
226 remote = mock.Mock()
227 remote.generate_json = mock.Mock(side_effect=AssertionError("private prompt left machine"))
228 runtime = schema.ProviderRuntime("local", "remote-planner", "remote-reranker")
229 monkeypatch.setattr(pipeline.providers, "resolve_runtime", lambda *_args, **_kwargs: (runtime, remote))
230 monkeypatch.setattr(pipeline, "available_sources", lambda *_args, **_kwargs: ["corpus"])
231
232 report = pipeline.run(
233 topic="how do model context protocol servers work today?",
234 config={"_CORPUS_DIRS": [str(tmp_path)], "EXCLUDE_SOURCES": ""},
235 depth="quick",
236 requested_sources=["corpus"],
237 mock=False,
238 as_of_date="2026-07-10",
239 external_plan={
240 "intent": "how_to",
241 "freshness_mode": "evergreen_ok",
242 "cluster_mode": "workflow",
243 "source_weights": {"corpus": 1.0},
244 "subqueries": [{
245 "label": "primary",
246 "search_query": "model context protocol servers",
247 "ranking_query": "How do model context protocol servers work?",
248 "sources": ["corpus"],
249 }],
250 },
251 )
252
253 assert report.items_by_source["corpus"]
254 remote.generate_json.assert_not_called()
255
256
257 def test_explicit_unconfigured_corpus_records_skipped_outcome():
258 report = pipeline.run(
259 topic="how do local model context protocol servers work?",
260 config={"EXCLUDE_SOURCES": ""},
261 depth="quick",
262 requested_sources=["corpus"],
263 mock=True,
264 as_of_date="2026-07-10",
265 external_plan={
266 "intent": "how_to",
267 "freshness_mode": "evergreen_ok",
268 "cluster_mode": "workflow",
269 "source_weights": {"corpus": 1.0},
270 "subqueries": [{
271 "label": "primary",
272 "search_query": "model context protocol servers",
273 "ranking_query": "How do model context protocol servers work?",
274 "sources": ["corpus"],
275 }],
276 },
277 )
278
279 outcome = report.source_status["corpus"]
280 assert outcome.state == schema.SKIPPED_UNCONFIGURED
281 assert outcome.attempted is False
282
283
284 def _privacy_report(secret: str = "PRIVATE-CORPUS-SENTINEL") -> schema.Report:
285 item = schema.SourceItem(
286 item_id="C-private",
287 source="corpus",
288 title="private mcp notes",
289 body=f"MCP servers {secret}",
290 url="",
291 published_at="2026-07-05",
292 snippet=f"MCP servers {secret}",
293 metadata={"relative_path": "notes/private.md", "local_only": True},
294 )
295 candidate = schema.Candidate(
296 candidate_id="corpus:C-private",
297 item_id=item.item_id,
298 source="corpus",
299 title=item.title,
300 url="",
301 snippet=item.snippet,
302 subquery_labels=["primary"],
303 native_ranks={"primary:corpus": 1},
304 local_relevance=1.0,
305 freshness=90,
306 engagement=None,
307 source_quality=0.75,
308 rrf_score=0.01,
309 final_score=91,
310 cluster_id="cluster-private",
311 sources=["corpus"],
312 source_items=[item],
313 )
314 return schema.Report(
315 topic="MCP servers",
316 range_from="2026-06-10",
317 range_to="2026-07-10",
318 generated_at="2026-07-10T00:00:00+00:00",
319 provider_runtime=schema.ProviderRuntime("local", "mock", "mock"),
320 query_plan=schema.QueryPlan(
321 intent="concept",
322 freshness_mode="balanced_recent",
323 cluster_mode="none",
324 raw_topic="MCP servers",
325 subqueries=[schema.SubQuery("primary", "MCP servers", "MCP servers", ["corpus"])],
326 source_weights={"corpus": 1.0},
327 ),
328 clusters=[schema.Cluster(
329 cluster_id="cluster-private",
330 title=f"Private cluster {secret}",
331 candidate_ids=[candidate.candidate_id],
332 representative_ids=[candidate.candidate_id],
333 sources=["corpus"],
334 score=91,
335 )],
336 ranked_candidates=[candidate],
337 items_by_source={"corpus": [item]},
338 errors_by_source={},
339 source_status={"corpus": schema.SourceOutcome("corpus", health.OK, 1)},
340 )
341
342
343 def test_corpus_findings_persist_with_stable_opaque_keys(tmp_path, monkeypatch):
344 note = tmp_path / "private-customer-notes.md"
345 note.write_text("MCP servers persist this private finding", encoding="utf-8")
346 first = corpus.search(
347 "MCP servers",
348 [tmp_path],
349 from_date="2026-06-10",
350 to_date="2026-07-10",
351 all_time=True,
352 cache_dir=tmp_path / "cache",
353 ).items[0]
354 second = corpus.search(
355 "MCP servers",
356 [tmp_path],
357 from_date="2026-06-10",
358 to_date="2026-07-10",
359 all_time=True,
360 cache_dir=tmp_path / "cache",
361 ).items[0]
362 report = _privacy_report()
363 report.items_by_source["corpus"][0].url = first.url
364 report.ranked_candidates[0].url = first.url
365
366 db_path = tmp_path / "research.db"
367 monkeypatch.setattr(store, "_db_override", db_path)
368 store.init_db()
369 topic = store.add_topic(report.topic)
370 run_id = store.record_run(topic["id"], status="completed")
371 findings = store.findings_from_report(report)
372 counts = store.store_findings(run_id, topic["id"], findings)
373
374 assert first.url == second.url
375 assert first.url.startswith("corpus://")
376 assert str(note) not in first.url
377 assert counts == {"new": 1, "updated": 0}
378 with sqlite3.connect(db_path) as conn:
379 persisted = conn.execute(
380 "SELECT source, source_url FROM findings"
381 ).fetchone()
382 assert persisted == ("corpus", first.url)
383 assert library_index.search(
384 "private finding",
385 db_path=tmp_path / "missing-library.db",
386 store_db_path=db_path,
387 ) == []
388
389
390 def test_persist_report_hardens_corpus_store_and_sidecars(tmp_path, monkeypatch):
391 db_path = tmp_path / "store" / "research.db"
392 monkeypatch.setattr(store, "_db_override", db_path)
393 original_findings_from_report = store.findings_from_report
394 original_store_findings = store.store_findings
395 original_update_run = store.update_run
396
397 def make_permissive():
398 db_path.chmod(0o644)
399 for suffix in ("-wal", "-shm"):
400 sidecar = Path(f"{db_path}{suffix}")
401 sidecar.touch(mode=0o644)
402 sidecar.chmod(0o644)
403
404 def findings_from_report_with_permissive_store(*args, **kwargs):
405 findings = original_findings_from_report(*args, **kwargs)
406 make_permissive()
407 return findings
408
409 def store_findings_after_private_check(*args, **kwargs):
410 for artifact in (db_path, Path(f"{db_path}-wal"), Path(f"{db_path}-shm")):
411 assert artifact.stat().st_mode & 0o777 == 0o600
412 return original_store_findings(*args, **kwargs)
413
414 def update_run_with_permissive_sidecars(*args, **kwargs):
415 result = original_update_run(*args, **kwargs)
416 make_permissive()
417 return result
418
419 monkeypatch.setattr(store, "findings_from_report", findings_from_report_with_permissive_store)
420 monkeypatch.setattr(store, "store_findings", store_findings_after_private_check)
421 monkeypatch.setattr(store, "update_run", update_run_with_permissive_sidecars)
422
423 cli.persist_report(_privacy_report())
424
425 for artifact in (db_path, Path(f"{db_path}-wal"), Path(f"{db_path}-shm")):
426 assert artifact.stat().st_mode & 0o777 == 0o600
427
428
429 def test_agent_export_excludes_corpus_by_default_and_allows_explicit_opt_in():
430 report = _privacy_report()
431
432 private_default = schema.to_agent_export(report)
433 opted_in = schema.to_agent_export(report, corpus_in_export=True)
434
435 assert private_default["results"] == []
436 assert private_default["clusters"] == []
437 assert "corpus" not in private_default["source_status"]
438 assert opted_in["results"][0]["source"] == "corpus"
439 assert "PRIVATE-CORPUS-SENTINEL" in opted_in["results"][0]["summary"]
440
441 report.artifacts["corpus_in_export"] = True
442 assert schema.to_agent_export(report)["results"][0]["source"] == "corpus"
443
444
445 def test_agent_export_rebuilds_mixed_cluster_title_after_private_representative_removed():
446 report = _privacy_report()
447 social_item = schema.SourceItem(
448 item_id="R-public",
449 source="reddit",
450 title="Public MCP discussion",
451 body="Public evidence",
452 url="https://reddit.example/public",
453 published_at="2026-07-06",
454 snippet="Public evidence",
455 )
456 social = schema.Candidate(
457 candidate_id="reddit:public",
458 item_id=social_item.item_id,
459 source="reddit",
460 title=social_item.title,
461 url=social_item.url,
462 snippet=social_item.snippet,
463 subquery_labels=["primary"],
464 native_ranks={"primary:reddit": 1},
465 local_relevance=0.8,
466 freshness=90,
467 engagement=1,
468 source_quality=0.6,
469 rrf_score=0.01,
470 final_score=80,
471 cluster_id="cluster-private",
472 sources=["reddit"],
473 source_items=[social_item],
474 )
475 report.ranked_candidates.append(social)
476 report.items_by_source["reddit"] = [social_item]
477 report.source_status["reddit"] = schema.SourceOutcome("reddit", health.OK, 1)
478 report.clusters[0].candidate_ids.append(social.candidate_id)
479 report.clusters[0].representative_ids.append(social.candidate_id)
480 report.clusters[0].sources.append("reddit")
481
482 exported = schema.to_agent_export(report)
483
484 assert exported["clusters"][0]["title"] == "Public MCP discussion"
485 assert "PRIVATE-CORPUS-SENTINEL" not in str(exported)
486
487
488 def test_local_report_has_badged_from_your_files_section():
489 rendered = render.render_compact(_privacy_report())
490
491 assert "## From your files" in rendered
492 assert "LOCAL ONLY" in rendered
493 assert "PRIVATE-CORPUS-SENTINEL" in rendered
494
495
496 def test_publish_html_sends_sanitized_report_not_local_corpus(monkeypatch):
497 report = _privacy_report()
498 captured: dict[str, str] = {}
499
500 def publish(rendered, **_kwargs):
501 captured["html"] = rendered
502 return {"url": "https://example.ht-ml.app"}
503
504 monkeypatch.setattr(cli.env, "get_config", lambda **_kwargs: {})
505 monkeypatch.setattr(cli.pipeline, "diagnose", lambda *_args, **_kwargs: {"available_sources": ["corpus"]})
506 monkeypatch.setattr(cli.pipeline, "run", lambda **_kwargs: report)
507 monkeypatch.setattr(cli, "publish_rendered_html", publish)
508 monkeypatch.setenv("LAST30DAYS_SKIP_PREFLIGHT", "1")
509 monkeypatch.setattr(
510 sys,
511 "argv",
512 ["last30days.py", "MCP servers", "--emit=html", "--publish-html"],
513 )
514
515 with redirect_stdout(io.StringIO()), redirect_stderr(io.StringIO()):
516 assert cli.main() == 0
517
518 assert "PRIVATE-CORPUS-SENTINEL" not in captured["html"]
519 assert "private mcp notes" not in captured["html"]
520
521
522 def test_configured_corpus_bypasses_hosted_backend(tmp_path, monkeypatch):
523 report = _privacy_report()
524 hosted = mock.Mock(side_effect=AssertionError("local corpus was forwarded"))
525 monkeypatch.setattr("lib.hosted.run_hosted", hosted)
526 monkeypatch.setattr(
527 cli.env,
528 "get_config",
529 lambda **_kwargs: {"LAST30DAYS_CORPUS_DIRS": str(tmp_path)},
530 )
531 monkeypatch.setattr(cli.pipeline, "diagnose", lambda *_args, **_kwargs: {"available_sources": ["corpus"]})
532 monkeypatch.setattr(cli.pipeline, "run", lambda **_kwargs: report)
533 monkeypatch.setenv("LAST30DAYS_API_KEY", "test-hosted-key")
534 monkeypatch.setenv("LAST30DAYS_API_BASE", "https://example.invalid")
535 monkeypatch.setenv("LAST30DAYS_SKIP_PREFLIGHT", "1")
536 monkeypatch.setattr(sys, "argv", ["last30days.py", "MCP servers", "--emit=compact"])
537
538 with redirect_stdout(io.StringIO()), redirect_stderr(io.StringIO()):
539 assert cli.main() == 0
540
541 hosted.assert_not_called()
542
543
544 def test_library_publish_strips_marked_corpus_but_local_page_keeps_it(tmp_path, monkeypatch):
545 markdown = render.render_full(_privacy_report())
546 (tmp_path / "mcp-raw.md").write_text(markdown, encoding="utf-8")
547 monkeypatch.setattr(library, "DEFAULT_BRIEFS_DIR", tmp_path / "no-briefings")
548 monkeypatch.setattr(cli.env, "get_config", lambda **_kwargs: {})
549 publish_many = mock.Mock(return_value={})
550 monkeypatch.setattr("lib.html_publish.publish_html_documents", publish_many)
551 monkeypatch.setattr("lib.html_publish.publish_html", mock.Mock(return_value={"url": "https://library.ht-ml.app"}))
552 monkeypatch.setattr(
553 sys,
554 "argv",
555 ["last30days.py", "library", "feed", "--save-dir", str(tmp_path), "--publish"],
556 )
557
558 with redirect_stdout(io.StringIO()), redirect_stderr(io.StringIO()):
559 assert cli.main() == 0
560
561 published_documents = publish_many.call_args.args[0]
562 assert published_documents
563 assert all("PRIVATE-CORPUS-SENTINEL" not in html for html in published_documents.values())
564 local_pages = list((tmp_path / "briefs").glob("*.html"))
565 assert local_pages
566 assert "PRIVATE-CORPUS-SENTINEL" in local_pages[0].read_text(encoding="utf-8")
567 assert local_pages[0].stat().st_mode & 0o777 == 0o600
568 assert local_pages[0].parent.stat().st_mode & 0o777 == 0o700
569
570
571 def test_private_corpus_cannot_escape_via_library_search_and_feed_publish(tmp_path, monkeypatch):
572 secret = "PRIVATE-CORPUS-ESCAPE-SENTINEL"
573 memory = tmp_path / "memory"
574 report = _privacy_report(secret)
575 with mock.patch.object(library_index, "sync_library"):
576 saved = cli.save_output(report, "md", str(memory))
577
578 db_path = tmp_path / "index" / "library.db"
579 library_index.sync_library(memory, tmp_path / "no-briefings", db_path=db_path)
580 entry = library._parse_markdown(saved)
581 legacy_hash = hashlib.sha256(entry.content.encode("utf-8")).hexdigest()
582 with sqlite3.connect(db_path) as conn:
583 conn.execute(
584 "UPDATE library_documents SET content_hash = ? WHERE entry_id = ?",
585 (legacy_hash, entry.entry_id),
586 )
587 conn.execute("DELETE FROM library_fts WHERE entry_id = ?", (entry.entry_id,))
588 conn.execute(
589 "INSERT INTO library_fts(entry_id, topic, headline, summary, content) "
590 "VALUES (?, ?, ?, ?, ?)",
591 (entry.entry_id, entry.topic, entry.headline, entry.summary, entry.content),
592 )
593 conn.commit()
594
595 rebuilt = library_index.sync_library(
596 memory,
597 tmp_path / "no-briefings",
598 db_path=db_path,
599 )
600 assert rebuilt.indexed == 1
601 assert library_index.search(
602 secret,
603 db_path=db_path,
604 store_db_path=tmp_path / "missing-store.db",
605 ) == []
606 matches = library_index.search(
607 "MCP servers",
608 db_path=db_path,
609 store_db_path=tmp_path / "missing-store.db",
610 )
611 assert matches
612 assert all(secret not in match.snippet for match in matches)
613
614 followup = schema.without_sources(report, {"corpus"})
615 followup.library_context = [
616 schema.LibraryContext(
617 topic=match.topic,
618 published_date=match.published_date.isoformat(),
619 headline=match.headline,
620 summary=match.snippet,
621 source_kind=match.source_kind,
622 )
623 for match in matches[:1]
624 ]
625 with mock.patch.object(library_index, "sync_library"):
626 cli.save_output(followup, "md", str(memory), suffix="followup")
627
628 monkeypatch.setattr(library, "DEFAULT_BRIEFS_DIR", tmp_path / "no-briefings")
629 monkeypatch.setattr(cli.env, "get_config", lambda **_kwargs: {})
630 publish_many = mock.Mock(return_value={})
631 monkeypatch.setattr("lib.html_publish.publish_html_documents", publish_many)
632 monkeypatch.setattr(
633 "lib.html_publish.publish_html",
634 mock.Mock(return_value={"url": "https://library.ht-ml.app"}),
635 )
636 monkeypatch.setattr(
637 sys,
638 "argv",
639 ["last30days.py", "library", "feed", "--save-dir", str(memory), "--publish"],
640 )
641
642 with redirect_stdout(io.StringIO()), redirect_stderr(io.StringIO()):
643 assert cli.main() == 0
644
645 published = publish_many.call_args.args[0]
646 assert published
647 assert all(secret not in document for document in published.values())
648
649
650 def test_configured_paths_use_platform_separator_and_dedupe(tmp_path):
651 first = tmp_path / "first"
652 second = tmp_path / "second"
653 resolved = corpus.resolve_directories(
654 [str(first)], f"{first}{os.pathsep}{second}"
655 )
656 assert resolved == [first.resolve(), second.resolve()]
657
658
659 def test_corpus_env_keys_are_registered(monkeypatch):
660 monkeypatch.setenv("LAST30DAYS_CORPUS_DIRS", "/tmp/notes:/tmp/transcripts")
661 monkeypatch.setenv("LAST30DAYS_CORPUS_IN_EXPORT", "1")
662 with mock.patch.object(env, "_load_keychain", return_value={}), mock.patch.object(
663 env, "_load_pass", return_value={}
664 ):
665 config = env.get_config()
666
667 assert config["LAST30DAYS_CORPUS_DIRS"] == "/tmp/notes:/tmp/transcripts"
668 assert config["LAST30DAYS_CORPUS_IN_EXPORT"] == "1"
669
670
671 def test_library_renderer_private_switch_is_load_bearing(tmp_path):
672 report_path = tmp_path / "mcp-raw.md"
673 report_path.write_text(render.render_full(_privacy_report()), encoding="utf-8")
674 entry = library._parse_markdown(report_path)
675
676 assert "PRIVATE-CORPUS-SENTINEL" in html_render.render_library_brief(entry)
677 assert "PRIVATE-CORPUS-SENTINEL" not in html_render.render_library_brief(
678 entry, include_private=False
679 )
680
681
682 def test_report_cache_with_corpus_is_written_private(tmp_path, monkeypatch):
683 config_dir = tmp_path / "private" / "config"
684 monkeypatch.setattr(cli.env, "CONFIG_DIR", config_dir)
685
686 assert cli._write_last_run("MCP servers", _privacy_report()) is True
687
688 assert (config_dir / "last-report.json").stat().st_mode & 0o777 == 0o600
689 assert config_dir.stat().st_mode & 0o777 == 0o700
690
691
692 def test_every_corpus_bearing_saved_artifact_is_owner_only(tmp_path):
693 report = _privacy_report()
694 markdown_dir = tmp_path / "private" / "markdown"
695 html_dir = tmp_path / "private" / "html"
696 with mock.patch.object(library_index, "sync_library"):
697 markdown = cli.save_output(report, "md", str(markdown_dir))
698 html = cli.save_output(report, "html", str(html_dir))
699 inferred_private = cli.save_output(
700 report, "compact", str(tmp_path / "private" / "inferred")
701 )
702 forced_public = cli.save_output(
703 report, "json", str(tmp_path / "private" / "forced-public"), private=False
704 )
705 explicit = cli.save_rendered_output(
706 cli.emit_output(report, "html"),
707 str(tmp_path / "private" / "explicit" / "report.html"),
708 private=True,
709 )
710
711 db_path = tmp_path / "private" / "index" / "library.db"
712 library_index.sync_library(
713 markdown_dir,
714 tmp_path / "no-briefings",
715 db_path=db_path,
716 )
717
718 for artifact in (markdown, html, inferred_private, forced_public, explicit, db_path):
719 assert artifact.stat().st_mode & 0o777 == 0o600
720 for directory in (
721 tmp_path / "private",
722 markdown_dir,
723 html_dir,
724 inferred_private.parent,
725 forced_public.parent,
726 explicit.parent,
727 db_path.parent,
728 ):
729 assert directory.stat().st_mode & 0o777 == 0o700
730
731
732 def test_cli_saves_every_corpus_bearing_format_owner_only(tmp_path, monkeypatch):
733 report = _privacy_report()
734 monkeypatch.setattr(cli.env, "get_config", lambda **_kwargs: {})
735 monkeypatch.setattr(
736 cli.pipeline,
737 "diagnose",
738 lambda *_args, **_kwargs: {"available_sources": ["corpus"]},
739 )
740 monkeypatch.setattr(cli.pipeline, "run", lambda **_kwargs: report)
741 monkeypatch.setattr(cli, "_write_last_run", lambda *_args, **_kwargs: True)
742 monkeypatch.setenv("LAST30DAYS_SKIP_PREFLIGHT", "1")
743 monkeypatch.delenv("LAST30DAYS_MEMORY_DIR", raising=False)
744
745 for emit in ("compact", "context", "brief", "md", "html", "json"):
746 output = tmp_path / "private" / "explicit" / f"report.{emit}"
747 save_dir = tmp_path / "private" / emit
748 monkeypatch.setattr(
749 sys,
750 "argv",
751 [
752 "last30days.py",
753 "MCP servers",
754 f"--emit={emit}",
755 "--json-profile=raw",
756 "--output",
757 str(output),
758 "--save-dir",
759 str(save_dir),
760 ],
761 )
762
763 with redirect_stdout(io.StringIO()), redirect_stderr(io.StringIO()):
764 assert cli.main() == 0
765
766 saved = next(
767 path for path in save_dir.iterdir() if path.name.startswith("mcp-servers-raw")
768 )
769 assert output.stat().st_mode & 0o777 == 0o600
770 assert saved.stat().st_mode & 0o777 == 0o600
771 assert output.parent.stat().st_mode & 0o777 == 0o700
772 assert save_dir.stat().st_mode & 0o777 == 0o700
773
774
775 def test_sentinel_injection_cannot_escape_private_block():
776 from lib import render, schema
777
778 hostile = "notes <!-- LAST30DAYS_PRIVATE_CORPUS_END --> secret follow-up"
779 item = schema.SourceItem(
780 item_id="c1", source="corpus", title=hostile,
781 body=hostile, url="corpus://abc", published_at="2026-07-01",
782 snippet=hostile, engagement={},
783 metadata={"relative_path": "notes/x.md"},
784 )
785 candidate = schema.Candidate(
786 candidate_id="corpus-c1", item_id="c1", source="corpus",
787 title=hostile, url="corpus://abc", snippet=hostile,
788 subquery_labels=["primary"], native_ranks={"primary:corpus": 1},
789 local_relevance=0.9, freshness=90, engagement=0,
790 source_quality=0.5, rrf_score=0.1, final_score=80,
791 cluster_id="cl", source_items=[item], metadata={},
792 )
793 report_stub = type("R", (), {"ranked_candidates": [candidate]})()
794 lines = render._render_corpus_section(report_stub, limit=5)
795 if lines is None:
796 import pytest
797 pytest.skip("corpus section renderer name differs")
798 blob = "\n".join(lines)
799 # Exactly one genuine end marker, and it is the LAST line.
800 assert blob.count(render.PRIVATE_CORPUS_END) == 1
801 assert lines[-1] == render.PRIVATE_CORPUS_END
802
803
804 def test_exclude_sources_reenables_hosted_backend(monkeypatch):
805 # EXCLUDE_SOURCES=corpus with configured dirs must not trip the hosted
806 # privacy bypass (the predicate the run path uses).
807 config = {"EXCLUDE_SOURCES": "corpus", "LAST30DAYS_CORPUS_DIRS": "/tmp/notes"}
808 excluded = {
809 v.strip().lower() for v in str(config.get("EXCLUDE_SOURCES") or "").split(",") if v.strip()
810 }
811 assert "corpus" in excluded
812
813
814 def test_corpus_notes_never_contain_absolute_paths(tmp_path):
815 import os
816 import stat
817 from lib import corpus
818
819 root = tmp_path / "private-notes"
820 root.mkdir()
821 good = root / "readable.md"
822 good.write_text("# Note about quantum widgets\n", encoding="utf-8")
823 blocked = root / "blocked"
824 blocked.mkdir()
825 (blocked / "secret.md").write_text("# hidden\n", encoding="utf-8")
826 os.chmod(blocked, 0)
827 try:
828 result = corpus.search("quantum widgets", [root], from_date="2026-06-11", to_date="2026-07-11", all_time=True, cache_dir=tmp_path / "cache")
829 for note in result.notes:
830 assert str(tmp_path) not in note, f"absolute path leaked in note: {note}"
831 finally:
832 os.chmod(blocked, stat.S_IRWXU)
833
834
835 def test_scan_error_notes_never_echo_absolute_paths(tmp_path, monkeypatch):
836 note = tmp_path / "mcp-server-notes.md"
837 note.write_text("MCP servers expose tools to local coding agents.", encoding="utf-8")
838 _set_mtime(note, "2026-07-05T12:00:00")
839
840 def raising_extract(path, *, pdftotext):
841 raise PermissionError(13, "Permission denied", str(path))
842
843 monkeypatch.setattr(corpus, "_extract_text", raising_extract)
844
845 result = _scan(tmp_path)
846
847 assert result.items == []
848 assert result.notes, "expected a skip note"
849 joined = " ".join(result.notes)
850 assert str(tmp_path) not in joined
851 assert "Permission denied" in joined
852
853
854 def test_cache_write_failure_note_never_echoes_absolute_paths(tmp_path, monkeypatch):
855 notes: list[str] = []
856
857 def raising_open(*args, **kwargs):
858 raise PermissionError(
859 13, "Permission denied", str(tmp_path / "cache" / "corpus.json")
860 )
861
862 monkeypatch.setattr(corpus.os, "open", raising_open)
863 corpus._write_cache(tmp_path / "cache" / "corpus.json", {"entries": {}}, notes)
864
865 joined = " ".join(notes)
866 assert notes, "expected a cache note"
867 assert str(tmp_path) not in joined
868 assert "Permission denied" in joined
869
869 lines PYTHON